基于python的HOG+SVM目标检测算法实现

15 篇文章 7 订阅

一、场景需求解读

  目标检测是一个很常见的计算机视觉任务,它在现实场景中具有很多的应用。随着深度学习技术的快速发展,当前主流的目标检测算法主要分为单阶段和双阶段,代表性的算法包括SSD和Faster-rcnn等;除此之外也可以分为基于Anchors和Anchors free的算法,尽管这些算法都能取得较高的精度,但是它们都需要依赖GPU和大量的训练样本,另外,这些算法的运行速度都比较慢,一般都是在GPU上面能获得近似实时的速度。对于现实生活中的一些场景而言,它们可能对算法的速度和成本有着较高的要求,但是检测任务又相对来讲比较简单,对于这种情况而言,传统的基于HOG+SVM的检测算法仍然具有较大的用武之地。下面展示了一个案例。
在这里插入图片描述

二、HOG算法简介

  HOG是一种在计算机视觉和图像处理中用来进行物体检测的描述子。通过计算和统计局部区域的梯度方向直方图来构成特征。Hog特征结合SVM分类器已经被广泛应用于图像识别中,尤其在行人检测中获得了极大的成功。
主要思想:在一幅图像中,局部目标的表象和形状能够利用梯度或边缘的方向密度分布来进行描述。其本质是梯度的统计信息,而梯度主要存在于边缘所在的地方。
算法优点:与其他的特征描述方法相比,HOG具有较多优点。由于HOG是在图像的局部方格单元上进行操作的,所以它对图像的几何和光学形变都能保持很好的不变性,这两种形变只会出现在更大的空间领域上。其次,在粗的空域抽样、精细的方向抽样以及较强的局部光学归一化等条件下,只要行人大体上能够保持直立的姿势,可以容许行人有一些细微的肢体动作,这些细微的动作可以被忽略而不影响检测效果。因此HOG特征特别适合于做图像中的人体检测。
实现流程

  • 步骤1-读取待检测的图片;
  • 步骤2-将输入图像灰度化(将输入的彩色图像的r,g,b值通过特定公式转换为灰度值);
  • 步骤3-采用Gamma校正法对输入图像进行颜色空间的标准化(归一化);
  • 步骤4-计算图像中每个像素的梯度值(包括大小和方向),捕获轮廓信息;
  • 步骤5-统计每个cell内的梯度直方图(不同梯度的个数),形成每个cell的特征描述子;
  • 步骤6-将每几个cell组成一个block(以3*3为例),一个block内所有cell的特征串联起来得到该block的HOG特征描述子;
  • 步骤7-将图像image内所有block块的HOG特征描述子串联起来得到该image(检测目标)的HOG特征描述子,这就是最终分类的特征向量。
    在这里插入图片描述

三、SVM算法简介

  支持向量机(support vector machines, SVM)是一种二分类模型,它的基本模型是定义在特征空间上的间隔最大的线性分类器,间隔最大使它有别于感知机;SVM还包括核技巧,这使它成为实质上的非线性分类器。SVM的的学习策略就是间隔最大化,可形式化为一个求解凸二次规划的问题,也等价于正则化的合页损失函数的最小化问题。SVM的的学习算法就是求解凸二次规划的最优化算法。具体的算法实现原理请参考该博客

四、基于HOG的目标检测算法训练流程

  • 步骤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()

六、非极大值抑制(NMS)简介及代码实现

  对于目标检测算法而言,通常检测出的结果中会存在一些重复或者冗余的情况,即输出了多个可能是人脸的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效果展示与分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  上图展示了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] 如果您对AI、自动驾驶、AR、ChatGPT等技术感兴趣,欢迎关注我的微信公众号“AI产品汇”,有问题可以在公众号中私聊我!
[2] 该博客是本人原创博客,如果您对该博客感兴趣,想要转载该博客,请与我联系(qq邮箱:1575262785@qq.com),我会在第一时间回复大家,谢谢大家的关注.
[3] 由于个人能力有限,该博客可能存在很多的问题,希望大家能够提出改进意见。
[4] 如果您在阅读本博客时遇到不理解的地方,希望您可以联系我,我会及时的回复您,和您交流想法和意见,谢谢。
[5] 本文测试的图片可以通过关注公众号AI产品汇之后找我索取!
[6] 本人业余时间承接各种本科毕设设计和各种小项目,包括图像处理(数据挖掘、机器学习、深度学习等)、matlab仿真、python算法及仿真等,有需要的请加QQ:1575262785详聊!!!

  • 41
    点赞
  • 458
    收藏
    觉得还不错? 一键收藏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值