이것저것 기록

[python] 3D Mesh (.stl) 데이터 그리드(패치) 분할하기 본문

코린이/실무를 위한 코딩 기록

[python] 3D Mesh (.stl) 데이터 그리드(패치) 분할하기

anweh 2021. 7. 1. 01:40

사용한 코드 및 샘플 데이터는 깃헙을 참고해주세요!

https://github.com/henewsuh/3Dmesh_split

 

henewsuh/3Dmesh_split

3D STL 데이터를 그리드(패치) 분할하는 코드. Contribute to henewsuh/3Dmesh_split development by creating an account on GitHub.

github.com


 

0. Task 정의 

  • 입력데이터: .stl 형식의 3D Mesh 데이터 
  • 출력데이터: 입력데이터를 사용자가 정의한 n x m로 분할한 3D Mesh 데이터 
  • 이와 비슷한 task로는, 2D 이미지 학습 시 패치 단위 분할이 있다. 예를 들어, 1024*1024 크기의 이미지를 512*512 크기의 작은 이미지 4개로 분할하는 것과 비슷하다. 
  • 그래서 이 task가 왜 중요하냐면... 3D 데이터 모델링 시 데이터의 크기가 너무 커서 한 번에 모델링 할 수 없을 때, 3D 패치 or 그리드 분할이 필요하다. 

 

1. Task 프로세스 

  • 대상 mesh의 바운딩 박스를 구한다. 바운딩 박스에는 AABB, OBB 두 가지 종류가 있는데, 이 두 바운딩 박스의 차이점은 나중에 따로 포스팅을 하던가 할 계획이다. (바빠서 까먹을듯...) 
  • 바운딩 박스를 n x m 으로 분할 시 생성되는 주요 좌표값을 구한다. 
    • 예를 들어, x축으로 2등분 / y축으로 3등분한다면 -- x축 좌표값 2개, y축 좌표값 3개가 필요하다. 
  • 위에서 구한 좌표값을 이용해 작은 바운딩 박스 오브젝트를 생성한다. 
  • 위에서 생성한 작은 바운딩박스로 (스탬프 찍듯이) 대상 mesh 데이터를 split한다. 

 

 

2. 코드

2.1 라이브러리 

import pyvista as pv
import trimesh
import pandas as pd
import random
import itertools 
import os 

3D 데이터 시각화에 꼭 pyvista를 사용해야하는 것은 아니다.

사실 이것 저것 추가 정보와 함께 보려면 plotly를 추천한다.

하지만 이번은 단순히 어떻게 분할되는지 결과 확인 목적이기 때문에 비교적 실행속도가 빠른 pyvista를 사용해도 괜찮을 것 같다. 

 

 

2.2 데이터 로드 

root_path = os.getcwd() 
pv_mesh = pv.read('bunny.stl') # polydata
tri_mesh = trimesh.load('bunny.stl') # base.Trimesh

pyvista를 사용하여 시각화 + 데이터 splitting을 진행할 것이기 때문에 -- polydata 형태로 데이터를 로드해야 한다. 

그러나 바운딩 박스를 구하기 위해선 trimesh 라이브러리를 base.Trimesh 형태로 로드해야하는 귀찮은 일(?)이 발생한다. 

때문에 우선 두 가지 방법을 모두 사용하여 데이터를 불러오고, 바운딩 박스를 구한 후에 -- 이 바운딩 박스를 polydata 형태로 변환해줄 것이다. 

참고로, bunny.stl 파일은 깃헙에 코드와 함께 올려두었습니다~ 

 

 

2.3 전처리 

# AABB & OBB 생성
obb = tri_mesh.bounding_box_oriented 
aabb = tri_mesh.bounding_box 

# Bounds 추출 
obb_bounds = obb.bounds
aabb_bounds = aabb.bounds

# trimesh's bounds를 (xMin, xMax, yMin, yMax, zMin, zMax)로 변형 
def align_bounds(bounds):
    temp = []
    for i in range(len(bounds)):
        aa = bounds[i]
        for j in range(len(aa)):
            jj = aa[j]
            temp.append(jj)
    align_bound = [temp[0], temp[3], temp[1], temp[4], temp[2], temp[5]]
    return align_bound 

obb_bounds_a = align_bounds(obb_bounds)
aabb_bounds_a = align_bounds(aabb_bounds)

# pyvista Box 오브젝트 생성 
pv_obb_box = pv.Box(bounds=obb_bounds_a, level=0, quads=True)
pv_aabb_box = pv.Box(bounds=aabb_bounds_a, level=0, quads=True)

자, 보시면 base.Trimesh 오브젝트로 바운딩 박스를 구하고, 바운딩 박스의 최외곽 점 정보를 얻었다. (bounds) 

그리고 이 bounds 정보를 사용하여 -- pyvista의 Box 오브젝트를 생성할 수 있게 -- 데이터를 조금 조작해줬다. (def align_bounds)

 

 

2.4 바운딩 박스 시각화

p = pv.Plotter() # 캔버스 정의 
p.add_mesh(pv_mesh, opacity=0.75, color='red')
p.add_mesh(pv_obb_box, opacity=0.25, color='blue')
p.add_mesh(pv_aabb_box, opacity=0.25, color='green')
p.show()

바운딩 박스를 시각화해보자. 두 바운딩 박스의 차이를 직관적으로 확인할 수 있다. 

이렇게 여러 데이터를 동시에 겹쳐서 시각화할 땐 '투명도'를 적극 활용하는 것을 추천한다. 

 

 

2.5 XY축 분할 박스 생성을 위한 좌표값 구하기  

# 바운딩박스를 구성하는 (x, y) 좌표의 최소/최대값 계산
tp = aabb_bounds_a 
x_min, x_max, y_min, y_max, z_min, z_max = tp[0], tp[1], tp[2], tp[3], tp[4], tp[5]


# X축 분할 
x_dist = x_max - x_min # X축 변 길이 계산
n_x = 3 # 몇 등분으로 분할할 것인지 -- 변경 가능 
x_interval = x_dist / float(n_x)
x_min_ls = [x_min + (x_interval*i) for i in range(0, 3)] # x_min 좌표부터 n_x번 간격을 더해줌 
x_max_ls = [x_min + (x_interval*i) for i in range(1, 4)] # 윗줄과 맞물리게끔 n_x번 간격을 더해줌


# Z축 분할 
z_dist = z_max - z_min 
n_z = 2
z_interval = z_dist / float(n_z)

z_min_ls = [z_min + (z_interval*i) for i in range(0, 2)]
z_min_ls = [[i] * 3 for i in z_min_ls]
z_min_ls = list(itertools.chain(*z_min_ls))

z_max_ls = [z_min + (z_interval*i) for i in range(1, 3)]
z_max_ls = [[i] * 3 for i in z_max_ls]
z_max_ls = list(itertools.chain(*z_max_ls))

이 문단이 사실상 핵심이다. 

바운딩 박스의 bounds 좌표는 총 6개이다. x, y, z축으로 각각 최소, 최대값 2개씩. 

최대값에서 최소값을 빼면 바운딩 박스의 길이가 나온다. (x_dist, z_dist) 

만약 내가 x축을 3등분할 것이라면, x축 변을 3등분하는 '간격'을 구한다. (x_interval) 

그리고 이 간격을 x축의 최소값에서 3번씩 더한다. 이 프로세스를 z축에도 동일하게 적용한다. 

 

 

2.6 XY축 분할 박스 생성을 위한 데이터프레임 구축 

bounds_df = pd.DataFrame(data=[], columns=['xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax'])
for i in range(6):
    bounds_df.loc[i] = aabb_bounds_a 
bounds_df['xmin'] = x_min_ls * 2
bounds_df['xmax'] = x_max_ls * 2
bounds_df['zmin'] = z_min_ls
bounds_df['zmax'] = z_max_ls

작은 바운딩 박스를 만들때 (pv.Box 오브젝트) 총 여섯개의 좌표값을 순서대로 넣어줘야한다. (xmin, xmax, ymin, ymax, zmin, zmax) 

여러 개의 작은 바운딩 박스를 만들 것이기 때문에, 이렇게 데이터프레임으로 정리하면 편하다. 

 

 

2.7 분할 파트 시각화 및 STL파일로 export 

for i in range(6):
    globals()['box_{}'.format(i)] = list(bounds_df.iloc[i])
    exec('extract_%s = pv_mesh.clip_box(box_%s, invert=False)'%(i, i))

p = pv.Plotter() 
for i in range(6): 
    color = (random.random(), random.random(), random.random())
    exec("n_cell = extract_%s.n_cells" %(i))
    
    evalCode = 'n_cell != 0'
    if eval(evalCode): 
        exec("pv.save_meshio('extract_%s.stl', extract_%s)" %(i,i))
        exec("p.add_mesh(extract_%s, color=%s)" %(i,color))
        exec("py_box_%s = pv.Box(bounds=box_%s, level=0, quads=True)" %(i,i))
        exec("p.add_mesh(py_box_%s, opacity=0.1, show_edges=True)" %(i))
        
p.show()

위에서도 말했듯이, 바운딩 박스를 '여러 개' 만들어야한다. 그리고 이 바운딩 박스는 각각 개별 객체로 생성하고, export해야한다. 

때문에 redundant한 명령을 지양하고자 exec()을 사용했다. 

exec()은 반복적인 코드를 실행할 때 엄청 유용하다. 

시각화 실행 결과는 다음과 같다. 

각 3D 패치 파일 또한 제대로 export 된 것을 확인할 수 있다. 

 

Comments