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

news2024/11/18 7:42:29

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

    • 0. 前言
    • 1. R-CNN 目标检测模型
      • 1.1 核心思想
      • 1.2 算法流程
    • 2. 实现 R-CNN 目标检测
      • 2.1 数据集准备
      • 2.2 获取区域提议和偏移量
      • 2.3 创建训练数据
      • 2.4 构建 R-CNN 架构
    • 3. R-CNN目标检测模型测试
    • 小结
    • 系列链接

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 的目标检测模型的工作流程:

R-CNN

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

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

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

区域提议与边界框偏移

在上图中,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 文件中:

数据样本示例
其中,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()

模型性能检测

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)

测试图像
使用测试图像生成预测结果大约需要 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)——目标检测基础

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1065226.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JUC系列-09】深入理解ReentrantReadWriteLock的底层实现

JUC系列整体栏目 内容链接地址【一】深入理解JMM内存模型的底层实现原理https://zhenghuisheng.blog.csdn.net/article/details/132400429【二】深入理解CAS底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/132478786【三】熟练掌握Atomic原子系列基本…

浅谈智能安全配电装置在老年人建筑中的应用

摘要&#xff1a;我国每年因触电伤亡人数非常多&#xff0c;大多数事故是发生在用电设备和配电装置。在电气事故中&#xff0c;无法预料和不可抗拒的事故是比较少的&#xff0c;大量用电事故可采取切实可行措施来预防。本文通过结合老年人建筑的特点和智能安全配电装置的功能&a…

教你三步搞定VsCode调试C++

目录 1 配置编译任务2 配置调试任务3 进行调试 1 配置编译任务 使用VsCode进行C开发时&#xff0c;除了在机器上安装必要的编译工具&#xff08;例如&#xff0c;gcc、g、cmake等&#xff09;之外&#xff0c;还需要在VsCode配置编译任务&#xff0c;从而可以通过点击或者快捷…

【MySql】mysql之进阶查询语句

目录 一、常用查询 1、order by按关键字排序❤ 1.1 升序排序 1.2 降序排序 1.3 结合where进项条件过滤再排序 1.4 多字段排序 2、and和or判断 2.1 and和or的使用 2.2 嵌套、多条件使用 3、distinct 查询不重复记录 4、group by 对结果进行分组 5、limit限制结果…

MySQL57部署与配置[Windows10]

下载原始安装包 https://dev.mysql.com/downloads/installer/https://downloads.mysql.com/archives/notifier/默认安装 MySQL57 默认安装 MySQL Notifier 环境变量配置 Path: C:\Program Files\MySQL\MySQL Server 5.7\binDBeaver数据库连接

【MySql】4- 实践篇(二)

文章目录 1. SQL 语句为什么变“慢”了1.1 什么情况会引发数据库的 flush 过程呢&#xff1f;1.2 四种情况性能分析1.3 InnoDB 刷脏页的控制策略 2. 数据库表的空间回收2.1 innodb_file_per_table参数2.2 数据删除流程2.3 重建表2.4 Online 和 inplace 3. count(*) 语句怎样实现…

websocket拦截

python实现websocket拦截 前言一、拦截的优缺点优点缺点二、实现方法1.环境配置2.代码三、总结现在的直播间都是走的websocket通信,想要获取websocket通信的内容就需要使用websocket拦截,大多数是使用中间人代理进行拦截,这里将会使用更简单的方式进行拦截。 前言 开发者工…

RK3568平台开发系列讲解(外设篇)AP3216C 三合一环境传感器驱动

🚀返回专栏总目录 文章目录 一、AP3216C 简介二、AP3216C驱动程序2.1、设备树修改2.2、驱动程序沉淀、分享、成长,让自己和他人都能有所收获!😄 📢在本篇将介绍AP3216C 三合一环境传感器的驱动。 一、AP3216C 简介 AP3216C 是由敦南科技推出的一款传感器,其支持环境光…

OpenWrt使用Privoxy插件修改UA

OpenWrt使用privoxy修改UA 1.安装privoxy插件 SSH连接到路由器 更新插件列表 update opkg安装插件 opkg install privoxy luci-app-privoxy luci-i18n-privoxy-zh-cn重启路由器 2.配置privoxy 打开配置页面 文件和目录 访问和控制 转发 杂项 日志 编辑配置 浏览器打开 …

Kaggle - LLM Science Exam(一):赛事概述、数据收集、BERT Baseline

文章目录 一、赛事概述1.1 OpenBookQA Dataset1.2 比赛背景1.3 评估方法和代码要求1.4 比赛数据集1.5 优秀notebook 二、BERT Baseline2.1 数据预处理2.2 定义data_collator2.3 加载模型&#xff0c;配置trainer并训练2.4 预测结果并提交2.5 deberta-v3-large 1k Wiki&#xff…

深入理解Linux网络笔记(三):内核和用户进程协作之epoll

本文为《深入理解Linux网络》学习笔记&#xff0c;使用的Linux源码版本是3.10&#xff0c;网卡驱动默认采用的都是Intel的igb网卡驱动 Linux源码在线阅读&#xff1a;https://elixir.bootlin.com/linux/v3.10/source 2、内核是如何与用户进程协作的&#xff08;二&#xff09; …

Godot 官方2D游戏笔记(1):导入动画资源和添加节点

前言 Godot 官方给了我们2D游戏和3D游戏的案例&#xff0c;不过如果是独立开发者只用考虑2D游戏就可以了&#xff0c;因为2D游戏纯粹&#xff0c;我们只需要关注游戏的玩法即可。2D游戏的美术素材简单&#xff0c;交互逻辑简单&#xff0c;我们可以把更多的时间放在游戏的玩法…

苍穹外卖

1、基础知识扫盲 项目从0到1 需求分析->设计->编码->测试->上线运维 角色 项目经理&#xff1a;对整个项目负责&#xff0c;任务分配&#xff0c;把控进度 产品经理&#xff1a;进行需求调研&#xff0c;输出需求调研文档&#xff0c;产品原型 UI设计师&…

【java计算机毕设】 留守儿童爱心捐赠管理系统 springboot vue html mysql 送文档ppt

1.项目视频演示 【java计算机毕设】留守儿童爱心捐赠管理系统 springboot vue html mysql 送文档ppt 2.项目功能截图 3.项目简介 后端&#xff1a;springboot&#xff0c;前端&#xff1a;vue&#xff0c;html&#xff0c;数据库&#xff1a;mysql&#xff0c;开发软件idea 留…

Springboot使用Aop保存接口请求日志到mysql

1、添加aop依赖 <!-- aop日志 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency> 2、新建接口保存数据库的实体类RequestLog.java package com.example…

volatile关键字使用总结

先说结论 1. volatile关键字可以让编译器层面减少优化&#xff0c;每次使用时必须从内存中取数据&#xff0c;而不是从cpu缓存或寄存器中获取 2. volatile关键字不能完全禁止指令重排&#xff0c;准确地说是两个volatile修饰的变量之间的命令不会进行指令重排 3. 使用volati…

BLE协议栈1-物理层PHY

从应届生开始做ble开发也差不读四个月的时间了&#xff0c;一直在在做上层的应用&#xff0c;对蓝牙协议栈没有过多的时间去了解&#xff0c;对整体的大方向概念一直是模糊的状态&#xff0c;在开发时也因此遇到了许多问题&#xff0c;趁有空去收集了一下资料来完成了本次专栏&…

毕业设计选题之Android基于移动端的线上订餐app外卖点餐安卓系统源码 调试 开题 lw

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人七年开发经验&#xff0c;擅长Java、Python、PHP、.NET、微信小程序、爬虫、大数据等&#xff0c;大家有这一块的问题可以一起交流&#xff01; &#x1f495;&…

【gcc】RtpTransportControllerSend学习笔记

本文是对大神 webrtc源码分析(8)-拥塞控制(上)-码率预估 的学习笔记。看了啥也没记住,所以跟着看代码先。CongestionControlHandler 在底层网络可用的时候,会触发RtpTransportControllerSend::OnNetworkAvailability()回调,这里会尝试创建CongestionControlHandler创建后即刻…

在VS Code中优雅地编辑csv文件

文章目录 Rainbow csv转表格CSV to Tablecsv2tableCSV to Markdown Table Edit csv 下面这些插件对csv/tsv/psv都有着不错的支持&#xff0c;这几种格式的主要区别是分隔符不同。 功能入口/使用方法Rainbow csv按列赋色右键菜单CSV to Table转为ASCII表格指令CSV to Markdown …