3D 이미지 데이터의 Point Cloud 변환 전처리¶



  • 활용 데이터:
    ModelNet10 데이터셋 (https://modelnet.cs.princeton.edu/),



1. 데이터셋 및 개요¶

1) ModelNet10 데이터셋 및 OFF 파일¶

ModelNet10 데이터셋은 ModelNet40 데이터셋의 일부로, 10개의 카테고리에 4,899개의 데이터가 존재하고 있으며, 이 중 80%에 해당하는 3,991개는 학습용, 나머지 908개의 데이터는 테스트용입니다.
CAD 데이터는 개체 파일 형식(OFF)으로 제공됩니다.

OFF 파일은 2D 및 3D 개체를 표현할 수 있는 Mesh를 저장하는 파일 포맷으로, Vertex(점)와 Face(면)로 개체의 삼각형 매쉬를 표현하며, 개체를 표현하는 가장 간단한 포맷입니다.


2) 데이터 다운로드¶

아래 코드의 주석을 제거한 뒤 다운로드 받거나, 위 사이트에서 직접 데이터 다운로드가 가능합니다.

In [1]:
# !wget http://3dvision.princeton.edu/projects/2014/3DShapeNets/ModelNet10.zip
In [2]:
# !unzip -q ModelNet10.zip;



3) 데이터 셋 구성¶

데이터 셋은 아래와 같이 구성되어 있으며, 각각의 하위 카테고리에 Train과 Test 폴더가 구별되어 각각 OFF 파일이 구성되어 있습니다.

  • bathtub(욕조)
  • bed(침대)
  • chair(의자)
  • desk(책상)
  • dresser(장롱)
  • monitor(모니터)
  • night_stand(침대스탠드)
  • sofa(소파)
  • table(테이블)
  • toilet(변기)

4) 포인트 클라우드¶


포인트 클라우드는 위 그림과 같이 어떤 좌표계에 속한 점들의 집합을 나타내는 것으로, 3차원 좌표계에서 X, Y, Z좌표로 정의되고 사물의 표현을 위해 사용됩니다.

5) Plotly¶

본 자료는 OFF 파일 포맷으로 작성된 Mesh 파일을 포인트 클라우드로 파이썬에서 변한한 뒤 이를 시각적으로 확인하는 것을 목적으로 합니다.

이를 위하여 Interactive하게 자료 표현이 가능한 시각화 패키지 Plotly를 활용하고자 합니다.
Plotly는 Matplotlib 및 Seaborn에서 제공하는 시각화 자료들을 더욱 역동적으로 표현 가능합니다.

이는 특히, 자유롭게 축을 회전시키며 값을 확인할 수 있기 때문에 3D 데이터의 표현에서 큰 장점을 가집니다.

또한 Plotly는 특히 Documentation이 잘 작성되어 API의 활용이나 예시들을 손쉽게 확인가능합니다. 자세한 사항은 https://plotly.com/python/ 링크를 통해 확인하실 수 있습니다.


2. 패키지 로드 및 코드 작성¶

In [3]:
import os
import numpy as np
import itertools
import math, random
random.seed = 0

import scipy.spatial.distance
import plotly.graph_objects as go
import plotly.express as px
import matplotlib.pyplot as plt

다운로드 완료된 ModelNet10 데이터셋의 경로를 지정하고 확인해보면, 총 10개의 카테고리로 데이터셋이 구성된 것을 확인할 수 있습니다.

In [4]:
path = './ModelNet10/'

folders = [content for content in sorted(os.listdir(path))]
classes = {folder: i for i, folder in enumerate(folders)}
classes
Out[4]:
{'bathtub': 0,
 'bed': 1,
 'chair': 2,
 'desk': 3,
 'dresser': 4,
 'monitor': 5,
 'night_stand': 6,
 'sofa': 7,
 'table': 8,
 'toilet': 9}



아래 코드는 OFF 파일을 읽는 코드입니다. OFF파일은 Vertex(점)와 Face(면)으로 이루어져 있어, 해당 파일에서부터 위 값을 리턴받습니다.

In [5]:
def read_off(file):
    if 'OFF' != file.readline().strip():
        raise('Not a valid OFF header')
    n_verts, n_faces, __ = tuple([int(s) for s in file.readline().strip().split(' ')])
    verts = [[float(s) for s in file.readline().strip().split(' ')] for i_vert in range(n_verts)]
    faces = [[int(s) for s in file.readline().strip().split(' ')][1:] for i_face in range(n_faces)]
    return verts, faces



아래 코드는 추출된 x, y, z 좌표로 부터 해당 개체를 Plotly로 표현하는 코드입니다.
이 중, layout 파라미터 부분의 Go.Layout()에서 개체를 표현하는 핵심 기능들을 정의합니다.

In [6]:
def visualize_rotate(data):
    x_eye, y_eye, z_eye = 1.25, 1.25, 0.8
    frames=[]

    def rotate_z(x, y, z, theta):
        w = x+1j*y
        return np.real(np.exp(1j*theta)*w), np.imag(np.exp(1j*theta)*w), z

    for t in np.arange(0, 10.26, 0.1):
        xe, ye, ze = rotate_z(x_eye, y_eye, z_eye, -t)
        frames.append(dict(layout=dict(scene=dict(camera=dict(eye=dict(x=xe, y=ye, z=ze))))))
    fig = go.Figure(data=data,
        layout=go.Layout(
            height=500,
            width=750,
            updatemenus=[dict(type='buttons',
                showactive=False,
                y=1,
                x=0.8,
                xanchor='left',
                yanchor='bottom',
                pad=dict(t=45, r=10),
                buttons=[dict(label='Play',
                    method='animate',
                    args=[None, dict(frame=dict(duration=50, redraw=True),
                        transition=dict(duration=0),
                        fromcurrent=True,
                        mode='immediate'
                        )]
                    )
                ])]
        ),
        frames=frames
    )

    return fig



아래 코드는 OFF 파일에서서 포인트 클라우드 변환 전처리 작업 전 OFF 파일 내의 점과 면의 정보를 가지고 marker로 표시하여 전체 데이터를 포인트 클라우드로 나타내는 코드입니다.

In [7]:
def pcshow(xs,ys,zs):
    data=[go.Scatter3d(x=xs, y=ys, z=zs, mode='markers')]
    fig = visualize_rotate(data)
    fig.update_traces(marker=dict(size=2,
                      line=dict(width=2,
                      color='DarkSlateGrey')),
                      selector=dict(mode='markers'))
    fig.show()



본격적으로 데이터를 데이터를 읽고, 표시해보도록 하겠습니다.
sofa 카테고리의 4번째 학습 데이터의 경우, 13,240개의 array로 구성되어있는 것을 확인할 수 있습니다.

In [8]:
with open(path+"sofa/train/sofa_0004.off", 'r') as f:
    verts, faces = read_off(f)
    
i,j,k = np.array(faces).T
x,y,z = np.array(verts).T
len(x)
Out[8]:
13240



위 Mesh 데이터를 Plotly로 시각화 한 결과입니다.
해당 프레임 내에서 자유롭게 화면 확대 및 축소, 드래그를 통한 시점 변경이 가능하며, Play를 누르면 회전하며 데이터가 변경되는 것을 확인할 수 있습니다.

In [9]:
visualize_rotate([go.Mesh3d(x=x, y=y, z=z, color='lightblue', opacity=0.35, i=i,j=j,k=k)]).show()



위 Mesh 데이터의 표현 방식을 단순히 marker로 변경하게 되는 경우, 포인트 클라우드로 표시할 수 있으나 그 형태를 확인하기 쉽지 않습니다.

In [10]:
visualize_rotate([go.Scatter3d(x=x, y=y, z=z, mode='markers', opacity=0.15)]).show()



따라서 plotly 내 update_traces 함수를 활용하여 포인트 클라우드로 표현하기 위한 함수를 pcshow로 새롭게 정의하였고,
그 시각화 결과는 아래와 같이 깔끔한 포인트로 표현되는 것을 확인할 수 있습니다.

In [11]:
pcshow(x,y,z)




3. 포인트 클라우드 생성¶

현재 샘플로 활용 중인 본 데이터의 경우 OFF 파일로부터 총 13,240의 vertex 및 face 정보가 저장되어 있습니다.
위 정보로부터 개별적인 포인트를 생성할 수 있으나, 카테고리마다, 혹은 같은 카테고리라 할지라도 개체의 크기나 형태에 따라 저장된 포인트의 수가 크게 차이가 날 수 있습니다.

이를 보정하기 위해, 전체 Mesh 정보로부터 Point를 샘플링을 수행하여 포인트 클라우드를 생성합니다.
이는 추후 인공지능 모델을 활용한 분류 문제 등에 활용할 때 연산량을 줄여주는 효과도 함께 가져올 수 있을 것입니다.

해당 작업을 수행하기 위해 PointSampler라는 클래스를 생성하였습니다.
위 클래에서는 vertex및 faces 정보로부터, 헤론의 공식(Heron's fomular)을 활용하여 vertex와 faces를 이루는 삼각형의 면적을 계산 후,
계산된 삼각형의 단면적을 가중치로 활용하여 삼각형의 무게중심에 Point cloud가 생성되도록 합니다.

In [12]:
class PointSampler(object):
    def __init__(self, output_size):
        assert isinstance(output_size, int)
        self.output_size = output_size
    
    def triangle_area(self, pt1, pt2, pt3):
        side_a = np.linalg.norm(pt1 - pt2)
        side_b = np.linalg.norm(pt2 - pt3)
        side_c = np.linalg.norm(pt3 - pt1)
        s = 0.5 * ( side_a + side_b + side_c)
        return max(s * (s - side_a) * (s - side_b) * (s - side_c), 0)**0.5

    def sample_point(self, pt1, pt2, pt3):
        # barycentric coordinates on a triangle
        # https://mathworld.wolfram.com/BarycentricCoordinates.html
        s, t = sorted([random.random(), random.random()])
        f = lambda i: s * pt1[i] + (t-s)*pt2[i] + (1-t)*pt3[i]
        return (f(0), f(1), f(2))
        
    
    def __call__(self, mesh):
        verts, faces = mesh
        verts = np.array(verts)
        areas = np.zeros((len(faces)))

        for i in range(len(areas)):
            areas[i] = (self.triangle_area(verts[faces[i][0]],
                                           verts[faces[i][1]],
                                           verts[faces[i][2]]))
            
        sampled_faces = (random.choices(faces, 
                                      weights=areas,
                                      cum_weights=None,
                                      k=self.output_size))
        
        sampled_points = np.zeros((self.output_size, 3))

        for i in range(len(sampled_faces)):
            sampled_points[i] = (self.sample_point(verts[sampled_faces[i][0]],
                                                   verts[sampled_faces[i][1]],
                                                   verts[sampled_faces[i][2]]))
        
        return sampled_points



작성된 클래스를 활용하여, 임의의 3,000개의 포인트 클라우드를 생성 후 확인한 시각화 결과는 아래와 같습니다.

In [13]:
pointcloud = PointSampler(3000)((verts, faces))
pcshow(*pointcloud.T)




4. 데이터 정규화(Normalize) 및 증강(Augmentation)¶

정형 데이터 셋을 활용하여 머신러닝 모델링을 진행할 때에도, 모델의 일반화 성능 향상이나, 과적합 방지 등 성능 개선을 위해 정규화,표준화 및 여러 샘플링 방법을 사용합니다.
이미지 데이터셋 같은 경우에도 모델의 성능 향상을 위해 여러 기법을 도입할 수 있습니다.

본 자료에서는 정규화 및 증강기법 적용시 포인트 클라우드가 어떻게 변환되는지 시각적으로 확인해보도록 하겠습니다.

먼저 Normalize 진행 시, 특정 지점에 많이 몰려있던 Point가 상대적으로 분산된 것을 확인할 수 있습니다.

In [14]:
class Normalize(object):
    def __call__(self, pointcloud):
        assert len(pointcloud.shape)==2
        
        norm_pointcloud = pointcloud - np.mean(pointcloud, axis=0) 
        norm_pointcloud /= np.max(np.linalg.norm(norm_pointcloud, axis=1))

        return  norm_pointcloud
In [15]:
norm_pointcloud = Normalize()(pointcloud)
pcshow(*norm_pointcloud.T)



이어서, 전체 포인트 클라우드에 랜덤한 회전을 추가하고, 거기에 추가적인 랜덤 노이즈를 발생시켜 보겠습니다.

In [16]:
class RandRotation_z(object):
    def __call__(self, pointcloud):
        assert len(pointcloud.shape)==2

        theta = random.random() * 2. * math.pi
        rot_matrix = np.array([[ math.cos(theta), -math.sin(theta),    0],
                               [ math.sin(theta),  math.cos(theta),    0],
                               [0,                             0,      1]])
        
        rot_pointcloud = rot_matrix.dot(pointcloud.T).T
        return  rot_pointcloud
    
class RandomNoise(object):
    def __call__(self, pointcloud):
        assert len(pointcloud.shape)==2

        noise = np.random.normal(0, 0.02, (pointcloud.shape))
    
        noisy_pointcloud = pointcloud + noise
        return  noisy_pointcloud



위 코드의 순서대로 Normalize를 수행한 데이터에 대하여 추가 전처리를 수행한 포인트 클라우드는 초기 Mesh 데이터에서 의자의 형태는 유지하되 골고루 분산된 것을 확인할 수 있습니다.

In [17]:
rot_pointcloud = RandRotation_z()(norm_pointcloud)
noisy_rot_pointcloud = RandomNoise()(rot_pointcloud)
pcshow(*noisy_rot_pointcloud.T)



본 자료를 통해 3D OFF 데이터를 읽어들이고, 이를 어떻게 포인트 클라우드로 변환시키는지,
변환된 포인트 클라우드 데이터에서 어떻게 전처리를 수행하여 추가적인 모델링을 수행할 수 있는지에 관하여 알아보았습니다.

이상으로 3D 이미지 데이터의 Point Cloud 변환 전처리를 마무리하도록 하겠습니다.

감사합니다.

참고문헌¶

  • The Trustees of Princeton University. (n.d.). Princeton ModelNet. Princeton University. Retrieved September 19, 2022, from https://modelnet.cs.princeton.edu/
  • Wikimedia Foundation. (2020, November 20). Off (file format). Wikipedia. Retrieved September 19, 2022, from https://en.wikipedia.org/wiki/OFF_(file_format)
  • Plotly. Plotly Python Graphing Library. (n.d.). Retrieved September 19, 2022, from https://plotly.com/python/
  • Charles, R. Q., Su, H., Kaichun, M., & Guibas, L. J. (2017). PointNet: Deep Learning on point sets for 3D classification and segmentation. 2017 IEEE Conference on Computer Vision and Pattern Recognition (CVPR). https://doi.org/10.1109/cvpr.2017.16
In [ ]: