3D λΉμ κΈ°μ΄ (3D Vision Basics)
3D λΉμ κΈ°μ΄ (3D Vision Basics)¶
κ°μ¶
3D λΉμ μ 2D μ΄λ―Έμ§λ‘λΆν° 3μ°¨μ μ 보λ₯Ό μΆμΆνκ³ λ³΅μνλ κΈ°μ μ λλ€. μ€ν λ μ€ λΉμ , κΉμ΄ λ§΅, ν¬μΈνΈ ν΄λΌμ°λ μ²λ¦¬, 3D μ¬κ΅¬μ±μ κΈ°μ΄λ₯Ό λ€λ£Ήλλ€.
λμ΄λ: ββββ
μ μ μ§μ: μΉ΄λ©λΌ μΊλ¦¬λΈλ μ΄μ , νΉμ§μ κ²μΆ/λ§€μΉ, μ νλμ
λͺ©μ°¨¶
- 3D λΉμ κ°μ
- μ€ν λ μ€ λΉμ μ리
- κΉμ΄ λ§΅ μμ±
- ν¬μΈνΈ ν΄λΌμ°λ
- Open3D κΈ°μ΄
- 3D μ¬κ΅¬μ±
- μ°μ΅ λ¬Έμ
1. 3D λΉμ κ°μ¶
3D λΉμ μ λͺ©ν¶
3D λΉμ νμ΄νλΌμΈ:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β 2D μ΄λ―Έμ§ ββββββΆ κΉμ΄ μΆμ ββββββΆ 3D μ¬κ΅¬μ± β
β β β
β β βββββββββββββββ β
β ββββββββββββΆβ κΉμ΄ μ 보 ββββββββΆ ν¬μΈνΈ ν΄λΌμ°λ β
β βββββββββββββββ β β
β β β
β βΌ β
β βββββββββββββββ β
β β 3D λ©μ¬ β β
β β 3D λͺ¨λΈ β β
β βββββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
κΉμ΄ μΆμΆ λ°©λ²:
βββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ
β λ°©λ² β μ€λͺ
β
βββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ€
β μ€ν
λ μ€ λΉμ β λ μΉ΄λ©λΌμ μμ°¨λ‘ κΉμ΄ κ³μ° β
β κ΅¬μ‘°κ΄ (Structured) β μλ €μ§ ν¨ν΄μ ν¬μ¬νμ¬ κΉμ΄ μΈ‘μ β
β ToF (Time-of-Flight)β λΉμ λΉν μκ°μΌλ‘ 거리 μΈ‘μ β
β λ¨μ κΉμ΄ μΆμ β λ¨μΌ μΉ΄λ©λΌ + λ₯λ¬λμΌλ‘ κΉμ΄ μμΈ‘ β
β LiDAR β λ μ΄μ μ€μΊλμΌλ‘ μ λ° κΉμ΄ μΈ‘μ β
βββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ
μ’νκ³ μ΄ν΄¶
μΉ΄λ©λΌ μ’νκ³:
Y (μ)
β
β
β
β_________ X (μ€λ₯Έμͺ½)
/
/
Z (μΉ΄λ©λΌ μ λ©΄ λ°©ν₯)
μλ μ’νκ³ β μΉ΄λ©λΌ μ’νκ³ λ³ν:
P_cam = R * P_world + t
μ΄λ―Έμ§ μ’νκ³:
βββββββββββββββββββββββΆ u (κ°λ‘, ν½μ
)
β
β β (cx, cy) μ£Όμ
β
βΌ
v (μΈλ‘, ν½μ
)
3D β 2D ν¬μ:
u = fx * (X/Z) + cx
v = fy * (Y/Z) + cy
2. μ€ν λ μ€ λΉμ μ리¶
μνΌν΄λΌ κΈ°νν¶
μνΌν΄λΌ κΈ°νν (Epipolar Geometry):
μνΌν΄ (e)
β
ββββββββββββΌβββββββββββ
β β β
β βββββββΌβββββββββββΌββββββ μνΌν΄λΌ μ
β P β β P'
β β β
ββββββββββββ΄βββββββββββ
μΌμͺ½ μ€λ₯Έμͺ½
μ΄λ―Έμ§ μ΄λ―Έμ§
3D μ Pκ° μΌμͺ½ μ΄λ―Έμ§μ μ pμ ν¬μλλ©΄,
μ€λ₯Έμͺ½ μ΄λ―Έμ§μμλ μνΌν΄λΌ μ μ μ΄λκ°μ p'λ‘ ν¬μλ¨.
ν΅μ¬ νλ ¬λ€:
βββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β νλ ¬ β μ€λͺ
β
βββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ€
β Essential Matrix β μ κ·νλ μ’νκ³μμ κΈ°ννμ κ΄κ³ β
β (E) β E = [t]x * R β
βββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ€
β Fundamental Matrixβ ν½μ
μ’νκ³μμ κΈ°ννμ κ΄κ³ β
β (F) β F = K'^(-T) * E * K^(-1) β
β β p'^T * F * p = 0 β
βββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββ
μμ°¨μ κΉμ΄¶
μ€ν
λ μ€ μμ°¨ (Disparity):
μΌμͺ½ μΉ΄λ©λΌ μ€λ₯Έμͺ½ μΉ΄λ©λΌ
C_L βββββββββββββββ C_R
β β
β b (λ² μ΄μ€λΌμΈ) β
β βββββββββββββββΊ β
β β
β β
βΌ βΌ
p_L d p_R
βββββββββββββββββββββββ
β β
β μμ°¨ (d) β
β d = x_L - x_R β
κΉμ΄ κ³μ°:
Z = (f * b) / d
μ¬κΈ°μ:
- Z: κΉμ΄ (μΉ΄λ©λΌλ‘λΆν°μ 거리)
- f: μ΄μ 거리
- b: λ² μ΄μ€λΌμΈ (λ μΉ΄λ©λΌ μ¬μ΄ 거리)
- d: μμ°¨ (ν½μ
λ¨μ)
μμ°¨ λ²μ μμ:
βββββββββββββββββββββββββββββββββββββββββββ
β 거리 β μμ°¨ (f=500, b=0.1m) β
βββββββββββΌββββββββββββββββββββββββββββββββ€
β 1m β 50 ν½μ
β
β 5m β 10 ν½μ
β
β 10m β 5 ν½μ
β
β 무νλ β 0 ν½μ
β
βββββββββββββββββββββββββββββββββββββββββββ
μ€ν λ μ€ μ ν©¶
import cv2
import numpy as np
def stereo_calibrate(obj_points, img_points_left, img_points_right,
K1, D1, K2, D2, img_size):
"""μ€ν
λ μ€ μΉ΄λ©λΌ μΊλ¦¬λΈλ μ΄μ
"""
flags = (cv2.CALIB_FIX_INTRINSIC +
cv2.CALIB_RATIONAL_MODEL)
ret, K1, D1, K2, D2, R, T, E, F = cv2.stereoCalibrate(
obj_points,
img_points_left,
img_points_right,
K1, D1,
K2, D2,
img_size,
flags=flags
)
print(f"μ€ν
λ μ€ μΊλ¦¬λΈλ μ΄μ
RMS μ€μ°¨: {ret:.4f}")
print(f"\nνμ νλ ¬ R:\n{R}")
print(f"\nνν μ΄λ λ²‘ν° T:\n{T.ravel()}")
print(f"\nλ² μ΄μ€λΌμΈ: {np.linalg.norm(T):.4f} λ¨μ")
return R, T, E, F
def stereo_rectify(K1, D1, K2, D2, img_size, R, T):
"""μ€ν
λ μ€ μ λ₯ (Rectification)"""
# μ λ₯ λ³ν κ³μ°
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(
K1, D1,
K2, D2,
img_size,
R, T,
alpha=0, # 0: μ ν¨ ν½μ
λ§, 1: λͺ¨λ ν½μ
newImageSize=img_size
)
# Q νλ ¬: μμ°¨ β 3D λ³νμ μ¬μ©
# [X Y Z W]^T = Q * [x y disparity 1]^T
print("Q νλ ¬ (μμ°¨ β 3D λ³ν):")
print(Q)
return R1, R2, P1, P2, Q, roi1, roi2
def create_rectification_maps(K, D, R, P, img_size):
"""μ λ₯ λ§΅ μμ±"""
map1, map2 = cv2.initUndistortRectifyMap(
K, D, R, P, img_size, cv2.CV_32FC1
)
return map1, map2
def rectify_stereo_pair(img_left, img_right, maps_left, maps_right):
"""μ€ν
λ μ€ μ΄λ―Έμ§ μ μ λ₯"""
rect_left = cv2.remap(img_left, maps_left[0], maps_left[1],
cv2.INTER_LINEAR)
rect_right = cv2.remap(img_right, maps_right[0], maps_right[1],
cv2.INTER_LINEAR)
return rect_left, rect_right
3. κΉμ΄ λ§΅ μμ±¶
StereoBM (Block Matching)¶
import cv2
import numpy as np
def compute_disparity_bm(left, right, num_disparities=64, block_size=15):
"""StereoBMμ μ΄μ©ν μμ°¨ λ§΅ κ³μ°"""
# κ·Έλ μ΄μ€μΌμΌ λ³ν
if len(left.shape) == 3:
left = cv2.cvtColor(left, cv2.COLOR_BGR2GRAY)
right = cv2.cvtColor(right, cv2.COLOR_BGR2GRAY)
# StereoBM μμ±
stereo = cv2.StereoBM_create(
numDisparities=num_disparities, # 16μ λ°°μ
blockSize=block_size # νμ, 5~21
)
# νλΌλ―Έν° μ‘°μ (μ ν)
stereo.setMinDisparity(0)
stereo.setSpeckleWindowSize(100)
stereo.setSpeckleRange(32)
stereo.setPreFilterType(cv2.STEREO_BM_PREFILTER_NORMALIZED_RESPONSE)
stereo.setPreFilterSize(9)
stereo.setPreFilterCap(31)
stereo.setTextureThreshold(10)
stereo.setUniquenessRatio(15)
# μμ°¨ κ³μ°
disparity = stereo.compute(left, right)
# μμ°¨ κ° μ κ·ν (16λ°°λ‘ μ€μΌμΌλμ΄ μμ)
disparity = disparity.astype(np.float32) / 16.0
return disparity
def visualize_disparity(disparity):
"""μμ°¨ λ§΅ μκ°ν"""
# μ ν¨ν μμ°¨λ§ μ¬μ©
valid_mask = disparity > 0
# μ κ·ν
disp_vis = np.zeros_like(disparity)
if np.any(valid_mask):
disp_min = np.min(disparity[valid_mask])
disp_max = np.max(disparity[valid_mask])
disp_vis = (disparity - disp_min) / (disp_max - disp_min) * 255
disp_vis = disp_vis.astype(np.uint8)
# 컬λ¬λ§΅ μ μ©
disp_color = cv2.applyColorMap(disp_vis, cv2.COLORMAP_JET)
# μ ν¨νμ§ μμ μμμ κ²μμμΌλ‘
disp_color[~valid_mask] = [0, 0, 0]
return disp_color
StereoSGBM (Semi-Global Block Matching)¶
def compute_disparity_sgbm(left, right, num_disparities=64, block_size=5):
"""StereoSGBMμ μ΄μ©ν μμ°¨ λ§΅ κ³μ°"""
# κ·Έλ μ΄μ€μΌμΌ λ³ν
if len(left.shape) == 3:
gray_left = cv2.cvtColor(left, cv2.COLOR_BGR2GRAY)
gray_right = cv2.cvtColor(right, cv2.COLOR_BGR2GRAY)
else:
gray_left, gray_right = left, right
# SGBM νλΌλ―Έν°
# P1, P2: μΈμ ν½μ
κ° μμ°¨ μ°¨μ΄μ λν νλν°
P1 = 8 * 3 * block_size ** 2
P2 = 32 * 3 * block_size ** 2
stereo = cv2.StereoSGBM_create(
minDisparity=0,
numDisparities=num_disparities,
blockSize=block_size,
P1=P1,
P2=P2,
disp12MaxDiff=1,
uniquenessRatio=10,
speckleWindowSize=100,
speckleRange=32,
preFilterCap=63,
mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY
)
# μμ°¨ κ³μ°
disparity = stereo.compute(gray_left, gray_right)
disparity = disparity.astype(np.float32) / 16.0
return disparity
def disparity_to_depth(disparity, Q):
"""μμ°¨ λ§΅μ κΉμ΄ λ§΅μΌλ‘ λ³ν"""
# Q νλ ¬μ μ΄μ©ν 3D μ¬ν¬μ
# points_3d[y, x] = [X, Y, Z, W]
points_3d = cv2.reprojectImageTo3D(disparity, Q)
# Z κ° (κΉμ΄) μΆμΆ
depth = points_3d[:, :, 2]
# μ ν¨νμ§ μμ κΉμ΄ νν°λ§
valid_mask = (disparity > 0) & (depth > 0) & (depth < 10000)
depth[~valid_mask] = 0
return depth, points_3d
def create_depth_colormap(depth, max_depth=10.0):
"""κΉμ΄ λ§΅ μκ°ν"""
# κΉμ΄ ν΄λ¦¬ν
depth_clipped = np.clip(depth, 0, max_depth)
# μ κ·ν (0-255)
depth_norm = (depth_clipped / max_depth * 255).astype(np.uint8)
# 컬λ¬λ§΅ μ μ© (κ°κΉμ΄ = λΉ¨κ°, λ¨Ό = νλ)
depth_color = cv2.applyColorMap(255 - depth_norm, cv2.COLORMAP_JET)
# μ ν¨νμ§ μμ μμ λ§μ€νΉ
depth_color[depth <= 0] = [0, 0, 0]
return depth_color
WLS νν°λ₯Ό μ΄μ©ν μμ°¨ κ°μ ¶
def compute_disparity_with_wls(left, right, num_disparities=64):
"""WLS νν°λ‘ κ°μ λ μμ°¨ λ§΅ κ³μ°"""
# κ·Έλ μ΄μ€μΌμΌ
gray_left = cv2.cvtColor(left, cv2.COLOR_BGR2GRAY)
gray_right = cv2.cvtColor(right, cv2.COLOR_BGR2GRAY)
# μΌμͺ½ λ§€μ²
left_matcher = cv2.StereoSGBM_create(
minDisparity=0,
numDisparities=num_disparities,
blockSize=5,
P1=8 * 3 * 5 ** 2,
P2=32 * 3 * 5 ** 2,
disp12MaxDiff=1,
uniquenessRatio=15,
speckleWindowSize=0,
speckleRange=2,
preFilterCap=63,
mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY
)
# μ€λ₯Έμͺ½ λ§€μ² (μΌμͺ½-μ€λ₯Έμͺ½ μΌκ΄μ± κ²μ¬μ©)
right_matcher = cv2.ximgproc.createRightMatcher(left_matcher)
# μμ°¨ κ³μ°
left_disp = left_matcher.compute(gray_left, gray_right)
right_disp = right_matcher.compute(gray_right, gray_left)
# WLS νν°
wls_filter = cv2.ximgproc.createDisparityWLSFilter(left_matcher)
wls_filter.setLambda(80000)
wls_filter.setSigmaColor(1.2)
# νν° μ μ©
filtered_disp = wls_filter.filter(left_disp, left, None, right_disp)
filtered_disp = filtered_disp.astype(np.float32) / 16.0
return filtered_disp
4. ν¬μΈνΈ ν΄λΌμ°λ¶
ν¬μΈνΈ ν΄λΌμ°λ μμ±¶
import cv2
import numpy as np
def create_point_cloud(depth, rgb, K):
"""κΉμ΄ λ§΅κ³Ό RGB μ΄λ―Έμ§λ‘ ν¬μΈνΈ ν΄λΌμ°λ μμ±"""
h, w = depth.shape
fx, fy = K[0, 0], K[1, 1]
cx, cy = K[0, 2], K[1, 2]
# ν½μ
μ’ν 그리λ
u = np.arange(w)
v = np.arange(h)
u, v = np.meshgrid(u, v)
# μ ν¨ν κΉμ΄ λ§μ€ν¬
valid = depth > 0
# 3D μ’ν κ³μ°
Z = depth[valid]
X = (u[valid] - cx) * Z / fx
Y = (v[valid] - cy) * Z / fy
# ν¬μΈνΈ ν΄λΌμ°λ (N x 3)
points = np.stack([X, Y, Z], axis=-1)
# μμ μ 보 (N x 3)
if len(rgb.shape) == 3:
colors = rgb[valid]
else:
colors = np.stack([rgb[valid]] * 3, axis=-1)
return points, colors
def subsample_point_cloud(points, colors, voxel_size=0.01):
"""볡μ
그리λλ‘ ν¬μΈνΈ ν΄λΌμ°λ λ€μ΄μνλ§"""
# 볡μ
μΈλ±μ€ κ³μ°
voxel_indices = np.floor(points / voxel_size).astype(int)
# κ³ μ ν 볡μ
λ§ μ ν
_, unique_indices = np.unique(
voxel_indices, axis=0, return_index=True
)
return points[unique_indices], colors[unique_indices]
def save_point_cloud_ply(filename, points, colors):
"""PLY νμμΌλ‘ ν¬μΈνΈ ν΄λΌμ°λ μ μ₯"""
n_points = len(points)
# PLY ν€λ
header = f"""ply
format ascii 1.0
element vertex {n_points}
property float x
property float y
property float z
property uchar red
property uchar green
property uchar blue
end_header
"""
with open(filename, 'w') as f:
f.write(header)
for i in range(n_points):
x, y, z = points[i]
r, g, b = colors[i]
f.write(f"{x:.6f} {y:.6f} {z:.6f} {int(r)} {int(g)} {int(b)}\n")
print(f"μ μ₯λ¨: {filename} ({n_points} ν¬μΈνΈ)")
ν¬μΈνΈ ν΄λΌμ°λ μ²λ¦¬¶
def remove_outliers_statistical(points, colors, nb_neighbors=20, std_ratio=2.0):
"""ν΅κ³μ μ΄μμΉ μ κ±°"""
from scipy.spatial import KDTree
# KD-Tree ꡬμΆ
tree = KDTree(points)
# κ° μ μ k-NN 거리 κ³μ°
distances, _ = tree.query(points, k=nb_neighbors + 1)
mean_distances = np.mean(distances[:, 1:], axis=1) # μκΈ° μμ μ μΈ
# μ 체 νκ· κ³Ό νμ€νΈμ°¨
global_mean = np.mean(mean_distances)
global_std = np.std(mean_distances)
# μ΄μμΉ λ§μ€ν¬
threshold = global_mean + std_ratio * global_std
inlier_mask = mean_distances < threshold
print(f"μ΄μμΉ μ κ±°: {len(points)} β {np.sum(inlier_mask)} ν¬μΈνΈ")
return points[inlier_mask], colors[inlier_mask]
def estimate_normals(points, k=30):
"""ν¬μΈνΈ ν΄λΌμ°λ λ²μ λ²‘ν° μΆμ """
from scipy.spatial import KDTree
from numpy.linalg import eig
tree = KDTree(points)
normals = np.zeros_like(points)
for i, point in enumerate(points):
# k-NN κ²μ
_, indices = tree.query(point, k=k)
neighbors = points[indices]
# 곡λΆμ° νλ ¬
centered = neighbors - np.mean(neighbors, axis=0)
cov = np.dot(centered.T, centered) / k
# κ°μ₯ μμ κ³ μ κ°μ κ³ μ 벑ν°κ° λ²μ
eigenvalues, eigenvectors = eig(cov)
min_idx = np.argmin(eigenvalues)
normals[i] = eigenvectors[:, min_idx]
return normals
5. Open3D κΈ°μ΄¶
Open3D μ€μΉ λ° κΈ°λ³Έ μ¬μ©¶
# pip install open3d
import open3d as o3d
import numpy as np
def create_open3d_point_cloud(points, colors=None):
"""Open3D ν¬μΈνΈ ν΄λΌμ°λ μμ±"""
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points)
if colors is not None:
# μμμ 0-1 λ²μλ‘ μ κ·ν
if colors.max() > 1:
colors = colors / 255.0
pcd.colors = o3d.utility.Vector3dVector(colors)
return pcd
def visualize_point_cloud(pcd):
"""ν¬μΈνΈ ν΄λΌμ°λ μκ°ν"""
# μ’νμΆ μΆκ°
coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(
size=0.5, origin=[0, 0, 0]
)
o3d.visualization.draw_geometries(
[pcd, coordinate_frame],
window_name="Point Cloud",
width=1280,
height=720,
point_show_normal=False
)
def process_point_cloud_open3d(pcd):
"""Open3Dλ‘ ν¬μΈνΈ ν΄λΌμ°λ μ²λ¦¬"""
print(f"μλ³Έ ν¬μΈνΈ μ: {len(pcd.points)}")
# 1. λ€μ΄μνλ§
pcd_down = pcd.voxel_down_sample(voxel_size=0.02)
print(f"λ€μ΄μνλ§ ν: {len(pcd_down.points)}")
# 2. μ΄μμΉ μ κ±°
pcd_clean, _ = pcd_down.remove_statistical_outlier(
nb_neighbors=20,
std_ratio=2.0
)
print(f"μ΄μμΉ μ κ±° ν: {len(pcd_clean.points)}")
# 3. λ²μ μΆμ
pcd_clean.estimate_normals(
search_param=o3d.geometry.KDTreeSearchParamHybrid(
radius=0.1, max_nn=30
)
)
# 4. λ²μ λ°©ν₯ μ λ ¬
pcd_clean.orient_normals_consistent_tangent_plane(k=15)
return pcd_clean
λ©μ¬ μ¬κ΅¬μ±¶
def reconstruct_mesh_poisson(pcd, depth=9):
"""ν¬μμ‘ νλ©΄ μ¬κ΅¬μ±"""
# λ²μ μ΄ νμν¨
if not pcd.has_normals():
pcd.estimate_normals()
pcd.orient_normals_consistent_tangent_plane(k=15)
# ν¬μμ‘ μ¬κ΅¬μ±
mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(
pcd, depth=depth
)
# μ λ°λ μμ μ κ±°
densities = np.asarray(densities)
density_threshold = np.quantile(densities, 0.01)
vertices_to_remove = densities < density_threshold
mesh.remove_vertices_by_mask(vertices_to_remove)
print(f"λ©μ¬ μ μ μ: {len(mesh.vertices)}")
print(f"λ©μ¬ μΌκ°ν μ: {len(mesh.triangles)}")
return mesh
def reconstruct_mesh_ball_pivoting(pcd):
"""λ³Ό νΌλ²ν
νλ©΄ μ¬κ΅¬μ±"""
if not pcd.has_normals():
pcd.estimate_normals()
# λ°κ²½ μΆμ
distances = pcd.compute_nearest_neighbor_distance()
avg_dist = np.mean(distances)
radii = [avg_dist, avg_dist * 2, avg_dist * 4]
mesh = o3d.geometry.TriangleMesh.create_from_point_cloud_ball_pivoting(
pcd, o3d.utility.DoubleVector(radii)
)
return mesh
def save_mesh(mesh, filename):
"""λ©μ¬ μ μ₯"""
o3d.io.write_triangle_mesh(filename, mesh)
print(f"λ©μ¬ μ μ₯λ¨: {filename}")
RGBD μ΄λ―Έμ§ μ²λ¦¬¶
def create_rgbd_from_opencv(color_img, depth_img, K):
"""OpenCV μ΄λ―Έμ§λ₯Ό Open3D RGBDλ‘ λ³ν"""
# BGR β RGB
color_rgb = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB)
# Open3D μ΄λ―Έμ§λ‘ λ³ν
color_o3d = o3d.geometry.Image(color_rgb)
depth_o3d = o3d.geometry.Image(depth_img.astype(np.float32))
# RGBD μ΄λ―Έμ§ μμ±
rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth(
color_o3d, depth_o3d,
depth_scale=1000.0, # mm β m
depth_trunc=3.0, # μ΅λ κΉμ΄
convert_rgb_to_intensity=False
)
return rgbd
def rgbd_to_point_cloud(rgbd, K, width, height):
"""RGBD μ΄λ―Έμ§μμ ν¬μΈνΈ ν΄λΌμ°λ μμ±"""
# Open3D μΉ΄λ©λΌ νλΌλ―Έν°
intrinsic = o3d.camera.PinholeCameraIntrinsic(
width, height,
K[0, 0], K[1, 1], # fx, fy
K[0, 2], K[1, 2] # cx, cy
)
# ν¬μΈνΈ ν΄λΌμ°λ μμ±
pcd = o3d.geometry.PointCloud.create_from_rgbd_image(
rgbd, intrinsic
)
return pcd
6. 3D μ¬κ΅¬μ±¶
λ€μ€ λ·° μ€ν λ μ€ (MVS) κ°λ ¶
λ€μ€ λ·° μ€ν
λ μ€ νμ΄νλΌμΈ:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β 1. μ΄λ―Έμ§ μμ§ β
β μ¬λ¬ κ°λμμ λμ 촬μ β
β π· π· π· π· π· β
β β
β 2. νΉμ§μ κ²μΆ λ° λ§€μΉ β
β SIFT, ORB λ±μΌλ‘ μ΄λ―Έμ§ κ° λμμ μ°ΎκΈ° β
β β βββββββββββ β β
β β
β 3. Structure from Motion (SfM) β
β μΉ΄λ©λΌ ν¬μ¦ μΆμ + ν¬μ ν¬μΈνΈ ν΄λΌμ°λ β
β π·βββββ β β
β π·βββββΌβββββ β β
β π·βββββ β β
β β
β 4. μ‘°λ° μ¬κ΅¬μ± (Dense Reconstruction) β
β λͺ¨λ ν½μ
μ λν΄ κΉμ΄ μΆμ β
β [:::::::::::] β
β β
β 5. λ©μ¬ μμ± β
β ν¬μΈνΈ ν΄λΌμ°λ β μΌκ°ν λ©μ¬ β
β β²β²β²β²β²β²β²β² β
β β
β 6. ν
μ€μ² λ§€ν β
β μλ³Έ μ΄λ―Έμ§λ‘ λ©μ¬μ ν
μ€μ² μ μ© β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Essential Matrix κΈ°λ° ν¬μ¦ μΆμ ¶
import cv2
import numpy as np
def estimate_pose_from_essential(pts1, pts2, K):
"""Essential Matrixλ‘ μλ ν¬μ¦ μΆμ """
# Essential Matrix κ³μ°
E, mask = cv2.findEssentialMat(
pts1, pts2, K,
method=cv2.RANSAC,
prob=0.999,
threshold=1.0
)
print(f"μΈλΌμ΄μ΄ λΉμ¨: {np.sum(mask) / len(mask) * 100:.1f}%")
# Essential Matrixμμ R, t 볡ꡬ
_, R, t, mask = cv2.recoverPose(E, pts1, pts2, K)
print(f"\nνμ νλ ¬ R:\n{R}")
print(f"\nνν μ΄λ λ²‘ν° t (λ¨μ 벑ν°):\n{t.ravel()}")
return R, t
def triangulate_points(pts1, pts2, K, R, t):
"""λ λ·°μμ 3D μ μΌκ°μΈ‘λ"""
# ν¬μ νλ ¬ ꡬμ±
P1 = K @ np.hstack([np.eye(3), np.zeros((3, 1))])
P2 = K @ np.hstack([R, t])
# μΌκ°μΈ‘λ
pts1_h = pts1.T # (2, N)
pts2_h = pts2.T
points_4d = cv2.triangulatePoints(P1, P2, pts1_h, pts2_h)
# λμ°¨ μ’ν β 3D μ’ν
points_3d = points_4d[:3] / points_4d[3]
return points_3d.T # (N, 3)
def incremental_sfm(images, K):
"""μ¦λΆμ SfM (κ°λ¨ν λ²μ )"""
# SIFT κ²μΆκΈ°
sift = cv2.SIFT_create()
# 첫 λ μ΄λ―Έμ§λ‘ μ΄κΈ°ν
kp1, desc1 = sift.detectAndCompute(images[0], None)
kp2, desc2 = sift.detectAndCompute(images[1], None)
# λ§€μΉ
bf = cv2.BFMatcher()
matches = bf.knnMatch(desc1, desc2, k=2)
# λΉμ¨ ν
μ€νΈ
good_matches = []
for m, n in matches:
if m.distance < 0.75 * n.distance:
good_matches.append(m)
pts1 = np.float32([kp1[m.queryIdx].pt for m in good_matches])
pts2 = np.float32([kp2[m.trainIdx].pt for m in good_matches])
# μ΄κΈ° ν¬μ¦ λ° 3D μ
R, t = estimate_pose_from_essential(pts1, pts2, K)
points_3d = triangulate_points(pts1, pts2, K, R, t)
# μΉ΄λ©λΌ ν¬μ¦ μ μ₯
camera_poses = [
{'R': np.eye(3), 't': np.zeros((3, 1))}, # 첫 λ²μ§Έ μΉ΄λ©λΌ
{'R': R, 't': t} # λ λ²μ§Έ μΉ΄λ©λΌ
]
print(f"μ΄κΈ° 3D μ μ: {len(points_3d)}")
# μ΄ν μ΄λ―Έμ§ μΆκ° (PnPλ‘ ν¬μ¦ μΆμ )
for i in range(2, len(images)):
kp_new, desc_new = sift.detectAndCompute(images[i], None)
# μ΄μ μ΄λ―Έμ§μ λ§€μΉ
matches = bf.knnMatch(desc2, desc_new, k=2)
good_matches = []
for m, n in matches:
if m.distance < 0.75 * n.distance:
good_matches.append(m)
# 3D-2D λμμ
obj_points = points_3d[[m.queryIdx for m in good_matches]]
img_points = np.float32([kp_new[m.trainIdx].pt for m in good_matches])
# PnPλ‘ ν¬μ¦ μΆμ
success, rvec, tvec, inliers = cv2.solvePnPRansac(
obj_points, img_points, K, None
)
if success:
R_new, _ = cv2.Rodrigues(rvec)
camera_poses.append({'R': R_new, 't': tvec})
print(f"μ΄λ―Έμ§ {i} λ±λ‘ μλ£ (μΈλΌμ΄μ΄: {len(inliers)})")
# λ€μ λ°λ³΅μ μν΄ μ
λ°μ΄νΈ
desc2 = desc_new
return points_3d, camera_poses
λ²λ€ μ‘°μ (Bundle Adjustment)¶
λ²λ€ μ‘°μ (Bundle Adjustment):
μΉ΄λ©λΌ νλΌλ―Έν°μ 3D μ μμΉλ₯Ό λμμ μ΅μ ν
μ΅μν λͺ©ν:
E = Ξ£_i Ξ£_j || x_ij - Ο(K, R_i, t_i, X_j) ||Β²
μ¬κΈ°μ:
- x_ij: μ΄λ―Έμ§ iμμ κ΄μΈ‘λ μ jμ 2D μ’ν
- Ο(): 3D β 2D ν¬μ ν¨μ
- K: μΉ΄λ©λΌ λ΄λΆ νλΌλ―Έν°
- R_i, t_i: μΉ΄λ©λΌ iμ ν¬μ¦
- X_j: 3D μ jμ μ’ν
μ΅μ ν λꡬ:
- Ceres Solver
- g2o
- SciPy (μμ λ¬Έμ μ©)
7. μ°μ΅ λ¬Έμ ¶
λ¬Έμ 1: μ€ν λ μ€ κΉμ΄ μΆμ ¶
μ€ν λ μ€ μ΄λ―Έμ§ μμμ κΉμ΄ λ§΅μ μμ±νμΈμ.
μꡬμ¬ν: - StereoBMκ³Ό StereoSGBM λΉκ΅ - μμ°¨ λ§΅ μκ°ν - κΉμ΄ λ§΅μΌλ‘ λ³ν - νμ§ κ°μ (νν°λ§)
ννΈ
# νλΌλ―Έν° νλ νμ
stereo = cv2.StereoSGBM_create(
numDisparities=128,
blockSize=5,
P1=8 * 3 * 5 ** 2,
P2=32 * 3 * 5 ** 2
)
# WLS νν°λ‘ κ°μ
wls_filter = cv2.ximgproc.createDisparityWLSFilter(stereo)
λ¬Έμ 2: ν¬μΈνΈ ν΄λΌμ°λ νν°λ§¶
λ Έμ΄μ¦κ° μλ ν¬μΈνΈ ν΄λΌμ°λλ₯Ό μ μ νμΈμ.
μꡬμ¬ν: - ν΅κ³μ μ΄μμΉ μ κ±° - 볡μ λ€μ΄μνλ§ - νλ©΄ μμ μΆμΆ - κ²°κ³Ό μκ°ν
ννΈ
import open3d as o3d
# μ΄μμΉ μ κ±°
pcd_clean, _ = pcd.remove_statistical_outlier(
nb_neighbors=20, std_ratio=2.0
)
# λ€μ΄μνλ§
pcd_down = pcd_clean.voxel_down_sample(0.02)
# νλ©΄ μΆμΆ (RANSAC)
plane_model, inliers = pcd_down.segment_plane(
distance_threshold=0.01,
ransac_n=3,
num_iterations=1000
)
λ¬Έμ 3: λ λ·°μμ 3D μ¬κ΅¬μ±¶
λ μ΄λ―Έμ§μμ 3D ν¬μΈνΈλ₯Ό μ¬κ΅¬μ±νμΈμ.
μꡬμ¬ν: - νΉμ§μ κ²μΆ λ° λ§€μΉ - Essential Matrix κ³μ° - μΉ΄λ©λΌ ν¬μ¦ 볡ꡬ - μΌκ°μΈ‘λμΌλ‘ 3D μ μμ±
ννΈ
# Essential Matrix
E, mask = cv2.findEssentialMat(pts1, pts2, K)
# ν¬μ¦ 볡ꡬ
_, R, t, _ = cv2.recoverPose(E, pts1, pts2, K)
# μΌκ°μΈ‘λ
points_4d = cv2.triangulatePoints(P1, P2, pts1.T, pts2.T)
points_3d = points_4d[:3] / points_4d[3]
λ¬Έμ 4: λ©μ¬ μ¬κ΅¬μ±¶
ν¬μΈνΈ ν΄λΌμ°λμμ 3D λ©μ¬λ₯Ό μμ±νμΈμ.
μꡬμ¬ν: - ν¬μΈνΈ ν΄λΌμ°λ μ μ²λ¦¬ - λ²μ λ²‘ν° μΆμ - ν¬μμ‘ λλ λ³Ό νΌλ²ν μ¬κ΅¬μ± - κ²°κ³Ό μ μ₯ λ° μκ°ν
ννΈ
# λ²μ μΆμ
pcd.estimate_normals()
pcd.orient_normals_consistent_tangent_plane(k=15)
# ν¬μμ‘ μ¬κ΅¬μ±
mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(
pcd, depth=9
)
# μ λ°λ μμ μ κ±°
densities = np.asarray(densities)
mesh.remove_vertices_by_mask(densities < np.quantile(densities, 0.01))
λ¬Έμ 5: μ€μκ° μ€ν λ μ€ λΉμ ¶
μΉμΊ λλ μ€ν λ μ€ μΉ΄λ©λΌλ‘ μ€μκ° κΉμ΄ μΆμ μ ꡬννμΈμ.
μꡬμ¬ν: - μΉ΄λ©λΌ μΊλ¦¬λΈλ μ΄μ μ μ© - μ€μκ° μμ°¨ κ³μ° - κΉμ΄ μκ°ν - FPS μΈ‘μ
ννΈ
# 리맡ν λ§΅ 미리 κ³μ°
map1_left, map2_left = cv2.initUndistortRectifyMap(...)
map1_right, map2_right = cv2.initUndistortRectifyMap(...)
while True:
# μ λ₯
rect_left = cv2.remap(left, map1_left, map2_left, cv2.INTER_LINEAR)
rect_right = cv2.remap(right, map1_right, map2_right, cv2.INTER_LINEAR)
# μμ°¨ κ³μ° (SGBM)
disparity = stereo.compute(rect_left, rect_right)
λ€μ λ¨κ³¶
- 22_Depth_Estimation.md - λ¨μ κΉμ΄ μΆμ , MiDaS, DPT, Structure from Motion