目标检测实战(一):模型选择、输入输出解析与边界框解码

各位同学,欢迎来到目标检测实战的第一讲。说实话,目标检测这块内容,是我在AI应用开发中花时间最多、踩坑也最多的领域之一。今天咱们就聊聊模型选择、输入输出解析,还有那个让人头疼的边界框解码。

嗯,先说说我个人的感受。刚开始做NNAPI开发时,我天真地以为把模型跑起来就完事了。结果呢?模型选错了,输入输出格式搞反了,边界框解码写错了——那叫一个惨。所以今天的内容,说白了就是帮你避开我当年踩过的坑。

一、模型选择:SSD vs YOLO

做目标检测,首先得选模型。目前移动端最主流的就是SSD和YOLO。我个人的习惯是:如果追求速度,选SSD;如果追求精度,选YOLO。但实际情况要复杂得多。

对比维度 SSD YOLO
推理速度 快(单阶段,无RPN) 较快(但比SSD略慢)
小目标检测 较好(多尺度特征图) 一般(需额外处理)
模型体积 小(MobileNet-SSD约20MB) 大(YOLOv4约250MB)
NNAPI兼容性 好(算子简单) 一般(需自定义算子)

我在项目中遇到过这样的情况:用YOLOv4跑NNAPI,结果发现某些算子不支持,最后不得不切回SSD。所以我的建议是——先确认你的设备支持哪些算子,再选模型。

小技巧: 如果你用的是高通骁龙芯片,优先考虑SSD-MobileNet系列。如果是联发科天玑,YOLO的兼容性反而更好。这是我在实际测试中发现的规律。

二、输入输出解析

模型选好了,接下来就是输入输出。这里我踩过最大的坑就是——输入张量的顺序搞反了。

以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_boxesdetection_scores。前者是边界框坐标,后者是置信度。嗯,这里要注意——坐标是归一化的,范围在0到1之间。

注意: 有些模型输出的坐标是 [ymin, xmin, ymax, xmax],有些是 [x, y, width, height]。我曾经因为搞混了这两种格式,调试了整整两天。一定要先看模型的文档!

三、边界框解码

终于到了最核心的部分——边界框解码。说白了,就是把模型输出的原始数据,转换成我们能用的坐标。

以SSD为例,解码过程是这样的:

  1. 从输出中提取 detection_boxes(形状:[1, num_detections, 4])
  2. 从输出中提取 detection_scores(形状:[1, num_detections])
  3. 根据置信度阈值过滤(比如0.5)
  4. 执行非极大值抑制(NMS)
  5. 将归一化坐标映射回原图尺寸
// 边界框解码核心代码
// 假设原图尺寸是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实现,建议使用现成的库函数
核心要点: 边界框解码的关键在于理解模型输出的格式。SSD输出的是归一化坐标,YOLO输出的是相对于网格的偏移量。两者解码方式完全不同。我建议你在调试时,先用一张已知结果的图片做测试,确保解码正确。

四、知识体系总览

为了让你更直观地理解整个流程,我画了一张图:

目标检测实战流程 模型选择 SSD vs YOLO 速度/精度权衡 输入处理 NCHW/NHWC 归一化/预处理 NNAPI推理 ANeuralNetworks 执行模型 输出解析 detection_boxes + detection_scores 提取原始数据 边界框解码 坐标映射 + NMS + 阈值过滤 数据流向

这张图清晰地展示了整个流程:从模型选择开始,到输入处理、模型推理、输出解析,最后到边界框解码。每一步都有坑,每一步都需要仔细处理。

五、实战避坑指南

最后,分享几个我亲身经历的教训:

  • 模型格式问题: 我曾经从TensorFlow Hub下载了一个SSD模型,结果发现它用的是NHWC格式,而我的NNAPI代码默认是NCHW。调试了整整一天才发现问题。
  • 坐标顺序问题: 有一次我写YOLO的解码代码,把 [x, y, w, h] 当成了 [x1, y1, x2, y2] 来处理。结果检测框全画错了位置。
  • 阈值选择问题: 置信度阈值设得太低,会出现大量误检;设得太高,又会漏检。我一般从0.5开始调,根据实际效果微调。
我的建议: 刚开始做目标检测时,先用一张简单的图片做测试。比如一张白纸上放一个红色方块。这样你能清楚地知道模型输出是否正确。等解码逻辑验证通过后,再换复杂场景。

好了,这一讲的内容就到这里。模型选择、输入输出解析、边界框解码,这三块是目标检测的基础。下一讲我们会深入NNAPI的具体实现,包括如何创建模型、设置输入输出、执行推理等。到时候我会用完整的代码示例来演示。


公众号:蓝海资料掘金营,微信deep3321