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

0. 前言

R-CNN (Regions with CNN features),是 R-CNN 系列目标检测算法的初代模型,其将“深度学习”和传统的“计算机视觉”的相结合,基于候选区域 (Region proposal) 检测目标对象。
《目标检测基础》中,我们已经了解了候选区域的概念如何从图像中生成候选区域。在本节中,我们将利用候选区域来完成图像中目标对象的检测和定位。

1. R-CNN 目标检测模型

1.1 数据集分析

为了训练模型,我们下载并使用数据集 VOCtrainval_11-May-2012.tar,其中包含了图像中的对象以及对象相应的边界框,是目标检测中常用的数据集之一。
我们首先分析数据集的相关信息,以更好的构建神经网络模型,以如下图像以及图像中相应的边界框坐标和对象类别为例:

数据集示例

对象的类别标签和边界框坐标在 XML 文件中提供,可以从 XML 文件中提取,如下所示:

XML示例文件
如果 xml["annotation"]["object"] 是一个列表,则表明图像中存在多个目标对象。xml["annotation"]["object"]["bndbox"] 中包含图像中存在的目标对象的边界框,其中边界框坐标包括 ”xmin””ymin””xmax””ymax”,分别表示边界框的左上角坐标 (xmin, ymin) 和右下角坐标 (xmax, ymax)xml["annotation"]["object"]["name"] 可用于提取图像中存在的对象的类别标签。
为了简单起见,在本节中,我们所使用的数据集中每个图像仅包含一个待定位目标对象。

1.2 R-CNN 模型分析

了解了训练数据集后,我们接下来介绍 R-CNN 目标检测模型检测和定位流程:

  • 提取图像中的候选区域
  • 计算候选区域与实际目标对象区域的距离:
    • 本质上,计算候选区域与对象实际区域的交并比 (Intersection over Union, IoU) 作为距离度量标准
  • 如果交并比大于某个阈值,可以认为该候选区域包含待定位的目标,否则,认为它不包含待定位目标:
    • 这意味着我们为每个候选区域生成了标签(是否包含待定位目标),其中候选区域的图像是输入,利用 IoU 阈值获取输出标签
  • 调整图像大小并将每个候选区域图像输入到 VGG16 模型,以提取候选区域图像的特征
  • 通过比较候选区域位置和目标对象的实际位置来修正候选区域边界框,并将其作为新的候选区域用于后续训练
  • 建立分类模型,将候选区域的特征映射为区域是否包含目标对象
  • 利用候选区域中的图像,构建回归模型,该模型将候选对象的输入特征映射到提取对象的准确边界框所需的偏移量,用于矫正候选边界框
  • 在最终获取的边界框上使用非极大值抑制:
    • 使用非极大值抑制可确保将大量重叠的候选区域减少为 1,仅保留包含对象可能性最高的候选对象
  • 通过使用非极大值抑制,我们可以将上述策略进行扩展,为包含多个目标对象的图像构建目标检测模型

上述过程的示意图如下所示:

R-CNN

接下来,我们将实现上一节中所介绍的基于 R-CNN 的目标检测算法。

2. 从零开始实现 R-CNN 目标检测

2.1 查看训练数据集

导入相关的库,并在构建目标检测模型前,首先构建数据集目录,并查看数据集总数据量:

import json, scipy, os, time
import sys, cv2, xmltodict
import matplotlib.pyplot as plt
# import tensorflow as tf
import selectivesearch
import numpy as np
import pandas as pd
import gc, scipy, argparse
from copy import deepcopy

xmls_root ="VOCtrainval_11-May-2012/VOCdevkit/VOC2012/"  # 数据集父目录
annotations = xmls_root + "Annotations/"
jpegs = xmls_root + "JPEGImages/"
XMLs = os.listdir(annotations)
print(XMLs[:10])
print(len(XMLs))

接下来,为了对数据集有所了解,随机采样数据集中一个 XML 文件,使用 xmltodict 库解析后,查看输出信息:

ix = np.random.randint(len(XMLs))
sample_xml = XMLs[ix]
sample_xml = '{}/{}'.format(annotations, sample_xml)
with open(sample_xml, "rb") as f:    # notice the "rb" mode
    d = xmltodict.parse(f, xml_attribs=True)
print(d)

可以看到,输入相关信息如下所示:

OrderedDict([('annotation', OrderedDict([('filename', '2010_000495.jpg'), ('folder', 'VOC2012'), ('object', OrderedDict([('name', 'motorbike'), ('bndbox', OrderedDict([('xmax', '482'), ('xmin', '13'), ('ymax', '328'), ('ymin', '32')])), ('difficult', '0'), ('occluded', '0'), ('pose', 'Left'), ('truncated', '0')])), ('segmented', '0'), ('size', OrderedDict([('depth', '3'), ('height', '375'), ('width', '500')])), ('source', OrderedDict([('annotation', 'PASCAL VOC2010'), ('database', 'The VOC2010 Database'), ('image', 'flickr')]))]))])

2.2 从零实现目标检测模型 R-CNN

(1) 熟悉数据集后,就可以开始从零实现目标检测模型 R-CNN,首先定义计算 IoU 和获取候选区域的函数(具体计算方法,参考《目标检测基础》):

def calc_iou(candidate, current_y, img_shape):
    boxA = deepcopy(candidate)
    boxB = deepcopy(current_y)
    boxA[2] += boxA[0]
    boxA[3] += boxA[1]
    iou_img1 = np.zeros(img_shape)
    iou_img1[int(boxA[1]):int(boxA[3]), int(boxA[0]):int(boxA[2])] = 1
    iou_img2 = np.zeros(img_shape)
    iou_img2[int(boxB[1]):int(boxB[3]), int(boxB[0]):int(boxB[2])] = 1
    iou = np.sum(iou_img1*iou_img2) / (np.sum(iou_img1) + np.sum(iou_img2) - np.sum(iou_img1*iou_img2))
    return iou
    
def extract_candidates(img):
    """
    排除所有占图像面积不到 5% 的候选区域
    """
    img_lbl, regions = selectivesearch.selective_search(img, scale=100, min_size=100)
    img_area = np.prod(img.shape[:2])
    candidates = []
    for region in regions:
        if region['rect'] in candidates:
            continue
        if region['size'] < (0.05 * img_area):
            continue
        x, y, w, h = region['rect']
        candidates.append(list(region['rect']))
    return candidates

(2) 导入预训练的 VGG16 模型,用于提取图像特征:

from keras.applications import vgg16
from keras.utils.vis_utils import plot_model
from keras.applications.vgg16 import preprocess_input
vgg16_model = vgg16.VGG16(include_top=False, weights='imagenet')

(3) 接下来,创建目标检测模型所需的输入和相应输出列表,为了简单起见,我们仅考虑包含一个目标对象的图像。
首先初始化所需列表:

train_data_length = 1000
final_cls = []
final_delta = []
iou_list = []
imgs = []

遍历图像,并且仅考虑包含一个目标对象的图像:

for ix, xml in enumerate(XMLs[:train_data_length]):
    print('Extracted data from {} xmls...'.format(ix), end='\r')
    xml_file = annotations + xml
    fname = xml.split('.')[0]
    with open(xml_file, 'rb') as f:
        xml = xmltodict.parse(f, xml_attribs=True)
        l = []
        if isinstance(xml["annotation"]["object"], list):
            # 本任务仅考虑包含单目标对象的图像
            continue

在以上代码中,我们首先遍历获取图像对应 xml 文件,并检查图像是否包含多个对象(如果 xml["annotation"]["object"] 的输出是列表,则图像包含多个对象)。
归一化对象位置坐标,这样做是由于归一化的边界框相对稳定,即使图像形状发生更改其归一化的对象边界框仍然不会改变。例如,如果对象的 xmin 位于整个图像 x 轴的 20%y 轴的 50% 之处,即使对图像进行缩放,它也是一样的(但是,如果我们处理原始像素值位置,则 xmin 值在图像缩放是就会被更改):

        bndbox = xml["annotation"]["object"]["bndbox"]
        for key in bndbox:
            bndbox[key] = float(bndbox[key])
        x1, x2, y1, y2 = [bndbox[key] for key in ['xmin', 'xmax', 'ymin', 'ymax']]

        img_size = xml["annotation"]["size"]
        for key in img_size:
            img_size[key] = float(img_size[key])
        w, h = img_size['width'], img_size['height']

        # 根据图片尺寸归一化边界框坐标
        x1 /= w
        x2 /= w
        y1 /= h
        y2 /= h
        label = xml["annotation"]["object"]["name"]
        y = [x1, y1, x2-x1, y2-y1, label] # 图像左上角x和y坐标,图像宽度和高度,目标对象标签

使用 extract_candidates() 函数,提取图像中的候选区域:

        # 图片路径
        file_name = jpegs + fname + '.jpg'
        # 读取并预处理图片
        img = cv2.resize(cv2.imread(file_name), (224, 224))
        candidates = extract_candidates(img)

在以上代码中,我们使用 extract_candidates 函数提取调整大小后的图像的区域提议。
遍历候选区域以计算每个候选区域与图像中对象的实际区域边界框的交并比,以及实际边界框和候选对象之间的对应偏移量以用于后续训练回归模型修正候选区域边界框:

        for jx, candidate in enumerate(candidates):
            current_y2 = [int(i*224) for i in [x1, y1, x2, y2]]
            iou = calc_iou(candidate, current_y2, (224, 224))
            candidate_region_coordinates = c_x1, c_y1, c_w, c_h = np.array(candidate)/224

            dx = c_x1 - x1
            dy = c_y1 - y1
            dw = c_w - (x2 - x1)
            dh = c_h - (y2 - y1)

            final_delta.append([dx, dy, dw, dh])

计算使用预训练的 VGG16 提取每个候选区域的特征,并根据与实际边界框间的 IoU 分配类别标签:

            if iou > 0.3:
                final_cls.append(label)
            else:
                final_cls.append('background')
            
            # 使用VGG16作为特征提取网络
            l = int(c_x1 * 224)
            r = int((c_x1 + c_w) * 224)
            t = int(c_y1 * 224)
            b = int((c_y1 + c_h) * 224)

            img2 = img[t:b,l:r,:3]
            img3 = cv2.resize(img2,(224,224)) # /255
            img3 = preprocess_input(img3.reshape(1,224,224,3))
            img4 = vgg16_model.predict(img3)
            imgs.append(img4)
            iou_list.append(iou)

创建输入和输出数组:

targets = pd.DataFrame(final_cls, columns=['label'])
# 使用get_dummies方法获取标签,因为这些类别值是分类文本值
labels = pd.get_dummies(targets['label']).columns
y_train = pd.get_dummies(targets['label']).values.astype(float)
x_train = np.array(imgs)
x_train = x_train.reshape(x_train.shape[0], x_train.shape[2], x_train.shape[3], x_train.shape[4])

(4) 构建、编译模型,并拟合模型:

from keras.models import Sequential
from keras.layers import Flatten, Dropout, Dense, Conv2D
model = Sequential()
model.add(Flatten(input_shape=((7, 7, 512))))
model.add(Dense(1024, activation='relu'))
model.add(Dense(y_train.shape[1], activation='softmax'))

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])

history = model.fit(x_train/x_train.max(), y_train,
                validation_split=0.1,
                epochs=10,
                batch_size=32,
                verbose=1)

在测试数据集上的分类准确率可以达到 80%。我们将输入数组除以 x_train.max(),用于进行归一化,对输入数据进行归一化可以更快速的训练模型。
(5) 从数据集中选择图片用于验证图像分类模型的结果(确保不使用训练数据集中的图像)。
选择用于测试的图像:

import matplotlib.patches as mpatches

ix = np.random.randint(train_data_length, len(XMLs))
filename = jpegs + XMLs[ix].replace('xml', 'jpg')

编写测试函数 test_predictions,对测试图像执行图像预处理以提取候选对象,对尺寸调整后的候选对象执行模型预测,过滤掉预测的背景类区域,最后绘制除背景类以外,概率最高的对象类的区域边界作为最终的候选区域:

def test_predictions(file_name):
    image = cv2.imread(file_name)
    img = cv2.resize(image, (224, 224))
    candidates = extract_candidates(img)

在以上代码中,我们调整输入图像的大小并从中提取候选区域。接下来,绘制所读取的图像,并初始化用于存储预测类别及其相应概率的列表:

    _, ax = plt.subplots(1, 2)
    ax[0].imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
    ax[0].set_title(file_name.split('/')[-1])

    pred_class = []
    pred = []

接下来,我们循环遍历这些候选区域,调整其大小,并将其传递给与训练后的 VGG16 模型。此外,我们将 VGG16 的输出特征送入分类模型中,得到候选区域图像属于不同类别的概率:

    for ix, candidate in enumerate(candidates):
        l, t, w, h = np.array(candidate).astype(int)
        img2 = img[t:t+h, l:l+w, :3]
        img3 = cv2.resize(img2, (224, 224)) #/255
        img3 = preprocess_input(img3.reshape(1, 224, 224, 3))
        img4 = vgg16_model.predict(img3)
        final_pred = model.predict(img4/x_train.max())
        pred.append(np.max(final_pred))
        pred_class.append(np.argmax(final_pred))

然后,将提取的候选区域对象中包含非背景对象的可能性最高的区域作为最终的候选区域,其中预测标签为 1 的区域对应于背景图像,同时我们将计算此区域在原始图像中对应的坐标:

    pred = np.array(pred)
    pred_class = np.array(pred_class)
    pred2 = pred[pred_class!=1]
    pred_class2 = pred_class[pred_class!=1]
    candidates2 = np.array(candidates)[pred_class!=1]
    x, y, w, h = candidates2[np.argmax(pred2)]
    new_x = int(x * image.shape[1] / 224)
    new_y = int(y * image.shape[0] / 224)
    new_w = int(w * image.shape[1] / 224)
    new_h = int(h * image.shape[0] / 224)

最后,我们绘制图像以及最终候选区域的矩形边界框。

    ax[1].set_title(labels[pred_class2[np.argmax(pred2)]])
    ax[1].imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
    rect = mpatches.Rectangle((new_x, new_y), new_w, new_h, fill=False, edgecolor='red', linewidth=1)
    ax[1].add_patch(rect)

(6) 使用新图像作为参数值,调用 test_predictions 函数:

file_name = '10.png'
test_predictions(file_name)
plt.show()

检测结果

可以看到,该模型可以准确地计算出图像中对象的类别,但包含目标对象的概率最高的边界框(最终候选区域)仍需要需要进行校正。这正是我们在下一步中需要做的——调整目标对象边界框。

2.3 调整目标对象边界框

(1) 构建并编译一个模型,该模型将预训练的 VGG16 提取的图像特征作为输入并预测边界框偏移量:

model2 = Sequential()
model2.add(Flatten(input_shape=((7, 7, 512))))
model2.add(Dense(1024, activation='relu'))
model2.add(Dense(4, activation='linear'))

model2.compile(loss='mean_absolute_error', optimizer='adam')
# 预测候选区域图像类别
pred = model.predict(x_train/x_train.max())
pred_class = np.argmax(pred, axis=1)

(2) 构建模型以预测边界框偏移量时,我们仅需要确保对那些可能包含目标对象的图像区域预测边界框偏移量:

for i in range(1000):
    samp=random.sample(range(len(x_train)),500)
    x_train2 = [x_train[i] for i in samp if pred_class[i]!=1]
    x_train2 = np.array(x_train2)
    final_delta2 = [final_delta[i] for i in samp if pred_class[i]!=1]
    model2.fit(x_train2/x_train.max(), np.array(final_delta2),
                validation_split = 0.1,
                epochs=1,
                batch_size=32,
                verbose=1)

在以上代码中,我们遍历输入数组数据集并创建一个新的数据集,该数据集中仅包括那些可能为非背景类的区域。此外,我们将之前的训练步骤重复 1000 次以微调模型。
(3) 构建使用图像路径作为参数,预测图像类别的函数,并修正边界框:

def test_predictions2(file_name):
    image = cv2.imread(file_name)
    img = cv2.resize(image, (224, 224))
    candidates = extract_candidates(img)
    _, ax = plt.subplots(1, 2)
    ax[0].imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
    ax[0].set_title(file_name.split('/')[-1])
    pred = []
    pred_class = []
    del_new = []

    for ix, candidate in enumerate(candidates):
        l, t, w, h = np.array(candidate).astype(int)
        img2 = img[t:t+h, l:l+w, :3]
        img3 = cv2.resize(img2, (224, 224)) # /255
        img3 = preprocess_input(img3.reshape(1, 224, 224, 3))
        img4 = vgg16_model.predict(img3)
        final_pred = model.predict(img4/x_train.max())
        delta_new = model2.predict(img4/x_train.max())[0]
        pred.append(np.max(final_pred))
        pred_class.append(np.argmax(final_pred))
        del_new.append(delta_new)
    
    pred = np.array(pred)
    pred_class = np.array(pred_class)
    non_bgs = (pred_class!=1)
    pred = pred[non_bgs]
    pred_class = pred_class[non_bgs]
    del_new = np.array(del_new)
    del_new = del_new[non_bgs]
    del_pred = del_new * 224
    candidates = C = np.array(candidates)[non_bgs]
    C = np.clip(C, 0, 224)
    C[:, 2] += C[:, 0]
    C[:, 3] += C[:, 1]

    bbs_pred = candidates - del_pred
    bbs_pred = np.clip(bbs_pred, 0, 224)
    bbs_pred[:, 2] -= bbs_pred[:, 0]
    bbs_pred[:, 3] -= bbs_pred[:, 1]
    final_bbs_pred = bbs_pred[np.argmax(pred)]

    x, y, w, h = final_bbs_pred
    new_x = int(x * image.shape[1] / 224)
    new_y = int(y * image.shape[0] / 224)
    new_w = int(w * image.shape[1] / 224)
    new_h = int(h * image.shape[0] / 224)

    ax[1].imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
    rect = mpatches.Rectangle((new_x,new_y), new_w, new_h, fill=False, edgecolor='red', linewidth=1)
    ax[1].add_patch(rect)
    ax[1].set_title(labels[pred_class[np.argmax(pred)]])

(4) 测试模型,同样提取仅包含一个对象的测试图像:

single_object_images = []
for ix, xml in enumerate(XMLs[N:]):
    xml_file = annotations + xml
    fname = xml.split('.')[0]
    with open(xml_file, 'rb') as f:
        xml = xmltodict.parse(f, xml_attribs=True)
        l = []
        if isinstance(xml["annotation"]["object"], list):
            continue
        single_object_images.append(xml["annotation"]["filename"])
        if ix > 100:
            break

我们遍历图像并识别和定位包含单个目标对象的图像。
(5) 最后,预测我们的测试图像:

# 使用测试集图像
# ix = 2
# filename = jpegs + single_object_images[ix]
# test_predictions2(filename)
# plt.show()
# 使用其他图像
file_name = '10.png'
test_predictions2(file_name)
plt.show()

校准边界框

如上所示,虽然使用边界框调整模型能够校正边界框。但是,仍然需要对边界框进行一些进一步的校正,我们可以当在更多数据上进行训练,以达到实现这一目标。

3. 非极大值抑制 (non-maximum suppression, NMS)

在本节中实现的 R-CNN 目标检测模型中,我们仅考虑了非背景的候选区域,并且仅使用具有最高对象概率的候选区域进行目标检测任务。但是,在图像中存在多个对象的情况下,使用上述策略并不能成功定位多个对象。
为了能够在图像中提取尽可能多的对象,我们可以使用非极大值抑制从候选区域中筛选候选对象,以便我们能够进行多目标检测。

3.1 非最大值抑制

在目标检测模型中,通常会得到多个候选区域,并为这些候选区域预测类别和偏移量,最后利用候选区域和偏移量得到预测边界框。但是,当候选区域数量较多时,同一个目标上可能会得到较多相似的预测边界框。为了保持结的简洁,我们通常需要移除相似的预测边界框,常用的方法称为非极大值抑制 (non-maximum suppression, NMS)。

3.2 非最大值抑制算法流程

有多种用于实现非极大值抑制的算法,我们将要实现的非最大值抑制 (non-maximum suppression, NMS) 算法如下:

  • (1) 从图像中提取候选区域
  • (2) 整形候选区域并预测图像中包含的对象类别
  • (3) 如果候选对象并非背景类对象,我们将保留候选对象
  • (4) 对于所有非背景类候选对象,我们根据它们包含对象的概率对其进行排序
  • (5) 根据 IoU,将第一个候选对象(通过类别概率降序排序后的序列)与所有其他候选者进行比较
  • (6) 如果任何其他候选区域与第一个候选区域之间的重叠面积大于一定阈值,则将其丢弃
  • (7) 在其余的候选对象中,再次考虑包含对象的可能性最高的候选区域对象
  • (8) 在步骤 (7) 中已过滤后的候选区域列表中,继续比较第一个候选对象与其余候选区域对象
  • (9) 重复进行步骤 (5) ~ (8),直到没有候选对象可以比较为止
  • (10) 在执行完前面的步骤之后,绘制最终保留的候选对象作为边界框

在下一小节中,我们将使用 Python 实现上述非极大值抑制算法。

3.3 实现非极大值抑制

(1) 从图像中提取所有包含非背景类对象的置信度较高的区域:

file_name = '8.png'
image = cv2.imread(file_name)
img = cv2.resize(image, (224, 224))
img_area = img.shape[0] * img.shape[1]
candidates = extract_candidates(img)
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
plt.show()

加载图像

(2) 对候选区域进行预处理——将它们传递给 VGG16 模型,然后预测每个候选区域的类别以及区域的边界框:

pred = []
pred_class = []
del_new = []

for ix, candidate in enumerate(candidates):
    l, t, w, h = np.array(candidate).astype(int)
    img2 = img[t:t+h, l:l+w, :3]
    img3 = cv2.resize(img2, (224, 224)) # /255
    img3 = preprocess_input(img3.reshape(1, 224, 224, 3))
    img4 = vgg16_model.predict(img3)
    final_pred = model.predict(img4/x_train.max())
    delta_new = model2.predict(img4/x_train.max())[0]
    pred.append(np.max(final_pred))
    pred_class.append(np.argmax(final_pred))
    del_new.append(delta_new)

pred = np.array(pred)
pred_class = np.array(pred_class)

(3) 提取非背景类候选区域及其对应的边界框偏移,过滤获取所有非背景类的候选区域的类别概率、类别和边界框偏移(预测类别为 1 表示背景类):

non_bgs = ((pred_class!=1))
pred = pred[non_bgs]
pred_class = pred_class[non_bgs]

del_new = np.array(del_new)
del_new = del_new[non_bgs]
del_pred = del_new * 224

(4) 使用边界框偏移值校正候选区域,同时确保 xmaxymax 坐标不能大于 224,此外,我们需要确保边界框的宽度和高度不能为负:

candidates = C = np.array(candidates)[non_bgs]
C = np.clip(C, 0, 224)
C[:, 2] += C[:, 0]
C[:, 3] += C[:, 1]

bbs_pred = candidates - del_pred
bbs_pred = np.clip(bbs_pred, 0, 224)

bbs_pred[:, 2] -= bbs_pred[:, 0]
bbs_pred[:, 3] -= bbs_pred[:, 1]
bbs_pred = np.clip(bbs_pred, 0, 224)

bbs_pred2 = bbs_pred[(bbs_pred[:, 2] > 0) & (bbs_pred[:, 3] > 0)]
pred = pred[(bbs_pred[:, 2] > 0) & (bbs_pred[:, 3] >0)]
pred_class = pred_class[(bbs_pred[:, 2] > 0) & (bbs_pred[:, 3] > 0)]

(5) 最后,绘制图像以及目标对象边界框:

fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
for ix, (x, y, w, h) in enumerate(bbs_pred2):
    new_x = int(x * image.shape[1] / 224)
    new_y = int(y * image.shape[0] / 224)
    new_w = int(w * image.shape[1] / 224)
    new_h = int(h * image.shape[0] / 224)
    rect = mpatches.Rectangle((new_x,new_y), new_w, new_h, fill=False, edgecolor='red', linewidth=1)

    ax.add_patch(rect)
plt.show()

候选区域

(6) 为了在边界框上执行非极大值抑制,定义函数 nms_boxes,该函数根据阈值(两个边界框的最小交集)、边界框坐标以及与每个边界框关联的概率值来执行 NMS
计算每个边界框的 xywh 值、它们的相应面积,并根据概率进行排序:

def nms_boxes(threshold, boxes, scores):
    x = boxes[:, 0]
    y = boxes[:, 1]
    w = boxes[:, 2]
    h = boxes[:, 3]
    areas = w * h
    order = scores.argsort()[::-1]

计算概率最高的候选区域与其余候选区域的交并比:

    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)
        xx1 = np.maximum(x[i], x[order[1:]])
        yy1 = np.maximum(y[i], y[order[1:]])
        xx2 = np.maximum(x[i]+w[i], x[order[1:]]+w[order[1:]])
        yy2 = np.maximum(y[i]+h[i], y[order[1:]]+h[order[1:]])
        w1 = np.maximum(0.0, xx2-xx1+1)
        h1 = np.maximum(0.0, yy2-yy1+1)
        inter = w1 * h1
        iou = inter / (areas[i] + areas[order[1:]] - inter)

选择 IoU 小于给定阈值的候选区域,根据上一小节中介绍的 NMS 算法流程,循环过滤候选区域列表,根据列表中类别概率最高的候选区域确定下一组候选区域列表:

        inds = np.where(iou <= threshold)[0]
        order = order[inds+1]

返回需要保留的候选索引:

    keep = np.array(keep)
    return keep

执行计算 NMS 的函数 nms_boxes

keep_box_ixs = nms_boxes(0.3, bbs_pred2, pred)

print(pred, keep_box_ixs)

绘制执行 NMS 后得到的边界框,可以看到我们删除了生成的所有其他相似的候选区域边界框:

fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))

for ix, (x, y, w, h) in enumerate(bbs_pred2):
    if ix not in keep_box_ixs:
        continue
    new_x = int(x * image.shape[1] / 224)
    new_y = int(y * image.shape[0] / 224)
    new_w = int(w * image.shape[1] / 224)
    new_h = int(h * image.shape[0] / 224)
    rect = mpatches.Rectangle(
        (new_x, new_y), new_w, new_h, fill=False, edgecolor='red', linewidth=1,)

    ax.add_patch(rect)
    centerx = new_x# + new_w/2
    centery = new_y + 20# + new_h - 10
    plt.text(centerx, centery,labels[pred_class[ix]]+" "+str(round(pred[ix],2)),fontsize = 20,color='red')

plt.show()

执行NMS后结果图像

小结

R-CNN 是基于候选区域的经典目标检测算法,其将卷积神经网络引入目标检测领域。本文首先介绍了 R-CNN 模型的核心思想与目标检测流程,然后使用 Keras 从零开始实现了一个基于 R-CNN 的目标检测模型,最后介绍了非极大值抑制用于移除相似的预测边界框。

系列链接

Keras深度学习实战(1)——神经网络基础与模型训练过程详解
Keras深度学习实战(2)——使用Keras构建神经网络
Keras深度学习实战(3)——神经网络性能优化技术
Keras深度学习实战(4)——深度学习中常用激活函数和损失函数详解
Keras深度学习实战(5)——批归一化详解
Keras深度学习实战(6)——深度学习过拟合问题及解决方法
Keras深度学习实战(7)——卷积神经网络详解与实现
Keras深度学习实战(8)——使用数据增强提高神经网络性能
Keras深度学习实战(9)——卷积神经网络的局限性
Keras深度学习实战(10)——迁移学习
Keras深度学习实战(11)——可视化神经网络中间层输出
Keras深度学习实战(12)——面部特征点检测
Keras深度学习实战(13)——目标检测基础详解

猜你喜欢

转载自blog.csdn.net/LOVEmy134611/article/details/125566214