문제 정의
3D 공간상의 곡면 점군(Point Cloud)과 각 점의 법선 벡터(Normal Vector)가 주어졌을 때, 해당 곡면을 2D 평면으로 전개(Unfolding 또는 Flattening)하여 근사적인 평면 좌표를 얻는 것이 목표입니다.
핵심 난제: 국소 좌표계 구축 및 일관성 유지
단일 점과 법선 벡터를 이용하면 해당 점에서의 국소 좌표계를 정의할 수 있습니다. 그러나 수백, 수천 개의 점으로 이루어진 점군에서는 각기 다른 국소 좌표계가 생성됩니다. 따라서 인접한 점들 간의 좌표계 연속성을 보장하기 위해 좌표계 일관성(Coordinate Consistency)을 맞추는 작업이 필수적입니다.
알고리즘 파이프라인
1. 입력 데이터 준비
구조광(Structured Light) 스캐닝이나 PCL(Point Cloud Library)의 법선 추정 기능을 통해 3D 점 좌표($P_i$)와 이에 대응하는 법선 벡터($n_i$)를 획득합니다.
2. 법선 벡터 방향 통일
법선 추정 알고리즘의 특성상 $n$과 $-n$의 방향이 일관되지 않게 산출될 수 있습니다. 이를 해결하기 위해 K-최근접 이웃(KNN) 그래프를 구축하고 방향을 전파합니다.
- 인접한 두 점의 법선 내적(Dot Product)이 0보다 작을 경우: $n_j = -n_j$로 반전시킵니다.
- 이 과정을 통해 전체 곡면의 법선 방향이 일관되도록 보장합니다.
3. 국소 좌표계 생성
각 점에 대해 직교 기저(Orthogonal Basis)를 형성합니다.
- $z_i = n_i$ (법선 방향)
- $x_i = \text{arbitrary vector} \perp z_i$
- $y_i = z_i \times x_i$
이를 통해 각 점에서의 회전 행렬 $R_i = [x_i, y_i, z_i]$를 얻습니다.
4. 국소 파라미터화 및 투영
3D 점을 2D 평면으로 매핑하기 위해 국소 투영을 수행합니다.
- $u_i = x_i \cdot (P_i - P_0)$
- $v_i = y_i \cdot (P_i - P_0)$
이 방식은 직관적이지만, 곡면의 곡률이 클 경우 오차가 누적되어 찌그러짐(Distortion)이 발생할 수 있습니다.
고급 기법: 전역 최적화 기반 전개
국소 투영의 한계를 극복하기 위해 전역 에너지 최소화 방식을 사용합니다.
E = Σ || (P_i - P_j) - R * (u_i - u_j, v_i - v_j) ||²
이 수식의 본질은 인접한 점들 사이의 국소 기하학적 관계를 최대한 유지하면서 2D 좌표를 찾는 것입니다.
주요 알고리즘
- LSCM (Least Squares Conformal Mapping): 각도 보존(Conformal) 특성을 가지며 자연스러운 전개 결과를 제공합니다.
- ARAP (As-Rigid-As-Possible): 국소적인 강체성(Rigidity)을 최대한 유지하려는 고급 최적화 기법입니다.
산업용 파이프라인 및 코드 구현
실제 산업 현장에서는 전역 PCA를 통해 대략적인 평면을 잡고, 국소 법선을 이용해 미세 보정하는 하이브리드 방식을 주로 사용합니다.
- 관심 영역(ROI) 추출
- PCA를 통한 주성분(Principal Directions) 도출
- 주평면을 전역 좌표계로 설정 및 1차 투영
- 국소 법선 벡터를 활용한 미세 조정(보상)
MATLAB 구현
% 파라메트릭 곡면 데이터 생성
[xGrid, yGrid] = meshgrid(linspace(-2, 2, 20), linspace(-2, 2, 20));
zGrid = 0.4 * xGrid.^2 + 0.3 * yGrid.^2;
points3D = [xGrid(:), yGrid(:), zGrid(:)];
% 중심점 계산 및 데이터 중앙 정렬
centroid = mean(points3D, 1);
centeredPoints = points3D - centroid;
% 공분산 행렬을 이용한 PCA 수행
covMatrix = (centeredPoints' * centeredPoints) / (size(centeredPoints, 1) - 1);
[eigVectors, eigValues] = eig(covMatrix);
[~, sortIdx] = sort(diag(eigValues), 'descend');
sortedVectors = eigVectors(:, sortIdx);
% 주성분 축 추출
axisU = sortedVectors(:, 1);
axisV = sortedVectors(:, 2);
% 2D 평면으로 투영 (전개)
uCoords = centeredPoints * axisU;
vCoords = centeredPoints * axisV;
% 결과 시각화
figure;
subplot(1,2,1);
scatter3(points3D(:,1), points3D(:,2), points3D(:,3), '.');
title('Original 3D Surface');
subplot(1,2,2);
scatter(uCoords, vCoords, '.');
title('Flattened 2D Plane');
axis equal;
PCL (C++) 구현
법선 벡터 추정
pcl::PointCloud<pcl::PointXYZ>::Ptr inputCloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile("sample.pcd", *inputCloud);
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> normalEstimator;
normalEstimator.setInputCloud(inputCloud);
pcl::search::KdTree<pcl::PointXYZ>::Ptr kdTree(new pcl::search::KdTree<pcl::PointXYZ>());
normalEstimator.setSearchMethod(kdTree);
pcl::PointCloud<pcl::Normal>::Ptr surfaceNormals(new pcl::PointCloud<pcl::Normal>);
normalEstimator.setRadiusSearch(0.05);
normalEstimator.compute(*surfaceNormals);
법선 방향 통일
Eigen::Vector3f viewpoint(0.0f, 0.0f, 1.0f);
for (size_t idx = 0; idx < surfaceNormals->size(); ++idx) {
Eigen::Vector3f currentNormal(surfaceNormals->points[idx].normal_x,
surfaceNormals->points[idx].normal_y,
surfaceNormals->points[idx].normal_z);
// 시점(Viewpoint)을 기준으로 법선 방향 보정
if (currentNormal.dot(viewpoint) < 0) {
currentNormal = -currentNormal;
}
surfaceNormals->points[idx].normal_x = currentNormal.x();
surfaceNormals->points[idx].normal_y = currentNormal.y();
surfaceNormals->points[idx].normal_z = currentNormal.z();
}
전역 PCA 및 2D 투영
Eigen::Vector4f cloudCentroid;
pcl::compute3DCentroid(*inputCloud, cloudCentroid);
Eigen::Matrix3f covarianceMatrix;
pcl::computeCovarianceMatrixNormalized(*inputCloud, cloudCentroid, covarianceMatrix);
Eigen::SelfAdjointEigenSolver<Eigen::Matrix3f> eigenSolver(covarianceMatrix);
Eigen::Matrix3f principalAxes = eigenSolver.eigenvectors();
// Eigen의 고유벡터는 기본적으로 오름차순으로 정렬됨
Eigen::Vector3f mainAxisU = principalAxes.col(2);
Eigen::Vector3f mainAxisV = principalAxes.col(1);
std::vector<Eigen::Vector2f> flattenedCoords;
flattenedCoords.reserve(inputCloud->size());
Eigen::Vector3f center3D = cloudCentroid.head<3>();
for (const auto& point : inputCloud->points) {
Eigen::Vector3f relativePos(point.x - center3D.x(),
point.y - center3D.y(),
point.z - center3D.z());
float projU = relativePos.dot(mainAxisU);
float projV = relativePos.dot(mainAxisV);
flattenedCoords.emplace_back(projU, projV);
}
국소 좌표계 보정 (곡률 보상)
전역 PCA만 사용할 경우 곡률이 큰 영역에서 늘어짐 현상이 발생합니다. 이를 방지하기 위해 국소 법선을 활용해 좌표계를 재정의합니다.
// 각 점에 대한 국소 좌표계 재구축
Eigen::Vector3f localZ = currentNormal;
Eigen::Vector3f localX = mainAxisU.cross(localZ).normalized();
Eigen::Vector3f localY = localZ.cross(localX);
// 국소 좌표계 기준 투영
float localU = relativePos.dot(localX);
float localV = relativePos.dot(localY);
핵심 개념 및 한계점
국소 vs 전역 접근법
법선 벡터 기반의 국소 접근법은 곡면의 미세한 형태를 정확히 반영하지만, 전역 PCA는 전체적인 구조의 안정성을 제공합니다. 두 방식의 적절한 융합이 필수적입니다.
전개의 수학적 본질
3D 곡면을 2D로 전개하는 과정은 본질적으로 저왜곡(Low-distortion) 매핑 함수를 찾는 최적화 문제입니다.
왜곡의 불가피성
원기둥이나 원뿔과 같은 가전면(Developable Surface)이 아닌 이상, 3D 곡면을 2D 평면으로 완벽하게 펼치는 것은 수학적으로 불가능하며 필연적으로 면적이나 각도의 왜곡이 발생합니다.