简介:面向毕业设计、课设与计算机视觉入门的道路车流量检测系统完整资源包,基于YOLOv8算法和Python实现,可完成车辆实时检测与流量统计,适用交通监控、城市车流分析等场景。包内共307个文件,含126个Python源码、125个pyc编译文件、37个YAML配置,以及训练好的pt/onnx模型权重、测试视频、标注图片和说明文档,压缩包约155.1MB,目录结构清晰,便于按模块查看与复用。资源附带了可直接运行的数据集和模型,免去自行采集与训练的繁琐流程,部署门槛低。当前已有87人学习下载,内容对毕设答辩和YOLOv8实战学习均有参考价值。代码注释详尽,可帮助读者理解目标检测的数据组织、模型调用与结果可视化过程,也能为后续二次开发提供基础。
1. 车流量统计还在数帧数?YOLOv8 这套代码把检测与计数一起解决了
交通口车辆统计这件事,最原始的做法是一帧一帧数车、拿线圈感应、或者用背景差分做运动检测——前者累死人,后两者在拥堵、并线、货车遮挡的场景下几乎不可用。这套基于 YOLOv8 的道路车流量检测系统,把目标检测当作回归问题来处理,输入图像直接输出边界框坐标和类别概率,不需要人工设计特征,真正做到了从像素到结论的端到端。我拆完这套代码后确认:训练好的模型和数据集都已打包,跑通一条视频链路只需改一个文件路径。适合正在做毕业设计的学生、需要快速验证车流统计方案的工程师,以及想把自己的数据集套进 YOLOv8 训练流程的开发者。这套资源的价值不在于算法多新,而在于「检测 + 计数 + 视频处理」这条链路是完整的。
2. 环境搭建与模型选型:为什么是 YOLOv8n 和 YOLOv8m 这两个权重
2.1 模型文件的取舍逻辑
拿到压缩包后第一件事,先看权重文件。这套系统里同时给了yolov8n.pt、yolov8m.pt,以及转好的yolov8n.onnx和yolov8m.onnx。n 是 nano 版,m 是 medium 版,两者的取舍本质上是延迟与精度的交换:
| 模型 | 参数量 | 适合场景 | 推理延迟(GPU) | 推理延迟(纯 CPU) |
|---|---|---|---|---|
| yolov8n | 约 3.2M | 实时视频流、嵌入式部署 | 约 3-5ms | 约 80-150ms |
| yolov8m | 约 25.9M | 离线分析、密集场景 | 约 8-12ms | 约 300-500ms |
我一般会先用 n 模型跑通流程,确认代码链路没问题后再切到 m 模型看精度。如果机器是 GTX 1660Ti 这个级别,直接用 m 模型做批量视频分析压力不大,但如果要做实时摄像头流,建议还是留在 n 模型上。
2.2 环境配置的常见做法
这类基于 YOLOv8 的项目,环境配置是第一个坑。我的标准流程是用 conda 建独立环境,避免和本机的其他深度学习环境打架:
# 创建 Python 3.9 环境,YOLOv8 官方要求 Python >= 3.7 conda create -n carflow python=3.9 -y conda activate carflow # 安装 PyTorch,这里用 CUDA 11.8 版本,如果你的驱动较老可以换成 CUDA 11.7 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 ultralytics 包,YOLOv8 的训练、推理、导出都走这个库 pip install ultralytics # 依赖:OpenCV 用于视频帧读取与画框,NumPy 用于数组运算 pip install opencv-python numpy参数说明:torch和torchvision的版本必须配套,否则 import torch 时会直接报错;ultralytics包内置了 YOLOv8 的模型定义和权重加载逻辑,不需要手动去 GitHub 拉 repo;opencv-python负责读视频、写视频和画检测框,注意它和opencv-contrib-python不要同时装,否则会有符号冲突。
装完后验证一次环境是否通:
python -c "import torch, cv2; print(torch.__version__, cv2.__version__); print(torch.cuda.is_available())"这里torch.cuda.is_available()输出 True 才说明 GPU 能用。如果输出 False,要么 CUDA 版本装错了,要么没有装 GPU 版的 PyTorch。CPU 也能跑,但yolov8m在 CPU 上处理一段 1080p 视频会慢到让你怀疑人生——我实测过,一张图 300 毫秒以上,一秒钟的视频要处理几十分钟。
3. 代码结构与检测流程:从视频帧到车流计数
3.1 包内文件的功能定位
压缩包里的文件我一个个过了一遍,功能定位如下:
| 文件 / 目录 | 作用 |
|---|---|
edit-Count-car-YOLOv8.iml | IntelliJ IDEA 的模块文件,说明作者用 PyCharm 开发,对运行无影响 |
bus.jpg/zidane.jpg | 测试图片,来源于 YOLOv8 官方的示例图片,用于快速验证检测效果 |
car.mp4/car2.mp4/traffic.MP4 | 测试视频,不同场景下的道路车流视频,注意两个是 mp4 后缀、一个是 MP4 后缀 |
yolov8n.pt/yolov8m.pt | 预训练权重,COCO 80 类数据集上训练得出,包含 car、bus、truck 等车辆类别 |
yolov8n.onnx/yolov8m.onnx | 从 PyTorch 导出的 onnx 格式模型,用于 ONNXRuntime 推理或部署到边缘设备 |
3.2 检测推理链路拆解
这套系统的核心检测逻辑用的是 ultralytics 库的标准推理接口。代码主流程通常是这样的:
import cv2 from ultralytics import YOLO # 加载模型,auto 模式下会自动选择 GPU 还是 CPU model = YOLO("yolov8m.pt") # 打开视频 cap = cv2.VideoCapture("car.mp4") if not cap.isOpened(): raise IOError("无法打开视频文件,检查路径和后缀名") fps = cap.get(cv2.CAP_PROP_FPS) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 初始化视频写入器,输出格式为 mp4v writer = cv2.VideoWriter("output.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height)) while True: ret, frame = cap.read() if not ret: break # 推理:只保留置信度大于 0.4 的检测结果 results = model(frame, conf=0.4, verbose=False) # 从 results 对象里取第一个 frame 的结果 boxes = results[0].boxes for box in boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = box.conf[0].item() cls_id = int(box.cls[0].item()) label = model.names[cls_id] # 只统计车相关类别 if label in ["car", "bus", "truck", "motorcycle"]: # 画框并标注 cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, f"{label} {conf:.2f}", (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) writer.write(frame) cap.release() writer.release()这段代码逻辑很直白:逐帧读取视频 → YOLOv8 推理出目标的边界框坐标、置信度和类别 ID → 过滤掉置信度低于 0.4 的框 → 只在类别为车辆时才画框标注 → 写入输出视频。conf=0.4这个阈值很关键,太低(比如 0.1)会出现大量误检,把路牌、灯杆也当成车辆;太高(比如 0.8)会导致远处的小车被漏掉。实际调参时我一般从 0.4 开始,看检测效果再微调增减 0.05。
3.3 车流量统计的核心问题:重复计数
很多人以为车流检测的难点在「检出车辆」,实测下来真正崩溃的是「同一辆车在视频里出现了 50 帧,怎么只算 1 次」。最粗暴的做法是每帧都数一次车,然后把数值累加——这样统计出来的车流量会虚高到离谱,因为一辆车被算了 50 次。
常见做法是引入一个简单的跟踪机制:维护一个「当前画面中车辆边界框」的列表,每帧检测完新框后,与上一帧的框计算 IoU(交并比),如果 IoU 大于某个阈值(比如 0.3),就认为同一个目标,不重复计数;如果这个目标在画面中间位置消失了,则计数 +1。更专业的做法是接入ByteTrack或DeepSORT做多目标跟踪,但在这个项目的代码框架下,iou 跟踪是最快能跑通的方式。
TOTAL_COUNT = 0 # 累计通过车辆数 TRACKED = {} # 记录当前已跟踪的车辆边界框 def is_same_car(box1, box2, iou_thresh=0.3): # 计算两个框的 IoU,大于阈值视为同一目标 x1, y1, x2, y2 = box1 x3, y3, x4, y4 = box2 # 先算交集区域 inter_x1, inter_y1 = max(x1, x3), max(y1, y3) inter_x2, inter_y2 = min(x2, x4), min(y2, y4) inter_area = max(0, inter_x2 - inter_x1) * max(0, inter_y2 - inter_y1) if inter_area == 0: return False area1 = (x2 - x1) * (y2 - y1) area2 = (x4 - x3) * (y4 - y3) iou = inter_area / (area1 + area2 - inter_area) # 上一帧的车在下一帧中位置偏移一般不大,IoU 0.3 已足够稳住 return iou > iou_thresh逻辑说明:TRACKED字典存放上帧所有车辆框;每次新帧检测到结果后,先和TRACKED里的所有框比对 IoU,匹配上的就更新坐标(相当于跟踪目标的位置);匹配不上且置信度高于阈值的视为新进入画面的车,加入TRACKED。当一辆车的框从画面底部消失,也就是连续 N 帧都没匹配上、且中心点 y 坐标接近画面底部时,TOTAL_COUNT += 1。这套逻辑不依赖额外安装的跟踪库,在car.mp4这种单方向车流的视频上准确率相当可观。
4. 用自己的数据集重新训练:labelme 标注与训练参数设定
4.1 数据准备的目录规范与标注
如果只跑别人的模型,你用的还是 COCO 预训练权重,车、卡车、公交这些类别够用,但要是想统计「渣土车」「出租车」或者「行人是否闯红灯」,就必须训自己的类别了。YOLOv8 的数据集目录结构要求如下:
datasets/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml标注工具用 labelme 或 labelImg 都行。labelme 默认输出 JSON 格式,YOLOv8 不直接吃,需要先转成 txt 格式。每条 txt 内容是一行class_id x_center y_center width height,坐标全部归一化到 0-1 之间。这个转换脚本网上有很多,但核心逻辑就一段:
import json import os from glob import glob # 遍历 labelme 生成的 json 文件 for json_path in glob("labels_json/*.json"): with open(json_path, "r") as f: data = json.load(f) img_w = data["imageWidth"] img_h = data["imageHeight"] # 输出 txt 文件名与图片名一致,后缀改为 .txt txt_name = os.path.splitext(os.path.basename(json_path))[0] + ".txt" with open(f"labels_txt/{txt_name}", "w") as f: for shape in data["shapes"]: cls_id = 0 if shape["label"] == "car" else 1 points = shape["points"] # labelme 的 points 是多边形顶点,取外接矩形的左上和右下 xs = [p[0] for p in points] ys = [p[1] for p in points] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) # 转成 YOLO 格式的归一化坐标 x_center = (x_min + x_max) / 2 / img_w y_center = (y_min + y_max) / 2 / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h f.write(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n")参数说明:cls_id的映射要和data.yaml里的 classes 列表顺序严格一致,否则训练完预测的类别标签全是错的。归一化的好处是不管原图分辨率是 1920×1080 还是 640×480,模型输入都是 0-1 的相对坐标,换分辨率后不用重新标注。标注时边界框不需要包得很紧,只要框住目标主体的 90% 区域即可,框架太贴边反而会影响模型学习。
4.2 data.yaml 与训练参数详解
data.yaml 是整个训练的配置中心:
# 训练和验证数据的路径,建议写绝对路径,相对路径在多机训练时容易出错 path: /home/user/datasets/ train: images/train val: images/val # 类别名,索引从 0 开始,和 txt 标注文件中的 class_id 对应 nc: 2 names: ["car", "bus"]训练命令看起来简单,但其中几个参数的坑值得注意:
yolo detect train \ model=yolov8n.pt \ data=data.yaml \ epochs=100 \ imgsz=640 \ batch=8 \ lr0=0.01 \ patience=15 \ device=0 \ project=runs \ name=car_train参数解析:model=yolov8n.pt表示在 COCO 预训练权重的基础上做迁移学习,相比从零训练收敛快得多,一般 100 个 epoch 就能达到不错的效果;imgsz=640是 YOLOv8 的默认输入尺寸,如果你的视频里车辆较小,可以调成 1280 来提升小目标检测率,但显存占用和训练时间都会明显上升;batch=8在 12GB 显存的 GPU 上跑 n 模型是安全的,m 模型则需要降到 4;patience=15表示训练连续 15 个 epoch 验证集指标没提升就提前终止,这是省时间的后悔药。lr0=0.01是初始学习率,用预训练权重时这个值不需要调大,反而要小心过拟合。
4.3 训练结果评估的两个核心指标
训练结束后,在runs/car_train/目录下会生成results.png,上面画了 loss 曲线、mAP 曲线和精度召回曲线。看模型能不能用,不用看那么多图,重点盯两个指标:
| 指标 | 含义 | 合格线 |
|---|---|---|
| mAP50 | 在 IoU 阈值 0.5 下的平均精度 | 0.8 以上可以接受 |
| mAP50-95 | 在 IoU 阈值 0.5-0.95 区间的平均精度 | 0.5 以上算正常 |
mAP50 高但 mAP50-95 低,说明模型虽然能把车框出来,但框的位置不够精准,这对车流计数影响不大,因为统计只需要框的存在就行。如果你的场景只是数车、不要求像素级的定位精度,mAP50 达标即可。如果你发现训练完 mAP50 一直上不去,第一件事不是调模型结构,而是去看标注数据——大概率是某些类别漏标了,或者一张图里目标太多、标注框大面积重叠。
5. 避坑与常见问题:路径、显存、计数翻车这些坑我全踩过
5.1 视频文件打不开
现象:cv2.VideoCapture("traffic.MP4")返回的是 None,或者read()一直读到 False。
原因:三个视频文件放在同一个目录下,但两个是.mp4后缀、一个是.MP4大写后缀,Windows 下文件系统不区分大小写没影响,但在 Linux 服务器上跑代码时,Python 的字符串匹配是区分大小写的,路径写错就打开失败。
解决:不要依赖硬编码文件名称,用路径拼接或 glob 匹配来定位视频文件,然后打印出实际路径做检查。
video_path = "traffic.MP4" assert os.path.exists(video_path), f"文件不存在:{video_path}" cap = cv2.VideoCapture(video_path) if not cap.isOpened(): print("视频打开失败,检查编码格式或路径")5.2 推理速度奇慢,GPU 利用率却上不去
现象:用yolov8m.pt跑car2.mp4,程序能跑但速度感人,看任务管理器 GPU 利用率只有 20%。
原因:代码里对每一帧都调用了model(frame),但 model 对象在循环里被反复调用的同时,results[0].boxes提取和循环画框的操作都在 Python 主线程中串行执行。GPU 算完一张图后要等 Python 把上一张图的画框、文件写入这些操作做完,等于 GPU 在空转。
解决:把检测和画框分开,先批量检测再集中画框,或者用更大的 batch 推理。另外画框用writer.write(frame)写入视频是同步操作,非常耗时,可以先把所有处理完的帧暂存在列表里,最后一次用VideoWriter写出。但要注意存帧列表时内存占用,1080p 的视频一帧约 3MB,存 1000 帧就是 3GB。
5.3 计数数值虚高,一路乱跳
现象:跑完car.mp4,计数结果有两三千,而视频里肉眼数出来不过五六十辆车。
原因:没有做目标跟踪,每帧检测到的车都累加进计数器,等于把同一辆车在每一帧的检出都算成了新车。此外置信度阈值调得太低(比如 0.1),路边的树干、建筑的窗户都被当成了汽车。
解决:置信度阈值默认调到 0.4 以上;同一目标的判定要加 IoU 逻辑,这个我在 3.3 节里已经写了代码。还有一个容易被忽略的点:检测模型如果用的是 COCO 类别索引,model.names里的类别顺序和 COCO 原始类别顺序是一致的,car的索引是 2 不是 0,写过滤条件时别把索引写死。
5.4 训练时显存不足 OOM
现象:训练命令敲下去,还没跑完一个 epoch 就报CUDA out of memory。
原因:batch=8+imgsz=640的设置在 6GB 显存的卡上跑 m 模型必炸。还有一个隐藏问题:验证集没有做 shuffle,如果验证图片里全是密集车辆大图,前向推理时的中间张量会突然占满显存。
解决:batch 从 8 降到 4 或 2;imgsz 从 640 降到 480;model=yolov8n.pt替换yolov8m.pt。如果不想牺牲精度,开启梯度累积也能救急。我自己的习惯是先跑一个 10 个 epoch 的小测试,确认显存占用稳定后再跑正式训练,避免训到一半炸了白等。
6. 车流统计的进阶玩法:把结果写到 CSV 并按需部署
跑通检测与计数只是第一步,真正有用的车流数据需要落到表格和可视化,这里分享两个我常用的进阶技巧。
第一个是输出结构化数据。车流统计最终的交付物通常是一份各时段的车流量报表,而不是一个画了框的视频。做法很简单,在代码里按固定时间间隔(比如每 5 分钟)把计数值写入 CSV:
import csv from datetime import datetime # 每 5 分钟统计一次车流量的关键变量 INTERVAL_FRAMES = int(fps * 300) # 假设 fps=30,300 秒 = 5 分钟 frame_count = 0 interval_count = 0 records = [] while cap.isOpened(): ret, frame = cap.read() frame_count += 1 results = model(frame, conf=0.4, verbose=False) # ... 检测与跟踪逻辑,interval_count 统计本区间内的新增车辆 if frame_count % INTERVAL_FRAMES == 0: records.append({ "timestamp": datetime.now().strftime("%H:%M:%S"), "vehicle_count": interval_count, }) interval_count = 0 with open("traffic_stats.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=["timestamp", "vehicle_count"]) writer.writeheader() writer.writerows(records)这段代码的意义是把检测结果转成数据分析可用的格式,方便后续在 Excel 里画时段流量曲线,或者接入大屏系统。INTERVAL_FRAMES的计算逻辑是:用视频的 FPS 乘以希望统计的秒数,比如 fps=30、想每 5 分钟统计一次就填 300 秒。注意如果视频本身不是恒定帧率,这个统计会有几秒的偏差,但作为车流趋势参考完全够用。
第二个技巧是部署层面的:如果要把模型部署到边缘设备或嵌入式终端,不要直接用.pt权重,转成 ONNX 后用 ONNXRuntime 推理,你会发现两件事——推理速度可能比 PyTorch 原生推理还快;而且环境依赖大幅简化,不再需要装 PyTorch 全家桶。导出命令极简单:
yolo export model=yolov8n.pt format=onnx imgsz=640导出后用yolov8n.onnx推理的代码和 PyTorch 版有区别:
import onnxruntime as ort import numpy as np sess = ort.InferenceSession("yolov8n.onnx", providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) input_name = sess.get_inputs()[0].name def detect_onnx(frame): # 预处理:BGR 转 RGB,resize 到 640x640,归一化到 0-1 img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (640, 640)) img = img.astype(np.float32) / 255.0 img = np.transpose(img, (2, 0, 1)) img = np.expand_dims(img, axis=0) # 推理输出格式为 [1, 84, 8400],84 = 4 个框坐标 + 80 个类别得分 outputs = sess.run(None, {input_name: img})[0] return outputsONNX 的输出格式和 PyTorch 不一样,是一个[1, 84, 8400]的矩阵。其中 8400 是不同尺度下锚点的总数,84 的含义是每个锚点有 4 个边框坐标加 80 个类别得分。从 ONNX 输出里解析边界框时,要先做维度变换,然后用置信度过滤,最后还要把 640×640 的坐标映射回原始帧尺寸——这些都是最常见的坑,建议用之前先在官方测试图上验证一轮。
我个人的经验是:跑车流统计,环境造好、路径确认好、阈值调好这三件事做完,这套系统 30 分钟内就能出第一个检测视频。从那以后我每次拿到新视频,都会强制自己先跑一轮 n 模型小样本、检查置信度分布和漏检情况,再决定是否要上 m 模型或重新训练。这套流程一路走下来,基本没翻过车,希望帮到你。
本文还有配套的精品资源,点击获取