CV - 目标检测

发布于:2025-04-07 ⋅ 阅读:(26) ⋅ 点赞:(0)

物体检测

目标检测和图片分类的区别:

图像分类(Image Classification)

目的:图像分类的目的是识别出图像中主要物体的类别。它试图回答“图像是什么?”的问题。
输出:通常输出是一个标签或一组概率值,表示图像属于各个预定义类别的可能性。例如,对于一张包含猫的图片,分类器可能会输出“猫”这个标签。
应用场景:适用于只需要了解图像整体内容的场景,如识别照片中的动物种类、区分不同的风景类型等。

目标检测(Object Detection)

目的:目标检测不仅需要识别图像中所有感兴趣物体的类别,还需要确定每个物体在图像中的具体位置。它试图回答“图像中有什么?它们在哪里?”的问题。
输出:除了给出物体的类别外,还会输出物体所在的边界框(bounding box),即用矩形框标记出每个物体的位置。例如,在自动驾驶场景下,系统不仅要能识别出行人、车辆等物体,还要精确地定位它们的位置以便做出安全决策。
应用场景:适合于需要知道图像内特定对象位置的情况,比如视频监控、自动驾驶汽车、医学影像分析等领域。

边缘框

  • 一个边缘框可以通过 4 个数字定义
    • (左上 x,左上 y,右下 x,右下 y)
    • (左上 x,左上 y,宽,高)
      在这里插入图片描述

目标检测数据集

  • 每行表示一个物体
    • 图片文件名,物体类别,边缘框
  • COCO (cocodataset.org)
    • 80 物体,330K 图片,1.5M 物体

总结

  • 物体检测识别图片里的多个物体的类别和位置
  • 位置通常用边缘框表示。

边缘框实现

%matplotlib inline
import torch
from d2l import torch as d2l

d2l.set_figsize()
img = d2l.plt.imread('../img/catdog.jpg')
d2l.plt.imshow(img);

在这里插入图片描述
定义在这两种表示法之间进行转换的函数

#@save
def box_corner_to_center(boxes):
    """从(左上,右下)转换到(中间,宽度,高度)"""
    x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
    cx = (x1 + x2) / 2
    cy = (y1 + y2) / 2
    w = x2 - x1
    h = y2 - y1
    boxes = torch.stack((cx, cy, w, h), axis=-1)
    return boxes

#@save
def box_center_to_corner(boxes):
    """从(中间,宽度,高度)转换到(左上,右下)"""
    cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
    x1 = cx - 0.5 * w
    y1 = cy - 0.5 * h
    x2 = cx + 0.5 * w
    y2 = cy + 0.5 * h
    boxes = torch.stack((x1, y1, x2, y2), axis=-1)
    return boxes

定义图像中狗和猫的边界框

# bbox是边界框的英文缩写
dog_bbox, cat_bbox = [60.0, 45.0, 378.0, 516.0], [400.0, 112.0, 655.0, 493.0]

boxes = torch.tensor((dog_bbox, cat_bbox))
box_center_to_corner(box_corner_to_center(boxes)) == boxes

在这里插入图片描述
将边界框在图中画出

#@save
def bbox_to_rect(bbox, color):
    # 将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式:
    # ((左上x,左上y),宽,高)
    return d2l.plt.Rectangle(
        xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1],
        fill=False, edgecolor=color, linewidth=2)

fig = d2l.plt.imshow(img)
fig.axes.add_patch(bbox_to_rect(dog_bbox, 'blue'))
fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red'));

在这里插入图片描述

数据集

李沐老师收集并标记了一个小型数据集,下面首先是下载该数据集:

%matplotlib inline
import os
import pandas as pd
import torch
import torchvision
from d2l import torch as d2l

#@save
d2l.DATA_HUB['banana-detection'] = (
    d2l.DATA_URL + 'banana-detection.zip',
    '5de26c8fce5ccdea9f91267273464dc968d20d72')

读取香蕉检测数据集

#@save
def read_data_bananas(is_train=True):
    """读取香蕉检测数据集中的图像和标签"""
    data_dir = d2l.download_extract('banana-detection')
    csv_fname = os.path.join(data_dir, 'bananas_train' if is_train
                             else 'bananas_val', 'label.csv')
    csv_data = pd.read_csv(csv_fname)
    csv_data = csv_data.set_index('img_name')
    images, targets = [], []
    for img_name, target in csv_data.iterrows():
        images.append(torchvision.io.read_image(
            os.path.join(data_dir, 'bananas_train' if is_train else
                         'bananas_val', 'images', f'{img_name}')))
        # 这里的target包含(类别,左上角x,左上角y,右下角x,右下角y),
        # 其中所有图像都具有相同的香蕉类(索引为0)
        targets.append(list(target))
    return images, torch.tensor(targets).unsqueeze(1) / 256

创建一个自定义 Dataseet 实例:

#@save
class BananasDataset(torch.utils.data.Dataset):
    """一个用于加载香蕉检测数据集的自定义数据集"""
    def __init__(self, is_train):
        self.features, self.labels = read_data_bananas(is_train)
        print('read ' + str(len(self.features)) + (f' training examples' if
              is_train else f' validation examples'))

    def __getitem__(self, idx):
        return (self.features[idx].float(), self.labels[idx])

    def __len__(self):
        return len(self.features)

为训练集和测试集返回两个数据加载器实例

#@save
def load_data_bananas(batch_size):
    """加载香蕉检测数据集"""
    train_iter = torch.utils.data.DataLoader(BananasDataset(is_train=True),
                                             batch_size, shuffle=True)
    val_iter = torch.utils.data.DataLoader(BananasDataset(is_train=False),
                                           batch_size)
    return train_iter, val_iter

读取一个小批量,并打印其中的图像和标签的形状

batch_size, edge_size = 32, 256
train_iter, _ = load_data_bananas(batch_size)
batch = next(iter(train_iter))
batch[0].shape, batch[1].shape

在这里插入图片描述
演示:

imgs = (batch[0][0:10].permute(0, 2, 3, 1)) / 255
axes = d2l.show_images(imgs, 2, 5, scale=2)
for ax, label in zip(axes, batch[1][0:10]):
    d2l.show_bboxes(ax, [label[0][1:5] * edge_size], colors=['w'])

在这里插入图片描述

QA 思考

Q1:如果在工业检测中数据集非常小(近百张),除了进行数据增强外,还有什么更好的方法吗?
A1:迁移学习:找一个非常好的,在一个比较大的目标检测数据集上训练的比较好的模型,然后拿过来进行微调。近百张其实也不算小的了,只是说模型训练出来不是很好。数据增强的话,对图像进行处理,对应的框也必须要相应的变化一下,这是比较麻烦的点。

后记

自己实现了一遍,然后也是使用的李沐老师在课件中提供的狗猫图片:

import torch
from matplotlib import pyplot as plt
from PIL import Image

def set_image_display_size():
    plt.figure(figsize=(8, 6))

def load_image(img_path):
    return Image.open(img_path)

def show_single_image(img):
    plt.imshow(img)
    plt.axis('on')  # 显示坐标轴
    plt.show()

def box_corner_to_center(boxes):
    x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
    cx = (x1 + x2) / 2
    cy = (y1 + y2) / 2
    w = x2 - x1
    h = y2 - y1
    boxes = torch.stack((cx, cy, w, h), axis=-1)
    return boxes

def box_center_to_corner(boxes):
    # 这里向左和向上都是减小的
    cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
    x1 = cx - 0.5 * w
    y1 = cy - 0.5 * h
    x2 = cx + 0.5 * w
    y2 = cy + 0.5 * h
    boxes = torch.stack((x1, y1, x2, y2), axis=-1)
    return boxes

def bbox_to_rect(bbox, color):
    # 将边界框(左上x, 左上y, 右下x, 右下y)格式转换成matplotlib格式:
    # ((左上x, 左上y), 宽, 高)
    return plt.Rectangle(
        xy=(bbox[0], bbox[1]), width=bbox[2] - bbox[0], height=bbox[3] - bbox[1],
        fill=False, edgecolor=color, linewidth=2)

if __name__ == "__main__":
    # 设置图像显示大小
    set_image_display_size()

    # 加载图像
    img_path = '../image/catdog.jpg'  # 确保路径正确
    img = load_image(img_path)
    show_single_image(img)

    # dog, cat 边界框
    dog_bbox, cat_bbox = [60.0, 45.0, 378.0, 516.0], [400.0, 112.0, 655.0, 493.0]

    # 转换为张量并测试转换函数是否正确
    boxes = torch.tensor((dog_bbox, cat_bbox))
    converted_boxes = box_center_to_corner(box_corner_to_center(boxes))
    # torch.allclose,判断两个张量是否在一定容差范围内相等。
    print("转换是否一致:", torch.allclose(converted_boxes, boxes))

    # 显示图像并绘制边界框,imshow 将 img 显示在画布上
    fig = plt.imshow(img)
    # add_patch 是 matplotlib 中用于在坐标轴上添加图形元素(如矩形、圆形等)的函数
    fig.axes.add_patch(bbox_to_rect(dog_bbox, 'blue'))  # 狗的边界框
    fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red'))  # 猫的边界框
    plt.show()

我在colab上跑了一下下面的代码,发现可以,下载不了的可能需要一点 “魔法”

import hashlib
import os
import tarfile
import zipfile
import pandas as pd
import requests
import torch
import torchvision

from matplotlib import pyplot as plt

"""
    这段代码本身并没有包含训练过程。它只是加载了已经标注好的香蕉检测数据集,并可视化了图像及其对应的边界框。
    这些边界框信息是预先标注好的(存储在 CSV 文件中),而不是通过模型训练得到的。
    
    read_data_bananas 函数读取数据集中的图像和标签:
        图像存储在文件夹中(如 bananas_train/images/)。
        标签存储在 CSV 文件中(如 bananas_train/label.csv)。
        每个标签包括图像名称、目标类别(香蕉类别的索引为 0)、以及边界框坐标。
        
    zip 结构类似如下:
            banana-detection/
        ├── bananas_train/
        │   ├── images/
        │   │   ├── image1.jpg
        │   │   ├── image2.jpg
        │   │   └── ...
        │   └── label.csv  # 包含每张图像的标注信息(即边界框和类别)
        └── bananas_val/
            ├── images/
            │   ├── image1.jpg
            │   ├── image2.jpg
            │   └── ...
            └── label.csv
"""
DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'

DATA_HUB['banana-detection'] = (
    DATA_URL + 'banana-detection.zip',
    '5de26c8fce5ccdea9f91267273464dc968d20d72')


def download(name, cache_dir=os.path.join('..', 'data')):
    """下载一个DATA_HUB中的文件,返回本地文件名"""
    # assert 条件, 错误信息
    assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}"
    """
        url:文件的下载地址。 http://d2l-data.s3-accelerate.amazonaws.com/banana-detection.zip
        sha1_hash:文件内容的 SHA-1 校验值,用于验证文件完整性。5de26c8fce5ccdea9f91267273464dc968d20d72
    """
    url, sha1_hash = DATA_HUB[name]
    """
        递归创建目录:包括所有必要的父目录。
        避免重复创建导致的错误:如果目录已经存在,不会抛出异常。
    """
    os.makedirs(cache_dir, exist_ok=True)
    """
        url.split('/')[-1]:提取 URL 路径的最后一部分(通常是文件名)。
        os.path.join(cache_dir, ...):将缓存目录和文件名组合成完整的本地文件路径。
    """
    fname = os.path.join(cache_dir, url.split('/')[-1])  # fname = ../data/banana-detection.zip
    if os.path.exists(fname):  # 文件存在,进入校验流程
        # 创建一个 SHA-1 哈希对象,用于计算文件的哈希值。
        sha1 = hashlib.sha1()
        # 以二进制模式读取文件
        with open(fname, 'rb') as f:
            while True:
                # 每次读取 1MB 数据(1048576 字节),更新哈希对象。
                data = f.read(1048576)
                # 为空,退出循环
                if not data:
                    break
                # 将读取的数据块逐步添加到哈希计算中
                sha1.update(data)
        """
            计算哈希值:
                调用 sha1.hexdigest() 获取最终的 SHA-1 哈希值(40 个字符的十六进制字符串)。
            校验哈希值:
                将计算得到的哈希值与预期的哈希值 sha1_hash 进行比较。
                如果两者相等,说明文件完整且未被篡改,直接返回文件路径(命中缓存)。
                否则,继续执行下载逻辑。
        """
        if sha1.hexdigest() == sha1_hash:
            return fname  # 命中缓存
    print(f'正在从{url}下载{fname}...')
    """
        使用 requests.get 方法发送 HTTP 请求,从远程服务器下载文件。
        参数说明:
            stream=True:以流式方式下载文件,避免一次性加载整个文件到内存中。
            verify=True:启用 SSL 证书验证,确保安全连接。
    """
    r = requests.get(url, stream=True, verify=True)
    """
        打开本地文件 fname,以二进制写入模式('wb')创建或覆盖文件。
            将下载的内容 r.content 写入文件。
            下载完成后,关闭文件。
    """
    with open(fname, 'wb') as f:
        f.write(r.content)
    return fname


def download_extract(name, folder=None):
    """下载并解压zip/tar文件"""
    fname = download(name)  # 下载文件的完整路径
    """
        os.path.dirname(fname):提取文件所在的目录路径(即父目录)。
            例如,如果 fname 是 '../data/example.zip',则 base_dir 是 '../data'。
        os.path.splitext(fname):将文件路径拆分为文件名和扩展名。
            例如,如果 fname 是 '../data/example.zip',则 data_dir 是 '../data/example',ext 是 '.zip'。
    """
    base_dir = os.path.dirname(fname)
    data_dir, ext = os.path.splitext(fname)
    if ext == '.zip':
        fp = zipfile.ZipFile(fname, 'r')
    elif ext in ('.tar', '.gz'):
        fp = tarfile.open(fname, 'r')
    else:
        assert False, '只有zip/tar/gz 文件可以被解压缩'
    # 解压文件到指定的目录 base_dir
    fp.extractall(base_dir)
    """
        如果 folder 参数存在:
            返回 os.path.join(base_dir, folder),即解压后目录下的指定子目录。
        如果 folder 参数不存在:
            返回 data_dir,即解压后的默认目录。
    """
    return os.path.join(base_dir, folder) if folder else data_dir


def read_data_bananas(is_train=True):
    """读取香蕉检测数据集中的图像和标签"""
    data_dir = download_extract('banana-detection')
    """
        根据 is_train 参数,确定加载训练集还是验证集的标注文件:
            如果 is_train=True,加载 'bananas_train/label.csv'。
            如果 is_train=False,加载 'bananas_val/label.csv'。
        使用 pandas 库的 read_csv 方法读取 CSV 文件内容。
        将 img_name 列设置为索引列(方便后续按图像名称访问数据)。
    """
    csv_fname = os.path.join(data_dir, 'bananas_train' if is_train
    else 'bananas_val', 'label.csv')  # for example: ../data/banana-detection/banana_train/label.csv
    csv_data = pd.read_csv(csv_fname)  # 读取 csv 文件
    # 将 img_name 列设置为索引列
    csv_data = csv_data.set_index('img_name')
    """
        images:用于存储读取的图像数据。
        targets:用于存储每张图像对应的标注信息。
    """
    images, targets = [], []
    """
        img_name:当前行的索引值(即图像名称)。
        target:当前行的数据(即标注信息)。
    """
    for img_name, target in csv_data.iterrows():
        images.append(torchvision.io.read_image(
            os.path.join(data_dir, 'bananas_train' if is_train else
            'bananas_val', 'images', f'{img_name}')))
        # 这里的target包含(类别,左上角x,左上角y,右下角x,右下角y),
        # 其中所有图像都具有相同的香蕉类(索引为0)
        targets.append(list(target))
    """
        images:图像数据列表。
        torch.tensor(targets).unsqueeze(1) / 256:
        将 targets 转换为 PyTorch 张量。
        使用 unsqueeze(1) 在维度 1 上增加一个维度,使其形状变为 (N, 1, 5),其中 N 是样本数量,5 是每个标注信息的长度。
        所有边界框坐标除以 256,进行归一化处理(假设图像大小为 256x256)。
    """
    return images, torch.tensor(targets).unsqueeze(1) / 256


class BananasDataset(torch.utils.data.Dataset):
    """一个用于加载香蕉检测数据集的自定义数据集"""

    def __init__(self, is_train):
        self.features, self.labels = read_data_bananas(is_train)
        print('read ' + str(len(self.features)) + (f' training examples' if
                                                   is_train else f' validation examples'))
        # such as : read 1000 training examples

    def __getitem__(self, idx):
        return self.features[idx].float(), self.labels[idx]

    def __len__(self):
        return len(self.features)


def load_data_bananas(batch_size):
    """加载香蕉检测数据集"""
    train_iter = torch.utils.data.DataLoader(BananasDataset(is_train=True),
                                             batch_size, shuffle=True)
    val_iter = torch.utils.data.DataLoader(BananasDataset(is_train=False),
                                           batch_size)
    return train_iter, val_iter


def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
    figsize = (num_cols * scale, num_rows * scale)
    """
        _:表示整个图形对象(这里用下划线忽略)
        axes:表示所有子图的数组
    """
    _, axes = plt.subplots(num_rows, num_cols, figsize=figsize)
    axes = axes.flatten()
    for i, (ax, img) in enumerate(zip(axes, imgs)):
        if torch.is_tensor(img):
            ax.imshow(img.numpy())
        else:
            ax.imshow(img)
        ax.axes.get_xaxis().set_visible(False)
        ax.axes.get_yaxis().set_visible(False)
        if titles:
            ax.set_title(titles[i])
    return axes


def bbox_to_rect(bbox, color):
    """将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式:
    ((左上x,左上y),宽,高)"""
    return plt.Rectangle(
        xy=(bbox[0], bbox[1]), width=bbox[2] - bbox[0], height=bbox[3] - bbox[1],
        fill=False, edgecolor=color, linewidth=2)


# 定义一个名为 numpy 的函数,它能够将 PyTorch 张量转换为 NumPy 数组,并且自动处理张量的梯度信息
numpy = lambda x, *args, **kwargs: x.detach().numpy(*args, **kwargs)


def show_bboxes(axes, bboxes, labels=None, colors=None):
    """显示所有边界框"""
    """
        功能:将输入对象转换为列表形式。
        如果 obj 是 None,返回默认值 default_values。
        如果 obj 不是列表或元组,则将其包装成单元素列表。
        否则,直接返回 obj。
        作用:确保 labels 和 colors 参数始终是列表形式,以便后续迭代操作。
    """

    def _make_list(obj, default_values=None):
        if obj is None:
            obj = default_values
        elif not isinstance(obj, (list, tuple)):
            obj = [obj]
        return obj

    """
        将 labels 转换为列表形式。如果未提供标签,则返回空列表。
        将 colors 转换为列表形式。如果未提供颜色,则使用默认颜色列表 ['b', 'g', 'r', 'm', 'c'](蓝色、绿色、红色、洋红色、青色)。
    """
    labels = _make_list(labels)
    colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])
    """
        遍历 bboxes 列表中的每个边界框 bbox。
        根据索引 i,从 colors 列表中循环选择颜色(防止颜色不足时重复使用)。
        调用 bbox_to_rect 函数,将边界框坐标转换为 Matplotlib 的矩形对象。
        假设 bbox_to_rect 是一个已定义的函数,用于将边界框坐标转换为 matplotlib.patches.Rectangle 对象。
        numpy(bbox):将边界框数据转换为 NumPy 数组(假设 bbox 是 PyTorch 张量)。
        使用 axes.add_patch(rect) 将矩形添加到绘图区域中。
    """
    for i, bbox in enumerate(bboxes):
        color = colors[i % len(colors)]
        rect = bbox_to_rect(numpy(bbox), color)
        axes.add_patch(rect)
        """
            检查是否提供了标签,并且当前索引 i 在标签范围内。
            根据边界框颜色选择标签文字的颜色:
            如果边界框颜色为白色('w'),标签文字颜色为黑色('k')。
            否则,标签文字颜色为白色('w')。
            使用 axes.text 方法,在边界框的左上角位置添加标签文本。
            rect.xy[0] 和 rect.xy[1]:矩形左上角的 x 和 y 坐标。
            va='center' 和 ha='center':设置文本垂直和水平居中对齐。
            fontsize=9:设置字体大小为 9。
            color=text_color:设置文本颜色。
            bbox=dict(facecolor=color, lw=0):为文本添加背景框,背景颜色与边界框一致,边框宽度为 0。
        """
        if labels and len(labels) > i:
            text_color = 'k' if color == 'w' else 'w'
            axes.text(rect.xy[0], rect.xy[1], labels[i],
                      va='center', ha='center', fontsize=9, color=text_color,
                      bbox=dict(facecolor=color, lw=0))


if __name__ == "__main__":
    batch_size, edge_size = 32, 256
    train_iter, _ = load_data_bananas(batch_size)
    """
        使用 iter(train_iter) 创建一个迭代器对象。
        调用 next() 获取迭代器的第一个批次的数据。
    """
    batch = next(iter(train_iter))
    """
        torch.Size([32, 3, 256, 256]):表示 32 张 RGB 图像,每张图像大小为 256x256。
        torch.Size([32, num_boxes, 5]):表示每张图像有 num_boxes 个边界框,每个边界框包含 5 个值(类别 + 坐标)。    
    """
    print(batch[0].shape, batch[1].shape)
    """
    功能:
        batch[0][0:10]:从 batch[0] 中提取前 10 张图像。
        .permute(0, 2, 3, 1):调整张量的维度顺序,将 (N, C, H, W) 转换为 (N, H, W, C),即从 PyTorch 的默认格式转换为适合显示的格式。
        / 255:将像素值归一化到 [0, 1] 范围(假设原始像素值范围是 [0, 255])。
    结果:
        imgs 是一个形状为 (10, 256, 256, 3) 的张量,表示 10 张归一化的 RGB 图像。
    """
    imgs = (batch[0][0:10].permute(0, 2, 3, 1)) / 255
    axes = show_images(imgs, 2, 5, scale=2)
    for ax, label in zip(axes, batch[1][0:10]):
        """
            label[0][1:5]:提取第一个边界框的坐标信息([x_min, y_min, x_max, y_max])。
            * edge_size:将归一化的坐标值还原为原始像素坐标。
            show_bboxes(ax, ...):在子图 ax 上绘制边界框,颜色为白色 ('w')。
        """
        show_bboxes(ax, [label[0][1:5] * edge_size], colors=['w'])
    plt.show()
"""
归一化是为了模型训练:确保输入数据在合理的范围内,加速收敛并提高稳定性。
还原是为了可视化:将数据转换回原始范围,以便人类可以直观地理解图像和标注信息。
"""

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到