简介:这份资源围绕Python与OpenCV实现视频车辆测速与速度检测,面向具备一定图像处理基础、希望把计算机视觉落地到交通场景的开发者与学习者。它解决的是从视频流中识别车辆、跟踪目标并估算行驶速度的完整实现问题,适合课程设计、项目练手或算法验证等场景。压缩包共10个文件,约64.69MB,包含mp4与avi演示视频、gif效果预览、py测速脚本、xml车辆检测模型、txt依赖清单及md说明文档,覆盖从运行环境到检测效果的完整链路。目前已有2787人学习下载,说明该方案在同类资源中具备一定参考价值。读者可据此理解视频读取、目标检测、帧间位移与速度换算的实现思路,并借助示例视频与脚本快速复现测速效果,为后续调参与功能扩展提供可运行的基础。
1. 用 Python 和 OpenCV 做视频车辆测速:一份能跑通的源码包拆解
路上跑的每一辆车都有一个瞬时速度,但要从一段普通监控视频里把它算出来,靠的不是雷达,而是像素和帧率之间的换算关系。这份「车辆测速」资源包给的就是这样一套东西:speed_check.py是主脚本,myhaar.xml是车辆检测用的级联分类器,cars.mp4、1.mp4、2.mp4是测试素材,requirements.txt锁依赖,跑完输出output.mp4、outpy.avi和output.gif。它解决的是「给定一段固定机位视频,估算画面里车辆速度」这个具体问题,适合已经会装 Python、想拿 OpenCV 练手目标检测与轨迹跟踪的从业者。整套逻辑不依赖深度学习权重,纯传统视觉,CPU 就能跑,这也是它值得拆的原因——门槛低,但坑一点不少。
2. 测速原理与源码结构:像素位移怎么变成 km/h
2.1 从帧差到速度:三个必须钉死的量
车辆测速的本质是一道换算题。视频是离散的帧序列,每帧之间车辆在画面上移动了一段像素距离,只要知道「每像素对应现实多少米」和「每秒有多少帧」,速度就能算出来。公式不复杂:
速度(m/s) = 像素位移 × 米每像素 / 帧间隔时间(s) 速度(km/h) = 速度(m/s) × 3.6真正难的是这三个量怎么定。帧率从视频元数据里读,OpenCV 的cap.get(cv2.CAP_PROP_FPS)直接给;帧间隔就是它的倒数。米每像素这个比例尺,是整份代码里最玄学的地方——它取决于相机高度、俯仰角、焦距,没有标定物就只能估。常见做法是在画面里找一条已知长度的参照,比如标准车道线宽度(国内一般 3.5 米左右)或者一辆已知车长的车,量出它在画面里占多少像素,反推比例尺。
这份资源包用的是固定机位视频,所以比例尺可以当成常数写死。speed_check.py里通常会有一个类似PIXELS_PER_METER或scale的变量,改它就能适配不同机位。如果你换了自己的视频,第一件事就是重新标这个值,否则算出来的速度会整体偏大或偏小,而且偏得很有规律——这正是新手最容易忽略的地方。
2.2 资源包文件逐个说清用途
把压缩包解开,文件不多,但每个都有明确分工。先看清单:
| 文件 | 作用 | 备注 |
|---|---|---|
speed_check.py | 主程序,检测+跟踪+测速 | 核心,需重点读 |
myhaar.xml | Haar 级联分类器,用于车辆检测 | 传统方法,非深度学习 |
cars.mp4/1.mp4/2.mp4 | 测试视频素材 | 固定机位,用于验证 |
requirements.txt | 依赖清单 | 主要是 opencv-python、numpy |
output.mp4/outpy.avi | 输出视频 | 带检测框和速度标注 |
output.gif | 输出动图 | 方便快速预览效果 |
README.md | 说明文档 | 记录运行方式和参数 |
myhaar.xml是这份资源的检测核心。Haar 级联是 OpenCV 里最经典的目标检测方案,靠积分图加速特征计算,用 AdaBoost 训练出一串弱分类器级联。它的优点是快、CPU 友好,缺点是召回率和精度都一般,对车辆这种形变大的目标尤其吃力。所以这份代码的检测框会抖,跟踪必须补位——这也是后面避坑章节要重点讲的。
2.3 主流程拆解:读帧、检测、跟踪、算速、写帧
speed_check.py的骨架基本是固定的五步循环。下面是我按这类项目常见写法还原的核心结构,你可以对照自己的源码看:
import cv2 import numpy as np # 1. 打开视频,读取帧率和尺寸 cap = cv2.VideoCapture("cars.mp4") fps = cap.get(cv2.CAP_PROP_FPS) frame_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) frame_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 2. 加载 Haar 分类器 car_cascade = cv2.CascadeClassifier("myhaar.xml") # 3. 输出视频写入器,编码用 mp4v fourcc = cv2.VideoWriter_fourcc(*"mp4v") out = cv2.VideoWriter("output.mp4", fourcc, fps, (frame_w, frame_h)) # 4. 比例尺:每像素对应多少米,必须按机位标定 PIXELS_PER_METER = 8.0 # 示例值,需按实际画面调整 # 5. 逐帧处理 prev_centers = {} # 记录上一帧各目标的中心点 while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) cars = car_cascade.detectMultiScale(gray, 1.1, 3) for (x, y, w, h) in cars: cx, cy = x + w // 2, y + h // 2 cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) # 与上一帧最近的中心点匹配,算像素位移 # 位移 / PIXELS_PER_METER / (1/fps) * 3.6 = km/h cv2.putText(frame, f"{cx},{cy}", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) out.write(frame) cap.release() out.release()这段代码里几个参数直接决定结果好坏。detectMultiScale的scaleFactor=1.1控制每次图像缩小的比例,越小检测越细但越慢;minNeighbors=3控制一个候选框要被多少个邻居确认才算数,调大能压误检但会漏检。PIXELS_PER_METER是标定值,前面说过必须按机位改。速度计算那行是整份代码的灵魂,把像素位移除以比例尺得到米,再除以帧间隔得到米每秒,乘 3.6 换成 km/h。
提示:Haar 检测出来的框每帧位置都会跳,直接用相邻两帧中心点算位移,速度会剧烈抖动。合格的做法是给每个目标加一个跟踪器(如
cv2.legacy.TrackerCSRT或简单的质心跟踪),用跟踪轨迹平滑位移,再算速度。
2.4 依赖安装与环境确认
requirements.txt里通常就两三个包,但版本对不上会直接报错。装之前先确认 Python 版本,3.8 到 3.10 最稳,太新的版本某些 OpenCV 老接口会变。
# 建议先建虚拟环境,避免污染全局 python -m venv venv # Windows 激活 venv\Scripts\activate # Linux / macOS 激活 source venv/bin/activate # 安装依赖 pip install -r requirements.txt # 如果 requirements 里没锁版本,手动装这两个 pip install opencv-python numpy装完验证一下 OpenCV 能不能正常导入,以及 Haar 分类器文件路径对不对:
import cv2 print(cv2.__version__) # 确认分类器能加载,返回非空即正常 cascade = cv2.CascadeClassifier("myhaar.xml") print(cascade.empty()) # 应为 False如果cascade.empty()返回True,说明myhaar.xml路径错了或者文件损坏,检测会一个框都不出,但程序不报错——这是最隐蔽的翻车点之一。运行主程序:
python speed_check.py跑完看output.mp4,如果画面里有绿框跟着车走,说明检测通了;如果框在抖或者速度数字乱跳,问题在跟踪和比例尺,往下看避坑章节。
3. 参数调优与速度标定:让数字接近真实值
3.1 比例尺标定的两种可操作方法
速度准不准,九成取决于比例尺。第一种方法是找参照物:在视频画面里找一条车道线,量出它在图像里横跨多少像素。假设车道线实际宽 3.5 米,在画面里占 28 像素,那PIXELS_PER_METER = 28 / 3.5 = 8。第二种方法是用已知车长的车:普通轿车约 4.6 米,量出它在画面里占多少像素,同样反推。
# 标定辅助:在画面上画参考线,量像素长度 import cv2 img = cv2.imread("frame.jpg") # 假设你量出车道线从 x=100 到 x=380,共 280 像素 lane_pixels = 380 - 100 lane_meters = 3.5 PIXELS_PER_METER = lane_pixels / lane_meters print(f"比例尺: {PIXELS_PER_METER:.2f} 像素/米")标定完把值填回speed_check.py。注意,如果相机有俯角,画面不同位置的像素-米比例其实不一样,近处大远处小。固定机位下取画面中段的值做近似,误差可以接受;要更准就得分区域标定,那是另一个工作量了。
3.2 检测参数与跟踪平滑的配合
Haar 检测的参数和跟踪策略要一起调。检测太灵敏,框乱跳,跟踪跟不上;检测太保守,车漏掉,轨迹断。我一般先把minNeighbors从 3 调到 5 压误检,再看漏检情况决定要不要降回来。
# 检测参数调优示例 cars = car_cascade.detectMultiScale( gray, scaleFactor=1.1, # 1.05~1.3,越小越慢越细 minNeighbors=5, # 3~6,越大误检越少但可能漏检 minSize=(40, 40), # 过滤太小的框,减少噪声 maxSize=(300, 300) # 过滤过大的框,避免整屏误检 )跟踪平滑用质心跟踪最简单:给每个检测框分配一个 ID,记录它的历史中心点,用最近几帧的平均位移代替单帧位移。这样速度曲线会平滑很多,不会一帧一个数。
from collections import deque # 每个目标保留最近 5 帧中心点,算平均位移 history = {} # id -> deque of (cx, cy) SMOOTH_N = 5 def update_speed(track_id, cx, cy, fps, ppm): if track_id not in history: history[track_id] = deque(maxlen=SMOOTH_N) history[track_id].append((cx, cy)) if len(history[track_id]) < 2: return 0.0 (x0, y0), (x1, y1) = history[track_id][0], history[track_id][-1] pixel_dist = ((x1 - x0) ** 2 + (y1 - y0) ** 2) ** 0.5 frames = len(history[track_id]) - 1 meters = pixel_dist / ppm seconds = frames / fps return meters / seconds * 3.6这段逻辑的关键是SMOOTH_N,取 5 左右比较平衡,太小还是抖,太大反应迟钝、速度滞后。ppm就是前面的比例尺。注意这里用的是首尾两点直线距离,如果车在画面里拐弯,这个距离会偏小,速度偏低——固定机位直行场景没问题,弯道场景要换成逐帧累加。
3.3 输出格式与编码选择
资源包里同时有output.mp4和outpy.avi,说明作者试过不同编码。OpenCV 写视频的编码器在 Windows 和 Linux 上表现不一样,mp4v通用性好但某些播放器不认,XVID配.avi兼容性更稳。
# 编码选择:mp4 用 mp4v,avi 用 XVID fourcc_mp4 = cv2.VideoWriter_fourcc(*"mp4v") fourcc_avi = cv2.VideoWriter_fourcc(*"XVID") out = cv2.VideoWriter("outpy.avi", fourcc_avi, fps, (frame_w, frame_h))如果写出来的视频是 0 字节或者打不开,八成是编码器和容器不匹配,或者帧尺寸和原视频对不上。VideoWriter的尺寸参数必须和cap读出来的宽高完全一致,差一个像素都会静默失败。output.gif一般是后期用别的工具转的,OpenCV 本身不直接写 GIF,别在代码里找写 GIF 的逻辑。
4. 避坑与常见问题排查:那些让结果全错的细节
4.1 检测框一个都不出,程序却不报错
现象:跑完output.mp4,画面原样输出,没有任何绿框,终端也没报错。原因通常是myhaar.xml没加载成功,CascadeClassifier加载失败时不会抛异常,只会让detectMultiScale永远返回空。解决:在检测前加一句assert not car_cascade.empty(), "分类器加载失败",把问题提前暴露。另外确认工作目录,相对路径myhaar.xml是相对运行命令的目录,不是脚本所在目录。
4.2 速度数字大得离谱或小得可怜
现象:算出来车速 300 km/h 或者 2 km/h。原因几乎都是比例尺PIXELS_PER_METER没标定,用了默认值。解决:按 3.1 的方法重新标定,用画面里的车道线或已知车长反推。还有一个隐蔽原因是帧率读错,某些视频CAP_PROP_FPS返回 0 或 90000,需要手动指定或用cap.get(cv2.CAP_PROP_FRAME_COUNT) / 时长反算。
4.3 同一个目标被反复检测,速度乱跳
现象:一辆车身上叠了好几个框,速度值来回跳。原因:Haar 检测对同一目标会产生多个重叠候选框,minNeighbors太小没压住。解决:调大minNeighbors,再加非极大值抑制(NMS)合并重叠框。OpenCV 自带cv2.dnn.NMSBoxes,把检测框和置信度传进去就能去重。
# 对检测框做 NMS 去重 boxes = [[x, y, w, h] for (x, y, w, h) in cars] confidences = [1.0] * len(boxes) # Haar 不给置信度,统一给 1 indices = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4) if len(indices) > 0: cars = [boxes[i] for i in indices.flatten()]4.4 跟踪 ID 频繁切换,轨迹断裂
现象:一辆车开着开着 ID 变了,速度重新从 0 开始算。原因:质心跟踪靠最近邻匹配,车被遮挡或检测漏帧时匹配失败。解决:给匹配加一个最大距离阈值,超过阈值才新建 ID,否则沿用最近的;再配合前面说的历史位移平滑,短暂漏检也能撑过去。如果场景复杂,直接换cv2.legacy.TrackerCSRT_create(),它对遮挡的鲁棒性好很多,代价是慢一点。
4.5 输出视频打不开或只有第一帧
现象:output.mp4播放器报错,或者只有一帧画面。原因:VideoWriter的fourcc和文件扩展名不匹配,或者帧尺寸写错。解决:mp4 配mp4v,avi 配XVID,尺寸严格用cap.get读出来的宽高。写完记得out.release(),不释放缓冲区视频不落盘。另外确认磁盘有空间,写满也会静默失败。
5. 进阶技巧:把测速结果做成可验证的数据
跑通只是起点,真正让这份资源有价值的是把速度变成可核对的数据。我一般会在speed_check.py里加一段 CSV 记录,每帧把目标 ID、中心点、瞬时速度写进去,跑完用表格核对哪一段速度异常。
import csv # 在循环外打开文件 with open("speed_log.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["frame", "track_id", "cx", "cy", "speed_kmh"]) # 循环内,算出速度后写入 # writer.writerow([frame_idx, track_id, cx, cy, round(speed, 2)])有了这份 CSV,验证方法就清晰了:挑一辆匀速行驶的车,看它的速度曲线是不是一条水平线;如果锯齿明显,说明平滑窗口SMOOTH_N太小;如果整体偏高,回去查比例尺。还可以拿视频里已知的参照——比如车经过两条车道线之间的时间——手算一个速度,和程序输出对比,误差在 10% 以内就算这份代码调到位了。
另一个实用技巧是分区域标定。固定机位下,画面下半部分(近处)和上半部分(远处)的像素-米比例能差一倍。做法是把画面按 y 坐标切成几段,每段用不同的PIXELS_PER_METER,检测框中心落在哪段就用哪个值。
# 分区域比例尺:近处和远处分别标定 def get_ppm(cy, frame_h): if cy > frame_h * 0.66: # 画面下三分之一,近处 return 6.0 elif cy > frame_h * 0.33: # 中间 return 9.0 else: # 远处 return 14.0这三个值要按你自己的画面重新标,别照抄。分区域之后,远处车的速度估算会准很多,因为透视压缩在远处最严重。
注意:Haar 分类器对夜间、逆光、雨雾场景基本失效,这份资源适合白天、光照均匀的固定机位视频。如果你的场景不满足,别硬调参数,直接换 YOLO 系列检测器,跟踪和测速逻辑可以原样复用。
从那以后我每次拿到这类测速代码,第一件事不是跑,而是先量画面里的车道线像素、确认帧率、把比例尺标死,再动检测参数。顺序反了,后面所有调参都是在错误的基础上打转。希望这份拆解帮到你,少走几段弯路。
本文还有配套的精品资源,点击获取