系列文章目录
VGG Image Classification Practical
Practical Category Detection
前言
对象类别检测的目的是在图像中识别和定位给定类型的对象。例如,应用程序包括在街道场景中检测行人、汽车或交通标志、感兴趣的对象(如网络图像中的工具或动物)或医学图像中的特定特征。给定对象类型(如人员),检测器接收图像作为输入,并以输出零、一个或多个包围框的形式产生图像中每一个对象发生的情况。关键的挑战是,探测器需要找到物体,而不管它们在图像中的位置和大小,以及姿态和其他变化因素,如衣服、照明、遮挡等等。
具体内容如下:(1)利用HOG特征描述图像区域;(2)建立基于HOG的滑动窗口检测器,将目标定位到图像中;(3)处理多尺度和多目标事件;(4)使用线性支持向量机来学习物体的外观;(5)从平均精度的角度来评估目标检测器;(6)使用难例挖掘学习对象检测器。
在做这项联系时一定要仔细看lab.py文件
一、检测原理
作为运行实例,我们利用德国交通标志检测基准的数据,考虑了路标检测的问题。该数据包括许多示例交通图像,以及一些包含不同大小和位置的一个或多个交通标志的更大的测试图像。它还附带真值,即为每个符号发生指定的边界框和符号标签,这是评估检测器的质量所必需的。
1.0加载数据集
# Load the data.
import lab
import torch
import copy
torch.set_grad_enabled(False)
imdb = lab.load_data('mandatory') ;
该数据集包含以下内容
imdb[‘train’][‘images’]: a list of train image names.
imdb[‘train’][‘boxes’]: a 𝑁×4 tensor of object bounding boxes, in the form [𝑥min,𝑦min,𝑥max,𝑦max] .
imdb[‘train’][‘box_images’]: for each bounding box, the name of the image containing it.
imdb[‘train’][‘box_labels’]: for each bounding box, the object label.
imdb[‘train’][‘box_patches’]: a 𝑁×364×64 tensor of image patches, one for each training object. Patches are in RGB format.
1.1可视化训练图案
from matplotlib import pyplot as plt
# Plot the training patches.
plt.figure(1, figsize=(12,12))
lab.imarraysc(imdb['train']['box_patches'])
plt.title('Training patches')
- 图像均值
# Plot the average patch.
#计算这些图像的均值,并可视化
plt.figure(2,figsize=(8,8))
lab.imsc(imdb['train']['box_patches'].mean(0))
plt.title('Training patches mean') ;
- Question: what can you deduce about the object variability from the average image?
- Question: most boxes extend slightly around the object extent. Why do you think this may be valuable in learning a detector?
尽可能地少漏掉一些特征像素,也稍微的借助下周围像素做出判断。
1.2 在训练中提取HOG特征
对象检测器通常工作在一层低层特征之上.我们将使用卷积神经网络来提取这些特征。然而,我们没有按照惯例从数据中学习它,而是采取一种捷径,创建一个手工制作的网络来计算HOG(面向渐变直方图)特征。以这种方式,我们仍然可以有效地利用底层的PyTorch库。
为了学习该对象的模型,我们首先从与现有训练示例相对应的图像块中提取特征。HOG由Lab.HOGNet()神经网络计算。该网络以灰度图像或RGB图像(或一批𝑁图像)作为输入,表示为PychTorch张量𝑁×𝐶×𝑊×𝐻阵列,其中𝐶C为1或3。 输出是一个𝑁×27×𝑊/cell-size×𝐻/cell-size维度数组,默认情况下单元格大小为8。
# Get the HOG extractor neural network.
hog_extractor = lab.HOGNet()
# Get the HOG representation of the trianing patches.
hog_train = hog_extractor(imdb['train']['box_patches'])
1.3简单的HOG模型
一个非常基本的对象模型可以通过对示例对象的特征进行平均,然后减去平均特征值。
# Average the HOG of the corresponding training images
w = torch.mean(hog_train, 0, keepdim=True)
w = w - w.mean()
模型可以通过呈现w来可视化,它是一个HOG特征数组。这可以使用HOG_Extractor.to-Image方法来完成。该方法采用一个𝑁×27×𝐻×𝑊 HOG张量,并返回相应的𝑁×1×𝐻𝑟×𝑊𝑟张量。
# Render the HOG descriptor
wim = hog_extractor.to_image(w)
# Plot it
plt.figure(figsize=(6,6))
lab.imsc(wim[0]) ;
- Spend some time to study this plot and make sure you understand what is visualized.
Question: Can you make sense of the resulting plot?
在回答这个问题时,要了解到明亮的地方表示该位置梯度高,再加上明亮的地方正好与上图里的圆形边框基本一致,所以hog特征提取能够对边缘敏感,从而检测出物体框架。
1.4测试模型
通过:(I)提取图像的HOG特征,(Ii)将模型转换到生成的特征图上,将模型与测试图像进行匹配。利用PyTorch卷积算子nn.Conv2d将模型包裹在相应的卷积层,可以方便地实现这一目的。
import torch
import torch.nn as nn
# Wrap the model w in a convolutional layer.
model = nn.Conv2d(27, 1, w.shape[2:], bias=False)
model.weight.data = w
print("Model parameters:", model)
# Extract the HOG representation of a test image.
im = lab.imread('data/mandatory.jpg')
hog = hog_extractor(im)
# Apply the model.
scores = model(hog)
- Model parameters: Conv2d(27, 1, kernel_size=torch.Size([8, 8]), stride=(1, 1), bias=False)
# Visualize the image.
plt.figure(1, figsize=(8,8))
lab.imsc(im[0])
plt.title('Input image')
# Visualize its HOG representation.
plt.figure(2, figsize=(8,8))
lab.imsc(hog_extractor.to_image(hog)[0])
plt.title('HOG representation')
# Visualize the scores as a heatmap.
plt.figure(3, figsize=(8,8))
lab.imsc(scores[0])
plt.title('Scores') ;
1.5提取上层检测
现在,模型已经应用于图像,我们有一个响应图得分。为了从中提取一个检测,我们(I)找出哪个图像框对应于分数张量中的每个条目,以及(Ii)找到具有最大分数的框。
第一步由lab.box_for_几十s函数完成,该函数以1×𝐻×𝑊记分映射为输入,返回4×𝐻×𝑊张量盒,其中Box[:,v_,u_]是一个向量[𝑢0,𝑣0,𝑢1,𝑣1],表示与分数[1,v,u]对应的图像空间中的边框。这里(𝑢0,𝑣0)是框的左上角,(𝑢1,𝑣1)右下角.
- Question: Inspect the code below. Why cell_size is involved in the calculations performed by boxes_for_scores?
更精确的框出位置,找得分最大的框
# Get the box coordinates for each score map location as a 4 x H x W tensor where
# score[0] is a 1 x H x W tensor.
boxes = lab.boxes_for_scores(model, scores[0])
# Convert `boxes` into a N x 4 list.
boxes = boxes.reshape(4,-1).permute(1,0)
# Do the same for `scores` and pick the maximum.
best, best_index = torch.max(scores[0].reshape(-1), 0)
box = boxes[best_index]
# Plot the results.
plt.figure(1, figsize=(10,10))
lab.imsc(im[0])
lab.plot_box(box)
plt.title(f'Detected object (score: {best:.3f})');
如何将分数图转换成方框。这里可以查看lab.box_to_scores的代码。
- 求出的最大值分为两个步骤,首先是在第2维(高度)和第1维(宽度)上的得分最大化,得到指数u和v。
- U和V是HOG的单位。我们将此转换为像素坐标,方法是乘以HOG单元大小,这也是HOGNet特征提取器应用的下采样因子。
- 模型模板在HOG单元格数量中的大小可以作为模型中内核的大小(请注意,在这种情况下,内核是方形的,因此PyTorch只保存一维)。然后,像以前一样,通过按单元格大小进行多采样,以像素为单位进行转换。
- 这样,我们就可以推导出物体包围框的左上角和右下角的坐标,并绘制它。
二、带有支持向量机的多尺度学习
(1)将检测器扩展到多个尺度上的目标搜索,以及(2)使用支持向量机学习一个更好的模型。让我们从加载针对特定类别路标的数据子集开始。 运行以下代码以重新加载数据结构,只保留“强制”标记。“强制”等级就是所有强制性交通标志的结合。
imdb = lab.load_data(meta_class='mandatory')
2.1 多尺度检测
from PIL import Image
for t, s in enumerate([1.5, 1, .6]):
# Load a target image in PIL format.
image = Image.open('data/mandatory.jpg')
# Change the resolution of the image.
image = image.resize([int(s*x) for x in image.size], Image.LANCZOS)
# Run the detector as shown above.
model = nn.Conv2d(27, 1, w.shape[2:], bias=False)
model.weight.data = w
hog = hog_extractor(lab.pil_to_torch(image))
scores = model(hog)
boxes = lab.boxes_for_scores(model, scores[0])
boxes = boxes.reshape(4,-1).permute(1,0)
best, best_index = torch.max(scores[0].reshape(-1), 0)
box = boxes[best_index]
# Plot the results for this resolution.
plt.figure(t, figsize=(10,10))
plt.imshow(image)
lab.plot_box(box)
plt.title(f'Detected object (score: {best:.3f})') ;
- Question. Does the result look right? 尺度1的正确,由于尺度变化,却没有函数进行修正,导致框发生了变化。
因为对象存在于与学习模板不同大小的图像中。为了找到各种大小的物体,我们上下缩放图像,并一遍又一遍地搜索对象。
import numpy as np
# Scale space configuraiton
min_octave = -1
max_octave = 3
num_octave_subdivisions = 3
scales = 2**np.linspace(min_octave, max_octave, num_octave_subdivisions * (max_octave - min_octave + 1))
# Print the scale values
print('Scales: ', ', '.join([f"{x:.2f}" for x in scales]))
Scales: 0.50, 0.61, 0.74, 0.91, 1.10, 1.35, 1.64, 2.00, 2.44, 2.97, 3.62, 4.42, 5.38, 6.56, 8.00
lab代码提供了一个成员函数‘HOGNet.test_at_Multiple_Scales’,该函数使用这些标度实现多尺度检测。
以下代码将多尺度检测应用于上面测试的三个图像尺寸。
for t, s in enumerate([1.5, 1, .6]):
# Load a target image in PIL format.
image = Image.open('data/mandatory.jpg')
# Change the resolution of the image.
image = image.resize([int(s*x) for x in image.size], Image.LANCZOS)
# Get all boxes and scores
boxes, scores, _ = hog_extractor.detect_at_multiple_scales(w, scales, image)
# Pick the best box
best, best_index = torch.max(scores, 0)
box = boxes[best_index]
# Plot the results for this resolution.
plt.figure(t, figsize=(10,10))
plt.imshow(image)
lab.plot_box(box)
plt.title(f'Detected object (score: {best:.3f})') ;
- Question: Compare this result with the one obtained above. What is the difference?
首先对于尺度1.5,0.6的的得分明显增加,在检测框上也比之前改善明显。在添加了多尺度函数后,明显修正了之前的错误。
2.2 收集正面和负面训练数据
到目前为止学到的模型太弱,不能很好地工作。现在是时候使用支持向量机来学习更好的支持向量机了。为此,我们需要准备适当的数据。我们已经有了积极的例子(从对象补丁中提取的特性)。
为了收集负样本(从非对象块中提取的特征),我们对大量的训练图像和样本块进行了均匀循环。为了速度,我们只扫描一个子集的图像。
好菜不怕多,但是我们要先看下这好菜到底怎么样。所以先来5个。
Collect positive training data
pos = hog_train
# Collect negative training data
neg = []
num_training_images = 5
for image_path in imdb['train']['images'][:num_training_images]:
print(f"Processing image {image_path}")
image = Image.open(image_path)
hog = hog_extractor(lab.pil_to_torch(image))
patches = nn.Unfold(w.shape[2:], stride=5)(hog)
n = patches.shape[1]
patches = patches[:, :, :-1:n//100].permute(0,2,1)
patches = patches.reshape(patches.shape[1], *w.shape[1:])
neg.append(patches)
neg = torch.cat(neg, 0)
Processing image data/signs/00001.jpeg
Processing image data/signs/00002.jpeg
Processing image data/signs/00025.jpeg
Processing image data/signs/00028.jpeg
Processing image data/signs/00032.jpeg
2.3 支持向量机学习模型
现在我们有了数据,我们可以学习一个支持向量机模型。为此,我们将使用lab.svm_scda()函数。该函数要求数据位于𝐷×𝑁矩阵中,其中𝐷为特征维数,𝑁为训练点数。我们还需要一个二进制标签向量,+1表示正,-1表示负:
# Pack the data into a matrix with one datum per row
#0轴叠加,然后重整矩阵
x = torch.cat((pos, neg), 0).reshape(len(pos) + len(neg), -1)
a = torch.cat((pos,neg), 0)
# Obtain the corresponding label vector
y = torch.cat((torch.ones(len(pos)), -torch.ones(len(neg))), 0)
print(f"the shape of a is {list(a.shape)}")
print(f"The shape of x is {list(x.shape)}")
print(f"The shape of y is {list(y.shape)}")
the shape of a is [280, 27, 8, 8]
The shape of x is [280, 1728]
The shape of y is [280]
最后,需要设置支持向量机求解器的参数𝜆λ。出于稍后将变得更清楚的原因,我们使用等效的𝐶参数。那么,学习支持向量机就成了一条直线。
# Set the SVM parameter.
C = 10
lam = 1 / (C * (len(pos) + len(neg)))
# Run the SVM.
w, b = lab.svm_sdca(x, y, lam=lam)
w = w.reshape(1, *pos[0].shape)
# Plot the learned model
wim = hog_extractor.to_image(w)
plt.figure(figsize=(6, 6))
lab.imsc(wim[0]) ;
- Question: Visualize the learned model w using the supplied code. Does it differ from the naive model learned before? How?
强化了外在边界学习,但是弱化了内部学习。我的另外一种理解,泛化了内部学习特征
2.4 评估模型质量
for t, s in enumerate([1, .85, .5]):
# Load a target image in PIL format.
image = Image.open('data/mandatory.jpg')
# Change the resolution of the image.
image = image.resize([int(s*x) for x in image.size], Image.LANCZOS)
# Get all boxes and scores
boxes, scores, _ = hog_extractor.detect_at_multiple_scales(w, scales, image)
# Pick the best box
best, best_index = torch.max(scores, 0)
box = boxes[best_index]
# Plot the results.
plt.figure(t, figsize=(10,10))
plt.imshow(image)
lab.plot_box(box)
plt.title(f'Detected object (score: {best:.3f})') ;
三、多目标检测与评估
3.1检测多个目标
在多个尺度上进行检测是不够的:我们还必须允许在图像中出现多个对象。为了做到这一点,我们选择了非最大抑制(NMS).
算法简单:从得分最高的检测开始,然后删除重叠大于阈值的任何其他检测。用于将候选检测与真值包围框进行比较的重叠度量被定义为相交面积与两个边界框的合并面积之比:
overlap(𝐴,𝐵)=|𝐴∩𝐵| / |𝐴∪𝐵|.
- Question: After non-maxima suppression we still get a significant number of boxes. Why do we want to return so many responses? In practice, it is unlikely that more than a handful of object occurrences may be contained in any given image… 因为要实现多目标检测,而机器并不知道图中到底有几个目标。
# Load a target image in PIL format.
image = Image.open('data/signs-sample-image.jpg')
# Get all boxes and scores.
boxes, scores, _ = hog_extractor.detect_at_multiple_scales(w, scales, image)
# Get the top boxes.
boxes, scores, _ = lab.topn(boxes, scores, 100)
# Apply non-maxima suppression.
boxes, scores, _ = lab.nms(boxes, scores)
# Plot the results.
plt.figure(t, figsize=(10,10))
plt.imshow(image)
for box in boxes:
lab.plot_box(box)
plt.title(f'Showing {len(boxes)} boxes') ;
3.2 检测器评估
我们现在要好好评估我们的探测器。我们使用Pascal VOC准则,计算平均精度(AP)。假设一个测试图像包含了大量的真值(𝑔1,…𝑔𝑚)和一个列表(𝑏1,𝑠1),…,(𝑏𝑛,𝑠𝑛)的候选检测𝑏𝑖与分数𝑠𝑖。下面的算法将这些数据转换成一个标签和分数列表(𝑠𝑖,𝑦𝑖),这些标签和分数可以用来计算精确回忆曲线,例如使用lab.pr()函数。该算法由lab.valenectiontions()实现,如下所示:
前提条件:假设候选检测(𝑏𝑖,𝑠𝑖)是通过降低分数𝑠𝑖来排序的。
为每个候选检测(𝑏𝑖,𝑠𝑖)分配一个真假标签𝑦𝑖∈{+1,−1}。为此,对于每个候选检测顺序:如果存在匹配的真值检测𝑔𝑗(重叠(𝑏𝑖,𝑔𝑗)大于50%),则认为候选检测为阳性(𝑦𝑖=+1)。此外,真值检测被从名单中删除。否则,候选检测为阴性(𝑦𝑖=−1)。
将仍未分配给候选对象列表的每个基本真理对象𝑔𝑖添加为结对(𝑔𝑗,−∞),标签为𝑦𝑗=+1。
- Questions:
Why are ground truth detections removed after being matched? 防止对一个目标生成多个检测框
What happens if an object is detected twice? 会生成第二个框
Can you explain why unassigned ground-truth objects are added to the list of candidates with −∞ score? 可能是参加下次目标检测。
为了应用该算法,我们首先需要在测试图像中找到真值包围框:
# Pick the first validation image.
image = imdb['val']['images'][1]
pil_image = Image.open(image)
# Pick all the gt boxes in the selected image.
sel = [i for i, box_image in enumerate(imdb['val']['box_images']) if box_image == image]
gt_boxes = imdb['val']['boxes'][sel]
# Plot the images and its boxes.
plt.figure(1, figsize=(8,8))
plt.imshow(pil_image)
for box in gt_boxes:
lab.plot_box(box, color='y')
plt.title(f'Found {len(gt_boxes)} boxes in image {image}.') ;
# Run the detector
boxes, scores, _ = hog_extractor.detect_at_multiple_scales(w, scales, pil_image)
boxes, scores, _ = lab.topn(boxes, scores, 100)
boxes, scores, _ = lab.nms(boxes, scores)
print(f'boxes={boxes},scores={scores}')
# Evaluate the detector and plot the results.
plt.figure(1, figsize=(12,12))
plt.imshow(pil_image)
results = lab.eval_detections(gt_boxes, boxes, plot=True)
plt.title('yellow: gt, red: false detections, green: ture detections') ;
3.3多幅图像
# Evaluate the model on the first 20 validation images.
results = lab.evaluate_model(imdb, hog_extractor, w, scales=scales, subset=('val',0,20))
# PLot the resulting AP.
plt.figure(1, figsize=(5,5))
_, _, ap = lab.pr(results['labels'], results['scores'], misses=results['misses'])
plt.title(f'Average precision: {ap*100:.2f}%') ;