做智能交通或者视频监控方向的开发者多半都接过类似的活给一段路口监控视频把行人和车辆分开数清楚还要让每个目标从进画面到出画面始终保持同一个编号不重复计数、不跟丢。我最近把一套基于YOLOv8模型的行人车辆多目标检测计数与跟踪系统完完整整跑了一遍从数据准备、模型训练到跟踪器接入、计数逻辑实现再到导出推理引擎部署到视频流上整个过程沉淀了不少经验。这篇文章就是这套系统的完整拆解适合刚接触目标检测与跟踪的开发者也适合已经用熟YOLO但想补上跟踪计数能力的工程师。我会把方案选型的原因、参数怎么定、代码怎么写、踩过哪些坑都讲清楚。先说结论这套系统的核心思路是“检测负责认跟踪负责记计数负责算”。检测模型给出每一帧里的行人、车辆位置框跟踪器把跨帧的同一个目标串成一条轨迹并分配稳定ID计数模块基于轨迹与虚拟区域/虚拟线的空间关系分门别类统计流量。三个环节各自独立又互相衔接拆开调试、组合部署都很方便。1. 整体架构与设计思路1.1 需求拆解检测、跟踪、计数分别解决什么问题拿到“行人车辆检测计数与跟踪”这个需求第一件事不是急着写代码而是把需求拆成三个独立的问题第一检测。模型要在每一帧画面里找到所有行人、车辆输出类别和边界框。这里的关键是类别定义。实际路口场景里“车辆”这个词太笼统是轿车、卡车还是公交车我的做法是训练时保留细类别推理后再按业务规则合并。比如car、truck、bus合并成“车辆”person单独保留摩托车和自行车视需求决定是否归入“非机动车”。这样做的好处是如果后来需求变化比如要单独统计公交车模型不用重新训练改一行合并逻辑就行。第二跟踪。视频是连续的但检测是逐帧独立的模型本身不知道上一帧的红色轿车和这一帧的红色轿车是不是同一辆。跟踪器的任务就是把跨帧的检测框关联起来给每个目标分配一个唯一的track ID。这个ID从目标出现到消失全程不变ID不跳变、不互换是后续计数可信度的基础。第三计数。有了稳定ID和轨迹计数才有意义。分两种典型需求一种是断面流量统计比如“由东向西的车流量”做法是画一条虚拟检测线目标轨迹穿过线的瞬间计数加一另一种是区域统计比如“停车场当前剩余车位”做法是画一个多边形区域目标进入区域时加一离开时减一得到实时在场数量。两种逻辑可以同时存在互不干扰。1.2 流程设计与模块解耦整个系统我设计成五个模块串联视频输入、目标检测、多目标跟踪、计数逻辑、结果输出。模块之间通过标准数据结构衔接前一个的输出就是后一个的输入。视频输入模块负责读视频流或文件做解码、缩放、帧率控制检测模块输入单帧图像输出检测框列表类别、置信度、坐标跟踪模块输入连续帧的检测框输出带track ID的目标列表计数模块输入带ID的轨迹点结合预设的线和区域输出统计数据结果输出模块把统计结果和标注后的画面推送到终端、接口或数据库这种解耦设计最大的好处是可替换性。检测模型今天用YOLOv8明天换YOLOv9甚至其他模型只要输出格式不变后面三个模块一行不用改。跟踪器也是同理我从DeepSORT切到ByteTrack只动了跟踪模块内部几十行代码计数模块完全无感。另外要注意计数模块不能依赖检测框的绝对像素坐标做硬编码。不同分辨率下同一个画面的坐标体系不一致我一般在读取配置时把虚拟线和区域坐标按分辨率比例归一化部署时乘以实际画面尺寸。这样视频源从1080P换到2K配置不用重新标一遍。2. 检测模型选型与训练要点2.1 为什么选YOLOv8选哪个版本YOLOv8是目前落地性价比最高的检测模型之一官方仓库开箱即用Python接口和命令行都齐全训练、验证、导出一条龙。对于行人车辆这类常规目标不需要自己从零改网络结构官方预训练权重在公开数据集上已经有很好的基础迁移到自己的场景收敛很快。版本选择上YOLOv8按照模型体积分成n/s/m/l/x五档。我实测下来在1080P视频流上n型号单帧检测耗时约5毫秒s型号约8毫秒m型号约12毫秒l型号约20毫秒以上。做多目标跟踪时检测速度会直接拉低整体帧率因为每一帧都要跑一次模型。如果摄像头数量多、服务器资源有限选n或s更稳如果追求精度画面里目标小且密集选m是性价比上限l在实时场景里容易吃不消。我最终选了m版本配合TensorRT加速后单路视频流能稳定跑在40 FPS以上。2.2 数据准备与标注细节模型最终效果的上限由数据决定。公开的COCO预训练权重虽然认识行人、汽车、公交车这些类别但实际监控场景与自然图像差异很大摄像头角度高、目标尺度小、遮挡多、光线复杂。所以我的经验是一定要用自己场景的数据做微调。标注工具我用开源的labelImg或X-AnyLabeling二选一都行关键是标注规范要统一。行人框要框到完整轮廓包含头部和脚部不要只框上半身车辆框要包含车身主体不要因为遮挡只框半个车头。类别建议拆细person、car、truck、bus、motorcycle、bicycle。这样训练时模型学到的特征更精细推理合并时也有余量。数据量方面我个人的经验值单场景固定摄像头标注3000到5000帧就能有明显效果提升多场景泛化需求建议每类目标不低于5000个实例。标注完之后要做随机划分训练集占80%验证集和测试集各10%。注意划分时最好按视频片段划分不要把一个连续视频的帧随机塞进训练集和验证集否则相邻帧高度相似验证集分数会虚高。2.3 训练参数与调参心得训练命令直接用官方仓库的格式配置文件里主要改三个地方数据集路径、类别数、训练超参数。我用的事例如下yolo detect train data/path/to/dataset.yaml \ modelyolov8m.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ project/path/to/project \ nameexp_person_vehicle \ patience20几个参数的取舍说明一下。输入尺寸imgsz默认640对监控场景够用往上涨到960能提升小目标召回但推理耗时也涨明显。我先用640跑通后期再对比960决定是否切换。batch大小取决于显卡显存我用的24GB显存卡跑m模型batch16没有问题显存小的降到8或4。patience设为20连续20轮验证集指标不涨就提前停止省时间也防止过拟合。训练过程中我重点关注两个指标一个是验证集的mAP50-95这是整体精度另一个是每个类别的召回率recall因为计数场景最怕漏检。漏检会导致目标中途消失跟踪ID断掉重新出现时被当成新目标又计一次数。如果发现哪类目标的recall偏低优先补标注这类目标的难例帧比盲目加训练轮数管用。调参时还注意一个坑类别不均衡。行人数量通常远多于卡车和公交车训练时模型容易偏向行人。解决方案是在数据配置里对少数类别做重复采样或者在损失权重上微调。Ultralytics的默认配置对类别均衡做了基础处理但长尾分布明显时还是要自己介入。3. 多目标跟踪方案与计数逻辑实现3.1 跟踪器选型ByteTrack与DeepSORT的取舍多目标跟踪的主流方案里DeepSORT和ByteTrack是两种截然不同的思路。DeepSORT的核心是“检测ReID外观特征”检测框和已有轨迹匹配时同时看位置运动相似度和外观特征相似度行人这种外观差异明显且长时间遮挡后重新出现的场景它表现更稳。但它的代价是多了一个ReID模型推理耗时上涨而且车辆在监控视角下外观相似度极高黑色轿车和黑色SUV长得差不多ReID特征区分度不够时反而引入误匹配。ByteTrack的思路则完全不同它纯粹基于检测框的位置交并比做关联不使用外观特征。它的关键创新是同时利用高置信度和低置信度检测框用低分框处理遮挡场景的轨迹保持。对我这个项目来说ByteTrack有两个不可抗拒的优势一是快纯几何计算没有额外模型负担二是车辆行人都适用不用为不同目标类型单独调ReID。实测在密集路口场景ByteTrack的ID切换率只比DeepSORT略高一点点但帧率提升接近20%果断选了ByteTrack。3.2 跟踪器与检测结果的衔接ByteTrack的接入核心是把检测结果转成标准格式后喂给跟踪器再从跟踪器拿回带ID的目标列表。我用的是官方b architecture实现但项目里做了一层简单封装把检测和跟踪串成一条完整链路。伪代码如下from ultralytics import YOLO import numpy as np class TrackEngine: def __init__(self, weights_path): self.detector YOLO(weights_path) self.tracker None # 初始化ByteTrack实例 def process_frame(self, frame): # 1. 检测得到boxes, scores, class_ids results self.detector(frame, verboseFalse)[0] boxes results.boxes.xyxy.cpu().numpy() scores results.boxes.conf.cpu().numpy() class_ids results.boxes.cls.cpu().numpy().astype(int) # 2. 过滤低置信度框阈值根据场景调参 keep scores 0.4 boxes boxes[keep] scores scores[keep] class_ids class_ids[keep] # 3. 交给跟踪器得到带ID的目标 tracks self.tracker.update(boxes, scores, class_ids, frame.shape) # tracks: [x1, y1, x2, y2, track_id, class_id, score] return tracks这里有个细节值得展开置信度阈值的选取直接影响跟踪效果。阈值设太高低分框被过滤掉跟踪器找不到目标轨迹中断阈值设太低大量误检框进入跟踪器产生大量碎片轨迹ID浪费严重。我的做法是先用0.5跑一遍观察如果发现ID切换频繁把阈值降到0.3到0.4让更多中等置信度框参与关联反而能稳定轨迹。ByteTrack的特性就是对低分框容忍度高放开阈值通常利大于弊。跟踪器内部还有一个参数是轨迹最大存活帧数我设的是60帧。目标被遮挡后检测框消失ByteTrack会保留轨迹并预测位置最多等60帧超过就判定目标已经离开。这个值要按场景调路口车辆通过时间短40到80帧都合理商场行人走动慢、遮挡多可以放到100帧以上。3.3 行人车辆分离统计与计数逻辑计数模块是整个系统里最需要结合业务逻辑的地方。我的实现同时支持两种模式虚拟线和虚拟区域。虚拟线的做法是在画面里画一条线段记录线段的两个端点然后判断目标在相邻两帧的位置变化。核心逻辑是判断目标中心点是否跨线以及跨线的方向。我用向量叉积判断点在线的哪一侧连续两帧符号发生变化就认为跨线旧位置到新位置的方向决定计入哪一类。def cross_line_count(prev_pos, curr_pos, line_start, line_end): def side(p): return (line_end[0] - line_start[0]) * (p[1] - line_start[1]) - \ (line_end[1] - line_start[1]) * (p[0] - line_start[0]) prev_side side(prev_pos) curr_side side(curr_pos) if prev_side * curr_side 0: # 发生了跨线根据方向向量判断是正向还是反向 direction curr_pos[0] - prev_pos[0] if direction 0: return in, 1 else: return out, 1 return None, 0虚拟区域的做法是用多边形框住一个区域判断目标中心点与多边形的包含关系。进入区域时该目标标记为“inside”离开时标记为“outside”并同步更新当前在场数量。多边形包含判断用射线法这种是图形学标准算法直接复用成熟实现即可。行人车辆分离统计非常简单跟踪结果里每个目标自带class_id我在计数时按类别分组。此外为了统计更准确同一目标只允许计数一次。实现上用一个字典记录每个track_id属于哪个方向或区域已经计过的ID直接跳过不需要依赖轨迹长度。这样即使跟踪器短暂断开重新分配新ID也只是多计一次不会在同一帧里反复累加。4. 部署落地与工程化优化4.1 模型导出与推理加速训练好的PyTorch模型不能直接用于生产环境推理速度不够。我的标准流程是PyTorch模型导出ONNX再从ONNX转TensorRT引擎。Ultralytics官方支持一条命令导出yolo export model/path/to/best.pt formatonnx dynamicFalse opset12 simplifyTrue yolo export model/path/to/best.pt formatengine device0 halfTrueTensorRT的FP16精度对精度损失非常小行人车辆这类大目标场景完全感知不到差异但推理速度能翻倍。我实测同一个m模型在RTX 3090上FP32大概10毫秒FP16降到5到6毫秒。如果是边缘设备如Jetson系列FP16更是必备选项。导出时还有两个易踩的坑。dynamicFalse意味着固定输入尺寸如果你的场景只需要固定分辨率就别开动态尺寸引擎优化更极致。batch固定为1因为做实时视频流是逐帧推理batch1的延迟最低。如果以后要支持多路视频同时跑建议导出batch4或8的版本用batch维度提升吞吐而不是开dynamic。4.2 帧处理与跳帧策略视频流处理最大的矛盾是推理速度和业务需求之间的平衡。检测加上跟踪单帧处理耗时大概在15到25毫秒之间浮动看起来能跑40 FPS左右但这是理想情况。实际视频流解码、图像缩放、结果绘制都会抢CPU时间多路并发时更明显。我的做法是引入跳帧检测机制。具体来说检测模型每两帧跑一次跟踪器每一帧都跑。也就是说奇数帧只做跟踪预测与IOU匹配偶数帧用新的检测结果校正轨迹。这个策略在车辆目标上效果极好因为车辆运动方向稳定两帧之间位置变化小跟踪预测足够可靠行人偶尔有急转弯两帧间隔也能覆盖。跳帧后检测的负载减半整体帧率提升约35%而计数精度几乎不受影响。如果要更进一步可以做ROI裁剪。固定摄像头场景下画面两侧的人行道、栏杆等区域永远不会出现目标可以预先标出有效检测区域把区域外的检测框直接过滤掉。这样不仅减少误检还能让跟踪器专注在有效区域减少无效轨迹的ID消耗。4.3 结果输出与业务对接计数结果只存在内存里不够最终要对接业务系统。我的输出方案分三种按需选择一是可视化输出。用OpenCV在帧上绘制检测框、track ID、计数统计文字写进输出视频文件或推送到RTMP流。这个用于现场大屏展示和人工复核简洁直观不经过中间服务器延迟最低。二是结构化数据输出。每个目标的信息整理成JSON时间戳、track_id、类别、置信度、框坐标、是否跨线、跨线方向。通过HTTP POST或MQTT推送到后端服务业务系统拿到数据自己做统计和存储。这个方案的优点是把“识别”和“业务”解耦后端可以灵活处理比如按时间段聚合车流量。三是状态记录输出。计数模块维护一份分钟级聚合结果每分钟输出一次“行人通行数、车辆通行数、当前区域内人数”等汇总数据写入数据库。这个适合长期趋势分析和大屏历史查询。我实际项目中三套方案同时启用现场画面走可视化、实时告警走MQTT、历史报表走数据库。5. 常见坑点与排查实录5.1 ID频繁切换怎么办最典型的现象是一辆黑色的车从画面这头开到那头track ID从1变成2又变成3计数就会重复统计。排查思路分三步。第一步检查检测框稳定性。置信度阈值偏高时目标被树遮一下检测就丢了跟踪器等不到新框旧轨迹死亡目标重现时分配新ID。处理方法就是降低阈值并调大轨迹最大存活帧数。第二步检查目标间距离。行人密集并行时两个人检测框发生IOU重合ByteTrack的纯几何关联容易把两人身份互换。这个场景下如果有硬需求就要换带外观特征的跟踪方案或者提高检测精度让框更贴合人体轮廓。第三步检查摄像头抖动。画面抖动会造成同一目标前后两帧的框位置偏移过大IOU匹配失败。解决方式是做帧稳定处理或适当放宽跟踪器的匹配阈值——ByteTrack支持按目标类别、运动速度设置不同的匹配距离阈值我调过一次后抖动场景的ID切换率下降了五成以上。5.2 漏检导致计数偏低漏检是计数系统的天敌特别是远处的小目标。监控摄像头视野深、角度高画面深处的行人和车辆可能只有十几个像素高检测模型在这种微小尺度上召回率下降明显。改善思路从三个阶段入手。数据层面专门补充远距离小目标的标注帧这是最有效的做法输入层面把推理分辨率从640提升到960小目标特征更清晰但耗时增加需要配合TensorRT压缩工程层面对画面底部近处和远处分别设置不同的置信度阈值近处要求0.5以上远处0.3以上就保留因为远处检测虽然置信度低但坐标偏差小跟踪器能接得住。这个分区域阈值策略实测在长直路口场景漏检率下降了约20%。5.3 夜间与恶劣天气的模型衰减夜间场景是行人车辆检测的另一个重灾区。画面暗、有车灯过曝、行人穿深色衣服几乎融入背景。我见过不少项目白天效果拉满一到晚上就现原形计数结果完全不可用。我的建议是如果业务覆盖夜间时段训练数据里必须包含夜间样本至少在总数据量里占20%以上。同时可以做简单图像增强比如在预处理阶段对暗区做直方图均衡化提升低照度下的对比度。如果摄像头本身支持红外模式优先用红外图像做训练和推理效果比任何算法提亮都稳定。雨雪天气同理收集对应天气的数据微调而不是指望一个通用模型吃遍四季。5.4 性能瓶颈定位的实用方法多路视频同时接入时帧率下降不一定是模型推理慢瓶颈常出现在视频解码和绘制环节。我的排查经验是分层打点单独测解码耗时、单独测检测耗时、单独测跟踪耗时、单独测可视化耗时看谁占比最大再动刀。实测中最常见的瓶颈反而在可视化绘制上——lavels数量多时每次循环绘制大段文字和矩形框的耗时能超过检测本身。解决方案是按需绘制线上推理时不绘制只输出数据需要可视化时用独立进程从队列里读取帧做绘制。另一个容易被忽视的点是内存拷贝OpenCV的BGR转RGB、numpy数组拷贝在大分辨率下耗时可观尽量复用同一块内存减少重复分配。做完这些优化后我的最终部署配置是YOLOv8m ByteTrack FP16 TensorRT单张中高端显卡稳定带动6路1080P摄像头每秒处理约25帧行人车辆计数误差控制在5%以内。这套配置和参数不是通用的黄金答案不同场景的摄像头角度、目标密度、光照条件都不一样但整个从架构设计到调参、排查的流程是完全可以复用的。最后再分享一个实际心得计数系统的精度评估一定要用人工复核的真实视频段来测指标别只看mAPmAP高不一定代表计数准确我最终验收都是拿真实路口的10分钟视频逐帧数人跑完对比绝对误差这个数字才是业务方真正关心的。