1. 핵심 개념
§01LiDAR는 정확한 기하학(거리, 3D 형태)을 제공하지만 색각이 없습니다. 먼 거리에서 기하학만으로는 보행자와 기둥을 구분할 수 없습니다. 카메라는 풍부한 의미론적 정보를 제공하지만 깊이가 부족합니다. 복잡한 특성 수준의 융합 네트워크를 설계하는 대신, PointPainting은 실용적인 접근 방식을 취합니다: 2D 분할 네트워크의 의미론적 클래스 점수로 각 LiDAR 포인트를 '칠하고', 강화된 포인트 클라우드를 기존의 모든 3D 검출기에 변경 없이 전달합니다.
"칠하기"라는 이름은 적절합니다: LiDAR 기하학을 이미지 캔버스에 투영하고, 각 픽셀에서 색(의미론적 점수)을 찾아 이 점수들을 3D로 가져옵니다. 3D 검출기는 (x,y,z) 대신 (x,y,z,s₁,…,sC)를 보게 됩니다. 여기서 s_i는 클래스 i에 대한 점수입니다.
2. 전체 파이프라인
§023. 투영 수학
§03월드/차량 좌표계의 LiDAR 포인트 P = (x,y,z)에 대해, 카메라 이미지의 픽셀 좌표를 계산합니다:
[u, v, w]ᵀ = K · T_cam_lidar · [x, y, z, 1]ᵀ
u_px = u/w, v_px = v/w
paint = seg_map[round(v_px), round(u_px), :] # C-클래스 소프트맥스 점수
point_painted = [x, y, z, paint₁, …, paintC]
이미지 경계 내에 투영되고 양의 깊이(w > 0)를 가진 포인트만 칠해집니다. 나머지는 영 점수 벡터(또는 배경 클래스 우선)를 유지합니다. 보정 행렬 K와 외부 매개변수 T_cam_lidar는 데이터셋에서 제공됩니다.
4. 지연 시간 및 파이프라이닝
§04병목은 2D 의미론적 분할입니다: 고해상도 이미지에서 DeepLabV3+와 같은 강력한 모델은 50–200ms가 걸리며, 이는 100ms LiDAR 주기를 훨씬 초과합니다. PointPainting은 파이프라이닝 팁을 제안합니다: 프레임 T−1의 분할 결과를 사용하여 프레임 T의 포인트 클라우드를 칠합니다. 카메라와 LiDAR은 공통 좌표계에 등록되고 연속 프레임 간의 장면 변화는 작기 때문에, 낡은 분할은 최소한의 오류를 도입하지만 지연 시간 병목을 제거합니다.
5. PyTorch 구현
§05import torch
import numpy as np
def paint_points(
points_lidar: np.ndarray, # N × 3 (x, y, z) LiDAR 좌표계
seg_map: np.ndarray, # H × W × C 소프트맥스 점수
T_cam_lidar: np.ndarray, # 4×4 외부 매개변수 (lidar → camera)
K: np.ndarray, # 3×4 내부 매개변수 / 투영 행렬
) -> np.ndarray:
# 각 LiDAR 포인트에 픽셀별 의미론적 점수를 추가합니다.
# 반환: painted_points: N × (3 + C)
H, W, C = seg_map.shape
N = points_lidar.shape[0]
# 동차 LiDAR 포인트: 4 × N
ones = np.ones((N, 1))
pts_h = np.hstack([points_lidar, ones]).T # 4 × N
# 카메라 프레임에 투영
pts_cam = T_cam_lidar @ pts_h # 4 × N (Rt 적용)
pts_2d = K @ pts_cam # 3 × N (K · [X;Y;Z;1])
# 픽셀 좌표로 정규화
depth = pts_2d[2, :] # 카메라 프레임의 Z
u = pts_2d[0, :] / (depth + 1e-6)
v = pts_2d[1, :] / (depth + 1e-6)
u_px = np.round(u).astype(int)
v_px = np.round(v).astype(int)
# 유효성 마스크: 범위 내이고 양의 깊이
valid = (depth > 0) & (u_px >= 0) & (u_px < W) & (v_px >= 0) & (v_px < H)
# 의미론적 점수 조회
scores = np.zeros((N, C), dtype=np.float32)
scores[valid] = seg_map[v_px[valid], u_px[valid], :] # N_valid × C
painted = np.hstack([points_lidar, scores]) # N × (3 + C)
return painted
# --- 배치 GPU 추론을 위한 PyTorch 버전 ---
def paint_points_torch(
points: torch.Tensor, # N × 3
seg_map: torch.Tensor, # H × W × C
T: torch.Tensor, # 4 × 4
K: torch.Tensor, # 3 × 4
) -> torch.Tensor:
N = points.shape[0]
H, W, C = seg_map.shape
ones = torch.ones(N, 1, device=points.device)
pts_h = torch.cat([points, ones], dim=1).T # 4 × N
pts_cam = T @ pts_h # 4 × N
pts_2d = K @ pts_cam # 3 × N
depth = pts_2d[2]
u = (pts_2d[0] / depth.clamp(min=1e-6)).long()
v = (pts_2d[1] / depth.clamp(min=1e-6)).long()
valid = (depth > 0) & (u >= 0) & (u < W) & (v >= 0) & (v < H)
scores = torch.zeros(N, C, device=points.device)
scores[valid] = seg_map[v[valid], u[valid]]
return torch.cat([points, scores], dim=1) # N × (3+C)
6. 결과
§06| 검출기 | 데이터셋 | 기준선 mAP | + PointPainting | 향상 |
|---|---|---|---|---|
| PointPillars | nuScenes | 40.1 | 46.4 | +6.3 |
| PointRCNN | nuScenes | 40.9 | 46.0 | +5.1 |
| MVXNet | nuScenes | 41.5 | 47.3 | +5.8 |
| PointPillars | KITTI (보행자) | 57.6% | 62.3% | +4.7% |
7. 평가
§07PointPainting의 매력은 그 단순성에 있습니다: 3D 검출기 아키텍처에 0개 변경이 필요합니다. 입력 채널을 3(또는 강도가 있으면 4)에서 3+C로 늘리기만 하면 됩니다. 가변 채널 입력을 허용하는 모든 검출기(대부분은)가 즉시 이점을 얻습니다. 이는 2D 분할 네트워크에 한 번만 비용을 지불하고 모든 다운스트림 검출기에 이점을 분산시킨다는 의미에서 "무료 점심" 융합입니다.
솔직한 제한: 이 방법은 기본적으로 2D 분할 품질에 의해 병목이 됩니다. 2D 모델이 보행자를 교통 신호로 혼동하면 그 잘못된 점수들이 포인트 클라우드에 굳혀지고 3D 검출기는 쉽게 회복할 수 없습니다. 폐색은 문제를 악화시킵니다: 자동차 뒤에 있는 LiDAR 포인트가 자동차 픽셀에 투영되면 폐색된 물체의 의미론이 아닌 자동차의 의미론을 받습니다. 순차적 특성은 1단계(2D 분할)의 오류가 2단계(3D 검출)에 의해 수정되지 않음을 의미합니다.
이러한 제한에도 불구하고 PointPainting은 센서 융합 벤치마크의 표준 기준선이 되었습니다. 후속 작업(AutoAlign, FusionPainting, BEVFusion)은 융합 정렬을 엔드-투-엔드로 학습하려고 시도하지만, PointPainting의 단순 투영 접근은 경쟁력 있으며 사소하게 재현 가능합니다.