목차
Trimesh, Open3d 등 다양한 point cloud 관련 라이브러리가 있는데, 그중에서 point_cloud_utils로 point cloud를 특정 파일로 저장하여 meshlab에서 시각화하는 방법을 알아보려 한다. 시각화 할 때 어떤 라이브러리가 가장 좋다는 건 딱히 없고, 직접 사용해보면서 각각의 장단점을 파악하고 상황에 맞게 사용하면 될 듯 하다. (사실 point_cloud_utils 라이브러리는 시각화가 아니라 point cloud를 다루는 다양한 기능을 제공해주는 라이브러리이다.)
본 포스팅에서 다룰 내용은 시각화에 사용할 간단한 함수들이고, github에 들어가보면 noise 생성, downsampling, mesh normal 계산, chamfer distance 계산 등 다양한 기능이 있으니 더 자세한 내용이 궁금하다면 깃허브를 참고하자.
Point cloud utils가 제공하는 기능들은 아래와 같다.
- Loading meshes and point clouds
- Saving meshes and point clouds
- Generating blue-noise samples on a mesh with Poisson-disk sampling
- Generate random samples on a mesh
- Downsample a point cloud to have a blue noise distribution
- Downsample a point cloud on a voxel grid
- Estimating normals from a point cloud
- Computing mesh normals per vertex
- Computing mesh normals per face
- Consistently orienting faces of a mesh
- Approximate Wasserstein (Sinkhorn) distance between two point clouds
- Chamfer distance between two point clouds
- Hausdorff distance between two point clouds
- K-nearest-neighbors between two point clouds
- Generating point samples in the square and cube with Lloyd relaxation
- Compute shortest signed distances to a triangle mesh with fast winding numbers
- Compute closest points on a mesh
- Deduplicating point clouds and meshes
- Removing unreferenced mesh verrtices
- Calculating face areas of a mesh
- Smoothing a mesh
- Computing connected componentes
- Decimating a triangle mesh
- Making a mesh watertight
- Ray/Mesh intersection
- Ray/Surfel intersection
- Computing curvature on a mesh
- Computing a consistent inside/outside for a triangle soup
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를 출력해보면 다음과 같다.
Batch size가 8이므로, point cloud를 제외한 각 데이터를 출력한 결과는 다음과 같다.
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파일이 생성된다.
이 파일들을 MeshLab에서 열어보면 다음과 같이 point cloud를 시각화해볼 수 있다!
최근댓글