简介:这套Python OpenCV车辆测速视频车速检测资源,面向计算机视觉初学者与后端开发人员,以可运行的工程演示视频,展示车辆检测与速度估算的完整流程。压缩包共14个文件,含10个Haar级联分类器XML(对应不同训练参数,用于车辆区域识别)、2个Python脚本(corner detection.py提取角点特征,speed.py计算速度)以及2个MP4测试视频,整体约59.38MB,结构清晰可快速部署实验。已有2420人学习下载,资源不仅提供可直接运行的检测代码,还覆盖视频帧预处理、背景减除与帧差法、轮廓与边缘检测、目标追踪以及基于像素位移换算真实速度等关键知识点,并给出与后端服务、音视频流集成的实现思路。借助这些内容,读者可动手跑通测速全流程,理解摄像头参数标定与物理尺寸换算的要点,并能在此基础上扩展深度学习模型以应对复杂场景,适合作为课程设计、毕业设计或实际测速项目的基础工程。
1. python opencv车辆测速视频车速检测:这套方案能解决什么,别指望它能替代雷达
园区出入口、乡镇路段或者临时施工区域,手里只有一段现成的监控视频,没有测速枪,也不想引入整套专用设备,这时候 python opencv车辆测速视频车速检测 这条技术路线就能派上用场:用OpenCV把视频读进来,识别出画面中的车辆并保持跟踪,再把像素位移换算成真实路面里程,最后输出每辆车通过测速区间的速度。这类方案适合交通量调查、临时限速提醒、工地和园区内的车速预警,但我会先把话说死:它是辅助观测工具,不是执法级测速仪,精度和稳定性离专业雷达还有量级差距。
真正劝退大多数新手的,不是模型检测不出车,而是三个不起眼的环节:透视标定、帧时间基准、目标ID稳定性。标定错了,整条路上所有车的速度成片偏大或偏小;时间戳乱了,匀速行驶的车被算成忽快忽慢;ID一旦重连,一辆车可能被算出两倍车速。下面按一套最小可运行系统的顺序走一遍:视频读取、车辆检测、轨迹跟踪、透视标定、速度计算,最后附上我踩坑之后的处置办法。代码按OpenCV 4的常规接口来写,拿到就能在本地跑。
2. 读取视频与搭建可运行骨架:先把路面坐标和帧时间基准立起来
2.1 背景差分还是检测模型:测速场景我推荐“检测器+追踪器”
OpenCV 里最传统的车辆检测法是 MOG2 背景差分,接口现成,不需要模型文件,早年不少人拿它做车流量统计。但放到测速场景里,它有三个致命伤:慢速或停车车辆会慢慢融进背景,前景块直接消失,轨迹断掉;树叶晃动、阴影变化会产生大量噪声块,必须堆形态学操作才能勉强稳定;车辆间距近时两辆车的前景连成一个连通域,质心被拉偏,速度自然算不准。这些在演示视频里都能跑通,一换现场就容易翻车。
所以现在做车速检测,常见做法是“检测器+追踪器”:检测模型负责把车找出来框住,追踪逻辑负责把每一帧的检测结果关联成同一条轨迹。检测解决“认出车”,追踪解决“保证是同一辆车”,测速需要连续且稳定的坐标序列,这套组合最稳。算力紧张时我一般用 YOLOv4-tiny 这类轻量模型,OpenCV 的 dnn 模块直接加载,不需要额外搭推理框架;再往上可以换 YOLOv5 导出的 ONNX,但代码结构不变。
2.2 最小可运行框架:打开视频、画出测速区间
先别急着上检测模型,第一步把视频源打通。很多时候视频本身有问题:文件损坏、编码不支持、分辨率异常,这些都会让后续工作白做。下面这段代码只做三件事:打开视频、把测速区间画出来、按 q 退出预览。
import cv2 video_path = "traffic_record.mp4" cap = cv2.VideoCapture(video_path) if not cap.isOpened(): raise IOError(f"无法打开视频文件: {video_path}") width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 测速区间:纵向取画面35%到85%这一截,避开画面边缘畸变 zone_top = int(height * 0.35) zone_bottom = int(height * 0.85) while cap.isOpened(): ret, frame = cap.read() if not ret: break cv2.line(frame, (0, zone_top), (width, zone_top), (0, 255, 0), 2) cv2.line(frame, (0, zone_bottom), (width, zone_bottom), (0, 0, 255), 2) cv2.rectangle(frame, (0, zone_top), (width, zone_bottom), (255, 255, 0), 1) cv2.imshow("speed_zone_preview", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()代码逻辑很简单,但测速区间的选取是有讲究的。zone_top和zone_bottom不要取到画面最远端和最底端:最远端车辆目标太小,检测容易漏;最底端车辆距离镜头太近,透视畸变最剧烈,像素距离和实际距离的比例变化最快。留出上下各 10% 到 15% 的余量,让检测和追踪在区间两端有缓冲,不容易把刚进入画面的车和即将离开画面的车弄混。
如果你的视频里车道是横向行驶的,把区间按列方向截取即可,后续所有基于 y 坐标的判断都改成 x 坐标,原理不变。
2.3 别直接相信 CAP_PROP_FPS:先统计每帧真实时间
车速计算离不开时间。很多人习惯直接用cap.get(cv2.CAP_PROP_FPS)得到帧率,再用1/FPS当作每帧时间差,但这里有个隐藏很深的坑:这个数值只是容器元数据,解码速度、机器负载、视频编码格式都会让真实读取节奏和它不一致。测速结果一旦依赖这个固定值,整条速度曲线都会出现系统性偏差。
import time cap = cv2.VideoCapture(video_path) fps_info = cap.get(cv2.CAP_PROP_FPS) sample_count = 300 start_time = time.time() read_count = 0 for _ in range(sample_count): ret, _ = cap.read() if ret: read_count += 1 duration = time.time() - start_time real_fps = read_count / duration if duration > 0 else 0.0 print(f"容器FPS={fps_info:.2f}, 实测读取FPS={real_fps:.2f}")跑完你会发现,两个 FPS 经常不一样,尤其在 H.264 长视频上。后面计算速度时,离线视频我一般用“帧序号/容器FPS”作为时间基准,因为cap.read()实际读取耗时不代表视频内时间;而实时摄像头流则直接记录time.time()。这两套时间基准混用,是测速结果忽快忽慢的常见根源,后文避坑部分会专门展开。
3. 车辆检测与追踪:给每辆车一个稳定的ID,测速才算成立
3.1 用OpenCV的DNN模块加载轻量检测模型:代码与推理参数
OpenCV 的 dnn 模块可以加载 YOLO、SSD、ONNX 等格式的模型,不需要额外安装深度学习框架。这里以 YOLOv4-tiny 为例,因为它对 CPU 友好,检测精度在车辆这类大目标上也够用。需要提前准备三个文件:weights 权重、cfg 配置、以及类别名映射,放工程目录的 models 文件夹下。
import cv2 import numpy as np weights_path = "models/yolov4-tiny.weights" config_path = "models/yolov4-tiny.cfg" net = cv2.dnn.readNet(config_path, weights_path) net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) def detect_vehicles(frame, conf_thresh=0.5, nms_thresh=0.4): h, w = frame.shape[:2] # 416是YOLO常用输入尺寸,越小越快但远距离小目标容易漏检 blob = cv2.dnn.blobFromImage( frame, 1/255.0, (416, 416), swapRB=True, crop=False ) net.setInput(blob) layer_names = net.getUnconnectedOutLayersNames() outputs = net.forward(layer_names) boxes, scores, class_ids = [], [], [] for output in outputs: for det in output: class_scores = det[5:] class_id = int(np.argmax(class_scores)) confidence = float(class_scores[class_id]) if confidence < conf_thresh: continue cx, cy, bw, bh = det[:4] * np.array([w, h, w, h]) x = int(cx - bw / 2) y = int(cy - bh / 2) boxes.append([x, y, int(bw), int(bh)]) scores.append(confidence) class_ids.append(class_id) idx = cv2.dnn.NMSBoxes(boxes, scores, conf_thresh, nms_thresh) results = [] if len(idx) > 0: for i in idx.flatten(): # COCO类别: 2=car, 5=bus, 7=truck if class_ids[i] in [2, 5, 7]: results.append((boxes[i], scores[i])) return resultsconf_thresh是置信度阈值,推荐 0.5 起步;如果现场漏检严重,可以降到 0.3,但要接受更多误检。nms_thresh是非极大值抑制阈值,默认 0.4 即可,多车重叠严重的场景下调到 0.3。输入尺寸 416 是精度和速度的平衡点;帧率要求高时改成 320,距离远、目标小时改成 608。
只过滤 car、bus、truck 是为了避免把行人、自行车算进去。有些场景也想测摩托车,就把类别 3 加进去,但摩托车目标小、速度快,轻量模型在远端的漏检率会明显上升,需要单独调参。
3.2 质心匹配追踪:检测框抖没关系,质心必须稳
检测模型每帧都输出独立的框,但不会告诉你这一帧的框和上一帧的框是不是同一辆车。最简单可靠的关联方法是质心匹配:把每帧检测框的中心点取出来,和已有的轨迹中心比较距离,距离最近且小于阈值的就认为是同一辆车。OpenCV 自带的 KCF、CSRT 跟踪器在单目标跟随上好用,但车辆流场景目标多、目标之间会相互遮挡,跟踪器容易漂移或锁错目标,我很少在测速项目里直接用它们。
import math from collections import defaultdict MAX_AGE = 0.5 # 轨迹存活时间,单位秒 MAX_PIXEL_DIST = 80 # 帧间质心最大匹配距离,单位像素 tracks = {} next_track_id = 0 def frame_to_points(results): """把检测框转成跟踪点,这里取框底部中心,更贴近车辆在地面的位置""" points = [] for box, _ in results: x, y, bw, bh = box points.append((int(x + bw / 2), int(y + bh))) return points def update_tracks(points, current_ts): global next_track_id used = set() # 贪心匹配:每个检测点找最近的未使用轨迹 for pt in points: best_id = None best_dist = MAX_PIXEL_DIST for tid, (tx, ty, last_ts) in tracks.items(): if tid in used: continue d = math.hypot(pt[0] - tx, pt[1] - ty) if d < best_dist: best_dist = d best_id = tid if best_id is not None: # 指数平滑,降低单帧检测框抖动的影响 tx, ty, _ = tracks[best_id] tracks[best_id] = ( 0.7 * tx + 0.3 * pt[0], 0.7 * ty + 0.3 * pt[1], current_ts ) used.add(best_id) else: tracks[next_track_id] = (pt[0], pt[1], current_ts) next_track_id += 1 # 删除超时轨迹,防止ID一直堆积 expired = [ tid for tid, (_, _, last_ts) in tracks.items() if current_ts - last_ts > MAX_AGE ] for tid in expired: tracks.pop(tid, None)这里有个细节:跟踪点用框底部中心,而不是框中心。因为 YOLO 检测框的上半部分是车身,车辆在地面的投影点更靠近框底部。把底部中心投影到路面坐标系时,误差比框中心小很多。MAX_PIXEL_DIST要根据视频分辨率调整,1080p 画面下 60 到 100 像素都是常见取值,720p 则减半。
贪心匹配在车辆少、单向行驶的场景下足够用。车辆密集、相互穿插时,贪心会出错,可以换成匈牙利算法做全局最优匹配,核心目标不变:让每个 ID 在测速区间内保持唯一。
3.3 轨迹生命周期:进入测速区、越过边界、结算速度
有了稳定ID,还需要一个状态机来控制测速流程。车辆从远到近行驶时,质心先进入测速区上边界,记录进入时刻;一路跟踪到越过下边界,再计算通过时间。这是测速最简单且最不容易受单帧抖动影响的结算方式。
zone_status = {} zone_entry = {} finished = {} def update_zone_state(current_ts): for tid, (cx, cy, _) in tracks.items(): if tid not in zone_status: zone_status[tid] = "waiting" if zone_status[tid] == "waiting" and zone_top <= cy <= zone_bottom: # 车辆进入测速区间,记录进入时的位置和时间 zone_status[tid] = "in_zone" zone_entry[tid] = (cx, cy, current_ts) elif zone_status[tid] == "in_zone": if cy > zone_bottom: # 车辆越过测速区间下边界,准备结算 sx, sy, st = zone_entry[tid] dt = current_ts - st pixel_dist = math.hypot(cx - sx, cy - sy) finished[tid] = { "pixel_dist": pixel_dist, "dt": dt, "speed_kmh": None } zone_status[tid] = "done" elif cy < zone_top: # 反向行驶或者车辆倒退,重置状态 zone_status[tid] = "waiting"这一步只存像素距离和时间差,速度换算要等第 4 章完成透视标定后再做。为什么不用每一帧的瞬时速度?因为检测框抖动会让相邻帧的位移变化很不稳定,而“进入点到离开点”的距离跨度大,单帧抖动被平均掉,稳定性好得多。
4. 从像素坐标到实际距离:透视标定这部分直接决定测速精度
4.1 为什么直接算像素/秒不靠谱:透视畸变让同一速度出现十倍差异
很多初学者直接把第 3 章的像素距离除以时间,得到“像素/秒”,再随便乘个系数当速度,结果往往惨不忍睹。问题在于摄像头成像的近大远小:同样一辆车,在画面远端行驶 10 米可能只占 4 个像素,在画面近端行驶 10 米可能占 40 个像素。一个固定系数根本没办法描述这种变化。
正确思路是把画面坐标投影到路面平面,也就是做一个逆透视变换。摄像头视角下的道路是个梯形,还原成俯视图后,路面上的距离才是线性的。这一步不做,后续所有速度值都没有可信度。
4.2 单应矩阵标定:getPerspectiveTransform 与像素坐标转路面坐标
标定的实操方式很简单:在目标车道地面上找四个物理位置已知的点,记录它们在画面里的像素坐标,再量出这些点之间的真实距离,然后用cv2.getPerspectiveTransform求单应矩阵。四个点最好围成一个矩形,长边顺着行车方向,短边覆盖车道宽度;我在现场一般用卷尺和临时标定锥,十几分钟就能搞定。
import cv2 import numpy as np # 画面坐标点:取自同一车道的四个地面位置 # 近端两个点、远端两个点,现实里围成 3m x 12m 的矩形 src_pts = np.float32([ (120, 380), (680, 380), # 远端两个点,横向距离3m (80, 640), (720, 640), # 近端两个点,横向距离同样是3m ]) # 对应路面坐标,单位直接用米 dst_pts = np.float32([ (0, 0), (3.0, 0), (0, 12.0), (3.0, 12.0), ]) H = cv2.getPerspectiveTransform(src_pts, dst_pts) def pixel_to_road(px, py): """把画面坐标转换成路面坐标,返回 (米, 米)""" src = np.array([[[px, py]]], dtype=np.float32) dst = cv2.perspectiveTransform(src, H) return dst[0][0][0], dst[0][0][1]getPerspectiveTransform至少需要 4 组对应点,而且点不能有三点共线的情况。H 矩阵的意义是把画面梯形区域映射成一个米制矩形区域,之后车辆坐标可以直接放进米制坐标系里做距离计算。验证标定是否正确,最简单的办法是:把路面坐标下的四个角点画回到画面上,看它们是否和原车道区域重合;或者站在标定区域里走两步,人工确认画面中移动距离和路面坐标的对应关系。
4.3 没有标定板也能标:用路面虚线间距反推距离
有些项目现场不方便进场测量,这时候可以利用路面标线。很多道路的车道分界虚线是等距排列的,每个虚线段长度和间隔都有明确标准;但不同等级道路标线规格不一样,建议现场确认或参考道路施工资料,不要照搬固定数值。
# 沿车道方向找五个标线端点,记录像素坐标和实际纵向距离 # 假设每段虚线间距实测为6米 known_y_px = [210, 285, 360, 435, 510] known_y_m = [0.0, 6.0, 12.0, 18.0, 24.0] def pixel_row_to_meter(py): """ 简化标定:适合车道方向基本平行于画面纵轴的场景 用一维插值把像素y坐标映射成路面纵向距离 """ return float(np.interp(py, known_y_px, known_y_m))这个做法的前提很严格:摄像头正对道路、车道方向与画面纵轴基本平行、道路无明显坡度。只要这些条件有一条不满足,误差会迅速扩大。它适合原型验证和快速评估,正式项目还是建议用 4.2 节的双向单应矩阵标定。
5. 车速计算与避坑:为什么测出来忽快忽慢是常事
5.1 车速公式、时间窗口与异常值过滤
拿到路面坐标后,速度公式就很简单:距离除以时间。把第 3 章轨迹里的像素坐标转为路面坐标,再用进入点和离开点计算平均速度。
def estimate_speed(entry_point, exit_point): """ entry_point / exit_point: (路面x米, 路面y米, 时间秒) 返回 km/h """ x0, y0, t0 = entry_point x1, y1, t1 = exit_point if t1 <= t0: return None dist_m = math.hypot(x1 - x0, y1 - y0) speed_ms = dist_m / (t1 - t0) return speed_ms * 3.6 # m/s 转 km/h3.6是米每秒转公里每小时的固定系数。单次结算我只用进入点和离开点,中间所有帧不参与平均,这样最稳定。如果希望进一步平滑,可以像这样处理:不止记录进入点,而是在测速区间内每帧都记一个路面坐标点,最后取中间 N 个点两两计算速度后取中位数。中位数比平均值更能抵抗个别异常帧。
zone_entry和finished字典需要在主循环里和update_tracks联动。车辆越过下边界时,把pixel_to_road的结果传进estimate_speed。如果算出来的速度小于 0 或大于 200km/h,我一般直接丢弃,并把这条轨迹标记为异常,避免异常数据污染统计结果。
5.2 坑1:检测框上下抖动,速度曲线像心电图
现象:同一辆车在测速区间内速度值来回跳,30km/h 和 80km/h 交替出现,画面回放时车辆明明在匀速行驶。
原因:轻量检测模型输出的检测框并不稳定,尤其是远端小目标,边界框可能上下左右抖动几个像素。单帧间距离差分对这种抖动极其敏感,一帧差 3 个像素,换算成速度就是很大的波动。
解决:放弃单帧瞬时速度,只用测速区间入口和出口的整体位移计算平均速度。同时把跟踪点的指数平滑系数从 0.3 进一步调低到 0.2,让轨迹响应更迟钝。如果车辆在区间内行驶时间足够长,还可以把轨迹分成前段、中段、后段分别计算速度,取中位数。
5.3 坑2:漏检导致ID重连,测出两倍车速
现象:输出数据里偶尔出现一辆车速度超过 200km/h,回放视频时该区域只有一辆正常行驶的车。
原因:检测模型漏了一帧,轨迹因为超时被清理,下一帧这辆车被分配了新 ID;而旧 ID 的进入点还留在zone_entry里。新 ID 从区间中部开始跟踪,数据错位后,计算出的时间差很短,距离却很大,速度自然翻倍。
解决:MAX_AGE设置在 0.3 到 0.5 秒之间,漏检一两帧后轨迹不立即删除;同时在新轨迹第一次进入测速区间时,必须确认它确实从区间外进入,而不是在区间内突然出现。另外给速度设置物理上限,超过 200km/h 的结果直接丢弃,再结合是否有zone_entry记录来判断是否发生了 ID 重建。
5.4 坑3:帧时间戳不准,匀速行驶的车被算成忽快忽慢
现象:所有车辆的速度曲线同时呈锯齿状,即使路面平整、车辆匀速,结果也波动。
原因:代码里用了time.time()记录每帧时间,但 OpenCV 的cap.read()耗时不稳定;或者反过来,直接用了容器 FPS 但实际解码掉帧。离线视频和实时流的时间基准没有分开处理,是这类问题的常见来源。
解决:离线视频处理时,统一用frame_index / cap.get(cv2.CAP_PROP_FPS)作为每帧时间戳,不要掺杂time.time()。实时摄像头流才用time.time(),因为网络流、USB 摄像头的帧率本身就不可靠。还有一个更稳的做法:进入测速区间和越过下边界时,都用帧序号差除以有效帧率,而不是逐帧累加时间。
5.5 坑4:多车道共用一套标定,外侧车道整体测速偏高
现象:外侧车道的车普遍比内侧车道快 5 到 10km/h,但现场看车流速度差不多。
原因:标定时四个点只覆盖了一条车道宽。逆透视变换默认路面是一个平面,但摄像头视角下,车道横向位置不同,像素与米的比例关系也不同。单车道标定框外推到多车道,误差自然出现。
解决:标定矩形尽量覆盖所有要测速的车道,横向宽度取整个路面的宽度,而不是只取一条车道。如果摄像头画面里车道跨度太大,一条单应矩阵管不过来,就把画面按车道切成几个 ROI,每个 ROI 单独标定一组H,车辆进入哪个 ROI 就使用哪组矩阵。
6. 进阶:把测速结果落地成CSV与标记视频,再用两组数据验算
6.1 把结果导出成CSV和叠加视频
测速做完不能只停留在控制台打印。把每一辆车的 ID、进入时间、驶出时间、速度写进 CSV,同时生成一份叠加了速度数字的标记视频,方便后续核对。
import csv # 写入测速结果 with open("speed_result.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["track_id", "entry_time", "exit_time", "speed_kmh"]) for tid, speed in finished.items(): writer.writerow([tid, speed["entry_time"], speed["exit_time"], round(speed["speed_kmh"], 1)]) # 用VideoWriter输出标记视频 out_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) out_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) out = cv2.VideoWriter("result_video.mp4", cv2.VideoWriter_fourcc(*"mp4v"), cap.get(cv2.CAP_PROP_FPS), (out_w, out_h)) # 每帧检测、跟踪结束后,在车辆框上方绘制速度 # cv2.putText(frame, f"{speed_kmh:.1f} km/h", (text_x, text_y), # cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2) # out.write(frame)CSV 的好处是可以直接拖进统计软件做分位数、超速比例分析;标记视频的作用是人工抽检。两份结果一起看,才能确认测速逻辑没有在看不见的地方出错。
6.2 回放验算与标定检查
最后一步也是最容易被跳过的一步:人工回放验证。我会在标定区间里找两辆有代表性的车,一辆走内侧车道、一辆走外侧车道,手动用视频播放器看它从进入测速区间到离开的秒数,再用已知标定距离估算速度。和程序输出对比,误差超过 10% 就先查标定,再查ID。曾经有一次我发现所有车速都偏慢,排查下来是标定时把路面矩形长边量错了,多点了一米。
这套流程跑顺畅之后,还能继续扩展:加入往期视频批量处理、分时段统计车流量和平均车速、把异常速度帧截成小视频片段。做一个稳定的测速工具,关键不在于模型多强,而在于标定是否有据可查、时间基准是否统一、轨迹ID是否有约束。我自己做完一套测速,最后一步一定是把标记视频和CSV并排打开抽查,这个习惯帮我拦下了不少看似正常实则离谱的结果。希望这篇文章能帮你把车速检测这条路走得更顺。
本文还有配套的精品资源,点击获取