售货柜识别实战:RTSP拉流、视频抽帧与YOLO推理流水线详解
熟悉我的老朋友可能知道,这些年我陆续做过不少安防和边缘设备的视觉项目,踩过的坑攒下来能写个小册子。今天这篇是售货柜识别项目的一段实战总结,主题非常直接:IPC 拉流、视频抽帧、YOLO 识别,把这三个环节串成一条能稳定跑的完整流水线。
为什么想写这个?因为网上讲 RTSP 拉流的帖子很多,讲 YOLO 训练的帖子也很多,但真正把“拉流 → 抽帧 → 识别”按项目级要求组装到一起、并讲清楚中间各种坑的,少之又少。大多数教程的终点是“能跑出个框”,根本没提现场网络抖动、内存膨胀、断线重连和延迟飙升这些真实问题。这篇就重点补上这部分。
这篇适合两类人:一类是刚开始接触视频流识别、想把摄像头画面接进 YOLO 的开发者;另一类是在做售货柜、闸机、巡检等相关项目的工程师,看看我的选型思路和踩坑记录,至少能给你省两三天排查时间。项目本身不复杂,复杂的是把每一环都焊死。
1. 售货柜场景的特殊性:为什么简单的识别项目会卡在拉流这一环
1.1 售货柜对实时性和稳定性的双重要求
售货柜业务是很典型的边缘视觉场景:一个柜子就是一个微型便利店,消费者开门、取货、关门,系统必须在十几秒内判断出“谁拿了什么”“拿了几个”。这意味着视频识别不能有明显的处理延迟,又不允许因为识别模块崩溃导致整个交易流程卡死。
具体拆开看,有三个指标绕不开:一是首帧响应速度,二是识别准召率(尤其是同类不同款商品容易混淆的场景),三是 7×24 小时连续运行的稳定性。很多人上来就把精力放在第三个指标上,疯狂调 YOLO 模型结构,却发现漏检和误检根本没改善。原因往往不在模型本身,而在源头就出问题了:拉流延迟太大、抽帧太密或太疏、喂给模型的画面是花屏或时间戳错乱的帧。从源头开始捋,比一上来就调模型的收益高得多。
1.2 系统整体架构:从摄像头到识别结果的数据流
数据流程其实就四步:IPC(网络摄像头)通过 RTSP 协议把视频流传到拉流服务,拉流服务按抽帧策略截取有效画面,YOLO 推理进程拿到帧后做目标检测,最后把识别结果抛给上层业务系统。很多人以为“拉流、抽帧、识别”是三个互相独立的模块,实际项目里它们之间的缓冲策略、超时处理、重连机制,才是真正决定项目能不能上线的东西。
先给一张物理部署参考(文字描述):售货柜顶部或侧边安装 IPC,通过 PoE 网线接入交换机,边缘计算盒子(如 RK3588、Jetson Orin)或工控机连同一个交换机,跑 YOLO 推理服务;推理结果再通过 MQTT 或 HTTP 上报到云端业务后台。这套拓扑决定了视频流走的是局域网,RTSP 延迟理论上能控制在毫秒级,但实际跑起来你会发现,局域网也挡不住网线松动、交换机丢包这类物理故障。
现在说结论:好的流水线不是把三个环节堆在一起,而是让每个环节都有独立的缓冲、独立的健康检查、独立的恢复逻辑。这也是我后来做所有视频类项目都坚持的架构原则。
2. RTSP 拉流与模拟摄像头的选型取舍
2.1 RTSP 协议的基本工作方式
RTSP 本身不传视频数据,它更像一个控制播放的遥控器:客户端通过 DESCRIBE 请求拿媒体描述,通过 SETUP 建立会话,通过 PLAY 让服务器开始推流;真正承载视频编码数据的,是 RTP 包。所以排错的时候要清楚,RTSP 层的错误和 RTP 层的丢包是两回事——前者是会话断了,后者是流还在但你拿到的画面已经花成马赛克了。
拉流端的主流做法是用 FFmpeg 或 GStreamer,少量原型项目会用 OpenCV 的 VideoCapture。我的建议是:做原型用 OpenCV 确实方便,但正式项目一定要用 FFmpeg 或专门的流媒体库。原因很简单——OpenCV 的 VideoCapture 拉流时,对断线重连、超时控制、硬解码支持都很弱,一旦 IPC 网络抖动,它宁可卡死也不告诉你发生了什么;FFmpeg 至少能让你拿到清晰的首帧时间、丢包率、解码错误码,排查问题时有据可查。
拉流时还有一个容易忽略的参数:传输协议。IPC 一般同时支持 TCP 和 UDP 拉流,TCP 更稳定但延迟略高,UDP 延迟低但容易丢包。售货柜走局域网,我建议直接用 TCP,画面握手和重传都比 UDP 可靠,实测延迟差异可以忽略。
2.2 hiksimulator 这类模拟器的使用场景与配置
开发阶段通常没有真实售货柜和真实 IPC,有人会先拿 USB 摄像头顶着,或者用手机 RTSP 推流软件——这些算模拟源,但都不够稳定。这次我用了 hiksimulator 这类 IPC 模拟器,它用软件模拟摄像头,能输出 RTSP 流,并支持 H.264/H.265 编码、分辨率、帧率可配置。它的最大价值是能生成稳定、带时间戳、基本不丢包的流,方便你在开发阶段把流水线逻辑调通,等整个链路稳定后再去现场接真实 IPC,就不会出现“代码在本地跑得好好的,一接真机就歇菜”的情况。
模拟器配置的关键参数无外乎这几项:
- 分辨率:建议 1080P 或 720P,根据你的算力定。算力不够时优先 720P,YOLO 输入也会缩放到 640,过高的原图分辨率只会白耗解码资源。
- 编码格式:优先 H.264,兼容性比 H.265 好。H.265 在低带宽下更省码流,但很多边缘设备的硬解支持做得不如 H.264 成熟。
- 帧率:20fps 或 30fps。如果抽帧间隔是 1 秒,更高的源帧率并不会让你多识别更多画面,反而会占用更多网络带宽和 CPU 解码资源。
另外,模拟器 RTSP 默认端口是 554,可以改成别的,但建议还是遵循常规端口,后面排查问题能少绕一道弯。
2.3 拉流端常见的网络问题与缓冲策略
到了真实环境,RTSP 拉流的坑主要有三类:
- 网络闪断或 IPC 重启后,RTSP 会话不会自动恢复,必须自己实现重连。
- 带宽拥塞时交换机会丢包,画面出现花屏或绿屏,需要靠解码重同步来兜底。
- RTSP 并发会话数超限,一些低端 IPC 允许的最大并发连接只有 4 路或 8 路,多个服务同时连会被拒绝。
缓冲策略上,我不建议把缓冲调得太大。很多人为了让画面“看起来流畅”,把缓冲区拉到几秒甚至十几秒——识别服务确实不卡了,但商品出门动作发生在 3 秒前,你第 15 秒才识别到,业务上已经没有意义。视频识别类场景的拉流缓冲尽量控制在 1 秒以内,甚至在边缘设备上采用零缓冲加丢帧处理,宁可丢弃部分帧,也要保住实时性。
提示:判断拉流缓冲有没有过大的一个简单方法——看画面里物体移动的“残影感”。残影明显说明缓冲区里积压了旧帧,识别结果的时效性已经出问题了。
3. 抽帧策略:既想让 YOLO 看得清,又不想让它“看吐”
3.1 为什么不能直接拿视频流喂给 YOLO
你如果直接把 25fps 或 30fps 的视频流全部接进 YOLO 推理,资源消耗会非常大。显卡或 NPU 要逐帧去做检测,CPU 要逐帧解码,而售货柜场景里商品从货架被拿走的过程虽然关键,但整个视频周期里大量画面是静止的,根本没有目标需要检测。实际项目中,绝大多数目标动作都是短促的,30fps 意味着 80% 以上的算力浪费在没变化或变化很小的画面上。
另一个更深层的原因是:YOLO 这类目标检测模型本身不做时间维度上的记忆,每张图都是独立推断。你想提高识别准确率,与其拼命提高输入帧率,不如在抽帧时保证每一帧的质量和信息量——清晰度、曝光、视角、商品完整性都正常。换句话说,识别系统需要的抽帧策略核心是“在正确的时间抓正确的画面”,而不是“抓得越勤越好”。
3.2 定时抽帧、帧差抽帧和事件抽帧怎么选
抽帧策略大致分三种,也可以混用:
- 定时抽帧:每隔固定时间取一帧。实现最简单,但不够聪明,商品被拿取的瞬间可能恰好落在两帧之间。
- 帧差抽帧:比较相邻帧的画面差异,差异大于阈值再送入识别。相当于“画面有变化才看”,非常省算力,适合检测静止背景下的移动目标。
- 事件抽帧:配合传感器触发,比如售货柜的开门信号触发一轮高频率抽帧,关门后恢复正常频率。这是和业务耦合最紧密的做法,也最准确。
项目实践中我推荐“事件 + 帧差”组合:开门瞬间立刻触发一轮 2fps 左右的密集抽帧,持续 3 到 5 秒;之后每 0.5 秒或 1 秒做一次帧差判断,有变化就送识别,没变化就跳过。这样既保证关键动作不漏,又把算力消耗压到最低。
3.3 抽帧不影响画面质量的三个关键细节
“抽帧不影响画面”这个说法严格讲不准确——你抽取的是送入识别的帧,原视频流该怎么录还怎么录。真正要保证的是“抽出来的帧不要带病”。常见的问题有三个:抽到半帧图像、抽到编码花屏的帧、抽到因曝光正在调整而忽明忽暗的帧。
为避免这些问题,我在代码里通常做三道检查:
颜色通道检查。OpenCV 读出的帧是 BGR 顺序,而 YOLO 训练时通常用 RGB 输入,取帧后必须转一次通道顺序,否则模型看到的颜色是反的,类别识别容易错乱。
缩放时机检查。取到原始帧后不要立刻做简单 resize 拉伸,先用等比例缩放再做 letterbox 填充,补到模型输入尺寸(640×640 或 416×416)。直接拉伸会让商品变形,检测框位置也会偏离真实位置。
帧时间戳记录。每一帧送进模型前打上时间戳,这样即使后面排了很长的处理队列,业务上报时也能用“识别结果的真实采集时间”而不是“处理时间”去对齐事件。很多问题(比如“识别结果比实际动作慢了 3 秒”)靠这个时间戳能快速定位。
4. YOLO 识别环节:不是下载个权重就完事
4.1 售货柜场景的模型选型逻辑
售货柜商品识别是一个“细粒度分类 + 目标检测”混合问题。同样一瓶可乐,不同角度、不同光照、部分遮挡,对模型都是挑战。如果直接用 YOLOv8n 这类轻量模型在 CPU 上跑,速度是上去了,但对细小商品、重叠商品的漏检率会比较高;如果直接上 YOLOv8x,精度好一些,边缘盒子又喘不过气。
选型要看推理硬件:
| 硬件平台 | 建议模型 | 单帧推理耗时参考 |
|---|---|---|
| RK3588(NPU 加速) | YOLOv8s/m 或 YOLO11s | 30ms ~ 50ms |
| Jetson Orin NX | YOLOv8m 或更大 | 20ms ~ 60ms |
| 纯 CPU(如工控机 i5) | YOLOv5s / YOLOv8n | 80ms ~ 200ms |
另外,YOLO 自带的 COCO 80 类预训练权重在这里几乎没有直接价值,它里面没有“可口可乐”“农夫山泉”这类商品类别。你必须针对自己柜内的 SKU 集数据、做标注、做训练。数据量不需要一下子上万张,每个 SKU 先保证不同角度和光照下的图片,一般 100 到 200 张/类,加上数据增强,已经能训练出一个能上线的初版模型。训练数据标注格式如果是 KITTI、COCO 或 VOC,记得先统一转成 YOLO 格式的 txt 文件,网上有现成的转换脚本,但转换完一定要抽查样本框,避免坐标归一化计算错误。
4.2 模型推理的预处理和后处理要点
预处理阶段,最容易翻车的是归一化方式。YOLO 训练时一般用 0~1 归一化,而 OpenCV 读出来的 BGR 帧值域是 0~255,如果忘了除以 255 就直接送推理,模型精度会明显下降。
后处理阶段,目标检测的原始输出是一个很大的特征张量,需要做解码——把网格坐标换算成像素坐标、做置信度过滤、做 NMS 去重、再映射回原图坐标。这些步骤每个框架的 API 不一样,但逻辑是相同的。最容易被忽略的是 NMS 的 IoU 阈值和置信度阈值,它们直接影响漏检和误检:
- NMS 的 IoU 阈值:建议设在 0.5 左右。售货柜场景里同一个商品可能出现多个重叠框,阈值太高会导致不同检测框合并失败,阈值太低又可能把一个商品的两个框全筛掉。
- 置信度阈值:不能一刀切。不同 SKU 的类间相似度不同,需要在验证集上分别看 Precision 和 Recall 曲线后设置。有的类 0.4 以下容易误检,有的类 0.5 以上又容易漏检,分开调更合理。
4.3 模型部署到边缘设备的三个注意点
YOLO 推理服务部署到边端,至少要注意三件事:
- 内存管理:长时间推理最容易踩的坑是内存泄漏。每处理一帧后,中间张量必须即时释放或纳入内存池,特别在 Python 环境下,循环里创建 numpy 数组而不回收,跑一天内存就爆炸。
- 健康检查:推理进程要有探活机制,一旦卡死或崩溃要能自动重启,不能带病运行。售货柜是无人值守设备,靠人工去现场重启的代价太高。
- 版本绑定:模型权重文件和预处理、后处理代码的版本必须绑在一起。升级模型后忘记同步预处理参数,会导致现场结果突然变坏,而且很难查。
这三件事在本地跑 demo 时都碰不到,但放到 7×24 小时的无人环境里,每一条都可能造成业务停滞。
5. 三段流水线的串联与工程化落地
5.1 用队列解耦拉流、抽帧和识别的速度差
拉流是 25fps 连续进行,抽帧是 0.5 到 2fps 按需进行,推理是 100 到 300ms 一帧——三个环节速度天生不匹配。如果用同步串联,整个系统会被最慢的环节拖死,识别一卡,拉流缓冲越积越多,最终延迟大到不可用。用队列解耦是常规做法,但要控制队列长度。
我项目里的设计思路是:拉流线程只负责把最新一帧写入一个循环缓冲区,只保留最新帧,不阻塞,满了就覆盖;抽帧线程每隔设定间隔去读这个缓冲区里的最新帧;推理线程通过另一个队列接收抽帧线程送来的帧,推理完成后把结果放到输出队列,由上报线程消费。这样做的好处是,即使推理偶尔慢了 3 秒,拉流和抽帧也不会被拖死,丢失的只是处理不过来的中间帧——在售货柜场景,这是可以接受的。
接着强调一个关键点:队列里永远只放“最新帧”,不要让旧帧排队等待。推理慢的时候,新帧应该直接替换掉队列里的旧帧,而不是继续排队堆积。
5.2 流水线核心代码骨架
下面给一个简化但能表达核心思想的 Python 代码骨架,用 OpenCV 拉流、线程池或独立线程处理,推理部分用 onnxruntime 做示例。真机部署时把
run_yolo_infer
里的实现换成你的 RKNN、TensorRT 或 OpenVINO 后端即可。
import cv2
import time
import queue
import threading
import numpy as np
RTSP_URL = "rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101"
MODEL_PATH = "best.onnx"
INPUT_SIZE = 640
SAMPLE_INTERVAL = 0.5 # 抽帧间隔,单位秒
frame_cache = {} # 循环缓冲区,保存最新一帧
frame_lock = threading.Lock()
infer_queue = queue.Queue(maxsize=2) # 推理队列,最多缓存2帧
result_queue = queue.Queue() # 结果队列,供上报线程消费
def pull_stream():
"""拉流线程:只负责把最新帧写入 frame_cache"""
global frame_cache
while True:
cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
if not cap.isOpened():
time.sleep(3)
continue
while True:
ret, frame = cap.read()
if not ret:
cap.release()
print("stream broken, reconnecting...")
break
with frame_lock:
frame_cache = {"ts": time.time(), "bgr": frame}
cap.release()
time.sleep(3)
def sample_frames():
"""抽帧线程:间隔取最新帧,送入推理队列,不排队堆积"""
next_time = time.time()
while True:
now = time.time()
if now >= next_time:
with frame_lock:
latest = frame_cache.get("bgr")
ts = frame_cache.get("ts")
if latest is not None:
if infer_queue.full():
try:
infer_queue.get_nowait() # 丢弃旧帧
except queue.Empty:
pass
infer_queue.put((ts, latest))
next_time += SAMPLE_INTERVAL
time.sleep(0.01)
def yolo_process():
"""推理线程:从推理队列取帧,完成预处理 -> 推理 -> 后处理,产出结果"""
import onnxruntime as ort
sess = ort.InferenceSession(MODEL_PATH)
while True:
ts, frame_bgr = infer_queue.get()
t0 = time.time()
dets = run_yolo_infer(sess, frame_bgr) # 预处理/推理/后处理
infer_ms = (time.time() - t0) * 1000
result_queue.put({"ts": ts, "infer_ms": infer_ms, "dets": dets})
这段代码把三个环节拆成了三个独立线程,线程间的通信只有队列和锁。你没有看错,我用
frame_cache
保存最新帧而不是用队列去堆积,这个细节很重要。抽帧线程把最新帧交给推理线程后,即使模型推理需要 300ms,新来的帧仍然在最近的位置被处理,不会出现积压导致的识别结果严重滞后。
5.3 日志、监控与异常恢复机制
一条流水线如果不上日志、不做监控,基本等于裸奔。至少要记录:拉流会话的连接、断开、重连事件;每一帧的采集时间、推理耗时;识别结果、置信度;系统 CPU 和内存水位。
异常恢复机制我强烈建议用 systemd 或 supervisor 管理进程:拉流断线时自动重连,重连次数超过阈值就重启拉流进程,同时把告警推到钉钉或企业微信。实测中,线上环境出现的大部分问题都能从日志里定位,关键是日志要“有内容”而不是只打 INFO 级别的一堆废话。每一帧的耗时和错误码都打全,排查效率完全不一样。
注意:不要把业务逻辑和识别逻辑写在同一个普通函数里层层调用。拆成独立进程或独立线程之后,即使推理进程崩溃,拉流、抽帧和上报还能继续跑,重启推理服务后流水线能自动恢复。这个架构带来的韧性,在无人值守的售货柜上价值巨大。
6. 踩坑记录与性能调优:现场环境的意外永远比你想的多
6.1 拉流断开不自动重连,程序卡死
真机接上后遇到的第一个坑就是 IPC 重启后,RTSP 连接断了,程序没有感知,一直卡在读帧上。OpenCV 的 VideoCapture 在断流时,不是返回 False 就是一直阻塞,最气人的是它可能直接崩溃。后来我统一改成后台拉流线程,读帧失败计数,连续失败超过阈值就销毁并重建拉流会话,同时给 RTSP 增加心跳检测,超过设定时间没有收到流数据就主动触发重连。
排查工具上,我用 FFmpeg 命令行直接拉流测试,能快速确认是不是 IPC 本身的问题。比如
ffmpeg -rtsp_transport tcp -i rtsp://... -t 10 -f null -
,这条命令跑 10 秒,如果输出里的
frame=
数量稳定增长,说明推流正常,问题在组件;如果中途报
Error while decoding stream
,那就要查网络和编码参数了。
6.2 抽帧间隔和识别速度不匹配导致的漏检
第二个坑出现在同时跑两路摄像头时:你以为 0.5 秒抽一帧,但推理一帧要 250ms,抽帧队列里的帧越来越多,模型处理完的“新帧”其实是 1 秒前的旧帧。商品动作早就过去了,系统还在识别旧画面,动作结束之后结果才报出来。
解决思路就是前面代码骨架里写的:抽帧不是往队列里“塞”,而是往缓冲里“替换最新帧”,新帧来了旧帧直接丢掉。这样即使推理慢一点,处理的最多也只是“最新的画面”,结果不至于滞后太多。顺带说一句,这个方案要比单纯增大队列容量靠谱得多,队列容量再大也只是延迟爆炸前的缓冲。
6.3 降低 CPU 和内存占用的实测技巧
最后说几个我在售货柜盒子上实测有效的降资源技巧:
- 多路摄像头如果型号和编码相同,可以复用同一个解码上下文,尤其 H.264 流优先走硬解,软解几路同时开 CPU 直接拉满。
- YOLO 推理的 batchsize 不要乱设,边缘盒子上设为 1 最稳,batchsize 调大并不会提升单帧速度,反而容易触发显存或 NPU 内存峰值。
- 限制队列和环形缓冲区长度,不要让数据无限制堆积。长时间跑内存膨胀的根因,绝大多数是无界队列。
- 把识别进程和业务进程分开,不要写在同一个进程里。否则一旦内存膨胀,你根本分不清是业务代码泄漏还是识别代码泄漏。
按这四条调完,我那个 RK3588 盒子的 CPU 占用从 80% 多降到了 40% 左右,连续跑一周再没出现过死机。GPU/NPU 的利用率也稳定在一个合理的区间,不像之前那样时不时打满。
调优是边跑边做的,不是开工前一次性做完的。先保证功能正确、流水线稳定,再根据监控数据做针对性的资源压缩,这个顺序能省很多无效工作。
整个售货柜项目做下来,我最大的体会是:视频识别的难点从来不在 YOLO 模型本身,而在“把稳定、低延迟的画面送到模型面前”这段路上。拉流会话管理、抽帧时序、中间缓冲策略、异常恢复机制,每一环都比想象中重要。尤其当你面对的是 7×24 小时无人值守的设备,任何一个环节的脆弱都会被时间无限放大。希望这篇流水线实战记录能帮你少走点弯路,尤其是那些只会在本地跑 demo 时看不出来的细节坑。如果你也在做类似的视频识别项目,建议先花一天时间把拉流和抽帧这两个环节打扎实,再回头去调模型,你会发现原本以为复杂的项目突然变简单了不少。
更多推荐

所有评论(0)