基于传统目标检测算法识别目标并定位其位置

基于传统目标检测算法识别目标并定位其位置

  1. 实验目的
    理解模型的保存与加载;
    掌握传统目标检测算法流程;
    掌握滑动窗口、图像金字塔、非极大值抑制等概念;
    掌握 resnet50 分类模型的使用;
  2. 实验内容
    实验背景

目标检测是人工智能在工业领域应用较广泛的领域之一。掌握传统目标检测算法,对于理解成熟算法的原理(如YOLO、SSD、Faster RCNN)具有重要作用。

实验内容

本实验使用传统目标检测算法识别目标。本实验将首先使用图像金字塔不断缩小图像规模得到多张图像,然后使用滑动窗口处理每张图像得到多个ROI,并将得到的一批ROI传入ResNet50分类模型做分类,最后使用非极大值抑制算法确定最终边界框,以得到最终目标检测结果。
3. 实验数据
数据集位置

数据集图像存放在/home/dataset目录下,图像名字是 cicinnus_dog.jpg
数据集格式

本案例用到的数据是一张小狗图像。
dog_1.jpg

  1. 实验知识点
    Python语言编程;
    NumPy和OpenCV库的使用;
    滑动窗口、图像金字塔、非极大值抑制的原理;
    ResNet50分类模型的使用
  2. 实验时长
    共4学时,具体安排如下:
    定义目标检测算法(2学时)
    预测检测结果(2学时)
  3. 实验环境
    Linux Ubuntu 操作系统
    Jupyter 代码编辑器
    Python 3.7.4
    Tensorflow 2.3.0
    Keras 2.4.3
  4. 实验分析
    定义目标检测算法。传统目标检测算法包含4个部分:首先使用图像金字塔按比例不断缩小图像尺寸得到多张图像,然后对每张图像使用滑动窗口选取ROI区域,得到一批ROI后,将ROI传入分类模型做分类,最终使用非极大值抑制方法处理边界框。
    预测检测结果。调用目标检测算法中定义的4个方法,并使用ResNet50模型进行分类识别。
  5. 实验过程
    8.1 目标检测算法
    本模块对目标检测算法的每一步进行了定义。传统目标检测算法分为四个步骤:

首先通过图像金字塔不断缩小图像规模得到多张图像;
然后使用滑动窗口处理每张图像得到多个ROI;
将得到的一批ROI传入分类模型做分类;
使用非极大值抑制算法确定最终边界框。
因此,在本模块中定义了4个函数sliding_window()、image_pyramid()、classify_batch()、non_max_suppression()分别对应目标检测的4步。

把所有用到的库都导入。

导入库

from keras.applications import imagenet_utils
import numpy as np
import imutils
8.1.1 滑动窗口
滑动窗口(Sliding Window)是宽度和高度固定的矩形区域 (ROI),可在图像上“滑动”。

本部分定义滑动窗口方法,通过遍历图像选取固定大小的矩形区域,并返回 x 坐标、y 坐标以及当前窗口。

滑动窗口

@image: 输入图像

@step: 值一般为 4像素 或 8像素 或 16像素

@ws: 窗口的高度和宽度

def sliding_window(image, step, ws):
# 在图像上进行窗口滑动
for y in range(0, image.shape[0] - ws[0], step): # 遍历高度像素
for x in range(0, image.shape[1] - ws[1], step): # 遍历宽度像素
# 返回 x 坐标、y 坐标、当前窗口
yield (x, y, image[ y: y + ws[0], x: x + ws[1]])
8.1.2 图像金字塔
图像金字塔(Image Pyramid)是对图像进行一定比例的缩放。图像金字塔的作用在于解决目标检测中的尺度问题。在比较早的时候,是通过改变滑动窗口的形式来检测图像中大小不一的物体,而目前阶段,更多的是采用滑动窗口规格不变,改变图片大小来检测图像中尺度不一致的物体。金字塔的层级越多,计算量更大,花费的时间会更多,但是,在某种程度上能获得更准确的结果。

图像金字塔的具体实现如下:输入图像后,每次变化 scale 倍,当小于可接受物体的最小尺寸 minSize 结束。

图像金字塔

@image: 输入图像

@scale: 图像金字塔每次变化的倍数

@minSize: 可接受的物体的最小尺寸

def image_pyramid(image, scale=1.5, minSize=(224, 224)):
# yield 原始图像
yield image
# 不断遍历,得到金字塔的所有图片
while True:
# 计算下一个金字塔的图片
w = int(image.shape[1] / scale)
image = imutils.resize(image, width = w)
# 如果不符合最小尺寸了,就终止循环
if image.shape[0] < minSize[1] or image.shape[1] < minSize[0]:
break
# yield 下一个金字塔的图片
yield image
8.1.3 图像分类识别
为了提高目标检测速度,我们传入分类模型的数据不是单个ROI,而是一批ROI,并且过滤掉识别概率较低的ROI。具体实现过程如下:

图像分类识别

@model:图像分类模型

@batchROIs:批量roi

@ batchLocs:批量x、y坐标

@labels:key: 预测的物体;value: 坐标区域和预测的概率

@minProb:最低识别概率

@top:得到预测的前top个结果

@dims:图像分类模型需要的图片尺寸

def classify_batch(model, batchROIs, batchLocs, labels, minProb=0.5, top=10, dims=(224, 224)):
# 预测
preds = model.predict(batchROIs)
P = imagenet_utils.decode_predictions(preds, top=top) # "vgg16", "vgg19", "resnet"都需要这行代码

# 循环预测的值
for i in range(0, len(P)):
    for (_, label, prob) in P[i]:
        # 过滤掉识别概率较低的记录
        if prob > minProb:
            # 得到检测区域
            (pX, pY) = batchLocs[i]
            box = (pX, pY, pX + dims[0], pY + dims[1])

            # 更新 label的值
            L = labels.get(label, [])
            L.append((box, prob))
            labels[label] = L

# 返回label的值
return labels        

8.1.4 非极大值抑制
非极大值抑制(Non-maxima Suppression, NMS)抑制不是极大值的元素,可以理解为局部最大搜索,也可以理解为只取置信度最高的一个识别结果。 当滑动窗口ROI接近物体时,会识别出物体。ROI越接近物体,置信度越高。

过程:对于Bounding Box的列表B及其对应的置信度S,采用下面的计算方式:选择具有最大score的检测框M,将其从B集合中移除并加入到最终的检测结果D中。通常将B中剩余检测框中与M的IoU大于阈值Nt的框从B中移除。重复这个过程,直到B为空。

结论:从上面的列表中挑选一个置信度最高的为最终的结果。

非极大值抑制

@boxes:检测到的区域

@probs:识别的物体的概率

def non_max_suppression(boxes, probs=None, overlapThresh=0.3):
# 如果boxes没有值,返回空List
if len(boxes) == 0:
return []
# 如果像素数据是int,转换为float类型
if boxes.dtype.kind == "i":
boxes = boxes.astype("float")

# 初始化选定区域的列表
pick = []
# 得到boxes的坐标
x1 = boxes[:, 0]
y1 = boxes[:, 1]
x2 = boxes[:, 2]
y2 = boxes[:, 3]

# 计算检测区域的面积,然后按识别概率由低到高进行排序
area = (x2 - x1 + 1) * (y2 - y1 + 1)
idxs = y2
if probs is not None:
    idxs = probs
idxs = np.argsort(idxs)

# 循环遍历idxs,直至idxs没有元素
while len(idxs) > 0:
    # 得到idxs是最后一个元素
    last = len(idxs) - 1
    i = idxs[last]
    pick.append(i)
    # 找到最大的bounding的起始坐标(x1,y1),找到最小的bounding的终止坐标(x2,y2)
    xx1 = np.maximum(x1[i], x1[idxs[:last]])
    yy1 = np.maximum(y1[i], y1[idxs[:last]])
    xx2 = np.minimum(x2[i], x2[idxs[:last]])
    yy2 = np.minimum(y2[i], y2[idxs[:last]])
    # 计算 bounding 的宽度和高度
    w = np.maximum(0, xx2 - xx1 + 1)
    h = np.maximum(0, yy2 - yy1 + 1)
    
    # 计算重叠的比率
    overlap = (w * h) / area[idxs[:last]]
    # 只要重叠率大于重叠阈值的,从idxs中删掉这个元素
    idxs = np.delete(idxs, np.concatenate(([last], np.where(overlap > overlapThresh)[0])))

# 只返回我们选中的box
return boxes[pick].astype("int")

8.2 预测检测结果
本模块的作用是调用8.1中定义的目标检测算法,以及使用ResNet50分类模型,检测单张RGB图像,并输出目标检测结果。

8.2.1 导入库

导入包

from keras.preprocessing.image import img_to_array
from keras.applications import imagenet_utils
import numpy as np
import time
import cv2
import matplotlib.pyplot as plt
8.2.2 配置GPU资源
TensorFlow 2.x 会自动检测环境中是否有GPU。如果有GPU,则会使用GPU。否则会使用CPU。

如果没有GPU,执行本部分也不会报错。

由于TensorFlow默认会申请全部GPU资源。为了达到GPU资源共享的目的,设置GPU资源按需申请。

让GPU资源按需申请

import tensorflow as tf
config = tf.compat.v1.ConfigProto()
config.gpu_options.allow_growth = True
session = tf.compat.v1.Session(config=config)
8.2.3 设置随机种子
为了保证每次初始化相同权重,保证对比实验的正确性和可复现,设置随机数种子。

import random
seed = 2021
random.seed(seed)
np.random.seed(seed)
tf.random.set_seed(seed)
8.2.4 设置全局变量
这些常量都是目标检测用到的一些指定参数。如滑动窗口移动多少像素、图像金字塔每次变化的倍数、ROI的大小、批的大小等。

全局常量

CONFIDENCE = 0.5 # 图像分类的最低准确率
INPUT_SIZE = (350, 350) # 把输入图片统一处理成这个尺寸,如果图片过大提高处理速度
WIN_STEP = 16 # 滑动窗口每次移动几个像素
PYR_SCALE = 1.5 # 图像金字塔每次变化的倍数
ROI_SIZE = (224, 224) # 感兴趣区域的尺寸,因为ResNet的输入图像的尺寸是 (224, 224)
BATCH_SIZE = 64 # 每满 BATCH_SIZE 条记录,便检测一次
8.2.5 加载ResNet50模型
传统目标检测算法的第三步是对ROI进行分类识别,此处我们只需要调用预训练好的CNN分类模型即可,以ResNet50为例。

实例化ResNet50()类,ResNet50预训练模型会自动从因特网下载。

加载CNN模型-ResNet50

from keras.applications import ResNet50
print("[INFO] 加载CNN模型...")
model = ResNet50(weights="imagenet", include_top=True)
[INFO] 加载CNN模型...
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/resnet/resnet50_weights_tf_dim_ordering_tf_kernels.h5
102973440/102967424 [==============================] - 11s 0us/step
8.2.6 改变图片尺寸
由于传统目标检测算法的执行速度较慢,尤其对于较大图像,需要花费较长的时间,因此,通过resize缩小图像尺寸,提高算法的执行速度。

读取图片,并改变图片尺寸

image_file = '/home/dataset/cicinnus_dog.jpg'
orig = cv2.imread(image_file) # 读取图片
resized = cv2.resize(orig, INPUT_SIZE, interpolation=cv2.INTER_CUBIC) # 改变图片尺寸
8.2.7 定义图像变量和结果变量
初始化保存处理后的图像变量,便于后续使用。
定义用于存储识别结果的变量。变量是字典类型,key是预测的物体,value是坐标区域和预测的概率。

初始化保存处理后的图像的变量

batchROIs = None # 批量感兴趣区域(ROI)
batchLocs = [] # 批量(x, y)坐标

存储预测结果

key: 预测的物体

value: 坐标区域和预测的概率

labels = {}
8.2.8 目标检测
该部分正式做目标检测。首先循环遍历图像金字塔,然后对金字塔的每一张图像循环遍历滑动窗口,再把遍历后的图像区域填充到batch中,当batch满了之后,就把batch中的数据做图像分类。

print("[INFO] 开始检测图片中的物体...")
start = time.time() # 记录检测物体时的起始时间

循环遍历图像金字塔

batch_rois = None # 此行代码纯属为了理解 batchROIs 的值
for image in image_pyramid(resized, scale=PYR_SCALE, minSize=ROI_SIZE):
# 循环遍历滑动窗口
for (x, y, roi) in sliding_window(resized, WIN_STEP, ROI_SIZE):
roi = img_to_array(roi) # 图片像素类型由uint8转换为float32
roi = np.expand_dims(roi, axis=0) # 扩展成4维,第1维是样本的条数,即1
roi = imagenet_utils.preprocess_input(roi) # "vgg16", "vgg19", "resnet"都需要这行代码

    # 如果batchROIs的值是空,那么用roi赋值。否则,将roi的值添加在batchROIs下面
    # 第1次循环,batchROIs的尺寸是(1, 224, 224, 3),第2次循环,尺寸是(2, 224, 224, 3), 第3次循环,尺寸是(3, 224, 224, 3)
    if batchROIs is None:
        batchROIs = roi
    else:
        batchROIs = np.vstack([batchROIs, roi])
    # 更新 batchLocs 的值
    batchLocs.append((x, y))

    # 检测 batch 是否满了,如果满了就识别
    if len(batchROIs) == BATCH_SIZE:
        # 对 batch 里的样本做图像分类识别
        labels = classify_batch(model, batchROIs, batchLocs, labels, minProb=CONFIDENCE)    
        batch_rois = batchROIs # 此行代码纯属为了理解 batchROIs 的值
        
        # 清空 batchROIs 和 batchLocs 的值,准备下一个batch
        batchROIs = None 
        batchLocs = []

[INFO] 开始检测图片中的物体...
2023-04-27 01:07:04.286522: I tensorflow/compiler/mlir/mlir_graph_optimization_pass.cc:185] None of the MLIR Optimization Passes are enabled (registered 2)
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/imagenet_class_index.json
40960/35363 [==================================] - 0s 7us/step
8.2.9 处理遗留的ROI
由于定义全局常量中已定义batch_size的大小为64,即每满足64个ROI就会检测,若仅有65个ROI,就会有一个ROI没有被检测,因此,需判断是否有ROI没有被检测,若有遗留,则送入分类模型识别。

判断是否有遗留的 ROIs 没有被图像分类模型识别。如果有,就识别

if batchROIs is not None:
labels = classify_batch(model, batchROIs, batchLocs, labels, minProb=CONFIDENCE)

目标检测程序结束,输出时间

end = time.time()
print("[INFO] 检测用了 {:.4f} 秒".format(end - start))
[INFO] 检测用了 119.1461 秒
8.2.8中已经记录了检测的开始时间,此部分记录检测结束时间,并用结束时间减开始时间,以得到检测用时,并输出。

8.2.10 处理不存在检测目标的情况
若图像中不存在标签中所包含的类别,或者没有正确检测出图像中包含的类别,则输出“没有检测到物体”。

如果没有检测到物体

if not labels:
print('没有检测到物体')
8.2.11 非极大值抑制处理
该部分主要应用非极大值抑制算法对边界框进行处理:首先循环遍历所有的类别,对于每一个类别都调用non_max_suppression函数进行非极大值抑制处理,并且根据处理后区域的坐标,画矩形框,并将预测的物体类别和概率显示出来。最终,将图像检测结果显示出来。

循环遍历 labels,处理每一个类别

for k in labels.keys():
clone = resized.copy() # 用于显示

# 使用非极大值抑制算法抑制弱的、重叠的检测区域
boxes = np.array([p[0] for p in labels[k]])
proba = np.array([p[1] for p in labels[k]])
boxes = non_max_suppression(boxes, proba)

# 在每一个识别区域上画上边框,画出坐标区域,并把预测到的物体写在上面
for (xA, yA, xB, yB) in boxes:
    cv2.rectangle(clone, (xA, yA), (xB, yB), (0, 0, 255), 2)
    display_label = '%s: %.3f' %(k, proba[np.argmax(proba)])
    print(display_label)
    cv2.putText(clone, display_label, (10,25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,0,255), 2) # 写上物体及概率
    
# 显示图像
plt.figure()
plt.imshow(cv2.cvtColor(clone, cv2.COLOR_BGR2RGB))

Old_English_sheepdog: 0.546
Bouvier_des_Flandres: 0.549
soft-coated_wheaten_terrier: 0.931

共有检测3种结果。

检测结果1 是Old_English_sheepdog(古英国牧羊犬),检测概率为0.546。

下图是古英国牧羊犬。可以发现,2者确实有一定的相似度。

image.png image.png

检测结果2 是Bouvier_des_Flandres(法兰德斯牧牛犬),检测概率为0.543。

下图是法兰德斯牧牛犬。可以发现,2者确实有一定的相似度。

image.png image.png

检测结果3是 soft-coated_wheaten_terrier(爱尔兰软毛麦色梗),检测概率为0.929,在3种检测结果中概率最高。

下图是爱尔兰软毛麦色梗。可以发现,2者相似度非常高。

image.png image.png

  1. 实验结果(结论)
    从结果中可以看到,该目标检测算法检测出了三种结果,其中soft-coated_wheaten_terrier类别的概率为0.929,是检测出的三类中最高的。出现三种检测结果的原因是,滑动窗口处理一张图片时会得到不同的ROI,每一个ROI都需要输入ResNet50中进行分类,由于实际三种犬比较相似,因此出现了多种结果。这也就说明了传统目标检测算法对于相似度比较高的目标,检测的准确性不够高。此外,检测用时为4.1148秒,速度较慢。

本实验是为了让同学们了解传统目标检测算法流程,掌握滑动窗口、图像金字塔、非极大值抑制等的python实现。目前已提出多种one-stage的目标检测算法,速度较快,且准确性较高,后面的课程会练习更好的目标检测模型,同学们如果有时间也可以查阅资料进行了解。

posted @ 2025-04-14 20:36  chenoojkk  阅读(175)  评论(0)    收藏  举报