“多摄像头路口项目”一眼看去是个检测任务,但真正做起来,最先卡的往往不是模型精度,而是业务方把一摞统计需求拍在你桌上时的窒息感:每个进口方向的车流量、早晚高峰的过街行人量、左转直行的分类占比……我上个月刚把一个试点路口的YOLO26方案从Demo推到上线,中间踩了一堆文档里不会写的坑,这篇就把服务器+多摄像头从拉流、推理到统计报表的完整链路拆开讲清楚,给准备做同类项目的朋友一个参照。
1. 多摄像头路口项目:需求拆解比模型更费神
1.1 业务方真正要的不是“检测框”,而是“统计报表”
先说一个大多数教程不会告诉你的真相:在智能交通这种行业项目里,检测框只是中间产物,没有任何一个客户会坐在大屏前看实时框,他们要的是“今天7点到9点东进口直行通过多少辆、左转多少辆、北进口行人过街多少人次”,甚至要按15分钟粒度输出趋势曲线。
这意味着我们做落地时,核心不是把YOLO26的mAP再刷高几个点,而是把“检测—跟踪—计数—统计”这条链路做成稳定可交付的东西。项目刚开始,业务方给了一张表格,列了十几个字段:方向、车道、车型分类、流量、平均车速、行人过街次数、违规闯入预警。真做起来才发现,一个“方向”字段就要牵扯到摄像头的安装角度、虚拟线/区域划定、跟踪轨迹的方向判定三件事。
我的建议是:动手前先把统计口径定死。比如“车流量”是按通过停止线的车辆数算,还是按进入路口区域算?“过街行人”是按进入斑马线区域算,还是按完整穿越斑马线算?这两个口径不同,计数结果可能差20%。我们最后和业务方逐条对齐,形成了一份《统计口径说明》,后面所有开发和验收都按这份文档走,避免上线后扯皮。
1.2 为什么选了YOLO26而不是上一代模型
选型这件事,一开始团队内部也有争议。有人觉得用上一代成熟版本就够了,毕竟资料多、踩坑记录全;有人想试试更新版本的新特性。最后拍板用YOLO26,主要看中三点:
- 小目标检测能力更好。路口场景里,远端来的车辆在1080P画面里可能只有几十个像素宽,尤其是对向车道,一辆轿车刚进入画面时整框可能不到30×30像素。YOLO26在这类小目标上的召回率明显比上一代好。
- 推理速度上限高。我们用TensorRT做加速后,单张1080P图像的端到端推理可以压到5毫秒以内(batch=1时的测值),这给多路摄像头并发留下了余量。
- 部署生态成熟。虽然YOLO26名字听着新,但从导出ONNX到转TensorRT的流程和之前一样顺畅,没有出现“新模型无法落地”的尴尬期。
当然,选型不是越新越好。如果项目只做单路离线视频分析,上一代版本完全够用。但多摄像头实时场景对单路延迟和吞吐都有硬指标,YOLO26的收益是实打实的。
2. 服务器与摄像头端:视频流的接入设计
2.1 多路RTSP拉流的解码瓶颈
很多人在测试环境只用单路视频文件跑通模型,一上现场就翻车,根因就是视频流接入没设计好。我们试点路口的配置是4个摄像头,每个1080P@25fps,通过RTSP协议推流到机房服务器。如果按网上最常见的方式写个脚本循环读帧,很快就会发现CPU被解码占满,显卡利用率却只有30%,推理进程饿着肚子等数据。
问题出在FFmpeg软解上。4路1080P的H.264码流,软解CPU占用率能吃掉十几个核心,留给后续处理的资源所剩无几。我们的解法是改用GPU硬解,也就是NVDEC。具体来说,用FFmpeg的hwaccel参数指定cuda,直接让显卡把视频帧解码到显存,解码几乎不占CPU。
这里有一份简化的拉流参数,实测可以稳定跑4路:
ffmpeg -rtsp_transport tcp -i "rtsp://user:pass@camera_ip:554/stream1" \ -c:v h264_cuvid -vf scale_npp=1920:1080 -f rawvideo -pix_fmt bgr24 pipe:1注意-rtsp_transport tcp很关键。现场网络环境里UDP拉流经常丢包花屏,TCP虽然延迟略高一点,但稳定性好得多。摄像头如果支持RTSP over TCP,建议优先。
2.2 用队列+多进程把“拉流”和“推理”彻底解耦
最开始我们图省事,拉流和推理放在同一个进程里:读一帧、推理一帧、再读下一帧。结果解码偶尔卡顿一下,整体延迟就跟着抖动。后来改成典型的生产者-消费者架构:拉流进程只管读帧,把帧放进队列;推理进程从队列取帧,处理完再交给统计模块。
结构大致是这样的:
- 每个摄像头对应一个拉流进程,进程内做硬解,输出numpy数组。
- 所有拉流进程把帧放入一个共享队列(我们用multiprocessing.Queue或带锁的deque)。
- 推理进程组从队列批量取帧,组成batch跑TensorRT。
- 推理结果连同帧的时间戳、摄像头ID一起发给跟踪与统计模块。
这里有个容易被忽略的细节:队列长度必须控制。如果拉流速度持续大于推理速度,队列会无限增长,延迟越来越大,最后看起来就像“实时”,实际看的是半分钟前的画面。我们设了最大队列长度,满了就直接丢弃旧帧。丢帧不影响统计,因为计数依赖的是跟踪轨迹的连贯性,我们依靠时间戳做校正,不依赖“每一帧都不落”。
时间戳这块我多说一句:所有帧必须带两个时间——摄像头采集时间(如果RTSP里带)和服务器接收时间。后续统计时段划分、平均速度估算都靠它。只记“处理时间”是不行的,一旦推理排队,处理时间就失真了。
3. 从检测框到路口统计:跟踪与虚拟区域
3.1 划定虚拟检测区域
模型输出的是整幅画面里的所有目标框,但业务方只要路口特定区域内的统计结果。我们做法是在画面坐标系里划定多边形ROI,比如:
- 每个进口方向的车道停止线前画一个四边形,作为车辆计数区。
- 斑马线区域画一个矩形或多边形,作为行人过街计数区。
- 路口内部区域单独画一个ROI,用于滞留车辆报警。
判定逻辑很简单:取目标框底边中点(因为底边通常贴着地面,比中心点更能代表目标在地面的位置),判断这个点是否落在ROI内。只检测框与ROI有重叠或者中心点在ROI内,在车流密集时都会产生很大的计数误差,我们统一用“底边中点进入ROI”作为进入判据。
def point_in_polygon(point, polygon): # 标准射线法判断 x, y = point n = len(polygon) inside = False p1x, p1y = polygon[0] for i in range(1, n + 1): p2x, p2y = polygon[i % n] if y > min(p1y, p2y): if y <= max(p1y, p2y): if x <= max(p1x, p2x): if p1y != p2y: xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x if p1x == p2x or x <= xinters: inside = not inside p1x, p1y = p2x, p2y return inside划定ROI时有个经验:不要贴着画面边缘。画面边缘经常有目标只露出一半,底边中点不稳定,计数忽高忽低。宁可把ROI往里缩十几个像素,统计结果更干净。
3.2 跟踪器选型与ID切换引发的计数误差
检测只是瞬时的,要统计“一辆车从出现到离开”只有一条路:跟踪。我们试过两个方案:
- DeepSort类方案:ReID特征+卡尔曼滤波,ID稳定性好,但对行人这种外观变化大的目标,特征提取耗时高,实时性吃紧。
- ByteTrack方案:纯运动关联,不提取外观特征,速度快,但在目标密集、相互遮挡多的路口,ID切换率会升高。
最终我们选了ByteTrack,然后用业务逻辑去弥补ID切换问题。为什么?因为路口车辆的运动会受到车道的强约束,同一辆车不大可能突然横穿到另一条车道,我们可以利用轨迹的连续性来过滤掉不合理的ID抖动。
ID切换如何解决?场景是这样的:一辆黑色轿车右转,车身被旁边一辆公交车短暂遮挡,重新出现后被分配了新ID。如果在“目标第一次进入ROI”就计数一次,之后换ID不会再计入,这个误差就规避了;但如果我们是按“目标穿过停止线”计数,且换成新ID后目标还在ROI内,就会被重复记一次,那就必须给每个目标加状态机。
我们的做法是:为每个目标维护一个状态——NEW(新出现)、COUNTED(已计数)、LEFT(已离开)。只有当目标从NEW变成COUNTED,计数才加1,而且一旦COUNTED,即使ID被重新分配,只要位置还连续,也不会重复累加。简单说:用“轨迹段”代替“目标ID”作为计数单元。
3.3 方向判定:轨迹向量比单帧位置靠谱
车流量不仅要统计“多少辆”,还要分方向。业务方要的是东进口左转、直行、右转分别多少,行人过街是南北方向还是东西方向。
方向判定我们试过两种思路:
- 思路一:看目标进入的是哪个车道ROI、从哪个ROI离开。这种方案准确率高,但需要画很多细粒度的ROI,每个方向画一个,现场调试量大。
- 思路二:利用跟踪轨迹的起点和终点计算位移向量,按向量与画面坐标轴的角度分类。
我们最终用思路二为主,思路一为辅。具体做法是:保存目标最近5帧的位置,计算平均速度向量,然后根据角度映射到预设方向。比如在画面坐标系中,朝右上方移动的车辆归为“东进口直行”,朝左移动归为“西进口左转”之类。这里没有通用公式,因为每个摄像头的朝向和安装高度都不同,必须现场标定一次。
一个调参经验:方向判定的角度区间不要只分45度一档。靠近路口时车辆轨迹实际是弧线,尤其右转车,轨迹向量会从“直行”缓缓转到“右转”,容易在区间边界抖动。我们给每个方向留了10度左右的死区,轨迹角度落在死区里就沿用上一帧的方向,直到离开死区。
4. 数据修正与高峰报表生成
4.1 统计误差的三个来源
系统上线试运行了三天,我们和人工计数对比,发现误差基本在5%-8%。误差主要来自三类情况:
- 误检:路边的交通标志牌、广告牌上的车图片会被识别成真车。这类目标进入ROI后会停留很久,干扰区域计数。
- 漏检:夜间对向车灯过曝、雨天雨刷遮挡、行人打伞导致检测框丢失,目标轨迹断裂。
- 重复计数:前面说的ID切换,以及行人站在斑马线中间犹豫、来来回回走动,可能被记成多次“过街”。
针对误检,我们在ROI外沿加了一个“观察区”:目标必须先出现在观察区,再进入ROI,才会计数。广告牌车辆永远停在ROI外,直接被过滤掉。 针对漏检,我们用跟踪预测来兜底:检测丢失后,卡尔曼滤波还能维持几帧预测轨迹,短时间漏检不会让目标立即消失。只有连续丢失超过15帧,才判定目标离开。 针对行人徘徊,我们加了一条“最小穿越时间”规则:行人从进入斑马线到离开至少需要2秒,低于这个时间的“穿越”视为无效,多半是目标在ROI边缘晃了一下。
4.2 时间窗口聚合与可视化输出
单帧的检测和计数结果没有业务价值,真正交付的是按时间窗口聚合的数据。我们以1分钟为最小粒度,每分钟生成一个统计桶(bucket),包含:
| 字段 | 说明 |
|---|---|
| camera_id | 摄像头编号 |
| direction | 方向/车道 |
| vehicle_count | 该方向车辆通过数 |
| pedestrian_count | 斑马线区域过街行人次数 |
| avg_speed | 平均车速(km/h) |
| time_bucket | 分钟级时间戳 |
统计桶写入时序数据库,前端报表基于这些桶做聚合。比如业务方要看“早高峰车流量”,就查7:00-9:00的bucket,按5分钟重采样成曲线。
这里有一个需要注意的点:数据库写入不要直接面向下游报表,中间加一层清洗。原始数据里偶尔会有单个分钟桶异常偏高(比如一辆大挂车被拆成两个目标),我们用拉依达法则做一次离群值过滤,把超过均值三倍标准差的桶标记出来,由人工复核,而不是直接展示。
5. 实测性能与现场踩坑记录
5.1 单卡推理性能与算力预留
整个系统在服务器上的性能表现,我列一组实测数据供参考(都是4路1080P@25fps同时分析):
- 解码:GPU硬解,4路总CPU占用不到1个核心。
- 推理:batch=4跑YOLO26(TensorRT FP16),平均每批耗时约12-15毫秒,折算单帧约3-4毫秒。
- 跟踪+统计:ByteTrack加ROI判定,CPU占用约2个核心。
- 端到端延迟:从摄像头采集到画面出现在监控大屏,约300-500毫秒,主要在网络传输和显示缓冲,推理本身占比很低。
算力上我们留了约35%的余量。因为现场高峰时段(早晚高峰、节假日)目标数量会暴涨,检测框多了,NMS和跟踪关联的计算量都会上升,余量不足时延迟会突然拉高。建议所有做这类项目的朋友,按峰值负载的1.5倍预留算力,别按平均负载配。
5.2 三个经典的现场问题
实际部署中我们遇到几个预料之外的坑,单独拿出来说说。
第一个是摄像头时钟不同步。四个摄像头来自不同批次,NTP同步没配好,导致流里的时间戳差距越来越大。统计时段按分钟聚合时,同一辆车可能被记到前后两个桶里。最后我们在服务器统一打时间戳,完全不理睬摄像头自带的采集时间,才把这个问题压下去。
第二个是夜间车灯造成的检测框分裂。对向车灯直射时,一辆车可能被检测成两个紧挨着的框,计数直接翻倍。我们用NMS后处理加了一个“同色框合并”策略:两个框中心距离小于阈值,且面积相近,就合并成一个。实测夜间误差从12%降到了6%。
第三个是树影和雨刷带来的误检。阳光强烈时,路面树影边缘在模型看来很像“行人轮廓”;下雨天雨刷摆动会让挡风玻璃区域产生类似“车辆边缘”的纹理。这类问题模型层面很难根治,我们在现场做的是调低夜间模式的置信度阈值,同时把“目标最小连续出现帧数”从1帧提高到3帧,宁可慢半拍,也不误报。
5.3 一个绕不开的运维细节:模型热更新
最后说一个长期运维视角的问题。交通场景的视觉分布会随时间变化:夏天植被茂盛、冬天光秃秃、新铺的沥青路面反光特性不同、甚至隔壁工地开工后粉尘都会影响画面。模型上线不是终点,而是迭代的起点。
我们为这个项目设计了简单的闭环:每周导出一次误检和漏检样本,人工筛选后并入训练集,增量训练YOLO26的一个微调版本。整个推理服务支持热更新权重,新模型验证通过后直接替换,不影响线上运行。这个机制看着简单,但能保证系统在换季、修路、新摄像头启用这些节点上不会逐渐退化。
6. 落地后的几点体会
回看这个项目,YOLO26本身解决的是“看得到、认得准”的问题,真正决定项目好坏的反而是那些工程化细节:视频流怎么接、ROI怎么画、ID切换怎么处理、统计口径怎么定。模型精度再高,如果统计模块的重复计数处理不当,业务方照样会质疑你的数据。
如果让我给准备做类似项目的团队一个建议,就是前期一定要安排至少一周的现场数据采集。用目标路口的真实视频去跑通完整链路,而不是拿公开数据集或者自己录的测试视频开发。现场的光线角度、镜头畸变、树木遮挡这些因素,是任何测试集都模拟不出来的。多跑几遍真实数据,后面上线时遇到的问题会少一大半。
这个项目我们还在继续迭代,下一步计划接入更多路口的摄像头,做区域级的联动分析。YOLO26的底子撑得住这个扩展,剩下的就是不停地在现场打磨。后面如果有了新的收获,我会再整理一篇,把联动分析和跨摄像头跟踪的实战经验写出来。