目标检测实战(一):模型选择、输入输出解析与边界框解码
各位同学,欢迎来到目标检测实战的第一讲。说实话,目标检测这块内容,是我在AI应用开发中花时间最多、踩坑也最多的领域之一。今天咱们就聊聊模型选择、输入输出解析,还有那个让人头疼的边界框解码。
嗯,先说说我个人的感受。刚开始做NNAPI开发时,我天真地以为把模型跑起来就完事了。结果呢?模型选错了,输入输出格式搞反了,边界框解码写错了——那叫一个惨。所以今天的内容,说白了就是帮你避开我当年踩过的坑。
一、模型选择:SSD vs YOLO
做目标检测,首先得选模型。目前移动端最主流的就是SSD和YOLO。我个人的习惯是:如果追求速度,选SSD;如果追求精度,选YOLO。但实际情况要复杂得多。
| 对比维度 | SSD | YOLO |
|---|---|---|
| 推理速度 | 快(单阶段,无RPN) | 较快(但比SSD略慢) |
| 小目标检测 | 较好(多尺度特征图) | 一般(需额外处理) |
| 模型体积 | 小(MobileNet-SSD约20MB) | 大(YOLOv4约250MB) |
| NNAPI兼容性 | 好(算子简单) | 一般(需自定义算子) |
我在项目中遇到过这样的情况:用YOLOv4跑NNAPI,结果发现某些算子不支持,最后不得不切回SSD。所以我的建议是——先确认你的设备支持哪些算子,再选模型。
二、输入输出解析
模型选好了,接下来就是输入输出。这里我踩过最大的坑就是——输入张量的顺序搞反了。
以SSD为例,输入通常是 [1, 3, 300, 300],也就是NCHW格式。但有些模型用的是NHWC。你想想看,如果搞反了,模型输出的结果会是什么?全是乱码。
// 正确的输入预处理
// 假设输入是300x300的RGB图像
float* inputBuffer = new float[1 * 3 * 300 * 300];
// 注意:NNAPI默认使用NCHW
// 如果你从Camera2获取的是NV21格式,需要先转成RGB
// 然后按CHW顺序排列
// 伪代码示例
for (int c = 0; c < 3; c++) {
for (int h = 0; h < 300; h++) {
for (int w = 0; w < 300; w++) {
inputBuffer[c * 300 * 300 + h * 300 + w] =
pixelData[h][w][c] / 255.0f;
}
}
}
输出解析更麻烦。SSD的输出通常有两个:detection_boxes 和 detection_scores。前者是边界框坐标,后者是置信度。嗯,这里要注意——坐标是归一化的,范围在0到1之间。
[ymin, xmin, ymax, xmax],有些是 [x, y, width, height]。我曾经因为搞混了这两种格式,调试了整整两天。一定要先看模型的文档!
三、边界框解码
终于到了最核心的部分——边界框解码。说白了,就是把模型输出的原始数据,转换成我们能用的坐标。
以SSD为例,解码过程是这样的:
- 从输出中提取
detection_boxes(形状:[1, num_detections, 4]) - 从输出中提取
detection_scores(形状:[1, num_detections]) - 根据置信度阈值过滤(比如0.5)
- 执行非极大值抑制(NMS)
- 将归一化坐标映射回原图尺寸
// 边界框解码核心代码
// 假设原图尺寸是640x480
float scaleX = 640.0f;
float scaleY = 480.0f;
// 从模型输出中获取边界框
float* boxes = outputBoxes; // 形状:[1, 100, 4]
float* scores = outputScores; // 形状:[1, 100]
// 过滤低置信度检测
std::vector<Rect> results;
for (int i = 0; i < 100; i++) {
if (scores[i] < 0.5f) continue; // 阈值过滤
// 注意:坐标是 [ymin, xmin, ymax, xmax]
float ymin = boxes[i * 4 + 0] * scaleY;
float xmin = boxes[i * 4 + 1] * scaleX;
float ymax = boxes[i * 4 + 2] * scaleY;
float xmax = boxes[i * 4 + 3] * scaleX;
// 裁剪到图像范围内
xmin = std::max(0.0f, std::min(xmin, scaleX));
ymin = std::max(0.0f, std::min(ymin, scaleY));
xmax = std::max(0.0f, std::min(xmax, scaleX));
ymax = std::max(0.0f, std::min(ymax, scaleY));
results.push_back({xmin, ymin, xmax, ymax, scores[i]});
}
// 执行NMS(非极大值抑制)
// 这里省略NMS实现,建议使用现成的库函数
四、知识体系总览
为了让你更直观地理解整个流程,我画了一张图:
这张图清晰地展示了整个流程:从模型选择开始,到输入处理、模型推理、输出解析,最后到边界框解码。每一步都有坑,每一步都需要仔细处理。
五、实战避坑指南
最后,分享几个我亲身经历的教训:
- 模型格式问题: 我曾经从TensorFlow Hub下载了一个SSD模型,结果发现它用的是NHWC格式,而我的NNAPI代码默认是NCHW。调试了整整一天才发现问题。
- 坐标顺序问题: 有一次我写YOLO的解码代码,把
[x, y, w, h]当成了[x1, y1, x2, y2]来处理。结果检测框全画错了位置。 - 阈值选择问题: 置信度阈值设得太低,会出现大量误检;设得太高,又会漏检。我一般从0.5开始调,根据实际效果微调。
好了,这一讲的内容就到这里。模型选择、输入输出解析、边界框解码,这三块是目标检测的基础。下一讲我们会深入NNAPI的具体实现,包括如何创建模型、设置输入输出、执行推理等。到时候我会用完整的代码示例来演示。