safesight-edge/docs/bugfix_yolov8_fp16.md

250 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# YOLOv8 FP16 数据解析错误修复
## 问题描述
**现象:** 使用 YOLOv8 RKNN 模型(如 `yolov8n-640.rknn`、`best-640.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 详细说明
### 问题现象
使用新转换的模型 `best-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/FP32RKNN 都返回 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<int>(i);
out.size = outputs[i].size;
out.data = reinterpret_cast<const uint8_t*>(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('best-rk3588.rknn')
```
---
## 调试技巧
### 验证模型输出
```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"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