这个系列写到第7篇,前面几篇把YOLO在RK平台上的模型转换、推理加速、摄像头接入都过了一遍。但说句实在话,检测跑通了并不等于业务能落地,能框出商品和能算出顾客该付多少钱之间,还隔着整整一个“业务闭环”的距离。这一篇我们就来解决这个核心问题:顾客开门、拿商品、关门,这三个动作发生之后,系统怎么自动算出一笔正确的订单。内容会聚焦在商品比对算法、结算状态机、RK平台的算力分配,以及大量从真实设备调试里踩出来的坑上,正在做无人货柜边缘视觉方案的开发者可以直接照着这个思路搭。
1. 业务闭环设计:一次购物要拆成多少个状态
1.1 一次完整购物的时间线
无人售货柜的结算逻辑,本质上是个状态机问题。我之前见过不少新手直接写“循环检测,检测完就对比”,这样在演示Demo里能跑通,但一放到真实场景就各种漏单、错扣。原因是真实购物过程有太多中间态,比如顾客开门了但不买、拿起商品又放下、换了另一瓶、甚至把商品拿到手里转了一圈再放回去。
我建议把一次购物拆成这样的状态流:
- 待机状态:设备低频运行,摄像头处于休眠或低帧率模式,只靠人体感应模块(红外、毫米波雷达或微波雷达)判断是否有人接近。
- 唤醒预热:人体感应触发后,摄像头唤醒并开始预采集画面,这个过程一般要提前于开门动作1到2秒,保证开门瞬间摄像头已经处于稳定曝光状态。
- 开门前基准帧采集:门磁或光电开关检测到门打开的瞬间,立刻抓取当前画面作为“基准帧”。这是整个结算逻辑的地基。
- 营业中持续检测:门保持打开期间,YOLO以3到5帧每秒的频率持续检测货架区域,这期间只需要维护检测结果,不需要做结算判断。
- 关门收尾帧采集:门磁检测到关门动作后,再抓取一帧或多帧作为“收尾帧”。
- 差异比对与订单生成:把基准帧和收尾帧做商品差异分析,得到“减少了什么、多出了什么”,生成购物清单。
- 支付扣款与状态复位:调起支付流程,完成后回到待机状态。
这套流程里最容易被忽视的就是第2步。很多团队直接把摄像头设为常开模式,觉得省事,但带来的问题有两个:一是设备长期高负载运行,发热和功耗都在增加,尤其户外柜夏天很容易过热降频;二是常开会录下大量无意义画面,调试时想找一次有效购物记录要翻几个小时录像。所以我强烈建议把人体感应模块做进主流程,用GPIO中断唤醒,而不是靠轮询视频流。
1.2 为什么要“开门前基准帧”
结算逻辑里存在一个先验信息:货架上摆了什么,在开门之前是“已知集合”。无人售货柜不像开放货架,它的SKU是固定的、理货位置也是固定的。既然如此,就不需要每次结算都从头识别整个货架,而应该把“开门前状态”当作标准答案,只关注“变化”的部分。
这就是基准帧最大的价值:它帮我们把问题从“识别所有商品”降维成“识别商品变化”。前者需要解决各种光照、遮挡、视角问题,后者只需要做好前后差异比对。举个生活中的例子,你出门前手机放在桌上,回来发现手机不见了,你不需要重新扫描整个房间才知道桌面少了什么,你只需要对比“之前有什么”和“现在有什么”。售货柜结算也是同样的逻辑。
实际操作中,基准帧的采集时机很讲究。如果抓到的是“门已经打开一半”的画面,顾客手可能已经伸进去了,会把拿取动作的一部分信息破坏掉。所以门磁信号要够快,或者直接用光电开关挡在门缝位置,在门缝露出一条缝的时候就触发采集。另外,基准帧不要只抓一帧,我会连续抓3到5帧,取检测结果最稳定的那一帧作为基准,因为单帧画面可能恰好有反光或者运动模糊。
1.3 摄像头布局与货架区域划分
在讲比对算法之前,必须先说清楚摄像头怎么装。市面上主流的视觉货柜有两种方案:一种是每层一个摄像头垂直向下拍,另一种是柜子对角装两个广角摄像头交叉覆盖。我做的项目用的是每层一个摄像头的方案,原因是这样生成的画面天然是“俯视矩形”,非常容易做货架槽位划分。
每一层货架在画面里可以预先标定出若干个固定区域。比如一层货架宽80厘米,放5个槽位,每个槽位在图像上有固定的多边形坐标。这个坐标在设备安装后做一次标定即可,后续不需要改动。检测框的中心点落在哪个槽位里,就认为该槽位上有一个商品。
区域划分的好处是:比对逻辑不需要做跨帧的目标追踪,只要按槽位对比“基准状态”和“收尾状态”就行。这比跟踪ID要简单得多,也稳定得多。因为售货柜里的商品基本是静态放置的,顾客拿取会让商品短暂移位,但最终要么被拿走、要么被放回某个位置,网格映射能把绝大多数误判挡在门外。
2. 商品比对逻辑:用差异检测还原用户动作
2.1 检测结果的数据模型
YOLO模型输出的是一组检测框,每个框包含4个坐标、一个类别ID、一个置信度。在结算场景里,我们要给这组数据额外加一个“槽位编号”字段。这样一来,每一帧画面都被抽象成一个数组,数组长度等于槽位总数,每个元素记录对应槽位上检测到的商品类别编码。
举个例子,某一层货架有5个槽位,基准帧的槽位状态是:
| 槽位编号 | 基准状态 | 类别ID | 置信度 |
|---|---|---|---|
| 1 | 有商品 | 可乐 | 0.92 |
| 2 | 有商品 | 可乐 | 0.88 |
| 3 | 有商品 | 雪碧 | 0.90 |
| 4 | 有商品 | 雪碧 | 0.85 |
| 5 | 空 | 无 | - |
收尾帧则拿到另一组槽位状态。比对就是逐槽位对比状态差异。需要特别说明的是,这里不要直接用“检测到的类别是否等于基准类别”来判断,因为检测置信度会有波动,同一瓶可乐在不同光照下,模型输出概率可能从0.92掉到0.6,这时候类别ID不变,但置信度变了。对比逻辑应该以“槽位上有没有商品”为第一层判断,而不是以“类别置信度高不高”为第一层判断。
2.2 三层确认:帧内检测、区域映射、消失确认
我在项目里落地了一套“三层确认”机制,专门解决商品被遮挡、手部干扰、短暂移位这些常见问题。
第一层是帧内检测,YOLO在营业状态持续输出每帧的检测结果,这一步不需要做额外处理。第二层是区域映射,把检测框的中心点投影到预先标定的槽位编号。第三层是消失确认,某一槽位连续多帧都检测不到商品,才认为该商品可能被拿走了。
这个三层确认很关键。如果只靠单帧判断,会出现“顾客的手遮住商品一瞬间,漏检一帧,系统就误判为取走”的情况。营业状态用3到5帧每秒的速度采集,连续5帧确认大概需要1到1.7秒,这段时间足够滤掉绝大多数瞬时遮挡。实际测试下来,取消失确认的窗口为5到8帧比较合适,太短误判多,太长会拖慢关门后的结算响应速度。
还有一个细节:关门后的收尾帧不要只拍一帧。因为顾客关门瞬间手可能还在柜门附近,货架上商品被身体或手臂挡住的情况非常常见。我的做法是关门后连续抓5帧,每一帧都做一次槽位状态提取,最后把5帧的状态做投票合并。比如5帧里有4帧认为1号槽位空了,就接受这个结果。
2.3 拿取和放回怎么判定
差异比对的结果会分成三类:
- 商品减少:基准帧有商品,收尾帧没有,判定为“顾客取走”。
- 商品增多:基准帧为空,收尾帧有商品,判定为“顾客放回”,需要在最终的购物车里扣减。
- 类别变化:基准帧是可乐,收尾帧变成了雪碧,这属于“先取走A,又放回B”的组合动作。
类别变化的处理要格外谨慎。真实场景里顾客可能拿起一瓶可乐看了看,又放下,但因为是斜着放的,模型可能会把倾斜的可乐识别成雪碧,导致误判。所以我的策略是:只有当类别变化的置信度同时满足“新类别置信度大于0.6”和“新类别连续3帧稳定出现”两个条件,才接受变化,否则保持基准状态。
放回商品的判定还有个更稳妥的替代方案:如果货柜装了重力感应层板或者压电传感器,可以用重量变化来辅助确认。视觉判定“槽位增加一瓶雪碧”,重力数据同时显示重量净变化在雪碧单瓶重量范围内,两条信息交叉验证,基本就能确定这是放回。视觉与称重双模态融合,是当前无人货柜防错扣最靠谱的方案。
3. YOLO模型在RK平台上的部署与算力规划
3.1 模型选型与算力预算
提到RK平台,市面上目前用得最多的两款芯片是RK3568和RK3588。它们的NPU算力差距很大:RK3568约1 TOPS,RK3588约6 TOPS。这个差距直接决定了你能跑多大的模型、同时跑几路视频流。
我的经验是:
| 平台 | 适合模型 | 单路640x640推理延迟 | 建议推理路数 |
|---|---|---|---|
| RK3568 | YOLOv5s / YOLOv8n | 40-80ms | 1至2路 |
| RK3588 | YOLOv8s / YOLOv8n | 10-20ms | 3至4路 |
如果是RK3568,必须用YOLOv8n这种轻量模型,并且输入分辨率尽量控制在640以内。如果硬跑YOLOv8s,单帧延迟会到150ms以上,营业状态3fps的帧率都很难稳定维持,而且NPU长期满载会导致芯片表面温度升到80度以上,这对户外柜是不可接受的。
如果是RK3588,就可以从容一些。YOLOv8s配合1280的分辨率也能跑到实时,但我仍然建议用YOLOv8n,原因不是算力不够,而是“不需要”。售货柜商品是静态摆放的,目标尺寸相对固定,640分辨率足够识别一瓶罐装饮料;更高的分辨率只会增加预处理和后处理耗时,带来收益却很有限。把节省下来的算力留给多路视频流,让每个货架层都能获得独立的推理通道,远比单层跑超分辨率更有价值。
3.2 RKNN转换与量化
YOLO模型跑在RK平台上,必须经过RKNN-Toolkit2转换成.rknn格式。转换这一步有几个坑值得提前说。
第一,导出ONNX时要把模型输出改成RK平台友好的格式。YOLOv8的原生输出包含3个不同尺度的检测头,转ONNX后建议把输出节点统一concat成一个tensor,这样RKNN在解析时不会出现输出数量对不上的情况。第二,量化数据集一定要用真实货柜图片,不能用COCO的通用图片。这一点非常关键。RKNN默认的量化校准需要几百张图片,如果你喂给它的是COCO数据集里的自然图片,它统计出来的激活值范围跟真实货柜场景差异会很大,量化后的模型在货柜上的精度损失会明显增加。我自己习惯在现场采集100到300张不同光照、不同角度、包含商品和空槽位的图片,做成量化校准集再喂给工具。
下面是一个简化的RKNN转换流程示例:
from rknn.api import RKNN rknn = RKNN() rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform='rk3588' ) ret = rknn.load_onnx(model='yolov8n.onnx') ret = rknn.build(do_quantization=True, dataset='real_cabinet.txt') rknn.export_rknn('yolov8n.rknn')转换完成后,一定不要在PC上直接测精度就完事,必须烧到板子上配合真实摄像头跑一遍。很多时候量化的精度损失只在特定光照下才暴露,PC端用静态图片测试根本发现不了。
3.3 多路并行与系统架构
如果货柜有3层,每层一个摄像头,那么在RK3588上最合理的架构是:把3路摄像头全部接入,每路一个独立的RKNN推理上下文,分别跑同一个模型。RK平台的NPU调度器支持多路并发,3路YOLOv8n同时推理,总延迟会比单路增加约50%,但依然能稳定工作在10到25帧每秒的总吞吐。
线程模型上,我建议拆成三个线程:
- 采集线程:负责从摄像头拉帧,统一缩放到模型输入尺寸,推到推理队列。
- 推理线程:不断从队列取帧,调用RKNN推理,完成NMS和后处理,输出检测结果。
- 业务线程:消费检测结果,维护每个槽位的状态机,触发基准帧/收尾帧的保存,生成订单。
注意,不要让业务逻辑阻塞推理线程。如果NMS写在Python里,而推理是在C接口层跑的,那么在Python侧做NMS会严重影响吞吐。建议把后处理也下沉到C或C++层,或者至少用numpy向量化操作,避免逐框循环。
4. 自动结算与异常兜底:既不错扣也不漏扣
4.1 从检测类别到商品价格的映射
检测模型输出的类别ID,必须经过一张SKU映射表转换成具体的商品名和单价。这张表是部署时由商家配置的,包括类别ID、商品名称、单价、条码、重量这些字段。比如类别ID 0对应的可能是“330ml罐装可乐”,单价3元。
结算的核心逻辑就是维护一个购物车字典:
cart = {} # key为SKUID,value为数量 # 遍历槽位差异 for slot_id in diff_result: if diff_result[slot_id] == "商品减少": sku = sku_table[slot_id] cart[sku.id] = cart.get(sku.id, 0) + 1 elif diff_result[slot_id] == "商品增多": sku = sku_table[slot_id] cart[sku.id] = cart.get(sku.id, 0) - 1这里要注意,某些SKU包装尺寸较大,一个商品可能同时跨两个相邻槽位。这时候区域映射会产生“一个商品占两个槽位编号”的情况。我的解决方案是:在槽位标定时,把这类大件商品的槽位合并,标记为“占用跨区”,比对时按合并区域处理。否则结算会出现同一瓶饮料被算两次的情况。
4.2 防错扣的时间稳定性校验
前面提到用“连续N帧消失确认”来过滤瞬时遮挡,但还有一类更难处理的情况:“商品短暂被拿起并放回,但放回位置偏了,模型在收尾帧里没检测到”。比如顾客拿起一瓶矿泉水,看了看,又放回,但瓶身是倒着放的,模型对这种非直立姿态的检出率会下降。
如果系统只做“基准帧 vs 收尾帧”的对比,这类情况会被误判为“取走”,造成错扣。所以我增加了“最终态复核”机制:在收尾帧判定某个槽位减少后,再翻看营业状态最后10帧的检测记录,如果该槽位在这10帧里曾经被重新检测到同类别商品,则取消这次“减少”判定。这个机制有点类似电商系统里的“先冻结后确认”,能显著减少临时起意放回导致的误扣。
时间稳定性校验的完整规则我整理成了这样:
| 场景 | 判定规则 | 说明 |
|---|---|---|
| 正常取走 | 槽位连续8帧为空,且收尾复核帧仍为空 | 确认扣款 |
| 拿起又放回 | 槽位短暂为空后重新出现同类别商品 | 取消减少判定 |
| 换货 | 基准类别A消失,收尾出现类别B且B稳定3帧 | 按减少A、增加B处理 |
| 疑似误检 | 基准和收尾类别一致,但中间有跳变 | 保持基准状态不动 |
4.3 云端复核与多模态兜底
无论边缘逻辑写得多严谨,总会有意想不到的情况。我的项目里保留了云边协同的兜底通路:当边缘侧出现无法确认的差异(比如槽位状态冲突、置信度低于阈值、或者同一个槽位在基准和收尾都检测到商品但类别ID不同)时,设备会把基准帧和收尾帧的裁剪图、检测结果、时间戳打包上传到云端,由云端更强大的模型或者运营人员远程复核。
云端复核不是实时路径,会有几秒到十几秒的延迟,所以用户看到的是“订单生成中”的状态。这里需要设计一个合理的超时机制,如果30秒内云端没有返回结果,先按边缘侧结果扣款,后续有争议再走退款流程。这样既不被云端延迟卡脖子,又能在大部分异常场景保住准确率。
如果设备装了称重传感器,我强烈建议把重量变化作为交叉验证通道接入结算逻辑。视觉识别“少了1瓶可乐”,重量显示净变化只有50克,明显不对,这时候就应该触发云端复核,而不是直接扣款。多模态融合的价值在这种边缘Case里体现得最明显。
4.4 多人同时购物的处理
多人同时在柜前购物,对视觉结算来说是灾难级的挑战。单目视觉很难区分“两个人分别拿了什么”,因为画面里可能出现交叉遮挡、商品被传递、甚至一个人开门另一个人伸手进来等复杂情况。
我目前的处理方案是分等级应对:
- 基础场景:限定同一时间只允许一个顾客操作,判断依据是柜门前的人体感应是否持续被单目标占用。如果检测到多个独立人体目标同时存在,系统会进入“保守模式”。
- 保守模式:每一笔差异都额外叠加云端复核,扣款前需要额外确认,甚至引导顾客分次扫码开门。
- 长期方向:把摄像头从单目升级为双目立体视觉,结合深度信息做人体姿态估计和手部轨迹追踪,这样才能在多人场景下更可靠地关联“谁拿了什么”。这个方向我这边的项目还在迭代,后续有阶段性成果再单独写一篇。
5. 实战踩过的坑与排查思路
5.1 玻璃反光与虚检
无人售货柜几乎都带玻璃门,而玻璃反光会让YOLO凭空“看到”不存在的商品。我调试时遇到过最离谱的情况:柜内明明什么东西都没有,模型在空货架上检测出3瓶饮料,仔细看才发现是玻璃门反射了旁边路人衣服上的图案。
应对反光,我试过几种手段。物理层面最简单的是给摄像头加偏振镜,能大幅削弱玻璃反射的偏振光。软件层面,可以在采集线程里做一次简单的区域掩膜,把玻璃边缘区域置黑,让检测器忽略这些区域。还有人用“检测结果必须在基准帧和收尾帧之间保持一致才采信”这个规则来过滤虚检,但这只能过滤开门前后的虚检,营业状态里的虚检依然可能造成误确认,所以最好的办法还是从源头削弱。
5.2 相似包装SKU的区分困难
当货柜卖的是同一品牌不同口味的饮料时,模型分类很容易混淆。可乐和零度可乐、雪碧和柠檬味雪碧,外观差异极小,YOLO这种通用目标检测网络在低分辨率输入下很难区分它们。
我的建议是不要指望在检测阶段就精准分类。可以把检测模型的任务简化为“只判断槽位上有商品还是没商品”,把“具体是哪个SKU”交给另一个环节来解决。具体做法是:检测到槽位商品后,中心点附近的区域再裁剪成小图,用一个小型分类网络或者图像检索模块做精细识别。这个二次分类网络的输入可以更大一些,比如96x96或128x128,识别精度远高于把检测网络的类别数强行加多。此外,我还在商品外包装上加了二维码/条形码作为终极兜底,让用户在结算确认页自主选择口味,双保险。
5.3 光照变化导致检测抖动
室内售货柜还好,灯光基本恒定。户外柜就不一样了,正午阳光直射和夜间补光灯下的画面亮度差异非常大。YOLO在量化之后对光照变化更敏感,可能白天检测正常,到了傍晚就开始丢检测框。
我解决这个问题的思路有三个层面。第一,在摄像头SENSOR的AE算法上下功夫,把曝光目标区锁定在货架区域,而不是全画面平均亮度,避免天空或地面过曝影响货架区域。第二,做输入归一化时,不要把亮度信息全丢给模型自适应,建议在采集链路增加一个简单的直方图均衡或CLAHE预处理,把亮度抖动压平。第三,量化校准集里刻意加入不同光照的图片,让量化过程保留对亮度变化的鲁棒性。这三招叠加之后,夜间和白天切换时候的检测稳定性有明显改善。
5.4 调试神器:订单事件回放系统
做无人货柜开发,最痛苦的不是模型训练,而是出了错单、漏单之后,没法快速定位原因。所以我做了一个“事件回放”工具,把每次购物的关键信息完整保存下来,包括:开门前基准帧图片、关门后收尾帧图片、营业状态的所有检测结果、槽位状态变化记录、最终生成的购物清单、支付结果。
每次发生用户投诉或者账单争议,我只需要调出这条订单记录,按照时间线把状态变化逐帧播放出来,就能很快定位是“检测漏框”“分类错误”“还是逻辑Bug”。这个工具也可以用来持续迭代模型:攒够一批误检样本后,把它们加入下一轮训练集,模型越跑越准。建议所有做货柜视觉结算的团队,一定要在开发初期就把事件回放能力做进去,不然后期排查问题会崩溃。
5.5 从Demo到可部署,差的不只是准确率
最后说一个心态上的体会。很多开发者在实验室里把YOLO跑通了,觉得“精度还行”,就直接拉到现场部署,结果上线第一天就错单不断。我踩过同样的坑,事后复盘发现,问题往往不在模型本身,而在工程链路:视频采集帧率不稳定、门磁信号抖动导致基准帧抓早或抓晚、网络协议栈阻塞导致结算上报超时、设备掉电后状态没有恢复完整。
我的建议是,无论你的模型精度是多少,先假设它会出错,然后把所有能兜底的工程机制都做上:状态级联、事件回放、云端复核、人工介入通道。视觉只是这个系统里的一环,真正让用户信任这台风柜的,是它出错之后能有多快被纠正过来。
回到这个系列的主题,YOLO在RK平台上的部署只是手段,自动结算才是真正创造商业价值的一公里。我个人做下来的经验是:先把一个SKU、一层货架、一个柜子的闭环跑通,不要一开始就追求全品类、全场景覆盖。把基准帧、区域映射、消失确认、收尾复核这几条核心逻辑吃透,再往后加功能就会顺利很多。后续还可以在这个框架上继续扩展:多人购物识别、手势轨迹追踪、甚至根据顾客拿取动作做营销推送,这些都是很有价值的演进方向,我们后面找机会再聊。