0. 前言

R-CNN (Region-based Convolutional Neural Network) 是 R-CNN 系列目标检测算法的初代模型,其将“深度学习”和传统的“计算机视觉”的相结合,在深度学习的框架下实现了高效的物体检测和识别。R-CNN 的核心思想是将目标检测任务分解为候选区域提取、特征提取、目标分类和边界框回归四个步骤。R-CNN 中的 “Region-based” 指的就是区域提议(候选区域),用于在图像中识别对象。在《目标检测基础》中,我们已经了解了区域提议的概念如何从图像中生成候选区域。在本节中,我们将利用区域提议来完成图像中目标对象的检测和定位。

1. R-CNN 目标检测模型

1.1 核心思想

R-CNN 的核心思想是将目标检测任务分解为候选区域提取、特征提取、目标分类和边界框回归四个步骤。首先,使用选择性搜索等方法从输入图像中提取出一组候选区域。然后,对每个候选区域使用卷积神经网络提取特征表示。接下来,通过一个分类器对每个候选区域进行目标分类,输出目标的类别标签。最后,使用回归器对每个候选区域的边界框位置进行微调,以更准确地框出目标的位置。
R-CNN 采用了两个重要的创新点:候选区域提取和共享卷积特征。通过选择性搜索等方法,R-CNN 可以仅对候选区域进行处理,大大减少计算量。同时,R-CNN 通过共享卷积特征来提取每个候选区域的特征表示,从而在目标分类和边界框回归之间实现信息的共享和重复使用。

1.2 算法流程

下图说明了基于 R-CNN 的目标检测模型的工作流程:

PyTorch深度学习实战(20)——从零开始实现R-CNN目标检测-LMLPHP

根据上图,可以观察到基于 R-CNN 模型执行目标检测需要以下步骤:

  1. 在输入图像中提取区域提议,确保提取足够多的区域提议,以免错过图像中的潜在对象
  2. 调整所有区域提议的尺寸,获得具有固定尺寸的输入图像
  3. 将调整后的区域提议输入到网络中,通常使用预训练模型提取区域提议的特征图
  4. 创建数据集以训练模型,其中输入是预训练网络提取到区域提议的特征图,输出是与每个区域提议对应的类别及其相对于真实边界框的偏移量,如果一个区域提议与目标对象的 IoU 大于给定阈值,则该区域负责预测与其重叠的目标对象所属的类别以及区域提议与目标对象的真实边界框之间的偏移量

以下是为根据区域提议得到的边界框偏移和目标对象的真实类别示例:

PyTorch深度学习实战(20)——从零开始实现R-CNN目标检测-LMLPHP

在上图中,o (红色)表示区域提议(虚线边界框)的中心,x 表示人物对象的真实边界框(实线边界框)的中心。计算区域提议边界框与真实边界框之间的偏移量,即计算两个边界框的中心坐标之间的差值 (dx, dy) 以及边界框高度和宽度之间的差值 (dw, dh)

  1. 网络包含两个输出头,一个用于预测目标对象类别,另一个用于预测区域提议与真实边界框之间的偏移量
  2. 训练模型时,编写自定义损失函数同时最小化目标分类误差和边界框偏移量误差,损失函数通常包含两部分:
    (1) 目标分类误差,使用交叉熵损失函数 (Cross-Entropy Loss):
    L c l s = − ∑ i = 1 N y i l o g ( p i ) + ( 1 − y i ) l o g ( 1 − p i ) L_{cls}=-\sum_{i=1}^Ny_ilog(p_i)+(1-y_i)log(1-p_i) Lcls=i=1Nyilog(pi)+(1yi)log(1pi)
    其中, y i ∈ 0 , 1 y_i∈{0,1} yi0,1 表示第 i i i 个样本的真实标签为 01 p i ∈ [ 0 , 1 ] p_i∈[0,1] pi[0,1] 表示模型预测第 i i i 个样本为正样本的概率。
    (2) 边界框偏移量误差,使用 Smooth L1 Loss
    L r e g ( t i , v i ) = s m o o t h L 1 ( t i − v i ) L_{reg}(t_i,v_i)=smooth_{L1}(t_i-v_i) Lreg(ti,vi)=smoothL1(tivi)
    其中, t i t_i ti 表示第 i i i 个样本的真实边界框偏移量, v i v_i vi 表示模型预测的第 i i i 个样本的边界框偏移量, s m o o t h L 1 smooth_{L1} smoothL1Smooth L1 Loss 函数,用于控制误差的范围。
    因此,总体损失函数可以表示为:
    L = L c l s + λ L r e g L=L_{cls}+\lambda L_{reg} L=Lcls+λLreg
    其中, λ \lambda λ 是正则化参数,用于控制目标分类误差和边界框偏移量误差在损失函数中的权重比例。

2. 实现 R-CNN 目标检测

我们已经从理论上讲解了 R-CNN 的工作原理,在本节中,我们将学习如何使用 PyTorch 构建 R-CNN 模型。

  1. 准备数据集
  2. 定义区域提议提取和IoU计算函数
  3. 创建训练数据
    为模型创建输入数据
    调整区域提议尺寸
    使用预训练模型提取区域特征图
    为模型创建输出数据
    使用预定义类别或背景标签标记每个区域提议
    如果区域提议对应于目标对象而不非背景,则获取区域提议与真实边界框之间的偏移量
  4. 定义并训练神经网络模型
  5. 预测新图像

2.1 数据集准备

为了构建目标检测模型,从 Kaggle下载公开数据集,为了简单起见,在代码中,我们只处理包含公共汽车或卡车的图像。下载用于训练模型的数据集并解压,完成后导入所需库并查看数据样本示例:

IMAGE_ROOT = 'open-images-bus-trucks/images'
DF_RAW = pd.read_csv('open-images-bus-trucks/df.csv')
print(DF_RAW.head())

可以看到图像及其相应的标签存储在 CSV 文件中:

PyTorch深度学习实战(20)——从零开始实现R-CNN目标检测-LMLPHP
其中,XMinXMaxYMinYMax 对应于图像中目标对象的边界框坐标,LabelName 对应于图像类别。

数据集下载完成后,我们继续对数据集进行处理以用于模型训练:

  1. 获取每个图像及其对应的类别和边界框
  2. 获取每个图像中的区域提议及其与真实边界框对应的交并比 (Intersection over union, IoU),以及区域提议相对于真实边界框需要进行修正的偏移量
  3. 为每个类别分配数字标签(除了公交车和卡车类别之外,还需要一个额外的背景类别),当区域提议与真实边界框的 IoU 值低于阈值时即被视为背景类别
  4. 将每个区域提议调整为相同大小,以便将它们输入到神经网络

综上,我们需要调整区域提议的大小,为每个区域提议分配标签,并计算区域提议相对于真实边界框的偏移量。

(1) 指定图像的位置并读取 CSV 文件中的真实边界框数据:

import selectivesearch
from torchvision import transforms, models, datasets
from torchvision.ops import nms
import os
import torch
import numpy as np
from torch.utils.data import DataLoader, Dataset
from glob import glob
from random import randint
import cv2
from pathlib import Path
import torch.nn as nn
from torch import optim
from matplotlib import pyplot as plt
import pandas as pd
import matplotlib.patches as mpatches

device = 'cuda' if torch.cuda.is_available() else 'cpu'

IMAGE_ROOT = 'open-images-bus-trucks/images/images'
DF_RAW = pd.read_csv('open-images-bus-trucks/df.csv')
print(DF_RAW.head())

(2) 定义类 OpenImages,返回图像及其包含的目标对象的类别、目标对象边界框以及图像的文件路径。

将数据帧 (df) 和图像文件夹路径 (image_folder) 作为输入传递给 __init__ 方法,并提取数据帧中不重复的 ImageID 值 (self.unique_images),这是因为一张图像可能包含多个对象,因此数据帧中多个行可能对应于相同的 ImageID 值:

class OpenImages(Dataset):
    def __init__(self, df, image_folder=IMAGE_ROOT):
        self.root = image_folder
        self.df = df
        self.unique_images = df['ImageID'].unique()
    def __len__(self):
        return len(self.unique_images)

定义 __getitem__ 方法,获取与索引 (ix) 对应的图像 (image_id),图像中目标对象的边界框坐标 (box)、类别,并返回图像、边界框、类别和图像路径:

    def __getitem__(self, ix):
        image_id = self.unique_images[ix]
        image_path = f'{self.root}/{image_id}.jpg'
        image = cv2.imread(image_path, 1)[...,::-1] # conver BGR to RGB
        h, w, _ = image.shape
        df = self.df.copy()
        df = df[df['ImageID'] == image_id]
        boxes = df['XMin,YMin,XMax,YMax'.split(',')].values
        boxes = (boxes * np.array([w,h,w,h])).astype(np.uint16).tolist()
        classes = df['LabelName'].values.tolist()
        return image, boxes, classes, image_path

(3) 检查样本图像及图像中包含的目标对象的类别和边界框:

ds = OpenImages(df=DF_RAW)
im, bbs, clss, _ = ds[21]

# print(clss)
def show_bbs(im, bbs, clss):
    fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
    ax.imshow(im)
    for ix, (xmin, ymin, xmax, ymax) in enumerate(bbs):
        rect = mpatches.Rectangle(
                (xmin, ymin), xmax-xmin, ymax-ymin, 
                fill=False, 
                edgecolor='red', 
                linewidth=1)
        ax.add_patch(rect)
        centerx = xmin # + new_w/2
        centery = ymin + 20# + new_h - 10
        plt.text(centerx, centery, clss[ix],fontsize = 20,color='red')
    plt.show()

for i in range(20):
    im, bbs, clss, _ = ds[i]
    show_bbs(im, bbs, clss )

(4) 定义 extract_iouextract_candidates 函数:

def extract_candidates(img):
    img_lbl, regions = selectivesearch.selective_search(img, scale=200, min_size=100)
    img_area = np.prod(img.shape[:2])
    candidates = []
    for r in regions:
        if r['rect'] in candidates: continue
        if r['size'] < (0.05*img_area): continue
        if r['size'] > (1*img_area): continue
        x, y, w, h = r['rect']
        candidates.append(list(r['rect']))
    return candidates

def extract_iou(boxA, boxB, epsilon=1e-5):
    x1 = max(boxA[0], boxB[0])
    y1 = max(boxA[1], boxB[1])
    x2 = min(boxA[2], boxB[2])
    y2 = min(boxA[3], boxB[3])
    width = (x2 - x1)
    height = (y2 - y1)
    if (width<0) or (height <0):
        return 0.0
    area_overlap = width * height
    area_a = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1])
    area_b = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1])
    area_combined = area_a + area_b - area_overlap
    iou = area_overlap / (area_combined+epsilon)
    return iou

我们已经定义了准备数据和初始化数据加载器所需的所有函数,在下一节中,我们将获取区域提议(神经网络模型的输入区域)、真实的边界框偏移量以及对象类别(预期输出)。

2.2 获取区域提议和偏移量

在本节中,我们将学习如何创建模型相对应的输入和输出值。模型输入使用选择性搜索提取的候选区域,输出包括候选区域的类别以及(如果候选区域包含目标对象)边界框的偏移量。

(1) 初始化空列表以存储文件路径 (FPATHS)、真实边界框 (GTBBS)、对象类别 (CLSS)、边界框与区域提议之间的偏移量 (DELTAS)、区域提议位置 (ROIS) 和区域提议与真实边界框的交并比 (IOUS):

FPATHS, GTBBS, CLSS, DELTAS, ROIS, IOUS = [], [], [], [], [], []

(2) 遍历数据集并填充初始化后的列表。

使用所有数据样本进行训练,也可以使用部分数据样本训练,数据样本越大,训练时间和准确度就越高:

N = 2000
for ix, (im, bbs, labels, fpath) in enumerate(ds):
    if(ix==N):
        break

使用 extract_candidates() 函数从每个图像 (im) 中提取候选区域,以绝对像素值表示 (XMinXmaxYMinYMax 可作为图像形状的比例给出),并将提取的区域坐标从 (x,y,w,h) 转换为 (x,y,x+w,y+h) 表示:

    H, W, _ = im.shape
    candidates = extract_candidates(im)
    candidates = np.array([(x,y,x+w,y+h) for x,y,w,h in candidates])

初始化 iousroisdeltasclss 为空列表,用于存储每个图像中每个候选区域与真实边界框的交并比、区域提议位置,边界框偏移量和每个候选区域的类别。遍历 SelectiveSearch 中的所有区域提议,并将那些具有较高 IoU 值且属于标签为 bus/truck 类别的区域提议存储为 bus/truck 提议,其余区域提议存储为背景提议:

    ious, rois, clss, deltas = [], [], [], []

将所有候选区域与图像中所有真实边界框的交并比存储在 ious 中,其中 bbs 是图像中不同目标对象的真实边界框,candidates 是区域提议候选项:

    ious = np.array([[extract_iou(candidate, _bb_) for candidate in candidates] for _bb_ in bbs]).T

遍历每个候选项并存储候选 XMin (cx)、YMin (cy)、XMax (cX) 和 YMax (cY) 值:

    for jx, candidate in enumerate(candidates):
        cx,cy,cX,cY = candidate

提取与候选框相对应的所有真实边界框的 IoU 值:

        candidate_ious = ious[jx]

获取具有最高 IoU 的候选区域的索引 (best_iou_at) 以及相应的真实边界框 (best_bb):

        best_iou_at = np.argmax(candidate_ious)
        best_iou = candidate_ious[best_iou_at]
        best_bb = _x,_y,_X,_Y = bbs[best_iou_at]

如果 IoU (best_iou) 大于给定阈值 (0.3),则为候选区域分配对应的类别标签,否则将其标记为背景:

        if best_iou > 0.3:
            clss.append(labels[best_iou_at])
        else:
            clss.append('background')

获取所需的偏移量 (delta) 以将当前区域提议转换为最佳区域提议对应的候选项 best_bb (即真实边界框),换句话说,应调整当前提议坐标,才能使其完全与真实边界框 best_bb 对齐:

        delta = np.array([_x-cx, _y-cy, _X-cX, _Y-cY]) / np.array([W,H,W,H])
        deltas.append(delta)
        rois.append(candidate / np.array([W,H,W,H]))

将文件路径、IoUroi、类别偏移量和真实边界框添加到结果列表中:

    FPATHS.append(fpath)
    IOUS.append(ious)
    ROIS.append(rois)
    CLSS.append(clss)
    DELTAS.append(deltas)
    GTBBS.append(bbs)

获取图像路径名称并将获取到的所有信息——FPATHSIOUSROISCLSSDELTASGTBBS 存储在列表中:

FPATHS, GTBBS, CLSS, DELTAS, ROIS = [item for item in [FPATHS, GTBBS, CLSS, DELTAS, ROIS]]

到目前为止,类别形式依旧是它们的名称,在神经网络模型训练过程,需要将类别转换为对应的索引,背景类别的索引为 0,公交车类别的索引为 1,卡车类别的索引为 2

(3) 为每个类别分配索引:

targets = pd.DataFrame([clss for l in CLSS for clss in l], columns=['label'])
label2target = {l:t for t,l in enumerate(targets['label'].unique())}
target2label = {t:l for l,t in label2target.items()}
background_class = label2target['background']

我们已经为每个区域提议分配了一个类别,并创建了边界框偏移作为另一目标输出。在下一节中,我们将获取与获得的信息 (FPATHSIOUSROISCLSSDELTASGTBBS) 相对应的数据集和数据加载器。

2.3 创建训练数据

我们已经获取了所有的图像数据、区域提议,并得到了每个区域提议中目标对象的类别,以及区域提议与真实边界框相对应的偏移量,这些区域提议与真实边界框具有较高的交并比 (Intersection over union, IoU)。在本节中,我们将根据区域提议的真实标签准备数据集类,并从中创建数据加载器。
接下来,我们将每个区域提议调整为相同的形状并进行归一化。

(1) 定义对图像执行归一化的函数:

normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406],
                                 std=[0.229, 0.224, 0.225])

(2) 定义函数 preprocess_image() 预处理图像 (img),在该函数中,调整通道顺序,对图像进行归一化,并将其注册到设备中:

def preprocess_image(img):
    img = torch.tensor(img).permute(2,0,1)
    img = normalize(img)
    return img.to(device).float()

(3) 定义函数解码类别预测结果:

def decode(_y):
    _, preds = _y.max(-1)
    return preds

(4) 使用预处理后的区域提议和真实标签,定义数据集类 RCNNDataset

class RCNNDataset(Dataset):
    def __init__(self, fpaths, rois, labels, deltas, gtbbs):
        self.fpaths = fpaths
        self.gtbbs = gtbbs
        self.rois = rois
        self.labels = labels
        self.deltas = deltas
    def __len__(self):
        return len(self.fpaths)

根据区域提议获取缩放图像,并获取与类别和边界框偏移相关的真实标签:

    def __getitem__(self, ix):
        fpath = str(self.fpaths[ix])
        image = cv2.imread(fpath, 1)[...,::-1]
        H, W, _ = image.shape
        sh = np.array([W,H,W,H])
        gtbbs = self.gtbbs[ix]
        rois = self.rois[ix]
        bbs = (np.array(rois)*sh).astype(np.uint16)
        labels = self.labels[ix]
        deltas = self.deltas[ix]
        crops = [image[y:Y,x:X] for (x,y,X,Y) in bbs]
        return image, crops, bbs, labels, deltas, gtbbs, fpath

定义 collate_fn,执行裁剪图像的缩放和归一化 (preprocess_image):

    def collate_fn(self, batch):
        input, rois, rixs, labels, deltas = [], [], [], [], []
        for ix in range(len(batch)):
            image, crops, image_bbs, image_labels, image_deltas, image_gt_bbs, image_fpath = batch[ix]
            crops = [cv2.resize(crop, (224,224)) for crop in crops]
            crops = [preprocess_image(crop/255.)[None] for crop in crops]
            input.extend(crops)
            labels.extend([label2target[c] for c in image_labels])
            deltas.extend(image_deltas)
        input = torch.cat(input).to(device)
        labels = torch.Tensor(labels).long().to(device)
        deltas = torch.Tensor(deltas).float().to(device)
        return input, labels, deltas

(5) 创建训练、验证数据集和数据加载器:

n_train = 9*len(FPATHS)//10
train_ds = RCNNDataset(FPATHS[:n_train], ROIS[:n_train], CLSS[:n_train], DELTAS[:n_train], GTBBS[:n_train])
test_ds = RCNNDataset(FPATHS[n_train:], ROIS[n_train:], CLSS[n_train:], DELTAS[n_train:], GTBBS[n_train:])
print(len(test_ds))
from torch.utils.data import TensorDataset, DataLoader
train_loader = DataLoader(train_ds, batch_size=2, collate_fn=train_ds.collate_fn, drop_last=True)
test_loader = DataLoader(test_ds, batch_size=2, collate_fn=test_ds.collate_fn, drop_last=True)

2.4 构建 R-CNN 架构

我们已经了解了如何准备数据,在本节中,我们将学习如何构建R-CNN目标检测模型用于预测区域提议类别及其对应的偏移量,以便在图像中的目标对象周围绘制边界框:

  1. 定义 VGG 主干网络用于提取图像特征
  2. 使用预训练模型获取经过归一化缩放后的区域提议特征
  3. VGG 主干网络上添加带有 sigmoid 激活的全连接层,以预测对应于区域提议的类别
  4. 添加另一全连接层预测边界框的四个偏移量
  5. 为以上两个输出(一个预测类别,另一个预测边界框的四个偏移量)定义损失函数
  6. 训练模型,预测区域提议的类别和边界框的四个偏移量

(1) 定义 VGG 主干网络:

vgg_backbone = models.vgg16(pretrained=True)
vgg_backbone.classifier = nn.Sequential()
for param in vgg_backbone.parameters():
    param.requires_grad = False
vgg_backbone.eval().to(device)

(2) 定义 R-CNN 网络模块。

定义模型类:

class RCNN(nn.Module):
    def __init__(self):
        super().__init__()

定义主干网络 (self.backbone),以及输入分支计算类别分数 (self.cls_score) 和边界框偏移值 (self.bbox):

        feature_dim = 25088
        self.backbone = vgg_backbone
        self.cls_score = nn.Linear(feature_dim, len(label2target))
        self.bbox = nn.Sequential(
              nn.Linear(feature_dim, 512),
              nn.ReLU(),
              nn.Linear(512, 4),
              nn.Tanh(),
            )

定义对应于类预测 (self.cel) 和边界框偏移回归 (self.sl1) 的损失函数:

        self.cel = nn.CrossEntropyLoss()
        self.sl1 = nn.L1Loss()

定义前向传播方法 forward,利用 VGG 主干网路 (self.backbone) 获取图像特征 (feat),然后进一步将其通过分类和边界框回归方法传递,以获取类别概率 (cls_score) 和边界框偏移量 (bbox):

    def forward(self, input):
        feat = self.backbone(input)
        cls_score = self.cls_score(feat)
        bbox = self.bbox(feat)
        return cls_score, bbox

定义损失函数 (calc_loss),如果真实类别为背景,不会计算与偏移量对应的回归损失:

    def calc_loss(self, probs, _deltas, labels, deltas):
        detection_loss = self.cel(probs, labels)
        ixs, = torch.where(labels != 0)
        _deltas = _deltas[ixs]
        deltas = deltas[ixs]
        self.lmb = 10.0
        if len(ixs) > 0:
            regression_loss = self.sl1(_deltas, deltas)
            return detection_loss + self.lmb * regression_loss, detection_loss.detach(), regression_loss.detach()
        else:
            regression_loss = 0
            return detection_loss + self.lmb * regression_loss, detection_loss.detach(), regression_loss

(3) 定义函数 train_batch(),用于在批数据上训练模型:

def train_batch(inputs, model, optimizer, criterion):
    input, clss, deltas = inputs
    model.train()
    optimizer.zero_grad()
    _clss, _deltas = model(input)
    loss, loc_loss, regr_loss = criterion(_clss, _deltas, clss, deltas)
    accs = clss == decode(_clss)
    loss.backward()
    optimizer.step()
    return loss.detach(), loc_loss, regr_loss, accs.cpu().numpy()

(4) 定义函数 validate_batch(),用于验证模型:

@torch.no_grad()
def validate_batch(inputs, model, criterion):
    input, clss, deltas = inputs
    with torch.no_grad():
        model.eval()
        _clss,_deltas = model(input)
        loss, loc_loss, regr_loss = criterion(_clss, _deltas, clss, deltas)
        _, _clss = _clss.max(-1)
        accs = clss == _clss
    return _clss, _deltas, loss.detach(), loc_loss, regr_loss, accs.cpu().numpy()

(5) 创建模型对象,获取损失,然后定义优化器和训练 epoch 数:

rcnn = RCNN().to(device)
criterion = rcnn.calc_loss
optimizer = optim.SGD(rcnn.parameters(), lr=1e-3)
n_epochs = 10
  1. 训练模型:
train_loss_epochs = []
train_loc_loss_epochs = []
train_regr_loss_epochs = []
train_acc_epochs = []
val_loc_loss_epochs = []
val_regr_loss_epochs = []
val_loss_epochs = []
val_acc_epochs = []
for epoch in range(n_epochs):
    train_loss = []
    train_loc_loss = []
    train_regr_loss = []
    train_acc = []
    val_loc_loss = []
    val_regr_loss = []
    val_loss = []
    val_acc = []
    _n = len(train_loader)
    for ix, inputs in enumerate(train_loader):
        loss, loc_loss, regr_loss, accs = train_batch(inputs, rcnn, 
                                                      optimizer, criterion)
        pos = (epoch + (ix+1)/_n)
        train_loss.append(loss.item())
        train_loc_loss.append(loc_loss.item())
        train_regr_loss.append(regr_loss.item())
        train_acc.append(accs.mean())
    train_loss_epochs.append(np.average(train_loss))
    train_loc_loss_epochs.append(np.average(train_loc_loss))
    train_regr_loss_epochs.append(np.average(train_regr_loss))
    train_acc_epochs.append(np.average(train_acc))
        
    _n = len(test_loader)
    for ix,inputs in enumerate(test_loader):
        _clss, _deltas, loss, \
        loc_loss, regr_loss, accs = validate_batch(inputs, 
                                                rcnn, criterion)
        pos = (epoch + (ix+1)/_n)
        val_loss.append(loss.item())
        val_loc_loss.append(loc_loss.item())
        val_regr_loss.append(regr_loss.item())
        val_acc.append(accs.mean())
    val_loss_epochs.append(np.average(val_loss))
    val_loc_loss_epochs.append(np.average(val_loc_loss))
    val_regr_loss_epochs.append(np.average(val_regr_loss))
    val_acc_epochs.append(np.average(val_acc))

模型在训练和验证数据上的损失变化情况如下:

epochs = np.arange(n_epochs)+1
plt.subplot(121)
plt.plot(epochs, train_acc_epochs, 'bo', label='Training accuracy')
plt.plot(epochs, val_acc_epochs, 'r', label='Test accuracy')
plt.title('Training and Test accuracy over increasing epochs')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()
plt.grid('off')
plt.subplot(122)
plt.plot(epochs, train_loss_epochs, 'bo', label='Training loss')
plt.plot(epochs, val_loss_epochs, 'r', label='Test loss')
plt.title('Training and Test loss over increasing epochs')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.grid('off')
plt.show()

PyTorch深度学习实战(20)——从零开始实现R-CNN目标检测-LMLPHP

3. R-CNN目标检测模型测试

在本节中,我们将利用训练后的 R-CNN 模型来预测和绘制目标对象边界框以及边界框内的目标对象类别:

  1. 在测试图像中提取区域提议
  2. 调整每个区域提议的大小并进行归一化
  3. 将经过处理的区域提议图像通过前向传播后预测类别和偏移量。
  4. 执行非极大值抑制,仅获取具有包含对象的具有最高置信度的边界框

(1) 修改 show_bbs() 函数用于可视化 R-CNN 检测结果:

def show_bbs(im, bbs, clss, ax):
    # fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
    ax.imshow(im)
    for ix, (xmin, ymin, xmax, ymax) in enumerate(bbs):
        rect = mpatches.Rectangle(
                (xmin, ymin), xmax-xmin, ymax-ymin, 
                fill=False, 
                edgecolor='red', 
                linewidth=1)
        ax.add_patch(rect)
        centerx = xmin # + new_w/2
        centery = ymin + 20# + new_h - 10
        plt.text(centerx, centery, clss[ix],fontsize = 20,color='red')

(2) 定义函数 test_predictions() 在测试图像上进行预测。

函数将文件名作为输入:

def test_predictions(filename, show_output=True):

读取图像并提取候选区域:

    img = np.array(cv2.imread(filename, 1)[...,::-1])
    candidates = extract_candidates(img)
    candidates = [(x,y,x+w,y+h) for x,y,w,h in candidates]

循环遍历候选区域,调整图像大小并预处理图像:

    input = []
    for candidate in candidates:
        x,y,X,Y = candidate
        crop = cv2.resize(img[y:Y,x:X], (224,224))
        input.append(preprocess_image(crop/255.)[None])
    input = torch.cat(input).to(device)

预测类别和偏移量:

    with torch.no_grad():
        rcnn.eval()
        probs, deltas = rcnn(input)
        probs = torch.nn.functional.softmax(probs, -1)
        confs, clss = torch.max(probs, -1)

提取不属于背景类别的候选区域,并将候选区域与预测的边界框偏移值相加得到预测边界框:

    candidates = np.array(candidates)
    confs, clss, probs, deltas = [tensor.detach().cpu().numpy() for tensor in [confs, clss, probs, deltas]]

    ixs = clss!=background_class
    confs, clss, probs, deltas, candidates = [tensor[ixs] for tensor in [confs, clss, probs, deltas, candidates]]
    bbs = (candidates + deltas).astype(np.uint16)

使用非极大值抑制 (non-maximum suppression, NMS) 消除重复边界框 (IoU 大于 0.05 的边界框可以认为是重复的),在重复的边界框中,选择置信度最高的边界框,并丢弃其余边界框:

    ixs = nms(torch.tensor(bbs.astype(np.float32)), torch.tensor(confs), 0.05)
    confs, clss, probs, deltas, candidates, bbs = [tensor[ixs] for tensor in [confs, clss, probs, deltas, candidates, bbs]]
    if len(ixs) == 1:
        confs, clss, probs, deltas, candidates, bbs = [tensor[None] for tensor in [confs, clss, probs, deltas, candidates, bbs]]

获取置信度最高的边界框:

    if len(confs) == 0 and not show_output:
        return (0,0,224,224), 'background', 0
    if len(confs) > 0:
        best_pred = np.argmax(confs)
        best_conf = np.max(confs)
        best_bb = bbs[best_pred]
        x,y,X,Y = best_bb

绘制图像与预测边界框:

    _, ax = plt.subplots(1, 2, figsize=(20,10))
    ax[0].imshow(img)
    # show(img, ax=ax[0])
    ax[0].grid(False)
    ax[0].set_title('Original image')
    print(len(confs))
    if len(confs) == 0:
        ax[1].imshow(img)
        ax[1].set_title('No objects')
        plt.show()
        return
    ax[1].set_title(target2label[clss[best_pred]])
    show_bbs(img, bbs=bbs.tolist(), clss=[target2label[c] for c in clss.tolist()], ax=ax[1])
    # ax[1].title('predicted bounding box and class')
    plt.show()
    return (x,y,X,Y),target2label[clss[best_pred]],best_conf

(3) 在测试图像上执行函数 test_predictions

for i in range(30):
    image, crops, bbs, labels, deltas, gtbbs, fpath = test_ds[i]
    test_predictions(fpath)

PyTorch深度学习实战(20)——从零开始实现R-CNN目标检测-LMLPHP
使用测试图像生成预测结果大约需要 1.5 秒,大部分时间用于生成区域提议、调整每个区域提议的尺寸、将它们输入到 VGG 主干网络、使用训练后的模型生成预测结果。

小结

R-CNN 是基于候选区域的经典目标检测算法,其将卷积神经网络引入目标检测领域,其思想和方法为后续的目标检测算法发展奠定了基础。尽管 R-CNN 在目标检测领域取得了很大的成功,但因为它需要逐个处理候选区域,导致其速度较慢。本文首先介绍了 R-CNN 模型的核心思想与目标检测流程,然后使用 PyTorch 从零开始实现了一个基于 R-CNN 的目标检测模型。

系列链接

PyTorch深度学习实战(1)——神经网络与模型训练过程详解
PyTorch深度学习实战(2)——PyTorch基础
PyTorch深度学习实战(3)——使用PyTorch构建神经网络
PyTorch深度学习实战(4)——常用激活函数和损失函数详解
PyTorch深度学习实战(5)——计算机视觉基础
PyTorch深度学习实战(6)——神经网络性能优化技术
PyTorch深度学习实战(7)——批大小对神经网络训练的影响
PyTorch深度学习实战(8)——批归一化
PyTorch深度学习实战(9)——学习率优化
PyTorch深度学习实战(10)——过拟合及其解决方法
PyTorch深度学习实战(11)——卷积神经网络
PyTorch深度学习实战(12)——数据增强
PyTorch深度学习实战(13)——可视化神经网络中间层输出
PyTorch深度学习实战(14)——类激活图
PyTorch深度学习实战(15)——迁移学习
PyTorch深度学习实战(16)——面部关键点检测
PyTorch深度学习实战(17)——多任务学习
PyTorch深度学习实战(18)——目标检测基础

10-07 11:03