简介:Faster R-CNN 目标检测源码资源包是一份面向计算机视觉学习者和算法复现者的完整工程,针对目标检测中的候选区域生成、特征提取与分类回归等核心问题,提供了基于 PyTorch 的可运行实现,也适合作为毕业论文或科研项目的参考基线。压缩包共含 42 个文件,以 31 个 Python 脚本为主体,另含备份文件、环境/依赖配置文件和 README 说明文档,整体体积仅 137KB。已有 134 人学习浏览,说明其在目标检测方向具有一定参考价值。资源内部不仅实现了 Faster R-CNN 主框架、RPN 区域提议网络、ROI Pooling 与分类回归层,还集成 ResNet50、MobileNetV2、VGG 等多种骨干网络和 FPN 特征金字塔,并附带训练、多 GPU 训练、验证、预测、mAP 曲线绘制、COCO 评估及数据划分等工具脚本,便于从环境搭建到自定义数据集训练全流程实践。对于想深入理解目标检测原理或快速启动相关实验的开发者,这份小型源码包能节省大量整理时间。
1. Faster-RCNN目标检测代码:为什么两阶段模型仍然值得细读
Faster-RCNN目标检测代码 是目标检测领域最值得拆开读的一类工程实现。相比 YOLO 把候选框回归和类别判断合在同一个前向里,Faster R-CNN 把“在哪找目标”和“目标是什么”拆成两个阶段,结构上更接近模块化设计:backbone 负责提特征,RPN 负责给候选框,RoI Head 负责精分类与坐标回归。也正因为这样,小目标漏检、长尾类别召回低、换主干网络这些需求,在代码里都有明确的落点,改起来比一体化模型更容易定位。下文按日常调试顺序来写:先建立模型代码坐标系,再跑通推理,接着进训练循环,最后落到验证与排错。适合刚跑熟 YOLO、想对比两阶段差异的读者,也适合已经在用 Faster R-CNN,但还没把 anchor、RoI align 这些概念对应到具体代码行的工程师。文中涉及的可执行示例均以 torchvision 的实现为基础,这也是目前最主流的二次开发起点。
2. 看懂Faster-RCNN代码的模型骨架:backbone、RPN与RoI Head
2.1 用模型打印建立全图坐标
很多人开始改代码时,会对着论文里的结构图去读代码,效率不高。torchvision 版的 Faster R-CNN 虽然是一个 nn.Module,但 forward 里同时处理了训练和推理两条路径,看打印结果比看结构图更直接。我一般先执行三行代码:
import torchvision from torchvision.models.detection import fasterrcnn_resnet50_fpn model = fasterrcnn_resnet50_fpn(weights=None, num_classes=91) model.eval() for name, module in model.named_children(): print(name, type(module).__name__)输出从上到下是backbone、rpn、roi_heads、transform,前三个是模块,第四个负责输入预处理和坐标还原。BackboneWithFPN输出多尺度特征字典,在后续代码里统一叫features,key 是字符串"0"到"4";rpn吃掉特征字典后给出一批proposals,roi_heads再根据 proposal 从特征字典中抠出对应区域做分类和回归。理解这份代码不能按nn.Sequential的层序去读,要按数据流读。
forward的入口是transform,它做归一化、resize 并记录缩放比例,训练分支还会在这里把 GT 的 boxes 坐标同步缩放。代码里有一个非常隐蔽的分支:训练时传入model(images, targets)才有 loss 返回,不传targets会静默进入推理分支。如果写训练循环时把 targets 拼错 key,loss 不会报错但一直不更新,这类问题靠看日志里的 loss 数值变化才能发现。
2.2 锚点生成与分配:RPN 代码里最容易被改坏的参数
锚点不需要手动提供,代码里自动生成,这是 Faster R-CNN 和传统 proposal 方法最大的差异之一。下面几行能快速看到默认配置:
rpn = model.rpn print(rpn.anchor_generator.sizes) print(rpn.anchor_generator.aspect_ratios) for name, mod in rpn.head.named_children(): print(name, mod)输出分别是五层锚点尺寸(32, 64, 128, 256, 512)和宽高比(0.5, 1.0, 2.0),head 里有rpn_conv、objectness、pred_bbox_deltas三个卷积。objectness的卷积核数量是 3,对应每个位置的 3 种宽高比;pred_bbox_deltas的卷积核数量是 12,含义是3 种锚点 × 4 个回归量。这里最容易陷入的误判是,只有 600 像素的输入分辨率下仍保留 512 这个大锚点,它已经超出有效感受野,训练阶段 RPN 会被迫匹配这个大尺度目标,loss 不降或小目标漏检经常源于此。先确认输入min_size,再决定要不要改 sizes 参数。
RPN 阶段的正负样本不是直接算 IoU 就完事,而是走 Matcher:
| 匹配规则 | 默认阈值 | 训练中的作用 |
|---|---|---|
| 锚点与 GT 的 IoU 大于 0.7 | 正样本 | 参与 objectness 前景二分类 |
| IoU 小于 0.3 | 负样本 | 参与背景二分类 |
| IoU 介于 0.3 与 0.7 | ignore | 不计 loss,不参与参数更新 |
allow_low_quality_matches=True | 保留每组最高 IoU 锚点 | 防止某个 GT 完全没有正样本锚点 |
调参时优先看rpn_positive_fraction,默认 0.5。如果训练日志里正样本比例一直偏低,问题多半在锚点生成尺度,而不是 IoU 阈值上。
2.3 RoI Head 的代码路径:从 proposals 到分类回归
RoI Head 这一段的代码密度最高,我同样建议在改代码前先敲一遍:
roi = model.roi_heads print(roi.box_roi_pool) # MultiScaleRoIAlign(output_size=(7, 7), sampling_ratio=2) print(roi.box_head) # TwoMLPHead(in_channels=12544, hidden_layer=1024) print(roi.box_predictor) # FastRCNNPredictor(in_channels=1024, num_classes=91)box_roi_pool就是常说的 RoIAlign,output_size=7表示任意尺寸的 proposal 都会采样成 7×7 特征。sampling_ratio=2是每个 bin 的采样点数,改成 1 能省一点耗时,但小目标回归精度会掉。TwoMLPHead的in_channels是256×7×7,256 来自 FPN 的通道数,所以换 backbone 时只要输出的通道数不是 256,这一层就需要一起改。
两阶段在这里体现得最明显:第一阶段用自己的定位 loss 训练 RPN,第二阶段在裁剪后的特征上重新做 IoU 匹配,默认阈值是 0.5,和 RPN 阶段的 0.7 是两套监督。第一次读代码的人经常把这两个阈值当成同一个,改 RPN 阶段阈值后第二阶段结果完全没有变化,原因就在这里。
3. 用Faster-RCNN目标检测代码跑推理:权重加载与输出解析
3.1 最小推理代码和输出格式
Faster-RCNN 的推理输出不是 tensor,而是 dict,坐标也是原图坐标不是归一化结果。下面这段是能在 10 行内跑通的最小示例:
from PIL import Image import torch from torchvision.models.detection import ( fasterrcnn_resnet50_fpn, FasterRCNN_ResNet50_FPN_Weights, ) from torchvision import transforms as T weights = FasterRCNN_ResNet50_FPN_Weights.COCO_V1 model = fasterrcnn_resnet50_fpn(weights=weights) model.eval() device = "cuda" if torch.cuda.is_available() else "cpu" model.to(device) x = T.ToTensor()(Image.open("street.jpg").convert("RGB")).unsqueeze(0).to(device) with torch.no_grad(): pred = model(x)[0] print(pred.keys()) # dict_keys(['boxes', 'labels', 'scores'])boxes形状是(N, 4),四个值为[x1, y1, x2, y2],坐标系是原始图片像素位置,不是 0 到 1 的归一化区间。这是因为GeneralizedRCNNTransform在postprocess阶段把预测坐标乘回了缩放比例。如果你自己提前把图片 resize 过再喂进网络,后续所有面积过滤和可视化都必须回到原图尺寸做。
labels是 int64 类别索引,不是 one-hot 向量;scores是对应类别的置信度。细节上,COCO 预训练模型把背景当作 label 0 保留,但推理输出里不会出现 0,因为背景结果会被丢掉。业务数据如果类别编号从 0 开始,转到 COCO 评测格式时整体要加 1。
3.2 三个后处理参数:score_thresh、nms_thresh、detections_per_img
默认后处理参数是为测评准备的,不是为线上部署准备的。创建模型时可以这样重写:
model = fasterrcnn_resnet50_fpn( weights=FasterRCNN_ResNet50_FPN_Weights.COCO_V1, min_size=640, max_size=1200, box_score_thresh=0.35, box_nms_thresh=0.5, box_detections_per_img=300, )| 参数 | torchvision 默认值 | 作用 | 线上常用调整 |
|---|---|---|---|
box_score_thresh | 0.05 | 得分低于此值的框被直接丢弃 | 调到 0.4~0.5,假正例明显变少 |
box_nms_thresh | 0.5 | 同类高度重叠框做 NMS 的 IoU 阈值 | 密集堆叠场景降到 0.3 |
box_detections_per_img | 300 | 每张图最多返回的检测框数 | 小目标密集任务调到 500 |
min_size/max_size | 800 / 1333 | 输入短边和长边的缩放目标 | 按业务分辨率适当缩小以提速 |
min_size不是目标最小尺寸,是图像短边的缩放目标;max_size是长边的上限。这两个值对推理耗时的影响比模型结构更直接,把max_size从 1333 降到 1024,耗时常能降到六成,AP 通常只掉零点几个点。box_score_thresh默认很低,只有 0.05,所以 COCO 权重刚加载直接跑会输出大量低分框,看起来模型不行,实际只是阈值没提上来。
3.3 把模型输出解析成业务协议
业务对接时最常用的是 COCO 风格[x, y, width, height]。转换代码建议这样写:
def pred_to_coco(pred, img_w, img_h): out = [] boxes = pred["boxes"].detach().cpu().numpy() labels = pred["labels"].detach().cpu().numpy() scores = pred["scores"].detach().cpu().numpy() for box, label, score in zip(boxes, labels, scores): x1, y1, x2, y2 = box x1 = min(max(x1, 0), img_w) y1 = min(max(y1, 0), img_h) x2 = min(max(x2, 0), img_w) y2 = min(max(y2, 0), img_h) if x2 <= x1 or y2 <= y1: continue out.append({ "category_id": int(label), "score": round(float(score), 4), "bbox": [round(x1, 1), round(y1, 1), round(x2 - x1, 1), round(y2 - y1, 1)], }) return outFPN 输出的框偶尔会在图像边缘产生负坐标或超出宽高的坐标,这段代码做了边界截断。另一个容易犯的错是把x2 - x1写反成x1,这类问题在评测时不会立刻报错,但 mAP 会低得莫名其妙。
4. 训练Faster-RCNN代码:数据集封装、损失与超参数
4.1 自定义数据集和 collate_fn 的写法
Faster R-CNN 的 DataLoader 和普通分类任务不太一样,它需要的数据结构是List[dict],而不是堆好的张量。每个 target 里必须有boxes和labels两个键:
from torch.utils.data import Dataset class DetectionDataset(Dataset): def __init__(self, records): self.records = records def __len__(self): return len(self.records) def __getitem__(self, idx): rec = self.records[idx] # rec 含字段: image(BGR ndarray), boxes(list of XYXY), labels image = rec["image"][:, :, ::-1].copy() # BGR -> RGB boxes = torch.as_tensor(rec["boxes"], dtype=torch.float32) labels = torch.as_tensor(rec["labels"], dtype=torch.int64) return image, {"boxes": boxes, "labels": labels} def collate_fn(batch): return [item[0] for item in batch], [item[1] for item in batch]boxes必须是 XYXY 格式,因为内部在算 IoU 时按这个假设写。labels从 1 开始,0 被 torchvision 保留给背景,如果源数据集的类别从 0 编号,要整体加 1。
collate_fn返回的是两个 list 而不是 torch.stack 的结果。原因是 batch 内图片尺寸不一定相同,torchvision 的GeneralizedRCNNTransform会在 forward 内部统一缩放,提前 stack 会直接报形状不一致。反过来,如果数据已经裁成固定尺寸,用 stack 也能跑,但一旦之后加入随机裁剪就会现原形,所以统一用宽松写法最省事。
4.2 训练主循环:返回的是 loss 字典
一个能正常收敛的训练循环骨架如下:
from torch.optim import SGD from torch.optim.lr_scheduler import StepLR params = [p for p in model.parameters() if p.requires_grad] optimizer = SGD(params, lr=0.005, momentum=0.9, weight_decay=0.0005) lr_scheduler = StepLR(optimizer, step_size=3, gamma=0.1) for epoch in range(epochs): model.train() for images, targets in dataloader: images = [image.to(device) for image in images] targets = [{k: v.to(device) for k, v in t.items()} for t in targets] loss_dict = model(images, targets) losses = sum(loss for loss in loss_dict.values()) optimizer.zero_grad() losses.backward() optimizer.step() lr_scheduler.step() print({k: round(v.item(), 3) for k, v in loss_dict.items()})model.train()和model.eval()在这里直接影响 RPN 采样的 proposal 数量,不能省。model(images, targets)返回的字典有四个 key:
| loss 名称 | 作用 | 数值异常时优先检查 |
|---|---|---|
loss_objectness | RPN 的前景/背景二分类 | 锚点尺度和输入分辨率不匹配 |
loss_rpn_box_reg | RPN 的锚点坐标回归 | 目标框坐标存在 NaN 或没有归一化 |
loss_classifier | RoI Head 的类别预测 | 类别索引从 0 开始,被当成背景 |
loss_box_reg | RoI Head 的坐标精修 | labels 值超过了num_classes |
训练时要把这四项单独打出来,只看 total loss 会掩盖内部问题。比如loss_objectness已经稳定下降,但loss_rpn_box_reg始终在抖,说明前景框找到了但坐标没学稳,这时候去调rpn_positive_fraction比盲目降学习率更有效。
4.3 训练必调的 4 个超参数与梯度累加
| 超参数 | 起调值 | 影响 | 常见误用 |
|---|---|---|---|
lr | 0.005(SGD) | 整体收敛速度 | 大于 0.02 时 RPN loss 剧烈震荡 |
batch_size | 2 | 显存占用和 BN 统计 | batch=1 也能跑,但每个 step 差异大 |
box_positive_fraction | 0.25 | RoI Head 采样中正样本比例 | 单类别任务可以设到 0.5 |
step_size | 3 | 学习率下降周期 | 数据量少于几千张时 step=3 偏快 |
显存不够但想用更大 batch 时,用梯度累加。需要注意,累加逻辑是把多个 batch 的梯度累积后做一次更新,学习率仍按大 batch 去理解:
accum_steps = 4 model.zero_grad() for step, (images, targets) in enumerate(dataloader): images = [img.to(device) for img in images] targets = [{k: v.to(device) for k, v in t.items()} for t in targets] loss_dict = model(images, targets) loss = sum(v for v in loss_dict.values()) / accum_steps loss.backward() if (step + 1) % accum_steps == 0: optimizer.step() optimizer.zero_grad()除以accum_steps是为了让累计梯度量级和真正的大 batch 一致。这里不能偷懒只对一个 head 的 loss 累加,四个单项的 scale 各不相同,单独加权反而容易让某个 loss 占主导。
5. 验证与排错:把Faster-RCNN目标检测代码调到可用状态
5.1 训练间隙做快速数值检查
不用等完整 mAP,训练时每轮看几个数值就能判断模型是否进入有效学习。代码片段如下:
model.eval() with torch.no_grad(): pred = model(images)[0] keep = pred["scores"] > 0.5 num_dets = int(keep.sum()) boxes_keep = pred["boxes"][keep] w = boxes_keep[:, 2] - boxes_keep[:, 0] h = boxes_keep[:, 3] - boxes_keep[:, 1] ratios = w / (h + 1e-6) print(num_dets, float(ratios.mean()), float(pred["scores"][keep].mean()))num_dets很低,说明阈值太高或模型在验证图上基本无响应;ratios明显偏离业务目标的宽高比,说明回归还没收敛;mean score长期接近 1,则要警惕过拟合。把这组检查放到验证集前 20 张图上,一轮只要几秒。
5.2 三个高频问题的定位方向
| 现象 | 定位 | 处理 |
|---|---|---|
| 训练时 loss 下降正常,验证集框全空 | 验证时没调用model.eval(),或用了训练模式下的 RPN 采样 | 验证入口先切换模型状态 |
| 换自定义数据集后 loss 为 NaN | boxes 里有空框或负宽高,坐标 dtype 不统一 | 在 Dataset 的__getitem__里过滤掉x2<=x1的框 |
| 单类别数据 mAP 特别低 | 类别索引从 0 开始,被当成背景 | 统一把 labels 加 1,输出时再减回来 |
5.3 ONNX 导出时最容易踩的坑
Faster R-CNN 推理输出的boxes数量每张图都不一样,直接用torch.jit.trace导出时容易把 batch 维度或候选框数量记成固定值,换一张图就报 shape 不匹配。常见做法是先把输入固定成(1, 3, 800, 1200)再 trace,并在导出前把box_score_thresh调高,减少输出框数量的浮动范围。保存权重之后,第一件事是用同一张训练图跑一遍原始 PyTorch 模型和导出模型,对比pred["boxes"][:5]是否一致;如果坐标对不上,问题通常在预处理里的均值方差没有固化进去。部署时不要在每一次请求里重建模型,把model.eval()和权重加载放到进程启动时,模型会一直复用同一份显存,输出坐标也稳定一致。
本文还有配套的精品资源,点击获取