1. 동기
§012D 이미지 기반 탐지기는 픽셀이 균등하게 배열되어 있기 때문에 이미지 평면 전체에 걸쳐 조밀하게 제안을 생성합니다. 3D에서 LiDAR 점 구름은 희소하고 불규칙합니다 — 대부분의 부피는 비어있습니다. 2D 앵커 그리드를 3D로 확장(AVOD, PointPillars처럼)하면 빈 복셀에 계산을 낭비하고 클래스별 앵커 크기를 손으로 조정해야 합니다.
PointRCNN의 핵심 통찰: 전경점은 이미 객체 표면에 있습니다. 각 전경점이 자신의 3D 바운딩 박스 제안에 투표하게 하면 어떨까요? 이 상향식 접근법은 자연스럽게 객체가 실제로 있는 곳에 제안을 집중시킵니다.
2. 2단계 아키텍처
§023. 1단계 — 상향식 제안 생성
§03PointNet++ 백본은 N개의 입력 점 각각을 점별 특성 벡터로 인코딩합니다. 분할 헤드는 각 점을 전경(객체 위) 또는 배경으로 분류합니다. 지도 신호는 3D 바운딩 박스 주석을 사용합니다: GT 박스 내부의 모든 점이 전경입니다.
각 전경점은 자신을 앵커로 하는 3D 제안을 예측합니다. x, z, θ에 대해서는 구간 기반 인코딩을 사용하고 y, h, w, l에 대해서는 직접 회귀를 사용합니다.
- 분할을 위한 초점 손실(Focal Loss) — 클래스 불균형이 심합니다(전경 약 5%). 초점 손실은 쉬운 배경 예제의 가중치를 낮춥니다.
- 구간 기반 x/z 인코딩 — 점으로부터의 x와 z 오프셋을 S개의 이산 구간으로 나눕니다(예: S=12); 네트워크는 구간 인덱스(교차 엔트로피)와 구간 내 잔차(L1)를 예측합니다. 전체 범위에 대한 회귀 불안정성을 방지합니다.
- 구간 기반 각도 θ — 각도를 30도의 12개 구간으로 나눕니다; 구간 분류 + 잔차. 3D 박스의 π 주기성을 깔끔하게 처리합니다.
- y, h, w, l에 대한 직접 회귀 — 운전 시나리오에서 높이는 작은 범위; 너비/길이는 평균 사전 정보로부터 로그 스케일 잔차로 회귀합니다.
4. 2단계 — RCNN 정제
§041단계에서 점수가 높은 상위 300개 제안(NMS 후)이 2단계로 전달됩니다. 각 제안 박스에 대해 PointRCNN은 약간 확대하고 내부에 있는 모든 원본 LiDAR 점을 수집합니다. 이 점들은 정규 좌표계로 변환됩니다 (제안 중심에 중심화, 방향과 정렬), 정제 작업을 회전 불변으로 만듭니다.
정규 점 집합은 또 다른 PointNet++로 공급되어 최종 박스 정제 (x,y,z,h,w,l,θ에 대한 잔차)와 IoU 기반 신뢰도 점수를 예측합니다. 이 2단계 설계 — 정규 공간에서 정제되는 거친 제안 — Faster R-CNN의 관심 영역 풀링을 반영하지만 3D 점 구름 공간에서입니다.
5. PyTorch 구현
§05import torch
import torch.nn as nn
import torch.nn.functional as F
class ProposalHead(nn.Module):
# 1단계 헤드: 점별 전경/배경 분류 + 구간 기반 3D 박스 회귀.
# in_ch: PointNet++ 백본의 점별 특성 차원
# num_bins: x, z, theta 인코딩용 S개 구간
def __init__(self, in_ch=256, num_bins=12):
super().__init__()
self.num_bins = num_bins
# 포크 전 공유 MLP
self.shared = nn.Sequential(
nn.Conv1d(in_ch, 256, 1), nn.BatchNorm1d(256), nn.ReLU(),
nn.Conv1d(256, 256, 1), nn.BatchNorm1d(256), nn.ReLU(),
)
# 전경 / 배경 이진 분류
self.seg_head = nn.Conv1d(256, 1, 1)
# 박스 회귀 헤드
# x: num_bins 분류 + num_bins 잔차
# z: num_bins 분류 + num_bins 잔차
# theta: num_bins 분류 + num_bins 잔차
# y, h, w, l: 직접 회귀 (4개 값)
self.x_bin_cls = nn.Conv1d(256, num_bins, 1)
self.x_bin_res = nn.Conv1d(256, num_bins, 1)
self.z_bin_cls = nn.Conv1d(256, num_bins, 1)
self.z_bin_res = nn.Conv1d(256, num_bins, 1)
self.theta_cls = nn.Conv1d(256, num_bins, 1)
self.theta_res = nn.Conv1d(256, num_bins, 1)
self.reg_head = nn.Conv1d(256, 4, 1) # y, h, w, l
def forward(self, point_feat):
# point_feat: B × C × N (PointNet++ 백본의 출력)
# 손실 계산용 원본 예측의 딕셔너리 반환.
x = self.shared(point_feat) # B × 256 × N
seg_logits = self.seg_head(x).squeeze(1) # B × N (전경/배경)
preds = {
'seg': seg_logits,
'x_bin': self.x_bin_cls(x), # B × S × N
'x_res': self.x_bin_res(x),
'z_bin': self.z_bin_cls(x),
'z_res': self.z_bin_res(x),
'theta_bin':self.theta_cls(x),
'theta_res':self.theta_res(x),
'reg': self.reg_head(x), # B × 4 × N (y,h,w,l)
}
return preds
def decode_proposals(preds, points_xyz, num_bins=12, search_range=3.0):
# 점별 예측을 3D 제안으로 변환.
# preds: ProposalHead.forward()의 출력
# points_xyz: B × N × 3 (각 점의 x, y, z)
# 반환: B × N × 7 (x, y, z, h, w, l, theta)
B, N, _ = points_xyz.shape
S = num_bins
bin_size = 2 * search_range / S
# x
x_bin = preds['x_bin'].argmax(1) # B × N
x_res = preds['x_res'].gather(1, x_bin.unsqueeze(1)).squeeze(1)
x = points_xyz[..., 0] + (x_bin.float() - S/2 + 0.5) * bin_size + x_res
# z (유사)
z_bin = preds['z_bin'].argmax(1)
z_res = preds['z_res'].gather(1, z_bin.unsqueeze(1)).squeeze(1)
z = points_xyz[..., 2] + (z_bin.float() - S/2 + 0.5) * bin_size + z_res
# theta
t_bin = preds['theta_bin'].argmax(1)
t_res = preds['theta_res'].gather(1, t_bin.unsqueeze(1)).squeeze(1)
import math
theta = (t_bin.float() * (2*math.pi / S) - math.pi) + t_res
reg = preds['reg'] # B × 4 × N
y = points_xyz[..., 1] + reg[:, 0, :].permute(1,0)[...,0] # 단순화
proposals = torch.stack([x, points_xyz[...,1], z,
reg[:,1,:].permute(1,0)[...,0],
reg[:,2,:].permute(1,0)[...,0],
reg[:,3,:].permute(1,0)[...,0], theta], dim=-1)
return proposals # B × N × 7
6. 결과
§06| 방법 | 양식 | 자동차 쉬움 | 자동차 중 | 자동차 어려움 |
|---|---|---|---|---|
| VoxelNet | LiDAR | 77.47% | 65.11% | 57.73% |
| SECOND | LiDAR | 83.13% | 73.66% | 66.20% |
| PointPillars | LiDAR | 82.58% | 74.31% | 68.99% |
| PointRCNN | LiDAR | 92.13% | 86.96% | 75.42% |
7. 해설
§07PointRCNN은 2단계 탐지-후-정제 패러다임이 3D 점 구름에 깔끔하게 변환됨을 보여주었습니다 — 복셀 그리드가 필요 없어도 고품질 3D 제안을 얻을 수 있습니다. 상향식 접근법은 우아합니다: 모든 전경점을 제안 앵커로 사용하여 네트워크는 분할의 부작용으로 조밀하고 기하학적으로 인식하는 후보를 무료로 생성합니다.
구간 기반 박스 인코딩이 널리 채택되었습니다. 순수 회귀는 각도 불연속 근처에서 취약합니다(예: 0과 2π는 같은 방향이지만 수치적으로 멉니다); 구간 분류는 이를 완전히 회피합니다. 후속 작업(PV-RCNN, Voxel-RCNN)은 이 인코딩을 유지하면서 백본을 업그레이드했습니다.
제한: 1단계 전경 분할은 훈련 시간에 점별 3D 지표 라벨이 필요합니다 — 주석을 달기에 비용이 많이 듭니다. 실제로 3D 박스 주석만으로도 점-박스 내 라벨을 자동으로 파생시킬 수 있으므로 이는 실제 제약이 아닙니다.