|
## 目标检测概述
### 目标检测任务定义
目标检测是计算机视觉的核心任务之一,旨在找出图像中所有感兴趣的目标,确定它们的**类别**和**位置**。
```
输入: 图像
输出: [(类别1, 边界框1), (类别2, 边界框2), ...]
```
### 传统方法 vs 深度学习方法
| 方法类型 | 特征提取 | 检测方式 | 精度 | 速度 |
|---------|---------|---------|------|------|
| 传统方法(HOG+SVM, DPM) | 手工设计特征 | 滑窗+分类器 | 较低 | 慢 |
| 深度学习(R-CNN系列) | CNN自动学习 | 候选区域+分类 | 高 | 较慢 |
| 单阶段检测(YOLO/SSD) | CNN自动学习 | 直接回归 | 高 | 快 |
### 边界框表示
边界框(Bounding Box)常用两种表示方式:
**1. (x1, y1, x2, y2)** - 左上角和右下角坐标
```
bbox = [x_min, y_min, x_max, y_max]
```
**2. (x, y, w, h)** - 中心点坐标和宽高
```
bbox = [center_x, center_y, width, height]
```
### 目标检测挑战
1. **尺度变化**:目标在图像中的大小差异大
2. **姿态变化**:目标的朝向、形态各异
3. **遮挡**:目标被部分遮挡
4. **光照变化**:不同光照条件下的外观差异
5. **背景干扰**:复杂背景导致的误检
### R-CNN历史意义
R-CNN(Regions with CNN,2014)是首个将深度学习成功应用于目标检测的工作,实现了**检测精度的飞跃**:
- VOC 2012 mAP: 31% → 53%(提升22个百分点)
- 奠定了Two-Stage检测器的基础框架
|
|
## Selective Search算法
### Selective Search原理
Selective Search是一种**区域提议算法**,用于生成可能包含目标的候选区域。
### 核心思想
基于图像的层次化分割,通过合并相似区域生成候选框:
1. **图像分割**:使用图论方法将图像分割成小区域
2. **层次化合并**:不断合并相似的区域
3. **生成候选框**:记录合并过程中的边界框
### 相似度度量
综合多种特征计算区域相似度:
```
S = a1 × S_color + a2 × S_texture + a3 × S_size + a4 × S_fill
```
- **S_color**:颜色直方图相似度
- **S_texture**:纹理相似度(HOG/SIFT)
- **S_size**:尺寸相似度(鼓励小区域先合并)
- **S_fill**:填充度(边界框重叠程度)
### 工作流程
```
输入图像
↓
生成初始分割(如Felzenszwalb算法)
↓
计算所有区域对相似度
↓
合并最相似的区域
↓
更新相似度(计算新区域与其他的相似度)
↓
重复直到只剩一个区域
↓
输出所有层次的候选框
```
### 多策略组合
Selective Search采用多种策略组合提升召回率:
- **颜色空间**:RGB、HSV、Lab
- **相似度组合**:不同权重组合
- **阈值变化**:不同的初始化阈值
最终生成约**2000个候选区域**。
### 优缺点
| 优点 | 缺点 |
|-----|------|
| 召回率高,几乎不漏检 | 速度慢(CPU需2秒) |
| 不需要训练 | 候选框质量参差不齐 |
| 通用性强,不依赖特定类别 | 无法针对特定任务优化 |
|
|
## CNN特征提取
### R-CNN中的CNN
R-CNN使用**预训练的CNN模型**提取每个候选区域的特征。
### 网络架构
R-CNN论文中使用的是**AlexNet**(后来也可用VGG等更深的网络):
```
候选区域 (约2000个)
↓
变形(warp)到固定尺寸 227×227
↓
CNN前向传播 (AlexNet/VGG16)
↓
提取特征 (4096维向量)
↓
后续分类/回归
```
### 特征提取细节
**1. 区域变形**
- 将任意大小的候选区域变形到227×227
- 方法:直接拉伸或保留长宽比填充
**2. CNN前向传播**
- 每个区域独立进行一次前向传播
- 使用ImageNet预训练权重
- 提取fc7层输出作为特征(4096维)
**3. 特征存储**
- 提前计算所有区域的特征并保存
- 减少训练时的重复计算
### 计算复杂度
R-CNN的主要瓶颈在于CNN特征提取:
```
时间复杂度 = N × T CNN
```
- N: 候选区域数量 (~2000)
- T CNN: 单次CNN前向传播时间 (~0.05秒)
- 总时间: ~100秒/图像
### 网络选择
| 网络层数 | 特征维度 | 精度 | 速度 |
|---------|---------|------|------|
| AlexNet (8层) | 4096 | 58.5% | 基准 |
| VGG16 (16层) | 4096 | 66.0% | 更慢 |
|
|
## SVM分类与边界框回归
### SVM分类
R-CNN为**每个类别**训练一个**二分类SVM**。
### 训练过程
**1. 正样本准备**
- 候选框与真实框IoU > 0.5
- 每个类别收集约1000个正样本
**2. 负样本准备**
- IoU < 0.3(背景)
- 随机采样,保持正负比例平衡
**3. 特征输入**
```
输入: CNN提取的4096维特征
训练: 线性SVM (类别k vs 非类别k)
输出: 分类得分
```
**4. 硬负挖掘**
- 初步训练后,用难分样本重新训练
- 提升分类器性能
### 边界框回归
针对每个检测到的框,训练一个回归器微调位置。
**输入特征**:
```
φ(P) = [P_x, P_y, P_w, P_h] # 候选框坐标
φ(G) = [G_x, G_y, G_w, G_h] # 真实框坐标
```
**学习目标**:预测变换
```
t_* = T(P_*) # 预测变换
其中: t_x = (G_x - P_x) / P_w
t_y = (G_y - P_y) / P_h
t_w = log(G_w / P_w)
t_h = log(G_h / P_h)
```
**损失函数**(正则化最小二乘):
```
Loss = ||t_* - ŵ_*φ(P)||² + ||ŵ||²
```
### Non-Maximum Suppression (NMS)
去除重复检测的后处理步骤:
```
算法:
1. 按置信度排序所有检测框
2. 选择最高分的框
3. 删除与其IoU > 阈值(0.3)的框
4. 重复2-3直到所有框处理完
```
### 完整流程
```
候选区域 (2000个)
↓
CNN特征提取
↓
SVM分类 (20个类别 × 2000)
↓
边界框回归
↓
NMS去除重复
↓
最终检测结果
```
|
|
## 目标检测评估指标
### IoU (Intersection over Union)
IoU用于衡量两个边界框的重叠程度:
```
IoU = (A ∩ B) / (A ∪ B)
= 重叠面积 / 并集面积
```
**应用场景**:
- 判断正负样本(IoU > 0.5为正样本)
- 评估检测精度
- NMS去除重复检测
### mAP (mean Average Precision)
平均精度均值是目标检测的主要评估指标。
**计算步骤**:
1. **计算每个类别的AP**:
- 按置信度排序检测结果
- 计算Precision-Recall曲线
- 计算AP(曲线下面积)
2. **计算mAP**:
```
mAP = (AP_1 + AP_2 + ... + AP_C) / C
```
### Precision-Recall曲线
```
Precision = TP / (TP + FP) # 检测正确的比例
Recall = TP / (TP + FN) # 找出目标的比例
AP = ∫ Precision(Recall) dRecall
```
### COCO评估指标
COCO数据集使用更全面的评估:
| 指标 | 说明 |
|-----|------|
| AP | IoU=0.50:0.95的平均 |
| AP50 | IoU=0.50的AP |
| AP75 | IoU=0.75的AP |
| AP_small | 小目标AP |
| AP_medium | 中目标AP |
| AP_large | 大目标AP |
### R-CNN性能
| 数据集 | mAP | 说明 |
|-------|-----|------|
| VOC 2010 | 53.3% | R-CNN (AlexNet) |
| VOC 2012 | 53.1% | R-CNN (AlexNet) |
| VOC 2012 | 66.0% | R-CNN (VGG16) |
| | | |
|
|
## Fast R-CNN (2015)
### 核心改进
Ross Girshick在R-CNN之后提出Fast R-CNN,解决了R-CNN的主要缺陷。
### R-CNN的问题
| 问题 | 说明 |
|-----|------|
| **重复计算** | 2000个区域分别前向传播,大量重复 |
| **训练复杂** | 多阶段训练(CNN预训练+SVM+回归) |
| **速度慢** | GPU上约0.2秒/图(特征提取) |
| **空间信息丢失** | 区域变形破坏空间结构 |
### Fast R-CNN创新
**1. 共享卷积计算**
```
输入图像
↓
整图CNN前向传播(一次)
↓
卷积特征图
↓
Selective Search区域映射到特征图
↓
RoI Pooling
↓
分类 + 回归
```
**2. RoI Pooling**
- 将任意大小的RoI特征区域池化到固定尺寸
- 两级池化:
- 先按比例划分网格
- 每个格子内Max Pooling
**3. 多任务损失**
```
L = L_cls + λ × L_bbox
L_cls: 分类损失 (交叉熵)
L_bbox: 边界框回归损失 (平滑L1)
```
**4. 端到端训练**
- 统一网络,联合训练
- 不再需要单独的SVM
### 网络结构
```
输入图像
↓
Conv Layers (共享)
↓
RoI Pooling
↓
FC Layers
↓
├── 分类输出 (类别概率)
└── 回归输出 (边界框调整)
```
### 性能对比
| 模型 | mAP (VOC07) | 训练时间 | 测试速度 |
|-----|-------------|---------|---------|
| R-CNN | 66.0% | 84小时 | 50秒/图 |
| Fast R-CNN | 66.9% | 9.5小时 | 0.32秒/图 |
|
|
## Faster R-CNN (2015)
### 核心突破
Faster R-CNN用**神经网络**替代Selective Search,实现**完全端到端**的检测。
### Region Proposal Network (RPN)
RPN是一个轻量级的卷积网络,用于生成候选区域。
**核心思想**:
- 在共享的卷积特征图上滑动小网络
- 每个位置预测k个候选区域
- 通过锚框(Anchor)机制
### RPN网络结构
```
共享卷积特征图
↓
3×3卷积 (256维)
↓
┌─────────────┐
│ 1×1卷积 │
├─────────────┤
│ 2k (目标性)│
│ 4k (坐标) │
└─────────────┘
```
### Anchor机制
每个位置预设k个锚框(通常k=9):
```
3种尺度: 128², 256², 512²
3种比例: 1:1, 1:2, 2:1
共9个Anchor
```
### RPN损失
```
L = L_cls + λ × L_bbox
L_cls: 目标性损失 (是否有目标)
L_bbox: 边界框回归 (仅对正Anchor)
```
**正样本定义**:
- 与真实框IoU最高的Anchor
- IoU > 0.7的Anchor
**负样本定义**:
- IoU < 0.3的Anchor
### 端到端训练
**4步交替训练**:
1. 训练RPN
2. 用RPN结果训练Fast R-CNN
3. 微调RPN(共享卷积层)
4. 再次微调Fast R-CNN
### 性能提升
| 模型 | mAP (VOC07) | 候选区域生成时间 |
|-----|-------------|----------------|
| Fast R-CNN + SS | 66.9% | 2秒 (CPU) |
| Faster R-CNN | 73.2% | 0.01秒 (GPU) |
|
|
## RoI Pooling详解
### RoI Pooling作用
将不同大小的候选区域特征映射到固定尺寸,以便输入全连接层。
### 工作原理
**输入**:
- 卷积特征图 (H×W×C)
- RoI坐标 (x, y, w, h)
**输出**:
- 固定尺寸特征 (如7×7×C)
**操作**:
```
1. 将RoI划分为 k×k 个子区域
(子区域大小 = RoI尺寸 / k)
2. 对每个子区域进行Max Pooling
3. 输出 k×k×C 的特征
```
### 示例
假设卷积特征图大小为 28×28,RoI为(14,14,256):
```
目标输出: 7×7×256
1. 将14×14划分为7×7网格
每个格子: 2×2
2. 每个格子内取最大值
(2×2 → 1)
3. 输出7×7×256
```
### RoI Align改进
RoI Pooling存在**量化误差**问题(两次取整)。
**RoI Align**(Mask R-CNN提出):
- 不进行量化
- 使用双线性插值
- 保持空间精度
### 代码示意
```python
# RoI Pooling 伪代码
def roi_pooling(feature_map, rois, output_size):
pooled_features = []
for roi in rois:
# 提取RoI区域
roi_feature = feature_map[:, roi.y:roi.y+roi.h,
roi.x:roi.x+roi.w]
# 调整大小
pooled = resize(roi_feature, output_size, mode='max')
pooled_features.append(pooled)
return torch.stack(pooled_features)
```
|
|
## RPN详解
### RPN完整架构
Region Proposal Network是Faster R-CNN的核心创新。
### 网络详解
```
输入: 共享卷积特征图 (约512×38×38)
↓
3×3卷积 (512通道)
↓
┌───┴───┐
↓ ↓
1×1 Conv 1×1 Conv
(2×9×k) (4×9×k)
↓ ↓
目标性 边界框
(18通道) (36通道)
```
### Anchor计算
**9个Anchor的位置**(每个滑动窗口位置):
```
Scale: {128, 256, 512}
Ratio: {0.5, 1.0, 2.0}
生成:
128×128, 128×256, 128×512
256×256, 256×512, 256×1024
512×512, 512×1024, 512×2048
```
### 坐标变换
RPN输出的是相对于Anchor的偏移量:
```
tx = (x - xa) / wa
ty = (y - ya) / ha
tw = log(w / wa)
th = log(h / ha)
逆变换:
x = wa × exp(tx) + xa
y = ha × exp(ty) + ya
w = wa × exp(tw)
h = ha × exp(th)
```
### 训练样本分配
**每个mini-batch**:
- 256个Anchor
- 正样本: 128个 (IoU > 0.7)
- 负样本: 128个 (IoU < 0.3)
- 忽略: 中等IoU的Anchor
### Proposal后处理
```
RPN输出 (~12000个Anchor)
↓
去除超出边界的
↓
按置信度排序,取前2000
↓
NMS (IoU阈值0.7)
↓
最终约300个候选区域
```
### 为什么RPN比SS快?
| 方法 | 计算方式 | 速度 |
|-----|---------|------|
| Selective Search | CPU运行传统算法 | 2秒 |
| RPN | GPU卷积 + 共享特征 | 0.01秒 |
RPN复用了检测网络的卷积特征,几乎不增加额外计算量。
|
|
## Two-Stage检测器总结
### 系列对比
| 特性 | R-CNN | Fast R-CNN | Faster R-CNN |
|-----|-------|-----------|-------------|
| 候选区域 | Selective Search | Selective Search | RPN |
| 特征提取 | 独立CNN | 共享CNN | 共享CNN |
| 训练方式 | 多阶段 | 端到端 | 端到端 |
| 测试速度 | ~50秒/图 | ~2秒/图 | ~0.2秒/图 |
| mAP (VOC07) | 66.0% | 66.9% | 73.2% |
### Two-Stage特点
**优点**:
- 精度高,尤其是小目标
- 候选区域聚焦,减少背景干扰
- 灵活的特征提取
**缺点**:
- 速度相对较慢(相比YOLO/SSD)
- 架构复杂,训练调试困难
- 内存占用较大
### One-Stage vs Two-Stage
| 对比维度 | One-Stage (YOLO/SSD) | Two-Stage (Faster R-CNN) |
|---------|---------------------|--------------------------|
| 流程 | 直接回归 | 先提议再检测 |
| 速度 | 快 (45+ FPS) | 较慢 (5-10 FPS) |
| 精度 | 略低 | 高 |
| 小目标 | 较弱 | 较强 |
| 负样本 | 极度不平衡 | 通过提议过滤 |
### 发展脉络
```
R-CNN (2014)
↓ 解决: 重复计算
Fast R-CNN (2015)
↓ 解决: Selective Search瓶颈
Faster R-CNN (2015)
↓ 扩展: 更多功能
Mask R-CNN (2017)
↓ 优化: 架构改进
Cascade R-CNN (2018)
```
### 现代Two-Stage检测器
**Mask R-CNN**:添加分割分支
**Cascade R-CNN**:级联回归提升IoU
**PointRend**:精细化分割
**Sparse R-CNN**:稀疏注意力机制
|
|
## R-CNN 网络结构
### 模型特点
R-CNN 对**每个候选区域独立**进行 CNN 特征提取,是三阶段、非共享的架构。
```
输入图像
↓
Selective Search → 约2000个候选区域
↓
每个区域 warp 到 227×227
↓
AlexNet 独立前向传播(重复2000次)
↓
SVM 分类 + 边界框回归
```
### 核心缺陷
- **特征不共享**:2000个区域各算一次 CNN,计算量 = 2000 × T_cnn
- **多阶段训练**:CNN → SVM → BBox Reg 三个独立模型
- **空间信息丢失**:warp 破坏原始比例
### 网络代码(独立特征提取)
```python
import torch
import torch.nn as nn
class RCNNFeatureExtractor(nn.Module):
"""R-CNN: 使用 AlexNet 提取单个区域的 4096 维特征"""
def __init__(self):
super().__init__()
# 卷积特征提取部分
self.features = nn.Sequential(
nn.Conv2d(3, 96, 11, stride=4), nn.ReLU(),
nn.MaxPool2d(3, stride=2),
nn.Conv2d(96, 256, 5, padding=2), nn.ReLU(),
nn.MaxPool2d(3, stride=2),
nn.Conv2d(256, 384, 3, padding=1), nn.ReLU(),
nn.Conv2d(384, 384, 3, padding=1), nn.ReLU(),
nn.Conv2d(384, 256, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(3, stride=2),
)
# 全连接层,输出 4096 维特征
self.classifier = nn.Sequential(
nn.Linear(256 * 6 * 6, 4096), nn.ReLU(),
nn.Linear(4096, 4096), nn.ReLU(),
)
def forward(self, x):
# x: 单个候选区域 warp 后的 227×227 图像
x = self.features(x)
x = torch.flatten(x, 1)
return self.classifier(x) # 4096维特征
# R-CNN 的致命点:必须循环处理每个候选区域
extractor = RCNNFeatureExtractor()
features = [extractor(region) for region in proposals] # 重复2000次!
```
|
|
## Fast R-CNN 网络
### 模型特点
Fast R-CNN 的关键改进是**共享卷积计算** + **RoI Pooling**,图像只前向传播一次。
```
输入图像
↓
VGG16 卷积层(整图,只算1次)→ 共享特征图
↓
候选区域映射到特征图
↓
★ RoI Pooling(固定尺寸输出)★ ← ROI 的核心作用
↓
全连接层 → 分类 + 边界框回归(端到端)
```
### RoI 的作用(对应代码)
RoI Pooling 解决了「不同大小候选区域无法输入全连接层」的问题:
- 把任意尺寸的特征区域池化为**固定 7×7**
- 这样后续 FC 层输入维度恒定
### 网络代码(共享卷积 + RoI Pooling)
```python
import torch
import torch.nn as nn
class FastRCNN(nn.Module):
"""Fast R-CNN: 共享卷积 + RoI Pooling"""
def __init__(self, num_classes=20):
super().__init__()
# 1. 共享卷积 backbone(整图只算一次)
self.backbone = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(),
nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2, stride=2),
# ...更多卷积块...
nn.Conv2d(256, 512, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2, stride=2),
)
# ★★★ ROI 作用对应代码:RoI Pooling ★★★
# 将任意大小候选区域池化到固定 7×7,输出维度恒定
self.roi_pool = nn.AdaptiveMaxPool2d((7, 7))
# 2. 全连接分类 + 回归头
self.fc = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096), nn.ReLU(),
nn.Linear(4096, 4096), nn.ReLU(),
)
self.cls_score = nn.Linear(4096, num_classes) # 分类
self.bbox_pred = nn.Linear(4096, num_classes * 4) # 回归
def forward(self, image, rois):
# 整图共享卷积(只算1次)— 这是 Fast 比 R-CNN 快的关键
feat_map = self.backbone(image) # [1, 512, H/16, W/16]
# 对每个候选区域在特征图上做 RoI Pooling
pooled = []
for roi in rois:
x1, y1, x2, y2 = roi
roi_feat = feat_map[:, :, y1:y2, x1:x2] # 取出RoI区域
pooled.append(self.roi_pool(roi_feat)) # → 7×7
pooled = torch.cat(pooled) # [N, 512, 7, 7]
x = torch.flatten(pooled, 1)
x = self.fc(x)
return self.cls_score(x), self.bbox_pred(x)
```
|
|
## Faster R-CNN 网络
### 模型特点
Faster R-CNN 用 **RPN(区域提议网络)** 替代 Selective Search,实现完全端到端。
```
输入图像
↓
共享卷积 Backbone(VGG16/ResNet)
↓
共享特征图 ──────────┐
↓ ↓
★ RPN 生成候选区域 ★ ★ RoI Pooling ★
↓ ↓
└──→ proposals →→ 全连接 → 分类 + 回归
```
### RPN 的作用(对应代码)
RPN 是一个轻量卷积网络,在共享特征图上滑动,为每个位置预测 k 个 Anchor 的「目标性」和「坐标偏移」,从而用神经网络自动生成候选区域(替代慢速的 Selective Search)。
### 完整网络代码
```python
import torch
import torch.nn as nn
class FasterRCNN(nn.Module):
"""Faster R-CNN: RPN + RoI Pooling 端到端"""
def __init__(self, num_classes=20, num_anchors=9):
super().__init__()
# 1. 共享卷积 backbone
self.backbone = self._build_backbone()
# ★★★ RPN 作用对应代码:区域提议网络 ★★★
self.rpn = RPN(in_channels=512, num_anchors=num_anchors)
# ★★★ ROI 作用对应代码:RoI Pooling ★★★
self.roi_pool = nn.AdaptiveMaxPool2d((7, 7))
# 2. 检测头
self.fc = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096), nn.ReLU(),
nn.Linear(4096, 4096), nn.ReLU(),
)
self.cls_score = nn.Linear(4096, num_classes)
self.bbox_pred = nn.Linear(4096, num_classes * 4)
def _build_backbone(self):
return nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(),
nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2, 2),
# ...简化版 VGG16...
nn.Conv2d(64, 512, 3, padding=1), nn.ReLU(),
)
def forward(self, image):
feat_map = self.backbone(image) # 共享特征图
proposals, rpn_cls, rpn_reg = self.rpn(feat_map) # RPN 生成候选区域
pooled = []
for roi in proposals:
x1, y1, x2, y2 = roi.long()
pooled.append(self.roi_pool(feat_map[:, :, y1:y2, x1:x2]))
pooled = torch.cat(pooled) if pooled else torch.zeros(0, device=image.device)
x = torch.flatten(pooled, 1)
x = self.fc(x)
return self.cls_score(x), self.bbox_pred(x), rpn_cls, rpn_reg
```
|
|
## RoI Pooling 实现(ROI 作用详解)
### RoI 的作用
RoI(Region of Interest)Pooling 解决 Faster/Fast R-CNN 中**候选区域尺寸不一**的问题:
1. 不同候选区域映射到特征图后大小不同
2. 全连接层要求固定输入维度
3. RoI Pooling 把任意大小区域统一池化到固定尺寸(如 7×7)
```
RoI 特征区域 (任意 H×W)
↓
划分为 7×7 个子区域
↓
每个子区域做 Max Pooling
↓
输出固定 7×7 特征
```
### 完整实现代码
```python
import torch
import torch.nn as nn
class RoIPooling(nn.Module):
"""RoI Pooling 完整实现
ROI 的核心作用:
- 输入: 任意大小的 RoI 特征区域
- 输出: 固定大小 (output_size × output_size) 的特征
- 意义: 使后续全连接层维度恒定
"""
def __init__(self, output_size=(7, 7), spatial_scale=1.0/16):
super().__init__()
self.output_size = output_size # 目标固定尺寸
self.spatial_scale = spatial_scale # 特征图相对原图的下采样比
def forward(self, feature_map, rois):
# feature_map: [B, C, H, W] 共享卷积特征图
# rois: [N, 5] (batch_idx, x1, y1, x2, y2)
out_h, out_w = self.output_size
pooled_features = []
for roi in rois:
batch_idx, x1, y1, x2, y2 = roi
# 将原图坐标映射到特征图坐标(spatial_scale)
x1 = int(x1 * self.spatial_scale)
y1 = int(y1 * self.spatial_scale)
x2 = int(x2 * self.spatial_scale)
y2 = int(y2 * self.spatial_scale)
# 取出该 RoI 对应的特征区域(任意大小)
roi_feat = feature_map[batch_idx, :, y1:y2, x1:x2]
c = roi_feat.shape[0]
# 子区域大小(可能不整除,需向上取整保证覆盖)
sub_h = max((y2 - y1) // out_h, 1)
sub_w = max((x2 - x1) // out_w, 1)
# ★ ROI 作用核心:Max Pooling 到固定尺寸 ★
pooled = nn.functional.max_pool2d(
roi_feat.unsqueeze(0),
kernel_size=(sub_h, sub_w),
stride=(sub_h, sub_w),
)
# 若池化后尺寸仍不匹配,自适应调整到 output_size
if pooled.shape[-2:] != (out_h, out_w):
pooled = nn.functional.adaptive_max_pool2d(
pooled, (out_h, out_w))
pooled_features.append(pooled.squeeze(0))
return torch.stack(pooled_features) # [N, C, out_h, out_w]
# RoI Align 改进:用双线性插值避免量化取整误差
# 代码中使用 torchvision.ops.roi_align 即可
import torchvision.ops as ops
# pooled = ops.roi_align(feature_map, rois, output_size=(7,7))
```
|
|
## RPN 实现(RPN 原理与作用)
### RPN 的原理与作用
RPN(Region Proposal Network)是 Faster R-CNN 的核心创新:
1. **原理**:在共享特征图上用 3×3 卷积滑动,每个位置预测 9 个 Anchor(3尺度×3比例)的目标性和坐标偏移
2. **作用**:用神经网络自动生成高质量候选区域,替代 CPU 上的 Selective Search(2秒→0.01秒)
3. **优势**:与检测网络共享卷积特征,几乎不增加计算量
```
共享特征图 (H×W×512)
↓
3×3 卷积(滑动窗口)
↓
┌────────────┬────────────┐
│ 1×1 卷积 │ 1×1 卷积 │
│ 2×9=18 通道 │ 4×9=36通道 │
│ (目标性) │ (坐标偏移) │
└────────────┴────────────┘
```
### 完整实现代码
```python
import torch
import torch.nn as nn
class RPN(nn.Module):
"""Region Proposal Network 完整实现
RPN 原理:滑动窗口 + Anchor 机制
RPN 作用:神经网络自动生成候选区域
"""
def __init__(self, in_channels=512, num_anchors=9):
super().__init__()
self.num_anchors = num_anchors
# ★ RPN 原理对应代码:3×3 滑动窗口卷积 ★
# 在共享特征图上滑动,融合空间上下文
self.conv = nn.Conv2d(in_channels, in_channels, 3, padding=1)
self.relu = nn.ReLU()
# ★ RPN 作用对应代码:两个 1×1 卷积分支 ★
# 分支1: 预测每个 Anchor 是否包含目标 (objectness)
self.cls_layer = nn.Conv2d(in_channels, num_anchors * 2, 1)
# 分支2: 预测每个 Anchor 的坐标偏移 (dx, dy, dw, dh)
self.bbox_layer = nn.Conv2d(in_channels, num_anchors * 4, 1)
def forward(self, feature_map):
# feature_map: [B, 512, H, W] 共享卷积特征图
B, _, H, W = feature_map.shape
# 1. 滑动窗口卷积(融合每个位置的邻域信息)
t = self.relu(self.conv(feature_map)) # [B, 512, H, W]
# 2. 目标性预测:每个位置 num_anchors 个框 × 2(前景/背景)
rpn_cls = self.cls_layer(t) # [B, 18, H, W]
rpn_cls = rpn_cls.permute(0, 2, 3, 1).reshape(B, -1, 2)
# 3. 坐标回归:每个位置 num_anchors 个框 × 4(坐标)
rpn_reg = self.bbox_layer(t) # [B, 36, H, W]
rpn_reg = rpn_reg.permute(0, 2, 3, 1).reshape(B, -1, 4)
# 4. 生成 Anchor(9种尺度×比例)
anchors = self._generate_anchors(H, W) # [B, H*W*9, 4]
# 5. RPN 作用:用预测的偏移修正 Anchor 得到候选区域
proposals = self._decode_proposals(anchors, rpn_reg)
# 6. 后处理:排序 + NMS,取前 300 个
proposals = self._filter_proposals(proposals, rpn_cls)
return proposals, rpn_cls, rpn_reg
def _generate_anchors(self, H, W):
# 3 种尺度 × 3 种比例 = 9 个基准 Anchor
scales = [64, 128, 256]
ratios = [0.5, 1.0, 2.0]
anchors = []
for s in scales:
for r in ratios:
h = s * (r ** 0.5)
w = s / (r ** 0.5)
anchors.append([-w/2, -h/2, w/2, h/2])
# 在每个特征点位置放置这 9 个 Anchor
return torch.tensor(anchors) # [9, 4]
def _decode_proposals(self, anchors, regs):
# 用回归偏移修正 Anchor 坐标
xa = (anchors[:, 0] + anchors[:, 2]) / 2
ya = (anchors[:, 1] + anchors[:, 3]) / 2
wa = (anchors[:, 2] - anchors[:, 0])
ha = (anchors[:, 3] - anchors[:, 1])
# 逆变换:把网络输出的偏移转成实际框坐标
tx, ty, tw, th = regs[..., 0], regs[..., 1], regs[..., 2], regs[..., 3]
cx = tx * wa + xa
cy = ty * ha + ya
w = torch.exp(tw) * wa
h = torch.exp(th) * ha
return torch.stack([cx-w/2, cy-h/2, cx+w/2, cy+h/2], dim=-1)
def _filter_proposals(self, proposals, scores):
# 按前景得分排序,取前 12000
scores = scores.softmax(dim=-1)[:, :, 1] # 取前景概率
_, order = scores.sort(descending=True)
proposals = proposals[order[:12000]]
# NMS 去重(IoU 阈值 0.7)
from torchvision.ops import nms
keep = nms(proposals, scores[order[:12000]], 0.7)
return proposals[keep[:300]] # 最终 300 个候选区域
```
|
|
### 全流程概览
Faster R-CNN 完整实战流程:**数据准备 → Anchor生成与样本分配 → 网络定义 → 联合训练(RPN+检测头) → 预测推理 → 评估**
本示例使用 PyTorch 搭建简化版 Faster R-CNN,自动生成随机数据演示全流程,无需下载 VOC 数据集。
### 1. 环境准备
```python
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision.ops import nms
DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
NUM_CLASSES = 3 # 背景 + 猫 + 狗(示例)
print(f"使用设备: {DEVICE}")
```
### 2. Anchor 生成
```python
def generate_anchors(feature_h, feature_w, stride=16,
scales=[64, 128, 256], ratios=[0.5, 1.0, 2.0]):
"""在特征图每个位置生成 9 个 Anchor"""
base = []
for s in scales:
for r in ratios:
h = s * (r ** 0.5)
w = s / (r ** 0.5)
base.append([-w/2, -h/2, w/2, h/2])
base = torch.tensor(base) # [9, 4]
# 在每个特征点放置 Anchor(映射回原图坐标)
anchors = []
for y in range(feature_h):
for x in range(feature_w):
cx = x * stride + stride / 2
cy = y * stride + stride / 2
anchors.append(base + torch.tensor([cx, cy, cx, cy]))
return torch.stack(anchors) # [H*W*9, 4]
```
### 3. 正负样本分配
```python
def assign_targets(anchors, gt_boxes, pos_iou=0.7, neg_iou=0.3):
"""根据 IoU 划分 Anchor 的正负样本"""
ious = box_iou(anchors, gt_boxes) # [num_anchors, num_gt]
max_iou, gt_idx = ious.max(dim=1)
labels = torch.zeros(len(anchors), dtype=torch.long) # 0=背景
labels[max_iou < neg_iou] = 0 # 负样本
labels[max_iou >= pos_iou] = 1 # 正样本
# 保证每个 gt 至少有一个正样本
for i in range(len(gt_boxes)):
best_anchor = ious[:, i].argmax()
labels[best_anchor] = 1
return labels, gt_idx
```
|
|
### Faster R-CNN 网络架构回顾
```
输入图像 (3, H, W)
↓
Backbone (ResNet/VGG) → 共享特征图
↓
┌─────── RPN (生成候选区域) ───────┐
│ 3×3 Conv → 1×1 Conv (cls + reg) │
└───────────────────────────────────┘
↓ proposals (300个)
RoI Pooling (→ 7×7)
↓
FC Layers → 分类 + 边界框回归
```
### 1. 共享 Backbone
```python
class SimpleBackbone(nn.Module):
"""简化版 VGG 风格 backbone,输出步长16"""
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(),
nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2, 2), # /2
nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(),
nn.Conv2d(128, 128, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2, 2), # /4
nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(),
nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2, 2), # /8
nn.Conv2d(256, 512, 3, padding=1), nn.ReLU(),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(),
)
def forward(self, x):
return self.features(x) # stride≈8(示例简化)
```
### 2. RPN 模块
```python
class RPNHead(nn.Module):
def __init__(self, in_ch=512, num_anchors=9):
super().__init__()
self.conv = nn.Conv2d(in_ch, in_ch, 3, padding=1)
self.cls = nn.Conv2d(in_ch, num_anchors * 2, 1) # 目标性
self.reg = nn.Conv2d(in_ch, num_anchors * 4, 1) # 坐标
def forward(self, x):
t = F.relu(self.conv(x))
return self.cls(t), self.reg(t)
```
### 3. RoI Pooling + 检测头
```python
class RoIHead(nn.Module):
"""RoI Pooling + FC 分类回归头"""
def __init__(self, num_classes=3):
super().__init__()
self.roi_pool = nn.AdaptiveMaxPool2d((7, 7))
self.fc = nn.Sequential(
nn.Linear(512 * 7 * 7, 1024), nn.ReLU(),
nn.Linear(1024, 1024), nn.ReLU(),
)
self.cls = nn.Linear(1024, num_classes)
self.reg = nn.Linear(1024, num_classes * 4)
def forward(self, feat, proposals):
pooled = [self.roi_pool(feat[:, :, p[1]:p[3], p[0]:p[2]])
for p in proposals]
pooled = torch.cat(pooled).flatten(1)
x = self.fc(pooled)
return self.cls(x), self.reg(x)
```
### 4. 完整模型
```python
class FasterRCNNModel(nn.Module):
def __init__(self, num_classes=3, num_anchors=9):
super().__init__()
self.backbone = SimpleBackbone()
self.rpn = RPNHead(512, num_anchors)
self.roi_head = RoIHead(num_classes)
def forward(self, image, proposals=None):
feat = self.backbone(image)
rpn_cls, rpn_reg = self.rpn(feat)
if proposals is None:
proposals = self.get_proposals(rpn_cls, rpn_reg)
cls, reg = self.roi_head(feat, proposals)
return cls, reg, rpn_cls, rpn_reg, proposals
def get_proposals(self, rpn_cls, rpn_reg):
# 简化:随机生成示例 proposals
return torch.tensor([[0, 10, 10, 100, 100],
[0, 50, 50, 200, 200]], dtype=torch.float)
```
|
|
### 多任务损失
Faster R-CNN 训练包含两部分损失:
```
L_total = L_RPN + L_RoI
L_RPN = L_cls(RPN目标性) + λ·L_reg(RPN坐标)
L_RoI = L_cls(类别分类) + λ·L_reg(检测框坐标)
```
### 1. 损失函数实现
```python
def smooth_l1_loss(pred, target, beta=1.0):
"""平滑 L1 损失,对异常值更鲁棒"""
diff = torch.abs(pred - target)
loss = torch.where(diff < beta, 0.5 * diff ** 2 / beta,
diff - 0.5 * beta)
return loss.mean()
def rpn_loss(rpn_cls, rpn_reg, labels, targets, num_anchors=9):
"""RPN 损失:分类(目标性) + 回归"""
rpn_cls = rpn_cls.permute(0, 2, 3, 1).reshape(-1, 2)
rpn_reg = rpn_reg.permute(0, 2, 3, 1).reshape(-1, 4)
# 只对正样本计算回归损失
pos = labels == 1
cls_loss = F.cross_entropy(rpn_cls, labels)
reg_loss = smooth_l1_loss(rpn_reg[pos], targets[pos]) if pos.sum() > 0 else 0
return cls_loss + 2.0 * reg_loss
def roi_loss(cls_pred, reg_pred, cls_gt, reg_gt):
"""RoI 检测头损失"""
cls_loss = F.cross_entropy(cls_pred, cls_gt)
reg_loss = smooth_l1_loss(reg_pred, reg_gt)
return cls_loss + 1.0 * reg_loss
```
### 2. 训练循环
```python
import torch.optim as optim
model = FasterRCNNModel(num_classes=NUM_CLASSES).to(DEVICE)
optimizer = optim.SGD(model.parameters(), lr=0.005,
momentum=0.9, weight_decay=5e-4)
EPOCHS = 5
for epoch in range(EPOCHS):
model.train()
for images, gt_boxes, gt_labels in train_loader:
images = images.to(DEVICE)
optimizer.zero_grad()
# 前向传播
cls, reg, rpn_cls, rpn_reg, proposals = model(images)
# 计算损失(RPN + RoI 双重损失)
rpn_labels, rpn_targets = assign_targets(all_anchors, gt_boxes)
loss_rpn = rpn_loss(rpn_cls, rpn_reg, rpn_labels, rpn_targets)
loss_roi = roi_loss(cls, reg, roi_labels, roi_targets)
loss = loss_rpn + loss_roi
loss.backward()
optimizer.step()
print(f"Epoch {epoch+1}/{EPOCHS} loss={loss.item():.4f} "
f"rpn={loss_rpn.item():.4f} roi={loss_roi.item():.4f}")
```
|
|
### 端到端预测流程
```
输入图像
↓
Backbone → 共享特征图
↓
RPN → proposals (300)
↓
RoI Pooling + FC → 类别 + 坐标
↓
阈值过滤 + NMS
↓
最终检测结果
```
### 预测代码
```python
@torch.no_grad()
def predict(model, image, conf_thresh=0.8, nms_thresh=0.3):
"""Faster R-CNN 端到端推理"""
model.eval()
image = image.to(DEVICE)
# 1. 前向传播
cls, reg, rpn_cls, rpn_reg, proposals = model(image)
# 2. 计算类别概率
scores = F.softmax(cls, dim=1) # [N, num_classes]
labels = scores.argmax(dim=1) # 预测类别
confs = scores.max(dim=1).values # 置信度
# 3. 过滤背景 + 置信度阈值
keep = (labels > 0) & (confs > conf_thresh)
boxes = decode_boxes(proposals, reg)[keep]
labels = labels[keep]
confs = confs[keep]
# 4. 按类别做 NMS
final_boxes, final_labels, final_scores = [], [], []
for c in labels.unique():
idx = (labels == c)
keep_idx = nms(boxes[idx], confs[idx], nms_thresh)
final_boxes.append(boxes[idx][keep_idx])
final_labels.append(labels[idx][keep_idx])
final_scores.append(confs[idx][keep_idx])
return torch.cat(final_boxes), torch.cat(final_labels), torch.cat(final_scores)
def decode_boxes(proposals, reg):
"""用 RoI 回归偏移修正 proposals 坐标"""
px = (proposals[:, 0] + proposals[:, 2]) / 2
py = (proposals[:, 1] + proposals[:, 3]) / 2
pw = proposals[:, 2] - proposals[:, 0]
ph = proposals[:, 3] - proposals[:, 1]
dx, dy, dw, dh = reg[:, 0], reg[:, 1], reg[:, 2], reg[:, 3]
gx = dx * pw + px
gy = dy * ph + py
gw = torch.exp(dw) * pw
gh = torch.exp(dh) * ph
return torch.stack([gx-gw/2, gy-gh/2, gx+gw/2, gy+gh/2], dim=1)
```
### 可视化
```python
import matplotlib.pyplot as plt
import matplotlib.patches as patches
CLASSES = ['__bg__', 'cat', 'dog']
def visualize(image, boxes, labels, scores):
fig, ax = plt.subplots(1)
ax.imshow(image.permute(1, 2, 0).cpu())
for box, label, score in zip(boxes, labels, scores):
x1, y1, x2, y2 = box.tolist()
rect = patches.Rectangle((x1, y1), x2-x1, y2-y1,
linewidth=2, edgecolor='lime', fill=False)
ax.add_patch(rect)
ax.text(x1, y1-5, f"{CLASSES[label]} {score:.2f}",
color='lime', fontsize=12)
plt.axis('off')
plt.show()
# boxes, labels, scores = predict(model, test_image)
# visualize(test_image, boxes, labels, scores)
```
|
|
### 评估指标 mAP
```python
def compute_ap(rec, prec):
"""计算单个类别的 AP(使用11点插值)"""
ap = 0.0
for t in np.arange(0, 1.1, 0.1):
p = prec[rec >= t].max() if (rec >= t).any() else 0
ap += p / 11
return ap
def compute_map(all_detections, all_ground_truths, iou_thresh=0.5):
"""计算 mAP"""
aps = []
for c in range(1, NUM_CLASSES):
detections = all_detections[c]
gt = all_ground_truths[c]
# 按置信度排序
detections = sorted(detections, key=lambda x: x['score'], reverse=True)
tp = np.zeros(len(detections))
fp = np.zeros(len(detections))
for i, det in enumerate(detections):
ious = [box_iou(det['box'], g) for g in gt]
best = max(ious) if ious else 0
if best >= iou_thresh:
tp[i] = 1
else:
fp[i] = 1
tp = np.cumsum(tp); fp = np.cumsum(fp)
recall = tp / max(len(gt), 1)
precision = tp / (tp + fp + 1e-8)
aps.append(compute_ap(recall, precision))
return np.mean(aps) # mAP
```
### 三模型性能对比
| 模型 | mAP (VOC07) | 速度 | 候选区域 | 特征提取 |
|-----|-------------|------|---------|---------|
| R-CNN | 66.0% | 50秒/图 | Selective Search | 独立CNN |
| Fast R-CNN | 66.9% | 0.32秒/图 | Selective Search | 共享卷积+RoI Pooling |
| **Faster R-CNN** | **73.2%** | **0.2秒/图** | **RPN** | 共享卷积+RPN+RoI |
### 训练调优要点
1. **Anchor 尺度**:需匹配数据集中目标尺寸分布
2. **正负样本比例**:RPN 保持 1:1,检测头用 OHEM 处理不平衡
3. **学习率策略**:Warmup + Step Decay,RPN 单独预热
4. **多尺度训练**:提升对不同尺寸目标的鲁棒性
|
|
### 个人思考
**为什么 Two-Stage 精度更高?**
Faster R-CNN 通过 RPN 先「聚焦」可能含目标的区域,再精分类,天然抑制了背景干扰,正负样本比 YOLO 更平衡,因此小目标检测能力更强。
**RPN 与 RoI Pooling 的协同**
两者都建立在**共享特征图**上:RPN 用它生成 proposals,RoI Pooling 用它提取 proposal 特征。这种共享是速度提升的根本原因——卷积计算只做一次。
**工程实践建议**
- 生产环境推荐使用 `torchvision.models.detection.fasterrcnn_resnet50_fpn`,它已实现 RoI Align、FPN、平衡采样等最佳实践
- Anchor-free 检测器(FCOS、CornerNet)省去了 Anchor 调参,逐渐成为新趋势
- 对于实时性要求高的场景,YOLO/SSD 更合适;追求精度则选 Faster R-CNN 系列
**演进脉络的个人理解**
```
R-CNN 证明了「深度学习+候选区域」可行
↓ Fast R-CNN 解决「重复计算」→ 引入 RoI Pooling
↓ Faster R-CNN 解决「Selective Search 瓶颈」→ 引入 RPN
↓ 核心思想:让网络自己学习「去哪里看」和「看什么」
```
这三大组件(Backbone 共享、RPN、RoI Pooling)奠定了现代 Two-Stage 检测器的基石。
|
R-CNN论文: Rich feature hierarchies for accurate object detection and semantic segmentation
Fast R-CNN论文: Fast R-CNN
Faster R-CNN论文: Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks
Faster R-CNN Python实现
Detectron2 (Facebook目标检测框架)