300x250

 

 

목차

     

     

    Trimesh, Open3d 등 다양한 point cloud 관련 라이브러리가  있는데, 그중에서 point_cloud_utils로 point cloud를 특정 파일로 저장하여 meshlab에서 시각화하는 방법을 알아보려 한다. 시각화 할 때 어떤 라이브러리가 가장 좋다는 건 딱히 없고, 직접 사용해보면서 각각의 장단점을 파악하고 상황에 맞게 사용하면 될 듯 하다. (사실 point_cloud_utils 라이브러리는 시각화가 아니라 point cloud를 다루는 다양한 기능을 제공해주는 라이브러리이다.)

     

    Fig 1. Point Cloud Utils

     

    본 포스팅에서 다룰 내용은 시각화에 사용할 간단한 함수들이고, github에 들어가보면 noise 생성, downsampling, mesh normal 계산, chamfer distance 계산 등 다양한 기능이 있으니 더 자세한 내용이 궁금하다면 깃허브를 참고하자.

    Point cloud utils가 제공하는 기능들은 아래와 같다.

     

     

     

    point_cloud_utils and MeshLab Installation

     

    point_cloud_utils는 터미널에서 다음 명령어로 설치한다.

     

    pip install point-cloud-utils
    conda install point_cloud_utils -c conda-forge # 아나콘다 가상환경 사용하는 경우

     

    그리고 MeshLab은 링크에서 다운받을 수 있다.

     

     

     

     

    Useful Functions and Classes

     

     

     

    Load 관련 함수

     

    Load 관련된 함수들은 다음과 같다. PLY, STL, OFF, OBJ, 3DS, VRML 2.0, X3D, COLLADA 등 다양한 포맷(확장자)의 mesh 파일을 읽어올 수 있다.

    # Load vertices and faces for a mesh
    v, f = pcu.load_mesh_vf("path/to/mesh")
    
    # Load vertices and per-vertex normals
    v, n = pcu.load_mesh_vn("path/to/mesh")
    
    # Load vertices, per-vertex normals, and per-vertex-colors
    v, n, c = pcu.load_mesh_vnc("path/to/mesh")
    
    # Load vertices, faces, and per-vertex normals
    v, f, n = pcu.load_mesh_vfn("path/to/mesh")
    
    # Load vertices, faces, per-vertex normals, and per-vertex colors
    v, f, n, c = pcu.load_mesh_vfnc("path/to/mesh")
    • v : vertices
    • f : faces
    • n : 각 vertice의 normal
    • c : 각 vertice의 color

     

     

     

    Save 관련 함수

     

    Save 관련된 함수들은 다음과 같다.

    v, f, n, c = pcu.load_mesh_vfnc("input_mesh.ply")
    
    # Save vertices
    pcu.save_mesh_v("path/to/mesh", v)
    
    # Save mesh vertices and faces
    pcu.save_mesh_vf("path/to/mesh", v, f)
    
    # Save mesh vertices and per-vertex normals
    v, n = pcu.save_mesh_vn("path/to/mesh", v, n)
    
    # Save mesh vertices, per-vertex normals, and per-vertex-colors
    v, n, c = pcu.save_mesh_vnc("path/to/mesh", v, n, c)
    
    # Save mesh vertices, faces, and per-vertex normals
    v, f, n = pcu.save_mesh_vfn("path/to/mesh", v, f, n)
    
    # Save vertices, faces, per-vertex normals, and per-vertex colors
    v, f, n, c = pcu.save_mesh_vfnc("path/to/mesh", v, f, n, c)

     

    아래에서 설명할 TriangleMesh object 자체를 저장하거나 좀 더 구체적인 vertex, face 관련 정보를 주어 원하는 경로에 저장할 수 있다.

    import point_cloud_utils as pcu
    
    pcu.save_triangle_mesh("[path to save mesh]", v=[position of vertices],
    						f=[face indices], vn=[vertex normals], fn=[face normals])
                            
    triangle_mesh_object.save("[path to save mesh]") # directly save pcu.TriangleMesh object

     

     

     

     

    TriangleMesh class

     

    TriangleMesh는 좀 더 복잡한 정보를 포함하는 mesh class이다.

    TriangleMesh 클래스로 인스턴스를 생성하게 되면 mesh의 vertices, faces와 관련된 다음과 같은 정보를 담을 수 있다. 모든 데이터는 numpy array이다. 없으면 안되는 정보는 required로 표시하였다.

    • vertex_data
      • positions [V, 3] : 점의 위치 (required)
      • normals [V, 3] : 각 점의 normal
      • texcoords [V, 2] : 각 점의 uv coordinate
      • tex_ids [V,] : 각 점이 해당하는 texture(Triangle.textures)에 대한 index
      • colors [V, 4] : 각 점의 RBGA color (값 범위 : 0.0 ~ 1.0)
      • radius [V,] : 각 점의 반지름
      • quality [V,] : 각 점의 quality measure
      • flags [V,] : 각 점의 32bit 정수 flag
    • face_data
      • vertex_ids [F, 3] : 각 점(TriangleMesh.vertex_data.positions)이 해당하는 face의 index (required)
      • normals [F, 3], colors [F, 4], quality [F,], flags [F,] : 각 face의 normal, RBGA color, quality measure, flag
      • wedge_colors [F, 3, 4], wedge_normals [F, 3, 3], wedge_texcoords [F, 3, 2], wedge_tex_ids [F, 3] : 각 wedge의 RBGA color, normal, uv coordinate, texture index
    • textures : mesh에 사용되는 image file paths (list) (required)

     

     

     

     

     

    Example with ShapeNet dataset

     

     

     

     

    Dataset Preparation

     

    ShapeNet이라는 3D pointcloud 데이터셋을 시각화해볼 것이다. 준비단계를 건너뛰고 바로 'visualization with point_cloud_utils'로 넘어가도 상관없다!

    먼저 dataset을 가져올 dataset.py를 작성한다.

    import os
    import random
    from copy import copy
    import torch
    from torch.utils.data import Dataset, DataLoader
    import numpy as np
    import h5py
    from tqdm.auto import tqdm
    
    class ShapeNetCore(Dataset):
    
        GRAVITATIONAL_AXIS = 1
        
        def __init__(self, path, cates, split, scale_mode, transform=None):
            super().__init__()
            assert isinstance(cates, list), '`cates` must be a list of cate names.'
            assert split in ('train', 'val', 'test')
            assert scale_mode is None or scale_mode in ('global_unit', 'shape_unit', 'shape_bbox', 'shape_half', 'shape_34')
            self.path = path
            if 'all' in cates:
                cates = cate_to_synsetid.keys()
            self.cate_synsetids = [cate_to_synsetid[s] for s in cates]
            self.cate_synsetids.sort()
            self.split = split
            self.scale_mode = scale_mode
            self.transform = transform
    
            self.pointclouds = []
            self.stats = None
    
            self.get_statistics()
            self.load()
    
        def get_statistics(self):
    
            basename = os.path.basename(self.path)
            dsetname = basename[:basename.rfind('.')]
            stats_dir = os.path.join(os.path.dirname(self.path), dsetname + '_stats')
            os.makedirs(stats_dir, exist_ok=True)
    
            if len(self.cate_synsetids) == len(cate_to_synsetid):
                stats_save_path = os.path.join(stats_dir, 'stats_all.pt')
            else:
                stats_save_path = os.path.join(stats_dir, 'stats_' + '_'.join(self.cate_synsetids) + '.pt')
            if os.path.exists(stats_save_path):
                self.stats = torch.load(stats_save_path)
                return self.stats
    
            with h5py.File(self.path, 'r') as f:
                pointclouds = []
                for synsetid in self.cate_synsetids:
                    for split in ('train', 'val', 'test'):
                        pointclouds.append(torch.from_numpy(f[synsetid][split][...]))
    
            all_points = torch.cat(pointclouds, dim=0) # (B, N, 3)
            B, N, _ = all_points.size()
            mean = all_points.view(B*N, -1).mean(dim=0) # (1, 3)
            std = all_points.view(-1).std(dim=0)        # (1, )
    
            self.stats = {'mean': mean, 'std': std}
            torch.save(self.stats, stats_save_path)
            return self.stats
    
        def load(self):
    
            def _enumerate_pointclouds(f):
                for synsetid in self.cate_synsetids:
                    cate_name = synsetid_to_cate[synsetid]
                    for j, pc in enumerate(f[synsetid][self.split]):
                        yield torch.from_numpy(pc), j, cate_name
            
            with h5py.File(self.path, mode='r') as f:
                for pc, pc_id, cate_name in _enumerate_pointclouds(f):
    
                    if self.scale_mode == 'global_unit':
                        shift = pc.mean(dim=0).reshape(1, 3)
                        scale = self.stats['std'].reshape(1, 1)
                    elif self.scale_mode == 'shape_unit':
                        shift = pc.mean(dim=0).reshape(1, 3)
                        scale = pc.flatten().std().reshape(1, 1)
                    elif self.scale_mode == 'shape_half':
                        shift = pc.mean(dim=0).reshape(1, 3)
                        scale = pc.flatten().std().reshape(1, 1) / (0.5)
                    elif self.scale_mode == 'shape_34':
                        shift = pc.mean(dim=0).reshape(1, 3)
                        scale = pc.flatten().std().reshape(1, 1) / (0.75)
                    elif self.scale_mode == 'shape_bbox':
                        pc_max, _ = pc.max(dim=0, keepdim=True) # (1, 3)
                        pc_min, _ = pc.min(dim=0, keepdim=True) # (1, 3)
                        shift = ((pc_min + pc_max) / 2).view(1, 3)
                        scale = (pc_max - pc_min).max().reshape(1, 1) / 2
                    else:
                        shift = torch.zeros([1, 3])
                        scale = torch.ones([1, 1])
    
                    pc = (pc - shift) / scale
    
                    self.pointclouds.append({
                        'pointcloud': pc,
                        'cate': cate_name,
                        'id': pc_id,
                        'shift': shift,
                        'scale': scale
                    })
    
            # Deterministically shuffle the dataset
            self.pointclouds.sort(key=lambda data: data['id'], reverse=False)
            random.Random(2020).shuffle(self.pointclouds)
    
        def __len__(self):
            return len(self.pointclouds)
    
        def __getitem__(self, idx):
            data = {k:v.clone() if isinstance(v, torch.Tensor) else copy(v) for k, v in self.pointclouds[idx].items()}
            if self.transform is not None:
                data = self.transform(data)
            return data

     

    위 dataset class를 사용하여 dataset instance를 생성해준 후, dataloader를 만들어준다.

    dataset_instance = ShapeNetCore(path=dataset_path, cates=categories,
                                    split='train', scale_mode='shape_unit', transform=None)
    
    train_loader = DataLoader(dataset=dataset_instance,
                              batch_size=8,
                              shuffle=False)
    
    data = next(iter(train_loader)) # dataset class의 __getitem__을 통해 가져온 data

     

    Data가 dictionary 형태로 저장되어 있는데, key를 출력해보면 다음과 같다.

     

    Fig 2. Keys of ShapeNet Data

     

    Batch size가 8이므로, point cloud를 제외한 각 데이터를 출력한 결과는 다음과 같다.

     

    Fig 3. Data Example

     

     

     

     

    Visualization with point_cloud_utils and MeshLab

     

    이제 본격적으로 point cloud를 시각화해보자. 다음과 같이 라이브러리를 import해준다.

    import point_cloud_utils as pcu

     

    아래 코드를 통해 dataloader로 불러온 point들을 vertices로 주어서 ply파일에 저장한다.

    import point_cloud_utils as pcu
    
    for i in range(len(data['pointcloud'])):
        pc = data['pointcloud'][i].detach().cpu().numpy()
        triangle_mesh_obj = pcu.TriangleMesh()
        triangle_mesh_obj.VertexData.positions = pc
        triangle_mesh_obj.save("/root/data_sj/DPMPC/ShapeNet_examples/{}.ply".format(i))

     

    batch size가 8이므로 다음과 같이 8개의 ply파일이 생성된다.

     

    Fig 4. Saved .ply files

     

    이 파일들을 MeshLab에서 열어보면 다음과 같이 point cloud를 시각화해볼 수 있다!

     

    Fig 5. Chair visualization example

     

    728x90
    • 네이버 블러그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기