CVPR 2019
PointRCNN: 점 구름으로부터의 3D 객체 제안 생성 및 탐지
Shaoshuai Shi · Xiaogang Wang · Hongsheng Li — CUHK MMLab
KITTI 자동차 AP (중)86.96% @ IoU 0.7
파이프라인2단계, 앵커 프리
입력원본 LiDAR 점 구름
백본PointNet++
구간 기반 x/z 인코딩 (S=6 구간 표시) 구간 0 구간 1 구간 2 ✓ 구간 3 구간 4 구간 5 점 p 목표 잔차 δ 예측: 구간_분류 (소프트맥스) + 잔차_회귀 (L1) — 안정적인 훈련
PointRCNN Two-Stage Detection Pipeline

1. 동기

§01

2D 이미지 기반 탐지기는 픽셀이 균등하게 배열되어 있기 때문에 이미지 평면 전체에 걸쳐 조밀하게 제안을 생성합니다. 3D에서 LiDAR 점 구름은 희소하고 불규칙합니다 — 대부분의 부피는 비어있습니다. 2D 앵커 그리드를 3D로 확장(AVOD, PointPillars처럼)하면 빈 복셀에 계산을 낭비하고 클래스별 앵커 크기를 손으로 조정해야 합니다.

PointRCNN의 핵심 통찰: 전경점은 이미 객체 표면에 있습니다. 각 전경점이 자신의 3D 바운딩 박스 제안에 투표하게 하면 어떨까요? 이 상향식 접근법은 자연스럽게 객체가 실제로 있는 곳에 제안을 집중시킵니다.

2. 2단계 아키텍처

§02
점 구름 N × 3 1단계 — 상향식 제안 PointNet++ 백본 전경/배경 분할 3D 제안 구간+잔차 인코딩 2단계 — RCNN 정제 점 구름 영역 풀링 정규 변환 + MLP 박스 정제 + 신뢰도 점수 최종 3D 박스 (x,y,z,h,w,l,θ)
PointRCNN 2단계 파이프라인: 1단계는 상향식 분할을 통해 점별 제안 생성; 2단계는 풀링된 국소 점을 사용하여 정제

3. 1단계 — 상향식 제안 생성

§03

PointNet++ 백본은 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에 대한 직접 회귀 — 운전 시나리오에서 높이는 작은 범위; 너비/길이는 평균 사전 정보로부터 로그 스케일 잔차로 회귀합니다.
구간 기반 x/z 인코딩 (S=6 구간 표시) 구간 0 구간 1 구간 2 ✓ 구간 3 구간 4 구간 5 점 p 목표 잔차 δ 예측: 구간_분류 (소프트맥스) + 잔차_회귀 (L1) — 안정적인 훈련
구간 기반 3D 박스 인코딩: x와 z 범위를 S개 구간으로 나눔; 구간 인덱스(분류) + 구간 내 잔차(회귀) 예측

4. 2단계 — RCNN 정제

§04

1단계에서 점수가 높은 상위 300개 제안(NMS 후)이 2단계로 전달됩니다. 각 제안 박스에 대해 PointRCNN은 약간 확대하고 내부에 있는 모든 원본 LiDAR 점을 수집합니다. 이 점들은 정규 좌표계로 변환됩니다 (제안 중심에 중심화, 방향과 정렬), 정제 작업을 회전 불변으로 만듭니다.

정규 점 집합은 또 다른 PointNet++로 공급되어 최종 박스 정제 (x,y,z,h,w,l,θ에 대한 잔차)와 IoU 기반 신뢰도 점수를 예측합니다. 이 2단계 설계 — 정규 공간에서 정제되는 거친 제안 — Faster R-CNN의 관심 영역 풀링을 반영하지만 3D 점 구름 공간에서입니다.

5. PyTorch 구현

§05
pointrcnn_head.py — 1단계 전경 분할 + 제안 헤드
import 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
방법양식자동차 쉬움자동차 중자동차 어려움
VoxelNetLiDAR77.47%65.11%57.73%
SECONDLiDAR83.13%73.66%66.20%
PointPillarsLiDAR82.58%74.31%68.99%
PointRCNNLiDAR92.13%86.96%75.42%
KITTI 벤치마크 (3D 탐지, 자동차 클래스, IoU≥0.7): PointRCNN은 86.96% 중급 AP를 달성하여 모든 이전 단계 LiDAR 방법을 능가합니다. 2단계 정제는 이미 강력한 PointPillars 기준 대비 약 10% 향상을 제공합니다.

7. 해설

§07

PointRCNN은 2단계 탐지-후-정제 패러다임이 3D 점 구름에 깔끔하게 변환됨을 보여주었습니다 — 복셀 그리드가 필요 없어도 고품질 3D 제안을 얻을 수 있습니다. 상향식 접근법은 우아합니다: 모든 전경점을 제안 앵커로 사용하여 네트워크는 분할의 부작용으로 조밀하고 기하학적으로 인식하는 후보를 무료로 생성합니다.

구간 기반 박스 인코딩이 널리 채택되었습니다. 순수 회귀는 각도 불연속 근처에서 취약합니다(예: 0과 2π는 같은 방향이지만 수치적으로 멉니다); 구간 분류는 이를 완전히 회피합니다. 후속 작업(PV-RCNN, Voxel-RCNN)은 이 인코딩을 유지하면서 백본을 업그레이드했습니다.

제한: 1단계 전경 분할은 훈련 시간에 점별 3D 지표 라벨이 필요합니다 — 주석을 달기에 비용이 많이 듭니다. 실제로 3D 박스 주석만으로도 점-박스 내 라벨을 자동으로 파생시킬 수 있으므로 이는 실제 제약이 아닙니다.