博客算法工具链程序显示成功,检测框却消失: J6 上排查 NV12 输入

程序显示成功,检测框却消失: J6 上排查 NV12 输入

Sam Hui8小时前
1
0

同一个 YOLOv5s 模型,同样十张图片,正确输入时得到 51 个检测框。只把输入数据复制到内存的方式改错,十次运行依然全部返回成功,检测框却一个都没有了。

这次实验没有改模型,也没有访问非法地址。问题出在文件每行有 672 字节,当前 SDK 默认分配的输入缓冲每行却占 704 字节。直接把整个文件连续复制进去,文件长度检查能通过,模型读取到的图片却已经错行。

在 Codex 协助下,我先用官方工具建立真实 J6E 检测基线,再分别改变 U/V 顺序和行组织方式,比较接口返回、原始输出和最终检测框。这个过程让我更明确地看到:输入文件能读完、模型任务能执行,并不足以确认送进去的是那张图片。

一、先把正确图片送进模型

本轮使用官方 OE 3.9.1 优化模型包中的 YOLOv5s,输入大小 672×672,编译为 nash-e 单核 HBM。实际开发板为 J6E MatrixB,运行库为 UCP 3.15.8、HBRT 4.11.11。

SDK 的 50 张 COCO 示例图片按文件名提前固定拆分:前 40 张用于校准,后 10 张用于这次测试,图片内容哈希去重。模型采用 KL 校准、O2 编译;后处理阈值 0.4、NMS 阈值 0.45。

图片处理沿用 SDK 示例:保持宽高比缩放,在 672×672 画布中央补边,填充值 127,然后从 RGB 转成 NV12。NV12 把亮度放在 Y 平面,把色度 U、V 交错放在另一个平面。

这份 HBM 对外提供两个输入:

输入

类型与有效 shape

文件中的有效字节

Y

UINT8,[1,672,672,1]

451584

UV

UINT8,[1,336,336,2]

225792

我先用官方 hrt_model_exec 执行一张图,再用自己的 UCP 执行器运行同样两个输入文件。三个检测头的有效 INT32 原始输出逐字节一致。此后跑完十张基线,并核对自有后处理与 SDK 的检测框,十张的框数、坐标和分数逐数值一致。

完整 JPEG 入口随后也在板上执行了十张图片。生成的 NV12 与事先保存的 SDK 参考一致,三个原始输出与既有板端基线一致。这样,后面的故障对照就有了一条明确的正确路径。

二、第一个错误:U 和 V 放反了

NV12 的色度字节按照 U,V,U,V… 排列。把每对 U/V 对调后,文件大小完全不变,平面大小和 dtype 也不变,但颜色的含义已经改变。

我保留 Y 平面,只交换 UV 文件中的相邻字节;模型、内存组织和后处理全部保持不变。

十张错误输入全部成功执行,但三个检测头的 raw 输出全部变化。原本 51 个检测框变为 45 个;以正确输入结果为参照,类别相同且 IoU≥0.5 的一对一匹配只剩 39 个。

猫图是一个很直观的例子。正确输入得到一个猫框;U/V 交换后,不只是分数发生变化,还多出了另一个猫框。成功状态没有告诉我颜色顺序错了。

三、第二个错误:文件是连续的,缓冲区却有行间距

这里的 stride 可以理解成“走到下一行,要跨过多少字节”。它与图片一行的有效内容长度有关,但二者不一定相等。

这份模型查询到的输入 stride 含动态值,alignedByteSize 也是 -1。官方工具会补全这些值,默认按 64 字节对齐。对当前 672 宽的输入,补全后的实际属性是:

输入

使用的 stride

分配字节

Y

(473088,704,1,1)

473088

UV

(236544,704,2,1)

236544

704 是这次采用官方默认补全规则得到的行间距,并非所有 NV12 模型都固定使用这个数。

正确复制 Y 平面时,文件中的第 row 行从 row×672 开始;缓冲区里的第 row 行从 row×704 开始。两行之间多出的 32 字节是当前缓冲的对齐间隔。
这里假定 file_bytes 是已读入 Y 文件的 std::vector<std::uint8_t>,并已检查长度及缓冲容量。UV 平面也有相同的 704 字节行间距,只是有效行数为 336。实际执行器按照查询并补全后的各维 stride 计算元素地址,上面这段代码只展示本轮输入的行复制关系。

如果直接这样写:

分配的缓冲比文件大,复制并不会越界。但模型按 704 字节跨到下一行,读到的是原文件中偏移了 32 字节的位置。越往后,内容错位越明显。

我将这种错误复制放进独立故障版本,其余代码与正确执行器相同;内存仍合法分配并清零。十张运行全部退出 0,三个检测头全部变化,最终十张图都没有得到超过阈值的检测框。

四、两种错误放在一起看

输入方式

成功退出

raw 输出变化

检测框总数

与正确输入匹配的框

正确输入

10/10

基线

51

51

U/V 交换

10/10

30/30 个检测头

45

39

紧密文件直接复制到 704 stride 缓冲

10/10

30/30 个检测头

0

0

匹配采用同一条贪心规则:先按参考框分数从高到低处理,再从尚未匹配的同类别预测框里选 IoU 最大者,IoU≥0.5 才计入;每个预测框只用一次。这张表检查的是输入错误如何改变同一模型的结果,没有使用真实目标标注来计算 mAP。

alt text

图1|三个版本使用同一原图和同一 HBM。错误行复制版本没有超过阈值的框,图片仍然显示正常,因为可视化背景使用的是原 JPEG;模型实际读取的输入缓冲已经错行。

故障后,我重新使用正确执行器,补跑三张正常图片,每张重复两次。三个检测头的原始输出都恢复为基线。这一步用于确认差异随输入组织方式出现和消失。

五、我现在会先检查哪些东西?

这次排错后,我把输入核对放到最终效果图之前。

先确认模型真正有几个输入,以及每个输入的 dtype、有效 shape 和 stride。遇到动态值时,按当前 SDK 支持的规则补全,不把 -1 当作可直接分配的字节数。

随后核对文件代表什么:RGB 还是 BGR,NV12 还是其他排列,Y 和 UV 是否拆分。文件长度正确只能说明数量相符,不能证明颜色和布局含义相符。

再检查文件到 SDK 缓冲区的复制。紧密存储文件需要按实际 stride 写入,内存分配大小和有效数据大小分别记录。接入阶段可以导出实际输入,与官方路径逐字节比较。

最后看 raw 输出和统一后处理后的检测结果。先保持模型、阈值、NMS 和坐标恢复不变,才能把变化定位到输入这一层。

这些检查已经放进本轮固定模型执行器和 JPEG 工具。单图入口可以直接接收新 JPEG,输出原始数据、JSON 和标注图;故障代码则单独保留,用于复现实验,不混进正常入口。

当前运行库的 allocator ERROR 日志仍然存在,原因未确认;本文保留日志,并用官方输出对照和故障后的恢复结果检查执行一致性。

这轮最有价值的现象,我认为,是两个错误都绕过了“任务是否成功”的检查。一个改错颜色,一个改错行组织,程序照样结束。把输入的含义和内存布局核对清楚,才知道模型到底看到了什么。

参考资料

  • OE 3.9.1:samples/onnx_ptq/examples/detection/yolov5x/preprocess.py 和 postprocess.py。
  • OE 3.9.1:hrt_model_exec/src/util/tensor_util.cpp 中动态 stride 补全和内存分配逻辑。
  • 本轮 J6E 原始输入、张量属性、运行日志和检测框 JSON。

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