做无人售货柜项目,绕不开的一个核心问题:怎么稳定识别用户从柜子里拿走的到底是哪件商品。我前面两篇聊过硬件选型和整体系统框架,这篇终于到硬核环节——在RK平台上跑通YOLO商品识别的完整链路:环境搭建、数据集整理、模型训练、转RKNN部署,一条线走下来。这篇我尽量把每一条命令都写清楚,不玩虚的,适合正在做边缘视觉项目、尤其是不太熟悉RK平台NPU开发流程的朋友照着敲。
先说清楚这篇解决什么问题:把“摄像头拍到商品图”变成“程序直接告诉你这是可乐还是薯片”这套流程落地。R系列开发板负责算,YOLO模型负责认。后面所有内容都是围绕这个目标展开的。
1. 先搞清楚方案为什么长这样
1.1 商品识别为什么比想象中难
很多第一次做售货柜的朋友会觉得:识别商品不就是普通的目标检测吗?拿个开源模型就能跑。真上线之后你会发现事情没那么简单。
无人售货柜里的商品有这几个天然debuff:塑料瓶和易拉罐反光严重,PET材质瓶身几乎是透明的,袋装零食包装上的图案和文字会随着摆放角度发生折叠变形;柜内光照不均匀,某个角落可能长期处于暗区或者强曝光;最麻烦的是用户拿取商品时,手会遮挡住目标,可能连续好几帧都看不到完整瓶身。再加上货道上的商品排列密集,同类商品摆在一起,边界框很容易重叠。
所以这个场景不能靠简单的分类网络解决,必须用目标检测把“哪个位置是什么商品”一次性找出来。这也是为什么我用YOLO系列而不是ImageNet分类模型的原因——既要定位,又要分类,YOLO这类单阶段检测器在边缘设备上部署最成熟。
1.2 为什么是RK平台加YOLO而不是别的组合
先放一段方案对比,都是我实际调研过的路:
| 方案 | 成本 | 识别精度 | 维护性 | 适合场景 |
|---|---|---|---|---|
| 重力/重量传感器 | 低 | 差,相似重量商品完全无法区分 | 需要频繁校准 | 单货道形态单一的小柜子 |
| RFID标签 | 标签成本高,柜子改造成本高 | 只认标签不认商品,标签脱落就没辙 | 每个商品都要贴标 | 高端短保商品,SKU少的场景 |
| 云端视觉识别 | 算法迭代快 | 精度高,但依赖网络 | 需购GPU资源,延迟不可控 | 对延迟不敏感的场景 |
| 边缘视觉(RK + YOLO) | 板卡几百块,无功耗压力 | 精度够用,离线可用 | 模型可本地更新 | 售货柜、自助终端这类边缘设备 |
我当时选RK平台核心原因是两点:一是板卡上集成了NPU,跑量化后的YOLO模型完全没问题,整机功耗能压住;二是产品部署在现场,断网或者网络抖动时识别绝不能停,所有推理必须在本地完成。如果某天云端临时不可用,整个柜子就瘫痪了,这在商场上可没法交代。
1.3 整体流程只有这么长
不搞复杂架构,一条线走通即可:
摄像头画面 -> 抽帧 -> 图像预处理 -> RK板NPU推理(YOLO检测) -> 后处理NMS -> 识别结果与货道映射 -> 生成扣费订单
后两级关于业务逻辑的部分我之前那篇已经聊过,这篇集中在红框部分:环境、训练、转换、部署。搞清楚这个边界,后面看流程不会迷路。
2. 环境搭建:先把训练侧和板端侧两套环境洗干净
标题写着“一行行代码带敲”,那这一节就实打实来。环境分两套,一套是训练模型用的PC环境,另一套是RK板上跑推理的环境,千万别混在一起。
2.1 训练侧:Ubuntu + CUDA + PyTorch环境
我首选Ubuntu 20.04或者22.04,Windows也不是不行,但在CUDA环境和后续RKNN转换时总有一些莫名其妙的坑,老老实实Ubuntu省心。
装好系统之后,第一步确认显卡驱动:
nvidia-smi看到类似CUDA Version: 12.4的输出就行。驱动版本会影响你装CUDA工具链,但实际训练时PyTorch自带的CUDA runtime和你系统装的CUDA工具链是解耦的,主要看驱动支持的上限版本,不用强行装最新的CUDA Toolkit。
接下来创建独立环境,别一上来就pip install到系统Python里,后面版本冲突会让你怀疑人生:
conda create -n yolo python=3.10 -y conda activate yoloPython版本我建议3.10,YOLOv8和RKNN工具链在这个版本下兼容性最好,3.12某些底层库还没跟上。
安装PyTorch,CUDA版本根据自己的驱动来选:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121然后安装YOLO训练框架:
pip install ultralytics验证环境是否可用:
python -c "import torch; print(torch.cuda.is_available()); print(torch.__version__)"输出True说明GPU这边通了。这里有个常见问题:明明有NVIDIA显卡却输出False,多半是驱动没装好或者PyTorch版本和CUDA不匹配,下载的wheel跟你机器驱动不兼容。这个问题排查思路就一个:nvidia-smi能看到驱动,PyTorch才能用上CUDA,逐层确认。
2.2 RK侧环境:主机端rknn-toolkit2
RK平台的推理流程是:先用RKNN-Toolkit2在PC上将训练好的模型转换成RKNN格式,再拷贝到板子上用运行时库跑。这是典型的交叉开发模式——训练和转换在PC,推理在板子。
所以主机的另一个环境是rknn-toolkit2。我一般是单独建一个conda环境,避免和训练环境打架:
conda create -n rknn python=3.10 -y conda activate rknn pip install rknn-toolkit2==1.5.2版本号要看你的NPU驱动和板端runtime版本。这里有个硬性要求:主机端的rknn-toolkit2版本和板端的rknpu2运行时库版本必须对应,跨版本转换出来的模型在板子上可能直接崩溃。我建议你先确定板子的NPU驱动版本,再倒推安装匹配的工具链。
2.3 板载环境:rknpu2运行时库
板子端我用的系统是Ubuntu一个小众镜像,自带NPU驱动和运行时库。如果板子是干净系统,需要把rknpu2的运行时库拷进去:
git clone https://github.com/airockchip/rknpu2 cd rknpu2/runtime/Linux/librknn_api/aarch64 cp librknnrt.so /usr/lib/然后在板子上用Python调推理的话,再把rknn-toolkit2里的rknn推理接口库拷到项目目录就行。注意板端不用装完整版rknn-toolkit2,只需要运行时库和Python封装。
提示:rknpu2运行时库的版本必须和模型转换时用的rknn-toolkit2版本一致。这条规则优先级最高,很多“模型在板子上跑不了”的问题都是这俩版本没对上。
3. 数据准备:商品识别训练的地基工程
模型训练这事,数据质量决定精度上限。YOLO再强,标注错误的数据喂进去,训练出来也是一团浆糊。
3.1 数据采集:别全拿网图糊弄
我踩过一个大坑:一开始为了省事,大量使用商品官网图、白底图训练,结果放到现场识别率打对折。原因是现场柜内的光照、角度、背景和官网图差距太大,模型学到的特征根本没法迁移。
正确的采集方式是在实际的货柜内部或者模拟柜体内拍:
- 多角度:正视、俯视、侧视45度、用户拿取时的斜角度
- 多光照:白天自然光、夜间LED灯带、曝光不足的暗角情况
- 多放置状态:正放、倒放、横放、半遮挡、多件商品紧挨
我在项目里是拿一台USB摄像头架在柜内固定位置,把商品摆在不同货道,连续录制视频然后抽帧,能高效产生大量带真实场景噪声的样本。一帧一帧拍摄太慢了,视频抽帧能让你半天搞定上千张图。
3.2 标注格式与工具
标注工具我用LabelImg偏多,因为它导出YOLO格式很直接,界面也简单。框选目标后,直接保存会生成一个同名的txt文件,内容格式如下:
0 0.523437 0.482638 0.226562 0.319444 1 0.761718 0.206018 0.140625 0.277778这五个数字依次是:类别ID、边界框中心点x坐标(归一化)、中心点y坐标(归一化)、框宽度(归一化)、框高度(归一化)。归一化是相对于图片宽高而言的。
一个容易忽略的点:类别ID顺序在训练集和数据集配置文件中必须严格一致,最好一开始就用一个类别清单固定住。我项目里维护了一份classes.txt,比如:
0 可乐 1 雪碧 2 某品牌薯片 3 某品牌苏打水 ...加新商品时只追加,不修改已有ID,否则以前标注的txt全部要重做。
3.3 目录结构与划分
YOLO训练框架对目录结构有约定,我用的是最常见的布局:
dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── dataset.yaml批量划分可以用一个简单脚本:
import os, random, shutil random.seed(42) data_dir = "all_imgs" train_dir = "dataset/images/train" val_dir = "dataset/images/val" os.makedirs(train_dir, exist_ok=True) os.makedirs(val_dir, exist_ok=True) imgs = [f for f in os.listdir(data_dir) if f.endswith(".jpg")] val_batch = random.sample(imgs, int(len(imgs) * 0.2)) for img in imgs: src_img = os.path.join(data_dir, img) src_label = os.path.join("all_labels", img.replace(".jpg", ".txt")) if img in val_batch: shutil.copy(src_img, val_dir) shutil.copy(src_label, "dataset/labels/val") else: shutil.copy(src_img, train_dir) shutil.copy(src_label, "dataset/labels/train")比例我习惯8:2。这里有一个想法要说清楚:验证集不只是随便留出20%就完事,验证集里的图片场景分布要尽量贴近真实部署环境,别训练集全是白天光线,验证集全是暗光,那样loss曲线会很有欺骗性。
3.4 数据增强和负样本
YOLOv8内置了mosaic、翻转、色域变换这些增强手段,默认开,不用多操心。但有一个东西必须自己加:负样本。
什么叫负样本?就是完全不包含任何商品的图片,比如柜内空无一物的背景、手伸进去但没抓东西的瞬间。这些图对应的txt文件是空的。加入负样本是为了降低误检率,防止模型在处理空货道时乱报目标。我建议负样本比例控制在总样本量的10%左右,过多会稀释正样本的学习。
4. 一行行跑通YOLO训练全流程
4.1 模型选型:YOLOv8n还是YOLOv8s
YOLOv8有几个规格,nano、small、medium,参数量和计算量递增。边缘部署不是越大越好,关键看NPU算力和延迟预算。
我实测下来在RK平台上:
| 模型规格 | 参数量 | mAP50-95(典型商品集) | 单帧延迟(NPU,INT8量化后) |
|---|---|---|---|
| YOLOv8n | 约3.2M | 0.55-0.62 | 约20-35ms |
| YOLOv8s | 约11.2M | 0.62-0.70 | 约40-60ms |
| YOLOv8m | 约25.9M | 0.68-0.75 | 约80-120ms |
无人售货柜的识别帧率不需要太高,拿到稳定的识别结果比追求高帧率更关键,但延迟也不能超过200ms,不然用户体验很割裂。我最终选了YOLOv8s做权衡,如果柜内SKU不多,目标种类就十来个,直接用YOLOv8n也完全够用,推理速度还能快一倍。
4.2 修改数据集配置和超参数
在项目目录下写一个dataset.yaml:
path: /home/user/project/dataset train: images/train val: images/val names: 0: cola 1: sprite 2: chips 3: sodanames里的顺序和标注txt里的类别ID一致。这一步错位,训练出来的模型预测结果就全是乱的。
训练时我用的命令行参数如下:
yolo detect train \ data=dataset.yaml \ model=yolov8s.pt \ epochs=200 \ imgsz=640 \ batch=32 \ device=0 \ lr0=0.01 \ patience=50逐行说明:
data:刚写的dataset.yaml路径model:先用官方预训练权重做迁移学习,而不是从随机初始化开始训练。迁移学习能显著缩短收敛时间,商品数据本身就少,用预训练权重是常规操作epochs:200轮起步。如果你数据量只有几百张,开太多轮容易过拟合,配合下面的早停机制imgsz:输入分辨率。商品识别里很多目标是中小尺寸,640够用。如果发现小目标漏检严重,可以提高到960,但推理速度会明显下降batch:取决于GPU显存,32G显存跑YOLOv8s这个量级没压力;显存不够就把batch降到16lr0:初始学习率0.01是默认值,迁移学习时不用改。如果训练时loss剧烈震荡,可以降到0.005patience:验证集指标连续50轮不提升就自动停止,省时间
4.3 训练中间看什么
训练启动后别干等,重点关注两个东西:train/loss曲线和验证阶段的mAP指标。
正常情况下loss曲线是平滑下降的,如果出现loss先降后升,八成是学习率过大或者过拟合。mAP50在商品识别里会比mAP50-95高得多,因为商品类别的框基本都是方正的,对IoU没那么敏感。
训练结束后,在runs/detect/train/weights/下会生成best.pt和last.pt。best.pt是验证集上表现最好的权重,部署用这个。
4.4 一次完整的验证
训练完先别急着转RKNN,在PC上用测试图看一下效果:
yolo predict model=runs/detect/train/weights/best.pt source=test_images/ imgsz=640输出结果里会有识别出的框、类别和置信度。这一步看着简单,却能提前发现一堆问题:类别有没有错乱、框有没有偏移、是不是明显漏检。如果这一步效果就很差,先回去查数据标注,模型本身的问题反而少。
5. 模型转换:从PyTorch到RKNN
这是RK平台项目里最核心的一步,也是拦路虎最多的一步。很多人在这一步卡住,看到各种算子不支持就慌了。
5.1 PyTorch模型导出ONNX
RKNN-Toolkit不能直接吃PyTorch的pt权重,要先转成ONNX格式。还是用工具链:
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640 opset=12opset=12要特别注意。RKNN工具链对太新的ONNX算子支持有限,opset超过12后,某些算子转换时会炸。我自己就遇到过opset=17导出后,转换报“不支持某个Split变体”的错,改成12就一切正常。
导出的best.onnx可以用Netron打开可视化,检查输出节点对不对。YOLOv8的ONNX输出格式和v5不同,是一个1x84x8400的张量,8400是不同尺度特征图上的候选框数量,84是4个坐标、1个置信度加80个类别分数。做部署后处理时要按这个格式解析,别套v5的旧代码。
5.2 RKNN转换脚本
新建一个convert.py:
from rknn.api import RKNN rknn = RKNN() rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform="rk3588", quantized_dtype="w8a8" ) ret = rknn.load_onnx(model="best.onnx") if ret != 0: print("load onnx failed") exit(-1) ret = rknn.build(do_quantization=True, dataset="quant_dataset.txt") if ret != 0: print("build failed") exit(-1) rknn.export_rknn("best.rknn") rknn.release()几个参数逐个说明:
mean_values和std_values:做输入归一化用的。我训练时没做特殊归一化,所以这里就是经典的0减去后除以255。如果你训练时用了自定义归一化,这里必须和训练时完全一致,否则推理结果会差到离谱target_platform:指定NPU平台型号,不同平台生成模型格式不能互通quantized_dtype="w8a8":权重和激活都量化为INT8。RK平台在INT8下推理速度远快于FP16,前提是校准集质量要好
5.3 量化校准集
quant_dataset.txt文件里列的是用于量化的图片路径,每行一张图。这个校准集是INT8量化误差的决定因素,比模型结构本身还关键。
我写的第一版校准集用的全是训练集图片,量化后精度下降了几个点,换成现场柜内实拍图之后,精度几乎无损。因为量化的本质是用一组真实输入来统计每层的激活值分布,校准集的分布越接近实际场景,量化误差就越小。
校准集我一般放200到300张图,覆盖所有SKU、不同光照和角度。文件内容就是一行一个路径,没有其他花头:
/home/user/quant_imgs/001.jpg /home/user/quant_imgs/002.jpg ...5.4 RK平台板端推理
模型转换出来之后,写一个板端推理脚本,Python跑起来验证:
import numpy as np from rknnlite.api import RKNNLite rknn_lite = RKNNLite() rknn_lite.load_rknn("best.rknn") rknn_lite.init_runtime() img = cv2.imread("test.jpg") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (640, 640)) img = img.astype(np.float32) / 255.0 img = np.expand_dims(img, axis=0) outputs = rknn_lite.inference(inputs=[img])这一步很容易出事儿的地方在两处。第一,缩放方式:训练时YOLO默认用letterbox保持宽高比的填充式缩放,板端推理如果直接resize拉伸,框的位置会错位。要么按letterbox方式处理,要么在解析坐标时补偿缩放比例。第二,输入张量布局:rknn推理接口默认接受HWC的numpy输入,不需要自己转NCHW。
outputs拿到的是一个1x84x8400的数组。解码坐标时要把x、y、w、h还原到原图尺度:
h, w = orig_img.shape[:2] gain = min(640 / w, 640 / h) pad_x = (640 - w * gain) / 2 pad_y = (640 - h * gain) / 2然后把每个候选框的坐标减去pad再除以gain,就得到原图坐标。最后再做NMS,YOLO默认的输出里一个目标会有多个候选框,不滤掉重框没法用。
6. 常见问题排查与避坑速查
做成一张表,都是我实际踩过的或者同行反复问过的:
| 现象 | 可能原因 | 排查顺序与解决办法 |
|---|---|---|
| 训练时loss是NaN | 学习率过大、数据集有标注越界 | 先看标注txt里有没有大于1或者小于0的坐标,再降lr到0.001 |
| mAP很高但现场识别差 | 训练集和现场场景分布差异大 | 重新采集现场图加到训练集,重点覆盖不同光照 |
| RKNN转换报算子错误 | ONNX版本过高、模型结构过新 | 导出ONNX时固定opset=12,换YOLOv8这种原生支持的模型 |
| 量化后精度掉3个点以上 | 校准集太少或分布偏差大 | 校准集增加到300张,全部用现场实拍图 |
| 板端推理结果全为零 | 输入预处理和训练不一致 | 检查letterbox、归一化方式、数据布局 |
| 推理耗时高 | 模型规格大、输入分辨率高 | 换YOLOv8n、imgsz降到640、确认INT8量化生效 |
| 重复框一大堆 | 后处理NMS没做或阈值不对 | 自写NMS,IoU阈值设0.45-0.5,置信度阈值0.25 |
| 两个相似商品经常混淆 | 类别内视觉差异太小 | 增加该类样本量、单独补充难负样本、考虑把相似商品合并成一个SKU处理 |
其中一个很有代表性的问题:量化后精度看起来还行,但部署到现场发现某个特定角度、特定光线下总是漏检。这种问题往往是量化校准集里正好缺少这个角度的图,把对应图片补进校准集重新量化即可,收费昂贵但见效快。
还有一个容易被忽略的时延细节:在板端如果每个摄像机帧都做一次完整推理,NPU占用率会很高。无人售货柜场景下,我采用“运动检测触发”策略——只有画面中检测到位移变化时才跑YOLO推理。实测下来整机功耗和发热都明显下降,识别准确率也没受影响。
最后分享两个我在实际项目里的体会
这套流程走通一次之后,再回头做其他边缘识别项目就轻车熟路了。我个人体会最深的几点:
数据质量真的决定精度天花板。训练模型本身只是流水线操作,花大量时间在调参上,不如多拍500张现场照片、多标注一轮来的收益大。我在这个项目里前前后后采集了三轮数据,每一轮都让准确率上了一个台阶。
RK平台的NPU量化是一把双刃剑,INT8推理速度快,但如果校准集没弄好,精度损失会远超预期。我的习惯是每次转换前先拿100张验证集图做一次量化精度对比,确定校准集没问题再往板子上部署。
最后,别追求一步到位。先拿YOLOv8n跑通全链路,再根据实测延迟和精度换s或者调整分辨率。链路通了你才有底气做后续优化。祝各位售货柜项目早日落地稳定出单。