目标检测是一个很常见的计算机视觉任务,它在现实场景中具有很多的应用。随着深度学习技术的快速发展,当前主流的目标检测算法主要分为单阶段和双阶段,代表性的算法包括SSD和Faster-rcnn等;除此之外也可以分为基于Anchors和Anchors free的算法,尽管这些算法都能取得较高的精度,但是它们都需要依赖GPU和大量的训练样本,另外,这些算法的运行速度都比较慢,一般都是在GPU上面能获得近似实时的速度。对于现实生活中的一些场景而言,它们可能对算法的速度和成本有着较高的要求,但是检测任务又相对来讲比较简单,对于这种情况而言,传统的基于HOG+SVM的检测算法仍然具有较大的用武之地。下面展示了一个案例。
HOG是一种在计算机视觉和图像处理中用来进行物体检测的描述子。通过计算和统计局部区域的梯度方向直方图来构成特征。Hog特征结合SVM分类器已经被广泛应用于图像识别中,尤其在行人检测中获得了极大的成功。
主要思想:在一幅图像中,局部目标的表象和形状能够利用梯度或边缘的方向密度分布来进行描述。其本质是梯度的统计信息,而梯度主要存在于边缘所在的地方。
算法优点:与其他的特征描述方法相比,HOG具有较多优点。由于HOG是在图像的局部方格单元上进行操作的,所以它对图像的几何和光学形变都能保持很好的不变性,这两种形变只会出现在更大的空间领域上。其次,在粗的空域抽样、精细的方向抽样以及较强的局部光学归一化等条件下,只要行人大体上能够保持直立的姿势,可以容许行人有一些细微的肢体动作,这些细微的动作可以被忽略而不影响检测效果。因此HOG特征特别适合于做图像中的人体检测。
实现流程:
支持向量机(support vector machines, SVM)是一种二分类模型,它的基本模型是定义在特征空间上的间隔最大的线性分类器,间隔最大使它有别于感知机;SVM还包括核技巧,这使它成为实质上的非线性分类器。SVM的的学习策略就是间隔最大化,可形式化为一个求解凸二次规划的问题,也等价于正则化的合页损失函数的最小化问题。SVM的的学习算法就是求解凸二次规划的最优化算法。具体的算法实现原理请参考该博客。
步骤1-从训练数据集中获取P个正样本块,并计算这P个正样本块的HOG特征描述子;
步骤2-从训练数据集中获取N个负样本块,并计算这N个负样本块的HOG特征描述子,其中N>>P;
步骤3-在这些正样本和负样本块上面训练一个SVM分类器模型;
步骤4-应用hard-negative-mining。对于负面训练集中的每个图像和每个可能的图像比例,在图像上面应用滑动窗口。在每个窗口中计算相应的HOG特征描述符并应用分类器。如果您的分类器(错误地)将给定窗口分类为一个对象(它将绝对存在误报),记录与误报补丁相关的特征向量以及分类的概率。这种方法被称为hard-negative-mining。具体效果如下图所示:
步骤5-首先获取使用hard-negative-mining技术获取到的错误的正样本块,然后按照概率值对它们进行排序;接着使用这些样本块重新训练分类器模型;
步骤6-将训练好的模型应用到测试图片中;
步骤7-对预测的结果使用NMS去除冗余的BB。
训练代码如下所示,具体的训练数据集从该链接下载,最终将会获得一个训练好的分类模型。
import cv2
import numpy as np
import random
def load_images(dirname, amout = 9999):
img_list = []
file = open(dirname)
img_name = file.readline()
while img_name != '': # 文件尾
img_name = dirname.rsplit(r'/', 1)[0] + r'/' + img_name.split('/', 1)[1].strip('\n')
img_list.append(cv2.imread(img_name))
img_name = file.readline()
amout -= 1
if amout <= 0: # 控制读取图片的数量
break
return img_list
# 从每一张没有人的原始图片中随机裁出10张64*128的图片作为负样本
def sample_neg(full_neg_lst, neg_list, size):
random.seed(1)
width, height = size[1], size[0]
for i in range(len(full_neg_lst)):
for j in range(10):
y = int(random.random() * (len(full_neg_lst[i]) - height))
x = int(random.random() * (len(full_neg_lst[i][0]) - width))
neg_list.append(full_neg_lst[i][y:y + height, x:x + width])
return neg_list
# wsize: 处理图片大小,通常64*128; 输入图片尺寸>= wsize
def computeHOGs(img_lst, gradient_lst, wsize=(128, 64)):
hog = cv2.HOGDescriptor()
# hog.winSize = wsize
for i in range(len(img_lst)):
if img_lst[i].shape[1] >= wsize[1] and img_lst[i].shape[0] >= wsize[0]:
roi = img_lst[i][(img_lst[i].shape[0] - wsize[0]) // 2: (img_lst[i].shape[0] - wsize[0]) // 2 + wsize[0], \
(img_lst[i].shape[1] - wsize[1]) // 2: (img_lst[i].shape[1] - wsize[1]) // 2 + wsize[1]]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
gradient_lst.append(hog.compute(gray))
# return gradient_lst
def get_svm_detector(svm):
sv = svm.getSupportVectors()
rho, _, _ = svm.getDecisionFunction(0)
sv = np.transpose(sv)
return np.append(sv, [[-rho]], 0)
# 主程序
# 第一步:计算HOG特征
neg_list = []
pos_list = []
gradient_lst = []
labels = []
hard_neg_list = []
svm = cv2.ml.SVM_create()
pos_list = load_images(r'G:/python_project/INRIAPerson/96X160H96/Train/pos.lst')
full_neg_lst = load_images(r'G:/python_project/INRIAPerson/train_64x128_H96/neg.lst')
sample_neg(full_neg_lst, neg_list, [128, 64])
print(len(neg_list))
computeHOGs(pos_list, gradient_lst)
[labels.append(+1) for _ in range(len(pos_list))]
computeHOGs(neg_list, gradient_lst)
[labels.append(-1) for _ in range(len(neg_list))]
# 第二步:训练SVM
svm.setCoef0(0)
svm.setCoef0(0.0)
svm.setDegree(3)
criteria = (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 1000, 1e-3)
svm.setTermCriteria(criteria)
svm.setGamma(0)
svm.setKernel(cv2.ml.SVM_LINEAR)
svm.setNu(0.5)
svm.setP(0.1) # for EPSILON_SVR, epsilon in loss function?
svm.setC(0.01) # From paper, soft classifier
svm.setType(cv2.ml.SVM_EPS_SVR) # C_SVC # EPSILON_SVR # may be also NU_SVR # do regression task
svm.train(np.array(gradient_lst), cv2.ml.ROW_SAMPLE, np.array(labels))
# 第三步:加入识别错误的样本,进行第二轮训练
# 参考 http://masikkk.com/article/SVM-HOG-HardExample/
hog = cv2.HOGDescriptor()
hard_neg_list.clear()
hog.setSVMDetector(get_svm_detector(svm))
for i in range(len(full_neg_lst)):
rects, wei = hog.detectMultiScale(full_neg_lst[i], winStride=(4, 4),padding=(8, 8), scale=1.05)
for (x,y,w,h) in rects:
hardExample = full_neg_lst[i][y:y+h, x:x+w]
hard_neg_list.append(cv2.resize(hardExample,(64,128)))
computeHOGs(hard_neg_list, gradient_lst)
[labels.append(-1) for _ in range(len(hard_neg_list))]
svm.train(np.array(gradient_lst), cv2.ml.ROW_SAMPLE, np.array(labels))
# 第四步:保存训练结果
hog.setSVMDetector(get_svm_detector(svm))
hog.save('myHogDector.bin')
测试代码如下所示。
import cv2
import numpy as np
hog = cv2.HOGDescriptor()
hog.load('myHogDector.bin')
cap = cv2.VideoCapture(0)
while True:
ok, img = cap.read()
rects, wei = hog.detectMultiScale(img, winStride=(4, 4),padding=(8, 8), scale=1.05)
for (x, y, w, h) in rects:
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.imshow('a', img)
if cv2.waitKey(1)&0xff == 27: # esc键
break
cv2.destroyAllWindows()
对于目标检测算法而言,通常检测出的结果中会存在一些重复或者冗余的情况,即输出了多个可能是人脸的BB,那么我们通常都需要使用NMS技术来获得一个最准确的BB,下图展示了一个实例。
NMS的原理-NMS的本质是搜索局部极大值,抑制非极大值元素。
NMS的作用-当算法对一个目标产生了多个候选框的时候,选择 score 最高的框,并抑制其他对于改目标的候选框。
NMS的应用场景-一幅图中有多个目标(如果只有一个目标,那么直接取 score 最高的候选框即可)。
NMS的输入-算法对一幅图产生的所有的候选框,以及每个框对应的 score (可以用一个 5 维数组 dets 表示,前 4 维表示四个角的坐标,第 5 维表示分数),阈值 thresh。
NMS的输出-正确的候选框组(dets 的一个子集)。
# coding=utf-8
# 导入python包
import numpy as np
import cv2
def non_max_suppression_slow(boxes, overlapThresh):
# 如果输入为空,直接返回空列表
if len(boxes) == 0:
return []
# 初始化列表索引
pick = []
# 获取边界框的坐标值
x1 = boxes[:,0]
y1 = boxes[:,1]
x2 = boxes[:,2]
y2 = boxes[:,3]
# 计算边界框的区域大小并按照右下角的y坐标进行排序
area = (x2 - x1 + 1) * (y2 - y1 + 1)
idxs = np.argsort(y2)
while len(idxs) > 0:
# 获取索引列表中的最后一个索引,将索引值添加到所选索引的列表中,然后使用最后一个索引初始化禁止显示列表。
last = len(idxs) - 1
i = idxs[last]
pick.append(i)
suppress = [last]
# 遍历索引列表中的所有索引
for pos in xrange(0, last):
# 获取当前的索引
j = idxs[pos]
# 查找边界框起点的最大(x,y)坐标和边界框终点的最小(x,y)坐标
xx1 = max(x1[i], x1[j])
yy1 = max(y1[i], y1[j])
xx2 = min(x2[i], x2[j])
yy2 = min(y2[i], y2[j])
# 计算边界框的宽和高
w = max(0, xx2 - xx1 + 1)
h = max(0, yy2 - yy1 + 1)
# 计算区域列表中计算的边界框和边界框之间的重叠率
overlap = float(w * h) / area[j]
# 如果它们具有较大的重叠率,则抑制掉它
if overlap > overlapThresh:
suppress.append(pos)
# 从禁止显示列表中的索引列表中删除所有索引
idxs = np.delete(idxs, suppress)
# 返回选择的边界框
return boxes[pick]
# 构建一个列表,其中包含将与其各自的边界框一起检查的图像
images = [
("audrey.jpg", np.array([
(12, 84, 140, 212),
(24, 84, 152, 212),
(36, 84, 164, 212),
(12, 96, 140, 224),
(24, 96, 152, 224),
(24, 108, 152, 236)])),
("bksomels.jpg", np.array([
(114, 60, 178, 124),
(120, 60, 184, 124),
(114, 66, 178, 130)])),
("gpripe.jpg", np.array([
(12, 30, 76, 94),
(12, 36, 76, 100),
(72, 36, 200, 164),
(84, 48, 212, 176)]))]
# 循环遍历所有的图像
for (imagePath, boundingBoxes) in images:
# 读取图片并进行复制
print ("[x] %d initial bounding boxes" % (len(boundingBoxes)))
image = cv2.imread(imagePath)
orig = image.copy()
# 遍历每一个矩形框并绘制它们
for (startX, startY, endX, endY) in boundingBoxes:
cv2.rectangle(orig, (startX, startY), (endX, endY), (0, 0, 255), 2)
# 应用非极大值抑制处理
pick = non_max_suppression_slow(boundingBoxes, 0.3)
print ("[x] after applying non-maximum, %d bounding boxes" % (len(pick)))
# 绘制处理之后的矩形框
for (startX, startY, endX, endY) in pick:
cv2.rectangle(image, (startX, startY), (endX, endY), (0, 255, 0), 2)
# 显示结果
cv2.imshow("Original", orig)
cv2.imshow("After NMS", image)
cv2.waitKey(0)
上图展示了NMS算法的处理效果。上面分别展示了3个不同测试图片上的测试效果,红色的边界框表示原始的进行NMS处理之前的效果,绿色的边界框表示进行NMS处理之后的效果,通过上面的结果我们可以发现NMS算法可以很好的抑制掉那些重复的边界框,最终找到一个最准确的边界框。
上面仅仅展示了一种NMS算法,如果你进行实际的测试之后你可能会发现这个算法的处理速度比较慢,并不能满足你的性能要求。那你聪明的你肯定想要了一种可以用来进行算法加速的思路,那就是是使用numpy和cython进行算法加速,下面展示了一个加速版本的NMS算法实现。
# --------------------------------------------------------
# Fast R-CNN
# Copyright (c) 2015 Microsoft
# Licensed under The MIT License [see LICENSE for details]
# Written by Ross Girshick
# --------------------------------------------------------
import numpy as np
cimport numpy as np
cdef inline np.float32_t max(np.float32_t a, np.float32_t b):
return a if a >= b else b
cdef inline np.float32_t min(np.float32_t a, np.float32_t b):
return a if a <= b else b
def cpu_nms(np.ndarray[np.float32_t, ndim=2] dets, np.float thresh):
cdef np.ndarray[np.float32_t, ndim=1] x1 = dets[:, 0]
cdef np.ndarray[np.float32_t, ndim=1] y1 = dets[:, 1]
cdef np.ndarray[np.float32_t, ndim=1] x2 = dets[:, 2]
cdef np.ndarray[np.float32_t, ndim=1] y2 = dets[:, 3]
cdef np.ndarray[np.float32_t, ndim=1] scores = dets[:, 4]
cdef np.ndarray[np.float32_t, ndim=1] areas = (x2 - x1 + 1) * (y2 - y1 + 1)
cdef np.ndarray[np.int_t, ndim=1] order = scores.argsort()[::-1]
cdef int ndets = dets.shape[0]
cdef np.ndarray[np.int_t, ndim=1] suppressed = \
np.zeros((ndets), dtype=np.int)
# nominal indices
cdef int _i, _j
# sorted indices
cdef int i, j
# temp variables for box i's (the box currently under consideration)
cdef np.float32_t ix1, iy1, ix2, iy2, iarea
# variables for computing overlap with box j (lower scoring box)
cdef np.float32_t xx1, yy1, xx2, yy2
cdef np.float32_t w, h
cdef np.float32_t inter, ovr
keep = []
for _i in range(ndets):
i = order[_i]
if suppressed[i] == 1:
continue
keep.append(i)
ix1 = x1[i]
iy1 = y1[i]
ix2 = x2[i]
iy2 = y2[i]
iarea = areas[i]
for _j in range(_i + 1, ndets):
j = order[_j]
if suppressed[j] == 1:
continue
xx1 = max(ix1, x1[j])
yy1 = max(iy1, y1[j])
xx2 = min(ix2, x2[j])
yy2 = min(iy2, y2[j])
w = max(0.0, xx2 - xx1 + 1)
h = max(0.0, yy2 - yy1 + 1)
inter = w * h
ovr = inter / (iarea + areas[j] - inter)
if ovr >= thresh:
suppressed[j] = 1
return keep
[1] 参考链接1
[2] 参考链接2
[1] 该博客是本人原创博客,如果您对该博客感兴趣,想要转载该博客,请与我联系(qq邮箱:1575262785@qq.com),我会在第一时间回复大家,谢谢大家的关注.
[2] 由于个人能力有限,该博客可能存在很多的问题,希望大家能够提出改进意见。
[3] 如果您在阅读本博客时遇到不理解的地方,希望您可以联系我,我会及时的回复您,和您交流想法和意见,谢谢。
[4] 本文测试的图片可以通过该链接进行下载。网盘链接&#8211; 提取码:e31o。
[5] 本人业余时间承接各种本科毕设设计和各种小项目,包括图像处理(数据挖掘、机器学习、深度学习等)、matlab仿真、python算法及仿真等,有需要的请加QQ:1575262785详聊!!!