
简介这份资源面向具备一定Python基础、希望入门计算机视觉与智能交通方向的开发者围绕「视频中车辆测速」这一典型任务提供可运行的OpenCV实现方案。包内共14个文件以10个xml分类器文件、2个mp4示例视频和2个py脚本为主压缩包约59.38MBxml用于车辆与目标检测模型加载mp4提供真实道路测试素材py脚本则串联视频读取、车辆检测、追踪与速度换算等核心流程。内容涉及背景减除、帧差与光流检测、Canny与Sobel边缘提取、轮廓分析、卡尔曼滤波追踪以及基于摄像头参数和车道物理尺寸的像素位移到真实车速的换算思路并延伸至后端服务集成与音视频同步处理。目前已有2424人学习下载适合作为课程设计、毕业项目或算法练手的参考帮助读者快速跑通测速流程并理解各环节的工程取舍。1. 从一段路口监控说起python opencv车辆测速视频车速检测到底在算什么手里有一段路口或高速匝道的监控视频画面里车来车往领导或甲方丢过来一句话能不能算出每辆车大概跑多快。这就是 python opencv车辆测速视频车速检测 要解决的事。它不依赖雷达、不依赖地感线圈只用一段普通摄像头录下的视频通过 OpenCV 做目标跟踪把车辆在连续帧里的像素位移换算成真实世界的位移再除以时间得到速度。整套流程的核心难点不在写代码而在标定像素到米的换算系数怎么来、帧率怎么用、跟踪框抖动怎么压。适合有 python 基础、会装 opencv、想拿视频做交通分析的从业者。新手能照着跑通最小版本熟手能看清误差边界和参数取舍。下面按「先立住原理、再动手复现、最后避坑」的顺序讲透。2. 测速的物理模型与 OpenCV 跟踪选型为什么不能直接拿像素差除以帧2.1 像素速度到真实速度的换算链条视频测速的本质是一个比例换算问题。摄像头固定、朝向固定、焦距固定的前提下画面里同一深度的物体像素位移和真实位移近似成正比。设标定得到的比例系数为k单位是「米/像素」某辆车在第t帧到第tn帧之间跟踪框中心移动了d像素视频帧率为fps那么真实位移 d * k (米) 时间间隔 n / fps (秒) 速度 d * k * fps / n (米/秒) 换算 km/h 再乘 3.6这条公式是整个方案的骨架。看起来简单但每一步都有坑。k不是常数它随车辆在画面中的纵深位置变化近处大、远处小。所以严格来说只有在一个「标定平面」附近k才近似稳定。常见做法是在路面上选一段已知长度的参照物比如车道虚线国标一段虚线加间隔约 15 米具体以现场为准量出它在画面里占多少像素反推k。这一步做不准后面跟踪再准速度也是错的。fps必须用视频真实帧率不能想当然写 25 或 30。很多监控视频封装帧率和实际播放帧率不一致用cv2.VideoCapture读出来的CAP_PROP_FPS才是准的。如果视频被抽帧或变速过这个值会骗你需要手动核对。n是两帧之间的间隔帧数。间隔太小像素位移只有一两个像素噪声占比大间隔太大车辆可能已经变道或被遮挡跟踪框跟丢。经验上让位移落在 20 到 80 像素之间比较稳具体靠调。2.2 跟踪器选型为什么我一般不用光流做车辆测速OpenCV 里能用的跟踪手段不少选错了后面全是血泪。常见几类方法原理适合场景测速里的问题帧差法相邻帧相减找运动区域背景极稳、车少只能给运动掩膜拿不到稳定 ID无法逐车测速光流法跟踪角点运动矢量纹理丰富、小位移车辆表面纹理弱时角点丢失且算的是局部运动不是整车背景建模MOG2/KNN 分离前景固定摄像头能检测不能跟踪阴影和光照突变误检多相关滤波跟踪KCF/CSRT 等单目标、短时需要手动或检测器给初始框遮挡后易漂移检测跟踪YOLO 检测 卡尔曼/匈牙利匹配多车、复杂场景依赖检测模型但最稳工业界主流做视频车速检测我一般会走「检测 跟踪」这条路。检测器负责每帧找出车在哪跟踪器负责把不同帧的同一辆车关联起来给它一个稳定 ID。OpenCV 自带cv2.TrackerCSRT_create()这类单目标跟踪器配合一个检测器哪怕先用简单的背景建模或形态学找车也能跑。如果不想引入深度学习依赖可以用cv2.createBackgroundSubtractorMOG2()出前景再用轮廓找车用质心距离做简易关联。这套纯 OpenCV 方案在车流不密集、光照稳定的路口够用也是新手最容易复现的起点。选 CSRT 而不是 KCF是因为 CSRT 在车辆这种有遮挡、有尺度变化的场景下精度更好代价是慢一点。测速对实时性要求没那么高离线跑视频完全能接受。如果要多车同时跟踪单目标跟踪器要开多个实例管理起来麻烦这时候更推荐检测器每帧出框自己写一个基于 IoU 和质心距离的匹配逻辑或者上cv2.legacy.MultiTracker注意新版 OpenCV 里部分跟踪器挪到了 legacy 命名空间装错版本会报module cv2 has no attribute TrackerCSRT_create这是新手最常翻的车之一。2.3 最小可跑通流程读视频、标定、跟踪、算速度先把骨架搭出来细节后面填。下面这段代码是纯 OpenCV 的最小版本用背景建模找车用质心关联给简易 ID用固定k算速度。它能跑但精度一般作用是让你先看到数字动起来。import cv2 import numpy as np # 打开视频确认帧率真实值 cap cv2.VideoCapture(road.mp4) fps cap.get(cv2.CAP_PROP_FPS) if fps 0: fps 25.0 # 兜底但一定要人工核对 print(真实帧率:, fps) # 标定系数米/像素需要你现场量参照物后填 K 0.05 # 示例值千万别直接拿去用 # 背景建模适合固定摄像头 bg cv2.createBackgroundSubtractorMOG2(history500, varThreshold36, detectShadowsTrue) tracks {} # id - 最近一次质心 next_id 0 frame_idx 0 while True: ret, frame cap.read() if not ret: break frame_idx 1 fg bg.apply(frame) # 去阴影MOG2 把阴影标成 127阈值化掉 _, fg cv2.threshold(fg, 200, 255, cv2.THRESH_BINARY) # 形态学去噪 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) fg cv2.morphologyEx(fg, cv2.MORPH_OPEN, kernel) fg cv2.dilate(fg, kernel, iterations2) contours, _ cv2.findContours(fg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) centers [] for c in contours: area cv2.contourArea(c) if area 800: # 过滤小噪点按分辨率调 continue x, y, w, h cv2.boundingRect(c) cx, cy x w // 2, y h // 2 centers.append((cx, cy, x, y, w, h)) # 简易质心关联距离小于阈值认为是同一辆车 new_tracks {} for (cx, cy, x, y, w, h) in centers: matched None for tid, (px, py, pframe) in tracks.items(): if abs(cx - px) 60 and abs(cy - py) 60: matched tid break if matched is None: matched next_id next_id 1 else: px, py, pframe tracks[matched] dt (frame_idx - pframe) / fps if dt 0: dist_px np.hypot(cx - px, cy - py) speed_ms dist_px * K / dt speed_kmh speed_ms * 3.6 cv2.putText(frame, f{speed_kmh:.1f}km/h, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) new_tracks[matched] (cx, cy, frame_idx) cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) tracks new_tracks cv2.imshow(speed, frame) if cv2.waitKey(1) 0xFF 27: break cap.release() cv2.destroyAllWindows()逻辑说明背景建模出前景轮廓找车质心距离做最朴素的关联。速度用「当前帧质心」和「该 ID 上一次出现帧的质心」算dt用帧号差除以帧率。参数说明history控制背景模型记忆长度车流大时调大varThreshold越小越敏感误检也多面积阈值800必须按你的分辨率改1080p 下车辆轮廓面积通常几千到几万像素质心匹配阈值60像素要按车速和帧率调太快会跟丢太大会串车。这段代码的定位是「让你先跑起来」它的速度数字会跳、会飘因为质心抖动、关联错误、k不准三个问题全在。下一章讲怎么把它做稳。3. 把速度做稳标定、跟踪优化与参数整定的实操细节3.1 标定系数 K 怎么量才靠谱K是整个方案里最容易被糊弄、又最不能糊弄的参数。三种常见做法按可靠性排序第一种现场实测参照物。在摄像头视野内的路面上找或放一个已知长度的物体横跨车辆行驶方向量出它在画面里的像素长度。比如一条车道宽 3.5 米在画面里占 140 像素那K 3.5 / 140 0.025米/像素。注意要量「垂直于光轴、且和车辆同一纵深」的方向斜着量会引入透视误差。第二种用车道虚线推算。国标车道虚线一般「线段长 6 米、间隔 9 米」一组 15 米但各地执行有差异必须现场确认。在画面里量出一组虚线的像素长度反推K。这个方法不用额外放东西但依赖你对现场标线的确认。第三种用已知尺寸的车辆。比如常见家用车宽约 1.8 米在画面里量出车宽像素反推K。这个方法最不靠谱因为车型差异大而且车宽方向往往不垂直于光轴。提示K只在标定平面附近成立。如果车辆从远处开到近处纵深变化大单一K会让远处速度偏小、近处偏大。要更准得做透视标定用cv2.getPerspectiveTransform把画面映射成俯视图在俯视图里像素和真实距离才是线性关系。这一步是进阶新手先用单K跑通再考虑升级。3.2 用 CSRT 跟踪器替代质心关联质心关联在车少时能用车一多就串 ID。换成 CSRT 单目标跟踪配合检测器给初始框稳定性上一个台阶。下面这段演示「检测到新车就起一个 CSRT 跟踪器」的写法import cv2 cap cv2.VideoCapture(road.mp4) fps cap.get(cv2.CAP_PROP_FPS) K 0.025 # 米/像素按你的标定填 bg cv2.createBackgroundSubtractorMOG2(history500, varThreshold36, detectShadowsTrue) trackers {} # id - {tracker: csrt, last_center: (x,y), last_frame: idx} next_id 0 frame_idx 0 def make_tracker(frame, box): t cv2.TrackerCSRT_create() t.init(frame, box) return t while True: ret, frame cap.read() if not ret: break frame_idx 1 # 每帧先更新已有跟踪器 dead [] for tid, info in trackers.items(): ok, box info[tracker].update(frame) if not ok: dead.append(tid) continue x, y, w, h [int(v) for v in box] cx, cy x w // 2, y h // 2 px, py info[last_center] dt (frame_idx - info[last_frame]) / fps if dt 0: dist_px ((cx - px) ** 2 (cy - py) ** 2) ** 0.5 speed_kmh dist_px * K / dt * 3.6 cv2.putText(frame, fID{tid} {speed_kmh:.1f}km/h, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) info[last_center] (cx, cy) info[last_frame] frame_idx for tid in dead: del trackers[tid] # 每隔几帧做一次检测给新目标起跟踪器 if frame_idx % 5 0: fg bg.apply(frame) _, fg cv2.threshold(fg, 200, 255, cv2.THRESH_BINARY) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) fg cv2.morphologyEx(fg, cv2.MORPH_OPEN, kernel) fg cv2.dilate(fg, kernel, iterations2) contours, _ cv2.findContours(fg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for c in contours: if cv2.contourArea(c) 1500: continue x, y, w, h cv2.boundingRect(c) # 判断是否已被现有跟踪器覆盖避免重复起跟踪 cx, cy x w // 2, y h // 2 covered False for info in trackers.values(): tx, ty info[last_center] if abs(cx - tx) w and abs(cy - ty) h: covered True break if not covered: trackers[next_id] { tracker: make_tracker(frame, (x, y, w, h)), last_center: (cx, cy), last_frame: frame_idx, } next_id 1 cv2.imshow(speed, frame) if cv2.waitKey(1) 0xFF 27: break cap.release() cv2.destroyAllWindows()逻辑说明已有跟踪器每帧更新拿到新框算速度检测器每隔 5 帧跑一次发现没被覆盖的新目标就起一个 CSRT。参数说明检测间隔5是速度和稳定性的折中车快就调小面积阈值1500按分辨率调覆盖判断用框宽高做阈值防止同一辆车被重复起跟踪。注意cv2.TrackerCSRT_create()在部分 OpenCV 版本里要写成cv2.legacy.TrackerCSRT_create()装完先print(cv2.__version__)确认报AttributeError就换 legacy 写法。3.3 速度平滑别让数字像心电图原始逐帧速度抖动很大因为跟踪框中心每帧都在跳。直接显示会被人质疑「这测的什么玩意」。常见平滑手段滑动平均最简单保留最近 N 次速度取均值。N 取 5 到 10太大响应迟钝太小没效果。卡尔曼滤波更专业把「位置 速度」作为状态用观测到的位置去更新输出的速度天然平滑。OpenCV 有cv2.KalmanFilter但配置矩阵对新手不友好容易调出玄学结果。我一般先用滑动平均够用就不上卡尔曼。还有一个技巧不要每帧都算速度而是每隔固定帧数算一次用较长的时间基线。比如每 5 帧算一次dt就是 5/fps位移更大相对噪声更小。代价是速度更新频率降低但对显示来说完全够。from collections import deque class SpeedSmoother: def __init__(self, window7): self.buf deque(maxlenwindow) def update(self, v): self.buf.append(v) return sum(self.buf) / len(self.buf) # 每个跟踪器挂一个 smoother # speed smoother.update(raw_speed)参数说明window越大越平滑但越滞后路口场景 5 到 7 比较合适。如果车辆速度变化剧烈比如急刹平滑会掩盖真实变化这种场景要慎用或改用卡尔曼。4. 避坑与排查视频测速里最容易翻车的 5 个地方4.1 现象速度整体偏大或偏小但跟踪看起来没问题原因K标定错了或者fps用错。这是最常见的翻车。很多人拿网上示例的K直接套自己的视频或者把fps硬编码成 30而视频实际是 25。解决先打印cap.get(cv2.CAP_PROP_FPS)核对帧率再用已知长度的参照物重新标定K。验证方法找一辆你知道大概速度的车比如跟车实测看算出来差多少反推K的修正系数。如果整体偏大固定倍数多半是K或fps的问题不是跟踪的问题。4.2 现象同一辆车速度忽大忽小数字乱跳原因跟踪框中心抖动或者 ID 串了。质心关联在车辆靠近、遮挡、阴影干扰时会串 ID导致速度用错对象算。解决换 CSRT 跟踪器加滑动平均检测间隔别太密。如果还跳检查是不是阴影被当成车了——MOG2 的detectShadowsTrue会把阴影标成 127必须用阈值 200 把它滤掉否则阴影轮廓会让框变大、中心偏移。4.3 现象远处车辆速度明显偏小近处偏大原因透视效应。单一K只在标定平面成立远处同样真实位移对应更少像素算出来速度偏小。解决做透视变换把画面映射成俯视图再测速。用cv2.getPerspectiveTransform选画面里一个梯形路面区域映射成矩形在矩形里像素和真实距离线性对应。这一步需要你选四个点选点要选在路面上、尽量覆盖车辆行驶区域。映射后重新标定K速度一致性会好很多。4.4 现象程序报module cv2 has no attribute TrackerCSRT_create原因OpenCV 版本差异。4.x 某些版本把跟踪器挪到了cv2.legacy命名空间或者你装的是精简版opencv-python-headless缺模块。解决先print(cv2.__version__)和print(hasattr(cv2, legacy))。如果有 legacy改用cv2.legacy.TrackerCSRT_create()。如果没有说明装的是不带 contrib 的版本换opencv-contrib-python。安装命令按你的环境来pip 环境用pip install opencv-contrib-pythonconda 环境用conda install -c conda-forge opencv。装完重启解释器再试。4.5 现象视频读出来帧数不对或者读到一半就结束原因视频编码问题或者cv2.VideoCapture后端不支持该格式。有些监控视频是 H.265 封装OpenCV 默认后端解不了。解决先确认cap.isOpened()为 True再打印cap.get(cv2.CAP_PROP_FRAME_COUNT)和实际读到的帧数对比。如果差很多换后端比如cv2.VideoCapture(path, cv2.CAP_FFMPEG)。还不行就用 ffmpeg 先把视频转成 H.264 的 mp4 再读。这一步是环境问题不是算法问题别在代码里死磕。5. 进阶用透视变换 检测器把测速做到能交付前面几章跑通的是「能出数字」的版本这一章讲怎么做到「数字能拿给人看」。核心两件事透视变换消除纵深误差检测器替代背景建模提升鲁棒性。透视变换的关键是选对四个点。在画面里找一块路面最好是矩形区域比如两条车道线之间的梯形。选四个角点映射到一个你设定尺寸的矩形比如宽 800 高 600 像素对应真实世界宽 7 米两车道高 20 米。这样映射后横向 800 像素对应 7 米K 7/800 0.00875米/像素纵向同理。映射后车辆在俯视图里运动速度计算不再受纵深影响。import cv2 import numpy as np # 源点画面里路面梯形的四个角顺序 左上 右上 右下 左下 src np.float32([[320, 400], [960, 400], [1200, 700], [80, 700]]) # 目标点映射后的矩形宽 800 高 600 dst np.float32([[0, 0], [800, 0], [800, 600], [0, 600]]) M cv2.getPerspectiveTransform(src, dst) def to_birdseye(frame): return cv2.warpPerspective(frame, M, (800, 600)) # 标定映射后 800 像素对应真实 7 米 K 7.0 / 800 # 米/像素参数说明src四个点必须按「左上、右上、右下、左下」顺序顺序错了映射会翻转。选点要选在路面上、尽量是矩形区域的角选得越准K越接近真实。dst的尺寸自己定但K要按dst宽度和对应真实宽度重算。映射后画面边缘会有拉伸车辆只在中间区域测速才准。检测器方面如果允许引入深度学习YOLO 系列是主流检测框比背景建模稳得多尤其在有阴影、光照变化、车辆颜色接近路面时。用 YOLO 出框再用前面讲的 CSRT 或简易关联做跟踪速度稳定性会明显提升。如果不想引入深度学习至少把背景建模的参数调好加形态学、加阴影过滤、加面积和长宽比过滤车辆轮廓长宽比一般在 1.2 到 3 之间能压掉大部分误检。验证方法找一段有已知速度参考的视频或者用一辆车跟车实测对比算出来的速度和真实速度。误差在 ±10% 以内对非执法场景就算能交付了。如果要求更高得做相机标定内参 外参用cv2.calibrateCamera那套复杂度上一个量级一般项目用不上。我自己的习惯是先拿单K版本跑通看数字量级对不对再做透视变换看远处近处速度是否一致最后才考虑换检测器。顺序反了容易在环境配置上耗掉半天还没看到速度数字。这套方案值不值得做取决于你的场景——固定摄像头、车流不密集、精度要求 ±10% 左右纯 OpenCV 就够要执法级精度得上雷达或专业测速设备视频测速只能做辅助。希望帮到你。本文还有配套的精品资源点击获取