赞
踩
目标检测的主要任务有两步:1. 确定目标位置;2. 对目标位置的目标进行分类;前者属于定位问题,后者属于分类问题;
分类问题在前面的学习有了合理的解决办法,目前主要是定位问题怎么处理,这也是目标检测的重点;
首先,目标定位一般使用矩形来作为边界框,边界框的表示方式有两种:一种是使用图像中左上角和右下角两点确定边界框,一种是根据矩形特定位置点以及宽和高确定边界框;plt.Rectangle
采取的方式是左上角以及宽和高;
# 两点式转化为中心式 def box_2p_to_center(box_2p): left, up, right, dowm = box_2p[:, 0], box_2p[:, 1], box_2p[:, 2], box_2p[:, 3] x = (left + right) / 2 y = (up + dowm) / 2 width = right - left height = dowm - up return np.c_[x, y, width , height] # 中心式转化为两点式 def box_center_to_2p(box_center): x, y, width, height = box_center[:,0], box_center[:,1], box_center[:,2], box_center[:,3] left = x - width/2 right = x + width/2 up = y - height/2 dowm = y + height/2 return np.c_[left, up, right, dowm]
这里以这张猫狗图片为例子,可以直接保存测试;
使用 plt.Rectangle
在图片上标记
import numpy as np import matplotlib.pyplot as plt def bbox_to_rect(bbox, color): return plt.Rectangle( xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1], fill=False, edgecolor=color, linewidth=2) img = plt.imread('./cat-dog.jpg') dog_bbox, cat_bbox,bak_bbox = [20.0, 5.0, 440.0, 480.0], [445.0, 86.0, 730.0, 490.0],[390.0, 17.0, 600.0, 71.0] fig = plt.imshow(img) fig.axes.add_patch(bbox_to_rect(dog_bbox, 'yellow')) fig.axes.text(dog_bbox[0]+5,dog_bbox[1]+20,'dog',style='italic',bbox={'facecolor': 'white', 'alpha': 0.5, 'pad': 0}) fig.axes.add_patch(bbox_to_rect(bak_bbox, 'red')) fig.axes.text(cat_bbox[0]+5,cat_bbox[1]+20,'cat',style='italic',bbox={'facecolor': 'white', 'alpha': 0.5, 'pad': 0}) fig.axes.add_patch(bbox_to_rect(cat_bbox, 'blue')) fig.axes.text(bak_bbox[0]+5,bak_bbox[1]+20,'background',style='italic',bbox={'facecolor': 'white', 'alpha': 0.5, 'pad': 0}) plt.axis('off') plt.savefig('cat-dog_.jpg')
得到如下结果:
在实际操作中,我们可以使用 LabelImg
快速标记出目标位置并给出目标分类制作数据;LabelImg
的链接如下: labelImg
选择性搜索(Selective Search, SS)方法是通过图像中的纹理,边缘,颜色等信息对图像进行自底向上的分割,然后对分割区域进行不同尺度的合并,在合并过程中,每生成一个新的区域就产生一个候选框,区域肯定是不规则,我们通过选取区域的最大外接矩阵作为候选框区域,这种方法速度较慢;
在这篇博客中尝试使用了颜色分割合并:[Initial Image Segmentation Generator]论文实现:Efficient Graph-Based Image Segmentation;该方法的一个重要特点是,它能够在低变异性图像区域保留细节,而忽略了高变异性图像区域的细节。也就是说在变化不大的区域可以对细节进行保留,在变化很大的区域对细节进行剔除,这个效果根据后面合并小区域得到的。
在这里我们可以利用 OpenCV
中 createSelectiveSearchSegmentation()
函数来获取候选框区域;该函数除了安装基本的 OpenCV
外,还需要安装拓展,代码: pip install opencv-contrib-python --user
import cv2 as cv
def get_ss_rects(img):
cv.setUseOptimized(True)
ss = cv.ximgproc.segmentation.createSelectiveSearchSegmentation()
ss.setBaseImage(img)
ss.switchToSelectiveSearchFast()
rects = ss.process()
return rects
测试一下SS方法:
import tensorflow as tf import cv2 as cv import matplotlib.pyplot as plt def get_ss_rects(img): cv.setUseOptimized(True) ss = cv.ximgproc.segmentation.createSelectiveSearchSegmentation() ss.setBaseImage(img) ss.switchToSelectiveSearchFast() rects = ss.process() return rects im = cv.imread('./cat-dog.jpg') rects = get_ss_rects(im) imOut = im.copy() for i, rect in enumerate(rects): x, y, w, h = rect cv.rectangle(imOut, (x,y), (x+w,y+h), (0,255,0), 1, cv.LINE_AA) plt.imshow(imOut[:,:,::-1]) plt.axis('off') plt.show()
得到结果如图所示:
得到处理时间为:CPU times: total: 2.08 s Wall time: 2.36 s
锚框和锚点
首先要介绍的是锚框和锚点,其定义是根据原图和特征图来说明的,首先原图(origin map)采取池化的方式缩小尺寸变成特征图(feature map);这样导致特征图某一点与原图某一块相对应,特征图的那一点被称为锚点;其对应的那一块被称为锚框,锚框的中心点也可以看做是锚点;
将锚框按照比例(如 1:2, 1:1, 2:1)进行变换,然后再依次进行宽和高的缩放和放大(如8, 16, 32)就可以得到9个框;
其中池化缩小的倍数被称为 base_size
,变换比例被称为 ratios
,对宽和高放大的倍数被称为 scales
;
使用代码获取特征图上第一个点的锚框实现如下:
def generate_anchors(base_size=16, ratios=[0.5, 1, 2], scales=2**np.arange(3, 6)): """ Generate anchor (reference) windows by enumerating aspect ratios X scales wrt a reference (0, 0, 15, 15) window. """ base_anchor = np.array([1, 1, base_size, base_size]) - 1 ratio_anchors = _ratio_enum(base_anchor, ratios) anchors = np.vstack([_scale_enum(ratio_anchors[i, :], scales) for i in range(ratio_anchors.shape[0])]) return anchors def _whctrs(anchor): """ Return width, height, x center, and y center for an anchor (window). """ w = anchor[2] - anchor[0] + 1 h = anchor[3] - anchor[1] + 1 x_ctr = anchor[0] + 0.5 * (w - 1) y_ctr = anchor[1] + 0.5 * (h - 1) return w, h, x_ctr, y_ctr def _mkanchors(ws, hs, x_ctr, y_ctr): """ Given a vector of widths (ws) and heights (hs) around a center (x_ctr, y_ctr), output a set of anchors (windows). """ ws = ws[:, np.newaxis] hs = hs[:, np.newaxis] anchors = np.hstack((x_ctr - 0.5 * (ws - 1), y_ctr - 0.5 * (hs - 1), x_ctr + 0.5 * (ws - 1), y_ctr + 0.5 * (hs - 1))) return anchors def _ratio_enum(anchor, ratios): """ Enumerate a set of anchors for each aspect ratio wrt an anchor. """ w, h, x_ctr, y_ctr = _whctrs(anchor) size = w * h size_ratios = size / ratios ws = np.round(np.sqrt(size_ratios)) hs = np.round(ws * ratios) anchors = _mkanchors(ws, hs, x_ctr, y_ctr) return anchors def _scale_enum(anchor, scales): """ Enumerate a set of anchors for each scale wrt an anchor. """ w, h, x_ctr, y_ctr = _whctrs(anchor) ws = w * scales hs = h * scales anchors = _mkanchors(ws, hs, x_ctr, y_ctr) return anchors
获取特征图上所有点的锚框如下:
def get_anchors(height, width, base_size=16, ratios=[0.5, 1, 2], scales=2**np.arange(3, 6)): anchor_rect = [] base_anchors = generate_anchors(base_size, ratios, scales) for i in range(width): for j in range(height): for item in base_anchors: x1, y1, x2, y2 = item x1, x2 = x1 + base_size*i, x2 + base_size*i y1, y2 = y1 + base_size*j, y2 + base_size*j item = [x1, y1, x2, y2] anchor_rect.append(item) anchor_rect = np.array(anchor_rect) return anchor_rect # 画出锚框 def bbox_to_rect(bbox, color): return plt.Rectangle( xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1], fill=False, edgecolor=color, linewidth=0.5) # 一步到位 def plot_anchors(anchors): fig = plt.figure(figsize=(10, 10)) # 获取范围,方便限制坐标轴 a, b = np.min(anchors, axis=0), np.max(anchors, axis=0) plt.scatter([a[0], b[2]], [a[1], b[3]], c='white') ax = plt.gca() for anchor in anchors: ax.add_patch(bbox_to_rect(anchor, 'red')) plt.axis('off') plt.show() # 获取 40,40 特征图上的锚框 anchors = get_anchors(40,40) plot_anchors(anchors)
得到结果:
子任务1:对锚框进行二分类
二分类任务主要是判断锚框是背景还是前景,现有数据只有大量的锚框以及目标(Ground Truth, GT)框,我们需要根据目标框对锚框进行分类,这里我们可以采用 IOU
计算方式:
I
O
U
=
A
∩
B
A
∪
B
IOU=\frac{A \cap B}{ A \cup B}
IOU=A∪BA∩B
多对一的代码实现如下:
# numpy 实现 def compute_iou(boxes, box): # 计算交集 xy_max = np.minimum(boxes[:, 2:], box[2:]) xy_min = np.maximum(boxes[:, :2], box[:2]) inter = np.clip(xy_max-xy_min, a_min=0, a_max=np.inf) inter = inter[:, 0]*inter[:, 1] # 计算面积 area_boxes = (boxes[:, 2]-boxes[:, 0])*(boxes[:, 3]-boxes[:, 1]) area_box = (box[2]-box[0])*(box[3]-box[1]) return inter/(area_box+area_boxes-inter) # tensorflow 实现 def compute_iou(boxes, box): # 计算交集 boxes, box = tf.cast(boxes, dtype=tf.float32), tf.cast(box, dtype=tf.float32) xy_max = tf.minimum(boxes[:, 2:], box[2:]) xy_min = tf.maximum(boxes[:, :2], box[:2]) inter = tf.clip_by_value(xy_max - xy_min, clip_value_min=0., clip_value_max=tf.int32.max) inter = inter[:, 0]*inter[:, 1] # 计算面积 area_boxes = (boxes[:, 2]-boxes[:, 0])*(boxes[:, 3]-boxes[:, 1]) area_box = (box[2]-box[0])*(box[3]-box[1]) return inter/(area_box+area_boxes-inter)
定义损失,这里随机抽取了128个正例,128个反例,因此每一个 batch_size 中有 batch_size * 256
个例子;
其中正例我采用的方法是从 IOU 得分最高的64个中加权有放回抽取得到;反例则是在 IOU 小于 0.3 的集合中随机抽取得到;
def compute_cls(y_true, y_pred): """y_pred 是一个 [batch_size, 50, 38, 9] 的四维空间""" y_pred = tf.reshape(y_pred, [tf.shape(y_pred)[0], -1]) anchors = get_anchors(50,38) indexs_list = [] labels_list = [] all_indices = tf.TensorArray(tf.int32, size=0, dynamic_size=True) all_labels = tf.TensorArray(tf.int32, size=0, dynamic_size=True) for ix in tf.range(tf.shape(y_true)[0]): iou = compute_iou(anchors, y_true[ix]) # Sorting and selecting indices for sampling. Note: tf.argsort and tf.gather are used instead of np.argsort and np.random.choice sorted_indices = tf.argsort(iou, direction='DESCENDING') top_k_indices = sorted_indices[:64] positive_sampled_indices = tf.gather(top_k_indices, tf.random.categorical(tf.math.log(tf.expand_dims(weights, axis=0)), 128, dtype=tf.int32))[0] # Negative sampling. tf.where and tf.random.shuffle are used here. neg_mask = tf.cast(iou < 0.3, tf.int32) neg_indices = tf.where(neg_mask) neg_sampled_indices = tf.cast(tf.random.shuffle(neg_indices)[:128], dtype=tf.int32) # Combining indices and labels positive_indexs = tf.concat([tf.fill([128, 1], ix), tf.expand_dims(positive_sampled_indices, axis=1)], axis=1) negative_indexs = tf.concat([tf.fill([128, 1], ix), tf.expand_dims(neg_sampled_indices[:, 0], axis=1)], axis=1) # Creating labels tensor labels = tf.concat([tf.ones_like(positive_sampled_indices), tf.zeros_like(neg_sampled_indices[:, 0])], axis=0) # Gathering indices and concatenating lists all_indices = all_indices.write(all_indices.size(), tf.concat([positive_indexs, negative_indexs], axis=0)) all_labels = all_labels.write(all_labels.size(), labels) final_indexs = tf.reshape(all_indices.stack(), [-1, 2]) final_labels = tf.reshape(all_labels.stack(), [-1]) return tf.keras.losses.binary_crossentropy(final_labels, tf.gather_nd(y_pred, final_indexs))
子任务2:对锚框进行边框回归
边框回归(Bounding Box Regression,BBR),由于锚框的尺寸和尺度都是固定的,现实情况中基本不可能出现目标尺寸和尺度与锚框相同,因此我们需要调整一下锚框;因此:Bounding-box regression 是用来对算法提取的预测框Region Proposal进行微调,使其更加接近于物体的真实标注框Ground Truth。
从红色变成绿色,比较简单是思路就是 先平移后放缩
的方式,实现步骤如下:
P
a
n
:
G
x
′
=
A
w
⋅
d
x
(
A
)
+
A
x
G
y
′
=
A
h
⋅
d
y
(
A
)
+
A
y
Z
o
o
m
:
G
w
′
=
A
w
⋅
e
x
p
(
d
w
(
A
)
)
G
h
′
=
A
h
⋅
e
x
p
(
d
h
(
A
)
)
其中 A x , A y , A w , A h A_x, A_y, A_w, A_h Ax,Ay,Aw,Ah 表示红色框的中心点,宽和高; G x , G y , G w , G h G_x, G_y, G_w, G_h Gx,Gy,Gw,Gh 表示绿色框的中心点,宽和高; G x ′ , G y ′ , G w ′ , G h ′ G_x', G_y', G_w', G_h' Gx′,Gy′,Gw′,Gh′表示预测的中心点,宽和高; d x ( A ) , d y ( A ) , d w ( A ) , d h ( A ) d_x(A),d_y(A),d_w(A),d_h(A) dx(A),dy(A),dw(A),dh(A)表示学习的平移和放缩参数;
其损失可以利用
d
x
(
A
)
,
d
y
(
A
)
,
d
w
(
A
)
,
d
h
(
A
)
d_x(A),d_y(A),d_w(A),d_h(A)
dx(A),dy(A),dw(A),dh(A) 以及 中间变量
t
x
,
t
y
,
t
w
,
t
h
t_x,t_y,t_w,t_h
tx,ty,tw,th 来表示:
Z
o
o
m
:
t
w
=
log
G
w
A
w
t
h
=
log
G
h
A
h
P
a
n
:
t
x
=
G
x
−
A
x
A
w
t
y
=
G
y
−
A
y
A
h
d x ( A ) , d y ( A ) , d w ( A ) , d h ( A ) d_x(A),d_y(A),d_w(A),d_h(A) dx(A),dy(A),dw(A),dh(A)是由锚点所在的channel向量 ϕ ( A ) \phi(A) ϕ(A)经过线性变换得到,但由于使用了线性变换,锚点向量的表达能力不强,最主要的就是指数对数这种非线性变换难以学习和拟合,而指数和对数缩放大小是正数,不能删除;以 t w t_w tw 为例子证明如下:
t w = log ( G w A w ) = log ( A w + G w − A w A w ) = log ( 1 + G w − A w A w ) = G w − A w A w t_w = \log(\frac{G_w}{A_w})=\log(\frac{A_w+G_w-A_w}{A_w})=\log(1+\frac{G_w-A_w}{A_w})=\frac{G_w-A_w}{A_w} tw=log(AwGw)=log(AwAw+Gw−Aw)=log(1+AwGw−Aw)=AwGw−Aw
只有在
G
w
−
A
w
G_w-A_w
Gw−Aw 接近于 0 的情况下,可以消去对数,因此可以使用非线性变换的方式优化该算法,这也是 YOLOv2
做的优化;为了使
G
w
−
A
w
G_w-A_w
Gw−Aw接近于0,这里要求 IOU
必须要大于0.6
损失可以得到:
L
=
∑
∣
t
−
W
ϕ
(
A
)
∣
+
λ
∣
∣
W
∣
∣
1
\mathcal{L} = \sum|t-W\phi(A)| + \lambda||W||_1
L=∑∣t−Wϕ(A)∣+λ∣∣W∣∣1
实现损失代码如下:
def compute_reg(y_true, y_pred): """y_pred 是一个 [batch_size, 50, 38, 9, 4] 的五维空间""" y_pred = tf.reshape(y_pred, [tf.shape(y_pred)[0], -1, 4]) # Reshape to handle flattened predictions anchors = get_anchors(50, 38) # Ensure this function exists and returns expected anchors all_da = tf.TensorArray(tf.float32, size=0, dynamic_size=True) all_t = tf.TensorArray(tf.float32, size=0, dynamic_size=True) for ix in tf.range(tf.shape(y_pred)[0]): iou = compute_iou(anchors, y_true[ix]) indexs = tf.reshape(tf.where(iou > 0.6), [-1]) da = tf.gather(y_pred[ix], tf.cast(indexs, tf.int32)) # Gather based on indices g = tf.gather(y_true, [ix]) # Gather ground truths a = tf.gather(anchors, tf.cast(indexs, tf.int32)) # Gather anchors g = tf.cast(g, tf.float32) a = tf.cast(a, tf.float32) # Calculate t_x, t_y, t_w, t_h (assuming g and a are in the correct format) t_w = tf.math.log((g[:, 2] - g[:, 0]) / (a[:, 2] - a[:, 0])) t_h = tf.math.log((g[:, 3] - g[:, 1]) / (a[:, 3] - a[:, 1])) t_x = ((g[:, 0] + g[:, 2]) / 2 - (a[:, 0] + a[:, 2]) / 2) / (a[:, 2] - a[:, 0]) t_y = ((g[:, 1] + g[:, 3]) / 2 - (a[:, 1] + a[:, 3]) / 2) / (a[:, 3] - a[:, 1]) t = tf.stack([t_x, t_y, t_w, t_h], axis=1) all_da = all_da.write(ix, da) all_t = all_t.write(ix, t) # Stack and reshape for loss computation da = tf.reshape(all_da.stack(), [-1, 4]) t = tf.reshape(all_t.stack(), [-1, 4]) return tf.reduce_mean(tf.abs(da - t))
使用 tf.while_loop
计算如下:
def compute_reg(y_true, y_pred): """y_pred 是一个 [batch_size, 50, 38, 9, 4] 的五维空间""" y_pred = tf.reshape(y_pred, [tf.shape(y_pred)[0], -1, 4]) # Reshape to handle flattened predictions anchors = get_anchors(50, 38) # Ensure this function exists and returns expected anchors ix = tf.constant(0, dtype=tf.int32) n = tf.cast(tf.shape(y_true)[0], dtype=tf.int32) total_loss = tf.constant(0.0, dtype=tf.float32) def cond(ix, total_loss): return ix < n def body(ix, total_loss): iou = compute_iou(anchors, y_true[ix]) indexs = tf.reshape(tf.where(iou > 0.6), [-1]) da = tf.gather(y_pred[ix], tf.cast(indexs, tf.int32)) # Gather based on indices g = tf.gather(y_true, [ix]) # Gather ground truths a = tf.gather(anchors, tf.cast(indexs, tf.int32)) # Gather anchors g = tf.cast(g, tf.float32) a = tf.cast(a, tf.float32) # Calculate t_x, t_y, t_w, t_h (assuming g and a are in the correct format) t_w = tf.math.log((g[:, 2] - g[:, 0]) / (a[:, 2] - a[:, 0])) t_h = tf.math.log((g[:, 3] - g[:, 1]) / (a[:, 3] - a[:, 1])) t_x = ((g[:, 0] + g[:, 2]) / 2 - (a[:, 0] + a[:, 2]) / 2) / (a[:, 2] - a[:, 0]) t_y = ((g[:, 1] + g[:, 3]) / 2 - (a[:, 1] + a[:, 3]) / 2) / (a[:, 3] - a[:, 1]) t = tf.stack([t_x, t_y, t_w, t_h], axis=1) # Compute loss for the current sample and accumulate it sample_loss = tf.reduce_mean(tf.abs(da - t)) total_loss += sample_loss return tf.add(ix, 1), total_loss # Increment index and return updated loss accumulation _, final_loss = tf.while_loop(cond, body, [ix, total_loss]) return final_loss
使用SS或者RPN生成推荐区域后,会出现许多的框实际上是指向同一目标,因此会存在大量的沉余框,需要剔除;如图所示:
SS由于信息量过少,可能只能使用 IOU
进行解决,而RPN由于在 softmax
阶段产生了概率,所以RPN可以使用非极大值抑制(Non-Maximum Suppression,NMS),其思想是搜索局部极大值,抑制非极大值元素,其流程如下:
假设有六个候选框,根据分类器类别分类概率做排序,从小到大排列为:ABCDEF
1. 从最大概率矩形框F开始,分别判断A~E与F的重叠度IOU是否大于某个设定的阈值;
2. 假设B、D与F的重叠度超过阈值,那么就扔掉B、D;并标记第一个矩形框F,是我们保留下来第一个框;
3. 从剩下的矩形框A、C、E中,选择概率最大的E,然后判断E与A、C的重叠度,重叠度大于一定 的阈值,那么就扔掉;并标记E是我们保留下来的第二个矩形框;
4. 一直重复这个过程,找到所有曾经被保留下来的矩形框;
这只是一个循环过程,很好实现:
def nms(boxes, scores, iou_threshold): """boxes 是一个 [-1, 4], scores 是一个 [-1] """ boxes, scores = tf.cast(boxes, tf.float32), tf.cast(scores, tf.float32) nms_indices = tf.TensorArray(tf.int32, size=0, dynamic_size=True) def cond(boxes, scores, nms_indices): return tf.reduce_any(tf.not_equal(scores, 0)) def body(boxes, scores, nms_indices): idx = tf.argsort(scores, direction='DESCENDING') scores = tf.gather(scores, idx) boxes = tf.gather(boxes, idx) current_box = tf.gather(boxes, idx[0]) nms_indices = nms_indices.write(nms_indices.size(), idx[0]) ious = compute_iou(boxes, current_box) mask = tf.math.less(ious, iou_threshold) scores = tf.cast(mask, tf.float32) * scores return boxes, scores, nms_indices _, _, nms_indices = tf.while_loop(cond, body, [boxes, scores, nms_indices]) final_indices = nms_indices.stack() final_boxes = tf.gather(boxes, final_indices) return final_boxes
可以得到如下结果:
由于神经网络操作需要输入的尺寸一样才能进行一些常规的算法操作,而框定的图像由于边框回归,锚框比例和大小的作用下,出现图像大小不一致的情况,这里我们需要把图像大小固定为某一尺寸;
这里有两种办法:
金字塔映射和普通池化固定大小不同,金字塔映射固定的是处理后的尺寸,操作是动态的;普通池化固定的是操作,处理后的尺寸是动态的;SPP-Net 对候选框采取了多个尺寸(5x5,3x3,1x1)的金字塔映射,然后将金字塔展平进行全连接分类;
一般来说,在固定图像大小之前还可以进行一次边框回归增加准确性;
这部分过于简单,主要原则在最后一层的激活层:单目标使用sigmoid,多目标使用softmax;不要忘了 无目标
Faster RCNN原理篇(一)——Bounding-Box Regression边界框回归的学习和理解_边界框为什么要归一化-CSDN博客
Faster RCNN原理篇(二)——RoIPooling和RoIAlign的学习和理解_roipooling 归一化-CSDN博客
Faster RCNN原理篇(三)——区域候选网络RPN(Region Proposal Network)的学习、理解-CSDN博客
一文读懂Faster RCNN - 知乎 (zhihu.com)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。