上一篇文章,我们聊了为什么我选择用 Rust 来为 RKNN 库构建一个安全、独立的地基。当地基打好,我信心满满地将所有模块串联起来,加载了官方示例中那张经典的bus.jpg
,期待着一次完美的推理。
程序没有崩溃,有输出了!我兴奋地将结果绘制出来,然后……我傻眼了。
案发现场:一切混乱的开始
眼前的景象让我大跌眼镜。这哪里是目标检测,这简直是抽象艺术……
图一:最初的错乱图
图注:最初的推理结果,大量的框重叠、错位,甚至把远处的建筑识别成了“truck”
大量的检测框毫无逻辑地堆叠在一起,置信度都低得可怜。很明显,有什么东西从根上就错了。
第一轮审讯:NMS 与置信度的“嫌疑”
我的第一反应是:是不是后处理的参数太松了?我尝试调整 NMS(非极大值抑制)的阈值和置信度阈值,希望能过滤掉这些杂乱的框。经过一番调整,我得到了一张稍微“干净”一点的图,但问题也更清晰了:
图二:初步清理后的结果
提高阈值后,大部分杂乱的框消失了,但暴露了两个核心问题
这张图暴露了两个致命问题:
- 那么大个 Bus 呢? 画面中最显眼的主体——公交车,完全没有被识别出来。
- 诡异的检测框:中间那个黑衣人,检测框从半个脑袋一直延伸到了脚底,明显是错位的。
我当时陷入了一个错误的猜想,但这个猜想推动了调查的进行:“是不是我的 NMS 算法和官方的有出入,导致它抑制错了框?比如,其实模型已经预测出了很多个完美的框,但我的 NMS 算法选了一个最差的留了下来?”
案情转折:一个无法忽视的线索
为了验证这个猜想,我继续调整代码,并设法让公交车的框出现了。但结果却让我更加困惑:
图三:公交车出现,但身份成谜
图注:公交车终于被检测到了,但它的身份却是“truck”,且置信度低得可怜。
1 | --- 检测结果 (4 个) --- |
这个结果让我瞬间否定了之前对 NMS 的怀疑。NMS 只会抑制或保留检测框,它不可能改变一个物体的类别。公交车被识别成了卡车,这绝对不是后处理能干出来的事!
我当时就断定:一定是输入模型的图像就有问题!
这个想法如同一道闪电,让我瞬间跳出了后-处理的思维定势,开始审视整个数据流的源头——图像预处理。
最终审判:揭开“黑盒”的真相
我决定做一个对照实验来最终确认我的猜想。我用 Python 和 OpenCV(被认为是“黄金标准”)生成了一份预处理好的“完美”输入数据,然后让我的 Rust 程序去加载这份数据进行推理。
结果,程序在调用后处理函数后,就“沉默”了,一个检测结果都没打印出来。
这意味着什么?这意味着,即使喂给模型的数据在几何层面(缩放、灰边)是完美的,模型推理出的所有置信度分数都低得可怜,全部都被我的阈值过滤掉了。
案件的真相至此水落石出。问题不在于后处理,也不在于image-rs
库的几何变换,而在于那个我一直忽略的、也是唯一的“黑盒”:RKNN 驱动内部,从 UINT8
到模型实际需要的 INT8
类型的隐式转换。
我一直天真地以为,只要把[0, 255]
的u8
像素喂给驱动,它就能“自动”转换成模型需要的i8
数据。事实证明,这个“自动转换”的行为和我的预期完全不符。
胜利时刻:放弃幻想,掌控一切
在追求极致性能和确定性的嵌入式 AI 领域,我们不能信任任何“黑盒”。我必须亲手实现这个转换,确保送入 NPU 的数据在数值层面是 100%正确的。
正确的流程应该是:u8 [0, 255]
-> 归一化为 f32 [0.0, 1.0]
-> 再用模型自身的 scale
和 zp
参数量化为 i8 [-128, 127]
我立刻重写了我的预处理函数,将归一化和手动量化这两个关键步骤加了进去。
1 | // rkyolo-core/src/lib.rs (重构后的位置) |
当我用这套全新的预处理流程再次运行bus.jpg
时——“王者归来”。
(图四:最终的正确结果)
图注:在修正了数值问题后,公交车被高置信度地正确识别,所有可见的人物也都被精准框出。
复盘总结
这次艰难的调试经历,教会了我三件事:
- 数据是第一原则:当代码逻辑看似无懈可击时,首先要怀疑数据的“纯净度”,尤其是在数值层面。
- 别信“自动挡”:在底层交互中,任何看似方便的“黑盒”都可能隐藏着与你预期不符的行为。掌控每一个转换环节,才能获得确定性的结果。
- 调试是一门科学:通过观察现象、大胆假设、设计实验、层层排除,再复杂的 Bug 也终将水落石出。
此战之后,我的项目不仅走上了正轨,更重要的是,我真正理解了“模型部署”这四个字的重量。我们不再是 API 的调用者,而是能够深入数值细节,确保数据在整个流水线中正确流动的系统构建者。