行人检测基本流程
在实验 1 到实验 3 中我们分别学习了滑动窗口、图像金字塔、方向梯度直方图。本节实验我们将结合这些方法来构建一个传统的行人检测算法。简单来说行人检测就是在提供的图像中,我们想要计算机分辨出哪些是人并且用矩形框标记出人出现在图片中的哪些位置。下图左上角图片中有一个人,如果我们想要用传统的目标检测方法检测到这个人的话,一般分为下面几个步骤。
使用图像金字塔将图片按一定缩放比例生成不同尺寸图片(下图序号 1 所示)。
使用滑动窗口在每张不同尺寸的图片上从左至右、从上向下滑动(下图序号 2 所示)。
将滑动窗口滑过的每个区域使用方向梯度直方图进行特征描述,获得 HOG 特征(下图序号 3 所示)。
将获取到的 HOG 特征使用机器学习分类器(支持向量机)进行分类(下图序号 4 所示)。
最后在图片中使用矩形框标记出被分类器认为是人的类别(下图序号 5 所示)。
功能函数
现在我们已经知道了构建一个传统的行人检测方法的流程,下面让我们开始编写相应的代码来实现行人检测项目。首先我们先下载将要用到的图片。
!wget https://labfile.oss.aliyuncs.com/courses/3096/man.jpg
!wget https://labfile.oss.aliyuncs.com/courses/3096/people.jpg
我们还需要下载在实验 4 中训练好的模型 model。
!wget https://labfile.oss.aliyuncs.com/courses/3096/model
我们还将会用到前面几节实验构建的函数,首先是 HOG类
from skimage import feature
class HOG:
def __init__(self, orientations = 9, pixelsPerCell = (8, 8),
cellsPerBlock = (3, 3), transform = False):
self.orienations = orientations
self.pixelsPerCell = pixelsPerCell
self.cellsPerBlock = cellsPerBlock
self.transform = transform
def describe(self, image):
hist = feature.hog(image, orientations = self.orienations,
pixels_per_cell = self.pixelsPerCell,
cells_per_block = self.cellsPerBlock,
transform_sqrt = self.transform)
return hist
我们还需使用滑动窗口函数,函数需要 3 个输入参数 image、window 和 step。
def sliding_window(image, window = (64, 128), step = 4):
for y in range(0, image.shape[0] - window[1], step):
for x in range(0, image.shape[1] - window[0], step):
yield (x, y, image[y:y + window[1], x:x + window[0]])
其中 image 表示输入图片。window 表示滑动窗口的高和宽,这里设置一个默认值 (64, 128) 表示滑动窗口的宽为 64,高为 128。step 表示滑动窗口移动一次的步长,这里我们设置一个默认的步长为 4。
接下来是要使用到的图像金字塔函数,该函数需要用 OpenCV 对图片进行缩放,所以我们需要导入 cv2 模块。该函数有三个参数 image、top、ratio。
import cv2
def pyramid(image, top = (224, 224), ratio = 1.5):
yield image
while True:
(w, h) = (int(image.shape[1] / ratio), int(image.shape[0] / ratio))
image = cv2.resize(image, (w, h), interpolation = cv2.INTER_AREA)
if w < top[1] or h < top[0]:
break
yield image
其中 image 表示输入图片。top 表示图像将会被缩小的最小尺寸,我们将这个参数设置一个默认值为 (224, 224),第一个 224 表示图片的高,第二个 224 表示图片的宽。第三个参数 ratio 表示每次图像将会被缩小的倍数,我们给这个参数设置了一个默认值为 1.5 表示图片每次缩小 1.5 倍。
缩放
在处理数据集和检测时我们都需要对图片进行缩放,同时我们会希望图片在缩放后它的宽和高的比例保持不变,这样图片就不会被过度的拉伸导致变形,所以我们需要定义一个函数让输入图片可以按照宽高比进行缩放。
我们定义一个函数并将函数命名为 resize ,这个函数有 3 个输入参数 image、height、width。image 表示输入图片,height 表示输出图片的高,width 表示输出图片的宽。
def resize(image, height = None, width = None):
h, w = image.shape[:2]
dim = None
if width is None and height is None:
return image
if width is None:
dim = (int(w * (height / h)), height)
else:
dim = (width, int(h * (width / w)))
resized = cv2.resize(image, dim, interpolation = cv2.INTER_AREA)
return resized
在第 2 行,使用 shape 方法获取输入图片的高 h 和 宽 w,然后定义一个 dim 变量用于存放缩放后图片的宽和高。
然后使用 if 语句做一个判断,如果不提供 height 和 width 给函数,则函数将返回原图片(见上面代码 5 - 6 行)。如果我们只提供输出图片的高 height,那么输出图片的宽就等于输入图片的宽 w 乘以宽高比 height / h。同样的,如果只提供输出图片的宽 width,那么输出图片的高 height 就等于输入图片的高 h 乘以宽高比,这里的宽高比就变成了 width / w(见上面代码 8 - 11 行)。
最后使用 cv2.resize 函数缩放图片,函数的第二个参数 dim 就是上面计算得到的输出图片的宽和高。最后返回缩放后的图片。
坐标变换
在实验 2 中讲到,我们使用滑动窗口在图像金字塔的每一层中寻找目标,当分类器判断滑动窗口中的区域是人的时候,我们就会保存该滑动窗口的坐标,这样就可以使用矩形框标记出行人。但是因为滑动窗口是在不同尺寸的图片上滑动,如果我们直接使用保存的坐标在原始图片上画出矩形框,那么矩形框内很大概率就不是检测到的目标了。
如下图,左图是图像金字塔中的其中一个图层,图中的绿色矩形框就是一个滑动窗口并且矩形框内有一个人,那么就表示我们检测到了目标,所以我们就需要保存左图中矩形框的坐标。但是保存的坐标是相对于当前矩形框所在的图片,如果我们直接用保存的坐标在原图中绘制矩形框,那么其结果就会像右图中绿色矩形框一样,绿色矩形框内只有背景而没有人,而我们希望的是绘制右图中红色的矩形框。为了解决这个问题,我们就需要进行坐标变换。
首先我们创建一个名为 coordinate_transformation 的函数,这个函数需要 7 个输入参数 height、width、h、w、x、y、roi。
def coordinate_transformation(height, width, h, w, x, y, roi):
if h is 0 or w is 0:
print("divisor can not be zero !!")
img_h = int(height/h * roi[1])
img_w = int(width/w * roi[0])
img_y = int(height/h * y)
img_x = int(width/w * x)
return (img_x, img_y, img_w, img_h)
其中 height, width 表示原始图片的的高和宽,h, w 表示当前所处图像金字塔图层的高和宽,x, y 表示滑动窗口的左上角顶点的坐标,roi 是一个元组,表示滑动窗口的宽和高。
在函数内我们首先需要判断 h, w 是否为 0,因为在坐标转换过程中 h 和 w 将作为除数计算两张图片的缩放比例。如果 h 或 w 为 0 则输出一段字符串告诉用户除数不能为 0(见上面代码 2 - 3 行)。
上面第 5 - 6 行代码是滑动窗口宽和高的转换计算方法。转换后的高 img_h 就等于 roi[1] 乘以两张图片的高的比,roi[1] 表示滑动窗口的高。转换后的宽 img_w 就等于 roi[0] 乘以两张图片的宽的比,roi[0] 表示滑动窗口的宽。这里需要注意的是这个两个值必须是整数。这一步的目的是为了让滑动窗口的尺寸尽量贴合目标,即希望在用矩形框标记目标时能够将目标完全包含在矩形框中。
上面第 8 - 9 行代码是滑动窗口左上角的顶点坐标转换的计算方法,img_y、img_x 就是转换后的坐标,计算方法同上面计算滑动窗口的宽高转换一样,需要注意的是这两个值必须是整数。
最后我们将 img_x、img_y、img_w、img_h 作为函数的返回值输出。这样我们就能使用这四个值在原始图片上绘制矩形框了。
检测模块
现在我们已经构建好了需要的函数,接下来我们就可以使用这些模块对图片中的行人进行检测了。首先我们导入 joblib 模块用于加载训练好的模型。我们创建一个 img_path 变量用于保存图片路径。
import joblib
img_path = "man.jpg"
下面我们定义三个常量,ratio 表示在图像金字塔中每层图像将会被缩小的倍数, 1.5 表示每层图片将被缩小 1.5 倍。i_roi 表示滑动窗口的尺寸,(64, 128) 表示滑动窗口的宽为 64,高为 128。step 表示滑动窗口的步长,这里我们设置为 20。top 表示图像金字塔中的图像将会被缩小的最小尺寸,我们将这个参数设置为 (128, 128)。
ratio = 1.5
i_roi = (64, 128)
step = 20
top = (128, 128)
我们使用导入的 joblib 模块的 load 方法加载分类模型。
model = joblib.load("model")
我们使用 HOG 类初始化一个 hog 实例,并将 transform 参数设为 True。
hog = HOG(transform = True)
接下来我们先使用 cv2.imread 函数读取要用于检测行人的图片。然后使用 resize 方法将图片保持宽高比进行缩放,图片缩放后的高设置为 500,然后将我们缩放后的图片转换为灰度图。最后我们使用 shape 方法获取灰度图的高和宽,这两个值将用于坐标变换。
image = cv2.imread(img_path)
resized = resize(image, height = 500)
gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
height, width = gray.shape[:2]
下面我们创建一个空列表 roi_loc,当滑动窗口检测到人,我们就将这个滑动窗口的左上角顶点坐标、宽、高存储在里面。然后我们先使用一个 for 循环获取每层缩放的图片,然后使用 shape 方法获取每层图片的高 h 和 宽 w。
再用一个 for 循环在每层图片上进行滑动窗口的操作。在循环内先将获取的每个滑动窗口使用 cv2.resize 缩放到相同的尺寸 (64, 128),然后使用 hog 实例的 describe 方法提取缩放图片的 HOG 特征并将其传赋值给 hist。
在第二个 for 循环内我们使用一个 if 语句判断滑动窗口内是否是人。model.predict([hist])[0] 表示使用 model 的 predict 方法对输入值 hist 进行分类,如果分类的结果是 1 就表示滑动窗口内是人,则 if 语句是真,那么就执行 coordinate_transformation 函数进行坐标变换。
坐标变换函数需要输入七个值,height 和 width 是图像金字塔最底层图片的高和宽,也就是尺寸最大图片的高和宽;h 和 w 是检测到人的滑动窗口所处的图层的高和宽;x 和 y 是滑动窗口左上角顶点的坐标;i_roi 是滑动窗口的宽和高。坐标变换函数的输出值共有 4 个,img_x 和 img_y 表示经过变换后滑动窗口的左上角顶点坐标,img_w、img_h 表示变换后滑动窗口的宽和高。我们将这 4 个输出值作为列表保存在 roi_loc 中。
roi_loc = []
for image in pyramid(gray, top = (128, 128), ratio = ratio):
h, w = image.shape[:2]
for (x, y, roi) in sliding_window(image, window = i_roi, step = step):
roi = cv2.resize(roi, (64, 128), interpolation = cv2.INTER_AREA)
hist = hog.describe(roi)
if model.predict([hist])[0]:
img_x, img_y, img_w, img_h = coordinate_transformation(height, width, h, w, x, y, i_roi)
roi_loc.append([img_x, img_y, img_w, img_h])
然后我们用矩形框标记出被分类器认为是人的区域。
for (x, y, w, h) in roi_loc:
cv2.rectangle(resized, (x, y), (x + w, y + h), (0, 255, 0), 2)
接下来通过下面的代码来显示检测结果,首先从 matplotlib 导入 pyplot,然后我们使用 %matplotlib inline 魔法函数让图片在页面中显示。我们使用 plt.figure(figsize = (10,10)) 创建一个宽和高都是 10 英寸的图像实例。然后使用 resized[:,:,::-1] 切片方法将图片通道的顺序调转,最后使用 plt.imshow 在页面中呈现绘图后的结果。
from matplotlib import pyplot as plt
%matplotlib inline
plt.figure(figsize = (10,10))
resized = resized[:,:,::-1]
plt.imshow(resized)
执行整个脚本,如果没有意外的话我们能看到类似下图的结果。
我们将 img_path = "man.jpg" 修改为 img_path = "people.jpg",再重新执行脚本,我们将得到类似下图的结果。
可以看出我们已经用矩形框框出图片中的人了,但是我们也发现图中有太多的矩形框相互重叠、遮挡,在下一节实验中我们将剔除冗余的矩形框,让每个目标值被一个矩形框标记。通过观察图片我们还会发现有些没有人的地方也被矩形框标记了,其原因是这些地方可能和人比较相似或我们的模型不够鲁棒,我们可以通过增加训练样本或把这些被误认为是人的区域放入训练样本中进行训练来解决这个问题。