
简介这套基于RCNN的汽车检测项目面向学习深度学习和计算机视觉的MATLAB开发者聚焦利用区域卷积神经网络实现图像中的车辆识别与定位适用于自动驾驶、交通监控等场景的算法实验与课程设计。资源共301个文件以296张汽车图像样本作为主要训练与测试数据另有两个.m源码脚本负责核心逻辑一个.mlapp交互界面便于可视化操作一个.asv自动备份文件一个.mat存放模型或工作区数据压缩包仅74.89MB便于直接运行和二次开发。代码覆盖数据准备与标注、候选区域生成、CNN特征提取、分类与边界框回归、非极大值抑制后处理等RCNN核心流程同时给出超参数调优和使用mAP评估检测效果的实现思路。项目已有138人学习浏览对希望从理论走向MATLAB实战的入门及进阶使用者而言是一份完整且可动手复现的参考项目。1. 基于RCNN的汽车检测先把“基于什么”这件事说清楚汽车检测这个需求从停车场计费到辅助驾驶都能遇到但“基于RCNN”这个说法其实横跨了三代方法。最早的RCNN靠Selective Search提候选区域再逐个送入CNN分类单张图要跑上千次前向Fast RCNN用ROI Pooling把特征共享起来速度上了一个台阶到了Faster RCNNRPN网络直接把候选框生成也交给模型整个流程才第一次真正端到端。现在做项目除非是为了复现论文做对比实验否则起点直接选Faster RCNN是最划算的。这是系列里的训练篇重点解决把模型真正练起来的问题数据怎么准备、配置怎么设、训练时看哪些参数。新手按章节顺序来有经验的人可以直接跳到第三节看参数表。2. 汽车检测数据准备从标注格式到训练/验证划分2.1 按VOC目录结构组织图像与标注Faster RCNN的训练管线通常都接受VOC格式也有不少项目直接转成COCO的JSON标注。两者都能跑我一般建议你统一成VOC因为XML是纯文本出错了肉眼能看而且标注工具导出后再转格式的成本最低。典型的VOC目录长这样data/car/ ├── JPEGImages/ │ ├── 000001.jpg │ └── ... ├── Annotations/ │ ├── 000001.xml │ └── ... └── ImageSets/ └── Main/ ├── train.txt └── val.txtJPEGImages里放原图Annotations里放同名XML每个XML需要包含图片尺寸和每辆车的边界框坐标。ImageSets/Main下维护训练与验证的图片名列表每行一个不带扩展名的文件名。有些框架支持只给JPEGImages和Annotations就能训练但把划分文件也补齐能少踩很多坑。如果你手里的标注来自LabelImg或CVAT导出结果通常已经是VOC XML直接放进去就能用。但如果是CSV、YOLO的txt或者其他自定义格式就需要一个转换脚本。下面这个函数可以把记录着图片路径、框坐标和类别的列表写成合法的VOC XMLimport os import cv2 import xml.etree.ElementTree as ET def save_voc_xml(img_path, boxes, labels, save_dir): # boxes: [[xmin, ymin, xmax, ymax], ...], 坐标是像素整数 h, w cv2.imread(img_path).shape[:2] annotation ET.Element(annotation) ET.SubElement(annotation, filename).text os.path.basename(img_path) size ET.SubElement(annotation, size) ET.SubElement(size, width).text str(w) ET.SubElement(size, height).text str(h) ET.SubElement(size, depth).text 3 for box, label in zip(boxes, labels): obj ET.SubElement(annotation, object) ET.SubElement(obj, name).text label ET.SubElement(obj, difficult).text 0 bndbox ET.SubElement(obj, bndbox) ET.SubElement(bndbox, xmin).text str(int(box[0])) ET.SubElement(bndbox, ymin).text str(int(box[1])) ET.SubElement(bndbox, xmax).text str(int(box[2])) ET.SubElement(bndbox, ymax).text str(int(box[3])) tree ET.ElementTree(annotation) tree.write(os.path.join(save_dir, os.path.basename(img_path).replace(.jpg, .xml)))这里用cv2.imread读图是为了拿到真实宽高。XML里的size字段会被数据加载器用来把坐标映射到模型输入尺寸写错会导致训练时目标位置全偏。坐标直接记录像素值不在XML里做归一化归一化交给训练框架处理。difficult标记为0表示这张图里的目标都参与训练如果你有严重遮挡的汽车样本可以标成1多数框架默认会过滤掉。2.2 按类别分布划分训练集与验证集汽车数据往往来自不同拍摄场景如果直接随机切分可能训练集里全是白天路况验证集里全是夜间最后指标虚高或虚低。我一般先按“同一场景或同一视频片段”分组再在组内划分。很多行车记录仪截帧数据的文件名里带时间戳或摄像头编号这一步通常需要一点人工判断。下面是一个按文件列表划分的脚本适合单场景数据做快速验证import os import random def split_dataset(image_dir, xml_dir, train_ratio0.85, seed42): random.seed(seed) images [f for f in os.listdir(image_dir) if f.endswith(.jpg)] random.shuffle(images) split int(len(images) * train_ratio) os.makedirs(data/car/ImageSets/Main, exist_okTrue) for prefix, files in [(train, images[:split]), (val, images[split:])]: with open(fdata/car/ImageSets/Main/{prefix}.txt, w) as f: for name in files: f.write(os.path.splitext(name)[0] \n) print(ftrain{len(images[:split])}, val{len(images[split:])})固定seed很重要否则每次跑出来的划分都不同训练结果没法对比。train_ratio在样本量上万时可以调到0.9几千张时0.85比较稳。划分完用下面的脚本检查一遍比直接开训省时间。注意带视频连续帧的数据集先按片段分组再切分。否则训练集和验证集会出现同一辆车的连续帧mAP虚高但真实场景掉点。2.3 训练前扫描一遍标注文件最容易让Faster RCNN训练翻车的不是模型配置而是标注文件里有负数坐标、坐标倒置或者根本没有目标。RPN在前几轮会因为这些异常样本产生极端loss后面很难拉回来。import os import xml.etree.ElementTree as ET def scan_annotations(xml_dir): for fname in os.listdir(xml_dir): root ET.parse(os.path.join(xml_dir, fname)).getroot() objs root.findall(object) if not objs: print(f{fname}: 没有目标建议删除该样本或补标) continue w int(root.find(size/width).text) h int(root.find(size/height).text) for obj in objs: box obj.find(bndbox) xmin, xmax int(box.find(xmin).text), int(box.find(xmax).text) ymin, ymax int(box.find(ymin).text), int(box.find(ymax).text) if xmin xmax or ymin ymax: print(f{fname}: 坐标倒置, (xmin, ymin, xmax, ymax)) if xmin 0 or ymin 0 or xmax w or ymax h: print(f{fname}: 坐标超出图像范围, (xmin, ymin, xmax, ymax)) scan_annotations(data/car/Annotations)这个脚本检查三类问题空标注、坐标倒置、坐标越界。如果某一类错误比较集中去标注工具里批量修正别改代码硬扛。Faster RCNN的RPN会基于这些框去采正负样本坐标错了模型学到的目标位置根本说不清。3. 用Faster RCNN在本地跑通汽车检测训练3.1 训练框架与主干网络怎么选选框架和主干是汽车检测项目里最影响后续效率的决策。目前做Faster RCNN训练MMDetection和Detectron2是两种主流选择社区资料多配置驱动改模型不需要动训练循环。我自己常用MMDetection它把RPN、ROIHead、数据增强拆成配置项适合需要反复调参的检测项目。主干网络选ResNet-50还是ResNet-101取决于GPU显存和预期精度。ResNet-50加FPN在8GB显存下能跑起来是预算有限时的默认选项ResNet-101精度高一点但参数量和训练时间多出接近一倍。汽车目标相对结构化ResNet-50足够应付大多数场景。在MMDetection中Faster RCNN的模型配置由backbone、rpn_head、roi_head三大部分构成。拿到官方提供的faster_rcnn_r50_fpn配置文件后第一件事是把类别数改成你的实际类别数。只检测汽车一个类别时修改如下model dict( roi_headdict( bbox_headdict( num_classes1, # 只检测 car不含背景 ) ) )如果检测多类车比如轿车、卡车、大巴num_classes改成实际类别数。注意num_classes不包含背景背景由框架自动处理。改完类别数再看数据集配置。假如你前面把标注统一成了COCO格式数据集部分这样写# 注意MMDetection 不同版本对 classes 字段写法略有差异 # 2.x 在 dataset 里通过 classes 元组指定Detectron2 则在 metadata 里配置 classes (car,) data dict( traindict( classesclasses, ann_filedata/car/annotations/train.json, img_prefixdata/car/train/, ), valdict( classesclasses, ann_filedata/car/annotations/val.json, img_prefixdata/car/val/, ), )classes的顺序影响模型输出的类别索引固定下来后不要中途改动。train和val的ann_file分开指向不同文件避免验证集参与反向传播。如果用了VOC标注格式把dataset_type换成VOCDataset并把路径指向Annotations和ImageSets目录即可。3.2 启动训练的命令与参数含义配置修改完就可以启动训练。以MMDetection为例一条最小命令是这样python tools/train.py configs/faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py \ --work-dir work_dirs/car_detection \ --seed 42--work-dir指定日志和权重输出目录断点默认也从这里恢复。--seed固定随机种子便于复现训练结果。启动后几分钟内日志里会出现类似这样的迭代记录Epoch [1][50/1000] lr: 0.002000 eta: 2:30:00 loss: 1.832 loss_rpn_cls: 0.231 loss_rpn_loc: 0.412 loss_cls: 0.602 loss_bbox: 0.587 grad_norm: 3.21这一行信息量很大。lr是当前学习率前500次迭代会按warmup策略从0慢慢升到目标值。loss是各项损失之和单看数值没有绝对标准关键是看它随迭代是否稳定下降。loss_rpn_cls与loss_rpn_loc来自RPN前者衡量候选框是否包含目标的分类误差后者衡量候选框位置回归误差loss_cls和loss_bbox来自ROIHead衡量最终检测头的分类与回归误差。如果loss_rpn_cls一直不降先检查候选框采样和标注匹配策略如果loss_cls不降再看类别平衡和难例挖掘。grad_norm过大会导致训练震荡常见处理是设置梯度裁剪。3.3 预训练权重与resume的取舍汽车检测训练几乎不会从随机初始化开始。常见做法是加载在COCO上预训练的权重把前面的卷积层当作通用特征提取器只微调后面的检测头。MMDetection的配置文件默认会在首次训练时下载对应backbone的预训练权重网络不稳定时可以预先准备好权重文件再用load_from指定路径python tools/train.py configs/faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py \ --work-dir work_dirs/car_detection \ --load-from /path/to/resnet50_coco_pretrained.pth这里要区分load_from和resume。load_from是加载预训练权重开始新训练resume是从之前的训练断点恢复同时恢复优化器状态、学习率调度和epoch计数。训练中断后续跑用resume别用load_from否则学习率从头开始前期loss会出现奇怪的抖动。注意初学阶段先确认日志里各项loss在下降再考虑调参数。不要一上来就动rpn_nms_threshold这类候选框设置。4. 汽车检测训练中必看的指标与关键参数调整4.1 从损失曲线判断训练是否正常训练跑起来后不要只盯着总loss分开看RPN和ROIHead各自的损失更有效。我习惯在日志里固定输出四项loss_rpn_cls、loss_rpn_loc、loss_cls、loss_bbox然后借助TensorBoard或简单脚本每隔100个iteration打印均值。正常训练中前几百次迭代四项损失都会快速下降尤其loss_rpn_cls会从2左右降到0.3以下这说明RPN开始能区分背景和汽车区域。如果loss_rpn_loc降到0.1以下依然检测不准问题多半出在回归目标编码或者框匹配阈值上不是网络容量不够。另一个常见现象是loss_cls初期下降明显、后期长期横盘这通常是分类任务本身简单横盘不代表不收敛要结合mAP判断。Faster RCNN汽车检测的收敛节奏大约在epoch 6到7进入loss平台期。如果按step调度把学习率降一档loss还能继续下行一段检测框的位置精度会有肉眼可见的提升。如果loss大幅逼近0反而要警惕验证集泄漏或标注重复。4.2 影响汽车检测效果的关键参数与推荐值下面列出我在调汽车检测模型时最常改的参数及调整方向。不同数据集和框架版本有差异但规律基本一致参数常见默认值汽车场景调整建议风险点base_lr0.01数据量小时用0.005过大导致loss震荡batch_size2/卡显存不足时降到1减小后应同步降lrwarmup_iters500数据量大可提到1000太长会拖慢收敛lr_step[8, 11]小数据集提前到[6, 9]降太晚loss停在平台期rpn_nms_threshold0.7车辆密集场景调0.6太高候选框重叠严重score_threshold0.05推理时调到0.3~0.5影响召回率base_lr和batch_size之间近似线性关系batch size减半lr最好也减半。warmup_iters让模型在前几百步稳住参数尤其加载预训练权重时过大的初始lr容易破坏主干里已经学好的特征。rpn_nms_threshold控制候选框冗余度高速上车辆密集时从0.7降到0.6召回率通常会好转代价是训练时间变长。4.3 汽车目标训练失败的常见症状与排查顺序第一个症状是loss不降或反升。优先查标注是否越界、类别名是否大小写不一致、lr是否过大。汽车检测数据集里经常混入路牌、行人等错误标注这些样本会让分类损失一直压不下去先跑一遍2.3的扫描脚本。第二个症状是显存溢出。车载相机拍出的图片分辨率高原图直接进模型容易爆显存。常见做法是在数据加载配置里把img_scale缩小例如从1333x800缩到1000x600。分辨率降低对远处小车的检测影响明显只建议在显存受限时使用。第三个症状是远处小汽车完全检不到。原因是小目标在高层特征图上占的像素少RPN的anchor尺寸覆盖不够。常见做法是打开FPN并给rpn_head的anchor_generator增加小尺寸基础边长比如加入(8, 16)。汽车检测里小目标问题远比类别混淆问题常见调anchor比调loss权重更直接。5. 让汽车检测训练更快的三个实用技巧5.1 冻结主干先让RPN和检测头收敛汽车数据集如果只有几千张全量微调ResNet-50既慢又容易过拟合。我一般先冻结backbone的前面几个stage只训练后面层和RPN、ROIHead。在MMDetection的backbone配置里通过frozen_stages控制model dict( backbonedict( typeResNet, frozen_stages2, # 冻结 stem 和 layer1这两部分不再产生梯度 ), )frozen_stages的数值表示冻结到第几个阶段2代表前两个阶段的参数彻底不参与反向传播。这样节省的显存和计算时间在数据量不大时非常可观。等到RPN能稳定框出汽车再把frozen_stages改成0解冻全部层用较小学习率微调一轮。5.2 开混合精度训练只要显卡支持Tensor CoreAMP基本是零成本收益。MMDetection训练命令加一个--amp选项即可python tools/train.py configs/faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py \ --work-dir work_dirs/car_detection_amp \ --amp开启后显存占用大约减少三成训练速度提升明显。如果训练出现NaN不要立刻关掉AMP先看是不是loss scale出了问题常见处理是调小初始scale或确认动态loss scale已经开启。在汽车检测这种batch size不大的场景AMP对最终精度影响很小值得常开。5.3 用梯度累积等价扩大batch size显存只够跑batch size为2时常见做法是用梯度累积把有效batch size做大。自己写训练循环时核心代码是accumulation_steps 4 optimizer.zero_grad() for i, (images, targets) in enumerate(loader): loss model(images, targets) / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()每个step的loss除以累计步数再反传等效于把batch size从2扩成8。注意BN层的统计量仍按当前mini-batch计算梯度累积不能解决BN在极小batch下的统计偏差。如果模型里BN占大头不如减小输入分辨率换取真batch size。本文还有配套的精品资源点击获取