首發(fā)于個人博客
理論分析
YOLO從v2版本開始重新啟用anchor box,YOLOv2網(wǎng)絡(luò)的網(wǎng)絡(luò)輸出為尺寸為[b,125,13,13]
的tensor,要將這個Tensor變?yōu)樽罱K的輸出結(jié)果,還需要以下的處理:
- 解碼:從Tensor中解析出所有框的位置信息和類別信息
- NMS:篩選最能表現(xiàn)物品的識別框
解碼過程
解碼之前渣慕,需要明確的是每個候選框需要5+class_num個數(shù)據(jù)竿奏,分別是相對位置x,y,相對寬度w,h鹏往,置信度c和class_num個分類結(jié)果,YOLOv2-voc中class_num=20,即每個格點對應(yīng)5個候選框伊履,每個候選框有5+20=25個參數(shù)韩容,這就是為什么輸出Tensor的最后一維為5*(20+5)=125。
上圖為一個框所需要的所有數(shù)據(jù)構(gòu)成唐瀑,假設(shè)這個框是位于格點X,Y的群凶,對應(yīng)的anchor box大小為W,H,位置相關(guān)參數(shù)的處理方法如下所示哄辣,其中请梢,分別是輸出Tensor在長寬上的值,這里
力穗;
分別為原圖片的長和寬:
置信度和類別信息處理方法如下所示:
當(dāng)格點置信度大于某個閾值時毅弧,認(rèn)為該格點有物體,物體類別為class_id對應(yīng)的類別
NMS
NMS為非最大值抑制当窗,用在YOLO系統(tǒng)中的含義指從多個候選框標(biāo)記同一個物品時够坐,從中選擇最合適的候選框。其基本思維很簡單:使用置信度最高的候選框標(biāo)記一個物體崖面,若其他候選框與該候選框的IOU超過一個閾值元咙,則認(rèn)為其他候選框與該候選框標(biāo)記的是同一個物體,丟棄其他候選框巫员。
具體實現(xiàn)時庶香,可以將所有候選框進(jìn)行排序,置信度高的在前简识,置信度低的在后赶掖。從置信度高的候選框開始遍歷所有候選框,對于某一個候選框七扰,將之后所有的候選框與其計算IOU奢赂,若IOU高于一個閾值,則丟棄置信度低的候選框戳寸。算法流程圖如下所示:
代碼分析
這里選擇的是marvis開源的基于Pytorch的YOLOv2代碼呈驶,其優(yōu)勢在于所有的部分均使用Python實現(xiàn),沒有使用Cython疫鹊,無需編譯即可使用袖瞻,且依賴較少,文件管理比較扁平拆吆。
解碼部分
解碼部分在utils.py
文件中聋迎,由get_region_boxes
函數(shù)實現(xiàn)。首先是準(zhǔn)備部分枣耀,這里首先獲取了輸出的相關(guān)信息霉晕,yolo-voc網(wǎng)絡(luò)下有b為batch,預(yù)測模式下一般為1,h=w=13牺堰。隨后reshape了輸出拄轻,其維度變?yōu)?25,13*13*5),改變維度的目的是方便后面處理的索引伟葫。
def get_region_boxes(output, conf_thresh, num_classes, anchors, num_anchors, only_objectness=1, validation=False):
anchor_step = len(anchors) / num_anchors
if output.dim() == 3:
output = output.unsqueeze(0)
batch = output.size(0)
assert(output.size(1) == (5 + num_classes) * num_anchors)
h = output.size(2)
w = output.size(3)
output = output.view(batch * num_anchors, 5 + num_classes, h * w).transpose(
0, 1).contiguous().view(5 + num_classes, batch * num_anchors * h * w)
all_boxes = []
隨后是處理x恨搓,y的部分,xs和ys就是處理后的候選框中心點相對坐標(biāo)筏养,grid_x和grid_y與output[0]shape相同斧抱,分別表示對應(yīng)output位置的候選框所屬的格點坐標(biāo)X與Y,這里的xs和ys實現(xiàn)了上述公式中的和
grid_x = torch.linspace(0, w - 1, w).repeat(h, 1).repeat(batch *
num_anchors, 1, 1).view(batch * num_anchors * h * w).cuda()
grid_y = torch.linspace(0, h - 1, h).repeat(w, 1).t().repeat(
batch * num_anchors, 1, 1).view(batch * num_anchors * h * w).cuda()
print("outputs shape", output.shape)
xs = torch.sigmoid(output[0]) + grid_x
ys = torch.sigmoid(output[1]) + grid_y
之后為處理w,h的部分渐溶,與處理x,y的部分類似辉浦,最終ws和hs為修正后的物品尺寸信息,實現(xiàn)了和
茎辐。其中W和H分別為當(dāng)前anchor box的建議尺寸宪郊。
anchor_w = torch.Tensor(anchors).view(
num_anchors, anchor_step).index_select(1, torch.LongTensor([0]))
anchor_h = torch.Tensor(anchors).view(
num_anchors, anchor_step).index_select(1, torch.LongTensor([1]))
anchor_w = anchor_w.repeat(batch, 1).repeat(
1, 1, h * w).view(batch * num_anchors * h * w).cuda()
anchor_h = anchor_h.repeat(batch, 1).repeat(
1, 1, h * w).view(batch * num_anchors * h * w).cuda()
ws = torch.exp(output[2]) * anchor_w
hs = torch.exp(output[3]) * anchor_h
接下來是獲取置信度的部分和類別部分,獲取該anchor box的置信度為det_confs=sigmoid(c)
荔茬。隨后處理類別信息废膘,先對類別信息對應(yīng)的數(shù)據(jù)做softmax操作竹海,隨后獲取其最大值cls_max_confs
和最大值所在的位置cls_max_ids
慕蔚,其中位置cls_max_ids
對應(yīng)每個anchor box框住的“物品”的類別。
det_confs = torch.sigmoid(output[4])
cls_confs = torch.nn.Softmax()(
Variable(output[5:5 + num_classes].transpose(0, 1))).data
cls_max_confs, cls_max_ids = torch.max(cls_confs, 1)
cls_max_confs = cls_max_confs.view(-1)
cls_max_ids = cls_max_ids.view(-1)
隨后是一些其他的處理過程斋配,例如獲取格點數(shù)量sz_hw
孔飒,anchor box的數(shù)量sz_hwa
等,函數(shù)convert2cpu
是在CPU上復(fù)制一個該數(shù)據(jù)艰争,注意這里是拷貝坏瞄,并不是將數(shù)據(jù)從GPU轉(zhuǎn)移到CPU上。
sz_hw = h * w
sz_hwa = sz_hw * num_anchors
det_confs = convert2cpu(det_confs)
cls_max_confs = convert2cpu(cls_max_confs)
cls_max_ids = convert2cpu_long(cls_max_ids)
xs = convert2cpu(xs)
ys = convert2cpu(ys)
ws = convert2cpu(ws)
hs = convert2cpu(hs)
if validation:
cls_confs = convert2cpu(cls_confs.view(-1, num_classes))
隨后是一個解碼的大循環(huán)甩卓,分析見下面的注釋
for b in range(batch):
boxes = []
# boxes為容納所有候選框的list
for cy in range(h):
for cx in range(w):
for i in range(num_anchors):
# 遍歷每一個anchor box鸠匀,這里訪問位于格點cx,cy的第i個anchor box
ind = b * sz_hwa + i * sz_hw + cy * w + cx
# 獲取該anchor box在det_conf中對應(yīng)的index
det_conf = det_confs[ind]
if only_objectness:
conf = det_confs[ind]
else:
conf = det_confs[ind] * cls_max_confs[ind]
# 處理置信度
if conf > conf_thresh:
# 若置信度大于閾值,則認(rèn)為該anchor box有效
bcx = xs[ind]
bcy = ys[ind]
bw = ws[ind]
bh = hs[ind]
cls_max_conf = cls_max_confs[ind]
cls_max_id = cls_max_ids[ind]
# 獲取所有相關(guān)信息逾柿,包括長缀棍,寬猜极,位置照卦,置信度和類別
box = [bcx / w, bcy / h, bw / w, bh / h,
det_conf, cls_max_conf, cls_max_id]
# 處理數(shù)據(jù),其中位置信息x,y,尺寸信息w,h均歸一化椅邓,使其與輸入圖片尺寸解耦
if (not only_objectness) and validation:
for c in range(num_classes):
tmp_conf = cls_confs[ind][c]
if c != cls_max_id and det_confs[ind] * tmp_conf > conf_thresh:
box.append(tmp_conf)
box.append(c)
boxes.append(box)
# 將處理好的anchor box信息保存在boxes中
all_boxes.append(boxes)
return all_boxes
NMS部分
NMS也在utils.py
中弱匪,函數(shù)名為nms
青瀑。該函數(shù)中,首先實現(xiàn)對所有候選框的排序。這里使用det_confs
獲取了置信度從大到小的anchor box的坐標(biāo)位置sortIds
斥难。
def nms(boxes, nms_thresh):
if len(boxes) == 0:
return boxes
det_confs = torch.zeros(len(boxes))
for i in range(len(boxes)):
det_confs[i] = 1 - boxes[i][4]
_, sortIds = torch.sort(det_confs)
隨后實現(xiàn)候選框的篩選枝嘶,從高置信度的候選框開始遍歷,對于每個候選框boxes[sortIds[i]]
哑诊,遍歷所有置信度低于該候選框且置信度不為0(置信度為0表示該候選框被拋棄)的候選框躬络,若低置信度候選框與高置信度候選框的IOU大于閾值,則拋棄低置信度候選框搭儒。
out_boxes = []
for i in range(len(boxes)):
# 按置信度從高到低遍歷
box_i = boxes[sortIds[i]]
if box_i[4] > 0:
# 置信度大于0表示該候選框沒有在之前的篩選中被拋棄
out_boxes.append(box_i)
for j in range(i + 1, len(boxes)):
# 遍歷所有置信度低于該候選框的候選框
box_j = boxes[sortIds[j]]
if bbox_iou(box_i, box_j, x1y1x2y2=False) > nms_thresh:
# 若置信度低的候選框與該候選框IOU大于一定值穷当,拋棄低置信度候選框
box_j[4] = 0
return out_boxes