文章目录
前言
本文对OpenMMLab在Monocular 3D detection领域做的两项工作FCOS3D和PGD(也被称作FCOS3D++)进行介绍。
在此之前,建议大家通过这篇博客:“3Dfy” A General 2D Detector: 纯视觉 3D 检测再思考,来回顾单目3D目标检测的更多细节。
FCOS3D
T. Wang, X. Zhu, J. Pang, and D. Lin. FCOS3D: Fully convolutional one-stage monocular
3d object detection. In Proceedings of the IEEE/CVF International Conference on Computer
Vision (ICCV) Workshops, 2021.
论文
代码
概述
3D检测由于其固有的不适定性,比传统的2D情况更具挑战性,这主要体现在深度信息的缺乏。在本文中,我们通过建立在全卷积单级检测器上的实践来研究这个问题,并提出了一个通用框架FCOS3D。具体而言,我们首先将通常定义的7-DoF 3D位置投影到2D图像上,并获得投影的中心点,与之前的2D中心相比,我们将其命名为3D中心。利用该投影,3D中心包含2.5D信息,即2D位置及其相应深度。2D位置可以进一步减少到从图像上的某个点的2D偏移,这用作可以在不同特征级别之间归一化的唯一2D属性。相比之下,深度、3D尺寸和方向被视为解耦后的3D属性。然后,考虑到对象的2D比例,将对象分布到不同的特征级别,并仅根据训练过程的投影3D中心进行分配。此外,基于3D中心用2D高斯分布重新定义中心度,以拟合3D目标公式。所有这些都使该框架简单而有效,消除了任何2D检测或2D-3D对应先验。
主要创新点
- 将7-DoF三维属性解耦为2D(位置偏移)和3D属性(深度、尺寸和旋转角等)
- 考虑目标的2D比例,将目标分布到不同的特征级别,并仅根据训练过程的投影三维中心进行分配
- 使用基于3D中心的2D高斯分布来表示3D Center-ness(来确定哪些点更靠近中心,并抑制远离目标中心的低质量预测)
主要框架结构
全卷积一阶段检测器通常由三个部件组成:用于特征提取的Backbone、用于多级分支构造的Neck和用于密集预测的Head
- Backbone:使用预训练的ResNet101以及可变形卷积DCN进行特征提取,为了避免更多的内存开销,固定第一个卷积块参数
- Neck:生成特征层 P3-P7(按照原始 FCOS 获得P3到P5,然后使用两个卷积块对P5进行下采样,以获得P6和P7),每个特征层用于检测不同尺度的目标
-
Head:要处理两个关键问题:
- 如何将目标分布到不同的特征级别和不同的点?也就是2D引导的多层3D预测
- 如何设计架构?本文遵循 RetinaNet 和 FCOS,每个包含4个共享参数的卷积层和 small heads 用于不同的 targets 预测,回归分支需要较高的解耦程度,即每个子 targets 都设置一个 head
回归目标
在回归分支中,不同于FCOS在2D中的情况(回归每个点到顶部/底部/左侧/右侧的距离,如下图中的 t , b , l , r t,b,l,r t,b,l,r所示),FCOS3D将通常定义的7-DoF回归目标转换为2.5D中心和3D尺寸,其中2.5D中心可以通过相机固有矩阵轻松转换回3D空间。
回归2.5D中心可以进一步减少为回归从中心到特定前景点的偏移
Δ
x
,
Δ
y
\Delta x,\Delta y
Δx,Δy、 以及其相应的深度
d
d
d,对于3D尺寸,预测以下属性:
- w , l , h w,l,h w,l,h:目标的长宽高
- θ \theta θ:偏航角(以重力方向为轴,周期为 π \pi π)
- v x , v y v_x,v_y vx,vy:目标沿x方向和y方向的速度
- C θ C_{\theta} Cθ:即2-bin direction classification,考虑目标具有相反方向的情况,具有相同的 s i n ( θ ) sin(\theta) sin(θ)值
- c c c:即3D Center-ness,3D目标中心ness c。它作为一个软二进制分类器来确定哪些点更靠近中心,并有助于抑制那些远离对象中心的低质量预测
总的来说,分类分支需要输出目标的类别标签和属性标签,而回归分支则需要预测 Δ x , Δ y , d , w , l , h , θ , v x , v y , C θ , c \Delta x,\Delta y,d,w,l,h,\theta,v_x,v_y,C_{\theta},c Δx,Δy,d,w,l,h,θ,vx,vy,Cθ,c这些属性。
损失函数
对于分类分支和不同的回归分支,FCOS3D分别定义其损失,并对其进行加权求和:
-
目标分类,使用Focal Loss,其中
p
p
p是预测框的类概率,遵循原始论文的设置
α
=
0.25
,
γ
=
2
\alpha=0.25,\gamma=2
α=0.25,γ=2
L c l s = − α ( 1 − p ) γ log p L_{c l s}=-\alpha(1-p)^\gamma \log p Lcls=−α(1−p)γlogp - 属性分类,使用softmax分类损失,表示为 L a t t r L_{attr} Lattr
-
回归分支,对
Δ
x
,
Δ
y
,
d
,
w
,
l
,
h
,
θ
,
v
x
,
v
y
\Delta x,\Delta y,d,w,l,h,\theta,v_x,v_y
Δx,Δy,d,w,l,h,θ,vx,vy使用Smooth L1损失函数,对方向分类
C
θ
C_{\theta}
Cθ使用Softmax分类损失并表示为
L
d
i
r
L_{dir}
Ldir,对Centerness
c
c
c使用二元交叉熵(BCE)损失函数并表示为
L
c
t
L_{ct}
Lct
L l o c = ∑ b ∈ ( Δ x , Δ y , d , w , l , h , θ , v x , v y ) SmoothL1 ( Δ b ) L_{l o c}=\sum_{b \in\left(\Delta x, \Delta y, d, w, l, h, \theta, v_x, v_y\right)} \operatorname{SmoothL1}(\Delta b) Lloc=b∈(Δx,Δy,d,w,l,h,θ,vx,vy)∑SmoothL1(Δb) -
最终损失:
L
=
1
N
p
o
s
(
β
c
l
s
L
c
l
s
+
β
a
t
t
r
L
a
t
t
r
+
β
l
o
c
L
l
o
c
+
β
d
i
r
L
d
i
r
+
β
c
t
L
c
t
)
L=\frac{1}{N_{p o s}}\left(\beta_{c l s} L_{c l s}+\beta_{a t t r} L_{a t t r}+\beta_{l o c} L_{l o c}+\beta_{d i r} L_{d i r}+\beta_{c t} L_{c t}\right)
L=Npos1(βclsLcls+βattrLattr+βlocLloc+βdirLdir+βctLct)
推理过程
给定输入图像,通过网络进行推理,获取带有 class scores, attribute scores 和 center-ness 预测结果的 bounding boxes,之后将class score 和 centerness 相乘作为每个预测框的confidence,并在鸟瞰图中进行旋转非最大抑制(NMS),以获得最终结果。
2D引导的多层3D预测
为了训练具有FPN的检测器,我们需要设计一种将目标分配到不同级别特征层的策略,FCOS讨论了两个关键问题:
- 与anchor-based方法相比,如何使anchor-free检测器实现类似的Best Possible Recall(BPR)
- 由地面真值框重叠引起的难以解决的模糊问题
针对第一个问题,FCOS通过FPN的多级预测可以改善BPR,甚至比anchor-based方法获得更好的结果,因此FCOS3D也引入FPN的多级预测
针对第二个问题: - FCOS对于不同级别的特征图匹配不同大小的目标,考虑到2D检测的规模与3D检测需要关注的区域的大小直接一致,FCOS3D借助于3D bounding boxes的8个顶点在平面坐标系下的最大坐标和最小坐标(计算投影的3D边界框的外部矩形来生成2D边界框)来匹配不同层次的feature map,在该分配步骤中仅使用2D检测来过滤无意义的目标,完成目标分配后,FCOS3D的回归目标仅包括3D目标的相关属性
- 对于正样本分配的歧义性问题,即当一个点位于同一要素级别中的多个GT框内时,应将哪个框指定给它?FCOS使用 area-based 方法解决该歧义性问题,即当两个样本都符合要求时选尺寸小的样本;FCOS3D则认为这种方式对大目标不友好,提出了一种新的 dist-based 方案提升了精度,即挑选与中心更近的样本作为回归目标,因为更靠近物体中心的点可以获得更全面和平衡的局部区域特征,从而容易地产生更高质量的预测
- 除了上面的正样本分配方法,FCOS3D还提出了一种基于 3d-center 来确定正样本的方法,即只有和中心点距离小于 1.5 x stride(该级别特征图的步长) 的样本算作正样本
- 对每个回归分支的结果增加一个 scale 变换能涨点,该 scale 参数设置为网络可学习
2D高斯分布的3D中心度
FCOS为抑制远离目标中心的预测目标,增加了center-ness分支:
c
=
min
(
l
∗
,
r
∗
)
max
(
l
∗
,
r
∗
)
×
min
(
t
∗
,
b
∗
)
max
(
t
∗
,
b
∗
)
c=\sqrt{\frac{\min \left(l^*, r^*\right)}{\max \left(l^*, r^*\right)} \times \frac{\min \left(t^*, b^*\right)}{\max \left(t^*, b^*\right)}}
c=max(l∗,r∗)min(l∗,r∗)×max(t∗,b∗)min(t∗,b∗)
由于3D回归目标被更改为基于3D center-based 的范式,所以FCOS3D通过以投影的3D中心为原点的2D高斯分布来定义center-ness,其二维高斯分布简化为:
c
=
e
−
α
(
(
Δ
x
)
2
+
(
Δ
y
)
2
)
c=e^{-\alpha\left((\Delta x)^2+(\Delta y)^2\right)}
c=e−α((Δx)2+(Δy)2)
实验设置
实验数据集:NuScenes
评价指标
-
Average Precision metric(AP),使用地平面上的 2D center 与 GT 的距离 d 作为 threshold 进行匹配,避免使用 3D IoU 作为 threshold 对目标尺寸和朝向敏感的问题,其中
C
\mathbb{C}
C表示所有的类别,
D
=
{
0.5
,
1
,
2
,
4
}
\mathbb{D}=\{0.5,1,2,4\}
D={0.5,1,2,4}表示四个距离阈值:
m A P = 1 ∣ C ∣ ∣ D ∣ ∑ c ∈ C ∑ d ∈ D A P c , d m A P=\frac{1}{|\mathbb{C}||\mathbb{D}|} \sum_{c \in \mathbb{C}} \sum_{d \in \mathbb{D}} A P_{c, d} mAP=∣C∣∣D∣1c∈C∑d∈D∑APc,d -
五种True Positive metrics
- Average Translation Error (ATE): 2d 下的中心距离差距 (m)
- Average Scale Error (ASE): 1-IoU,IoU为对齐 translation 和 orientation 后计算的值
- Average Orientation Error (AOE):smallest yaw angle difference(radians)
- Average Velocity Error (AVE): 速度差异的 L2-Norm (m/s)
- Average Attribute Error (AAE):1−acc,其中 acc 指代属性分类准确度
-
NuScenes Detection Score(DNS),传统的mAP结合了对检测目标的位置、大小和方向的评估,但仍无法捕获该设置中的某些信息(如速度和属性),因此nuScenes提出了一个更全面、解耦但简单的度量,即NDS:
N D S = 1 10 [ 5 m A P + ∑ m T P ∈ T P ( 1 − min ( 1 , m T P ) ) ] N D S=\frac{1}{10}\left[5 m A P+\sum_{m T P \in \mathbb{T} P}(1-\min (1, m T P))\right] NDS=101[5mAP+mTP∈TP∑(1−min(1,mTP))]
源码复现
mmdetection3d算法库及nuScenes数据集的下载、配置可以参考官方博客:基于视觉的 3D 检测,本文不再赘述。
- 执行下面命令开始训练,主要要提前修改数据集路径:
CUDA_VISIBLE_DEVICES=0,1 tools/dist_train.sh configs/fcos3d/fcos3d_r101_caffe_fpn_gn-head_dcn_2x8_1x_nus-mono3d.py 2
- FCOS3D完整的网络结构如下(为了便于观察,去掉了backbone中的layer2-4层):
FCOSMono3D(
(backbone): ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): ResLayer(
(0): Bottleneck(
(conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
// 以下三层省略
(layer2):
(layer3):
(layer4):
)
init_cfg={'type': 'Pretrained', 'checkpoint': 'open-mmlab://detectron2/resnet101_caffe'}
(neck): FPN(
(lateral_convs): ModuleList(
(0): ConvModule(
(conv): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
)
(1): ConvModule(
(conv): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1))
)
(2): ConvModule(
(conv): Conv2d(2048, 256, kernel_size=(1, 1), stride=(1, 1))
)
)
(fpn_convs): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
(1): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
(2): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
(3): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
)
(4): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
)
)
)
init_cfg={'type': 'Xavier', 'layer': 'Conv2d', 'distribution': 'uniform'}
(bbox_head): FCOSMono3DHead(
(loss_cls): FocalLoss()
(loss_bbox): SmoothL1Loss()
(loss_dir): CrossEntropyLoss(avg_non_ignore=False)
(loss_attr): CrossEntropyLoss(avg_non_ignore=False)
(cls_convs): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
(1): ConvModule(
(conv): ModulatedDeformConv2dPack(
(conv_offset): Conv2d(256, 27, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(reg_convs): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
(1): ConvModule(
(conv): ModulatedDeformConv2dPack(
(conv_offset): Conv2d(256, 27, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(conv_cls_prev): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(conv_cls): Conv2d(256, 10, kernel_size=(1, 1), stride=(1, 1))
(conv_reg_prevs): ModuleList(
(0): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(1): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(2): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(3): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(4): None
)
(conv_regs): ModuleList(
(0): Conv2d(256, 2, kernel_size=(1, 1), stride=(1, 1))
(1): Conv2d(256, 1, kernel_size=(1, 1), stride=(1, 1))
(2): Conv2d(256, 3, kernel_size=(1, 1), stride=(1, 1))
(3): Conv2d(256, 1, kernel_size=(1, 1), stride=(1, 1))
(4): Conv2d(256, 2, kernel_size=(1, 1), stride=(1, 1))
)
(conv_dir_cls_prev): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(conv_dir_cls): Conv2d(256, 2, kernel_size=(1, 1), stride=(1, 1))
(conv_attr_prev): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(conv_attr): Conv2d(256, 9, kernel_size=(1, 1), stride=(1, 1))
(conv_centerness_prev): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 64, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(conv_centerness): Conv2d(64, 1, kernel_size=(1, 1), stride=(1, 1))
(scales): ModuleList(
(0): ModuleList(
(0): Scale()
(1): Scale()
(2): Scale()
)
(1): ModuleList(
(0): Scale()
(1): Scale()
(2): Scale()
)
(2): ModuleList(
(0): Scale()
(1): Scale()
(2): Scale()
)
(3): ModuleList(
(0): Scale()
(1): Scale()
(2): Scale()
)
(4): ModuleList(
(0): Scale()
(1): Scale()
(2): Scale()
)
)
(loss_centerness): CrossEntropyLoss(avg_non_ignore=False)
)
)
- 训练结束后,执行以下命令进行测试及可视化:
python tools/test.py configs/fcos3d/fcos3d_r101_caffe_fpn_gn-head_dcn_2x8_1x_nus-mini-mono3d.py work_dirs/fcos3d_r101_caffe_fpn_gn-head_dcn_2x8_1x_nus-mini-mono3d/latest.pth --show --show-dir ./outputs/fcos3d/
结果如下:
可以看到检测到的重叠框非常多,效果很差,分析可知应该是NMS阈值和得分阈值设置过低导致,修改/mmdetection3d/configs/_base_/models/fcos3d.py
中的test_cfg
,将score_thr
设置为0.2:
test_cfg=dict(
use_rotate_nms=True,
nms_across_levels=False,
nms_pre=1000,
nms_thr=0.8,
score_thr=0.2,
min_bbox_size=0,
max_per_img=200))
再次进行测试和可视化,结果如下:
PGD
T. Wang, X. Zhu, J. Pang, and D. Lin. Probabilistic and Geometric Depth: Detecting Objects in Perspective. Proceedings of the 5th Conference on Robot Learning, PMLR 164:1475-1485, 2022.
论文
代码
很有意思的是,这篇PGD的作者是FCOS3D原班人马,可以认为是FCOS3D++。FCOS3D是基于Direct Regression的,而PGD则是Geometry-based,是在FCOS3D的基础上,利用提出的head定制模块对深度回归部分进行了改进。
概述
当前的单目3D检测可以简化为实例深度估计问题:不准确的实例深度阻碍了所有其他3D属性预测,无法提高整体检测性能。先前的方法使用额外繁琐的深度估计模型来补充2D检测器的深度信息,或者直接将深度视为3D定位任务的一个维度来简化框架,但仍然使用简单的方法,以回归的方式从孤立的实例或像素中估计深度。我们观察到,除了每个对象本身,其他对象在图像*存,它们之间的几何关系可能是保证准确估计的有价值的约束。受这些观察的启发,我们提出了概率和几何深度(PGD),该方法联合利用概率深度不确定性和共存对象之间的几何关系,以实现精确的深度估计。具体而言,由于在这种不适定环境中,每个实例的初步深度估计通常是不准确的,因此我们结合了概率表示来捕获估计深度的不确定性。我们首先将深度值划分为一组区间,并通过分布的期望值计算深度,来自分布的top-k置信分数的平均值被视为深度的不确定性。
主要创新点
- PGD结合概率表示来捕获深度估计的不确定性,具体而言,首先将深度值划分为一系列离散的区间,然后通过分布的期望来计算深度值,从分布中得到的top-k的置信度的平均值视作深度的不确定性,如下图(a)所示
- 为了构建几何关系图,PGD构建了一个深度传播图来利用上下文信息促进深度估计。每个实例深度的不确定性为实例深度传播提供了有效指引。利用这一整体机制,可以很容易地利用高置信度确定预测,更重要的是,利用基于图的协同机制可以更精确地预测深度,如下图(b)所示
- 在KITTI 3D汽车检测基准上,PGD在性能和速度方面都显著优于其他工作,如下图(c)所示
深度估计
Oracle使用不同的数据集和指标进行分析,从左到右:KITTI上基于3D IoU的mAP、NuScenes检测分数(NDS)和NuScenes上基于距离的mAP。依次用真值来替换 3D 检测器不同输出结果时最终的检测性能(注意是替换不同 attribute 的 dense prediction map,这样可以将回归目标建模所带来的影响包含在内)。
可以发现,在深度估计的准确率只有当前水平时,其他的回归目标用真值替代并不能带来预期提升,反而有时候甚至会有副作用。而当深度估计准确时,检测性能可以实现质的提升。因此可以推断,纯视觉 3D 检测问题在当前发展阶段几乎可以被归结为一个 instance depth estimation 问题。
因此,PGD一方面建模了深度估计的不确定性,另一方面通过透视几何关系建立这些具有不确定性的检测目标之间的深度传播图,通过全局的信息来增强深度估计的准确度
主要框架结构
PGD在FCOS3D整体框架的基础上,主要关注实例深度估计的难题,首先引入概率深度估计模块来建模不确定性,然后从深度传播图中得到几何深度,最后融合二者得到最终的深度预测值
概率表示的不确定性建模 D P , D L D_P,D_L DP,DL
从这一部分开始,本文将围绕着 概率表示的局部深度估计+基于目标几何关系的深度估计 这两部分进行讨论,会出现大量复杂的数学推理和表示。
对于一阶段检测器,直接深度估计一般是沿着回归分支的一个small head,输出密集的深度图:
D
R
∈
R
H
×
W
D_R \in \mathbb{R}^{H \times W}
DR∈RH×W。本文在此基础上,考虑到深度值在一定范围内是连续的,将深度区间均匀量化为一组离散值,设置等距间隔,将其视为分类任务,离散化网络的输出为:
D
P
=
ω
T
softmax
(
D
P
M
)
D_P=\omega^T \text { softmax }\left(D_{P M}\right)
DP=ωT softmax (DPM)
其中,
ω
\omega
ω为人为设置的间隔点,
D
P
M
D_{PM}
DPM为深度值离散区间分类输出的feature map(这一块我也不太明白,可能不对)。每个孤立实例的局部深度估计为:
D
L
=
σ
(
λ
)
D
R
+
(
1
−
σ
(
λ
)
)
D
P
D_L=\sigma(\lambda) D_R+(1-\sigma(\lambda)) D_P
DL=σ(λ)DR+(1−σ(λ))DP
其中,
λ
\lambda
λ为数据不可知的参数,
σ
\sigma
σ为sigmoid函数。
透视几何体的深度传播 D G D_G DG
利用孤立实例的深度预测
D
L
D_L
DL和不确定性估计的深度置信分数,我们可以进一步基于上下文几何关系构建传播图。考虑典型的驾驶场景:可以利用一般约束,即几乎所有物体都在地面上。针对深度估计问题,我们提出了一种几何深度传播机制,考虑了实例之间的相互依赖性。已知相机的内参矩阵:
P
=
(
f
0
c
u
−
f
b
x
0
f
c
v
−
f
b
y
0
0
1
−
f
b
z
)
P=\left(\begin{array}{cccc} f & 0 & c_u & -f b_x \\ 0 & f & c_v & -f b_y \\ 0 & 0 & 1 & -f b_z \end{array}\right)
P=⎝
⎛f000f0cucv1−fbx−fby−fbz⎠
⎞
其中各参数含义如下:
- f f f:相机焦距,考虑到大多数相机在 u u u轴和 v v v轴上共享相同的焦距,因此这里用单个 f f f表示焦距
- c u , c v c_u,c_v cu,cv:相机在图像中的水平和垂直位置
- b x , b y , b z b_x,b_y,b_z bx,by,bz:相对于参考相机的基线(KITTI中非零,NuScenes为零)
给定相机坐标系下某点的3D位置
x
3
D
=
(
x
,
y
,
z
,
1
)
T
\mathbf{x}^{3 \mathrm{D}}=(x, y, z, 1)^T
x3D=(x,y,z,1)T,可以利用相机内参矩阵
P
P
P,将其投影为图像中的2D位置
x
2
D
=
(
u
′
,
v
′
,
1
)
T
\mathbf{x}^{2 \mathbf{D}}=\left(u^{\prime}, v^{\prime}, 1\right)^T
x2D=(u′,v′,1)T:
d
x
2
D
=
P
x
3
D
d \mathbf{x}_{\mathbf{2 D}}=P \mathbf{x}_{3 \mathrm{D}}
dx2D=Px3D
为了简化结果,将
v
0
v_0
v0替换为
v
+
c
v
v+c_v
v+cv,其中
v
v
v表示目标到地平线的距离(如下图所示,向下为正方向),然后我们得到:
v
d
=
f
(
y
−
b
y
+
c
v
b
z
)
v d=f\left(y-b_y+c_v b_z\right)
vd=f(y−by+cvbz)
u
u
u的关系类似。考虑到所有对象都在地面上的约束,对象的底部中心始终共享相同的
y
y
y(相机坐标中的高度),因此接下来主要考虑
v
v
v的关系。给定两个物体1和2,它们的中心深度之间的关系为:
d
2
=
v
1
v
2
d
1
+
f
v
2
(
y
2
−
y
1
)
≈
v
1
v
2
d
1
+
f
2
v
2
(
h
1
3
D
−
h
2
3
D
)
≜
d
1
→
2
P
d_2=\frac{v_1}{v_2} d_1+\frac{f}{v_2}\left(y_2-y_1\right) \approx \frac{v_1}{v_2} d_1+\frac{f}{2 v_2}\left(h_1^{3 D}-h_2^{3 D}\right) \triangleq d_{1 \rightarrow 2}^P
d2=v2v1d1+v2f(y2−y1)≈v2v1d1+2v2f(h13D−h23D)≜d1→2P
对于一幅图像上的n个目标,可以根据上述公式定义他们之间的几何深度信息:
d
i
G
=
∑
j
=
1
k
s
j
→
i
e
d
j
→
i
P
d_i^G=\sum_{j=1}^k s_{j \rightarrow i}^e d_{j \rightarrow i}^P
diG=j=1∑ksj→iedj→iP
其中,
s
j
→
i
e
\boldsymbol{s}_{j \rightarrow i}^e
sj→ie与目标之间的距离,
k
k
k为选定的与目标
i
i
i置信度
s
j
→
i
e
s_{j→i}^e
sj→ie 最高的目标集合。值得注意的是,
D
G
D_G
DG没有可学习的参数,不参与网络的反向转播过程。
概率和几何深度估计 D D D
网络的深度估计包含两个方面:局部的深度估计
D
L
D_L
DL以及基于目标之间几何关系的深度估计
D
G
D_G
DG,其中
α
∈
R
H
×
W
α∈R^{H×W}
α∈RH×W为可学习参数:
D
=
σ
(
α
)
∘
D
L
+
(
1
−
σ
(
α
)
)
∘
D
G
D=\sigma(\alpha) \circ D_L+(1-\sigma(\alpha)) \circ D_G
D=σ(α)∘DL+(1−σ(α))∘DG
源码复现
训练、测试及可视化同FCOS3D,在此不再赘述。
PGD整体框架中的backbone和neck与FCOS3D类似,但Head有很大改动,这里给出mmdetection3d中关于PGD检测头的配置信息:
(bbox_head): PGDHead(
(loss_cls): FocalLoss()
(loss_bbox): SmoothL1Loss()
(loss_dir): CrossEntropyLoss(avg_non_ignore=False)
(loss_attr): CrossEntropyLoss(avg_non_ignore=False)
(cls_convs): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
(1): ConvModule(
(conv): ModulatedDeformConv2dPack(
(conv_offset): Conv2d(256, 27, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(reg_convs): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
(1): ConvModule(
(conv): ModulatedDeformConv2dPack(
(conv_offset): Conv2d(256, 27, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(conv_cls_prev): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(conv_cls): Conv2d(256, 10, kernel_size=(1, 1), stride=(1, 1))
(conv_reg_prevs): ModuleList(
(0): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(1): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(2): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(3): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(4): None
(5): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
)
(conv_regs): ModuleList(
(0): Conv2d(256, 2, kernel_size=(1, 1), stride=(1, 1))
(1): Conv2d(256, 1, kernel_size=(1, 1), stride=(1, 1))
(2): Conv2d(256, 3, kernel_size=(1, 1), stride=(1, 1))
(3): Conv2d(256, 1, kernel_size=(1, 1), stride=(1, 1))
(4): Conv2d(256, 2, kernel_size=(1, 1), stride=(1, 1))
(5): Conv2d(256, 4, kernel_size=(1, 1), stride=(1, 1))
)
(conv_dir_cls_prev): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(conv_dir_cls): Conv2d(256, 2, kernel_size=(1, 1), stride=(1, 1))
(conv_attr_prev): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(conv_attr): Conv2d(256, 9, kernel_size=(1, 1), stride=(1, 1))
(conv_depth_cls_prev): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(conv_depth_cls): Conv2d(256, 6, kernel_size=(1, 1), stride=(1, 1))
(conv_centerness_prev): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(gn): GroupNorm(32, 64, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
(conv_centerness): Conv2d(64, 1, kernel_size=(1, 1), stride=(1, 1))
(scales): ModuleList(
(0): ModuleList(
(0): Scale()
(1): Scale()
(2): Scale()
(3): Scale()
)
(1): ModuleList(
(0): Scale()
(1): Scale()
(2): Scale()
(3): Scale()
)
(2): ModuleList(
(0): Scale()
(1): Scale()
(2): Scale()
(3): Scale()
)
(3): ModuleList(
(0): Scale()
(1): Scale()
(2): Scale()
(3): Scale()
)
(4): ModuleList(
(0): Scale()
(1): Scale()
(2): Scale()
(3): Scale()
)
)
(loss_centerness): CrossEntropyLoss(avg_non_ignore=False)
(loss_depth): SmoothL1Loss()
(loss_bbox2d): SmoothL1Loss()
(loss_consistency): GIoULoss()
)
Refernece
“3Dfy” A General 2D Detector: 纯视觉 3D 检测再思考
27. FCOS3D - 单阶段 3D 目标检测 (anchor-free)