售货柜视频识别流水线:RTSP拉流、抽帧与YOLO实战

发布时间:2026/9/10 3:22:53
售货柜视频识别流水线:RTSP拉流、抽帧与YOLO实战 售货柜项目看起来没那么复杂柜子里装一个摄像头拍到商品后用 YOLO 识别一下状态。但实际做起来从摄像头取流到输出业务结果中间要走的链路比大多数人预想的要长。IPC 拉流、视频抽帧、YOLO 识别这三件事每一件单独拿出来都能写一篇踩坑记录合在一起就是一套完整的实时识别流水线。我最早接手这类项目时习惯把整个流程放在一个脚本里VideoCapture 每读到一帧就调用 model.predict()结果 25 帧的视频流跑成了幻灯片GPU 使用率忽高忽低RTSP 偶尔一断整个进程直接卡死。后来把链路拆成拉流、抽帧、推理、上报四个模块每个模块独立完成一件事再通过队列串起来系统才真正稳定下来。这篇文章就把这套完整的流水线拆开讲适合正在做无人货柜、智能货架、门店安防的开发者参考。1. 整体设计方案先拆环节再拼流水线1.1 售货柜要解决的真实问题售货柜和普通安防监控最大的区别在于监控只需要“录像留证”而售货柜必须“实时判断”。通常情况下业务关注的不是某个顾客长什么样而是当前货道里的商品是否还在、库存是否不足、顾客有没有拿放异常。所以摄像头的作用不是单纯录视频而是变成一个持续工作的商品巡检员。比如一个卖饮料的柜体每个货道都放同一种商品YOLO 不需要识别几十种品牌只需要判断“这个位置有没有商品”。但如果做无人零售柜货道可能十几个每个货道可能还同时摆放不同商品这时就需要模型能够区分 SKU 类别。把业务目标先理清楚后面所有参数设定才有依据。不要一上来就追求识别几十类商品先判断“有货没货”往往比“这是哪一款商品”更容易落地也更容易让现场接受。另外一个容易被忽略的问题是视角固定。售货柜摄像头一般装在柜体顶部或层板前方视野相对固定光线变化也受柜内 LED 影响。这种场景比自动驾驶、路边监控简单得多属于限定场景下的目标检测。所以模型不需要做得特别大更重要的是抽帧稳定、推理快速、检测结果经得起连续验证。1.2 为什么要把链路拆成“流水线”视频流本质上是连续不断的数据源摄像头每秒产生 15 到 25 帧画面而 YOLO 模型在嵌入式设备上推理一张图可能需要 50 到 100 毫秒在 GPU 上也要 10 到 30 毫秒。如果每一帧都去推理算力永远跟不上还会因为上一帧没处理完导致下一帧在缓冲区里堆积画面延迟越来越高。更合理的思路是让每个环节各干各的拉流线程只负责从 IPC 摄像头取帧抽帧模块按照固定策略挑出需要的画面推理模块拿到一帧处理一帧业务模块再把结果聚合上报。这个结构和工厂流水线很像上游再快下游只要按自己的节拍消化系统整体吞吐就不会崩。它解决的核心问题是“速率不匹配”。拆开之后还有一个额外好处任何一环出问题都可以单独排查。拉流断了重启拉流模块模型推理挂了不需要重启整个系统业务侧要增加一个货道 ROI 判断也不影响底层视频链路。我在实际项目里深深体会这一点一开始用单线程串行处理出问题时很难定位是解码慢、推理慢还是网络卡拆成独立模块后看日志就能知道瓶颈在哪一环。1.3 四段流水线的输入输出定义先建立一个共识整条流水线由四个环节组成。拉流输入是 RTSP 地址输出是解码后的视频帧。这个环节的难点是视频解码开销、网络抖动、断线重连。抽帧输入是视频帧流输出是“值得做识别”的关键帧。这个环节决定系统是高效还是空转也直接影响实时性。YOLO 识别输入是一张图输出是检测框、类别、置信度。这个环节决定识别准确率和算力成本。结果上报输入是单帧检测结果输出是业务事件比如“3 号货道缺货”“饮料 A 库存不足”。这个环节决定系统对业务的可用性。每个环节之间用队列连接帧对象在队列中传递。拉流端永远不去感知业务端要什么业务端也永远不直接碰摄像头。这样做以后即便摄像头从 2 个扩到 20 个也只是多开几路拉流进程模型、抽帧逻辑都不需要大改。在设计初期务必要给每个环节定义好“数据单位”拉流输出是 numpy 数组或 cv::Mat抽帧输出是缩放后的推理图YOLO 输出是结构化的框。哪怕项目再小也建议把这几个接口提前约定清楚后面写代码会顺畅很多。2. 拉流环节IPC 摄像头 RTSP 取流详解2.1 RTSP 是什么IPC 地址该怎么填先解释一个容易混淆的点在售货柜、安防项目里IPC 通常指 IP Camera也就是网络摄像机它和软件开发里常说的进程间通信 IPC 是两码事。网络摄像机提供的视频流协议一般是 RTSPReal Time Streaming Protocol它负责控制会话的建立真正的视频数据通过 RTP 传输。简单理解RTSP 地址就是摄像头视频流的门牌号拿到了这个地址程序才能去读取画面。常见 RTSP 地址格式是rtsp://用户名:密码IP地址:端口/流路径。不同品牌的摄像头流路径差异很大有的可能形如/Streaming/Channels/101有的可能是/live/0/main这个必须看设备说明书或者用厂家提供的客户端查看。在给售货柜配置摄像头时建议提前规划静态 IP方便后面程序连接不要让摄像头走 DHCP否则路由器重启后 IP 一变拉流程序就连不上了。另外一个和稳定性强相关的选择是传输协议。RTSP 默认可以走 UDPUDP 延迟低但在弱网环境容易丢包画面会花屏、卡顿。生产环境我更推荐强制走 TCP也就是在拉流参数里加上rtsp_transporttcp虽然握手开销略大但画质和连接稳定性明显更好。在局域网的售货柜场景带宽非常充裕用 TCP 几乎没有副作用。2.2 用 OpenCV 拉流为什么动不动就卡OpenCV 的VideoCapture是最快的上手方式几行代码就能从 RTSP 地址读取帧。下面是一段最简单的拉流代码import cv2 url rtsp://admin:your_password192.168.1.64:554/Streaming/Channels/101 cap cv2.VideoCapture(url) while True: ret, frame cap.read() if not ret: break # 到这里 frame 就是一张 BGR 图像 cv2.imshow(preview, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release()这段代码在实验室环境没问题但上线后会发现两个痛点。第一个是 OpenCV 默认会缓存几帧读出来的画面不是最新的而是几百毫秒前的旧帧。解决办法是在打开视频流后设置cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)让缓存尽量浅。第二个是read()在断流时可能返回False也可能一直阻塞程序容易卡住不退出。这个问题不能完全靠 OpenCV 解决必须在读线程外部加超时机制看门狗。还有一个隐蔽问题OpenCV 拉 RTSP 时用的底层是 FFmpegH.264 解码默认走 CPU。如果摄像头是主码流 1080P 25 帧单路视频解码就能占满一个 CPU 核心。当一个售货柜同时接 4 路摄像头CPU 解码压力很大直接挤压推理环节的资源。所以下面的主码流、子码流选择非常关键。2.3 主码流和子码流该怎么选大多数网络摄像头同时提供主码流和子码流两者差异主要体现在分辨率和码率上。码流类型典型分辨率适用场景解码开销主码流1080P / 3MP / 4MP录像取证、精细识别高子码流640x360 / 704x576实时预览、模型推理低在售货柜项目里我的习惯是如果最终只做“商品有无识别”或 SKU 判别完全可以直接抽子码流。子码流分辨率虽然低但对 YOLO 这类目标检测算法来说只要物体占画面比例足够低分辨率反而能加快推理。真正的取证录像则用主码流可以单独录到 NVR 或本地存储不需要经过 AI 链路。这样既保住了画面质量又减轻了解码压力。当然也有例外。如果货道位于画面较远位置商品很小子码流可能无法看清细节。这时候就得用主码流抽帧同时把图片裁剪到目标区域再送模型。最好的办法是做一下现场实测把 1080P 帧和子码流帧分别喂给模型跑一遍对比检测置信度再决定用哪一路。不要一拍脑袋选择高分辨率算力成本最后都会体现在设备采购费用里。2.4 断线重连的工程处理IPC 摄像头断流是常态网线松动、摄像头重启、路由表变化都会导致 RTSP 握手失败。上线第一天遇到断流就先别急着优化模型先保证断线后能自动恢复。最简单的方案是在循环里重新打开视频流while True: cap cv2.VideoCapture(url) if not cap.isOpened(): print(连接失败2 秒后重试) time.sleep(2) continue while True: ret, frame cap.read() if not ret: print(读取失败触发重连) break process(frame) cap.release() time.sleep(2)这种写法已经能应付大部分场景但要注意两个问题。一是重连间隔不能太短否则摄像头没完全就绪时反复握手反而把设备搞得很忙。二是要在拉流线程里设置一个“帧存活时间”比如超过 5 秒没有新帧进来就主动断开并触发重连否则read()可能一直阻塞在底层无法感知断流。这里可以单独用一个 watchdog 线程检查帧时间戳超过阈值就暴力重建VideoCapture。日志方面建议把每次重连的原因都记录下来是目标不可达还是认证失败还是读帧超时。这样在后端监控里看到一个售货柜频繁重连就能快速定位是网络问题还是设备问题。不要只打印“retFalse”这种日志对排查毫无帮助。3. 抽帧策略什么帧该送什么帧该丢3.1 为什么不能每帧都送模型假设一台 IPC 输出 25 帧每秒YOLOv8s 在普通 GPU 上推理一张 640x640 的图大约需要 15 毫秒理论上每秒最多处理 60 张以上的图看起来似乎能跑满视频流。但如果摄像头有 4 路帧率就变成每秒 100 帧再加上预处理、后处理、结果上报推理服务很快就会被压垮。而且售货柜画面变化很慢货道里的商品可能一整晚都没动过不停重复识别相同画面毫无意义。在生产环境里系统资源要用在“变化”上。所以抽帧要解决的核心问题是如何在保证不漏关键事件的前提下把推理次数降到最低。无人售货柜场景没有太多快速运动物体通常 3 到 5 帧每秒的抽帧率就足够覆盖取货动作。如果担心顾客快速抓取漏检可以提高到 5 到 10 帧每秒但完全没有必要再做 25 帧全量推理。抽帧还有一个隐性收益降低后端存储压力。如果每帧都保存推理结果一天下来的数据量非常可观。按 5 帧每秒抽帧每小时只有 18000 帧相比 25 帧每秒的 90000 帧数据量下降 80%。这对日志检索、图片回放都友好。3.2 固定时间间隔还是帧间隔常见的抽帧方式有两种按时间间隔抽帧和按帧序号抽帧。按帧序号抽帧代码最简单比如if frame_index % 5 0就送一帧。但这种方式假设视频帧率是恒定的实际 RTSP 流在弱网环境会丢帧、跳帧后面摄像头重启后帧率也可能变化导致抽帧间隔跟着漂移。按时间间隔抽帧更可靠。用time.time()记录上次抽帧时间与当前时间对比达到目标间隔就抽一帧。这样不管摄像头是 15 帧还是 25 帧系统都能保持稳定的推理频率例如每 200 毫秒抽一帧对应 5 FPS 推理。下面是一个实用的抽帧逻辑last_extract 0.0 interval 0.2 # 每 200ms 抽 1 帧 ret, frame cap.read() now time.time() if ret and now - last_extract interval: last_extract now queue.put(frame)如果追求更高级的抽帧策略可以结合“变化检测”先用背景差法或图像直方图对比相邻帧的差异画面变化超过阈值时临时提高抽帧频率画面静止时则降下来。比如顾客打开柜门一瞬间触发连续抽帧平时每隔一秒才抽样一次。这种动态策略在省算力方面表现更好但实现复杂度也更高适合已经有稳定基线版本后再迭代。3.3 用多线程和队列防止画面“越积越旧”视频流是不间断的而模型推理需要时间。如果不加控制地往队列里塞帧队列会越长越长系统处理的是几秒前的旧画面。所以抽帧模块和推理模块之间必须用一个“有界队列”队列满时优先丢旧帧、保留新帧。这里给出一个最小实现思路。拉流线程把抽出来的帧放进队列推理线程从队列另一端取帧frame_queue queue.Queue(maxsize8) def capture_worker(): while True: ret, frame cap.read() if not ret: continue now time.time() if now - last_extract interval: last_extract now if frame_queue.full(): try: frame_queue.get_nowait() # 丢最旧的一帧 except queue.Empty: pass frame_queue.put(frame) def inference_worker(): while True: frame frame_queue.get() result model_infer(frame) handle_result(result)把队列容量设置为 8 的意思是推理端最多积压 8 帧超过后直接丢弃最旧的保证模型每次拿到的都是最新画面。这个“丢旧保新”的设计对实时售货柜很重要你永远希望模型识别的是当前时刻的货道状态而不是半分钟前的状态。3.4 抽帧时的分辨率与编码注意事项抽帧不等于原样保存。模型推理通常只关心固定尺寸比如 640x640所以在抽帧后、送模型前可以先做一次缩放这样能显著降低队列内存占用。一个 1080P 的 BGR 图像大约占 6MB640x640 只有 1.2MB队列 8 帧时差距就很明显。如果要保存历史图片用于审计不要保存压缩过的推理图。推理图经过缩放和边缘填充已经丢失了原始细节。建议走一条独立链路把关键帧按原始分辨率编码成 JPEG文件名带时间戳和摄像头编号。JPEG 质量可以设置在 80 到 90 之间既兼顾存储体积又不会让肉眼看到明显劣化。注意不要反复重压缩同一张图保存一次就不要再次转码。我在实际项目里见过一个坑为了省空间把 1080P 的帧先缩到 640x360然后存 JPEG之后要回放确认商品细节时发现画面糊得无法辨认。所以当时就定了一个规矩送 AI 推理的图和存证据的图分开处理推理图随便缩放证据图务必保持原始分辨率。抽帧逻辑本身不会“影响画面”但任何有损处理都必须明确知道数据流向。4. YOLO 识别从帧到检测结果的全过程4.1 模型选型和类别定义YOLO 发展到今天已经有很多版本YOLOv5、YOLOv8、YOLOv11 都是工业项目里常用的选择。对售货柜项目来说我不建议追求最重最大的模型而是优先选择 YOLOv8n 或 YOLOv8s这类轻量模型在 CPU 上能跑到可用的速度在 GPU 或边缘设备上更是轻松。目标检测模型的精度提升很多时候不取决于模型名称而取决于训练数据是否符合现场光照、角度和遮挡情况。售货柜的类别定义要符合业务不是模型能识别 COCO 的 80 类就完事。常见做法是自定义类别比如“商品 A”“商品 B”“空位”“顾客手部”。其中“顾客手部”是一个很有用的辅助类别它可以用来判断货道空置是真实缺货还是因为顾客正在拿取商品。这样业务事件可以设置延时确认机制避免手一挡住商品就误报缺货。另外一个细节商品类别不宜分得过细。如果两瓶饮料外包装相似AI 很难精确区分口味。这种场景请优先考虑“识别到商品区域配合货道编码确认 SKU”而不是让视觉模型去硬分相似包装。把视觉模型的强项和业务规则结合起来系统稳定性会高很多。4.2 预处理letterbox 到底解决了什么YOLO 模型通常把输入图片统一到正方形尺寸比如 640x640。如果直接把不同宽高比的图片粗暴缩放物体就会被拉伸变形检测精度明显下降。标准做法是 letterbox 等比缩放然后给不足的部分填充灰色边。这样做的好处是物体比例不变模型看到的画面和真实场景一致。def letterbox(img, new_shape(640, 640), color(114, 114, 114)): h, w img.shape[:2] target_h, target_w new_shape r min(target_h / h, target_w / w) new_w, new_h int(round(w * r)), int(round(h * r)) dw (target_w - new_w) / 2 dh (target_h - new_h) / 2 top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) resized cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR) return cv2.copyMakeBorder(resized, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor)这段代码把图片等比缩放到target_shape内计算出上下左右的填充宽度再复制边框。推理完成后还需要把检测框坐标从 640x640 映射回原始帧坐标这一步要记下缩放比例和填充尺寸反向运算即可。很多新人只记得 letterbox却忘了坐标映射导致画出来的框永远偏位。预处理还有一个容易忽略的步骤颜色通道顺序。OpenCV 读出来的是 BGR而模型训练时通常用 RGB 或归一化后的 RGB。如果直接送模型颜色会被反转检测结果可能异常。所以标准顺序是读取 BGR 帧 - letterbox - 转 RGB - 转 float - 归一化 - 转成 NCHW 张量。4.3 推理和后处理NMS 不能省模型输出不是干净的“框”而是一堆候选预测。以 YOLOv8 为例输出通常是一个[1, 84, 8400]的张量8400 表示不同尺度的候选框数量84 表示 4 个坐标 80 个类别得分需要经过置信度阈值过滤和非极大值抑制才能得到最终框。非极大值抑制NMS解决的核心问题是同一个物体周围可能产生很多重叠框模型对同一瓶饮料可能打了 5 个框NMS 会保留分数最高的框把这些重叠的都去掉。这个步骤如果跳过业务系统会收到大量重复检测事件误报率瞬间上升。而如果自己实现 NMS要注意把不同类别的框分开处理否则类别不同但框重叠的对象会被错误抑制。在实际部署时如果使用 Ultralytics 的 Python 包model.predict()已经内置了后处理非常方便。但如果走 ONNX Runtime 或 TensorRT就需要自己实现后处理或者使用自定义的 NMS 层。我建议至少把 NMS 逻辑封装成独立函数这样换推理引擎时后处理逻辑可以复用不至于每次都要从头排查。4.4 模型加速与设备部署部署阶段第一优先级是减少模型输入尺寸。比如从 640x640 降到 480x480推理速度可以提升 30% 到 50%而精度损失在售货柜场景通常可接受。前提是画面里的商品足够大太小则识别不到。第二优先级是开启半精度推理FP16 在支持 Tensor Core 的 GPU 上几乎无精度损失速度却有明显提升。第三优先级是使用 TensorRT 转换模型把网络结构、权重、后处理都固化下来能显著降低延迟。CPU 设备也不是不能跑选 YOLOv8n 并开启 OpenVINO 或 ONNX Runtime 的 CPU 优化在普通工控机上单路 640x640 推理可以做到 30 到 50 毫秒。如果一台售货柜只有 2 路摄像头CPU 推理仍然可行。但超过 4 路时强烈建议上一块入门级 GPU 或边缘 NPU 设备。不要只盯着模型精度设备算力和散热同样决定上线后的运行效果。推理环节的性能监控也很重要至少要把单帧推理耗时、单帧预处理耗时、后处理耗时都打印出来。这个数据能直接告诉你瓶颈在哪。我曾经遇到一个项目模型在 GPU 上推理只需 10 毫秒但预处理和 NMS 在 Python 里花了 60 毫秒最后一通优化反而在预处理代码上挖到了红利。5. 完整流水线的工程化落地5.1 一个最小可运行的整体骨架把前面几个模块串起来就是一个最小可运行的流水线。用两个线程一个负责拉流抽帧一个负责模型推理中间用有界队列连接。核心代码如下import cv2 import time import queue import threading from ultralytics import YOLO frame_queue queue.Queue(maxsize8) model YOLO(vending_yolo.pt) def read_and_sample(rtsp_url, interval0.2): cap cv2.VideoCapture(rtsp_url) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) last_extract 0 while True: ret, frame cap.read() if not ret: time.sleep(1) cap.open(rtsp_url) continue now time.time() if now - last_extract interval: last_extract now if frame_queue.full(): try: frame_queue.get_nowait() except queue.Empty: pass frame_queue.put(frame) def infer_loop(): while True: frame frame_queue.get() results model.predict(frame, imgsz640, conf0.45, verboseFalse) # 处理 results绘制框、生成业务事件 for r in results: for box in r.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() conf float(box.conf[0]) cls int(box.cls[0]) print(time.time(), cls, conf, x1, y1, x2, y2) t1 threading.Thread(targetread_and_sample, args(rtsp://...,), daemonTrue) t2 threading.Thread(targetinfer_loop, daemonTrue) t1.start() t2.start() t1.join()这段代码放到真实环境里已经能跑通但离生产还有距离。生产版本需要加入进程级看门狗、日志轮转、断线告警、结果持久化。我建议先把这个骨架放在一台测试机上跑一天观察帧队列是否经常打满、推理耗时是否稳定再逐步补充周边能力。5.2 多线程还是多进程我的取舍上面的示例用了两个线程简单方便在单路视频流场景完全够用。但 Python 的 GIL 对多线程有一些限制特别是当后处理逻辑全是 Python 循环时两个线程并不能真正并行。如果摄像头数量增多或者模型推理不是走 C 加速库而是纯 PyTorch CPU就要考虑多进程架构。多进程方案通常是这样一个拉流进程持有 VideoCapture把抽帧结果放进共享内存或消息队列一个推理进程读取帧并执行模型推理一个业务进程接收检测结果并上报。进程之间通过 multiprocessing.Queue 或 Redis Stream 通信。这样做的好处是单个进程崩溃不会把全部链路拖垮也更容易横向扩展到多路视频流。在项目初期我建议先在线程模型上跑通业务逻辑再用多进程做性能优化。不要一开始就把系统设计复杂否则排查问题时要先跨过一堆进程通信障碍。只有当线程模型的确在 CPU 占用、延迟上遇到天花板时才值得引入多进程。5.3 业务结果怎么用连续帧确认与事件触发模型输出单个检测框后不能直接当业务结果使用因为单帧误检在所难免。比如顾客的手在货道前晃了一下模型可能把手指误检成商品或者因为反光漏检了一帧。最实用的方法是做事件状态机只有当某个状态连续维持多帧才产生业务事件。举个例子检测目标“3 号货道空置”。模型每隔 200 毫秒输出一次检测结果如果连续 5 次都显示 3 号货道没有商品也就是持续约 1 秒才认为缺货事件成立。如果中间有一帧检测到商品则计数器清零。这种策略会牺牲一点响应时间但能大幅降低误报。对于无人零售场景晚一两秒上报完全没关系误报才是真正影响运营体验的问题。每个检测结果最好都带时间戳和摄像头编号。上报到后端时用“摄像头 货道 事件类型 时间”作为唯一键避免重复推送。如果业务上需要回放再把抽帧原图存到本地或对象存储里方便人工复核。5.4 可观测性帧率、延迟和资源监控流水线的健康度不能靠“偶尔看一眼画面”来判断必须有量化指标。我一般在售货柜节点上埋几个核心指标摄像头实时帧率每秒从 VideoCapture 读到的帧数、有效抽帧率每秒实际送入推理的帧数、单帧推理耗时、队列积压长度、CPU 和 GPU 使用率、RTSP 重连次数。这些指标定期打点上报超过阈值就告警。队列积压长度是最直观的瓶颈信号。如果队列长期处于满的状态说明推理速度跟不上抽帧速度要么调低抽帧频率要么换更快的推理引擎。如果队列长期为空则说明摄像头拉流或抽帧环节不稳定要先查网络和视频源。不要等到用户投诉“画面延迟大”才去排查这些指标能在问题影响业务之前就暴露风险。日志输出也要规范。建议每帧检测的关键结果按结构化日志输出用 JSON 格式记录时间、摄像头、类别、坐标、置信度。这样后续不管是做统计分析还是问题回溯都有据可依。现场工程师最怕的不是“功能有问题”而是“出了问题不知道之前发生了什么”。6. 常见问题与排查技巧实录6.1 RTSP 连接失败或反复断开连接失败的原因通常集中在几个点网络不通、端口不对、认证失败、流路径写错。排查时先用 FFmpeg 自带的 ffprobe 做一个快速验证ffprobe -rtsp_transport tcp -v error -show_entries streamcodec_name,width,height -of defaultnoprint_wrappers1 rtsp://admin:password192.168.1.64:554/Streaming/Channels/101如果这条命令能正常输出视频流信息说明地址没问题问题在代码层如果连这个都失败就要直接检查网络和设备。摄像头本机能否 ping 通、端口是否被封、账号密码是否包含特殊字符需要 URL 编码这些都要逐一排查。连接反复断开还有一个常见原因摄像头主动关闭了超过空闲阈值的 RTSP 会话。解决方案是用短连接刷新机制或者定期重连以维持会话。如果多路同时连接同一台摄像头还要注意设备并发路数限制监控型 IPC 通常只支持 6 到 12 路并发太多会直接拒绝连接。6.2 画面卡顿、马赛克和延迟过高画面卡顿要区分是“网络导致的马赛克”还是“解码导致的掉帧”。马赛克通常表示 RTP 丢包强制使用 TCP 传输能改善解码掉帧则表现为画面跳秒但不花屏这时候要考虑降低拉流分辨率或升级设备算力。延迟过高还有一个隐形帮凶OpenCV 的内部缓冲。前面提到设置CAP_PROP_BUFFERSIZE1只对部分后端有效如果问题依旧可以改成编码方式拉流比如用 FFmpeg 命令直接读取最新帧再把原始数据解析成图像。实测下来这种方式在部分摄像头上的延迟比 OpenCV 低不少但代码复杂度会上升适合把延迟当硬指标的售货柜项目。如果对实时性要求较高还可以要求摄像头开启低延迟模式关闭去噪、宽动态等加重处理的功能。这些图像增强算法会消耗编码时间对识别又没有明显帮助。6.3 推理速度慢GPU 占用不稳定推理速度慢先看模型是不是没走对推理引擎。装了 GPU 版 PyTorch但 ONNX Runtime 只用 CPU 跑这类配置错误很常见。检查方法是打印一次会话的 providers确认 CUDA 或 TensorRT 在后端真正生效。GPU 占用忽高忽低通常是因为输入图片尺寸过大或预处理成了瓶颈。一个大尺寸图片的 resize 和归一化在 CPU 上完成GPU 大部分时间在等待数据。解决方案是把预处理尽量放到 GPU 上做使用 CUDA 上的视频解码和图像处理或者至少把letterbox后的数据以连续内存块传给模型。6.4 同一目标被重复识别结果抖动模型检测结果在相邻帧之间会抖动同一瓶饮料可能这一帧检测到下一帧漏检再下一帧又出现。这时直接在业务层做“连续帧确认”是最简单的方案。设定一个状态机连续 N 帧检测到才算“有货”连续 N 帧检测不到才算“缺货”。这样抖动会被自然滤掉。另一个问题来自多个货道之间的边界。摄像头视角倾斜时一个商品可能横跨两个货道区域模型返回的框也被两个 ROI 同时命中。这种情况需要在业务层做一些区域归属逻辑比如计算检测框中心点落在哪个货道或者按重叠面积最大的原则归属避免同一个库存变化被重复计算。6.5 摄像头时间戳混乱导致审计异常售货柜的业务结果通常需要精确到秒如果摄像头本地时间不准记录的时间戳就会错乱给补货或纠纷处理带来很大麻烦。一定要在项目初始化时统一校准设备时间使用 NTP 对时并在拉流程序里主动记录系统时间而不是依赖摄像头内置时间戳。如果一台售货柜的摄像头在多路识别日志必须带上统一的时间基准。系统时间要用 UTC 时间戳或配置好时区否则夏令时或换设备后可能出现一小时偏差。这个小问题看似不起眼但等到客服查监控录像时就会变成一个大麻烦。6.6 多次跑掉才发现摄像头并发是上限最多见的情况是测试环境只有一台摄像头代码写得再精简也没问题。上线后一台设备接 8 路摄像头不断出现偶发断流。查到最后才发现很多廉价 IPC 虽然支持双码流但并发 RTSP 会话数有上限超过 4 路后就开始踢掉旧连接。所以在选型阶段一定要确认“同时在线视频流数量”这个参数给现场余量。摄像头轮询也是个折中方案不需要所有路同时实时推理可以每隔几秒切换一个摄像头抽帧。售货柜场景对单路连续监控的要求不高轮询能大幅降低设备和网络压力。缺点是不能同时捕捉两个柜门同时打开的状态需要根据业务取舍。这套流程跑通之后后面再换摄像头或者换模型其实都不费劲。整个项目里最难的反而不是 YOLO 模型本身而是拉流和抽帧这些看似不起眼的环节。希望这篇把链路拆开讲的经验能让你在落地售货柜视觉方案时少踩点坑。最后再分享一个小技巧把摄像头画面亮度、对比度调到统一标准识别效果会比每台设备各自为政稳定得多。