引言
目標(biāo)檢測是計(jì)算機(jī)視覺中一個(gè)非常流行的任務(wù),在這個(gè)任務(wù)中,給定一個(gè)圖像,你預(yù)測圖像中物體的包圍盒(通常是矩形的) ,并且識別物體的類型。在這個(gè)圖像中可能有多個(gè)對象,而且現(xiàn)在有各種先進(jìn)的技術(shù)和框架來解決這個(gè)問題,例如 Faster-RCNN 和 YOLOv3。
本文討論將討論圖像中只有一個(gè)感興趣的對象的情況。這里的重點(diǎn)更多是關(guān)于如何讀取圖像及其邊界框、調(diào)整大小和正確執(zhí)行增強(qiáng),而不是模型本身。目標(biāo)是很好地掌握對象檢測背后的基本思想,你可以對其進(jìn)行擴(kuò)展以更好地理解更復(fù)雜的技術(shù)。
問題陳述
給定一個(gè)由路標(biāo)組成的圖像,預(yù)測路標(biāo)周圍的包圍盒,并識別路標(biāo)的類型。這些路標(biāo)包括以下四種:
· 紅綠燈
· 停止
· 車速限制
· 人行橫道
這就是所謂的多任務(wù)學(xué)習(xí)問題,因?yàn)樗婕皥?zhí)行兩個(gè)任務(wù): 1)回歸找到包圍盒坐標(biāo),2)分類識別道路標(biāo)志的類型
數(shù)據(jù)集
它由877張圖像組成。這是一個(gè)相當(dāng)不平衡的數(shù)據(jù)集,大多數(shù)圖像屬于限速類,但由于我們更關(guān)注邊界框預(yù)測,因此可以忽略不平衡。
加載數(shù)據(jù)
每個(gè)圖像的注釋都存儲在單獨(dú)的 XML 文件中。我按照以下步驟創(chuàng)建了訓(xùn)練數(shù)據(jù)集:
· 遍歷訓(xùn)練目錄以獲得所有.xml 文件的列表。
· 使用xml.etree.ElementTree解析.xml文件。
· 創(chuàng)建一個(gè)由文件路徑、寬度、高度、邊界框坐標(biāo)( xmin 、 xmax 、 ymin 、 ymax )和每個(gè)圖像的類組成的字典,并將字典附加到列表中。
· 使用圖像統(tǒng)計(jì)數(shù)據(jù)字典列表創(chuàng)建一個(gè) Pandas 數(shù)據(jù)庫。
def filelist(root, file_type):
"""Returns a fully-qualified list of filenames under root directory"""
return [os.path.join(directory_path, f) for directory_path, directory_name,
files in os.walk(root) for f in files if f.endswith(file_type)]
def generate_train_df (anno_path):
annotations = filelist(anno_path, '.xml')
anno_list = []
for anno_path in annotations:
root = ET.parse(anno_path).getroot()
anno = {}
anno['filename'] = Path(str(images_path) + '/'+ root.find("./filename").text)
anno['width'] = root.find("./size/width").text
anno['height'] = root.find("./size/height").text
anno['class'] = root.find("./object/name").text
anno['xmin'] = int(root.find("./object/bndbox/xmin").text)
anno['ymin'] = int(root.find("./object/bndbox/ymin").text)
anno['xmax'] = int(root.find("./object/bndbox/xmax").text)
anno['ymax'] = int(root.find("./object/bndbox/ymax").text)
anno_list.append(anno)
return pd.DataFrame(anno_list)
· 標(biāo)簽編碼類列
#label encode target
class_dict = {'speedlimit': 0, 'stop': 1, 'crosswalk': 2, 'trafficlight': 3}
df_train['class'] = df_train['class'].apply(lambda x: class_dict[x])
調(diào)整圖像和邊界框的大小
由于訓(xùn)練一個(gè)計(jì)算機(jī)視覺模型需要的圖像是相同的大小,我們需要調(diào)整我們的圖像和他們相應(yīng)的包圍盒。調(diào)整圖像的大小很簡單,但是調(diào)整包圍盒的大小有點(diǎn)棘手,因?yàn)槊總(gè)包圍盒都與圖像及其尺寸相關(guān)。
下面是調(diào)整包圍盒大小的工作原理:
· 將邊界框轉(zhuǎn)換為與其對應(yīng)的圖像大小相同的圖像(稱為掩碼)。這個(gè)掩碼只有 0 表示背景,1 表示邊界框覆蓋的區(qū)域。
· 將掩碼調(diào)整到所需的尺寸。
· 從調(diào)整完大小的掩碼中提取邊界框坐標(biāo)。
def create_mask(bb, x):
"""Creates a mask for the bounding box of same shape as image"""
rows,cols,*_ = x.shape
Y = np.zeros((rows, cols))
bb = bb.astype(np.int)
Y[bb:bb, bb:bb] = 1.
return Y
def mask_to_bb(Y):
"""Convert mask Y to a bounding box, assumes 0 as background nonzero object"""
cols, rows = np.nonzero(Y)
if len(cols)==0:
return np.zeros(4, dtype=np.float32)
top_row = np.min(rows)
left_col = np.min(cols)
bottom_row = np.max(rows)
right_col = np.max(cols)
return np.array([left_col, top_row, right_col, bottom_row], dtype=np.float32)
def create_bb_array(x):
"""Generates bounding box array from a train_df row"""
return np.array([x,x,x,x])
def resize_image_bb(read_path,write_path,bb,sz):
"""Resize an image and its bounding box and write image to new path"""
im = read_image(read_path)
im_resized = cv2.resize(im, (int(1.49*sz), sz))
Y_resized = cv2.resize(create_mask(bb, im), (int(1.49*sz), sz))
new_path = str(write_path/read_path.parts[-1])
cv2.imwrite(new_path, cv2.cvtColor(im_resized, cv2.COLOR_RGB2BGR))
return new_path, mask_to_bb(Y_resized)
#Populating Training DF with new paths and bounding boxes
new_paths = []
new_bbs = []
train_path_resized = Path('./road_signs/images_resized')
for index, row in df_train.iterrows():
new_path,new_bb = resize_image_bb(row['filename'], train_path_resized, create_bb_array(row.values),300)
new_paths.append(new_path)
new_bbs.append(new_bb)
df_train['new_path'] = new_paths
df_train['new_bb'] = new_bbs