使用基于CNN的本地化器进行对象本地化

介绍

对象定位是指在图像中准确识别和定位感兴趣的对象的任务。它在计算机视觉应用中发挥着关键作用,使得对象检测、跟踪和分割等任务成为可能。在基于 CNN 的定位器的上下文中,对象定位涉及训练卷积神经网络(CNN)来预测边界框的坐标,该边界框紧密围绕图像中的对象。

定位过程通常遵循两步流程,其中骨干 CNN 提取图像特征,回归头预测边界框坐标。

学习目标

  • 了解卷积神经网络(CNN)的基础知识。
  • 解释用于定位模型的 CNN 架构。
  • 使用预训练的 CNN 模型实现定位器架构。

本文是数据科学博客马拉松的一部分。

卷积神经网络(CNNs)

卷积神经网络(CNN)是一类用于图像分析的深度学习模型。

它们的结构包括一个输入 ,该层接收图像数据,接着是使用卷积滤波器学习和提取特征的卷积 。激活函数引入非线性,池化层 减少空间维度。最后是 全连接层,用于最终预测。

CNN 学习分层特征,从低级特征(如边缘)开始,逐渐进展到复杂和抽象的特征(如形状和对象组合)。

在 CNN 的训练阶段,网络会自动学习识别和提取不同层次的特征。初始层捕捉低级特征,如边缘、角落和纹理,而更深层次则学习更复杂和抽象的特征,如形状、对象部件和对象组合。CNN 的分层结构允许其学习对平移、缩放、旋转和其他图像变换越来越不变的表示。

基于 CNN 的定位器架构

对象定位的基于 CNN 的定位器模型由 3 个组件组成:

1. CNN 骨干

利用 SQL 的力量:选择标准的 CNN 架构(如 ResNet 18、ResNet 50、VGG 等)来微调在 Imagenet 分类任务上预训练的模型。通过增加其他 CNN 层来增强骨干网络的特征图大小。

2. 向量化器

CNN 骨干的输出是一个 3D 张量。但定位器的结果输出是一个包含四个值的 1D 向量,对应于边界框的每个坐标。为了将 3D 张量转换为向量,我们使用向量化器或使用 Flatten 层作为替代方法。

3. 回归头

我们为这个任务构建了一个专门的完全连接的回归头。之后,从骨干获取的特征向量被馈送到回归头。回归头在末端包含 4 个节点,对应于 (x1、y1、x2、y2) 或任何其他等价的边界框表示。

更好地理解模型架构

该图显示了常见的基于 CNN 的定位器模型架构。简而言之,CNN 骨干接收 RGB 图像,然后生成特征图。然后我们使用一个 Flatten 层或全局平均池化层来形成一个 1D 特征向量。完全连接的回归头接收特征向量并进行预测。

CNN 网络为输入图像维护固定的大小,我们使用 Flatten 层将从 CNN 骨干获取的特征图转换为向量。然而,当使用 GAP(全局平均池化)等自适应层时,无需调整图像大小。

训练本地化器

导入必要的库

import ast
import math
import os

import cv2
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf

from functools import partial

from tensorflow.data import Dataset
from tensorflow.keras.applications import ResNet50
from tensorflow.keras import layers, losses, models, optimizers, utils 

构建组件

该架构接受大小为300×300,具有3个颜色通道的输入图像。

  • 骨干网络处理图像并提取高级特征。
  • 向量化器计算这些特征的固定长度向量表示。
  • 最后,回归头接收此向量并执行回归,输出4维向量作为最终预测。
IMG_SHAPE = (300, 300)

backbone = models.Sequential([
    ResNet50(include_top=False, 
    weights='imagenet', 
    input_shape=IMG_SHAPE + (3,)),
    layers.Conv2D(1024, 3, 2, activation='relu'),
    ], name='backbone' )

vectorizer = layers.GlobalAveragePooling2D(name='GAP_vectorizer')

regression_head = models.Sequential([
    layers.Dense(512, activation='relu'),
    layers.Dense(4)
], name='regression_head')

构建模型

通过组合先前定义的组件,即骨干网络、向量化器和回归头,定义一个完整的模型。

bbox_regressor = models.Sequential([
    backbone, 
    vectorizer,
    regression_head
])

bbox_regressor.summary()

utils.plot_model(bbox_regressor, "localizer.png", show_shapes=True)

下载数据集

我们使用自拍数据集。自拍数据集包含46,836张自拍照片。我们使用Haar级联生成面部边界框。一个CSV文件包含大约22K张图片的图像路径和边界框坐标。

该数据集可在以下网址获得:

https://www.crcv.ucf.edu/data/Selfie/Selfie-dataset.tar.gz

生成数据批次

DataGenerator类负责加载和预处理本地化任务的现有数据。

  • 它接受图像目录和包含图像路径和边界框信息的CSV文件作为输入。
  • 该类基于提供的分数将数据分为训练和测试子集。
  • 在生成期间,该类通过调整大小、转换颜色通道和规范化像素值来预处理每个图像。
  • 边界框坐标也将被规范化。

生成器为每个数据样本生成预处理的图像和相应的边界框。

class DataGenerator(object):
    def __init__(self, img_dir, _csv_path, train_max=0.8, test_min=0.9, target_shape=(300, 300)):
        for k, v in locals().items():
            if k != "self" and not k.startswith("_"):
                setattr(self, k, v)
        
        self.df = pd.read_csv(_csv_path)
        
    def __len__(self):
        return len(self.df)
        
    def generate(self, phase):
        assert phase in [None, 'train', 'test']
        _df = self.divide_data(phase)

        for rel_img_path, bbox in _df.values:
            img, bbox = self.preprocess_data(rel_img_path, bbox)
            img = tf.constant(img, dtype=tf.float32)
            bbox = tf.constant(bbox, dtype=tf.float32)
            yield img, bbox

    def preprocess_data(self, rel_img_path, bbox):
        bbox = np.array(ast.literal_eval(bbox))

        img_path = os.path.join(self.img_dir, rel_img_path)

        img = cv2.imread(img_path)
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        _h, _w, _ = img.shape
        img = cv2.resize(img, self.target_shape)
        img = img.astype(np.float32) / 127.0 - 1

        bbox = bbox / np.array([_w, _h, _w, _h])

        return img, bbox # np.expand_dims(bbox, 0)

    def divide_data(self, phase):
        train_max = int(self.train_max * len(self.df))
        
        _df = None
        
        if phase is None:
            _df = self.df
        elif phase == 'train':
            _df = self.df.iloc[:train_max, :].sample(frac=1)
        else:
            _df = self.df.iloc[train_max:, :]
            
        return _df 

加载和创建数据集

使用DataGenerator类和TensorFlow的Dataset API创建训练和测试数据集。

  • 使用TensorFlow的Dataset API创建训练和测试数据集。
  • 我们通过调用DataGenerator实例的“generate”方法,并指定“train”阶段来生成训练数据集。
  • 使用“test”阶段生成测试数据集。
  • 这两个数据集都被随机打乱,并使用批量大小为16的批量。

生成的train_dataset和test_dataset是TensorFlow Dataset对象,可以进一步处理或训练模型。

IMG_DIR = 'Selfie-dataset/images'
CSV_PATH = '3-lv1-8-4-selfies_dataset.csv'

BATCH_SIZE = 16

dataset_generator = DataGenerator(IMG_DIR, CSV_PATH)
train_max = int(len(dataset_generator) * 0.9)

train_dataset = Dataset.from_generator(partial(dataset_generator.generate,
 phase='train'), output_types=(tf.float32, tf.float32), 
 output_shapes = (IMG_SHAPE + (3,), (4,)))
 
train_dataset = train_dataset.shuffle(buffer_size=2 * BATCH_SIZE).batch(BATCH_SIZE)

test_dataset = Dataset.from_generator(partial(dataset_generator.generate, 
  phase='test'),output_types=(tf.float32, tf.float32), 
  output_shapes = (IMG_SHAPE + (3,), (4,)))

test_dataset = test_dataset.shuffle(buffer_size=2 * BATCH_SIZE).batch(BATCH_SIZE)

损失函数和性能度量

多种回归损失函数可用于训练边界框定位器。像MSE和Smooth L1这样的回归损失函数与其他回归任务的情况类似,并在真实边界框向量和预测边界框向量之间应用。

交并比(IoU)是用于边界框回归的常见性能度量。

该函数定义了一组函数,用于计算交并比(IoU)并评估模型预测的性能。它提供了计算IoU、以损失和IoU评估预测的手段,并将评估标准分配给变量。

def cal_IoU(b1, b2):
    zero = tf.convert_to_tensor(0., b1.dtype)

    b1_x1, b1_y1, b1_x2, b1_y2 = tf.unstack(b1, 4, axis=-1)
    b2_x1, b2_y1, b2_x2, b2_y2 = tf.unstack(b2, 4, axis=-1)
    
    b1_width = tf.maximum(zero, b1_x2 - b1_x1)
    b1_height = tf.maximum(zero, b1_y2 - b1_y1)
    b2_width = tf.maximum(zero, b2_x2 - b2_x1)
    b2_height = tf.maximum(zero, b2_y2 - b2_y1)
    
    b1_area = b1_width * b1_height
    b2_area = b2_width * b2_height

    intersect_x1 = tf.maximum(b1_x1, b2_x1)
    intersect_y1 = tf.maximum(b1_y1, b2_y1)
  
    intersect_y2 = tf.minimum(b1_y2, b2_y2)
    intersect_x2 = tf.minimum(b1_x2, b2_x2)

    intersect_width = tf.maximum(zero, intersect_x2 - intersect_x1)
    intersect_height = tf.maximum(zero, intersect_y2 - intersect_y1)
    
    intersect_area = intersect_width * intersect_height

    union_area = b1_area + b2_area - intersect_area
    iou = tf.math.divide_no_nan(intersect_area, union_area)
    return iou


def calculate_iou(y_true, y_pred):
    y_pred = tf.convert_to_tensor(y_pred)
    y_pred = tf.cast(y_pred, tf.float32)
    y_true = tf.cast(y_true, y_pred.dtype)
    iou = cal_IoU(y_pred, y_true)
    return iou


def evaluate(actual, pred):
    iou = calculate_iou(actual, pred)
    loss = losses.MSE(actual, pred)
    return loss, iou

criteron = evaluate

优化器和学习率调度器

我们使用指数衰减学习率来调度学习率,并使用Adam优化器进行优化。

EPOCHS = 10
LEARNING_RATE = 0.0003

lr_scheduler = optimizers.schedules.ExponentialDecay(LEARNING_RATE, 3600, 0.8)
optimizer = optimizers.Adam(learning_rate=lr_scheduler)

os.makedirs('checkpoints', exist_ok=True)

训练循环

它实现了一个训练循环,运行指定的epoch数。

  • 在每个epoch中,循环迭代训练数据集的批次。
  • 它执行前向传播以获得预测的边界框坐标,计算损失和IoU值,应用反向传播来更新模型的权重,并记录训练指标。
  • 每个epoch之后,计算平均训练损失和IoU。

模型在每个epoch结束时保存。

for epoch in range(EPOCHS):
    train_losses, train_ious = np.array([]), np.array([])

    for step, (inputs, labels) in enumerate(train_dataset):
      
        with tf.GradientTape() as tape:
            preds = bbox_regressor(inputs, training=True)
            loss, iou = criteron(labels, preds)

        grads = tape.gradient(loss, bbox_regressor.trainable_weights)
        optimizer.apply_gradients(zip(grads, bbox_regressor.trainable_weights))
        
        loss_value = tf.math.reduce_mean(loss).numpy()
        train_losses = np.hstack([train_losses, loss_value])
        
        iou_value = tf.math.reduce_mean(iou).numpy()
        train_ious = np.hstack([train_ious, iou_value])

        print('Training Loss : %f'%(step + 1, math.ceil(train_max / BATCH_SIZE),
         loss_value), end='')


    tr_lss, tr_iou = np.mean(train_losses), np.mean(train_ious)
    
    print('Train loss : %f  -- Train Average IOU : %f' % (epoch, EPOCHS, 
    tr_lss, tr_iou))    
    print()
    
    save_path = './models/checkpoint%d.h5' % (epoch)
    bbox_regressor.save(save_path)

预测

我们通过在图像中绘制边界框来可视化Bbox回归器预测的某些测试集中的图像的边界框。

for inputs, labels in test_dataset:
    bbox_preds = bbox_regressor(inputs, training=False).numpy() 
    bbox_preds = (bbox_preds * (dataset_generator.target_shape * 2)).astype(int)
    imgs = (127 * (inputs + 1)).numpy().astype(np.uint8)
    for idx, img in enumerate(imgs):
        x1, y1, x2, y2 = bbox_preds[idx]
        img = cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 0), 4)
        plt.imshow(img)
        plt.show()
    break

输出

结论

总之,基于CNN的本地化器在推动计算机视觉应用方面起着重要作用,特别是在对象定位任务中。本文强调了CNN在图像分析中的重要性,并解释了两步流程,包括用于特征提取的骨干CNN和用于预测边界框坐标的回归头。随着深度学习技术、更大的数据集和其他模态的整合的进步,对象本地化的未来具有巨大的潜力,承诺对行业产生重大影响,并改变视觉感知和理解。

重点

  • 基于CNN的本地化器对于推动计算机视觉应用至关重要,利用CNN学习图像的分层特征的能力。
  • 两步流程包括特征提取骨干CNN和回归头,常用于基于CNN的本地化器以实现准确的对象本地化。
  • 随着深度学习技术、更大的数据集和其他模态的整合的进步,对象本地化的未来具有巨大的潜力,承诺对自动驾驶、机器人、监视和医疗保健等行业产生重大影响。

本文中展示的媒体内容并非Analytics Vidhya所有,仅由作者自行决定使用。