# RK3588 YOLOv8 模型输出格式修复记录 ## 问题现象 使用新转换的 RKNN 模型 (`best-rk3588.rknn`) 进行推理时,出现以下异常现象: 1. **所有检测框的置信度分数相同** - 均为 `0.998047` 2. **框坐标异常** - 大量框为全图 `(0,0,768,768)` 或包含 `inf` 值 3. **部分帧无检测** - `valid_count=0`,`global_max_score=0.000580` ### 错误日志示例 ``` [ai_yolo] raw box i=0 cx=inf cy=inf w=inf h=inf score=0.998047 cls=0 [ai_yolo] raw box i=1 cx=inf cy=inf w=inf h=inf score=0.998047 cls=0 [ai_yolo] det: cls=0 score=0.998047 bbox=(0.000000,0.000000,768.000000,768.000000) ``` ## 根本原因 ### 1. RKNN 输出配置错误 (`src/ai_scheduler.cpp`) 原代码存在三个关键问题: #### 问题 A: 使用 INT8 原始输出 ```cpp // 错误代码 outputs[i].want_float = 0; // 获取 INT8 量化后的原始数据 ``` 当 `want_float=0` 时,RKNN 返回 INT8 量化数据,但代码错误地将其当作 FP16 解析。 #### 问题 B: 硬编码输出类型 ```cpp // 错误代码 out.type = RKNN_TENSOR_FLOAT16; // 硬编码为 FP16,与实际数据类型不符 ``` 实际获取的是 INT8 数据(`type=2`),但标记为 FP16(`type=1`),导致后续解析逻辑错误。 #### 问题 C: 缓冲区大小不足 ```cpp // 错误代码 uint32_t out_sz = ctx->output_attrs[j].n_elems * sizeof(uint16_t); // 只分配 2字节/元素 ``` 当 `want_float=1` 时,RKNN 返回 FP32(4字节/元素),但缓冲区只分配了 FP16 大小(2字节/元素),导致数据截断。 ### 2. 数值解释 `0.998047` 是 FP16 值 `0x3BFF` 转换为 FP32 后的结果: - FP16: `0x3BFF` = 0.99951171875 - 由于所有 class scores 被错误解析为相同的 FP16 值,导致所有检测分数相同 ## 解决方法 ### 修复 1: 启用 FP32 输出 (ai_scheduler.cpp:620) ```cpp // 修改前 outputs[i].want_float = 0; // 修改后 outputs[i].want_float = 1; // 请求 FP32 输出,RKNN 自动完成反量化 ``` ### 修复 2: 正确标记输出类型 (ai_scheduler.cpp:647) ```cpp // 修改前 out.type = RKNN_TENSOR_FLOAT16; // 修改后 out.type = RKNN_TENSOR_FLOAT32; // 与 want_float=1 对应 ``` ### 修复 3: 分配正确大小的缓冲区 (ai_scheduler.cpp:220) ```cpp // 修改前 uint32_t out_sz = ctx->output_attrs[j].n_elems * sizeof(uint16_t); // 修改后 uint32_t out_sz = ctx->output_attrs[j].n_elems * sizeof(float); // 4字节/元素 ``` ## 修复后的效果 ### 正常检测输出 ``` [ALARM][info] detections=[{cls=6 score=0.53 bbox=(129,344,427,407)}] [ALARM][info] detections=[{cls=6 score=0.43 bbox=(73,316,382,452)}] [ALARM][info] detections=[{cls=6 score=0.52 bbox=(108,315,299,453)}] ``` ### 关键改进 | 指标 | 修复前 | 修复后 | |------|--------|--------| | 分数分布 | 固定 0.998047 | 0.43 ~ 0.53 变化 | | 坐标范围 | 全图或 inf | 合理的 (x,y,w,h) | | 有效检测 | valid_count=0 或异常 | 稳定的有效检测 | ## 模型转换注意事项 ### PC 端导出 ONNX ```python from ultralytics import YOLO model = YOLO('best.pt') model.export( format='onnx', imgsz=768, opset=12, simplify=True, dynamic=False, ) ``` ### PC 端转换为 RKNN ```python from rknn.api import RKNN rknn = RKNN() rknn.config( target_platform='rk3588', mean_values=[[0.0, 0.0, 0.0]], std_values=[[255.0, 255.0, 255.0]], ) rknn.load_onnx( model='best.onnx', input_size_list=[[1, 3, 768, 768]] # NCHW 格式 ) rknn.build(do_quantization=False) # 先测试 FP16 rknn.export_rknn('best-rk3588.rknn') ``` ## 调试技巧 ### 1. 验证模型输出 ```python from rknnlite.api import RKNNLite import numpy as np rknn = RKNNLite() rknn.load_rknn('best-rk3588.rknn') rknn.init_runtime() input_data = np.ones((1, 3, 768, 768), dtype=np.float32) * 128 outputs = rknn.inference(inputs=[input_data]) print(f"Output shape: {outputs[0].shape}") print(f"Min/Max: {outputs[0].min():.4f} / {outputs[0].max():.4f}") print(f"First 10 values: {outputs[0].flatten()[:10]}") ``` ### 2. 检查输出类型 ```cpp // 在 ai_scheduler.cpp 中添加日志 LogInfo("[ai_scheduler] output[" + std::to_string(i) + "] type=" + std::to_string(ctx->output_attrs[i].type) + " qnt_type=" + std::to_string(ctx->output_attrs[i].qnt_type)); ``` ## 相关文件 - `src/ai_scheduler.cpp` - RKNN 推理核心逻辑 - `plugins/ai_yolo/ai_yolo_node.cpp` - YOLO 后处理 - `include/ai_scheduler.h` - 数据结构定义 ## 参考文档 - [RKNN Toolkit2 文档](https://github.com/rockchip-linux/rknn-toolkit2) - [RK3588 NPU 用户手册](https://www.rock-chips.com/uploads/pdf/RK3588%20NPU%20User%20Manual.pdf) - [YOLOv8 RKNN 部署指南](https://github.com/ultralytics/ultralytics) --- **修复日期**: 2026-02-28 **修复者**: AI Assistant **测试模型**: `models/best-rk3588.rknn`