RDK X5 葡萄检测模型量化、部署与推理
本页是一条可复现的板端链路:准备数据与环境 → 修改输出头并导出 ONNX → 生成校准数据 → 量化并传输
.bin→ 在 RDK X5 上做单张推理 → 核对后处理和实验边界。
原始内容来自本地实验记录;命令中的路径、IP、文件名和模型版本都属于当次环境事实,迁移到其他设备前必须重新核对。
1. 量化前准备
1.1 网络连接与 SSH
ping 192.168.0.150
ssh oyz@192.168.0.150
ip addr show ens33
1.2 从 Windows 传输数据到 Ubuntu
scp "E:\lianghua\grapes_openverse_20260326.zip" oyz@192.168.0.150:/home/oyz/
传输完成后,再登录检查:
ssh oyz@192.168.0.150
1.3 解压
unzip 文件名.zip -d 目标文件夹
1.4 替换数据集目录
rm -rf /home/oyz/rdkx5_dataset/raw_images
mv /home/oyz/raw_images /home/oyz/rdkx5_dataset/
这里本质上是把外部的新文件夹直接替换掉原有文件夹。
相关命令含义
mv= 移动 / 重命名cp= 复制rm= 删除(危险⚠️)
1.5 检查文件是否存在
ls -lh ~/rdkx5_models/grapes.pt
ls ~/rdkx5_dataset/raw_images/ | wc -l
这里后来发现:图片目录外面还套了一层 images。部署前应先用 find 或 ls 确认实际目录层级。
2. 进入 Docker 与环境检查
进入 Docker 后,查看 ultralytics 的实际安装路径:
python3 -c "import ultralytics; import os; print(os.path.dirname(ultralytics.__file__))"
实际输出:
/usr/local/lib/python3.10/dist-packages/ultralytics
这一步很关键:后面要改 ultralytics/nn/modules/head.py,必须确认修改的是当前导出时实际使用的环境文件。
3. Shell heredoc 与 EOF
EOF 是 End Of File 的缩写,经常出现在这种写法里:
python3 << EOF
print("hello")
EOF
或者:
cat > test_bin.py << 'EOF'
print("hello")
EOF
在 shell 里,这表示“这一段输入到这里结束”。
4. 导出 ONNX 与输出头修改
4.1 导出时出现的现象
导出 ONNX 时出现过这样的结果:
✅ ONNX 导出成功!❌ 错误:未找到 ONNX 文件但实际上日志里已经写明:
ONNX: export success ✅ 7.5s, saved as '/models/grapes.onnx'
说明:
- ONNX 实际已经导出成功
- 报错并不是导出失败
- 更可能是脚本最后检查的路径写错了。
4.2 什么是“输出头”
这里做的事情是:
把 Ultralytics 默认的 YOLO 检测头输出格式,改成 RDK X5 更容易处理的格式。
本质上是在修改 Detect.forward() 的输出方式。
4.3 为什么要改
默认导出的 YOLOv11 ONNX 往往只有 1 个合并输出,例如:
output shape(s) (1,5,21504)
这个大张量里混合了:
- 框回归
- 类别分数
- 多尺度信息 但 RDK X5 后处理更希望拿到:
- 6 个分离输出
- 通常是 3 个 bbox 输出 + 3 个 cls 输出
- 便于板端后处理解析
所以教程要求改
ultralytics/nn/modules/head.py。
4.4 默认输出和目标输出
原来默认输出
- 1 个合并输出
- 类似:
[1,5,21504]
修改后想要的输出
- 6 个分离输出
- 每个输出对应不同尺度 / 不同内容
- 更便于板端分开处理 bbox 和 cls
如果
head.py没改成功,就会出现:
print(len(model.graph.output)) # 输出:1(错误,应为6)
4.5 不修改会导致什么问题
典型症状就是: 模型能跑,但是没有检测框。 根本原因不是模型坏了,而是:
- 输出格式不匹配
- 输出布局不匹配
- 板端后处理按 6 个输出去解析
- 实际却给的是 1 个合并输出 于是推理虽然成功,但后处理读错了,最后看起来就是“没有检测框”。
4.6 如何检查修改是否成功
检查 1:head.py 是否真的改到了
python3 -c "
import ultralytics, os
head_path = os.path.join(os.path.dirname(ultralytics.__file__), 'nn/modules/head.py')
with open(head_path) as f:
content = f.read()
if 'permute(0, 2, 3, 1)' in content:
print('✅ 包含permute操作')
else:
print('❌ 缺少permute操作')
"
检查 2:导出的 ONNX 是否有 6 个输出
python3 << EOF
import onnx
model = onnx.load('/models/grapes.onnx')
print("输出数量:", len(model.graph.output))
for i, out in enumerate(model.graph.output):
print(i, out.name)
EOF
如果仍然只有 1 个输出,说明修改没有真正生效。
5. 校准数据准备
5.1 修改 prepare_calibration.py 的默认路径
因为图片最终确认是在:
/data/raw_images/images
所以需要把脚本里的默认路径改掉。 直接用:
sed -i "s|'/data/raw_images'|'/data/raw_images/images'|g" prepare_calibration.py
它的作用是把脚本里这行:
src_dir = sys.argv[1] if len(sys.argv) > 1 else '/data/raw_images'
替换成:
src_dir = sys.argv[1] if len(sys.argv) > 1 else '/data/raw_images/images'
然后检查:
grep "src_dir =" prepare_calibration.py
5.2 prepare_calibration.py 完整脚本
#!/usr/bin/env python3
import cv2
import numpy as np
import os
from pathlib import Path
import random
def prepare_calibration_data(
src_dir: str,
dst_dir: str,
input_size: int = 1024,
num_samples: int = 100,
seed: int = 42
):
os.makedirs(dst_dir, exist_ok=True)
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.webp']
image_files = []
for ext in image_extensions:
image_files.extend(Path(src_dir).glob(f'*{ext}'))
image_files.extend(Path(src_dir).glob(f'*{ext.upper()}'))
print(f"📁 找到{len(image_files)} 张图片")
if len(image_files) == 0:
print(f"❌ 错误:在{src_dir} 中没有找到图片!")
return 0
if len(image_files) < num_samples:
print(f"⚠️ 警告:图片数量({len(image_files)})少于需求({num_samples})")
num_samples = len(image_files)
random.seed(seed)
random.shuffle(image_files)
image_files = image_files[:num_samples]
print(f"🔄 准备{num_samples} 张校准图片...")
print(f" 输入尺寸:{input_size}x{input_size}")
print(f" 输出格式: RGB, float32, NCHW")
success_count = 0
fail_count = 0
for i, img_path in enumerate(image_files):
try:
img = cv2.imread(str(img_path))
if img is None:
print(f"⚠️ 跳过无法读取:{img_path.name}")
fail_count += 1
continue
h, w = img.shape[:2]
scale = min(input_size / h, input_size / w)
new_h, new_w = int(h * scale), int(w * scale)
resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
canvas = np.full((input_size, input_size, 3), 114, dtype=np.uint8)
y_offset = (input_size - new_h) // 2
x_offset = (input_size - new_w) // 2
canvas[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = resized
rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)
rgb_float = rgb.astype(np.float32)
rgb_nchw = rgb_float.transpose(2, 0, 1)
output_name = f'{success_count:06d}.f32'
output_path = os.path.join(dst_dir, output_name)
rgb_nchw.tofile(output_path)
success_count += 1
if (success_count) % 20 == 0 or success_count == num_samples:
print(f" 已处理{success_count}/{num_samples}")
except Exception as e:
print(f"❌ 处理失败{img_path.name}:{e}")
fail_count += 1
continue
print(f"\n✅ 校准数据准备完成!")
print(f" 成功:{success_count} 个")
print(f" 失败:{fail_count} 个")
print(f" 保存到:{dst_dir}")
file_size_mb = input_size * input_size * 3 * 4 / 1024 / 1024
total_size_mb = file_size_mb * success_count
print(f" 单文件大小:{file_size_mb:.2f} MB")
print(f" 总大小:{total_size_mb:.2f} MB")
return success_count
if __name__ == '__main__':
import sys
src_dir = sys.argv[1] if len(sys.argv) > 1 else '/data/raw_images/images'
dst_dir = sys.argv[2] if len(sys.argv) > 2 else '/data/calibration_f32'
input_size = int(sys.argv[3]) if len(sys.argv) > 3 else 1024
num_samples = int(sys.argv[4]) if len(sys.argv) > 4 else 100
count = prepare_calibration_data(src_dir, dst_dir, input_size, num_samples)
if count == 0:
print("\n❌ 校准数据准备失败,请检查图片路径")
sys.exit(1)
print(f"\n下一步: 创建YAML配置文件并执行量化")
5.3 校准数据成功输出(跑通结果保留)
执行:
python3 prepare_calibration.py
实际输出:
📁 找到 120 张图片
🔄 准备 100 张校准图片...
输入尺寸: 1024x1024
输出格式: RGB, float32, NCHW
已处理 20/100
已处理 40/100
已处理 60/100
已处理 80/100
已处理 100/100
✅ 校准数据准备完成!
成功: 100 个
失败: 0 个
保存到: /data/calibration_f32
单文件大小: 12.00 MB
总大小: 1200.00 MB
下一步: 创建YAML配置文件并执行量化
这一步已在当次环境中跑通;迁移到其他设备时仍需重新验证。
6. 量化输出文件
量化完成后得到的文件列表如下:
-rw-r--r-- 1 root root 4.1M Mar 27 21:04 grapes_detect_bayese_1024x1024_nv12.bin
-rw-r--r-- 1 root root 11M Mar 27 20:50 grapes_detect_bayese_1024x1024_nv12_calibrated_model.onnx
-rw-r--r-- 1 root root 10M Mar 27 20:05 grapes_detect_bayese_1024x1024_nv12_optimized_float_model.onnx
-rw-r--r-- 1 root root 10M Mar 27 20:05 grapes_detect_bayese_1024x1024_nv12_original_float_model.onnx
-rw-r--r-- 1 root root 89K Mar 27 21:04 grapes_detect_bayese_1024x1024_nv12_quant_info.json
-rw-r--r-- 1 root root 2.8M Mar 27 20:50 grapes_detect_bayese_1024x1024_nv12_quantized_model.onnx
-rw-r--r-- 1 root root 2.2M Mar 27 21:04 torch_jit_subgraph_0.html
-rw-r--r-- 1 root root 4.0K Mar 27 21:04 torch_jit_subgraph_0.json
这说明 .bin 和配套量化产物都已经生成。
7. hrt_model_exec model_info 结果
执行:
hrt_model_exec model_info --model_file /data/output_grapes_optimized/grapes_detect_bayese_1024x1024_nv12.bin
7.1 输入信息
input[0]:
name: images
input source: HB_DNN_INPUT_FROM_PYRAMID
valid shape: (1,3,1024,1024,)
aligned shape: (1,3,1024,1024,)
aligned byte size: 1572864
tensor type: HB_DNN_IMG_TYPE_NV12
tensor layout: HB_DNN_LAYOUT_NCHW
quanti type: NONE
stride: (0,0,0,0,)
7.2 输出信息
output[0]:
name: output0
valid shape: (1,128,128,64,)
aligned shape: (1,128,128,64,)
aligned byte size: 4194304
tensor type: HB_DNN_TENSOR_TYPE_F32
tensor layout: HB_DNN_LAYOUT_NHWC
quanti type: NONE
stride: (4194304,32768,256,4,)
output[1]:
name: 469
valid shape: (1,64,64,64,)
aligned shape: (1,64,64,64,)
aligned byte size: 1048576
tensor type: HB_DNN_TENSOR_TYPE_F32
tensor layout: HB_DNN_LAYOUT_NHWC
quanti type: NONE
stride: (1048576,16384,256,4,)
output[2]:
name: 477
valid shape: (1,32,32,64,)
aligned shape: (1,32,32,64,)
aligned byte size: 262144
tensor type: HB_DNN_TENSOR_TYPE_F32
tensor layout: HB_DNN_LAYOUT_NHWC
quanti type: NONE
stride: (262144,8192,256,4,)
output[3]:
name: 491
valid shape: (1,128,128,1,)
aligned shape: (1,128,128,1,)
aligned byte size: 65536
tensor type: HB_DNN_TENSOR_TYPE_F32
tensor layout: HB_DNN_LAYOUT_NCHW
quanti type: NONE
stride: (65536,512,4,4,)
output[4]:
name: 505
valid shape: (1,64,64,1,)
aligned shape: (1,64,64,1,)
aligned byte size: 16384
tensor type: HB_DNN_TENSOR_TYPE_F32
tensor layout: HB_DNN_LAYOUT_NCHW
quanti type: NONE
stride: (16384,256,4,4,)
output[5]:
name: 519
valid shape: (1,32,32,1,)
aligned shape: (1,32,32,1,)
aligned byte size: 4096
tensor type: HB_DNN_TENSOR_TYPE_F32
tensor layout: HB_DNN_LAYOUT_NCHW
quanti type: NONE
stride: (4096,128,4,4,)
这一步确认了:
- 输入确实是
NV12 - 输出确实是 6 个
- 前 3 个输出更像 bbox 分支
- 后 3 个输出更像 cls 分支
- 同时暴露了一个后处理中的重点:
bbox 输出是
NHWC,而 cls 输出的 layout 标记是NCHW;后处理必须按实际 shape 与 layout 解码。
8. .bin 文件传输与模型放置
最终使用 MobaXterm 取出 .bin 文件。
读取完整路径可以用:
readlink -f 文件名
在板端创建 models 目录:
mkdir -p models
ls -lh models/
9. 板端 Python 推理脚本:test_bin.py
9.1 test_bin.py 代码
from hobot_dnn import pyeasy_dnn as dnn
model_path = "/home/sunrise/models/grapes_detect_bayese_1024x1024_nv12.bin"
models = dnn.load(model_path)
model = models[0]
print("加载成功")
print("模型名:", model.name)
print("\n输入信息:")
for i, x in enumerate(model.inputs):
print(i, x.name, x.properties.shape, x.properties.dtype, x.properties.layout, x.properties.tensor_type)
print("\n输出信息:")
for i, x in enumerate(model.outputs):
print(i, x.name, x.properties.shape, x.properties.dtype, x.properties.layout, x.properties.tensor_type)
注意
正确导入方式是:
from hobot_dnn import pyeasy_dnn as dnn
不是:
import hobot_dnn as dnn
9.2 test_bin.py 成功输出(跑通结果保留)
sunrise@ubuntu:~$ python3 test_bin.py
[BPU_PLAT]BPU Platform Version(1.3.6)! soc info(x5)
[HBRT] set log level as 0. version = 3.15.55.0
[DNN] Runtime version = 1.24.5_(3.15.55 HBRT)
[A][DNN][packed_model.cpp:247][Model](2026-03-28,13:47:28.654.644) [HorizonRT] The model builder version = 1.24.3
加载成功
模型名: grapes_detect_bayese_1024x1024_nv12
输入信息:
0 images (1, 3, 1024, 1024) uint8 NCHW NV12
输出信息:
0 output0 (1, 128, 128, 64) float32 NHWC float32
1 469 (1, 64, 64, 64) float32 NHWC float32
2 477 (1, 32, 32, 64) float32 NHWC float32
3 491 (1, 128, 128, 1) float32 NCHW float32
4 505 (1, 64, 64, 1) float32 NCHW float32
5 519 (1, 32, 32, 1) float32 NCHW float32
sunrise@ubuntu:~$
到这里,只能确认模型部署链路已在该环境中完成;仍需用测试集核对精度。
10. 板端单张图 forward:第一版 run_once.py
接下来不是再折腾 .bin,而是:
第一步:喂一张图片跑 forward
先验证推理能不能真正输出张量。
第二步:把 6 个输出解码成检测框
也就是后处理:
- 提取类别分数
- 解码 bbox
- 还原到原图坐标
- NMS
- 画框
10.1 第一版 run_once.py(只做 forward,不做可视化)
from hobot_dnn import pyeasy_dnn as dnn
import cv2
import numpy as np
model_path = "/home/sunrise/models/grapes_detect_bayese_1024x1024_nv12.bin"
image_path = "/home/sunrise/test.jpg"
def bgr_to_nv12_opencv(image):
h, w = image.shape[:2]
yuv420p = cv2.cvtColor(image, cv2.COLOR_BGR2YUV_I420)
y_size = h * w
uv_size = y_size // 4
y = yuv420p[:y_size]
u = yuv420p[y_size:y_size + uv_size]
v = yuv420p[y_size + uv_size:]
uv = np.empty((uv_size * 2,), dtype=np.uint8)
uv[0::2] = u
uv[1::2] = v
nv12 = np.concatenate([y, uv])
return nv12
models = dnn.load(model_path)
model = models[0]
img = cv2.imread(image_path)
if img is None:
raise ValueError(f"读取图片失败:{image_path}")
img = cv2.resize(img, (1024, 1024))
nv12 = bgr_to_nv12_opencv(img)
outputs = model.forward(nv12)
print("推理成功,输出数量:", len(outputs))
for i, out in enumerate(outputs):
arr = out.buffer
print(f"output[{i}] shape =", np.array(arr).shape)
10.2 为什么“没有现象”
这版脚本只做了:
- 读图并送进模型
- 打印输出张量的 shape 它还没有做:
- 解码 bbox
- 计算得分
- NMS
- 画框
- 保存结果图 所以“没有现象”并不是因为终端运行,而是因为这版脚本本来就没有可视化逻辑。 但这一步其实已经说明:
- 图片读到了
.bin跑通了forward()成功了- 6 个输出都出来了 也就是:部署成功后的第一阶段验证已经通过。
11. 板端完整后处理版:run_once.py
后面开始扩展 run_once.py,实现:
- 读图
- Letterbox
- BGR 转 NV12
- BPU 推理
- 后处理
- 保存结果图
result.jpg
11.1 run_once.py 完整版
from hobot_dnn import pyeasy_dnn as dnn
import cv2
import numpy as np
import time
MODEL_PATH = "/home/sunrise/models/grapes_detect_bayese_1024x1024_nv12.bin"
IMAGE_PATH = "/home/sunrise/test.jpg"
SAVE_PATH = "/home/sunrise/result.jpg"
INPUT_SIZE = 1024
CONF_THRES = 0.25
IOU_THRES = 0.45
REG_MAX = 16
STRIDES = [8, 16, 32]
NUM_CLASSES = 1
def sigmoid(x):
return 1.0 / (1.0 + np.exp(-x))
def softmax(x, axis=-1):
x = x - np.max(x, axis=axis, keepdims=True)
e = np.exp(x)
return e / np.sum(e, axis=axis, keepdims=True)
def bgr_to_nv12_opencv(image):
h, w = image.shape[:2]
yuv420p = cv2.cvtColor(image, cv2.COLOR_BGR2YUV_I420).reshape(-1)
y_size = h * w
uv_size = y_size // 4
y = yuv420p[:y_size]
u = yuv420p[y_size:y_size + uv_size]
v = yuv420p[y_size + uv_size:y_size + uv_size * 2]
uv = np.empty((uv_size * 2,), dtype=np.uint8)
uv[0::2] = u
uv[1::2] = v
return np.concatenate([y, uv])
def letterbox(img, new_shape=1024, color=(114, 114, 114)):
h, w = img.shape[:2]
r = min(new_shape / h, new_shape / w)
new_unpad = (int(round(w * r)), int(round(h * r)))
resized = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
canvas = np.full((new_shape, new_shape, 3), color, dtype=np.uint8)
dw = new_shape - new_unpad[0]
dh = new_shape - new_unpad[1]
left = dw // 2
top = dh // 2
canvas[top:top + new_unpad[1], left:left + new_unpad[0]] = resized
return canvas, r, left, top
def make_grid(h, w):
yv, xv = np.meshgrid(np.arange(h), np.arange(w), indexing="ij")
grid = np.stack((xv, yv), axis=-1).reshape(-1, 2).astype(np.float32)
return grid
def dfl_decode(bbox_feat):
bbox_feat = bbox_feat.reshape(-1, 4, REG_MAX)
prob = softmax(bbox_feat, axis=2)
proj = np.arange(REG_MAX, dtype=np.float32)
dist = (prob * proj).sum(axis=2)
return dist
def decode_single_scale(bbox_out, cls_out, stride):
bbox_feat = np.array(bbox_out.buffer, dtype=np.float32).reshape(-1, 64)
cls_feat = np.array(cls_out.buffer, dtype=np.float32).reshape(-1, NUM_CLASSES)
num_points = bbox_feat.shape[0]
hw = int(np.sqrt(num_points))
h, w = hw, hw
grid = make_grid(h, w)
scores = sigmoid(cls_feat[:, 0])
dist = dfl_decode(bbox_feat)
cx = (grid[:, 0] + 0.5) * stride
cy = (grid[:, 1] + 0.5) * stride
l = dist[:, 0] * stride
t = dist[:, 1] * stride
r = dist[:, 2] * stride
b = dist[:, 3] * stride
x1 = cx - l
y1 = cy - t
x2 = cx + r
y2 = cy + b
boxes = np.stack([x1, y1, x2, y2], axis=1)
keep = scores > CONF_THRES
boxes = boxes[keep]
scores = scores[keep]
classes = np.zeros((len(scores),), dtype=np.int32)
return boxes, scores, classes
def nms(boxes, scores, iou_thres=0.45):
if len(boxes) == 0:
return []
x1 = boxes[:, 0]
y1 = boxes[:, 1]
x2 = boxes[:, 2]
y2 = boxes[:, 3]
areas = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1)
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
w = np.maximum(0.0, xx2 - xx1)
h = np.maximum(0.0, yy2 - yy1)
inter = w * h
union = areas[i] + areas[order[1:]] - inter
iou = inter / np.maximum(union, 1e-6)
inds = np.where(iou <= iou_thres)[0]
order = order[inds + 1]
return keep
def scale_boxes_back(boxes, scale, left, top, orig_w, orig_h):
boxes[:, [0, 2]] -= left
boxes[:, [1, 3]] -= top
boxes /= scale
boxes[:, 0] = np.clip(boxes[:, 0], 0, orig_w - 1)
boxes[:, 1] = np.clip(boxes[:, 1], 0, orig_h - 1)
boxes[:, 2] = np.clip(boxes[:, 2], 0, orig_w - 1)
boxes[:, 3] = np.clip(boxes[:, 3], 0, orig_h - 1)
return boxes
models = dnn.load(MODEL_PATH)
model = models[0]
orig = cv2.imread(IMAGE_PATH)
if orig is None:
raise ValueError(f"读取图片失败:{IMAGE_PATH}")
orig_h, orig_w = orig.shape[:2]
img, scale, left, top = letterbox(orig, INPUT_SIZE)
nv12 = bgr_to_nv12_opencv(img)
t0 = time.time()
outputs = model.forward(nv12)
t1 = time.time()
bbox_outputs = outputs[:3]
cls_outputs = outputs[3:]
all_boxes = []
all_scores = []
all_classes = []
for bbox_out, cls_out, stride in zip(bbox_outputs, cls_outputs, STRIDES):
boxes, scores, classes = decode_single_scale(bbox_out, cls_out, stride)
if len(boxes) > 0:
all_boxes.append(boxes)
all_scores.append(scores)
all_classes.append(classes)
if len(all_boxes) == 0:
print("没有检测到目标")
cv2.imwrite(SAVE_PATH, orig)
print("结果已保存到:", SAVE_PATH)
raise SystemExit
boxes = np.concatenate(all_boxes, axis=0)
scores = np.concatenate(all_scores, axis=0)
classes = np.concatenate(all_classes, axis=0)
keep = nms(boxes, scores, IOU_THRES)
boxes = boxes[keep]
scores = scores[keep]
classes = classes[keep]
boxes = scale_boxes_back(boxes.copy(), scale, left, top, orig_w, orig_h)
t2 = time.time()
for box, score in zip(boxes, scores):
x1, y1, x2, y2 = box.astype(int)
cv2.rectangle(orig, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(
orig,
f"grape{score:.2f}",
(x1, max(0, y1 - 10)),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(0, 255, 0),
2,
)
cv2.imwrite(SAVE_PATH, orig)
print(f"BPU推理:{(t1 - t0) * 1000:.1f} ms")
print(f"后处理:{(t2 - t1) * 1000:.1f} ms")
print("检测框数量:", len(boxes))
print("结果已保存到:", SAVE_PATH)
11.2 运行与查看结果图
运行:
python3 run_once.py
查看结果图:
xdg-open /home/sunrise/result.jpg
12. 当前跑通结果
已经出现过这样的运行结果:
BPU推理: 37.5 ms
后处理: 130.9 ms
检测框数量: 2
结果已保存到: /home/sunrise/result.jpg
这说明:
- 不仅 forward 跑通了
- 连后处理、画框、结果保存也已经真正走通了 只是效果还没有完全调准。
13. 当前效果分析
13.1 复杂图的表现
在背景复杂、叶片多、遮挡明显的图上,出现过:
- 能检到葡萄
- 但框的位置和范围不太稳定
- 出现明显的大假框
- 有些框只覆盖葡萄局部
这说明问题更像出在:
后处理里的某个假设不对,而不是
.bin或板端推理本身失效。
13.2 你总结过的最可能几个原因
原因 1:分类分支读取方式不对
已知分类输出是:
(1, 128, 128, 1) float32 NCHW(1, 64, 64, 1) float32 NCHW(1, 32, 32, 1) float32 NCHW但 shape 看起来又像 NHWC,因此直接用cls_out[..., 0]取值未必正确,必须结合 layout 和输出维度确认。
原因 2:DFL 解码还没完全对上
bbox 输出是:
(1,128,128,64)(1,64,64,64)(1,32,32,64)通常会按:64 = 4 * 16reg_max = 16来解码,但如果顺序、reshape、四边定义不完全对齐,就可能出现:- 框偏大
- 框漂移
- 假框很多
原因 3:置信度阈值太低
如果误框分数只有:
grape 0.33
那么提升阈值确实可能过滤掉一些低分误框。
原因 4:训练和部署时预处理不完全一致
如果训练时的预处理与板端写的 letterbox / resize / NV12 转换不完全一致,也可能造成框偏移。 不过从当前现象看,更核心的问题仍是后处理本身。
14. 当前阶段结论
到目前为止,可以明确确认已经完成的部分有:
- 数据传输
- Docker 环境确认
- ultralytics 路径定位
- 输出头修改思路跑通
- ONNX 导出成功
- 校准数据成功生成
.bin文件成功生成.bin成功传到板端- 板端模型成功加载
- 单张图 forward 成功
result.jpg已经能生成- 简单图片已经能识别到葡萄 也就是说: 教程主体和部署主体已经基本完成。 当前剩余的问题主要是: 后处理还没有完全对齐。
15. 后处理改进建议
15.1 做单尺度验证
分别只启用一个尺度:
- 只用
128x128 - 只用
64x64 - 只用
32x32看哪一路更容易出正确葡萄框,哪一路更容易出误框。
15.2 重新核对分类分支读取方式
重点确认:
- 单类别模型是否应按
reshape(-1, 1)读取 - 是否直接
sigmoid - layout 是否还需要额外调整
15.3 继续核对 DFL 解码顺序
重点检查:
l / t / r / b四边顺序- stride 乘法位置
- grid 中心点定义
- 是否与训练 / 导出时保持一致
15.4 用简单图和复杂图分开测试
建议保留两组图:
- 简单图
- 复杂图 如果简单图稳定、复杂图不稳,说明模型已经具备可用性,后续重点在后处理鲁棒性。 如果两者都不稳,再回头检查整体解析逻辑。
15.5 必要时回看训练标注
如果后处理继续优化后,框仍然总是落在葡萄局部而不是整串,就需要回头检查:
- 标注到底是整串还是局部
- 是否有大量偏小框
- 类别定义是否一致
16. 一句话总结
这次不是没有跑通,而是已经基本跑通了整条部署链路:
- 模型已经成功量化
.bin已经生成- 板端已经能加载模型
- 单张图已经能推理
- 结果图已经能生成 当前真正剩下的,不再是“能不能跑”,而是: 如何把后处理调准。