前言:让 AI 检测跑起来
上一篇文章讲完了 MIPI 摄像头在 5 寸 DSI 屏上的显示,以及 FCOS 模型的 AI 检测。但 FCOS 毕竟是官方预装好的模型,作为一个有追求的开发者(其实是想折腾),自然想跑一跑更经典的 YOLO 系列。
说到目标检测,YOLOv5 绝对是一个绕不开的名字。从 v2.0 到 v7.0,YOLOv5 凭借其速度和精度的平衡,成为了工业界最主流的检测模型之一。RDK X5 的 BPU 对它的支持也相当完善,官方 Model Zoo 里就有现成的配置。
但"支持"不等于"开箱即用"。从 PyTorch 的 .pt 文件到能在 RDK X5 上跑的 .bin 模型,中间隔着 ONNX 导出 → hb_mapper 检查 → PTQ 量化 → 板端部署 好几道坎。这篇文章记录了我从零开始,在 RDK X5 上跑通 YOLOv5 v2.0 的完整过程。
参考文章: 【4.1.1 ModelZoo概述】、【地平线旭日5 算法工具链】、《RDKX5模型转换及部署》 、 《Python轻松部署推理》,以及我的前两篇踩坑记录。
一、环境准备:OE 工具链和 Model Zoo
1.1 为什么需要 OE 工具链?
RDK X5 的 BPU(Bayes-e 架构)是一个专用的 AI 加速单元,它只支持 INT8 定点运算。而我们用 PyTorch 训练出来的模型是 FP32 浮点格式,需要经过量化才能上板运行。
地瓜官方提供了 Open Explorer(OE) 算法工具链,核心是一个 Docker 镜像,里面包含了:
hb_mapper:模型转换工具(检查 → 量化 → 编译)hb_perf:模型性能分析工具- 各种示例代码和配置文件
宿主机环境:WSL2-Ubuntu 22.04 + Docker + NVIDIA Container Toolkit(可选 GPU 加速)
端侧环境:RDK X5 + Ubuntu 20.04 + Python 3.10
1.2 创建 Python 环境
# 创建 conda 环境
conda create -n rdk_x5 python=3.10 -y
conda activate rdk_x5
1.3 下载和配置 OE
# 创建工作环境
mkdir -p ~/Projects/rdk_x5/dataset
cd ~/Projects/rdk_x5
# 下载 OE v1.2.8 交付包和 Docker 镜像
wget -c ftp://x5ftp:x5ftp%40123%24%25@vrftp.horizon.ai/OpenExplorer/v1.2.8_release/horizon_x5_open_explorer_v1.2.8-py310_20240926.tar.gz
wget -c ftp://x5ftp:x5ftp%40123%24%25@vrftp.horizon.ai/OpenExplorer/v1.2.8_release/docker_openexplorer_ubuntu_20_x5_cpu_v1.2.8.tar.gz
# 解压 OE 包
tar -xzvf horizon_x5_open_explorer_v1.2.8-py310_20240926.tar.gz
# 查看解压后的目录结构
tree -L 3 horizon_x5_open_explorer_v1.2.8-py310_20240926
# horizon_x5_open_explorer_v1.2.8-py310_20240926
# ├── package
# │ ├── board
# │ │ ├── hrt_tools
# │ │ └── install.sh
# │ └── host
# │ ├── ai_toolchain
# │ ├── host_package
# │ ├── hrt_tools
# │ ├── install.sh
# │ └── resolve.sh
# ├── README-CN
# ├── README-EN
# ├── resolve_all.sh
# ├── run_docker.sh
# └── samples/
# ├── ai_benchmark/
# ├── ai_toolchain/
# └── model_zoo -> ai_toolchain/model_zoo
# 导入 Docker 镜像
docker load < docker_openexplorer_ubuntu_20_x5_cpu_v1.2.8.tar.gz
设置环境变量(可以添加到 ~/.bashrc):
export version=v1.2.8
export oe_path=$(realpath ~/Projects/rdk_x5)
export ai_toolchain_package_path=$oe_path/horizon_x5_open_explorer_v1.2.8-py310_20240926
export dataset_path=$oe_path/dataset
# 设置别名方便启动
alias rdk_x5_oe_env="docker run -it --rm --shm-size=15g -v "$ai_toolchain_package_path":/open_explorer -v "$dataset_path":/data openexplorer/ai_toolchain_ubuntu_20_x5_cpu:v1.2.8-py310"
启动 Docker 并验证:
# 设置环境变量
export version=v1.2.8
export oe_path=$(realpath ~/Projects/rdk_x5)
export ai_toolchain_package_path=$oe_path/horizon_x5_open_explorer_v1.2.8-py310_20240926
export dataset_path=$oe_path/dataset
# CPU 模式启动
docker run -it --rm --shm-size=15g \
-v "$ai_toolchain_package_path":/open_explorer \
-v "$dataset_path":/data \
openexplorer/ai_toolchain_ubuntu_20_x5_cpu:v1.2.8-py310
# GPU 模式启动(需要 NVIDIA GPU)
docker run -it --rm --gpus all --shm-size=15g \
-v "$ai_toolchain_package_path":/open_explorer \
-v "$dataset_path":/data \
openexplorer/ai_toolchain_ubuntu_20_x5_gpu:v1.2.8-py310
# 在容器内验证
hb_mapper
看到帮助信息就说明环境配置成功了。
1.4 下载 Model Zoo
Model Zoo 是地瓜维护的算法案例仓库,里面有各种模型的现成配置:
cd ~/Projects/rdk_x5
git clone https://github.com/D-Robotics/rdk_model_zoo
YOLOv5 的相关文件在 rdk_model_zoo/samples/vision/yolov5/ 目录下,包括:
ptq_yamls/:PTQ 量化配置文件cpp/:C++ 部署示例imgs/:测试图片
二、模型导出:从 PyTorch 到 ONNX
2.1 下载 YOLOv5 v2.0 源码
为什么用 v2.0?因为这是官方 Model Zoo 支持的版本,配置文件齐全,不容易踩坑。
cd ~/Projects/rdk_x5
git clone https://github.com/ultralytics/yolov5.git
cd yolov5
git checkout v2.0
2.2 安装导出依赖
# 安装 YOLOv5 导出所需的依赖(注意版本)
pip install torch==2.3.1 torchvision==0.18.1
pip install onnx==1.16.0
pip install onnx-simplifier==0.5.0
pip install onnxscript==0.1.0
pip install protobuf numpy
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
2.3 修改 Detect 层输出格式
这是一个关键步骤。YOLOv5 默认的输出格式是 NCHW(batch, channels, height, width),但 BPU 需要的是 NHWC(batch, height, width, channels)。
修改 models/yolo.py 中的 Detect.forward 函数:
def forward(self, x):
# 新增:直接返回 NHWC 格式的特征图
return [self.m[i](x[i]).permute(0, 2, 3, 1).contiguous() for i in range(self.nl)]
# 原来的代码注释掉或删除...
2.4 修改 export.py 导出脚本
官方 models/export.py 默认会导出 TorchScript 和 CoreML,我们只需要 ONNX。复制出来一份修改:
cp models/export.py .
主要修改点:
- 跳过 TorchScript 和 CoreML(直接 raise ValueError 跳过)
- 指定 ONNX opset 版本为 11(BPU 只支持 opset 10/11)
- 添加 ONNX Simplify(图优化,减少冗余算子)
"""Exports a YOLOv5 *.pt model to ONNX and TorchScript formats
Usage:
$ export PYTHONPATH="$PWD" && python models/export.py --weights ./weights/yolov5s.pt --img 640 --batch 1
"""
import argparse
from models.common import *
from utils import google_utils
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default='./yolov5s.pt', help='weights path')
parser.add_argument('--img-size', nargs='+', type=int, default=[640, 640], help='image size')
parser.add_argument('--batch-size', type=int, default=1, help='batch size')
opt = parser.parse_args()
opt.img_size *= 2 if len(opt.img_size) == 1 else 1 # expand
print(opt)
# Input
img = torch.zeros((opt.batch_size, 3, *opt.img_size)) # image size(1,3,320,192) iDetection
# Load PyTorch model
google_utils.attempt_download(opt.weights)
model = torch.load(opt.weights, map_location=torch.device('cpu'), weights_only=False)['model'].float()
model.eval()
model.model[-1].export = True # set Detect() layer export=True
y = model(img) # dry run
# TorchScript export
try:
raise ValueError("skip TorchScript")
print('\nStarting TorchScript export with torch %s...' % torch.__version__)
f = opt.weights.replace('.pt', '.torchscript.pt') # filename
ts = torch.jit.trace(model, img)
ts.save(f)
print('TorchScript export success, saved as %s' % f)
except Exception as e:
print('TorchScript export failure: %s' % e)
# ONNX export
try:
import onnx
from onnxsim import simplify
print('\nStarting ONNX export with onnx %s...' % onnx.__version__)
f = opt.weights.replace('.pt', '.onnx') # filename
model.fuse() # only for ONNX
torch.onnx.export(model, img, f, verbose=False, opset_version=11, input_names=['images'],
output_names=['small', 'medium', 'big'] if y is None else ['output'])
# Checks
onnx_model = onnx.load(f) # load onnx model
onnx.checker.check_model(onnx_model) # check onnx model
print(onnx.helper.printable_graph(onnx_model.graph)) # print a human readable model
# simplify
onnx_model, check = simplify(
onnx_model,
dynamic_input_shape=False,
input_shapes=None)
assert check, 'assert check failed'
onnx.save(onnx_model, f)
print('ONNX export success, saved as %s' % f)
except Exception as e:
print('ONNX export failure: %s' % e)
# CoreML export
try:
raise ValueError("skip CoreML")
import coremltools as ct
print('\nStarting CoreML export with coremltools %s...' % ct.__version__)
# convert model from torchscript and apply pixel scaling as per detect.py
model = ct.convert(ts, inputs=[ct.ImageType(name='images', shape=img.shape, scale=1 / 255.0, bias=[0, 0, 0])])
f = opt.weights.replace('.pt', '.mlmodel') # filename
model.save(f)
print('CoreML export success, saved as %s' % f)
except Exception as e:
print('CoreML export failure: %s' % e)
# Finish
print('\nExport complete. Visualize with https://github.com/lutzroeder/netron.')
2.6 导出 ONNX 模型
# 下载官方权重
wget https://github.com/ultralytics/yolov5/releases/download/v2.0/yolov5s.pt -O yolov5s_tag2.0.pt
# 导出
python export.py --weights yolov5s_tag2.0.pt
导出成功后生成 yolov5s_tag2.0.onnx,检查一下:
python -c "import onnx; model=onnx.load('yolov5s_tag2.0.onnx'); print(model.opset_import[0].version)"
# 输出 11 就对了
三、模型量化:使用 hb_mapper 转换 BPU 模型
3.1 准备校准数据集
PTQ(Post-Training Quantization)需要 100-200 张校准图片。用训练集或验证集都可以:
mkdir -p ~/Projects/rdk_x5/horizon_x5_open_explorer_v1.2.8-py310_20240926/model/test/calibration_data_rgb_f32_coco_640
# 从 Model Zoo 拷贝示例图片
cp /path/to/test_image/*.jpg \
~/Projects/rdk_x5/horizon_x5_open_explorer_v1.2.8-py310_20240926/model/test/calibration_data_rgb_f32_coco_640/
3.2 拷贝文件到 OE 目录
mkdir -p ~/Projects/rdk_x5/horizon_x5_open_explorer_v1.2.8-py310_20240926/model/test
cp yolov5s_tag2.0.onnx ~/Projects/rdk_x5/horizon_x5_open_explorer_v1.2.8-py310_20240926/model/test/
# 从 Model Zoo 拷贝 PTQ 配置文件
cp ~/Projects/rdk_x5/rdk_model_zoo/samples/vision/yolov5/ptq_yamls/yolov5_detect_bayese_640x640_nv12.yaml \
~/Projects/rdk_x5/horizon_x5_open_explorer_v1.2.8-py310_20240926/model/test/
3.3 修改 YAML 配置文件
编辑 yolov5_detect_bayese_640x640_nv12.yaml:
model_parameters:
onnx_model: '/open_explorer/model/test/yolov5s_tag2.0.onnx' # 修改模型路径
march: "bayes-e" # RDK X5 架构
working_dir: 'yolov5s_tag_v2.0_detect_640x640_bayese_nv12'
output_model_file_prefix: 'yolov5s_tag_v2.0_detect_640x640_bayese_nv12'
input_parameters:
input_type_rt: 'nv12' # 运行时输入 NV12
input_type_train: 'rgb'
input_layout_train: 'NCHW'
norm_type: 'data_scale'
scale_value: 0.003921568627451 # 1/255
calibration_parameters:
cal_data_dir: './calibration_data_rgb_f32_coco_640' # 校准数据集
cal_data_type: 'float32'
preprocess: True # 自动预处理
calibration_type: 'default'
compiler_parameters:
compile_mode: 'latency' # 延迟优先
optimize_level: 'O3'
3.4 模型检查
启动 Docker 进行检查:
rdk_x5_oe_env
cd /open_explorer/model/test
hb_mapper checker --model-type onnx --march bayes-e --model yolov5s_tag2.0.onnx
检查通过会显示:
- ONNX 模型信息(输入 shape [1,3,640,640],三个输出 stride)
- BPU 支持的所有算子
- 预估性能(FPS、延迟、DDR 带宽)
3.5 执行模型转换
hb_mapper makertbin --model-type onnx --config yolov5_detect_bayese_640x640_nv12.yaml
等待几分钟,转换成功后会生成 yolov5s_tag_v2.0_detect_640x640_bayese_nv12/ 目录,结构如下:
yolov5s_tag_v2.0_detect_640x640_bayese_nv12/
├── main_graph_subgraph_0.html # 性能分析报告
├── main_graph_subgraph_0.json # 模型结构 JSON
├── yolov5s_tag_v2.0_detect_640x640_bayese_nv12.bin # 最终的 BPU 模型文件
├── yolov5s_tag_v2.0_detect_640x640_bayese_nv12_calibrated_model.onnx # 校准模型
├── yolov5s_tag_v2.0_detect_640x640_bayese_nv12_optimized_float_model.onnx # 优化后的浮点模型
├── yolov5s_tag_v2.0_detect_640x640_bayese_nv12_original_float_model.onnx # 原始浮点模型
├── yolov5s_tag_v2.0_detect_640x640_bayese_nv12_quant_info.json # 量化信息
└── yolov5s_tag_v2.0_detect_640x640_bayese_nv12_quantized_model.onnx # 量化后的模型
关键文件:yolov5s_tag_v2.0_detect_640x640_bayese_nv12.bin 是最终上板的模型文件。
3.6 模型验证
# 生成可视化结构图
hb_perf yolov5s_tag_v2.0_detect_640x640_bayese_nv12/yolov5s_tag_v2.0_detect_640x640_bayese_nv12.bin
# 检查输入输出
hrt_model_exec model_info --model_file yolov5s_tag_v2.0_detect_640x640_bayese_nv12/yolov5s_tag_v2.0_detect_640x640_bayese_nv12.bin
四、板端部署:Python 推理代码编写
4.1 上传模型和测试图片
# 在宿主机上创建工作目录
mkdir -p ~/Projects/rdk_x5/ModelZoo_Test/output
# 上传模型到开发板
rsync -avz ~/Projects/rdk_x5/horizon_x5_open_explorer_v1.2.8-py310_20240926/model/test/yolov5s_tag_v2.0_detect_640x640_bayese_nv12/ \
rdk_x5.local:~/Projects/rdk_x5/ModelZoo_Test/model/test/yolov5s_tag_v2.0_detect_640x640_bayese_nv12/
# 上传测试图片到开发板
rsync -avz ~/Projects/rdk_x5/ModelZoo_Test/test_data/ \
rdk_x5.local:~/Projects/rdk_x5/ModelZoo_Test/test_data/
# 上传推理脚本
rsync -avz yolov5_tag_v2_hobot_dnn.py rdk_x5.local:~/Projects/rdk_x5/ModelZoo_Test/
# 从开发板下载结果到本地
rsync -avz rdk_x5.local:~/Projects/rdk_x5/ModelZoo_Test/output/ ~/Projects/rdk_x5/ModelZoo_Test/output/
因为最近太平年比较火,所以我从《太平年》和《庆余年》拷贝了一些剧照图片,做为测试用的图片:
4.2 编写推理代码
这里使用 hobot_dnn(BSP API),它比 bpu_infer_lib 更底层但更灵活,且 bpu_infer_lib 已经不被官方推荐。
#!/usr/bin/env python3
# yolov5_tag_v2_hobot_dnn.py
import cv2
import numpy as np
from scipy.special import expit as sigmoid
from time import time
from hobot_dnn import pyeasy_dnn as dnn
import argparse
class BPU_Detect:
def __init__(self, model_path: str,
labelnames: list,
num_classes: int = None,
conf: float = 0.45,
iou: float = 0.45,
anchors: np.array = np.array([
[10,13, 16,30, 33,23], # P3/8
[30,61, 62,45, 59,119], # P4/16
[116,90, 156,198, 373,326], # P5/32
]),
strides = np.array([8, 16, 32]),
is_save: bool = True):
self.model_path = model_path
self.labelname = labelnames
self.conf = conf
self.iou = iou
self.anchors = anchors
self.strides = strides
self.is_save = is_save
# 加载模型
self.models = dnn.load(self.model_path)
self.model = self.models[0]
# 获取输入尺寸
self.input_shape = self.model.inputs[0].properties.shape
self.input_w = self.input_shape[2] # NCHW: [1,3,640,640]
self.input_h = self.input_shape[3]
self.nc = num_classes if num_classes else len(labelnames)
# 初始化特征图网格
self._init_grids()
print(f"模型加载成功: {self.input_w}x{self.input_h}, 类别数: {self.nc}")
def _init_grids(self):
"""初始化特征图网格和锚框"""
def _create_grid(stride: int):
grid_h = self.input_h // stride
grid_w = self.input_w // stride
# 生成网格坐标
grid_x = np.tile(np.linspace(0.5, grid_w - 0.5, grid_w), reps=grid_h)
grid_y = np.repeat(np.arange(0.5, grid_h + 0.5, 1), grid_w)
grid = np.stack([grid_x, grid_y], axis=0).transpose(1, 0)
grid = np.hstack([grid] * 3).reshape(-1, 2)
# 生成锚框
anchor_idx = int(np.log2(stride / 8))
anchors = np.tile(self.anchors[anchor_idx], grid_h * grid_w).reshape(-1, 2)
return grid, anchors
self.s_grid, self.s_anchors = _create_grid(self.strides[0])
self.m_grid, self.m_anchors = _create_grid(self.strides[1])
self.l_grid, self.l_anchors = _create_grid(self.strides[2])
def bgr2nv12_opencv(self, image):
"""BGR转NV12格式"""
height, width = image.shape[:2]
area = height * width
yuv420p = cv2.cvtColor(image, cv2.COLOR_BGR2YUV_I420).reshape((area * 3 // 2,))
y = yuv420p[:area]
uv_planar = yuv420p[area:].reshape((2, area // 4))
uv_packed = uv_planar.transpose((1, 0)).reshape((area // 2,))
nv12 = np.zeros_like(yuv420p)
nv12[:area] = y
nv12[area:] = uv_packed
return nv12
def PreProcess(self, img):
"""预处理:BGR → Resize → NV12"""
if isinstance(img, str):
orig_img = cv2.imread(img)
if orig_img is None:
raise ValueError(f"无法读取图片: {img}")
else:
orig_img = img
img_h, img_w = orig_img.shape[:2]
# Resize 到模型输入尺寸
input_tensor = cv2.resize(orig_img, (self.input_w, self.input_h))
input_tensor = self.bgr2nv12_opencv(input_tensor)
# 记录缩放比例,后处理时恢复坐标
self.y_scale = img_h / self.input_h
self.x_scale = img_w / self.input_w
return input_tensor
def PostProcess(self):
"""后处理:解码 → NMS"""
outputs = self.model_outputs
# 三个输出层分别处理 (stride 8, 16, 32)
s_pred = outputs[0].buffer.reshape([-1, (5 + self.nc)])
m_pred = outputs[1].buffer.reshape([-1, (5 + self.nc)])
l_pred = outputs[2].buffer.reshape([-1, (5 + self.nc)])
# 阈值筛选 + 特征解码(小目标层)
s_raw_max = np.max(s_pred[:, 5:], axis=1)
s_scores = 1 / ((1 + np.exp(-s_pred[:, 4])) * (1 + np.exp(-s_raw_max)))
s_valid = np.flatnonzero(s_scores >= self.conf)
s_ids = np.argmax(s_pred[s_valid, 5:], axis=1)
s_scores = s_scores[s_valid]
s_dxyhw = 1 / (1 + np.exp(-s_pred[s_valid, :4]))
s_xy = (s_dxyhw[:, 0:2] * 2.0 + self.s_grid[s_valid] - 1.0) * self.strides[0]
s_wh = (s_dxyhw[:, 2:4] * 2.0) ** 2 * self.s_anchors[s_valid]
s_xyxy = np.concatenate([s_xy - s_wh * 0.5, s_xy + s_wh * 0.5], axis=-1)
# 中目标层和大目标层类似处理...
m_raw_max = np.max(m_pred[:, 5:], axis=1)
m_scores = 1 / ((1 + np.exp(-m_pred[:, 4])) * (1 + np.exp(-m_raw_max)))
m_valid = np.flatnonzero(m_scores >= self.conf)
m_ids = np.argmax(m_pred[m_valid, 5:], axis=1)
m_scores = m_scores[m_valid]
m_dxyhw = 1 / (1 + np.exp(-m_pred[m_valid, :4]))
m_xy = (m_dxyhw[:, 0:2] * 2.0 + self.m_grid[m_valid] - 1.0) * self.strides[1]
m_wh = (m_dxyhw[:, 2:4] * 2.0) ** 2 * self.m_anchors[m_valid]
m_xyxy = np.concatenate([m_xy - m_wh * 0.5, m_xy + m_wh * 0.5], axis=-1)
l_raw_max = np.max(l_pred[:, 5:], axis=1)
l_scores = 1 / ((1 + np.exp(-l_pred[:, 4])) * (1 + np.exp(-l_raw_max)))
l_valid = np.flatnonzero(l_scores >= self.conf)
l_ids = np.argmax(l_pred[l_valid, 5:], axis=1)
l_scores = l_scores[l_valid]
l_dxyhw = 1 / (1 + np.exp(-l_pred[l_valid, :4]))
l_xy = (l_dxyhw[:, 0:2] * 2.0 + self.l_grid[l_valid] - 1.0) * self.strides[2]
l_wh = (l_dxyhw[:, 2:4] * 2.0) ** 2 * self.l_anchors[l_valid]
l_xyxy = np.concatenate([l_xy - l_wh * 0.5, l_xy + l_wh * 0.5], axis=-1)
# 合并所有预测结果
xyxy = np.concatenate((s_xyxy, m_xyxy, l_xyxy), axis=0)
scores = np.concatenate((s_scores, m_scores, l_scores), axis=0)
ids = np.concatenate((s_ids, m_ids, l_ids), axis=0)
# NMS
indices = cv2.dnn.NMSBoxes(xyxy.tolist(), scores.tolist(), self.conf, self.iou)
if len(indices) > 0:
indices = np.array(indices).flatten()
self.bboxes = (xyxy[indices] * np.array([self.x_scale, self.y_scale, self.x_scale, self.y_scale])).astype(np.int32)
self.scores = scores[indices]
self.ids = ids[indices]
else:
self.bboxes = np.array([], dtype=np.int32).reshape(0, 4)
self.scores = np.array([], dtype=np.float32)
self.ids = np.array([], dtype=np.int32)
def draw_detection(self, img, box, score, class_id):
"""绘制检测框"""
x1, y1, x2, y2 = box
colors = [(255,0,0), (0,255,0), (0,0,255), (255,255,0), (255,0,255), (0,255,255)]
color = colors[class_id % len(colors)]
cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
label = f"{self.labelname[class_id]}: {score:.2f}"
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
cv2.rectangle(img, (x1, y1-th-10), (x1+tw, y1), color, -1)
cv2.putText(img, label, (x1, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,0), 1)
def detect(self, img, output_path=None):
"""检测主函数"""
input_tensor = self.PreProcess(img)
self.model_outputs = self.model.forward(input_tensor)
self.PostProcess()
if isinstance(img, str):
draw_img = cv2.imread(img)
else:
draw_img = img.copy()
for cid, score, bbox in zip(self.ids, self.scores, self.bboxes):
print(f"({bbox[0]}, {bbox[1]}, {bbox[2]}, {bbox[3]}) -> {self.labelname[cid]}: {score:.2f}")
if self.is_save:
self.draw_detection(draw_img, bbox, score, cid)
if self.is_save and output_path:
cv2.imwrite(output_path, draw_img)
print(f"结果已保存: {output_path}")
return self.bboxes, self.scores, self.ids
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='YOLOv5 BPU 目标检测')
parser.add_argument('-m', '--model', type=str, required=True, help='模型文件路径')
parser.add_argument('-i', '--input', type=str, required=True, help='输入图片路径')
parser.add_argument('-o', '--output', type=str, default='output.jpg', help='输出图片路径')
parser.add_argument('-c', '--conf', type=float, default=0.25, help='置信度阈值')
parser.add_argument('--iou', type=float, default=0.45, help='NMS IoU阈值')
args = parser.parse_args()
# COCO 80 类
coconame = ["person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat",
"traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat",
"dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack",
"umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball",
"kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket",
"bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple",
"sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake",
"chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop",
"mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink",
"refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"]
detector = BPU_Detect(
model_path=args.model,
labelnames=coconame,
conf=args.conf,
iou=args.iou
)
detector.detect(args.input, args.output)
五、踩坑记录:那些让我抓狂的错误
5.1 类别数不匹配导致 IndexError
现象:
IndexError: index 19238 is out of bounds for axis 0 with size 19200
原因:YOLOv5 输出是 80 类 COCO,但我初始化时只给了 1 个类别名 ["person"],导致 nc=1,reshape 形状错误。
解决:确保类别列表长度与模型训练时一致:
coconame = ["person", "bicycle", ...] # 完整的 80 类
5.2 输出顺序混乱
现象:检测框位置偏移,小目标检测不到。
原因:YOLOv5 三个输出头分别是 stride 8/16/32(小/中/大目标),如果顺序搞错,解码时用的 stride 和 anchors 不匹配。
解决:用 hrt_model_exec model_info 确认输出顺序,确保:
- outputs[0]:stride 8(80×80网格)
- outputs[1]:stride 16(40×40网格)
- outputs[2]:stride 32(20×20网格)
5.3 NV12 转换花屏
现象:输入图片正常,但预处理后的图像有条纹/花屏。
原因:NV12 是 YUV420 的变体,UV 分量是交错排列(UVUV…),不是分开的(UU…VV…)。
解决:正确的 NV12 打包方式:
uv_packed = uv_planar.transpose((1, 0)).reshape((area // 2,))
5.4 NMS 后无结果
现象:置信度阈值设为 0.5 时,一个目标都检测不到。
原因:量化后的模型输出置信度普遍偏低,原始阈值太高。
解决:适当降低阈值:
python yolov5_tag_v2_hobot_dnn.py -c 0.15 -i test.jpg
六、实测效果
6.1 单张图片测试
cd ~/Projects/rdk_x5/ModelZoo_Test
python yolov5_tag_v2_hobot_dnn.py \
-m model/test/yolov5s_tag_v2.0_detect_640x640_bayese_nv12.bin \
-i test_data/p2929446215.jpg \
-o output/result.jpg \
-c 0.15
输出:
模型加载成功: 640x640, 类别数: 80
(274, 95, 341, 215) -> person: 0.63
(134, 83, 351, 613) -> person: 0.63
(334, 121, 544, 624) -> person: 0.62
...
检测到 12 个目标
结果已保存: output/result.jpg
6.2 批量测试
for img in test_data/*.jpg; do
python yolov5_tag_v2_hobot_dnn.py \
-m model/test/yolov5s_tag_v2.0_detect_640x640_bayese_nv12.bin \
-i "$img" \
-o "output/$(basename $img)-result.jpg" \
-c 0.15
done
实测结果如下:
6.3 性能数据
| 模型 | 输入尺寸 | 延迟 | FPS |
|---|---|---|---|
| YOLOv5s v2.0 | 640×640 | ~25ms | ~40 FPS |
| FCOS | 512×512 | ~20ms | ~50 FPS |
YOLOv5s 的精度明显高于 FCOS,但延迟稍大。对于实时性要求高的场景,可以考虑 YOLOv5n 或量化成更低比特。
七、总结与展望
通过这次部署,我完整走通了 PyTorch → ONNX → BPU 的转换流程,几个关键收获:
- Detect 层修改是必须的:NCHW → NHWC 的转换必须在导出时完成
- hb_mapper 检查不能省:能提前发现算子不支持的问题
- 校准数据集要足够:100-200 张有代表性的图片,太少会导致量化精度下降
- 后处理是灵魂:YOLO 的 anchor 解码、NMS 阈值调参直接影响检测效果
下一步计划:
- 尝试 YOLOv8 的 BPU 部署
- 集成到 MIPI 摄像头的实时检测中
- 对比 INT8 和混合精度的效果差异











