
简介一套基于MATLAB的车辆检测系统完整工程面向交通监控、车流统计和车速测量等应用场景适合图像处理及MATLAB GUI开发方向的学生和工程师使用。工程将视频采集、运动目标检测、瞬时速度与平均速度计算、车流量统计集成在可视化界面中并配有详细代码注释便于读者理解从图像预处理到结果输出的完整链路。项目代码覆盖背景差分、帧间运动估计、目标跟踪和计数等常见实现思路再配合GUI交互控件进行参数调整与结果展示实用性较强。压缩包共8个文件包含4个M程序文件、1个GUI界面文件、1个AVI视频样例、1个数据文件及1个导入说明文档整体仅483KB轻量精简解压后即可在MATLAB环境中运行体验。目前已有55人浏览学习适合希望掌握车辆检测算法与GUI交互设计的开发者进行练习和二次开发。1. MATLAB车辆检测系统为什么传统视觉方案反而更实用很多团队一提车辆检测就直接上YOLO但在固定机位的交通视频场景下MATLAB 自带的 Computer Vision Toolbox 用背景减除加 Blob 分析不需要训练样本就能识别车辆还能直接在同一个循环里完成车流量计数、速度计算和 GUI 展示。这个项目真正难的地方不是“检测出车”而是怎么避免同一辆车被重复计入车流量以及如何把像素位移换算成真实车速。它适合课程设计、毕业设计和快速原型验证也适合已有深度学习方案但想对比传统视觉成本的工程师。标题里的“详细注释”意味着代码不只是跑通还要让组员、答辩老师或同事能一行行读懂链路从 foreground mask 到 bounding box再到跨线计数和速度输出。2. 用背景减除做MATLAB车辆检测先选算法再调参数2.1 固定摄像头场景下为什么优先选背景减除而不是帧差法或深度学习车辆检测的常见算法有三类帧差法、背景减除、深度学习目标检测。帧差法用相邻两帧相减代码最简单但车辆静止或速度很慢时前后两帧几乎一致foreground mask 会消失造成漏检。深度学习检测精度高但 MATLAB 里要额外装 Deep Learning Toolbox还要准备数据集做训练或导入大网络在只有普通 CPU 的机器上往往跑不到实时帧率。背景减除正好卡在两者之间它用前几十帧建立背景模型后续每一帧都与背景模型比较和背景差异大的像素就标成前景。固定摄像头场景下背景像素在短时间内相对固定因此路灯、树木、路面的变化都能被模型慢慢吸收长时间运行时依然能把车辆挖出来。这里还有一个工程考虑后续要算速度和车流量本质上是追踪车辆质心位置而不是识别车辆品牌或颜色所以只需要“前景区域”而不是“目标分类”。背景减除输出的 foreground mask 可以直接输入给 Blob 分析几行代码就能拿到每个车辆区域的面积、质心和外接矩形。这样的数据链足以支撑后面的虚拟检测线和帧号差计算比深度学习方案更轻也更容易在答辩时解释清楚。2.2 用 vision.ForegroundDetector 生成前景掩码MATLAB 的 Computer Vision Toolbox 提供了基于混合高斯模型的vision.ForegroundDetector对应 OpenCV 里的 MOG2。它的原理是对每个像素保留若干个高斯分布像素值落在已有分布内就认为是背景否则认为是前景。摄像头不动时这条路线的宽容度很高。最小可运行代码段如下% 读取视频文件traffic_clip.mp4 用你自己的文件名替换 videoReader VideoReader(traffic_clip.mp4); % 建立前景检测器 foregroundDetector vision.ForegroundDetector( ... NumGaussians, 3, ... % 每个像素的混合高斯模型个数 NumTrainingFrames, 50, ... % 前50帧用于背景建模 LearningRate, 0.005); % 背景更新速度数值越大对光照变化越敏感 frameCount 0; while hasFrame(videoReader) frame readFrame(videoReader); frameCount frameCount 1; % 调用检测器并更新背景模型 foregroundMask foregroundDetector(frame); % 训练阶段的掩码不要直接用于后续检测 if frameCount 50 continue; end % 从第51帧开始foregroundMask 才代表真正的运动目标 end这段代码里有三个参数需要重点解释。NumGaussians设为 3 足够描述路面、阴影和车辆三态设得更大背景模型更复杂但内存占用和计算量也会上涨。NumTrainingFrames是背景建模帧数摄像头架好后场景越空越静止可以设为 30如果车流密集需要 80 到 100 帧才能学到干净的路面背景。LearningRate控制背景更新速度0.005 意味着大约 200 帧才会显著更新一次背景模型这样可以避免慢速车辆被当成背景吸收但遇到树影晃动会比较敏感。实际调试时可以把LearningRate从 0.001 到 0.02 扫一遍观察车辆经过时掩码是否连续。版本上还容易踩一个坑MATLAB R2016a 之前写step(foregroundDetector, frame)而较新版本直接写成foregroundDetector(frame)。两种写法在对旧项目做维护时都会遇到建议在新代码里用函数式调用代码更干净。2.3 形态学处理与 Blob 筛选的 3 个参数直接从背景减除得到的前景掩码会带很多孤立噪点车辆内部也可能出现空洞。常见做法是先做一次闭运算填充空洞再做一次开运算去除小噪点最后用bwareaopen删除面积过小的连通域。% 闭运算填充车辆内部空洞核选矩形更贴合车轮廓 maskClosed imclose(foregroundMask, strel(rectangle, [5, 5])); % 开运算去除路面上的孤立噪声点 maskOpened imopen(maskClosed, strel(rectangle, [3, 3])); % 删除面积小于200像素的前景块 maskClean bwareaopen(maskOpened, 200); % 用Blob分析提取车辆的外接框和质心 blobAnalyzer vision.BlobAnalysis( ... BoundingBoxOutputPort, true, ... AreaOutputPort, true, ... CentroidOutputPort, true, ... MinimumBlobArea, 200); [areas, centroids, bboxes] blobAnalyzer(maskClean);strel(rectangle, [5, 5])里的 5 表示闭运算核宽度核太大会把紧挨着的两辆车融合成一个 blob核太小背景噪声又消不掉。MinimumBlobArea是最关键的一个参数它和视频分辨率强相关。720P 画面里一辆小汽车在近处的投影面积可以达到几千像素在远处可能只有 100 多像素所以阈值不能只看一个值要按视频里最远处的车辆面积来定。下面是一组常用参考值视频分辨率最小车辆面积形态学核大小适用场景640x480100[3, 3]近距离路口1280x720200[5, 5]常规路侧机位1920x1080400[7, 7]俯拍且画面较大面积阈值不是越低越好。阈值低了行人、电动车、树叶影子都会被当成车辆阈值高了远处的小车在进入检测区域的前几帧会被丢掉直接影响后面的计数和速度测量。3. 车流量统计的实现虚拟检测线与状态机3.1 虚拟检测线的坐标设定车流量统计最常用的方案不是在整幅图像上做追踪网络而是在画面里画一条或多条虚拟检测线。车辆质心从检测线的一侧移动到另一侧就认为完成一次通过。这个方案和物理计数器的原理一样代码量小也方便在 GUI 上把检测线画出来给用户看。检测线位置需要避开三块区域画面边缘、车辆经常停放的位置、树影和斑马线。一般选在道路长度的中部靠下一点这样车辆已经完整进入画面外接框大小也相对稳定。假设视频画面大小为 720x1280道路从画面上方向下行驶检测线可以先放在纵坐标 420 附近。detectLineY 420; % 虚拟检测线的纵坐标按实际机位调整这里只记录一条线。如果需要区分双向车流需要再放一条线并区分车辆质心是从上往下还是从下往上穿越。单线无法判断方向但在大多单方向车流的课程设计中已经够用。3.2 防止同一辆车被重复计数的状态机计数最怕的不是漏检而是同一辆车反复横跳检测线。车辆质心靠近检测线时会随着外接框抖动而左右漂移可能一帧在线上一帧在线下如果只要“质心在线下就计数”一辆车可能会被记三次。正确做法是为每个轨迹维护当前在检测线哪一侧的状态只有检测到“从上一侧跨到另一侧”时才计数。% 轨迹数组每个元素保存质心坐标、所在侧、是否已计数 tracks struct(x, {}, y, {}, side, {}, counted, {}); % 匹配阈值单位像素可看成同一辆车两帧之间的最大移动距离 matchingThreshold 25; % 对每一帧提取到centroids后执行 for i 1:size(centroids, 1) x centroids(i, 1); y centroids(i, 2); % 找最近的已有轨迹 if isempty(tracks) nearestIdx -1; else dist2 (tracks.x - x).^2 (tracks.y - y).^2; [minDist, candidateIdx] min(dist2); if sqrt(minDist) matchingThreshold nearestIdx candidateIdx; else nearestIdx -1; end end if nearestIdx -1 % 新轨迹记录当前所在侧side 1表示检测线下方 newTrack.x x; newTrack.y y; newTrack.side sign(y - detectLineY); newTrack.counted false; tracks(end1) newTrack; %#okAGROW else % 更新已有轨迹位置 tracks(nearestIdx).x x; tracks(nearestIdx).y y; oldSide tracks(nearestIdx).side; newSide sign(y - detectLineY); % 状态变化且尚未计数才计一次 if ~tracks(nearestIdx).counted oldSide -1 newSide 1 vehicleCount vehicleCount 1; tracks(nearestIdx).counted true; end tracks(nearestIdx).side newSide; end end这段代码的核心是sign(y - detectLineY)。newSide大于 0 表示质心在检测线下方小于 0 表示在上方。轨迹第一次建立时只记录侧别不计数之后每一帧都更新侧别只有检测到oldSide -1变成newSide 1这个边沿时才计数。这样质心在检测线附近来回抖动不会触发计数因为只有跨越边界的那一次会被记为有效事件。matchingThreshold需要按帧率和车速调整。25fps 视频里一辆 40km/h 的车每帧移动约 0.44 米如果标定结果是每像素 0.02 米那就是 22 像素所以阈值取 25 合理。阈值太大会把相邻两车合成一条轨迹太小会让同一辆车被拆成多个 ID计数反而偏高。要真正校准这个值必须先做下一章的速度标定再把像素位移换算出来。3.3 用 MATLAB 把车流量输出成时段统计计数结果除了显示总数还应该按时间段输出比如每分钟一辆车。简单做法是把每辆车被计数的帧号存进数组处理完视频后统一换算成秒。countedFrames double(crossFrames); % 假设crossFrames里存了每次计数时的帧号 fps videoReader.FrameRate; timeInSeconds countedFrames / fps; % 按60秒窗口统计车流量 windowSize 60; edges 0:windowSize:max(timeInSeconds); countPerWindow histcounts(timeInSeconds, edges); % 输出表格 flowTable table(edges(1:end-1), countPerWindow, ... VariableNames, {TimeFromStartSecond, VehicleCount}); disp(flowTable);这里把帧号转成相对视频开始的时间戳再用histcounts按秒级分箱统计。注意crossFrames必须和上一节vehicleCount同时更新计数发生时就把当前frameCount推进数组否则后面统计无据可查。若视频长度超过几小时建议每隔 60 秒自动打印一次累计流量方便在 GUI 上做成动态表格。4. 速度与平均速度计算标定、计时与两种平均速度4.1 像素距离与物理距离的标定方法速度计算绕不开一个问题图像里车辆移动 100 像素物理上到底走了多少米。不做标定只能输出“像素/秒”没法直接给 km/h。交通视频里最可靠的标定物是车道分道线国标常见白色虚线长 6 米两线间隔 9 米也就是一组虚线从起点到下一个起点相距 15 米。在画面里手动点出两个已知物理距离的点就能算出每个像素对应多少米。% 在图像上取两个已知距离的像素坐标 p1 [210, 380]; % 画面上的点1 p2 [280, 420]; % 画面上的点2两点之间物理距离已知 % 已知两点间实际距离单位米 realDistanceMeters 10; % 换算比例每像素多少米 scaleMeterPerPixel realDistanceMeters / norm(p2 - p1);这个方法假设成像平面与道路平面平行实际监控大多是俯视或斜视画面近处和远处的尺度不完全一样。更精确的标定需要做透视变换或至少分段标定把道路分成远近两段分别计算远处的每像素米数和近处的每像素米数然后再换算。4.2 用两条检测线计算瞬时速度有了标定比例速度就可以用帧号差算。放两条检测线分别记为lineInY和lineOutY当车辆质心跨过第一条线时记录当前帧号跨过第二条线时再记录当前帧号两线之间的实际距离已知帧号差除以帧率就是通过时间。function vkmh computeSpeed(frameIn, frameOut, distanceMeters, fps) % computeSpeed 通过两线帧号差和物理距离计算速度 % frameIn 进入第一条检测线的帧号 % frameOut 离开第二条检测线的帧号 % distanceMeters 两条检测线之间距离单位米 % fps 视频帧率 dt (double(frameOut) - double(frameIn)) / fps; if dt 0 vkmh NaN; return; end vkmh (distanceMeters / dt) * 3.6; end调用时对每个轨迹维护两个标记是否已跨过第一线、是否已跨过第二线。跨第一线时只记录帧号跨第二线时调用computeSpeed得到该车的行驶速度。注意用质心位置穿过线的瞬间记录帧号而不是等外接框完全进入画面后再记录这样两线之间的距离定义更准确。质心位置在连续几帧间会有几个像素的抖动单帧触发产生的速度噪声很容易超过 ±3km/h。常见做法是对同一个轨迹的质心坐标做三帧滑动平均再用平滑后的质心判断是否跨线。更简单的替代是记录最近五帧中质心距离检测线最近的那一帧作为跨线帧也能压低抖动影响。4.3 时间平均速度 vs 空间平均速度mean(vList)是大多数人第一反应的平均速度写法但它是时间平均速度。假设两辆车通过同样 100 米路段一辆用 5 秒另一辆用 10 秒算术平均速度是 (2010)/215 m/s但路段上所有车的总行驶时间是 15 秒总距离 200 米空间平均速度只有 13.33 m/s。交通工程里说的平均车速通常指后者因为它更准确地反映道路拥堵程度。MATLAB 里写清两种平均速度很有好处% 单车速度序列单位km/h vList [36.2, 38.5, 37.1, 42.0]; % 时间平均速度单车速度的算术平均 temporalMean mean(vList); % 空间平均速度路段长度 / 平均通过时间 % 如果每辆车通过同一固定距离L先由速度反推通过时间 fixedDistanceKm 0.1; % 两线之间距离单位km hoursPerVehicle fixedDistanceKm ./ (vList / 3.6); spatialMean fixedDistanceKm / mean(hoursPerVehicle);这里fixedDistanceKm ./ (vList / 3.6)其实改对了。注意若把速度从km/h换算成m/s再算时间距离也要用m不要混用单位。实际 GUI 中建议两个值都展示并在输出框标注“时间平均速度”和“空间平均速度”因为答辩或报表里经常会被追问差别。5. 用 App Designer 把检测做成 GUI5.1 GUI 需要哪些控件传统 GUIDE 已经过时新项目应从 App Designer 起步。车辆检测系统的界面可以做成左侧视频显示、右侧参数控制的结构最少需要这些控件功能与名称对应关系如下控件类型名称作用UIAxesDisplayAxes显示原视频叠加检测框UIAxesFlowAxes显示车流量-时间曲线ButtonOpenVideoButton打开视频文件ButtonStartButton启动检测ButtonStopButton暂停或停止EditFieldDetectLineYField设置检测线坐标EditFieldThresholdField设置最小车辆面积LabelVehicleCountLabel显示累计车流量LabelAvgSpeedLabel显示平均速度App Designer 的自动布局比 GUIDE 好用但要注意回调函数里写app.xxxField.Value才能取到控件值不要用eval或get否则代码可读性会明显下降。核心算法应该全部放入独立函数GUI 只负责读取参数和显示结果。5.2 用 timer 驱动视频处理循环直接在按钮回调里写while循环会导致界面卡死因为 App Designer 的回调过程是单线程的等待循环结束时界面无响应。常见做法是用一个timer对象按帧周期触发回调每触发一次处理一帧。function StartButtonPushed(app, ~) % 读取参数 app.detectLineY str2double(app.DetectLineYField.Value); app.minBlobArea str2double(app.ThresholdField.Value); app.frameCount 0; app.vehicleCount 0; % 建timer固定速率触发每秒触发次数约等于视频帧率 app.detectionTimer timer( ... ExecutionMode, fixedRate, ... Period, 1 / getFrameRate(app.videoReader), ... TimerFcn, (~, ~) app.processFrame()); start(app.detectionTimer); end function processFrame(app) if ~hasFrame(app.videoReader) stop(app.detectionTimer); return; end frame readFrame(app.videoReader); app.frameCount app.frameCount 1; % 核心检测函数返回计数和速度 [mask, bboxes, newCount, speedList] ... detectVehicles(frame, app.detectLineY, app.minBlobArea, app.frameCount); % 更新显示 app.VehicleCountLabel.Text sprintf(车流量: %d, newCount); app.AvgSpeedLabel.Text sprintf(平均速度: %.1f km/h, mean(speedList)); endtimer的Period直接依赖视频帧率但实际摄像头帧率可能不准TimerFcn处理时间长过周期时Windows 会逐渐积压回调。更稳的写法是在processFrame里用tic/toc计算当前循环消耗时间动态调整 timer 的Period或者干脆不用 timer改用drawnow配合pause的 while 循环在停止按钮回调里置一个stopFlag结束循环。两种方式都能跑timer 方式更接近工业上位机里的定时采样思路。5.3 在 GUI 上叠加车辆框、检测线和实时指标UIAxes上显示图像有两种方式imshow(app.UIAxes, frame)每帧都调用一次会比image函数慢很多。常见做法是第一次创建图像对象之后只更新CData。% 初始化显示app.frameImage 保存图像句柄 if isempty(app.frameImageImage) app.frameImageImage imshow(frame, Parent, app.DisplayAxes); else app.frameImageImage.CData frame; end叠加检测线用line在同一个UIAxes上画一条红色水平线hold(app.DisplayAxes, on); line(app.DisplayAxes, [1 size(frame, 2)], [app.detectLineY app.detectLineY], ... Color, r, LineWidth, 1.5); hold(app.DisplayAxes, off);检测框用rectangle或insertShape都可以。insertShape会生成新图像然后imshow更新图像帧率会下降直接对axes画矩形则只更新少量线条属性性能更好。实时车流量曲线用plot的方式类似先保存数据每 30 帧刷新一次曲线即可不要每帧重绘全部历史点。6. 调参与验证让 MATLAB车辆检测在不同场景下少出错6.1 用已知速度的合成视频验证算法先造一段运动矩形视频验证速度和计数链路是否自洽。下面的代码生成一个宽度固定、每帧移动 3 像素的白色矩形videoOut VideoWriter(syntheticTest.avi); open(videoOut); for k 1:100 frame zeros(480, 640, 3, uint8); % 矩形从第50列开始每帧右移3像素 frame(200:280, 50 3*k:100 3*k, :) 255; writeVideo(videoOut, frame); end close(videoOut);把这段视频丢进检测系统应该得到矩形从第一线到第二线的帧号差。若两线实际像素距离为 60帧率 25fps则理论速度是60 / (frameOut - frameIn) * fps像素/秒。验证时把输出速度再反算成像素位移确认和实际每帧 3 像素一致。这个方法能快速排除标定错误再切换真实视频时背景复杂度会造成偏差但至少能证明代码主流程没问题。6.2 三个高频坑第一个高频坑是阴影把两辆车连成一片。树影和人影产生的 foreground mask 常和车辆粘连开运算核不够大时会直接把两个目标合并。解决方法是先对 mask 做一次bwareaopen再用regionprops检查长宽比过滤掉明显横向拉长的区域。也可以在 HSV 空间分析色度阴影区域饱和度和背景接近可做亮度比例阈值剔除。第二个高频坑是车辆在检测线上停车或者极慢速通过。背景减除的LearningRate偏大时车辆会被逐渐吸收进背景模型导致速度计算只覆盖到车辆进入的前半段计算出的速度偏低。遇到这种情况要调低LearningRate到 0.002 左右并且在轨迹跨第一线后设置一个超时上限比如 5 秒内未跨第二线就判为停车不计入平均速度。第三个高频坑是检测线位置靠近画面边缘。车辆刚进入画面时 blob 面积小、质心不稳定跨线帧号可能提前或滞后好几帧速度误差被放大。检测线应放在画面宽度中后段不要放在画面顶部 20% 或底部 10% 区域。多车道场景还要注意一条检测线只能统计一个方向如果画面里存在双向车流必须给每个方向分别配置一对进出检测线再用质心跳变方向区分。6.3 把算法写进函数文件让 GUI 和脚本结果完全一致最后建议把检测、计数、速度计算都封装成普通函数而不是塞在 GUI 回调里。例如detectVehicles(frame, detectLineY, minBlobArea, frameCount)接收一帧画面和参数返回计数结果和速度数组这样 GUI 里的 timer 和命令行批处理脚本都能调用同一份逻辑。验证时先跑一段视频的批处理版本看控制台输出的数据是否合理再打开 GUI 对同一段视频运行两个结果应该完全一致。如果 GUI 界面展示了不同数据通常不是算法问题而是 timer 回调过程中frameCount和vehicleCount的变量更新顺序错了。调整时应让状态变量全部保存在 App 对象里例如app.frameCount、app.vehicleCount避免回调闭包意外覆盖。本文还有配套的精品资源点击获取