最近在搞一个基于征程6M的3D目标检测项目,模型选型的时候在FCOS3D和CenterNet之间犹豫。看了社区里发布的HEAL参考算法数据,FCOS3D在J6M上的延迟只有1.89ms,NDS有30.23%,相当能打。实际部署下来发现3D检测的量化比2D麻烦不少,主要是深度估计头和角度回归头对精度特别敏感。这篇把FCOS3D在征程6上的完整部署流程和踩的坑记下来。
一、FCOS3D模型结构简析
FCOS3D是基于FCOS的3D目标检测框架,主要改进是把2D的检测头扩展成了3D属性预测头。模型结构:
· Backbone:EfficientNet-B0(社区提供的预训练权重)
· Neck:FPN
· Head:分类头 + 2D框回归头 + 深度估计头 + 角度回归头 + 属性预测头
输入shape是1x3x512x896,注意是512x896不是正方形,这和2D检测的640x640不一样。原因是3D检测需要更大的横向视野来覆盖车道宽度。
二、ONNX导出:3D头的特殊处理
FCOS3D的Detect头比YOLO复杂,除了分类和2D框,还要预测:
· 深度(depth)
· 角度(rotation)
· 三维框尺寸(dimensions)
· 属性(velocity等)
导出ONNX时,这些3D属性的输出节点要单独命名:
```python
torch.onnx.export(
model,
dummy,
'fcos3d.onnx',
opset_version=11,
input_names=['images'],
output_names=['cls_score', 'bbox_pred', 'dir_cls', 'centerness',
'depth', 'rotation', 'dim', 'velo'], # 3D属性单独输出
do_constant_folding=True,
)
```
注意:
dir_cls是角度方向分类(sin/cos分解),rotation是实际角度回归。这两个头在量化时特别敏感,后面会讲。
三、量化校准:3D属性的敏感性
3D检测的量化比2D麻烦,主要是因为:
1. 深度估计头:预测的是绝对深度(米),范围从5米到80米,分布不均匀。近距离目标多,远距离目标少,校准数据的分布直接影响远距离目标的精度。
2. 角度回归头:用的是sin/cos编码,值域[-1, 1],量化后分辨率只有256个等级(int8),角度精度受限制。
3. 三维框尺寸:车辆的尺寸范围比较小(长4-6米,宽1.5-2米,高1.2-1.8米),但不同类别差异大。
我的校准策略:
```yaml
calibration_parameters:
cal_data_dir: ./nuscenes_cal
calibration_type: mix
layer_config:
# 深度估计头升int16
- layer_name: "depth_predictor"
precision_mode: int16
# 角度回归头升int16
- layer_name: "dir_classifier"
precision_mode: int16
- layer_name: "rotation_predictor"
precision_mode: int16
# 分类头int8够用
- layer_name: "cls_score"
precision_mode: int8
```
深度头和角度头升int16后,NDS从27.1%回升到了30.23%,涨了3.1个百分点。延迟从1.6ms涨到了1.89ms,只多了0.29ms,完全可接受。
四、输入尺寸和预处理
FCOS3D的输入是512x896,预处理要注意:
```python
def preprocess(img, target_h=512, target_w=896):
# 原始图像1920x1080,先保持aspect ratio resize
h, w = img.shape[:2]
scale = min(target_h/h, target_w/w)
new_h, new_w = int(h*scale), int(w*scale)
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_CUBIC)
# 然后pad到512x896,右边和下边padding
padded = np.zeros((target_h, target_w, 3), dtype=np.uint8)
padded[:new_h, :new_w] = img
# normalize
padded = padded.astype(np.float32) / 255.0
padded = (padded - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225])
# CHW
return np.transpose(padded, (2, 0, 1)).astype(np.float32)
```
注意: 必须保持aspect ratio resize再pad,不能直接stretch到512x896,否则车辆的纵横比变了,3D框尺寸预测会偏。
五、后处理:从2D框到3D框
FCOS3D的后处理比YOLO复杂得多,需要在板端C++里实现:
```cpp
struct Detection3D {
float x2d, y2d, w2d, h2d; // 2D框
float depth; // 深度(米)
float rotation; // 角度(弧度)
float dim_w, dim_h, dim_l; // 3D框尺寸
float velo_x, velo_z; // 速度(可选)
float score;
int class_id;
};
vector PostProcess3D(
const float* cls_score,
const float* bbox_pred,
const float* dir_cls,
const float* centerness,
const float* depth,
const float* rotation,
const float* dim,
int num_classes,
float score_threshold = 0.3f
) {
vector detections;
for (int i = 0; i < num_anchors; i++) {
// 1. 分类分数 + centerness加权
float max_score = 0;
int cls_id = 0;
for (int c = 0; c < num_classes; c++) {
float s = cls_score[i * num_classes + c] * centerness[i];
if (s > max_score) {
max_score = s;
cls_id = c;
}
}
if (max_score < score_threshold) continue;
// 2. 解码2D框
float x = bbox_pred[i * 4 + 0];
float y = bbox_pred[i * 4 + 1];
float w = bbox_pred[i * 4 + 2];
float h = bbox_pred[i * 4 + 3];
// 3. 解码角度(sin/cos -> 弧度)
float dir_sin = dir_cls[i * 2 + 0];
float dir_cos = dir_cls[i * 2 + 1];
float rot = atan2(dir_sin, dir_cos);
// 4. 解码深度
float d = depth[i];
// 5. 解码3D尺寸
float w3d = dim[i * 3 + 0];
float h3d = dim[i * 3 + 1];
float l3d = dim[i * 3 + 2];
Detection3D det;
det.x2d = x; det.y2d = y; det.w2d = w; det.h2d = h;
det.depth = d;
det.rotation = rot;
det.dim_w = w3d; det.dim_h = h3d; det.dim_l = l3d;
det.score = max_score;
det.class_id = cls_id;
detections.push_back(det);
}
// 6. 3D NMS
return NMS3D(detections, 0.5f);
}
```
3D NMS和2D NMS的区别: 3D NMS要考虑深度方向的IoU。两个框在图像平面上重叠很大,但深度差很远时,不应该被NMS掉。
六、性能数据
FCOS3D在J6M上的实测性能:
配置 延迟 FPS NDS mAP
浮点 - - 30.59% 21.30%
全int8 1.60ms 625 27.12% 18.95%
mix(深度+角度int16) 1.89ms 529 30.23% 20.97%
mix+O3 1.72ms 581 30.23% 20.97%
注意:
1.89ms是单模型推理时间,实际产品里还要加预处理(3ms)和后处理(2ms),端到端延迟大概7ms,帧率约140fps。对于自动驾驶的3D检测来说完全够用。
七、踩坑记录
坑1:角度量化精度不足
角度回归头用int8量化时,sin/cos的值域[-1, 1]被映射到256个离散值,分辨率只有0.0078。对于小角度(比如5度=0.087弧度),量化误差可能达到10%。升int16后分辨率提升到0.00003,精度问题彻底解决。
坑2:深度分布不均匀
NuScenes数据集里80%的目标在0-40米范围内,只有20%在40-80米。校准数据如果不平衡,远距离目标的深度估计会系统性地偏近。我的解决方式是校准数据里按距离分层采样,确保每个距离段(0-20m, 20-40m, 40-60m, 60-80m)都有足够的样本。
坑3:输入尺寸搞错
FCOS3D的输入是512x896,不是640x640。我一开始按照YOLO的习惯导出了640x640的ONNX,结果编译时报input shape mismatch。查了半天才发现是输入尺寸错了。
坑4:后处理太慢
FCOS3D的anchor数量比YOLO多很多(512x896 feature map上每个点都有anchor),后处理里的循环如果写得不优化,CPU侧可能跑10ms以上。我的优化方式:
1. 先过滤掉score
2. 用SIMD加速角度解码(atan2可以用查表近似)
3. 3D NMS用深度分桶,先按深度排序,只比较同一深度桶内的框
优化后后处理从10ms降到了2.3ms。
八、注意事项总结
1. 3D属性头要升int16:深度和角度对量化敏感,int8精度不够。
2. 输入尺寸必须保持aspect ratio:不能直接stretch,否则3D框尺寸预测会偏。
3. 校准数据按距离分层:避免远距离目标精度系统性偏差。
4. 角度用sin/cos编码:不要直接回归弧度,sin/cos更稳定。
5. 后处理要优化:FCOS3D的anchor多,后处理CPU侧可能成为瓶颈。
6. 3D NMS要考虑深度:图像平面上重叠大但深度差远的框不要NMS掉。
7. 延迟看端到端:1.89ms是BPU推理时间,加预处理和后处理约7ms。
8. 深度估计范围要标定:不同相机焦距和安装高度,深度预测要乘标定系数。
