兩個名詞:目標的真實邊界(ground_truth bounding box)。而以像素為中心生成多個大小和寬高比(aspect ratio)的邊界框,稱為anchor box。
基於深度學習的目標檢測不使用傳統的滑窗生成所有的窗口作為候選區域,FasterRCNN提出的RPN網絡,處理較少但准確的候選區域。錨框即可理解為某個檢測點處的候選區域。
假設圖像高\(h\),寬為\(w\)。以每個像素為中心,大小\(s \in (0,1]\),寬高比\(r \gt 0\)。那么錨框的寬為\(ws\sqrt{r}\),高為\(\frac{hs}{\sqrt{r}}\)。(設錨框寬高為\(x\)和\(y\)。面積\(xy=s^2\),比例\(x/y=r\),計算出來\(x=s*\sqrt{r}~~,y=\frac{s}{\sqrt{r}}\),這里的計算出的都是歸一化后的寬度和高度是相對原圖,因此各自乘以原圖像寬度和高度。)。
設定一組大小和寬高比,\(s_1,...s_n\)和\(r_1,....r_m\)。顯然有\(mn\)種組合,圖像有\(wh\)個像素點,那么一共有\(whmn\)個錨框。數量太多,只關注含有\(r1\)和\(s1\)的框。即組合是\((s_1,r_k)\)和\((s_k,r_1)\),一共\(n+m-1\)個框。
程序實現
一幅圖像生成的anchor存放在一個數組里面,尺寸是(寬,高,anchor個數,4),弄成這種尺寸是便於可視化,如果知道某個像素點的所有anchor坐標,直接索引訪問即可。
def MultiBoxPrior(featuremap, sizes=[0.75,0.5,0.25], ratios=[1,2,0.5]):
# 生成anchor表示為 xmin ymin xmax ymax
pairs = []
# 組合配對m+n-1個錨框 后續使用根號r較多,因此這里直接開根號了
for r in ratios:
pairs.append([sizes[0], math.sqrt(r)])
for s in sizes[1:]:
pairs.append([s, math.sqrt(ratios[0])])
# pairs是每一組anchor的設置
# 對於(s,r)的錨框,對應的寬高是ws*sqrt(r)和hs/sqrt(r)
# 返回的錨框變量y的形狀(寬,高,以相同像素為中心的框個數,4) y[100,100,0,:]即為(100,100)像素位置第一個錨框坐標
pairs = np.array(pairs)
# 錨框的相對寬高 s*根號r 和 s/根號r
x = pairs[:, 0] * pairs[:, 1] # s*根號r
y = pairs[:, 0] / pairs[:, 1] # s/根號r
# 以像素點為中心,兩邊的偏移量
# 上面的x y 是anchor的寬高,下面計算的是兩邊的偏移,因此除以了2
# base_anchors 中心點(0,0) 錨框的對角坐標,相對偏移
# 如果中心像素坐標(i,j) 那么點應該是(i-x,j-y,i+x,j+y)
# base_anchors每一行就是一種錨框相對中心的偏移量 -x -y +x +y
base_anchors = np.stack([-x, -y, x, y], axis=1) / 2
#print(base_anchors)
h,w = featuremap.shape[-2:] # 特征圖的寬高,特征圖尺寸NCHW
# anchor_boxes尺寸如下 每一個點要有len(pairs)個錨框
anchor_boxs = np.zeros(shape=(h,w,len(pairs),4))
for i in range(h):
for j in range(w):
# 這一步是復制中心點坐標,生成多行。。便於后面與base_anchors相加
xx = [i / w ]*len(pairs)
yy = [j / h]*len(pairs)
# 中心點的坐標 就是(i,j) 歸一化到[0,1]
center_ = np.stack([xx, yy, xx, yy],axis=1)
a = center_ + base_anchors
anchor_boxs[i,j,:,:] = a
return anchor_boxs
生成anchor的代碼如上,原理是遍歷訪問每一個像素點,將像素坐標與anchor的相對偏移坐標加起來就行。for循環效率有點低,但很好理解。對每一組(s,r)計算base_anchor,即相對中心點的偏移,如圖所示。那么對於每一個像素點,它的anchor,直接就是像素坐標加上對應偏移即可

這樣計算出來的anchor是相對坐標。還原到原圖的像素坐標,只需要乘以寬或高即可。
實際上可以用numpy操作,https://zhuanlan.zhihu.com/p/115362157 這個文章里的實現更簡潔。
顯示某個像素點處的anchor如下。
def show_box(img,bbox,x,y):
# 這里為了方便。。img是cv2讀取的圖片
boxes = bbox[x,y,:,:] # 獲取像素坐標(x,y)處的anchor
h, w = img.shape[0], img.shape[1]
for box in boxes:
# 左上的坐標
top_x = int(box[0]*w)
top_y = int(box[1]*h )
# 右下的坐標
bottom_x = int(box[2]*w)
bottom_y = int(box[3]*h)
# 防止越界處理一下。。
top_x = 0 if top_x < 0 else top_x
top_y = 0 if top_y <0 else top_y
bottom_x = w if bottom_x > w else bottom_x
bottom_y = h if bottom_y > h else bottom_y
cv2.rectangle(img,(top_x,top_y),(bottom_x,bottom_y),(255,0,0),2)
return img
最后,測試一張圖片,繪制某點的anchor。
tmp = cv2.imread("222.jpg")
# 綠色框標注一下要測試的那個像素點(155,120)處
cv2.rectangle(tmp,(150,120),(155,125),(0,255,0),3)
# MultiBoxPrior輸入的是NCHW格式。這里只有一張圖,是HWC,交換一下。
bbox = MultiBoxPrior(tmp.transpose(2,0,1))
print(bbox.shape)
maps = show_box(tmp,bbox,150,120)
plt.imshow(maps)
(
)
