硬件环境: 训练机 Intel Core Ultra 7 265K + RTX 5070 Ti,部署机 Orange Pi 5 Plus(RK3588) 软件环境: Ubuntu 22.04,Python 3.10,ROS 2 Humble,Ultralytics 8.4.30,RKNN Toolkit2 2.3.2
任务目标: 在机械臂抓取系统中部署轻量级目标检测模型,识别圆柱体(cylinder)和长方体(box)两类物体,输出 2D 检测框,配合后续点云拟合完成 3D 位姿估计
一、项目背景简述和模型选择
本项目整体数据流如下(更详细的请参考上一篇抓取调试的流程图):
Orbbec Gemini 336L(RGB-D 相机)
│
├── RGB 图 → YOLOv8n 检测 → [cylinder/box + 2D bbox]
│
└── 深度图 → bbox 区域点云提取
│
RANSAC / PCA 几何拟合
│
3D 位置 + 朝向 + 物理尺寸
│
IK 求解 → 机械臂抓取
为什么选 YOLOv8n:
-
Rockchip 官方 rknn-model-zoo 提供完整的 YOLOv8 转换示例,工具链成熟
-
RK3588 NPU 推理约 25ms/帧(40fps),基本够用
-
任务暂定两类物体,场景固定,nano 版本精度完全够用
-
模型体积仅 6MB,部署成本极低
为什么不用 YOLOv8-OBB:
旋转框检测的价值在于直接从图像估计目标旋转角,但本系统用深度点云 PCA 主轴分析来获取更精确的 3D 朝向,OBB 的旋转角信息在这里是冗余的。因此 RGB 模型只需要负责"这是什么形状、在图像哪个位置",使用普通水平框检测即可,训练数据标注量也大幅减少。
二、数据集采集规划
2.1 物体准备
拍摄前准备三种颜色变体,覆盖实际使用中可能出现的形状外观:
圆柱体:
A. 白色/浅色药瓶(原始颜色)
B. 深色/有图案药瓶(不同品牌)
箱子:
A. 白色药盒(原始颜色)
B. 彩色包装盒(鲜艳颜色)
2.2 采集数量与场景分布
bash
单圆柱: 15 张 (颜色 A/B/C 各 5 张,覆盖平台各区域)
单箱子: 15 张 (颜色 A/B/C 各 5 张,长边方向各不同)
圆柱+箱子同框:50 张 (重点场景,多种颜色组合)
└── 正常光 15 张
└── 强光/侧光(台灯)15 张
└── 暗光(窗帘拉上)10 张
└── 多物体同框(3-4个)10 张
多圆柱: 10 张
多箱子: 10 张
总计: 100 张
2.3 拍摄要点
-
相机固定在实际部署位置,不能移动(确保与运行时视角一致)
-
平台四角的 ArUco 标定码必须保持可见,不被物体遮挡
-
每换一个光线场景,拍摄前等待曝光稳定再保存
-
箱子摆放时覆盖 0°/45°/90° 三种朝向
-
周围环境比较嘈杂,可以把环境也拍摄入画,增加鲁棒性
2.4 合并子目录
按场景分批拍摄完成后,合并为单一目录(这个是我为了方便统计,在脚本中做了不同图片的分类,但是上传标注的时候需要混合到一起)上传 Roboflow:
bash
mkdir -p dataset_merged
find dataset_raw -type f \( -name "*.jpg" -o -name "*.png" -o -name "*.jpeg" \) \
-exec cp {} dataset_merged/ \;
echo "总图片数: $(ls dataset_merged | wc -l)"
我直接写了一个半自动拍摄的脚本(注意我用的是奥比中光的相机,其余的相机自己替换一下启动部分就行):
python
#!/usr/bin/env python3
"""
dataset_capture.py --- 数据集拍摄辅助工具
依赖:
pip install opencv-python
ros2 环境中运行(需要 rclpy + cv_bridge)
用法:
python3 dataset_capture.py
python3 dataset_capture.py --output ~/my_dataset
快捷键:
SPACE 拍照
N 下一个批次
P 上一个批次
1-9 直接跳转到第 N 批(超过 9 批用 N/P)
D 删除最后一张(手抖了)
Q / ESC 退出并打印汇总
"""
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
import os
import argparse
from datetime import datetime
# ── 批次定义(按你的拍摄清单顺序) ────────────────────────────────────────
# 格式: (目录名, 目标张数, 屏幕提示文字)
BATCHES = [
# 单圆柱 15 张
("single_cyl_A", 5, "单圆柱 · 白色/浅色药瓶 | 近/远/左/右/中各1张"),
("single_cyl_B", 5, "单圆柱 · 深色/有图案药瓶 | 近/远/左/右/中各1张"),
("single_cyl_C", 5, "单圆柱 · 缠黑胶带 | 近/远/左/右/中各1张"),
# 单箱子 15 张
("single_box_A", 5, "单箱子 · 白色药盒 | 长边朝前×3 斜45°×2"),
("single_box_B", 5, "单箱子 · 彩色包装 | 长边朝前×3 斜45°×2"),
("single_box_C", 5, "单箱子 · 缠黑胶带 | 长边朝前×3 斜45°×2"),
# 同框·正常光 15 张
("both_normal_AA", 4, "同框·正常光 · A圆柱 + A箱子 | 物体分散在平台"),
("both_normal_AB", 4, "同框·正常光 · A圆柱 + B箱子 | 物体分散在平台"),
("both_normal_BC", 4, "同框·正常光 · B圆柱 + C箱子 | 物体分散在平台"),
("both_normal_CA", 3, "同框·正常光 · C圆柱 + A箱子 | 物体分散在平台"),
# 同框·台灯光 15 张
("both_lamp_1", 5, "同框·台灯 · 阴影落在另一个物体上 | 颜色组合随机"),
("both_lamp_2", 5, "同框·台灯 · 单侧强光 | 颜色组合随机"),
("both_lamp_3", 5, "同框·台灯 · 正面强光 | 颜色组合随机"),
# 同框·暗光 10 张
("both_dark_1", 5, "同框·暗光 · 窗帘拉上 · 颜色组合随机"),
("both_dark_2", 5, "同框·暗光 · 极暗 · 颜色区分度低"),
# 多物体同框 10 张
("multi_mix_1", 3, "3-4个物体·混合颜色·正常光 | 平台摆满"),
("multi_mix_2", 3, "3-4个物体·混合颜色·有互相遮挡"),
("multi_mix_3", 4, "3-4个物体·台灯光 | 平台摆满"),
# 多圆柱 10 张
("multi_cyl_1", 5, "多圆柱 · A+B+C各颜色 · 正常光"),
("multi_cyl_2", 5, "多圆柱 · A+B+C各颜色 · 台灯光"),
# 多箱子 10 张
("multi_box_1", 5, "多箱子 · 朝向各不同 · 正常光"),
("multi_box_2", 5, "多箱子 · 颜色混合 · 暗光"),
]
assert sum(b[1] for b in BATCHES) == 100, "批次总数应为100张"
class CaptureNode(Node):
def __init__(self, output_dir: str):
super().__init__('dataset_capture')
self.bridge = CvBridge()
self.output_dir = output_dir
self.batch_idx = 0
self.latest_frame = None
self.last_saved = None # 用于 D 键撤销
self.flash = 0 # 拍照白闪倒计时帧数
self.sub = self.create_subscription(
Image, '/camera/color/image_raw', self._img_cb, 10)
self.get_logger().info(f"输出目录: {output_dir} | 等待相机...")
# ── ROS 回调 ──────────────────────────────────────────────────────────
def _img_cb(self, msg):
self.latest_frame = self.bridge.imgmsg_to_cv2(msg, 'bgr8')
# ── 文件操作 ──────────────────────────────────────────────────────────
def _batch_dir(self, idx=None):
name = BATCHES[idx if idx is not None else self.batch_idx][0]
return os.path.join(self.output_dir, name)
def _count(self, idx=None):
d = self._batch_dir(idx)
if not os.path.isdir(d):
return 0
return len([f for f in os.listdir(d) if f.lower().endswith('.jpg')])
def capture(self):
if self.latest_frame is None:
return False
d = self._batch_dir()
os.makedirs(d, exist_ok=True)
ts = datetime.now().strftime('%H%M%S_%f')[:11]
path = os.path.join(d, f"{ts}.jpg")
cv2.imwrite(path, self.latest_frame, [cv2.IMWRITE_JPEG_QUALITY, 95])
self.last_saved = path
self.flash = 5
return True
def delete_last(self):
if self.last_saved and os.path.exists(self.last_saved):
os.remove(self.last_saved)
print(f" ✗ 已删除: {os.path.basename(self.last_saved)}")
self.last_saved = None
return True
return False
# ── 画面叠加层 ────────────────────────────────────────────────────────
def _draw(self, frame):
h, w = frame.shape[:2]
name, target, hint = BATCHES[self.batch_idx]
saved = self._count()
done = saved >= target
total_s = sum(self._count(i) for i in range(len(BATCHES)))
total_t = sum(b[1] for b in BATCHES)
# 半透明顶部栏
ov = frame.copy()
cv2.rectangle(ov, (0, 0), (w, 88), (0, 0, 0), -1)
cv2.rectangle(ov, (0, h - 44), (w, h), (0, 0, 0), -1)
cv2.addWeighted(ov, 0.55, frame, 0.45, 0, frame)
# 进度条
bar_x = int(w * saved / max(target, 1))
bar_c = (0, 210, 0) if done else (0, 160, 255)
cv2.rectangle(frame, (0, 68), (bar_x, 84), bar_c, -1)
cv2.rectangle(frame, (0, 68), (w, 84), (80, 80, 80), 1)
# 批次编号 + 名称
idx_str = f"[{self.batch_idx + 1:02d}/{len(BATCHES)}]"
cv2.putText(frame, f"{idx_str} {name}",
(10, 26), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
cv2.putText(frame, hint,
(10, 56), cv2.FONT_HERSHEY_SIMPLEX, 0.46, (190, 190, 190), 1)
# 右上角张数
cnt_str = f"{saved}/{target}"
cnt_color = (0, 255, 80) if done else (0, 200, 255)
cv2.putText(frame, cnt_str,
(w - 95, 34), cv2.FONT_HERSHEY_SIMPLEX, 1.0, cnt_color, 2)
# 底部提示栏
cv2.putText(frame,
"SPACE:拍照 N:下批 P:上批 D:撤销 1-9:跳批 Q:退出",
(8, h - 14), cv2.FONT_HERSHEY_SIMPLEX, 0.42, (160, 160, 160), 1)
total_str = f"总: {total_s}/{total_t}"
cv2.putText(frame, total_str,
(w - 120, h - 14), cv2.FONT_HERSHEY_SIMPLEX, 0.42, (160, 160, 160), 1)
# 完成标记
if done:
cv2.putText(frame, "DONE", (w // 2 - 40, 36),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 80), 2)
# 拍照白闪
if self.flash > 0:
alpha = self.flash / 5 * 0.45
white = frame.copy(); white[:] = (255, 255, 255)
cv2.addWeighted(white, alpha, frame, 1 - alpha, 0, frame)
self.flash -= 1
return frame
# ── 主循环 ────────────────────────────────────────────────────────────
def run(self):
cv2.namedWindow('Dataset Capture', cv2.WINDOW_NORMAL)
cv2.resizeWindow('Dataset Capture', 960, 620)
print("\n========== 数据集拍摄工具 ==========")
print(f"输出目录 : {self.output_dir}")
print(f"批次总数 : {len(BATCHES)} 目标张数: 100")
print("SPACE=拍照 N=下批 P=上批 D=撤销 1-9=跳批 Q=退出")
print("=====================================\n")
# 打印当前批次进度汇总
for i, (n, t, _) in enumerate(BATCHES):
s = self._count(i)
mark = "✓" if s >= t else f"{s}/{t}"
arrow = " ←" if i == self.batch_idx else ""
print(f" {mark:>5} {n}{arrow}")
print()
while rclpy.ok():
rclpy.spin_once(self, timeout_sec=0.01)
if self.latest_frame is None:
continue
display = self._draw(self.latest_frame.copy())
cv2.imshow('Dataset Capture', display)
key = cv2.waitKey(1) & 0xFF
if key in (ord('q'), ord('Q'), 27): # Q / ESC
break
elif key == ord(' '): # 拍照
if self.capture():
s = self._count()
t = BATCHES[self.batch_idx][1]
print(f" ✓ [{BATCHES[self.batch_idx][0]}] {s}/{t}")
if s >= t:
print(f" → 批次完成!按 N 切换下一批\n")
elif key in (ord('d'), ord('D')): # 撤销
self.delete_last()
elif key in (ord('n'), ord('N')): # 下一批
if self.batch_idx < len(BATCHES) - 1:
self.batch_idx += 1
self._announce()
elif key in (ord('p'), ord('P')): # 上一批
if self.batch_idx > 0:
self.batch_idx -= 1
self._announce()
elif ord('1') <= key <= ord('9'): # 数字键跳批
idx = key - ord('1')
if idx < len(BATCHES):
self.batch_idx = idx
self._announce()
cv2.destroyAllWindows()
self._summary()
def _announce(self):
n, t, hint = BATCHES[self.batch_idx]
s = self._count()
print(f"\n→ 批次 [{self.batch_idx+1:02d}] {n} 已有 {s}/{t} 张")
print(f" {hint}")
def _summary(self):
total = sum(self._count(i) for i in range(len(BATCHES)))
print(f"\n========== 拍摄结束 共 {total}/100 张 ==========")
for i, (n, t, _) in enumerate(BATCHES):
s = self._count(i)
mark = "✓" if s >= t else f"差{t - s}张"
print(f" {mark:>6} {n}: {s}/{t}")
print(f"\n图片保存在: {self.output_dir}")
print("下一步: 上传到 Roboflow 按批次导入并标注")
def main():
parser = argparse.ArgumentParser(description="数据集拍摄工具")
parser.add_argument('--output', default='./dataset_raw',
help='图片输出目录(默认 ./dataset_raw)')
args, _ = parser.parse_known_args()
rclpy.init()
node = CaptureNode(os.path.expanduser(args.output))
try:
node.run()
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
三、Roboflow 标注与增强
3.1 新建项目
登录 roboflow.com 后(我选的trial,如果后面过期了新创建一个号吧,不知道普通的会有什么区别):
python
Create New Project
→ Project Type: Object Detection
→ Annotation Group: 默认
→ Create Project
3.2 上传与标注
将 dataset_merged/ 里的所有图片拖入上传。
标注规范:
-
工具选择普通矩形框(快捷键
B),不要用 Smart Select 多边形工具 -
类别只有两个:
cylinder和box -
框要贴近物体轮廓,不留大量空白
-
同一张图里所有可见物体都必须标注,漏标会让模型把漏标物体学成背景
关于 Smart Select 工具: Roboflow 提供了基于 SAM3 的智能分割工具,可以自动生成多边形轮廓。但本项目训练的是目标检测模型(Object Detection),只需要矩形框,多边形标注不会带来任何精度提升,反而增加标注时间,直接关闭即可。
3.3 生成数据集版本
标注完成后进入 Versions → Generate New Version:
python
Preprocessing:
Resize → 640 × 640(YOLOv8 标准输入尺寸)
Augmentation:
Flip: Horizontal(水平翻转)
Rotation: ±15°
Grayscale: Apply to 15% of images ← 颜色无关性,对黑色物体最重要
Hue: ±20° ← 颜色偏移
Brightness: ±30% ← 光线强度变化
Exposure: ±25% ← 曝光变化
Blur: Up to 1.5px ← 轻微模糊
Noise: Up to 2% ← 噪声
倍数: 3x
不要添加 90° Rotate: 俯拍固定场景中相机不会旋转 90°,这个增强会产生不符合实际场景的训练样本,反而可能降低模型在真实场景下的性能。
python
Dataset Split:
Train: 80%
Valid: 20%
Test: 0%(实机测试比单独测试集更有意义)
关于 valid 集不增强: 这是 Roboflow 的正常设计。验证集的作用是评估模型在真实数据上的表现,如果对 valid 也做增强,评估结果就失去参考意义了。最终 train 246 张(原始 × 3x),valid 20 张(原始图)。
3.4 导出
python
Export Dataset → Format: YOLOv8 → Download zip
解压后结构:
data.yaml
train/
images/
labels/
valid/
images/
labels/
四、模型训练
4.1 环境配置
在训练机(Linux,RTX 5070 Ti)上:
python
# PyTorch(根据实际 CUDA 版本选择)
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu124
# Ultralytics(包含 YOLOv8 完整工具链)
pip install ultralytics
什么是 Ultralytics:
Ultralytics 是 YOLOv8 的官方开发框架,把模型定义、训练、验证、导出全部封装成了统一的命令行接口,直接用 yolo 命令就能完成所有操作,不需要手动写训练脚本。模型权重(yolov8n.pt)也会在首次训练时自动下载。
4.2 确认 data.yaml
python
cat data.yaml
正常内容:
train: ./train/images
val: ./valid/images
test: ./test/images
nc: 2
names: ['box', 'cylinder']
如果路径是绝对路径,需要改成相对路径,或在 yaml 所在目录执行训练命令。
4.3 训练命令
python
cd ~/Desktop/project/YOLOv8
yolo detect train \
data=data.yaml \
model=yolov8n.pt \
epochs=150 \
imgsz=640 \
batch=16 \
device=0
参数说明:
-
device=0:使用第一块 GPU,启动后确认日志里显示CUDA:0而非cpu -
epochs=150:对小数据集适当增加轮数,帮助充分收敛 -
batch=16:5070 Ti 有 16GB 显存,batch=16 只用 2.7GB,可以调大到 32 加快训练
训练速度参考:RTX 5070 Ti + 246 张图 + 150 epochs,几分钟就完成。
4.4 训练结果
最终验证集指标:
python
mAP50 = 0.995 ← 几乎满分,两类物体都能精准定位
mAP50-95 = 0.942 ← 高精度,框的位置也很准确
Precision = 0.998 ← 几乎没有误检
Recall = 1.000 ← 没有漏检
box: mAP50=0.995 mAP50-95=0.946
cylinder: mAP50=0.995 mAP50-95=0.938
注意: 验证集只有 20 张,结果有一定的乐观偏差。对于两类简单固定场景任务这个结果已经足够,实际部署效果需要在真实场景中进一步验证。
五、ONNX 导出
python
yolo export \
model=runs/detect/train/weights/best.pt \
format=onnx \
imgsz=640 \
opset=12
python
如果遇到的问题:缺少 ONNX 相关依赖
requirements: ['onnx>=1.12.0', 'onnxslim>=0.1.71', 'onnxruntime'] not found
解决:
pip install onnx onnxslim onnxruntime
导出结果:best.onnx
-
输入 shape:
(1, 3, 640, 640)- batch=1, RGB, 640×640 -
输出 shape:
(1, 6, 8400)- 6 = 4坐标 + 2类别,8400 = 候选框数量
六、RKNN 转换(PC 端)
6.1 安装 rknn-toolkit2
遇到的问题:直接安装会强制降级 PyTorch
pip install rknn-toolkit2 会触发下载 torch 2.4.0(797MB),与训练用的 torch 2.11 产生版本冲突。
解决:用 --no-deps 跳过依赖检查,手动安装必要依赖:
python
pip install rknn-toolkit2 --no-deps
pip install numpy==1.26.4 onnxruntime protobuf ruamel.yaml opencv-python fast-histogram
pip install protobuf==4.25.4 # rknn-toolkit2 要求 protobuf<=4.25.4
rknn-toolkit2 对 torch 版本的要求(<=2.4.0)只影响其内部某些量化功能,转换本身不需要 torch,跳过不影响使用。
6.2 转换脚本
python
# -*- coding: utf-8 -*- ← 必须加,否则中文注释会报 SyntaxError
from rknn.api import RKNN
rknn = RKNN(verbose=False)
rknn.config(
mean_values=[[0, 0, 0]],
std_values=[[255, 255, 255]],
target_platform='rk3588'
)
print("Loading ONNX...")
rknn.load_onnx(model='./runs/detect/train/weights/best.onnx')
print("Building...")
rknn.build(do_quantization=False)
print("Exporting...")
rknn.export_rknn('best.rknn')
rknn.release()
print("Done: best.rknn")
6.3 遇到的问题
问题:AttributeError: module 'onnx' has no attribute 'mapping'
原因:onnx 版本过新(2.x),rknn-toolkit2 2.3.2 内部使用了 onnx 1.x 的 API。
解决:
pip install onnx==1.16.0
问题:脚本报 SyntaxError: Non-UTF-8 code
原因:Python 文件包含中文字符但没有声明文件编码,Python 3 默认 UTF-8 但仍需显式声明。
解决:在脚本第一行加 # -*- coding: utf-8 -*-。
6.4 运行转换
python
python convert.py
输出:
I rknn-toolkit2 version: 2.3.2
I Loading: 100% 141/141
I OpFusing 0: 100% 100/100
...
I rknn building done.
Done: best.rknn
转换结果:best.rknn(约 7.5MB)
七、OrangePi 部署与验证
7.1 传输文件到板子上
python
scp best.rknn orangepi@192.168.x.x:~/Desktop/RobotProject/VisionPreparation/Model_rknn/
7.2 安装 rknn-toolkit-lite2
python
pip install rknn-toolkit-lite2==2.3.2
7.3 遇到的问题:RKNN Runtime 版本过旧
错误信息:
python
RKNN Runtime Information: librknnrt version: 1.4.0
Invalid RKNN model version 6
RKNN init failed. error code: RKNN_ERR_FAIL
原因分析:
OrangePi 系统预装的 librknnrt.so 是 1.4.0(2022年版本),而 rknn-toolkit2 2.3.2 生成的模型格式是版本 6,旧 Runtime 不认识新格式。toolkit 版本和 runtime 版本必须匹配。
尝试一(失败):用 LD_PRELOAD 加载新库
python
LD_PRELOAD=./rknn_libs/librknnrt.so python rknn_test.py
结果:日志仍显示 1.4.0,因为 rknnlite 内部用 ctypes.CDLL 直接加载系统库路径,LD_PRELOAD 环境变量对 ctypes 无效。
解决(替换系统库):
先做好备份,确保随时可以回退:
bash
# 下载匹配版本的 runtime 库
wget https://github.com/airockchip/rknn-toolkit2/raw/v2.3.2/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so
# 备份旧库(重要)
sudo cp /usr/lib/librknnrt.so /usr/lib/librknnrt.so.bak
# 替换新库
sudo cp librknnrt.so /usr/lib/librknnrt.so
sudo ldconfig
# 验证版本
strings /usr/lib/librknnrt.so | grep "librknnrt version"
# 输出: librknnrt version: 2.3.2
风险评估:
替换前用 ldconfig -p | grep rknn 和 systemctl list-units | grep rknn 确认系统中没有其他 rknn 项目依赖旧版本。本机依赖 librknnrt.so 的只有 librknn_api.so 和 librkllmrt.so(Rockchip 官方预装),新版本向下兼容,ROS 2 完全不依赖 rknn,替换安全。
如需回退:
bash
sudo cp /usr/lib/librknnrt.so.bak /usr/lib/librknnrt.so
sudo ldconfig
7.4 验证推理
bash
# rknn_test.py
import cv2
import numpy as np
from rknnlite.api import RKNNLite
rknn = RKNNLite()
rknn.load_rknn('./best.rknn')
rknn.init_runtime()
img = cv2.imread('./test.jpg')
img = cv2.resize(img, (640, 640))
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = np.expand_dims(img, 0)
outputs = rknn.inference(inputs=[img])
print("success output shape:", outputs[0].shape)
rknn.release()
运行结果:
bash
W rknn-toolkit-lite2 version: 2.3.2
I RKNN Runtime Information, librknnrt version: 2.3.2
I RKNN Model Information, version: 6, target platform: rk3588
W Query dynamic range failed (static shape model, ignore this warning)
success output shape: (1, 6, 8400)
推理成功,输出 shape (1, 6, 8400) 与预期一致。
八、依赖版本汇总
训练机(PC)
| 包 | 版本 | 说明 |
|---|---|---|
| Python | 3.10.19 | rknn-toolkit2 支持 3.8/3.10/3.11 |
| torch | 2.11.0+cu128 | 训练用,转换时不需要 |
| ultralytics | 8.4.30 | YOLOv8 完整工具链 |
| onnx | 1.16.0 | 必须降版,2.x 与 rknn 不兼容 |
| onnxslim | 最新 | ONNX 优化 |
| onnxruntime | 最新 | ONNX 推理验证 |
| rknn-toolkit2 | 2.3.2 | --no-deps 安装,避免 torch 降级 |
| numpy | 1.26.4 | rknn-toolkit2 要求 |
| protobuf | 4.25.4 | rknn-toolkit2 要求 <=4.25.4 |
| fast-histogram | 0.14 | rknn-toolkit2 依赖 |
OrangePi(RK3588)
| 包 | 版本 | 说明 |
|---|---|---|
| Python | 3.10 | 系统自带 |
| rknn-toolkit-lite2 | 2.3.2 | 推理库,需与 toolkit2 版本一致 |
| librknnrt.so | 2.3.2 | 替换系统旧版(1.4.0),备份为 .bak |
九、踩坑总结
坑 1:Roboflow 空标签背景图污染验证集
现象: 第一次训练结果 mAP50=0.522,Precision 只有 0.448,但 Recall 接近 1.0。
根因: 第一次手动分割验证集时,脚本 bug 导致带有空标签(无目标)的增强背景图进入了验证集。模型在这些图上的误检全部计入 Precision 分母,把 Precision 拉低。
解决: 重新在 Roboflow 生成数据集,使用平台内置的 80/20 划分,不再手动分割。
坑 2:Roboflow 90° Rotate 增强
现象: 训练时加了 90° Rotate 增强,模型在某些水平摆放的正常物体上误检率偏高。
根因: 俯拍固定场景中相机永远不会旋转 90°,这个增强产生了不符合实际场景的训练样本,让模型学到了错误的先验知识。
解决: 去掉 90° Rotate,只保留 ±15° 的小角度旋转。
坑 3:rknn-toolkit2 安装强制降级 PyTorch
现象: pip install rknn-toolkit2 触发下载 PyTorch 2.4.0(797MB),与现有 torch 2.11 冲突,安装时间极长。
根因: rknn-toolkit2 的 setup.py 里声明了 torch<=2.4.0 的硬性依赖,pip 的依赖解析器会强制降级。
解决: 用 --no-deps 跳过所有依赖安装,然后手动只装真正需要的包。转换功能本身不需要 torch,只有量化校准时才用到。
坑 4:onnx 版本与 rknn-toolkit2 不兼容
**现象:**AttributeError: module 'onnx' has no attribute 'mapping'
根因: rknn-toolkit2 2.3.2 内部调用了 onnx.mapping 这个在 onnx 2.x 中已经被移除的 API。
**解决:**pip install onnx==1.16.0
坑 5:RKNN Runtime 版本不匹配
现象:
librknnrt version: 1.4.0
Invalid RKNN model version 6
RKNN init failed
根因: OrangePi 系统预装了 2022 年的 Runtime 1.4.0,toolkit2 2.3.2 生成的模型格式版本(v6)是新格式,旧 Runtime 不支持。
尝试 LD_PRELOAD 失败: rknnlite 使用 ctypes 直接指定 /usr/lib/librknnrt.so 路径加载,LD_PRELOAD 无法介入 ctypes 的加载过程。
最终解决: 替换系统库,先备份确保可回退。替换前确认系统中无其他项目依赖旧版本,ROS 2 不依赖 rknn,风险可控。
坑 6:Python 文件编码声明
**现象:**SyntaxError: Non-UTF-8 code starting with '\xbc'
根因: 脚本里有中文注释,但没有在文件头声明编码格式。
解决: 所有包含中文的 Python 脚本第一行加:
# -*- coding: utf-8 -*-