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
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기