포인트 클라우드 데이터와 관련된 포스트를 3월 중순에 올린적이 있었다. TileDB를 이용해서 lidar 데이터를 다루는 포스트였고, 오늘 올리는 포스트는 Open3D를 이용해서 포인트 클라우드를 다루는 내용이다.
2026.03.21 - [GIS/01. GIS TIL] - [GIS] Python에서 TileDB 사용하여 LiDAR 데이터 활용하기
[GIS] Python에서 TileDB 사용하여 LiDAR 데이터 활용하기
오랜만에 LiDAR 데이터 관련해서 공부한 기록을 남기게 되었다..너무나 감사하게도 우아한 형제들의 로보틱스lab 팀의 계약직으로 들어갈 수 있게 되었다. 입사하기 전에 최대한 공부하고 들어가
dalleeoppaa.tistory.com
1️⃣ Open3D란 무엇인가?
1. Open3D 개념
3D 데이터를 다루는 개발자라면 한 번쯤 Point Cloud 데이터를 다뤄본 적이 있거나 혹은 다뤄볼 예정일 것이다. point cloud 데이터는 수만개 ~ 수천만개의 포인트로 구성되었으며, 라이다 데이터의 경우에는 x,y,z 좌표와 intensity, color 정보까지 포함되었기 때문에 데이터를 전처리하고 시각화 하는데 효율적인 방법이 필요했다. 이러한 문제에 가장 강력한 해답으로 Open3D가 존재한다.
Open3D는 3D 데이터 처리를 위한 소프트웨어 개발을 돕는 오픈소스 라이브러리입니다. Intel에서 주도하여 개발되었으며, C++와 Python을 모두 지원하여 굳이 C++를 알지 못해도 누구나 쉽게 사용할 수 있다.

2. Open3D 핵심 기능
- 3D 데이터 시각화(Visualization)
- 수백만 개의 점으로 이루어진 데이터를 마우스로 돌려가며 확인하고, 색상을 입히거나 법선 벡터를 표시하는 작업을 단 몇 줄의 코드로 구현할 수 있습니다.
- 데이터 전처리(Preprocessing)
- Voxel Downsampling(데이터 경량화)이나 Cropping(영역 추출), 노이즈 제거 같은 필수적인 전처리 기능을 제공합니다.
- 정합 및 복원(Registration & Reconstruction)
- 서로 다른 각도에서 찍은 두 개의 3D 데이터를 하나로 합치는 작업(ICP 알고리즘 등)이나, 점 데이터를 면(Mesh)으로 만드는 기능을 지원합니다.
- 딥러닝 연동(Open3D-ML)
- 최신 PyTorch나 TensorFlow와 연동되어 3D 데이터를 활용한 딥러닝 모델을 학습시키고 테스트하기에 최적화되어 있습니다.
2️⃣ Open3D 활용하기
본격적인 활용에 들어가기에 앞서, 본 포스트는 아래 포스트를 참조하였습니다. 좋은 포스트를 작성해주신 Amnah Ebrahim 님 감사합니다.
Gentle Introduction to Point Cloud Registration using Open3D
This tutorial is in continuation to the following articles:
medium.com
이제 jupyter lab을 활용해서 Open3D를 본격적으로 사용해보겠습니다. 콘다 가상환경에서 작업을 진행하였으며, 사용한 환경에 대한 설명은 TileDB의 포스트와 콘다 환경 설정 포스트를 참고해주시면 됩니다.
2026.02.01 - [GIS/01. GIS TIL] - GIS 분석을 위한 환경설정(with conda, uv)
GIS 분석을 위한 환경설정(with conda, uv)
Geospatial 데이터를 다루는 유튜브에서 OpenSource Geospatial 전문가 유튜버의 영상을 자주 시청했다. 몇 달 전에 전자책을 발매하셨는데 이번에 한글판이 배포되어서 25달러를 지불하고 구매해봤다.파
dalleeoppaa.tistory.com
2026.03.21 - [GIS/01. GIS TIL] - [GIS] Python에서 TileDB 사용하여 LiDAR 데이터 활용하기
[GIS] Python에서 TileDB 사용하여 LiDAR 데이터 활용하기
오랜만에 LiDAR 데이터 관련해서 공부한 기록을 남기게 되었다..너무나 감사하게도 우아한 형제들의 로보틱스lab 팀의 계약직으로 들어갈 수 있게 되었다. 입사하기 전에 최대한 공부하고 들어가
dalleeoppaa.tistory.com
1. 작업 환경 설정
TileDB를 공부했을 때 생성했던 콘다 환경을 그대로 사용하였습니다. 먼저 open3d 라이브러리 설치 후 필요한 라이브러리를 불러오겠습니다.
# 새로운 환경 생성 후 라이브러리 설치
conda create -n lidar -c conda-forge python=3.12 pdal python-pdal tiledb tiledb-py geopandas pandas numpy wget pyarrow
# 주피터 환경 설정
conda install -c conda-forge notebook
# 커널 연결해주기
conda install ipykernel
python -m ipykernel install --user --name lidar --display-name "Python (lidar)"
# 주피터랩 혹은 주피터 노트북 접속
jupyter lab

여기서 추가로 `Open3D`를 설치해줘야 하기 때문에 콘다 환경을 실행 후 터미널에서 `pip install open3d`를 입력해주었습니다.
import numpy as np
import matplotlib.pyplot as plt
import open3d as o3d
이번 분석에서는 필수 라이브러리만 불러오겠습니다. `numpy`와 `matplotlib`, `open3d`만 불러오면 됩니다.
2. 데이터 불러오고 확인하기
2-1. PLY(Polygon) Format
샘플 데이터를 불러오기 위해 `PLYPointCloud()` 클래스를 사용하겠습니다. 해당 클래스를 사용하면 Red Wood Living Room 라는 대표 3D 포인트 클라우드 데이터를 불러올 수 있습니다.
https://www.open3d.org/docs/latest/python_api/open3d.data.PLYPointCloud.html
open3d.data.PLYPointCloud - Open3D primary (unknown) documentation
Previous open3d.data.PCDPointCloud
www.open3d.org
ply_point_cloud = o3d.data.PLYPointCloud()
ply = o3d.io.read_point_cloud(ply_point_cloud.path)
print(ply)
# 출력 결과
PointCloud with 196133 points.
불러온 196133개의 포인트는 numpy 배열로 변환해주면 된다.
# np.asarray를 사용하여 가능하면 복사하지 않고 배열 뷰를 반환
print(np.asarray(ply.points))
# 출력 결과
[[0.65234375 0.84686458 2.37890625]
[0.65234375 0.83984375 2.38430572]
[0.66737998 0.83984375 2.37890625]
...
[2.00839925 2.39453125 1.88671875]
[2.00390625 2.39488506 1.88671875]
[2.00390625 2.39453125 1.88793314]]
이제 단 한줄짜리 코드를 이용해서 포인트 데이터를 3차원 시각화 해보도록 하겠습니다.
# o3d.visualization.draw_plotly 함수를 사용
o3d.visualization.draw_plotly([ply],
zoom=0.3412,
front=[0.4257, -0.2125, -0.8795],
lookat=[2.6172, 2.0475, 1.532],
up=[-0.0694, -0.9768, 0.2024])

2-2. PCD Format
이번에는 다른 포맷을 활용해서 시각화를 진행해보도록 하겠습니다. PLY 포맷과의 차이점으로는 다음과 같습니다.
- 로봇공학, 자율주행에서 주로 사용되는 포맷
- 점군 데이터 처리에 최적화되어 PLY보다 헤더 정보가 상세해서 데이터 타입(float, double 등)을 아주 정밀하게 지정할 수 있고, 대용량 데이터를 처리하는 속도가 매우 빠름
https://www.open3d.org/docs/latest/python_api/open3d.data.PCDPointCloud.html
open3d.data.PCDPointCloud - Open3D primary (unknown) documentation
Previous open3d.data.OfficePointClouds
www.open3d.org
dataset = o3d.data.PCDPointCloud()
pcd = o3d.io.read_point_cloud(dataset.path)
print(pcd)
print("="*60)
print(np.asarray(pcd.points))
o3d.visualization.draw_plotly([pcd],
zoom=0.3412,
front=[0.4257, -0.2125, -0.8795],
lookat=[2.6172, 2.0475, 1.532],
up=[-0.0694, -0.9768, 0.2024])
# 출력 결과
[Open3D INFO] Downloading https://github.com/isl-org/open3d_downloads/releases/download/20220201-data/fragment.pcd
[Open3D INFO] Downloaded to /Users/dallee/open3d_data/download/PCDPointCloud/fragment.pcd
PointCloud with 113662 points.
============================================================
[[1.16796875 1.01803279 0.96484375]
[1.16845131 1.01953125 0.96484375]
[1.16796875 1.02158833 0.96484375]
...
[2.19495988 2.62890625 1.45703125]
[2.19140625 2.63435388 1.45703125]
[2.19140625 2.62890625 1.45781052]]

3. 데이터 전처리(Preprocessing)
불러온 point cloud 데이터는 다양한 전처리를 통해 활용할 목적에 맞게 변경할 수 있다. 먼저 포인트의 개수를 줄여주는 Downsampling에 대해서 알아보도록 하겠습니다.
1. Voxel Downsampling 기법
1-1. 원리
무거운 라이다 데이터는 경량화 작업을 통해 처리 속도를 줄일 수 있습니다. 그 중 Voxel 기법은 3차원 공간을 voxel_size 크기의 작은 정육면체(voxel)들로 나누게 됩니다. 그리고 각 정육면체 안에 들어있는 여러 개의 점을 하나의 대표점으로 합쳐버립니다.
1-2. 샘플 코드
print("Downsample the point cloud with a voxel of 0.02")
downpcd = pcd.voxel_down_sample(voxel_size=0.025)
print(f"RAW {pcd}\nDecrease {downpcd}")
o3d.visualization.draw_plotly([downpcd],
zoom=0.3412,
front=[0.4257, -0.2125, -0.8795],
lookat=[2.6172, 2.0475, 1.532],
up=[-0.0694, -0.9768, 0.2024])
# 출력 결과
Downsample the point cloud with a voxel of 0.02
RAW PointCloud with 113662 points.
Decrease PointCloud with 10154 points.

앞서 불러왔던 포인트 클라우드와 비교해보면 포인트의 개수가 큰 폭으로 줄었음을 확인할 수 있다.
2. 정점 법선 추정(Vertex normal estimation)
2-1. 법선이란?
법선이란 간단하게 말해서 포인트가 바라보는 방향이다. 수학적 의미로는 어떤 평면에 수직인 벡터를 말한다.

그런데 왜 추정(Estimation)이라는 단어를 사용하는지에 대한 의문이 들었다.
3D 모델(Mesh)은 면(Face)이 있기 때문에 수직 방향을 바로 알 수 있습니다. 하지만 Point Cloud는 그냥 점들의 모임일 뿐입니다. 면이 없으니 '수직'이라는 개념 자체가 물리적으로 존재하지 않습니다.
그래서 컴퓨터는 다음과 같은 방법으로 방향을 추측(Estimate)합니다.
- 이웃 찾기: 특정 점 주변에 있는 가까운 점들을 모읍니다.
- 가상 평면 만들기: 그 점들을 가장 잘 설명하는 가상의 평면을 하나 만듭니다. (이때 PCA 같은 수학적 기법이 쓰입니다.)
- 수직 벡터 계산: 그 가상 평면에 수직인 화살표를 계산해서 해당 점의 '법선'으로 임명합니다.
2-2. 샘플 코드
print("Recompute the normal of the downsampled point cloud")
downpcd.estimate_normals(
search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30)
)
print("Print a normal vector of the 0th point")
print(downpcd.normals[0])
간단하게 코드 해석을 해보면
downpcd.estimate_normals(
search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30)
)
위 코드는 각 점이 어느 방향을 바라보고 있는지(표면의 기울기)를 계산합니다.
- `radius=0.1`: 내 주변 0.1 거리 안에 있는 이웃 점들을 참고
- `max_nn=30`: 최대 30개의 점만 참고해서 계산
이제 결과도 해석을 해보면
# 출력 결과
Recompute the normal of the downsampled point cloud
Print a normal vector of the 0th point
[-0.89563968 0.01397949 -0.44456061]
0번째 점이 가진 법선 벡터(x, y, z)값을 출력해본 것입니다. [-0.002, 0.106, -0.994]는 거의 아래쪽(-z 방향)을 향하고 있는 표면임을 알 수 있습니다.
3. 포인트 클라우드 잘라내기(Crop)
포인트 클라우드는 특정 포인트 클라우드를 잘라낼 수 있다. 샘플 데이터로 사용되었던 Red Wood Living Room 포인트 클라우드 데이터셋에서 의자를 잘라내보겠습니다.
3-1. 의자 포인트 데이터 불러오기
open3d에서는 의자만 별도로 추출된 point cloud 데이터가 존재합니다. `o3d.data.DemoCropPointCloud()` 클래스를 사용해서 불러오도록 하겠습니다.
demo_crop_data = o3d.data.DemoCropPointCloud()
vol = o3d.visualization.read_selection_polygon_volume(demo_crop_data.cropped_json_path)
chair = vol.crop_point_cloud(ply)
o3d.visualization.draw_plotly([chair],
zoom=0.3412,
front=[0.4257, -0.2125, -0.8795],
lookat=[2.6172, 2.0475, 1.532],
up=[-0.0694, -0.9768, 0.2024])
# 출력 결과
[Open3D INFO] Downloading https://github.com/isl-org/open3d_downloads/releases/download/20220201-data/DemoCropPointCloud.zip
[Open3D INFO] Downloaded to /Users/dallee/open3d_data/download/DemoCropPointCloud/DemoCropPointCloud.zip
[Open3D INFO] Created directory /Users/dallee/open3d_data/extract/DemoCropPointCloud.
[Open3D INFO] Extracting /Users/dallee/open3d_data/download/DemoCropPointCloud/DemoCropPointCloud.zip.
[Open3D INFO] Extracted to /Users/dallee/open3d_data/extract/DemoCropPointCloud.

3-2. 의자를 잘라낸 후 시각화하기
불러온 의자 point cloud를 Red Wood Living Room 에서 제거해보도록 하겠습니다. 원리는 다음과 같습니다.
- 전체 점군(ply)에 있는 점 하나하나를 잡고, 잘라냈던 의자 모델(chair)에 있는 가장 가까운 점까지의 거리(Distance)를 계산합니다.
- 계산된 거리 데이터를 가지고 의자가 아닌 점들을 골라내는 기준을 세웁니다.
- 위에서 찾은 의자가 아닌 점들의 번호(인덱스)를 가지고 새로운 점군 데이터를 만듭니다.
# 전체 점군(ply)에서 하나씩 포인트를 잡고 의자 점군(chair)까지의 거리를 계산
distance = ply.compute_point_cloud_distance(chair)
# 계산된 거리를 배열 형태로 변환
dists = np.asarray(distance)
# 거리가 0.01 이상인 인덱스 번호 추출
ind = np.where(dists > 0.01)[0]
ply_without_chair = ply.select_by_index(ind)
o3d.visualization.draw_plotly([ply_without_chair],
zoom=0.3412,
front=[0.4257, -0.2125, -0.8795],
lookat=[2.6172, 2.0475, 1.532],
up=[-0.0694, -0.9768, 0.2024])

4. 블록 껍질(convex hull) 확인
4-1. 블록 껍질이란?
블록 껍질이란 말 그대로 물체를 감싸는 가장 최소한의 블록을 의미합니다.
convex는 concave의 반대말로 볼록하다는 뜻입니다(출처: https://m.blog.naver.com/dorergiverny/223155023979).
그렇다면 convex hull은 왜 중요한 것일까?
- 충돌 감지 (Collision Detection): 자율주행 로봇이 의자와 부딪힐지 판단할 때, 복잡한 의자 점군 전체를 계산하는 것보다 단순한 껍질(hull)과 부딪히는지 계산하는 것이 훨씬 빠르고 안전합니다.
- 물체 크기 측정: 물체의 전체적인 부피나 점유 공간을 대략적으로 파악할 때 유용합니다.
- 노이즈 제거 및 단순화: 복잡한 기하학적 구조를 단순한 다각형 형태로 추상화할 때 사용합니다.
4-2. 샘플 코드
크롭된 의자 데이터를 감싸는 convex hull을 찾아보도록 하겠습니다.
hull, _ = chair.compute_convex_hull()
hull_ls = o3d.geometry.LineSet.create_from_triangle_mesh(hull)
hull_ls.paint_uniform_color((0, 1, 1))
o3d.visualization.draw_plotly([chair, hull_ls])

위 코드들은 각각 다음과 같은 역할을 수행합니다.
- `hull, _ = chair.compute_convex_hull()`
- 작업: 의자 점군 데이터를 감싸는 최소 크기의 볼록 다면체(Convex Hull)를 계산합니다.
- 원리: 울퉁불퉁한 의자의 모든 점이 안쪽에 들어가도록 팽팽하게 겉면을 만드는 수학적 모델링입니다. 결과물인 hull은 삼각형 면들로 이루어진 Mesh 형태가 됩니다.
- `hull_ls = o3d.geometry.LineSet.create_from_triangle_mesh(hull)`
- 작업: 면으로 된 껍질(hull)을 선(Line)으로만 이루어진 구조로 변환합니다.
- 이유: 면으로 놔두면 안쪽에 있는 의자가 가려져서 안 보일 수 있기 때문에, 뼈대(Wireframe)만 남겨서 안쪽의 의자와 겉면의 껍질을 동시에 보기 위함입니다.
- `hull_ls.paint_uniform_color((0, 1, 1))`
- 작업: 생성된 선들에 색을 입힙니다. (0, 1, 1)은 RGB 값으로 청록색(Cyan)을 의미합니다.
- `o3d.visualization.draw_plotly([chair, hull_ls])`
- 작업: 원래의 의자(chair)와 방금 만든 청록색 껍질(hull_ls)을 한 화면에 겹쳐서 보여줍니다.
시각화를 진행할 때 chair point cloud를 제외하고 convex hull만 표현할 경우 다음과 같습니다.
o3d.visualization.draw_plotly([hull_ls])

5. 클러스터링
point cloud 데이터는 클러스터링이 가능하다. 밀도기반 클러스터링 기법인 DBscan을 사용하여 점들의 밀집도를 측정 후 비슷한 객체들끼리 클러스터를 형성하게 된다.
with o3d.utility.VerbosityContextManager(
o3d.utility.VerbosityLevel.Debug) as cm:
labels = np.array(
pcd.cluster_dbscan(eps=0.02, min_points=10, print_progress=True)
)
print(labels)
max_label = labels.max()
print(f"point cloud has {max_label + 1} clusters")
colors = plt.get_cmap("tab20")(labels / (max_label if max_label > 0 else 1))
colors[labels < 0] = 0
pcd.colors = o3d.utility.Vector3dVector(colors[:, :3])
o3d.visualization.draw_plotly([pcd],
zoom=0.455,
front=[-0.4999, -0.1659, -0.8499],
lookat=[2.1813, 2.0619, 2.0999],
up=[0.1204, -0.9852, 0.1215])
# 출력 결과
[Open3D DEBUG] Precompute neighbors.
Precompute neighbors.[======================> [Open3D DEBUG] Done Precompute neighbors.
[Open3D DEBUG] Compute Clusters
Precompute neighbors.[========================================] 100%
[Open3D DEBUG] Done Compute Clusters: 7===========>] 97%
[0 0 0 ... 0 0 0]
point cloud has 7 clusters

긴 글 읽어주셔서 감사합니다! 다음 글에서 Open3D에 대해서 더욱 자세히 다뤄보도록 하겠습니다.
