目标检测案例

本文介绍了目标检测的基本概念、传统算法及基于深度学习的最新进展。涵盖One-Stage和Two-Stage算法的区别与特点,并通过实际案例展示了Faster R-CNN的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目标检测

理论

1. 什么是目标检测

目标检测即找出图像中所有感兴趣的物体,包含物体定位和物体分类两个子任务,同时确定物体的类别和位置。

2.传统算法概述

传统目标检测的方法一般分为三个阶段:首先在给定的图像上选择一些候选的区域,然 后对这些区域提取特征,最后使用训练的分类器进行分类。下面我们对这三个阶段分别进行介绍。

(1)区域选择:这一步是为了对目标的位置进行定位。由于目标可能出现在图像的任何位置,而且目标的大小、长宽比例也不确定,所以最初采用滑动窗口的策略对整幅图像进行遍历,而且需要设置不同的尺度,不同的长宽比。这种穷举的策略虽然包含了目标所有可能出现的位置,但是缺点也是显而易见的:时间复杂度太高,产生冗余窗口太多,这也严重影响后续特征提取和分类的速度和性能。

(2)特征提取:由于目标的形态多样性,光照变化多样性,背景多样性等因素使得设计一个鲁棒的特征并不是那么容易。然而提取特征的好坏直接影响到分类的准确性。

(3)分类:根据第二步提取到的特征对目标进行分类,分类器主要有 SVM,AdaBoost 等。

3.基于深度学习的目标检测算法

3.1算法概述

目标检测任务可分为两个关键的子任务:目标分类目标定位。目标分类任务负责判断输入图像或所选择图像区域(Proposals)中是否有感兴趣类别的物体出现,输出一系列带分数的标签表明感兴趣类别的物体出现在输入图像或所选择图像区域(Proposals)中的可能性。目标定位任务负责确定输入图像或所选择图像区域(Proposals)中感兴趣类别的物体的位置和范围,输出物体的包围盒、或物体中心、或物体的闭合边界等,通常使用方形包围盒,即Bounding Box用来表示物体的位置信息。

目前主流的目标检测算法主要是基于深度学习模型,大概可以分成两大类别:(1)One-Stage目标检测算法,这类检测算法不需要Region Proposal阶段,可以通过一个Stage直接产生物体的类别概率和位置坐标值,比较典型的算法有YOLO、SSD和CornerNet;(2)Two-Stage目标检测算法,这类检测算法将检测问题划分为两个阶段,第一个阶段首先产生候选区域(Region Proposals),包含目标大概的位置信息,然后第二个阶段对候选区域进行分类和位置精修,这类算法的典型代表有R-CNN,Fast R-CNN,Faster R-CNN等。目标检测模型的主要性能指标是检测准确度和速度,其中准确度主要考虑物体的定位以及分类准确度。一般情况下,Two-Stage算法在准确度上有优势,而One-Stage算法在速度上有优势。不过,随着研究的发展,两类算法都在两个方面做改进,均能在准确度以及速度上取得较好的结果。
在这里插入图片描述

3.2 One-Stage目标检测算法

One-Stage目标检测算法可以在一个stage直接产生物体的类别概率和位置坐标值,相比于Two-Stage的目标检测算法不需要Region Proposal阶段,整体流程较为简单。如下图所示,在Testing的时候输入图片通过CNN网络产生输出,解码(后处理)生成对应检测框即可;在Training的时候则需要将Ground Truth编码成CNN输出对应的格式以便计算对应损失loss。
在这里插入图片描述
目前对于One-Stage算法的主要创新主要集中在如何设计CNN结构、如何构建网络目标以及如何设计损失函数上,接下来我将从这三个方面进行阐述。

3.2.1 如何设计CNN结构

设计CNN网络结构主要有两个方向,分别为追求精度和追求速度。最简单的一种实现方式就是替换Backbone网络结构,即使用不同的基础网络结构对图像提取特征。举例来说,ResNet101的表征能力要强于MobileNet,然而MobileNet的计算量要远远低于ResNet101,如果将ResNet101替换为MobileNet,那么检测网络在精度应该会有一定的损失,但是在速度上会有一定提升;如果将MobileNet替换为ResNet101,那么检测网络在速度上会有一定的损失,但是在精度上会有一定的提升。

3.2.2 如何构建回归目标

如何构建网络回归目标即如何区分正负样本使其与卷积神经网络的输出相对应,最简单直接的方法是直接回归物体的相关信息(类别和坐标),稍微复杂一些,在回归坐标时可以回归物体坐标相对于anchor的偏移量等等。对于One-Stage检测方法主要有如下三种典型的回归目标构建方式,其中代表方法分别为YOLO系列算法、SSD系列算法以及CornerNet目标检测算法

3.2.3 如何设计损失函数

目标检测算法主要分为两个子任务,分别为物体分类和物体定位。损失主要包括分类损失(Cls Loss)和定位损失(Loc Loss),常见的损失组合主要有如下两种Cls Loss + Loc Loss(SSD系列算法)、Cls Loss + Obj Loss + Loc Loss (YOLO系列算法),其中YOLO系列算法相比于SSD系列算法多了Object Loss,即判断对应区域是否为物体的损失。除此之外,One-Stage目标检测算法的正负样本不均衡的问题比较严重,对于设计损失函数还会有一些针对创新。

Hard Negative Mining:即对于大量的负样本只挑取其中适当比例的损失较大的负样本来计算损失,其余损失较小的负样本忽略不计,防止负样本过多干扰网络学习;

Focal Loss:由于大多数都是简单易分的负样本(属于背景的样本),使得训练过程不能充分学习到属于那些有类别样本的信息;其次简单易分的负样本太多,可能掩盖了其他有类别样本的作用。Focal Loss希望那些hard examples对损失的贡献变大,使网络更倾向于从这些样本上学习。

3.3 Two-Stage目标检测算法

Two-Stage目标检测算法看作是进行两次One-Stage检测,第一个Stage初步检测出物体位置,第二个Stage对第一个阶段的结果做进一步的精化,对每一个候选区域进行One-Stage检测。整体流程如下图所示,在Testing的时候输入图片经过卷积神经网络产生第一阶段输出,对输出进行解码处理生成候选区域,然后获取对应候选区域的特征表示(ROIs),然后对ROIs进一步精化产生第二阶段的输出,解码(后处理)生成最终结果,解码生成对应检测框即可;在Training的时候需要将Ground Truth编码成CNN输出对应的格式以便计算对应损失loss。
在这里插入图片描述如上图所示,Two-Stage的两个阶段拆开来看均与One-Stage检测算法相似,所以我觉得Two-Stage可以看成是两个One-Stage检测算法的组合,第一个Stage做初步检测,剔除负样本,生成初步位置信息(Region of Interest),第二个Stage再做进一步精化并生成最终检测结果。目前对于Two-Stage算法的主要创新主要集中在如何高效准确地生成Proposals、如何获取更好的ROI features、如何加速Two-Stage检测算法以及如何改进后处理方法,接下来我将从这几个方面进行阐述。

3.3.1 如何高效准确地生成Proposals

如何高效准确地生成Proposals考虑的是Two-Stage检测算法的第一个Stage,获取初步的检测结果,供下一个Stage做进一步精化。接下来我将通过对比R-CNN、Faster R-CNN和FPN来做简要说明。

  1. R-CNN:生成Proposals的方法是传统方法Selective Search,主要思路是通过图像中的纹理、边缘、颜色等信息对图像进行自底向上的分割,然后对分割区域进行不同尺度的合并,每个生成的区域即一个候选Proposal。这种方法基于传统特征,速度较慢。
  2. Faster R-CNN:使用RPN网络代替了Selective Search方法,大大提高了生成Proposals的速度,具体实现策略同One-Stage检测算法.
  3. FPN:Faster R-CNN只采用顶层特征做预测,但我们知道低层的特征语义信息比较少,但是目标位置准确;高层的特征语义信息比较丰富,但是目标位置比较粗略。FPN算法把低分辨率、高语义信息的高层特征和高分辨率、低语义信息的低层特征进行自上而下的侧边连接,使得所有尺度下的特征都有丰富的语义信息,然后在不同尺度的特征层上进行预测,使得生成Proposals的效果优于只在顶层进行预测的Faster R-CNN算法。
3.3.2 如何获取更好的ROI features

在获取Proposals之后,如何获取更好的ROI features是Two-Stage检测算法第二个Stage的关键,只有输入比较鲁棒的情况下才能得到较好的输出。对于这个问题主要考虑的有两个方向,其一是如何获取Proposals的features,其二是得到Proposals的features之后如何align到同一个尺度

案例

本案例来自于kaggle全球小麦竞赛数据集,数据集和代码已经放在我的gitee上
gitee地址

1. 导入依赖包

import pandas as pd
import numpy as np
import cv2
import re
import albumentations as A
from albumentations.pytorch.transforms import ToTensorV2
import torch
import torchvision
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
from torch.utils.data import DataLoader, Dataset
from matplotlib import pyplot as plt
from tqdm.notebook import tqdm

2. 定义变量

num_classes = 2
num_epochs = 10
lr_scheduler = None
itr = 1
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
cpu_device = torch.device("cpu")

3. 加工数据集

class WheatDataset(Dataset):
    def __init__(self, dataframe, image_dir, transforms=None):
        super().__init__()
        self.image_ids = dataframe['image_id'].unique()
        self.df = dataframe
        self.image_dir = image_dir
        self.transforms = transforms
    def __getitem__(self, index: int):
        image_id = self.image_ids[index]
        records = self.df[self.df['image_id'] == image_id]
        image = cv2.imread(f'{self.image_dir}/{image_id}.jpg', cv2.IMREAD_COLOR)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)
        image /= 255.0
        boxes = records[['x', 'y', 'w', 'h']].values
        boxes[:, 2] = boxes[:, 0] + boxes[:, 2]
        boxes[:, 3] = boxes[:, 1] + boxes[:, 3]
        area = (boxes[:, 3] - boxes[:, 1]) * (boxes[:, 2] - boxes[:, 0])
        area = torch.as_tensor(area, dtype=torch.float32)
        labels = torch.ones((records.shape[0],), dtype=torch.int64)
        iscrowd = torch.zeros((records.shape[0],), dtype=torch.int64)
        target = {}
        target['boxes'] = boxes
        target['labels'] = labels
        # target['masks'] = None
        target['image_id'] = torch.tensor([index])
        target['area'] = area
        target['iscrowd'] = iscrowd
        if self.transforms:
            sample = {
                'image': image,
                'bboxes': target['boxes'],
                'labels': labels
            }
            sample = self.transforms(**sample)
            image = sample['image']

            target['boxes'] = torch.stack(tuple(map(torch.tensor, zip(*sample['bboxes'])))).permute(1, 0)
        return image, target, image_id
    def __len__(self) -> int:
        return self.image_ids.shape[0]
def collate_fn(batch):
    return tuple(zip(*batch))
# 训练集
def get_train_transform():
    return A.Compose([
        A.Flip(0.5),
        ToTensorV2(p=1.0)
    ], bbox_params={'format': 'pascal_voc', 'label_fields': ['labels']})
def expand_bbox(x):
    r = np.array(re.findall("([0-9]+[.]?[0-9]*)", x))
    if len(r) == 0:
        r = [-1, -1, -1, -1]
    return r
DIR_TRAIN = f'./train/'
train_df = pd.read_csv(f'./train.csv')
train_df[['x', 'y', 'w', 'h']] = np.stack(train_df['bbox'].apply(lambda x: expand_bbox(x)))
train_df.drop(columns=['bbox'], inplace=True)
train_df['x'] = train_df['x'].astype(np.float)
train_df['y'] = train_df['y'].astype(np.float)
train_df['w'] = train_df['w'].astype(np.float)
train_df['h'] = train_df['h'].astype(np.float)
image_ids = train_df['image_id'].unique()
train_ids = image_ids[:-665]
valid_ids = image_ids[-665:]
valid_df = train_df[train_df['image_id'].isin(valid_ids)]
train_df = train_df[train_df['image_id'].isin(train_ids)]
train_dataset = WheatDataset(train_df, DIR_TRAIN, get_train_transform())
# indices = torch.randperm(len(train_dataset)).tolist()
train_data_loader = DataLoader(
    train_dataset,
    batch_size=16,
    shuffle=False,
    num_workers=4,
    collate_fn=collate_fn
)
# 验证集
def get_valid_transform():
    return A.Compose([
        ToTensorV2(p=1.0)
    ], bbox_params={'format': 'pascal_voc', 'label_fields': ['labels']})
valid_dataset = WheatDataset(valid_df, DIR_TRAIN, get_valid_transform())
valid_data_loader = DataLoader(
    valid_dataset,
    batch_size=8,
    shuffle=False,
    num_workers=4,
    collate_fn=collate_fn
)

# 测试集
# DIR_TEST = f'./test/'

4. 定义模型

in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
model.to(device)

5. 定义损失函数和优化器

params = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.SGD(params, lr=0.005, momentum=0.9, weight_decay=0.0005)

6. 运行

class Averager:
    def __init__(self):
        self.current_total = 0.0
        self.iterations = 0.0

    def send(self, value):
        self.current_total += value
        self.iterations += 1

    @property
    def value(self):
        if self.iterations == 0:
            return 0
        else:
            return 1.0 * self.current_total / self.iterations
    def reset(self):
        self.current_total = 0.0
        self.iterations = 0.0
loss_hist = Averager()
# lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)
for epoch in range(num_epochs):
    loss_hist.reset()
    for images, targets, image_ids in tqdm(train_data_loader):
        images = list(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())
        loss_value = losses.item()
        loss_hist.send(loss_value)
        optimizer.zero_grad()
        losses.backward()
        optimizer.step()
        if itr % 50 == 0:
            print(f"Iteration #{itr} loss: {loss_value}")
        itr += 1
    if lr_scheduler is not None:
        lr_scheduler.step()
    print(f"Epoch #{epoch} loss: {loss_hist.value}")

在这里插入图片描述

7. 可视化

# 从训练集上测试
images, targets, image_ids = next(iter(train_data_loader))
images = list(image.to(device) for image in images)
targets = [{k: v.to(device) for k, v in t.items()} for t in targets]
boxes = targets[2]['boxes'].cpu().numpy().astype(np.int32)
sample = images[2].permute(1,2,0).cpu().numpy()
fig, ax = plt.subplots(1, 1, figsize=(16, 8))
for box in boxes:
    cv2.rectangle(sample,
                  (box[0], box[1]),
                  (box[2], box[3]),
                  (220, 0, 0), 3)
ax.set_axis_off()
ax.imshow(sample)

在这里插入图片描述

从验证集上测试

model.eval()
images, targets, image_ids = next(iter(train_data_loader))
images = list(image.to(device) for image in images)
targets = model(images)
targets = [{k: v.to(device) for k, v in t.items()} for t in targets]
boxes = targets[2]['boxes'].cpu().detach().numpy().astype(np.int32)
sample = images[2].permute(1,2,0).cpu().numpy()
fig, ax = plt.subplots(1, 1, figsize=(16, 8))
for box in boxes:
    cv2.rectangle(sample,
                  (box[0], box[1]),
                  (box[2], box[3]),
                  (220, 0, 0), 3)
ax.set_axis_off()
ax.imshow(sample)

在这里插入图片描述

8. 保存

torch.save(model.state_dict(), "fasterrcnn_resnet50_fpn.th")
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值