如果你手头刚好有一批道路视频帧,任务又一句话:在Matlab里完成汽车目标检测的训练和测试,第一反应大概率是翻文档找现成工具箱。但真实情况往往是数据没标注、网络不知道用哪个、跑完训练又看不懂loss曲线。我在一个交通场景模拟项目X里,用Matlab把Fast R-CNN的汽车目标检测完整跑了一遍,从图片整理、标注、网络组装到训练调参、测试部署都踩过坑。这篇博文就讲实际操作和文档里不写的那些细节,适合想快速在Matlab上落地目标检测的人,也适合准备对比传统方法做科研验证的开发者。
1. 汽车检测任务的整体链路:为什么是Fast R-CNN
1.1 需求拆解:这个"汽车目标检测"到底要检测什么
动手之前,我习惯先不碰网络,而是把"车辆检测"拆成三个问题:场景里有什么车、车在哪、有多大把握。模拟项目X的需求是从一组路口监控视频里定位出所有车辆矩形框,只关心"车"一个类别,不区分小轿车、SUV、卡车,也不需要车牌识别。这种需求在园区车流统计、停车场辅助管理、安防抽查里非常常见,和学术竞赛里的通用目标检测并不一样。竞赛要求对多类别、多尺度、密集小目标都有泛化能力,而单类车辆检测只需要把"前景车辆"和"道路背景"尽量分清。
拆完需求后有两点明显感觉:第一,分类压力不重,只有一个类别;第二,定位压力不小,车辆有正面、侧面、俯视、斜视各种形态,还有遮挡和光照变化。这两点直接影响后续所有选择——数据要覆盖什么类型的车、候选框生成怎么配、检测阈值怎么设。也就是说,选型不是先定网络再套需求,而是顺着需求逆向找一个"边界内最优"方案。
1.2 两阶段与单阶段之争:Fast R-CNN的优势不在速度
做方案评估时,业内常见的声音是"检测就该上YOLO,快"。我也用过YOLO,但在Matlab环境里做单类车辆检测,权衡之后仍然选了Fast R-CNN。原因是三个词:训练稳定、精度出众、链路清晰。
从目标检测流派看,R-CNN把检测拆成"候选区域+分类",每个候选框都要单独过一遍卷积网络,速度慢得离谱。Fast R-CNN做了关键改进——整张图只做一次卷积,把约2000个候选区域映射到特征图上,再用RoI池化拉成固定长度特征去做分类和回归。和R-CNN比,它省去了逐框过网络的巨大开销;和Faster R-CNN比,它用Selective Search生成候选区域,少了RPN网络这个需要联合优化的模块,训练整体更简单;和YOLO、SSD这类单阶段模型比,它的候选区域质量更高,对重叠目标和遮挡目标的误检更少。
我当时用同一批900张监控图片做了方案对比,直观感受如下:
| 方案 | 候选区域生成 | 推理速度 | 训练稳定性 | Matlab封装 |
|---|---|---|---|---|
| R-CNN | Selective Search,每框过网络 | 很慢,数十秒/张 | 流程繁琐 | 无现成函数 |
| Fast R-CNN | Selective Search,全图一次卷积 | 中等,GPU约几帧/秒 | 稳,单阶段优化 | 有现成封装 |
| Faster R-CNN | RPN网络生成 | 较快 | 需要联合训练RPN | 有现成封装 |
| YOLO v2/v3 | 网格直接回归 | 快,可实时 | 对超参敏感 | 有封装但经验少 |
单看速度,Fast R-CNN确实不如单阶段模型,丢到CPU上基本没法实时。但在模拟项目X里,验收标准不是实时,而是"离线批处理、准确率优先"。Fast R-CNN在候选框质量上赢得很明显,误检比单阶段模型克制,重叠车辆漏检也更少。选型就是这样,不是越新越快越好,而是在约束条件下挑一个综合指标最优的。
1.3 Fast R-CNN的技术边界:什么时候该换方案
把Fast R-CNN全流程跑通之后,有几个场景会非常明确地暴露出它的短板。第一个是实时视频分析,比如25帧每秒的卡口视频,Fast R-CNN在GPU上一般只能做到每秒几帧,CPU上每秒零点几帧都吃力;第二个是嵌入式端部署,显存和内存都紧张,候选框生成加RoI池化太重;第三个是密集小车流场景,候选框一多,NMS之后框与框之间还会挤压。遇到这三类需求,我建议直接考虑Faster R-CNN或者单阶段模型。
Fast R-CNN的最大价值,在于用可接受的速度换取可靠的精度,同时链路简单、可解释性强。它的角色有点像"基准方案"——先跑通它,理解每个环节,再考虑要不要换更快的模型。很多人一上来就挑战Faster R-CNN,被RPN训练搞得焦头烂额,反而不如从Fast R-CNN入手建立完整认知。
2. 训练数据的地基工程:图片整理、标注与切分
2.1 原始视频抽帧与清洗策略
一个检测项目最容易翻车的环节不是训练,而是数据。Fast R-CNN对数据质量相当敏感,如果某张图标注框歪了,损失曲线照样会下降,但测试AP会莫名其妙上不去。我当时从一段约40分钟的路口视频里抽帧,每隔10秒抽一帧,得到240帧,又补充了一批公开车辆数据集中的图片,最后整理成900张。抽帧这件事看着简单,其实很讲究:每隔固定秒数抽,容易把同一辆车连续出现在多帧里,相邻帧几乎一样;更好的做法是抽帧后按画面内容聚类,不同位次的车辆分布取样。
清洗也是不可省的一步。我会手动删掉严重模糊、大面积过曝、主要目标被杆子或路灯完全挡死的图。别心疼那几张图,脏数据对检测器的伤害远大于少几张干净图。
清洗之后我还会做一次分布统计:白天多少张、黄昏多少张、正面多少张、侧面多少张、大车小车比例、遮挡样本占比。统计的意义是防止训练集和测试集"长相差太远"。比如训练集全是白天大晴天的大车,测试集变成傍晚小雨的小轿车,AP断崖式下跌几乎是必然的。车辆检测不一定需要一开始就追求神级数据集,但至少要保证视角和光照有层次。
2.2 标注工具与标注规范:肉眼偏差就是模型天花板
Matlab自带的Ground Truth Labeler是视频标注利器,比Image Labeler更适合处理连续帧,因为可以直接在视频时间轴上打框并自动插值。但我更想强调的是标注规范本身。我定的规则只有三条:
- 矩形框必须贴合车身边缘,留少量余量,但不能把绿化带、路面、阴影裁进框里。
- 类别名统一叫vehicle,不搞car、truck、bus多类别命名,因为当前任务只关心"是不是车"。
- 被遮挡超过三分之一的车辆不标;如果它是画面里的主要目标,就补做一次增强后再标。
这三条规则看起来很朴素,却省了大量返工。多人标注时,不同人对"紧贴边缘"的理解不一致,所以标注前我会先标10张图作为范例,统一标准。
标注完成后不要急着训练,先用objectDetectorTrainingData把groundTruth对象转成训练用的table格式。这个表的结构很关键,一列是图像路径,另一列是cell数组,每个元素包含若干bbox和对应标签。这一步之后一定要做检查:抽样20张图,把所有标注框画出来看位置偏差;统计平均每图框数和漏标框数。如果一张图有8辆车却只标了1辆,训练出来的模型大概率漏检,而且这种漏检很隐蔽——loss照降,可视化检测结果时才暴露。
2.3 训练集、验证集、测试集的切分与坐标同步
切分数据时最容易犯的错误是直接随机打乱所有图片,然后发现验证集里出现了训练集同一段监控的相邻帧,目标几乎重叠,验证指标虚高。正确做法是按场景片段整体划分。模拟项目X的视频来自不同时段、不同路口,我就按连续片段粗分成7:2:1,保证训练集和测试集没有连续帧泄漏。
切分完就是图像尺寸和坐标问题。Fast R-CNN对输入尺寸有要求,4K原图直接扔进去,显存很容易爆。我的做法是统一把最短边缩放到600像素,最长边不超过1000像素,超出部分等比缩放。这里的核心关键点是:box坐标必须跟着同一个缩放系数换算。举个例子,图宽1920高1080,宽度缩到960,那么x坐标和框宽都要除以2,高同理。这个换算在训练脚本里做一次,存成新的图片和新的标注表,比训练过程中让工具箱每次解析原图再resize更可控、更不容易错。
数据增强方面,我只用了左右翻转、小范围亮度扰动和随机裁剪。注意翻转后box坐标变换是xNew = imageWidth - x - width,宽度不变;随机裁剪后要裁剪掉超出边界的框。数据增强能提升模型对光照和背景的鲁棒性,但不能无中生有造出新车型。我在增强后重新统计过框数量,发现有效框增加约40%,但多样性提升有限,真正撑起AP的还是标注质量和场景覆盖。
3. Fast R-CNN网络在Matlab中的组装与配置
3.1 把Fast R-CNN工作原理落到Matlab函数层面
要稳定复现Fast R-CNN,至少要清楚它在工具箱里对应的四个阶段。第一阶段是候选区域生成,默认用Selective Search,Matlab里可以通过名值对控制候选框数量,典型取值1000到2000。第二阶段是全图特征提取,本质是一个预训练卷积网络在自然图像上学到的特征复用到车辆检测。第三阶段是RoI池化,把每个候选框从特征图上对应位置抠出来,池化成固定大小,这是Fast R-CNN的核心算子。第四阶段是输出头,一个分支做类别判定,一个分支做边框精修回归。
在Matlab里这些头不用手搭,trainFastRCNNObjectDetector会自动把预训练分类网络末尾的分类层替换成检测头。你需要准备的只有训练数据表、基网络和训练参数。这个封装度给开发带来很大便利,但也有代价:如果不理解层内部结构,训练异常时排查会很吃力。第一次调用时建议先打印一下网络的Layers,看看最后一层被替换成了什么;再手动确认输入图像尺寸和输出类别数,心里有底再做训练。
3.2 预训练模型选择:ResNet-50不是万能但很稳
基网络选择直接决定训练时间和显存占用。我在三个预训练网络之间做过对比:ResNet-50、GoogLeNet和MobileNet-v2。ResNet-50的深层残差结构对车辆这种语义层次丰富的目标很友好,训练效果最好;GoogLeNet轻一些,参数少,训练快;MobileNet-v2在Matlab里的推理效率和部署兼容性都不错,更适合后续放到低算力设备。
以900张图、20个epoch为例,我的经验感受如下:
| 基网络 | 大致显存占用 | 单epoch训练耗时 | 最终检测效果 |
|---|---|---|---|
| ResNet-50 | 8-10 GB | 3-5分钟 | 最好 |
| GoogLeNet | 4-6 GB | 2-3分钟 | 中等 |
| MobileNet-v2 | 3-4 GB | 1-2分钟 | 中等偏弱 |
如果显卡显存只有6GB左右,我建议先用GoogLeNet跑通全流程,再考虑换ResNet-50提升精度。Matlab首次加载预训练网络会联网下载权重,确保网络环境能访问官方服务器,下载一次后本地会缓存,后续离线也能用。这个点经常把第一次跑的人卡住,实际上只是网络下载问题,不是代码问题。
3.3 一个可以直接跑通的训练脚本框架
把前面所有内容合起来,就是一个简洁的训练脚本。下面这段框架在模拟项目X里实际用过,细节按自己数据替换:
% 1. 加载训练数据表 trainData = objectDetectorTrainingData(gTruthTrain); % 2. 加载预训练网络 baseNet = resnet50; % 如果直接传网络给trainFastRCNNObjectDetector, % 函数会自动替换末尾分类层并添加检测头 % 3. 训练参数 options = trainingOptions('sgdm', ... 'InitialLearnRate', 1e-3, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.1, ... 'LearnRateDropPeriod', 10, ... 'MaxEpochs', 20, ... 'MiniBatchSize', 16, ... 'Shuffle', 'every-epoch', ... 'Verbose', true, ... 'Plots', 'training-progress', ... 'ValidationData', valData, ... 'ValidationFrequency', 30); % 4. 训练检测器 detector = trainFastRCNNObjectDetector(trainData, baseNet, options, ... 'NumRegionProposals', 2000);有几点容易踩:如果你手动修改了层图,就必须保证最后的层结构是"全连接层+softmax+分类层"的标准形态;如果只是想快速跑通,直接把预训练网络名称传给训练函数,让它自己改层。ValidationData格式必须和trainData完全一致,否则报错发生在训练中段,白白浪费大半天。MiniBatchSize不是越大越好,它越大梯度越稳但显存压力越大,我实测16在6GB卡上稳定训练,32就容易OOM。
4. 训练过程中的参数调优与收敛经验
4.1 训练options设计思路:先定学习率再谈效果
训练options是很多人"抄作业"的部分,但每个参数都有它该这么设的理由。Fast R-CNN用预训练权重初始化,网络已经处在一个不太深的局部盆地附近,直接给1e-2学习率很容易震荡发散,给1e-4又收敛太慢。我一般初始给1e-3,用piecewise在10个epoch后降为1e-4,后续看loss是否进入平台期再决定是否继续降。
Batch size的选择主要看显存,而不是"越大越好"。batch越大梯度更稳定,但爆显存就只能减小。如果减batch后loss震荡,就同步把学习率调低一点,两者是联动关系。MaxEpochs我建议在15到25之间。900张图、2000个候选框,20个epoch基本已经稳定收敛;如果20个epoch后验证AP还在缓慢爬升,可以续训。续训的方法是取出当前detector.Network作为下一轮训练的初始网络,而不是从头再跑一遍,能省大量时间。
训练过程一定要开着可视化,把Plots设成training-progress,边跑边观察loss和验证集指标。有一次我发现loss在某个epoch突然跳高,原因是学习率调度节点正好卡在数据shuffle的重组点上,梯度方向突变导致的尖刺,通常下一轮会回稳。知道这种形态存在,就不至于误判为训练失败。
4.2 让人头疼的四个训练"事故"及对策
第一,loss曲线发散或直接NaN。这个遇到两次,一次是学习率太高,一次是标签框里有坐标小于0的数据。解决方法是把InitialLearnRate降到3e-4以下,同时检查table里的bbox是否全为正数且宽高不为0。第二,显存不足。这是单类检测项目最常见的抱怨,可以按顺序尝试:减小MiniBatchSize、减小输入图像缩放尺寸、换轻量基网络、把NumRegionProposals从2000降到1000。候选框数量减半后显存压力明显下降,代价是召回率可能会小幅下降。
第三,训练完loss很低,但测试画面出现大量重叠框。这多半是NMS阈值和Score阈值不匹配,不是网络训练失败。后面测试阶段再调,先把检测框画出来看看,再用阈值过滤。第四,验证AP一直偏低。这个要回数据查:是不是数据分割泄漏?标注框是不是参差不齐?是不是训练集和验证集的光照分布差异太大?AP低不是超参问题,基本都是数据问题。
排查这些事故时,我习惯先做"冒烟测试":拿30张图、2个epoch跑一遍,确认全流程能通且loss明显下降,再扩大数据量和epoch。这个思路特别适合第一次在Matlab里跑深度学习的人,不用一上来就吃满全部数据,否则一个低级错误要等好几个小时才能暴露。
4.3 验证集监控与模型保存策略
训练过程中,我会同时盯两条信息:training-progress图里的训练损失,以及ValidationData上的平均精度AP。损失下降只说明模型在优化,但对没见过的图是否有效,只能看验证集。ValidationFrequency我设为30,每30个迭代验证一次。这个值太小会拖慢训练,太大又监控不到模型拐点。AP明显上涨的那一两轮,就是模型捡到宝的信号,应该在那轮附近保存权重。
Matlab训练结束后的detector对象本身就是模型,不需要像其他框架那样翻权重文件。但如果要做参数对比,原始detector会被覆盖,所以训练完我建议立刻保存:save('detector_resnet50_lr1e-3.mat', 'detector', 'options'),文件名带上关键超参。我会给每轮实验命名成"ResNet50_lr1e-3_batch16_k2000_v2"这种格式,后面回溯时一眼能看出是哪组参数。多人协作、多轮迭代时,这个习惯能省掉大量重复训练的时间。
5. 测试评估与应用落地
5.1 用detect函数做测试集验证与阈值选择
训练一结束,首先要面对的不是"模型好不好"这种抽象判断,而是"阈值设多少"这个具体操作。直接上默认阈值等于不做判断。我写的测试脚本会固定几组阈值——0.5、0.7、0.8——然后对测试集抽样图片各画一次检测框,人工翻看。阈值越高漏检越多,阈值越低误检越多。业务上偏"宁可错抓不可漏抓",阈值就放到0.5以下;如果要求"抓到的都是真车",阈值放到0.85以上。阈值没有标准答案,本质是业务容忍度的调参操作。
简单测试核心代码如下:
im = imread(testImgList{1}); [bboxes, scores, labels] = detect(detector, im, 'Threshold', 0.7); annotated = insertObjectAnnotation(im, 'Rectangle', bboxes, ... cellstr(labels), 'Color', 'green', 'LineWidth', 2); figure; imshow(annotated);detect内部已经做了NMS,但仍可能残留少量半重叠的重复框。我会再补一次处理:对IoU大于0.5的两个框,分数接近就保留高分,分数差距大也保留高分并丢掉低分框。这个逻辑不复杂,但在连续视频帧里能显著降低车辆计数的误差。
5.2 用AP评估模型与Bad Case定位
光看抽样图不能说明模型好坏,还要量化。单类检测最常用指标是IoU=0.5下的平均精确率AP,类别只有一个时AP和mAP等价。用Matlab自带的evaluateObjectDetection函数,传入groundTruth测试对象和检测结果,它会算出一系列指标和PR曲线。模拟项目X的模型在测试集上AP约0.92,表面不错,但看PR曲线右端能发现召回率升高时精确率掉得很快,说明模型对某些难样本会滥发框。
接下来就是定位难样本:把精确率跌落的召回率区间对应的图片翻出来,统计共同点。结果发现大部分是傍晚光线、车身颜色和路面颜色接近的图。这类case靠调阈值和NMS解决不了,只能回到数据环节补充傍晚样本,或者做直方图均衡化等预处理后重新训练。评估不是交差,而是为下一步迭代指方向。每次评估完都应该产出一份"下一轮要补什么数据"的清单,连续几轮之后模型会越调越准。
5.3 从检测器到可交付产物:视频批量检测与部署
项目X验收时需要输出一段完整检测视频。用Matlab做批量检测很直接:VideoReader逐帧读入,每帧调用detect,再把结果用insertObjectAnnotation画上去,最后用VideoWriter写回。帧率不用太指望,Fast R-CNN本就属于离线批处理模型,一分钟的素材大概要跑小十分钟,但验收场景不要求实时,慢就慢点。
vr = VideoReader('traffic_clip.mp4'); vw = VideoWriter('traffic_result.mp4'); open(vw); while hasFrame(vr) frame = readFrame(vr); [bboxes, scores, ~] = detect(detector, frame, 'Threshold', 0.75); outFrame = insertObjectAnnotation(frame, 'rectangle', bboxes, ... "car", 'Color', 'green'); writeVideo(vw, outFrame); end close(vw);如果要进一步打包成可执行文件,Matlab Compiler可以把预测脚本打包成独立应用,部署在装有MATLAB Runtime的机器上;如果想要跨语言调用,可以尝试导出ONNX模型,再到目标平台用对应推理引擎加载。注意,Fast R-CNN这类带自定义层的网络在不同版本对ONNX导出的支持度不一样,导之前先查一下版本说明。总体来说,对这个项目,在Matlab内部完成交付已经足够,不必强行切到别的技术生态。
最后分享一下个人体会。整个项目最花时间的不是训练,而是把数据整理到"训练接得住、测试可追溯"的状态。标注规则、坐标缩放、分布统计,这些琐碎步骤决定了后续所有环节的顺利程度。训练出问题先怀疑超参数,但超参数排查没问题时,一定要回头查数据和网络输入,不要在一个方向上死磕。很多人说"Fast R-CNN在Matlab里跑不动",多半是因为学习率太大、显存不够或者标注没对齐,而不是方法本身不行。如果你正准备在Matlab上做车辆检测,按这条链路走一遍,基本能一次把训练到测试跑通。