最近在看《深度学习全书 公式+推导+代码+TensorFlow》—— 清华大学出版社 这本书, 看到第8章——目标检测,其中有使用 HOG 进行目标检测的代码,觉得写的通俗易懂,就分享给大家~(但是课本中的代码有一些Bug,本人已经修改完了,现在的代码是可以跑通的)
1 # (1) 载入库,本例使用scikit-Image库
2 # 载入套件
3 import numpy as np
4 import matplotlib.pyplot as plt
5 from skimage.feature import hog
6 from skimage import data, exposure
7 plt.ion() # 打开交互模式
8
9 # (2) HOG测试:使用Scikit-Image内建的女航天员图像来测试HOG的效果。
10 # 获取测试图片
11 image = data.astronaut()
12
13 # 取得图片的 hog
14 # 参数:ndarray-输入图像 orientations-可选方向箱的数量 pixels_per_cell-可选的单元格大小(以像素为单位)
15 # cells_per_block-可选每个块中的单元格数 visualize-同时返回HOG的图像 multichannel-如果为True,则最后一个图像为u的被视为颜色通道
16 fd, hog_image = hog(image, orientations=8, pixels_per_cell=(16, 16),
17 cells_per_block=(1, 1), visualize=True, multichannel=True)
18
19 # 原图与 hog图比较
20 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6), sharex=True, sharey=True)
21
22 ax1.axis('off')
23 # ax1.imshow(image, cmap=plt.cm.gray)
24 ax1.imshow(image)
25 ax1.set_title('Input image')
26
27 # 调整对比,让显示比较清楚
28 # skimage.exposure.exposure 模块中的函数,在对图像进行拉伸或者伸缩强度水平后返回修改后的图像,
29 # 输入图像和输出图像的强度范围分别由 in_range 和 out_range 指定,用来拉伸或缩小输入图像的强度范围。
30 hog_image_rescaled = exposure.rescale_intensity(hog_image, in_range=(0, 10))
31
32 ax2.axis('off')
33 ax2.imshow(hog_image_rescaled, cmap=plt.cm.gray)
34 ax2.set_title('Histogram of Oriented Gradients')
35 # plt.show()
36
37 # (3) 收集正样本:使用scikit-learn内建的人脸数据集作为正样本,共有13233个。
38 # 收集正样本 (positive set)
39 # 使用 scikit-learn 的人脸资料集
40 from sklearn.datasets import fetch_lfw_people
41 faces = fetch_lfw_people()
42 positive_patches = faces.images
43 print("正样本数据大小 = ", positive_patches.shape)
44
45 # (4) 观察正样本中部分图片
46 # 显示正样本部份图片
47 fig, ax = plt.subplots(4, 6)
48 fig.suptitle('image for positive patches', fontsize=14)
49 for i, axi in enumerate(ax.flat):
50 axi.imshow(positive_patches[500 * i], cmap='gray')
51 axi.axis('off')
52
53 # (5)收集负样本 (negative set),使用Scikit-Image内建的数据集,共有9批
54 # 使用 Scikit-Image 的非人脸资料
55 from skimage import data, transform, color
56
57 imgs_to_use = ['hubble_deep_field', 'text', 'coins', 'moon',
58 'page', 'clock', 'coffee', 'chelsea', 'horse']
59 # images = [color.rgb2gray(getattr(data, name)()) for name in imgs_to_use]
60 images = []
61 for na in imgs_to_use:
62 temp = getattr(data, na)()
63 if len(temp.shape) == 3:
64 images.append(color.rgb2gray(temp))
65 else:
66 images.append(color.rgb2gray(color.gray2rgb(temp)))
67
68 print("负样本种类数量 = ", len(images))
69
70 # (6)增加负样本批数:将负样本转换为不同的尺寸,也可以使用数据增补技术。
71 # 将负样本转换为不同的尺寸
72 from sklearn.feature_extraction.image import PatchExtractor
73
74
75 # 转换为不同的尺寸
76 def extract_patches(img, N, scale=1.0, patch_size=positive_patches[0].shape):
77 extracted_patch_size = tuple((scale * np.array(patch_size)).astype(int))
78 # PatchExtractor:产生不同尺寸的图像
79 # PatchExtractor - 从图像集合中提取补丁 patch_size - 一个补丁的尺寸 max_patches - 每幅图像要提取的最补丁数
80 # random_state - 确定 max_patches is not None 时用于随机采样的随机数生成器
81 extractor = PatchExtractor(patch_size=extracted_patch_size, max_patches=N, random_state=0)
82 # Transform the image samples in X into a matrix of patch data.
83 patches = extractor.transform(img[np.newaxis]) # np.newaxis会增加新的一维数据
84 if scale != 1:
85 # skimage.transform.resize图片后会顺便把图片的像素归一化缩放到(0,1)区间内
86 patches = np.array([transform.resize(patch, patch_size) for patch in patches])
87 return patches
88
89
90 # 产生 27000 张图像
91 negative_patches = np.vstack([extract_patches(im, 1000, scale) for im in images for scale in [0.5, 1.0, 2.0]])
92 print(negative_patches.shape)
93
94 # (7)观察负样本中部分图片
95 # 显示部份负样本
96 fig, ax = plt.subplots(4, 6)
97 fig.suptitle('image for negative patches', fontsize=14)
98 # numpy.ndarray.flat 是将数组转换为1-D的迭代器,flat返回的是一个迭代器,可以用for访问数组每一个元素
99 for i, axi in enumerate(ax.flat):
100 axi.imshow(negative_patches[600 * i], cmap='gray')
101 axi.axis('off')
102
103
104 # (8)合并正样本与负样本
105 from skimage import feature # To use skimage.feature.hog()
106 from itertools import chain
107 # chain可以合并它们以形成一个新的单个列表
108 # X_train是数据,y_train实标签(正样本标签为1,负样本标签为0)
109 X_train = np.array([feature.hog(im) for im in chain(positive_patches, negative_patches)])
110 y_train = np.zeros(X_train.shape[0])
111 y_train[:positive_patches.shape[0]] = 1
112
113 # (9)使用 SVM 进行二分类的训练
114 from sklearn.svm import LinearSVC
115 from sklearn.model_selection import GridSearchCV
116
117 # GridSearchCV:用于对估计器的指定参数值进行详尽搜索。cv确定交叉验证切分策略。
118 # LinearSVC:实现线性支持向量机的分类。
119 # dual参数:选择算法以解决双重或原始优化问题。当n_samples> n_features时,首选dual = False。
120 # C为矫正过度拟合强度的倒数(惩罚系数,用来控制损失函数的惩罚系数,类似于LR中的正则化系数),使用 GridSearchCV 寻求最佳参数值
121 grid = GridSearchCV(LinearSVC(dual=False), {'C': [1.0, 2.0, 4.0, 8.0]}, cv=3)
122 grid.fit(X_train, y_train)
123 print("grid.best_score_ = ", grid.best_score_)
124
125 # (10)取得最佳参数值
126 # C 最佳参数值
127 print("grid.best_params_ = ", grid.best_params_)
128
129 # (11)依最佳参数值再训练一次,取得最终模型
130 model = grid.best_estimator_
131 model.fit(X_train, y_train)
132
133 # (12)取新图像测试:需先转换为灰阶图像
134 test_img = data.astronaut()
135 test_img = color.rgb2gray(test_img)
136 test_img = transform.rescale(test_img, 0.5) # 对图像进行缩放,参数是缩放的倍数
137 test_img = test_img[:120, 60:160]
138 plt.figure()
139 plt.imshow(test_img, cmap='gray')
140 plt.title('test img')
141 plt.axis('off')
142
143
144 # (13)定义滑动窗口函数
145 # 滑动视窗函数
146 def sliding_window(img, patch_size=positive_patches[0].shape, istep=2, jstep=2, scale=1.0):
147 Ni, Nj = (int(scale * s) for s in patch_size)
148 for i in range(0, img.shape[0] - Ni, istep):
149 for j in range(0, img.shape[1] - Ni, jstep):
150 patch = img[i:i + Ni, j:j + Nj]
151 if scale != 1:
152 patch = transform.resize(patch, patch_size)
153 yield (i, j), patch # (i, j)左上角坐标,patch是裁剪出来互动窗口图片,图片大小是(Ni, Nj)
154
155
156 # (14)计算HOG: 使用滑动窗口来计算每一滑动窗口的 Hog,导入模型辨识
157 indices, patches = zip(*sliding_window(test_img))
158 patches_hog = np.array([feature.hog(patch) for patch in patches])
159 print("patches_hog.shape = ", patches_hog.shape)
160
161 # 辨识每一视窗
162 labels = model.predict(patches_hog)
163 print(labels.sum()) # 侦测到的总数
164
165 # (15)显示这55个合格窗口
166 # 将每一个侦测到的视窗显示出来
167 fig, ax = plt.subplots()
168 ax.imshow(test_img, cmap='gray')
169 ax.set_title('Sliding window with detected faces')
170 ax.axis('off')
171 # 取得左上角座标
172 Ni, Nj = positive_patches[0].shape
173 indices = np.array(indices)
174 # 显示标签为正样本的框
175 for i, j in indices[labels == 1]:
176 # class matplotlib.patches.Rectangle(xy, width, height, *, angle=0.0, rotation_point='xy', **kwargs)
177 ax.add_patch(plt.Rectangle((j, i), Nj, Ni, edgecolor='red', alpha=0.3, lw=2, facecolor='none'))
178 candidate_patches = patches_hog[labels == 1]
179 print("candidate_patches.shape = ", candidate_patches.shape)
180
181
182 # (16)筛选合格窗口:使用Non-Maximum Suppression(NMS) 算法,剔除多余的窗口。
183 # 定义NMS算法函数:这是由Pedro Felipe Felzenszwalb等学者发明的算法,执行速度较慢,Tomasz Malisiewicz因此提出了改善的算法。函数的
184 # 重叠比例阈值(OverlapThresh)参数一般设为0.3~0.5
185 # Non-Maximum Suppression演算法
186 # https://www.pyimagesearch.com/2014/11/17/non-maximum-suppression-object-detection-python/
187 # Non-Maximum Suppression演算法 by Felzenszwalb et al.
188 # boxes:所有候选的视窗,overlapThresh:视窗重叠的比例门槛
189 def non_max_suppression_slow(boxes, overlapThresh=0.5):
190 if len(boxes) == 0:
191 return []
192
193 pick = [] # 储存筛选的结果
194 x1 = boxes[:, 0] # 取得候选的视窗的左/上/右/下 坐标
195 y1 = boxes[:, 1]
196 x2 = boxes[:, 2]
197 y2 = boxes[:, 3]
198
199 # 计算候选视窗的面积
200 area = (x2 - x1 + 1) * (y2 - y1 + 1)
201 idxs = np.argsort(y2) # 依视窗的底Y座标排序
202
203 # 比对重叠比例
204 while len(idxs) > 0:
205 # 最后一笔
206 last = len(idxs) - 1
207 i = idxs[last]
208 pick.append(i)
209 suppress = [last]
210
211 # 比对最后一笔与其他视窗重叠的比例
212 for pos in range(0, last):
213 j = idxs[pos]
214 print(i, j)
215 print("i = ", x1[i], y1[i], x2[i], y2[i], "j = ", x1[j], y1[j], x2[j], y2[j])
216 # 取得所有视窗的涵盖范围
217 xx1 = max(x1[i], x1[j])
218 yy1 = max(y1[i], y1[j])
219 xx2 = min(x2[i], x2[j])
220 yy2 = min(y2[i], y2[j])
221 w = max(0, xx2 - xx1 + 1)
222 h = max(0, yy2 - yy1 + 1)
223 # _, (ax1, ax2) = plt.subplots(1, 2)
224 # ax1.imshow(test_img, cmap='gray')
225 # ax1.axis('off')
226 # ax1.add_patch(plt.Rectangle((x1[i], y1[i]), Nj, Ni, edgecolor='red', alpha=0.3, lw=2, facecolor='none'))
227 # ax1.add_patch(plt.Rectangle((x1[j], y1[j]), Nj, Ni, edgecolor='blue', alpha=0.3, lw=2, facecolor='none'))
228 # ax2.imshow(test_img, cmap='gray')
229 # ax2.axis('off')
230 # ax2.add_patch(plt.Rectangle((xx1, yy1), xx2-xx1, yy2-yy1, edgecolor='Magenta', alpha=0.3, lw=2, facecolor='none'))
231
232 # 计算重叠比例
233 overlap = float(w * h) / area[j]
234 print("overlap = ", overlap)
235 # 如果大于门槛值,则储存起来
236 if overlap > overlapThresh:
237 suppress.append(pos)
238 # print("suppress = ", suppress)
239 pass
240 # 删除合格的视窗,继续比对
241 idxs = np.delete(idxs, suppress) # 在这里删除之后,idxs就为array([], dtype=int64)
242
243 # 传回合格的视窗
244 return boxes[pick]
245
246
247 # (17)呼叫 non_max_suppression_slow 函数,剔除处于的窗口
248 # 使用 Non-Maximum Suppression演算法,剔除多余的视窗。
249 candidate_boxes = []
250 for i, j in indices[labels == 1]:
251 candidate_boxes.append([j, i, Nj+j, Ni+i])
252 final_boxes = non_max_suppression_slow(np.array(candidate_boxes).reshape(-1, 4))
253
254 # 将每一个合格的视窗显示出来
255 fig, ax = plt.subplots()
256 ax.imshow(test_img, cmap='gray')
257 ax.set_title('Remaining sliding window after non-maximum suppression')
258 ax.axis('off')
259 # 显示
260 for x1, y1, x2, y2 in final_boxes:
261 ax.add_patch(plt.Rectangle((x1, y1), x2-x1, y2-y1, edgecolor='red', alpha=0.3, lw=2, facecolor='none'))
262 pass
注意:
1. matplotlib 在交互模式下想要显示图片内容且可以看变量的参数,可以在 262 行打上断点,就可以看到图片了。
2. 在非极大值抑制算法中,我们本应该要根据每个 boundingbox 的置信度得分排列 boundingbox,拿置信度得分最高的 boundingbox 和其他的 boundingbox 进行比较从而剔除和最高置信度得分的 boundingbox 重叠程度超过一定阈值的 boundingbox,但是这里使用的是选择右下角坐标 y 值最大的值作为第一个保留项,这里和实际的 NMS是不一样的。
运行结果: