
上个月去社区门口的一家便利店买饮料货架上我最常喝的那个口味空了三个位置店员苦笑着说白天高峰期根本顾不上逐层检查等发现时已经错过了一整天的销售窗口。这个场景让我想起之前做过的视觉识别项目——与其让店员用肉眼巡逻不如直接在货架上方装一双“电子眼”。于是我用两周时间从零做了一套智能零售货架商品缺货识别监控系统硬件选型、现场接线、算法训练、告警推送全部自己动手跑通。这篇文章是完整的复盘记录包含所有踩过的坑和最终可复现的方案给正在做智慧零售视觉项目、或者想在边缘设备上落地识别的读者一个参考。1. 项目定位给“没人盯的货架”装一双电子眼1.1 缺货问题的真实成本很多没做过零售的人会低估缺货的成本。商品在货架上的空位不止是少卖一件货的问题它直接影响消费者对门店的印象反复缺货会让顾客默认“这家店总是缺东西”慢慢流失到别处。零售行业的公开统计口径里缺货导致的销售额损失通常在3%到5%之间对一家日营业额一两万的小便利店来说一年下来就是一笔不小的数字。但现实是门店补货主要靠店员定时巡检。一个中大型便利店有几千个SKU货架层板多、排面深店员一天能认真巡查两三遍已经很不错。夜间值班、节假日高峰期、促销活动期间货架空置的时间窗口会被拉得很长。我调研的这家便利店店长每天会在开店前和傍晚各查一次货中午和晚上的高峰时段基本靠运气。这套系统的目标很直接用固定在货架上方的高清摄像头定时拍照通过边缘计算设备上的视觉算法识别货架上的空位一旦某个商品排面出现连续的空置状态就自动推送告警到店长手机和门店看板。省掉人工巡检的重复劳动把“发现缺货”的时间从小时级压缩到分钟级。1.2 系统三层架构整个系统按数据流方向分成三层这也是我推荐做此类项目时采用的标准结构采集层货架上方安装IP摄像头通过RTSP协议提供视频流或定时抓图。摄像头固定后视角不再变动这是后续算法稳定的前提。推理层货架附近的边缘计算板卡负责拉取图像、运行缺货检测模型把每个货架区域的状态写入本地数据库。所有推理都在本地完成不依赖云端网络断了也能继续工作。业务层告警推送、Web看板、历史报表。这一层可以跑在同一块板卡上也可以另配一台小主机看门店规模。对于单货架或两三个货架的试点项目三层可以合并在一块边缘板卡上。我实际部署时就是把推理和业务层放在同一台设备里系统结构简单故障点也少。1.3 技术选型的边界做之前我先划了一条线不做重量级方案。大型商超那种每层货架装电子价签、用压力传感货道判断库存的方案成本太高不适合普通便利店。摄像头加视觉算法是性价比最优的路径但它的短板也明显——只能看到“货架外立面”看不到货架深处的真实库存。所以这个系统定位是“排面缺货提醒”属于补货环节的前置预警而不是库存盘点系统。明确这个边界很重要它决定了后面的硬件投入、算法复杂度、以及门店对告警精度的期待值。2. 硬件选型这一步决定整个系统的上限2.1 摄像头分辨率、镜头与稳定性货架监控摄像头不能随便拿个家用摄像头凑合关键参数有三个分辨率、镜头焦距、以及关键自动功能是否可关闭。先说分辨率。标准便利店货架单组宽度大约1.2米有四五层隔板每层又分成若干个商品排面。如果要让算法识别出“某个排面空了”那每个商品单元的成像宽度至少要有30到40个像素。我用的摄像头输出1920乘1080实测对于1.2米宽的货架在2.5米高度俯视拍摄每个单瓶饮料的排面大约能占到60到80像素宽识别余量足够。如果货架更宽或者想看清小包装商品比如口香糖、糖果建议上400万像素模组。镜头焦距方面2.8mm到4mm比较合适视角够宽又不至于产生严重畸变。鱼眼镜头坚决不要用边缘畸变会让排面边界扭曲算法标定时会非常痛苦。我最终选了4mm定焦镜头垂直视角约60度正好覆盖一组货架。最容易被忽略的是自动功能的开关。家用摄像头默认开启自动曝光、自动白平衡、自动对焦这些功能在监控场景里反而是灾难。同一个货架早上阳光照进来和白天的荧光灯照射下画面色彩和亮度差异很大算法如果拿傍晚的图和早上的基线图做比较误报率会高到没法用。选摄像头时一定要确认支持手动固定曝光、固定白平衡而且能关闭自动对焦。工业相机或支持SDK调节的IPC一般都可以买之前问清楚不行就退货。我用的摄像头参数对比可以看这个表参数推荐范围说明分辨率1920×1080起步400万更稳保证每个排面至少30-40像素宽镜头焦距2.8mm-4mm定焦视角合适畸变可控光圈F1.6-F2.0室内光照足够大光圈夜视更好协议支持RTSP/ONVIF方便程序取流自动功能必须可手动关闭固定曝光白平衡这是不误报的前提2.2 边缘算力跑得动模型又不至于太贵推理设备的选择直接决定软件方案能跑多复杂的模型。我对比过三类方案第一类是x86迷你主机。优势是兼容性好环境配置简单Python生态完全无障碍OpenCV、PyTorch随便装。缺点是功耗高一般20到30瓦长期放在货架旁边对散热有要求而且价格不便宜。适合快速原型验证不适合作为最终部署形态。第二类是带NPU的ARM边缘板卡。这类板卡以低功耗、被动散热为卖点整板功耗五到十瓦可以7乘24小时运行价格比迷你主机低不少。跑轻量化的YOLO模型NPU加速后640输入分辨率推理速度大约在10到15帧每秒对于几分钟一次的巡检任务完全够用。缺点是环境配置坑多尤其是ONNX Runtime和NPU工具链的安装后面会细说。第三类是直接用普通ARM开发板不带NPU。这个方案只适合跑背景差分这类轻量算法跑YOLO会非常吃力CPU推理一张图要两三秒检查任务勉强能用但没什么余量。我的建议是如果你想快速把Demo跑起来先用x86迷你主机确定要长期部署换成带NPU的ARM板卡。我最终在试点门店用的是ARM板卡一块就够了同时跑采集、推理、Web服务三个进程CPU占用率大约60%。2.3 供电与网络最容易翻车的隐形环节硬件选型里最容易被新手低估的就是供电。零售门店的现场条件和实验室完全不同接线距离长、插座位置刁钻、还有各种电磁干扰。摄像头供电有两种主流方式POE供电和独立电源适配器。POE的优势是网线同时传数据和电力布线少一路强电而且POE交换机可以集中管理断电能远程看出来。独立电源适配器成本低但需要每个摄像头附近有插座而且多个适配器散落在货架区既不美观也不安全。这里有个很多人不知道的坑长距离低压直流供电的压降。12V电源通过20米网线或者细导线给摄像头供电如果导线截面积不够电压会掉到10V以下摄像头反复重启。我算过一笔账铜的电阻率约0.0175欧姆每平方毫米每米一根10米的0.5平方毫米导线电阻约0.35欧姆摄像头工作电流1A时压降0.35V单根线还能接受。但如果一台电源串联给三四台摄像头供电总电流到3A压降就超过1V摄像头在临界电压下工作故障率急剧上升。所以供电布线我坚持一个原则每个摄像头或每两个相邻摄像头单独拉线回POE交换机或电源分配器不串联不并联超载。网络方面用一个八口千兆交换机就够摄像头单独划分一个IP网段和门店收银系统的网络物理隔离避免视频流量干扰POS业务。3. 现场接线与安装施工顺序决定日后维护成本3.1 安装位置与视角标定摄像头的安装位置是整套系统里最不能返工的部分。装低了容易被顾客碰到装高了视角太平、看不到靠里的排面。我实测的经验是摄像头装在货架正前方的天花板或顶梁上距离货架平面1.8到2.5米俯角15到30度镜头的中心线对准货架的中层区域。这个角度既能看清所有层板的排面又不会让上层货架遮挡下层。装在侧面是不行的。侧面视角会产生严重的透视遮挡前面一列商品会挡住后面几列算法根本分不清哪个排面空了。必须保证摄像头大致正对货架。安装固定我用的是金属支架加扎带摄像头外壳上用螺丝锁紧支架支架另一端固定在墙体或货架立柱上。货架本身是金属的承重没问题但要注意摄像头必须固定在不会随货架晃动的结构上。我曾经试过把摄像头夹在货架顶层的层板上补货人员推货架时画面轻微晃动后面对比算法就炸了。视角标定我放在软件阶段做先在画面里手动圈出每个货架区域的ROI感兴趣区域把层板、排面的坐标存成配置文件。这个环节必须人工参与因为每个货架的层数和排面不一样全自动标定很容易错位。3.2 布线施工的完整流程现场布线的顺序很有讲究我踩过一遍坑之后总结出以下流程先确定摄像头安装位置再用电工胶带在墙面临时固定网线调整走线路径确保不遮挡消防设施和顾客通行区域。布线工具准备测线仪、剥线钳、水晶头、扎带、线槽。网线我用的超五类纯铜线长度超过50米就上六类。从摄像头端向交换机端逐段布线每隔50厘米用扎带固定在墙面线槽或货架立柱上。关键点线缆不能和220V电线平行走同一线槽距离至少保持30厘米否则画面会随机出现干扰纹。制作水晶头用测线仪逐根验证568B线序通断这一步返工率最高建议多练几遍。交换机端先接好上联线和电源再逐台接入摄像头每接一台就在交换机上确认对应网口灯亮起。供电测试24小时记录每台摄像头的功耗和表面温度确认被动散热环境不积热。整个布线花了大约半天时间。做完之后一定要拍布线的照片存档标好每一路网线对应的摄像头位置。后面维护时这个图能救你的命。3.3 网络调试与取流验证摄像头网络调试有个经典坑新摄像头的默认IP地址和门店局域网不在同一网段电脑直接连上去看不到设备。我的处理方式是先不接门店交换机把摄像头和调试笔记本直连手动给笔记本配一个和摄像头同网段的IP进摄像头的配置页改好固定IP后再接入交换机。摄像头IP规划我直接写死不用DHCP。原因很简单DHCP分配地址可能在摄像头重启后变化而软件配置文件和数据库里存的都是固定IP一旦变了整条链路就断了。我按货架位置顺序分配IP比如192.168.50.21到192.168.50.26一眼就能看出来是哪组货架。取流验证用通用播放器打开RTSP地址确认视频流畅、延迟低于1秒。然后对照实时画面微调摄像头的曝光参数锁死曝光和白平衡。这一步做完硬件部分才算真正验收通过。4. 软件全实现从取流到告警的完整链路4.1 软件架构与依赖软件部分我的代码结构分四个模块对应四条命令capture.py采集模块负责从RTSP拉流、定时抓图、存储原图。detect.py推理模块加载模型对图像进行缺货检测输出结果。alert.py告警模块查询检测结果按规则推送告警。server.pyWeb服务提供看板和状态查询。开发环境是Python 3.9核心依赖是OpenCV、NumPy、ONNX Runtime和Flask。ARM板卡上装OpenCV容易踩坑预编译wheel找不到就手动编译编译前先装好依赖库编译参数里记得开启GTK支持不然imshow用不了。建议直接用官方提供预编译包省时间。4.2 图像采集RTSP取流的正确姿势采集模块看似简单实际有个大坑OpenCV的VideoCapture读取RTSP流时底层是持续缓冲帧的如果推理速度低于视频帧率拿到的会是几秒前的旧帧实时性大打折扣。解决方法是把缓冲区强行设为1每次只保留最新帧。我用的采集核心代码import cv2 RTSP_URL rtsp://admin:password192.168.50.21:554/stream1 cap cv2.VideoCapture(RTSP_URL) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) if not cap.isOpened(): raise RuntimeError(摄像头打开失败检查IP和端口) while True: ret, frame cap.read() if not ret: print(取流失败准备重连) cap.release() cap cv2.VideoCapture(RTSP_URL) continue # 这里保存原图并交给检测模块 cv2.imwrite(latest_frame.jpg, frame)实际测试中RTSP长连接偶尔会断尤其是网络波动时。所以代码里必须有重连机制读取失败释放句柄等待5秒重新创建VideoCapture对象。我还在重连超过3次的情况下加了报警日志方便排查。另外采集任务每隔10分钟执行一次就够了。巡检频率太低发现不及时太高算力和存储都浪费。10分钟这个间隔是结合门店补货周期和顾客购物行为定的比较合理。如果要更实时可以把间隔压到5分钟但存储压力和CPU负载都会翻倍。4.3 缺货检测算法三种方案梯度递进这是整套系统的核心我按复杂度从低到高实现了三个方案实际部署时根据门店需求选择。方案A背景差分法。因为摄像头固定、货架固定理论上每个排面在正常满货状态下的图像是稳定的模板。检测时把当前帧和“满货基线图”做差分统计变化比例变化超过阈值就认为这个排面缺货。这个方案实现最快不需要任何标注数据适合单品类、少排面场景。基线差分检测的核心逻辑import cv2 import numpy as np def check_slot_empty(current, baseline, thresh0.15): # 转灰度降低颜色干扰 cur_gray cv2.cvtColor(current, cv2.COLOR_BGR2GRAY) base_gray cv2.cvtColor(baseline, cv2.COLOR_BGR2GRAY) diff cv2.absdiff(cur_gray, base_gray) _, mask cv2.threshold(diff, 40, 255, cv2.THRESH_BINARY) change_ratio np.count_nonzero(mask) / mask.size return change_ratio thresh # 和基线差异小说明商品被拿走了这个方案的缺陷也很明显货架补货后商品位置变化、顾客把商品拿到手上看了一眼又放回原位、促销立牌插入排面等情况都会让差分值异常。所以方案A只适合快速出Demo验证链路不适合长期稳定交付。方案B目标检测模型YOLO。先标注数据集训练一个能识别“饮料瓶”“零食袋”“盒装商品”等品类目标的检测模型然后统计每个ROI区域内的目标数量。ROI对应的排面区域目标数为0就判定缺货。这个方案比差分法稳健得多因为它认的是“商品本身”而不是“画面变化”。我采用的模型是YOLOv5s输入分辨率640推理一次在ARM板卡的NPU上约80毫秒。数据标注过程每个货架角度拍200到300张图用开源标注工具框出所有商品外接框类别按商品大类合并不按单品SKU分因为外观相近的同品类商品区分意义不大。训练集和验证集按8比2切分训练100轮最终验证集mAP0.5在0.92左右。训练命令参考python train.py --img 640 --batch 16 --epochs 100 \ --data shelf_data.yaml --weights yolov5s.pt --device 0部署时把PyTorch模型导出成ONNX再转成板卡NPU的格式推理代码用ONNX Runtime加载import cv2 import numpy as np import onnxruntime as ort session ort.InferenceSession(shelf_detect.onnx) def infer(frame): # 预处理resize到640x640归一化 img cv2.resize(frame, (640, 640)) blob img.astype(np.float32) / 255.0 blob blob.transpose(2, 0, 1)[None] inputs {session.get_inputs()[0].name: blob} outputs session.run(None, inputs)[0] return outputs # 再解析boxes、scores、classes方案C在方案B基础上加一个“空满分类器”。把每个排面ROI裁剪出来单独训练一个二分类模型有货/无货。理由是YOLO对遮挡严重的场景容易漏检分类器只看局部区域判断会更直接。缺点是工程量大每个品类都要采集正负样本。我最终给门店用的是方案B加方案A的混合策略YOLO先跑一轮检测如果某ROI检测到0个目标再跑差分法做二次确认两者都认为缺货才触发告警。这个策略把两种方法的误报率都压低了实现也不复杂。4.4 告警推送与Web看板告警模块原则是“宁可漏报不可轰炸”。店员和店长被无效告警骚扰几次之后就会彻底无视这个系统所以去重逻辑很重要。我的告警规则同一个排面连续两次巡检都判定缺货才触发首次告警触发后30分钟内不重复告警。这样设计是因为单次误判可能由顾客遮挡、光线抖动引起连续两次的缺货状态可信度高很多。首次告警的推送我用的是通用的Webhook方式写到企业即时通讯群机器人同时发一封带截图附件的邮件给店长。Webhook推送代码如下import requests import json def push_alert(shelf_name, position, image_path): url https://open.example.com/webhook/send payload { msgtype: text, text: { content: f{shelf_name}货架 {position} 排面缺货请及时补货 } } headers {Content-Type: application/json} resp requests.post(url, datajson.dumps(payload), headersheaders, timeout5) return resp.status_code 200Web看板用Flask写了个轻量页面展示当前所有货架的实时状态绿色排面表示正常红色表示缺货点击红色排面能看到最近一张现场截图。回传数据用SQLite存储每天凌晨归档前一天的巡检记录到CSV文件供店长做月度缺货复盘。4.5 定时任务与生命周期管理整个系统跑的是长驻进程我直接用系统自带的计划任务工具拉起采集和检测。定时任务配置如下*/10 * * * * cd /opt/shelf_monitor python capture.py logs/capture.log 21 */11 * * * * cd /opt/shelf_monitor python detect.py logs/detect.log 21采集和检测错开一分钟确保检测进程运行时最新的原图已经落盘。计划任务里只放采集和检测告警和Web服务作为常驻后台进程由系统服务管理开机自动拉起。日志管理不能省。每个模块都输出结构化日志包含时间戳、模块名、状态码、耗时。巡检任务连续失败3次所有告警暂停并在看板顶部显示“系统异常”横幅防止异常期间缺货漏报还无人知晓。5. 实测效果与排错手记整个系统在试点门店连续跑了三周期间我修了三个典型的误报问题每一个都很值得分享。5.1 货架标签反光导致连续误报上线第一天就发现两组货架频繁告警但现场明明货是满的。打开告警截图一看原来问题出在货架层板前沿的塑料价格标签上。当灯光角度变化时标签表面出现高光反射YOLO偶尔把这些标签误检为目标差分法又识别出强烈的像素变化误报就从这里来。解决办法分两步一是调整摄像头安装角度让镜头光轴和层板标签的反射角错开高光明显减少二是在差分法里加了一个预处理步骤先把图像转灰度再做CLAHE直方图均衡降低局部高光的影响。同时给YOLO的数据增强里增加了“高光遮挡”模拟让模型见过这种输入。三个措施叠加之后这类误报基本消失。5.2 早晚自然光变化导致阈值漂移某天下午四点半靠近门口的货架开始间歇性告警。排查后发现日落时阳光斜射进店货架暗部和亮部的对比度急剧变化背景差分法在暗部的灰度值大幅偏移固定阈值40不再适用。这个问题靠调阈值解决不了必须从根上处理。我在检测前加了全局光照归一化计算整幅图像的灰度均值和标准差按基准图的统计值做线性拉伸。同时让摄像头开启宽动态模式并锁定曝光时间把一天内的光照变化控制在较小范围。经过这个处理后差分法在早晨和傍晚的误报率都降到了可接受水平。5.3 补货人员的日常动作触发误判系统运行一周后收到一次真实告警但店员到货架前发现货是满的。查日志看到告警前两分钟有个补货员刚整理过货架。他把某个商品批量搬到前排瞬间出现了“商品缺失”的画面连续两次巡检正好落在整理窗口里。这个问题的本质是“补货动作”被误判成“缺货状态”。我的调整是增加“人工复位”机制补货员整理完货架后在门店平板电脑上点一下“货架已理”系统立刻重新抓图更新基线。同时把连续确认次数从2次提高到3次每次巡检间隔10分钟三次确认大约覆盖半小时补货动作造成的瞬时缺口基本会被过滤掉。5.4 三周运行的表现三周运行下来系统总共触发28次缺货告警其中26次得到确认是真实缺货2次是促销立牌遮挡排面导致的误报。26次真实缺货里店长说至少有20次是他们原本来不及发现的单次缺货从出现到告警的平均时间大约18分钟基本达到了设计预期。性能方面ARM板卡的CPU平均占用率55%左右内存占用约1.6GB整机功耗不到8瓦。一整天下来存储占用约1.2GB主要来自原图。6. 成本清单与后续扩展思路整套系统的成本是很多人关心的问题我做一份详细账单项目型号/规格单价估算数量小计高清摄像头400万像素POE IPC3003900边缘板卡8核ARM带NPU8501850POE交换机8口百兆POE2001200网线及辅材超五类线槽支架1501批150其他电源、扎带、标签等1001批100合计2200这个价格对比一个店员每天花两小时巡货架的人力成本大概两三个月就能回本。算法部分的软件成本主要是标注和训练时间我前后花了大约四个工作日如果后续接更多货架边际成本很低。扩展方向上我建议按三条线走第一条线是扩大覆盖范围把摄像头从3组增加到10组以上这时候边缘板卡的推理能力可能吃紧可以按区域拆分到多块板卡或者把视频流做低码率压缩后集中到一台服务器推理。第二条线是接入门店的POS和进销存数据。系统检测到缺货后自动核对库存台账如果库存里还有货说明商品放在仓库没上架推送给店长上货如果库存也为0直接生成补货建议单。这套闭环才能真正减少缺货损失。第三条线是商品识别级别加强。当前模型只能认出品类认不出具体到某个单品的SKU。用更细粒度的标注数据训练识别模型加上货架商品排列的拓扑约束可以做单品级缺货统计这个对连锁门店的供应链优化价值更大。最后说一点个人体会这种视觉项目真正吃时间的并不是算法训练而是现场环境适配。货架的反光、灯光的变化、顾客和店员的日常动作这些在实验室里永远遇不到到了真实门店全都变成要处理的问题。我的经验是先把安装环境和采集链路打磨稳定再谈算法精度顺序反了会被无穷无尽的误报拖垮。如果打算自己动手做我的建议是从一组货架、一个品类开始全流程跑通后再往外扩。先别追求多高的准确率把“发现缺货并在10分钟内通知到人”这个闭环做扎实系统就已经有实用价值了。