ModelNet10 데이터셋은 ModelNet40 데이터셋의 일부로, 10개의 카테고리에 4,899개의 데이터가 존재하고 있으며, 이 중 80%에 해당하는 3,991개는 학습용, 나머지 908개의 데이터는 테스트용입니다.
CAD 데이터는 개체 파일 형식(OFF)으로 제공됩니다.
OFF 파일은 2D 및 3D 개체를 표현할 수 있는 Mesh를 저장하는 파일 포맷으로, Vertex(점)와 Face(면)로 개체의 삼각형 매쉬를 표현하며, 개체를 표현하는 가장 간단한 포맷입니다.
아래 코드의 주석을 제거한 뒤 다운로드 받거나, 위 사이트에서 직접 데이터 다운로드가 가능합니다.
# !wget http://3dvision.princeton.edu/projects/2014/3DShapeNets/ModelNet10.zip
# !unzip -q ModelNet10.zip;
데이터 셋은 아래와 같이 구성되어 있으며, 각각의 하위 카테고리에 Train과 Test 폴더가 구별되어 각각 OFF 파일이 구성되어 있습니다.
포인트 클라우드는 위 그림과 같이 어떤 좌표계에 속한 점들의 집합을 나타내는 것으로, 3차원 좌표계에서 X, Y, Z좌표로 정의되고 사물의 표현을 위해 사용됩니다.
본 자료는 OFF 파일 포맷으로 작성된 Mesh 파일을 포인트 클라우드로 파이썬에서 변한한 뒤 이를 시각적으로 확인하는 것을 목적으로 합니다.
이를 위하여 Interactive하게 자료 표현이 가능한 시각화 패키지 Plotly
를 활용하고자 합니다.
Plotly
는 Matplotlib 및 Seaborn에서 제공하는 시각화 자료들을 더욱 역동적으로 표현 가능합니다.
이는 특히, 자유롭게 축을 회전시키며 값을 확인할 수 있기 때문에 3D 데이터의 표현에서 큰 장점을 가집니다.
또한 Plotly
는 특히 Documentation이 잘 작성되어 API의 활용이나 예시들을 손쉽게 확인가능합니다. 자세한 사항은 https://plotly.com/python/ 링크를 통해 확인하실 수 있습니다.
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개의 카테고리로 데이터셋이 구성된 것을 확인할 수 있습니다.
path = './ModelNet10/'
folders = [content for content in sorted(os.listdir(path))]
classes = {folder: i for i, folder in enumerate(folders)}
classes
{'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(면)으로 이루어져 있어, 해당 파일에서부터 위 값을 리턴받습니다.
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()
에서 개체를 표현하는 핵심 기능들을 정의합니다.
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로 표시하여 전체 데이터를 포인트 클라우드로 나타내는 코드입니다.
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로 구성되어있는 것을 확인할 수 있습니다.
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)
13240
위 Mesh 데이터를 Plotly
로 시각화 한 결과입니다.
해당 프레임 내에서 자유롭게 화면 확대 및 축소, 드래그를 통한 시점 변경이 가능하며, Play를 누르면 회전하며 데이터가 변경되는 것을 확인할 수 있습니다.
visualize_rotate([go.Mesh3d(x=x, y=y, z=z, color='lightblue', opacity=0.35, i=i,j=j,k=k)]).show()
위 Mesh 데이터의 표현 방식을 단순히 marker로 변경하게 되는 경우, 포인트 클라우드로 표시할 수 있으나 그 형태를 확인하기 쉽지 않습니다.
visualize_rotate([go.Scatter3d(x=x, y=y, z=z, mode='markers', opacity=0.15)]).show()
따라서 plotly
내 update_traces
함수를 활용하여 포인트 클라우드로 표현하기 위한 함수를 pcshow
로 새롭게 정의하였고,
그 시각화 결과는 아래와 같이 깔끔한 포인트로 표현되는 것을 확인할 수 있습니다.
pcshow(x,y,z)
현재 샘플로 활용 중인 본 데이터의 경우 OFF 파일로부터 총 13,240의 vertex 및 face 정보가 저장되어 있습니다.
위 정보로부터 개별적인 포인트를 생성할 수 있으나, 카테고리마다, 혹은 같은 카테고리라 할지라도 개체의 크기나 형태에 따라 저장된 포인트의 수가 크게 차이가 날 수 있습니다.
이를 보정하기 위해, 전체 Mesh 정보로부터 Point를 샘플링을 수행하여 포인트 클라우드를 생성합니다.
이는 추후 인공지능 모델을 활용한 분류 문제 등에 활용할 때 연산량을 줄여주는 효과도 함께 가져올 수 있을 것입니다.
해당 작업을 수행하기 위해 PointSampler라는 클래스를 생성하였습니다.
위 클래에서는 vertex및 faces 정보로부터, 헤론의 공식(Heron's fomular)을 활용하여 vertex와 faces를 이루는 삼각형의 면적을 계산 후,
계산된 삼각형의 단면적을 가중치로 활용하여 삼각형의 무게중심에 Point cloud가 생성되도록 합니다.
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개의 포인트 클라우드를 생성 후 확인한 시각화 결과는 아래와 같습니다.
pointcloud = PointSampler(3000)((verts, faces))
pcshow(*pointcloud.T)
정형 데이터 셋을 활용하여 머신러닝 모델링을 진행할 때에도, 모델의 일반화 성능 향상이나, 과적합 방지 등 성능 개선을 위해 정규화,표준화 및 여러 샘플링 방법을 사용합니다.
이미지 데이터셋 같은 경우에도 모델의 성능 향상을 위해 여러 기법을 도입할 수 있습니다.
본 자료에서는 정규화 및 증강기법 적용시 포인트 클라우드가 어떻게 변환되는지 시각적으로 확인해보도록 하겠습니다.
먼저 Normalize 진행 시, 특정 지점에 많이 몰려있던 Point가 상대적으로 분산된 것을 확인할 수 있습니다.
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
norm_pointcloud = Normalize()(pointcloud)
pcshow(*norm_pointcloud.T)
이어서, 전체 포인트 클라우드에 랜덤한 회전을 추가하고, 거기에 추가적인 랜덤 노이즈를 발생시켜 보겠습니다.
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 데이터에서 의자의 형태는 유지하되 골고루 분산된 것을 확인할 수 있습니다.
rot_pointcloud = RandRotation_z()(norm_pointcloud)
noisy_rot_pointcloud = RandomNoise()(rot_pointcloud)
pcshow(*noisy_rot_pointcloud.T)
본 자료를 통해 3D OFF 데이터를 읽어들이고, 이를 어떻게 포인트 클라우드로 변환시키는지,
변환된 포인트 클라우드 데이터에서 어떻게 전처리를 수행하여 추가적인 모델링을 수행할 수 있는지에 관하여 알아보았습니다.
이상으로 3D 이미지 데이터의 Point Cloud 변환 전처리를 마무리하도록 하겠습니다.
감사합니다.