# YOLOv8 FP16 数据解析错误修复 ## 问题描述 **现象:** 使用 YOLOv8 RKNN 模型(如 `object_det_yolov8n_coco_640_rk3588.rknn`、`ppe_det_yolov8_ppe11_768_rk3588.rknn`)时,无法检测到目标,跟踪器显示 `tracks=0`,模型输出为垃圾值。 **错误日志特征:** ``` [ai_yolo] First box: x=125969024.000000, y=38730874880.000000, w=0.000000, h=0.000000 [ai_yolo] ProcessOutputV8 result: valid_count=8400 out of 8400 boxes ``` - 所有 8400 个 anchor 都通过了阈值检测 - 坐标值为超大异常数字 - 检测分数为 `score=5561747627709562880.000000`(溢出值) --- ## 修复历史 ### 修复 #1: FP16 到 FP32 转换 (2025-02-26) **问题**: FP16 数据被错误地当作 FP32 解析 **解决**: 添加 `Fp16ToFp32` 转换函数,单独处理 FP16 分支 **文件**: `plugins/ai_yolo/ai_yolo_node.cpp` ### 修复 #2: RKNN 输出格式配置 (2025-02-28) **问题**: `ai_scheduler` 中 RKNN 输出配置错误导致数据类型混乱 **详细说明见下方** --- ## 修复 #2 详细说明 ### 问题现象 使用新转换的模型 `ppe_det_yolov8_ppe11_768_rk3588.rknn` 时出现: ``` [ai_yolo] raw box i=0 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) ``` - 所有检测分数相同(0.998047) - 坐标包含 `inf` 或全图 - 或完全没有检测(`valid_count=0`) ### 根本原因 `src/ai_scheduler.cpp` 中存在三个配置错误: #### 1. 使用 INT8 原始输出 ```cpp // 错误代码 (line 620) outputs[i].want_float = 0; // 获取 INT8 量化数据 ``` #### 2. 硬编码输出类型 ```cpp // 错误代码 (line 647) out.type = RKNN_TENSOR_FLOAT16; // 硬编码,与实际不符 ``` #### 3. 缓冲区大小不足 ```cpp // 错误代码 (line 220) uint32_t out_sz = ctx->output_attrs[j].n_elems * sizeof(uint16_t); // 仅 2字节/元素 ``` ### 为什么会导致 0.998047 - `0.998047` ≈ FP16 值 `0x3BFF` 转换为 FP32 的结果 - 当 INT8 数据被错误解析为 FP16 时,数值被错误解释 - 由于所有 class scores 被相同方式解析,导致分数统一 ### 解决方法 #### 修改 1: 启用 FP32 输出 ```cpp // src/ai_scheduler.cpp line 620 outputs[i].want_float = 1; // RKNN 自动完成反量化,返回 FP32 ``` #### 修改 2: 正确标记输出类型 ```cpp // src/ai_scheduler.cpp line 647 out.type = RKNN_TENSOR_FLOAT32; // 与 want_float=1 对应 ``` #### 修改 3: 分配正确大小的缓冲区 ```cpp // src/ai_scheduler.cpp line 220 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 或全图 | 合理的 bbox | | 检测 | 异常 | 稳定有效 | --- ## RKNN 输出类型参考 | `want_float` | 返回值类型 | 说明 | |-------------|-----------|------| | 0 | INT8 (原始量化值) | 需要手动反量化: `(val - zp) * scale` | | 1 | FP32 | RKNN 自动反量化,可直接使用 | **注意**: `want_float=1` 时,无论模型内部是 INT8/FP16/FP32,RKNN 都返回 FP32。 --- ## 完整修复代码 (src/ai_scheduler.cpp) ```cpp // 1. 请求 FP32 输出 (line 620) for (uint32_t i = 0; i < ctx->n_output; ++i) { outputs[i].want_float = 1; // 修改: 原来是 0 outputs[i].index = i; // ... } // 2. 标记正确的输出类型 (line 647) for (uint32_t i = 0; i < ctx->n_output; ++i) { auto& out = result.outputs[i]; out.index = static_cast(i); out.size = outputs[i].size; out.data = reinterpret_cast(outputs[i].buf); out.type = RKNN_TENSOR_FLOAT32; // 修改: 原来是 FLOAT16 out.zp = ctx->output_attrs[i].zp; out.scale = ctx->output_attrs[i].scale; // ... } // 3. 分配 FP32 缓冲区 (line 220) for (uint32_t j = 0; j < ctx->n_output; ++j) { // FP32 output: 4 bytes per element uint32_t out_sz = ctx->output_attrs[j].n_elems * sizeof(float); // 修改: 原来是 uint16_t if (out_sz > 0) { ctx->output_buffers[j].resize(out_sz); } else { ctx->output_buffers[j].clear(); } } ``` --- ## 模型转换建议 ### 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]], # 归一化到 [0,1] 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('ppe_det_yolov8_ppe11_768_rk3588.rknn') ``` --- ## 调试技巧 ### 验证模型输出 ```python from rknnlite.api import RKNNLite import numpy as np rknn = RKNNLite() rknn.load_rknn('ppe_det_yolov8_ppe11_768_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"Shape: {outputs[0].shape}") # 应为 (1, 15, 12096) print(f"Min/Max: {outputs[0].min():.4f} / {outputs[0].max():.4f}") # 应无 inf print(f"Values: {outputs[0].flatten()[:10]}") # 应无 0.998047 重复 ``` ### 检查数据类型 ```cpp // 在 ai_yolo_node.cpp 中添加 LogInfo("[ai_yolo] output type=" + std::to_string(outputs[0].type) + " size=" + std::to_string(outputs[0].size)); // 期望: type=0 (FLOAT32), size=725760 (对于 15*12096 FP32) ``` --- ## 相关文件 | 文件 | 说明 | |------|------| | `src/ai_scheduler.cpp` | RKNN 推理核心,本次修复的主要文件 | | `plugins/ai_yolo/ai_yolo_node.cpp` | YOLO 后处理,包含 FP16→FP32 转换 | | `include/ai_scheduler.h` | `InferOutput` 结构定义 | --- ## 参考 - [RKNN Toolkit2 文档](https://github.com/rockchip-linux/rknn-toolkit2) - [RK3588 NPU 快速入门](https://wiki.t-firefly.com/en/ROC-RK3588-PC/rockchip_npu.html) - `rknn_api.h` 中的数据类型定义 --- **修复日期**: 2025-02-28 **文档更新**: 2026-02-28