YOLOv8 目标检测模型训练与 RK3588 NPU 部署全记录

硬件环境: 训练机 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 多边形工具

  • 类别只有两个:cylinderbox

  • 框要贴近物体轮廓,不留大量空白

  • 同一张图里所有可见物体都必须标注,漏标会让模型把漏标物体学成背景

关于 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 rknnsystemctl list-units | grep rknn 确认系统中没有其他 rknn 项目依赖旧版本。本机依赖 librknnrt.so 的只有 librknn_api.solibrkllmrt.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 -*-
相关推荐
AI视觉网奇5 分钟前
公式动画软件学习笔记
人工智能·公式绘图
天天代码码天天8 分钟前
C# OnnxRuntime 部署 DDColor
人工智能·ddcolor
惠惠软件9 分钟前
豆包 AI 学习投喂与排名优化指南
人工智能·学习·语音识别
数据中心的那点事儿9 分钟前
从设计到运营全链破局 恒华智算专场解锁产业升级密码
大数据·人工智能
FluxMelodySun14 分钟前
机器学习(三十三) 概率图模型与隐马尔可夫模型
人工智能·机器学习
深兰科技18 分钟前
深兰科技与淡水河谷合作推进:矿区示范加速落地
java·人工智能·python·c#·scala·symfony·深兰科技
V搜xhliang024622 分钟前
OpenClaw、AI大模型赋能数据分析与学术科研 学习
人工智能·深度学习·学习·机器学习·数据挖掘·数据分析
PHOSKEY24 分钟前
3D工业相机对焊后缺陷全检——机械手焊接系统质量控制的最后关口
人工智能
Aaron158826 分钟前
8通道测向系统演示科研套件
人工智能·算法·fpga开发·硬件工程·信息与通信·信号处理·基带工程
每天进步一点点️30 分钟前
AI芯片制造的“择优录用”:解读 APU Cluster4 的 Harvesting 机制
人工智能·soc片上系统·半导体芯片