博客算法工具链征程6上FCOS3D的量化部署:3D检测延迟干到1.89ms的优化细节

征程6上FCOS3D的量化部署:3D检测延迟干到1.89ms的优化细节

默认265282026-08-30
28
0

最近在搞一个基于征程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. 深度估计范围要标定:不同相机焦距和安装高度,深度预测要乘标定系数。

算法工具链
社区征文征程6
评论0
0/600