
简介面向交通管理、城市规划及毕业设计等场景的道路车流量检测系统基于Python与YOLOv8算法实现提供从车辆识别、跟踪计数到结果可视化的完整解决方案。系统附带训练好的YOLOv8m与YOLOv8n模型权重PT/ONNX格式及测试视频能够快速适配不同精度与速度需求在复杂交通环境中保持稳定检测效果。资源共307个文件包括126个Python源码、125个Pyc编译文件、37个YAML配置另有MP4测试视频、XML标注和JPG示例图等压缩包整体约155.1MB目录结构清晰便于按模块查阅。目前已有87人学习下载适合具备入门Python基础、希望快速落地YOLOv8目标检测项目的开发者使用。整套代码运行门槛低能直接用于道路车辆计数、交通流量统计等实验大幅缩短毕业设计或项目开发周期。1. 道路车流量检测系统先搞明白这套 YOLOV8 代码解决什么问题道路车流量检测系统说白了就是调用 Python 里的 YOLOV8 模型从路口、高架或停车场的监控视频里把汽车检测出来再通过跟踪和跨线判断统计出一段时间内的过车数量。标题里白纸黑字写了“完整代码以及训练好的模型数据都有可以直接使用”这意味着你不需要从几十万张图片的训练起步解压项目后用现成权重推一段视频马上就能看到按时间统计的车流曲线。这类系统的实际使用场景很具体交管部门要评估路口拥堵园区要统计出入车辆高校毕业设计要做一个能演示的目标检测项目。对你来说最划算的路径不是自己从零写一个 YOLO 训练流程而是先基于这套代码把“检测—跟踪—计数”链路跑通再按自己的摄像头画面校准参数。认清这一点后面做事才有方向。不过我要先说一句拿到项目第一件事不是双击 main.py而是把它当成一台没调好的机器先安装环境再验证模型最后才改逻辑。接下来我就按这个顺序带你把整个项目拆开。2. 跑通最小系统环境搭建、代码结构与模型加载2.1 环境怎么选Ubuntu 20.04 和 Windows 都行CPU 也能先跑YOLOV8 是 Ultralytics 开源库做的那套目标检测框架它把训练、推理、导出封装得很干净。你的系统里只要有 Python 3.9 或 3.10安装一个 ultralytics 包就能把训练好的模型加载起来。很多人上来就纠结 GPU其实验证阶段完全可以用 CPU 顶着跑。在 Ubuntu 20.04 搭建 YOLOV8 环境 cpu 版本我试下来最稳的办法是建虚拟环境后直接 pip 安装。# 创建虚拟环境避免污染系统 Python python3 -m venv traffic_env source traffic_env/bin/activate # 安装 YOLOV8 及其依赖torch 会默认拉 CPU 版 pip install ultralytics opencv-python numpy如果你用的是 Windows 且装了 Anaconda那就在 Anaconda Prompt 里执行conda create -n traffic python3.9 -y再激活后面的 pip 命令完全一样。这里值得说清楚为什么必须用虚拟环境YOLOV8 对 numpy、torch、opencv 的版本有搭配要求直接装到系统环境里很容易和现有的 TensorFlow 或旧版 opencv 打架到时候报错全是内部依赖冲突排查起来非常痛苦。如果你在 Ubuntu 20.04 上装的是系统自带 Python 3.8还是优先升级到 3.9 以上部分依赖在 3.8 下虽然能跑但容易出奇怪问题。如果你有 N 卡想用 GPU 加速装完上述依赖后可以手动重装一个 CUDA 版 PyTorch命令是pip install torch --index-url https://download.pytorch.org/whl/cu121具体 CUDA 版本要看你的驱动但验证阶段不装也完全能跑。网络慢的时候可以在 pip 后面加国内镜像源比如清华源的-i https://pypi.tuna.tsinghua.edu.cn/simple能省不少时间。装完用python -c import ultralytics; print(ultralytics.__version__)检查一下能输出版本号说明环境基本通了。2.2 项目代码结构与权重文件放哪里这种道路车流量检测项目目录结构一般不会太乱解压后常见的布局如下。虽然不同作者命名习惯有差异但核心元素就三块代码、权重、数据。road-traffic/ ├── main.py # 主程序一般负责读视频、计数、输出结果 ├── detect.py # 模型推理脚本可以做单张图和视频测试 ├── requirements.txt # 依赖清单 ├── weights/ │ └── best.pt # 训练好的 YOLOV8 模型 ├── datasets/ │ ├── images/ # 图片样本可能会分 train/val │ └── labels/ # 标注 txt 或 json ├── runs/ # 训练或推理输出目录 └── README.md # 作者写的说明拿到项目后先打开 README看作者有没有说明入口文件是哪个、视频路径在哪、结果输出到哪。不要迷信单个 best.pt我在实际项目里收到过一些权重文件名看着正常实际上类别顺序和代码里写的不一致跑出来全是莫名其妙的标签。你需要确认一件事权重文件是用 COCO 预训练模型直接做的迁移学习还是另外定义的自定义数据集。这个可以从 README 或者检测输出标签看出来如果标签只有 car、bus、truck说明是专门为车辆检测训练的。如果项目里没有给你测试图片只是给了训练好的模型你可以用系统自带的摄像头当数据源cap cv2.VideoCapture(0)就可以读一帧来做验证。但更建议先找一张清晰的路口照片放进 datasets 目录这样每次跑的结果可复现排查也方便。2.3 用一段最小代码验证模型能出框环境装好了文件也找到了别急着跑整个系统。先用最小代码加载权重对一张测试图做推理确认模型本身没坏。这个习惯能帮你把“模型问题”和“业务代码问题”隔离开。import cv2 from ultralytics import YOLO # 1. 加载模型权重 model YOLO(weights/best.pt) # 2. 读取一张测试图 frame cv2.imread(datasets/road/images/val/0001.jpg) # 3. 推理conf 是置信度阈值imgsz 是输入尺寸 results model.predict(frame, conf0.25, imgsz640, devicecpu) # 4. 把检测框画到原图上 for r in results: for box, conf, cls in zip(r.boxes.xyxy, r.boxes.conf, r.boxes.cls): x1, y1, x2, y2 [int(v.item()) for v in box] label r.names[int(cls.item())] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f{label} {float(conf):.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(output.jpg, frame) print(saved output.jpg)这段代码的逻辑是model.predict 返回一个 Results 列表每个元素对应一帧输入r.boxes.xyxy 是一组左上角和右下角坐标conf 是置信度cls 是类别编号。把坐标转成 int 是因为 cv2.rectangle 不支持浮点数坐标。最后用 cv2.imwrite 保存结果这一步很关键因为只有保存下来你才能肉眼确认模型到底有没有把车框对。参数上conf0.25 是通常比较合理的下限像夜间或小目标场景这个值可以降到 0.15imgsz640 表示把图像缩放成 640x640 送进网络常见取值范围是 480 到 1280尺寸越大越吃显存但小目标召回率会好一些。如果模型是 COCO 预训练权重你只想统计车辆可以加上classes[2, 5, 7]分别对应 car、bus、truck这样能减少行人和自行车误检对计数的影响。除了写脚本Ultralytics 也提供一行命令做同样的事yolo predict modelweights/best.pt sourcedatasets/road/images/val devicecpu这个命令行适合快速看效果但它不会输出结构化坐标所以正式做车流量统计还是要回到 Python 脚本里操作数据结构。3. 车流量统计的核心车辆检测、跟踪与计数逻辑3.1 检测之后为什么要跟踪单帧画框不等于流量统计把模型跑通后很多人会自然想到每帧把所有车框出来然后从第 1 帧数到最后一帧框的数量累加不就是车流量吗这是典型的翻车思路。视频是 25 帧每秒一辆车从出现到消失至少经过上百帧如果你按帧累加同一辆车会被重复记一百次。正确的做法是先做目标跟踪让每一辆车在连续帧里拥有固定编号然后再对这个编号去重计数。YOLOV8 不像传统 YOLO 只做检测Ultralytics 库里直接封装了跟踪器支持 ByteTrack 和 BoT-SORT。你在代码里不需要手动引入额外的包只需把 predict 换成 track并且加上 persistTrue它就会在连续视频帧里维持跟踪 ID。我一般会选 ByteTrack它在车辆相互遮挡、短暂离开画面再回来的场景下ID 切换比纯卡尔曼滤波方案少一些。from ultralytics import YOLO import cv2 model YOLO(weights/best.pt) cap cv2.VideoCapture(datasets/road/test/road.mp4) while True: ret, frame cap.read() if not ret: break # persistTrue 保证上一帧的 ID 延续到当前帧 results model.track(frame, persistTrue, conf0.3, iou0.5, trackerbytetrack.yaml, devicecpu) if results[0].boxes.id is not None: for box, track_id in zip(results[0].boxes.xyxy, results[0].boxes.id): x1, y1, x2, y2 [int(v.item()) for v in box] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, str(int(track_id.item())), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(track, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码里最关键的是 model.track 返回的 boxes.id这个 id 就是跟踪算法给同一辆车分配的终生编号。persistTrue 告诉算法不要把视频帧当作独立图片处理而是使用帧间关联信息。tracker 参数我通常写 bytetrack.yaml因为它在车辆遮挡场景下比较稳。iou0.5 是 NMS 的阈值太高会导致同一个物体重复出框太低又会漏掉重叠目标。一个需要留神的地方跟踪 ID 并不是 100% 稳定的。当两辆车并排行驶互相遮挡时检测框短暂消失重新出现后算法可能分配新 ID。这时候计数逻辑如果只看 ID 首次出现就会把同一辆车算两次。这也是为什么很多成熟方案会在画面里设置检测线结合“过线状态翻转”来计数。3.2 虚拟检测线中心点跨线计数怎么算有了跟踪 ID接下来要回答的问题是一辆车经过哪一刻算“通过了一次”最简单可靠的方法是画一条虚拟检测线。比如在画面第 300 像素高度画一条横线车辆框的中心点从线上方走到下方就算完成一次过线。这个办法实现成本低而且比“统计框首次出现”更能抵抗跟踪 ID 短暂丢失。line_y 300 # 检测线位置根据摄像头画面调节 prev_y {} # 记录每个 id 上一帧的中心点 y total_count 0 # 这是在视频循环内部要做的处理突出核心逻辑 for box, track_id in zip(results[0].boxes.xyxy, results[0].boxes.id): x1, y1, x2, y2 [float(v) for v in box] cx (x1 x2) / 2 cy (y1 y2) / 2 tid int(track_id.item()) if tid not in prev_y: prev_y[tid] cy continue # 上一帧在线上方当前帧在线下方向下过线 if prev_y[tid] line_y and cy line_y: total_count 1 print(f车 {tid} 通过当前总数 {total_count}) prev_y[tid] cy这里的核心是前后两帧比较而不是当前帧单点判断。如果只判断cy line_y同一辆车在跨线后可能连续几帧都满足条件会导致重复计数。用prev_y[tid] line_y and cy line_y相当于捕捉“从线上方到线下方”的那一次状态翻转正好对应车轮压线的瞬间。需要反向计数就把两个比较符号互换改成prev_y[tid] line_y and cy line_y。检测线位置也很有讲究。我不建议把线放得太靠近画面边缘因为跟踪算法在目标刚出现和快离开时 ID 容易抖动放在中间偏下一点等跟踪 ID 稳定了再计数最省心。如果你的场景是多车道就画多条线或者一条线配合左右车道分区判断。另外检测线最好在程序初始化时由人工用鼠标点击选定放到配置文件里这样换场景不用改代码。3.3 按时间聚合车流量写入 CSV 或 SQLite实时计数出来的是一个不断累加的数字但业务上真正需要的是“每分钟多少辆车”或“每小时多少辆车”。这一步不能靠一个计数器一直累加而是要把计数结果按时间窗口切分。import time import csv from collections import defaultdict # 用分钟作为 key统计每个分钟内发生的过线次数 minute_buckets defaultdict(int) current_minute int(time.time()) // 60 # 在视频主循环里每次判定过线时 # total_count 1 minute_buckets[int(time.time()) // 60] 1 # 每检测到新的一分钟就把上一分钟数据落盘 if int(time.time()) // 60 ! current_minute: with open(flow.csv, a, newline) as f: writer csv.writer(f) writer.writerow([current_minute, minute_buckets[current_minute]]) current_minute int(time.time()) // 60把“过线事件”和“统计窗口”分开是关键。minute_buckets 用默认字典存分钟级计数时间戳int(time.time()) // 60得到的是当前时间所属的分钟编号当这个编号变化时说明上一分钟已经结束可以将它写入 CSV。这样即使程序中途退出之前每个分钟的数据也已经持久化不会丢。在流量不大的场景分钟级数据已经能看出车流早晚高峰规律。如果要输出小时级报表可以在读取 CSV 后按小时做一次聚合这里不展开。需要提醒的是计数事件里的时间戳最好用视频时间戳或系统时间但如果你是对历史视频做二次分析就不能用 time.time()而应该用视频帧号除以帧率得到相对于视频开始的时间这一点经常有人踩坑。4. 避坑指南环境、路径、数据格式的 5 个常用排查点先说明一个前提下面这些坑不是我这个项目里独有的而是所有 YOLOV8 道路目标检测项目里翻车频率最高的几类。每一类我都按“现象 → 原因 → 解决”的顺序写你可以当成排查手册来用。4.1 没建虚拟环境报错全是依赖冲突现象你按教程把 ultralytics 装上了但运行from ultralytics import YOLO时报一堆ModuleNotFoundError甚至提示 numpy 版本不对或者 torch 编译错误。原因系统 Python 里早就有一套旧版本 opencv、numpyultralytics 需要更高版本pip 又没强制升级导致导入混乱。这种环境问题最玄学同样的代码换台机器就正常。解决删掉现在的环境从头建虚拟环境。我一般用python -m venv traffic_env再激活后重新安装依赖。装好后用pip list确认 ultralytics、torch、numpy 都出现在同一环境下再跑脚本。以后每次开新项目都建独立环境这是最系统的规避方式比你在报错里找补要快得多。4.2 Windows 中文路径导致 cv2 读不到图片现象项目放在D:\道路流量\数据目录下cv2.imread()返回 None但文件确实存在。改成英文路径就正常。原因OpenCV 的 imread 在 Windows 上对中文路径支持不好这是老问题跟图像本身无关。解决要么把项目整体挪到全英文路径下要么用cv2.imdecode读取文件字节。第二种方法更通用import cv2 import numpy as np def imread_unicode(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR) frame imread_unicode(D:/道路流量/数据/0001.jpg)注意路径里使用正斜杠/反斜杠在字符串里需要用双斜杠转义。写这段代码时尽量用pathlib.Path能少踩很多转义坑。这个函数可以直接替换项目里的所有 imread 调用不影响其他逻辑。4.3 模型加载成功但检测结果全为空现象代码不报错输出图像已经保存但上面一个框都没有或者只有零星几个框。原因最常见的是置信度阈值太高尤其在夜间、光照差、车辆较小的情况下模型输出的 confidence 普遍在 0.2 以下另一种可能是你的权重是自定义类别推理代码却在按 COCO 类别过滤 classes。解决先把 conf 降到 0.1 测试一轮如果还是没框再查类别过滤。我习惯在排查时把classes参数先注释掉只看原始模型输出。如果降阈值后还是空就换一张目标明显、光照正常的测试图排除图片本身问题。这类问题用排除法几分钟就能定位别一直调阈值那样只会越调越糊涂。4.4 视频计数跳帧车流量被重复计算现象跑出来的车流量数字每分钟几百辆明显不合理或者画面播放速度忽快忽慢计数逻辑时而漏掉。原因视频循环没有按帧率控制速度cap.read()读多快就处理多快导致同一辆车在跟踪上一帧还没稳定时就被计数。另一个原因是循环里既做推理又做画图又做保存处理耗时超过帧间隔后面的帧被跳过跟踪 ID 就乱了。解决读视频时先拿到帧率fps cap.get(cv2.CAP_PROP_FPS)每处理完一帧用time.sleep(1 / fps)控制节奏或者用更准确的时间戳差做等待。如果处理速度跟不上先别保存视频只画框和计数再把视频流输出打开会好很多。还有一种情况是视频源本身是 30 帧每秒但代码里按 25 帧写死了时间基单位时间统计对不上排查时可以打印帧号和真实时间戳做对比。4.5 labelme 转 YOLO 格式后框全部错位现象用一个开源脚本把 labelme 的 json 转成 yolo txt训练时 loss 不降把训练样本可视化一看框的位置完全不对。原因labelme 的坐标是左上和右下的绝对像素YOLO 需要的是中心点 x、中心点 y、宽、高四个值都必须除以图像宽高做归一化。很多人转格式时忘记归一化或者类别编号从 1 开始写而 YOLO 约定从 0 开始。解决转换脚本先检查三个点框中心公式是否是((x1x2)/2)/width宽高是否取绝对值类别编号是否从 0 开始。转换完成后写一个小脚本把 txt 画回原图比对这一步是后悔药花五分钟检查能省一整天训练时间。后面我会在训练章节把转换脚本完整贴出来你可以直接照着核对。5. 训练自己的车流检测模型数据准备、标注与参数调优5.1 数据准备从路口视频抽帧2000 张是合理起点系统自带的模型未必适配你的摄像头角度和光线。常见做法是拍一段 1 小时左右的道路视频均匀抽帧生成 2000 到 3000 张图片再按 8:2 或 9:1 划分训练和验证集。抽帧不要太密集连续帧之间内容高度相似会造成训练集严重冗余模型容易过拟合到特定位置。import cv2 import os os.makedirs(datasets/road/images/train, exist_okTrue) cap cv2.VideoCapture(road.mp4) fps cap.get(cv2.CAP_PROP_FPS) interval int(fps) # 每秒抽 1 帧一小时约出 3600 张可再抽稀 frame_id 0 save_idx 0 while True: ret, frame cap.read() if not ret: break if frame_id % interval 0: save_path fdatasets/road/images/train/{save_idx:06d}.jpg cv2.imwrite(save_path, frame) save_idx 1 frame_id 1 cap.release()这段代码用frame_id % interval控制抽帧间隔fps作为间隔基准保证每秒至少拿到一张避免时间分布不均匀。抽帧后还要人工删掉大量重复的静止画面比如红灯时车队停着不动连续几十帧画面几乎一样这些都会让数据集单调。最后按 train/val 分目录存放YOLO 训练时直接指向这两个目录不再需要额外做划分逻辑。如果你不想从零开始标也可以用公开的车辆检测数据集作为预训练补充但要注意摄像头视角和你的场景是否接近。俯拍视角和水平视角训练出来的模型在换场景后效果差别很大。最稳的做法还是抽自己的视频哪怕先标 1000 张也比直接用公开数据集更贴合现场。5.2 labelme 标注车辆再转成 YOLO 需要的 txt标注工具用 labelme 比较常见因为它输出 json方便二次处理。标注车辆时建议矩形框选“car”“bus”“truck”分开虽然外观有差异但不同车型的尺寸比例对训练有影响类别分清楚对统计也有好处。labelme 的 json 是相对复杂的嵌套结构训练前必须转成 YOLO 的纯文本格式。转换脚本要处理的核心字段就是 imageWidth、imageHeight 和 shapesimport json import os CLASSES [car, bus, truck] # 顺序就是 YOLO 类别编号 def convert_labelme(json_path, out_txt): with open(json_path, r, encodingutf-8) as f: data json.load(f) h data[imageHeight] w data[imageWidth] lines [] for shape in data[shapes]: if shape[shape_type] ! rectangle: continue label shape[label] if label not in CLASSES: continue cls_id CLASSES.index(label) (x1, y1), (x2, y2) shape[points] cx ((x1 x2) / 2) / w cy ((y1 y2) / 2) / h bw abs(x2 - x1) / w bh abs(y2 - y1) / h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) txt_name os.path.splitext(os.path.basename(json_path))[0] .txt with open(os.path.join(out_txt, txt_name), w) as f: f.write(\n.join(lines))这段脚本有几个关键点。第一shape_type必须是 rectanglelabelme 里也可以画多边形很多车辆标着标着就画成了不规则多边形转换时容易漏掉。第二类别顺序 CLASSES 一旦确定就不能随便改除非你打算重新训练并保持顺序。第三输出坐标必须是浮点小数宽高用绝对值防止 x2 比 x1 小导致负值。转完后强烈建议用“回画脚本”把 txt 内容画到原图上框位置正确再进入下一步别等训练完再回头查数据。回画脚本的写法其实就是读取 txt 后把中心点坐标和宽高换算回左上角、右下角再画矩形这个流程跑一遍基本能发现所有格式问题。5.3 训练命令与三个必调参数数据准备好了就该 yolov8 训练自己的数据集了。YOLOV8 训练不需要写一堆训练循环Ultralytics 库用一句命令就能启动yolo detect train \ datadatasets/road/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ device0data.yaml 的内容通常长这样train: datasets/road/images/train val: datasets/road/images/val nc: 3 names: [car, bus, truck]这里我认为最值得调的是三个参数epochs、batch、imgsz。epochs 新手别贪多100 左右足够看出趋势如果验证集 loss 还在明显下降就继续训练。batch 根据显存来8GB 以下显存建议 batch8imgsz 保持 640显存 16GB 以上可以 batch16imgsz640 甚至涨到 800。这两个参数决定训练速度和显存占用不直接决定精度。另一个容易忽略的是 model 选择yolov8n 最快但精度低yolov8m 精度高但显存大道路车流量场景通常 yolov8s 就是性价比甜点。更深的网络结构改进可以先放一放能复现一个高精度的基线比什么天花乱坠的改进都重要。训练过程中退出控制台会自动保存模型下次恢复训练可以用resumeTrue。如果显存不够reduce batch 而不是降低 imgsz因为图像尺寸明显影响检测小目标的性能。5.4 用损失函数曲线判断训练状态别惦记着动不动就改结构训练跑完后Ultralytics 会在 runs/detect/train 目录下生成 results.csv里面记录了每一轮的训练损失、验证损失、mAP 等指标。很多人觉得深度学习是黑匣子其实这张表能直接告诉你模型到底有没有在学。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/train/results.csv) plt.plot(df[epoch], df[train/box_loss], labeltrain_box_loss) plt.plot(df[epoch], df[val/box_loss], labelval_box_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.savefig(loss_curve.png)画出来的曲线如果训练 loss 和验证 loss 同步下降而且最终都趋于平缓说明训练状态健康如果训练 loss 一路下降验证 loss 开始回升就是过拟合了解决办法是增加数据、增大数据增强强度或增加 dropout而不是继续增加 epoch。如果两者都保持在高位不降先检查标注文件是否错位、类别比例是否失衡比如只有小汽车没有公交车模型很难把公交车学好。这张 loss 曲线图是判断训练状态最直接的依据也是后续训练版本迭代的体检报告。我会把它和 best.pt 放在一起下次调参或者换数据时先看曲线方便对比。6. 进阶导出 ONNX 模型在 RK3588 部署并保留可视结果6.1 从 PyTorch 导出 ONNX为边缘设备部署做准备训练好的 best.pt 可以直接在 PC 上跑但如果要部署到 RK3588 这类带 NPU 的开发板上通常需要先把权重导出成 ONNX再用 RKNN 工具链转换成 rknn 格式。导出命令很简单yolo export modelweights/best.pt formatonnx imgsz640导出后你会得到一个 best.onnx可以用 onnxruntime 在 CPU 上做推理测试确认精度和原模型基本一致后再做后端转换。需要注意导出时指定和推理一致的 imgsz如果训练时用 640导出也保持 640尺寸不匹配会导致部署后 mAP 下降。ONNX 里的输出节点很多部署脚本要根据模型结构取框信息这一步建议直接参考 ultralytics 的导出范本比自己从零解析省事得多。6.2 一个值得养成的习惯把检测结果可视化保存最后我想说一个我自己的血泪教训。做车流量统计时如果只在控制台打印计数数字出现问题根本没法复盘比如某监控时段少算了一百辆车没有带框的视频你连哪条规则有问题都查不到。所以我建议所有跑过的视频都出一份带时间戳、带跟踪编号、带计数结果的输出视频哪怕帧率只有原来的一半也比纯数字强。一种简单的做法是在原来画框的基础上把当前 total_count 和时间戳用 putText 写到画面右上角再通过 cv2.VideoWriter 写文件。这样统计结果和过程是绑定的复查时直接定位到对应时间点。当你需要向别人展示系统效果时这份视频本身就是最好的交付物。希望这个习惯能帮你在车流量检测这条路上少走点弯路。本文还有配套的精品资源点击获取