作者:曾成訓(CH.Tseng)
有時我們的物件識別專案僅需要針對數種或單一種物件進行辨識,像是抓取道路中的汽車以便進行後續車牌或車型的辨識、取得影像中的犬貓以進行品種分類、框選影格中的人體,識別其關節特徵點來預測其動作等。
目前一些大型開源的深度學習圖庫如 Pascal VOC 及 COCO 等,皆提供種類繁多的物品標記圖片,其所訓練出的模型可辨識數十至上百種物品,因此我們可以利用這些開源圖庫來訓練辨識各種物件;但若我們只需要辨識貓與狗,卻需要辨識及過濾其它不相干的物品,不覺得太麻煩了嗎?是否能把該特定物品從這些開源圖庫取出,再加入其它或自己所搜集的圖片,單獨製作出專屬於該物品的 image dataset 呢?
Pascal VOC Dataset
Pascal VOC dataset 是由 PASCAL 組織所開源的影像圖庫,該組織全名相當冗長,為 Pattern Analysis, Statistical Modelling and Computational Learning Visual Object Classes,簡寫為 PASCAL,成立於 2003 年 12 月 1 日,是一個由歐盟所資助的研究機構。
PASCAL 的 VOC dataset 提供了物件識別模型最主要的兩種功能驗證:classification(分類)及detection(偵測),自 2005 年起每年舉辦一系列基於該 dataset 的 computer vision competition,識別範圍從基本的 Object Classification、Object Detection、Object Segmentation 到後來的 Human Layout、Action Classification,直到 2012 年的最後一屆為止,在此頁面還可看到歷屆的榜單。
VOC提供的物件類別
VOC dataset 的影像主要來自 flickr 與 Microsoft Research Cambridge(MSRC),包含 20 種不同的物件,區分為下列四種類別:
- Vehicles:Car、Bus、Bicycle、Motorbike、Boat、Train
- Household:Chair、Sofa、Dining table、TV/monitor、Bottle、Potted plant
- Animals:Cat、Dog、Cow、Horse、Sheep、Bird
- Person:Person

後方數字代表該類別加入的年份:1 為 2005、2 為 2006、3 為 2007(圖片來源:The PASCAL Visual Object Classes Challenge)
下方圖表統計不同物件所屬的圖片數目及標記數量,可以看到 Person 這個類別數量最多,其次為 Car。

不同物件所屬的圖片數目(圖片來源:The PASCAL Visual Object Classes Challenge)

不同物件所屬的標記數量(圖片來源:The PASCAL Visual Object Classes Challenge)
擷取感興趣的物件class
1. 請下載 VOC 2012 dataset(2 GB)
下載後的資料夾結構如下:
由於要使用於 Object detection,因此我們需要的是 Annotations 及 JPEGImages 這兩個資料夾,你會發現這兩個資料夾分別有 22,263 及 33,260 個檔案。
2. 下載 xml_file.txt 及 xml_object.txt 到工作目錄下
3. 參考下方的 pascalVOC_to_voc.py,修改一些參數:
pascalVOC_to_voc.py
# -*- coding: utf-8 -*- import cv2 from shutil import copyfile import os, time import os.path from xml.dom import minidom #------------------------------------------- labels_want = ["dog"] pose = ["rear", "left", "right", "frontal"] #use lower case, []--> all, ["rear", "left", "right", "frontal", "unspecified"] source_voc_path = "/DATA1/Datasets_download/Labeled/VOC/VOC_Dataset/2012/VOCdevkit/VOC2012/" source_images = "JPEGImages" source_labels = "Annotations" target_voc_path = "/DATA1/Datasets_mine/labeled/dog_voc" target_images = "images/" target_labels = "labels/" imgType = "jpg" xml_file = "xml_file.txt" object_xml_file = "xml_object.txt" def chkEnv(): if not os.path.exists(os.path.join(source_voc_path, source_images)): print("There is no source dataset in this path:", os.path.join(source_voc_path, source_images)) quit() if not os.path.exists(os.path.join(source_voc_path, source_labels)): print("There is no source dataset in this path:", os.path.join(source_voc_path, source_labels)) quit() if not os.path.exists(os.path.join(target_voc_path, target_images)): os.makedirs(os.path.join(target_voc_path, target_images)) print("Create the path:", os.path.join(target_voc_path, target_images)) if not os.path.exists(os.path.join(target_voc_path, target_labels)): os.makedirs(os.path.join(target_voc_path, target_labels)) print("Create the path:", os.path.join(target_voc_path, target_labels)) def getLabels(imgFile, xmlFile): labelXML = minidom.parse(xmlFile) labelName = [] labelXmin = [] labelYmin = [] labelXmax = [] labelYmax = [] totalW = 0 totalH = 0 countLabels = 0 #print(xmlFile) objects = labelXML.getElementsByTagName("object") for object in objects: pose_list = [] tmpArrays = object.getElementsByTagName("pose") for id, elem in enumerate(tmpArrays): pose_list.append(elem.firstChild.data) id_list = [] tmpArrays = object.getElementsByTagName("name") for id, elem in enumerate(tmpArrays): if(str(elem.firstChild.data) in labels_want): id_list.append(id) #print(xmlFile, id, pose_list, "TEST:", pose_list[id].lower()) if(len(pose_list)>id): if((pose_list[id].lower() in pose) and len(pose)>0): labelName.append(str(elem.firstChild.data) + "_" + pose_list[id]) else: labelName.append(str(elem.firstChild.data)) tmpArrays = object.getElementsByTagName("xmin") for id, elem in enumerate(tmpArrays): if(id in id_list): labelXmin.append(int(float(elem.firstChild.data))) tmpArrays = object.getElementsByTagName("ymin") for id, elem in enumerate(tmpArrays): if(id in id_list): labelYmin.append(int(float(elem.firstChild.data))) tmpArrays = object.getElementsByTagName("xmax") for id, elem in enumerate(tmpArrays): if(id in id_list): labelXmax.append(int(float(elem.firstChild.data))) tmpArrays = object.getElementsByTagName("ymax") for id, elem in enumerate(tmpArrays): if(id in id_list): labelYmax.append(int(float(elem.firstChild.data))) return labelName, labelXmin, labelYmin, labelXmax, labelYmax def writeObjects(label, bbox): with open(object_xml_file) as file: file_content = file.read() file_updated = file_content.replace("{NAME}", label) file_updated = file_updated.replace("{XMIN}", str(bbox[0])) file_updated = file_updated.replace("{YMIN}", str(bbox[1])) file_updated = file_updated.replace("{XMAX}", str(bbox[0] + bbox[2])) file_updated = file_updated.replace("{YMAX}", str(bbox[1] + bbox[3])) return file_updated def generateXML(imgfile, filename, fullpath, bboxes, imgfilename): xmlObject = "" for (labelName, bbox) in bboxes: #for bbox in bbox_array: xmlObject = xmlObject + writeObjects(labelName, bbox) with open(xml_file) as file: xmlfile = file.read() img = cv2.imread(imgfile) #print(os.path.join(datasetPath, imgPath, imgfilename)) cv2.imwrite(os.path.join(target_voc_path, target_images, imgfilename), img) (h, w, ch) = img.shape xmlfile = xmlfile.replace( "{WIDTH}", str(w) ) xmlfile = xmlfile.replace( "{HEIGHT}", str(h) ) xmlfile = xmlfile.replace( "{FILENAME}", filename ) xmlfile = xmlfile.replace( "{PATH}", fullpath + filename ) xmlfile = xmlfile.replace( "{OBJECTS}", xmlObject ) return xmlfile def makeLabelFile(filename, bboxes, imgfile): jpgFilename = filename + "." + imgType xmlFilename = filename + ".xml" #cv2.imwrite(os.path.join(datasetPath, imgPath, jpgFilename), img) xmlContent = generateXML(imgfile, xmlFilename, os.path.join(target_voc_path, target_labels, xmlFilename), bboxes, jpgFilename) file = open(os.path.join(target_voc_path, target_labels, xmlFilename), "w") file.write(xmlContent) file.close #-------------------------------------------- if __name__ == "__main__": chkEnv() i = 0 imageFolder = os.path.join(source_voc_path, source_images) for file in os.listdir(imageFolder): filename, file_extension = os.path.splitext(file) file_extension = file_extension.lower() if(file_extension == ".jpg" or file_extension==".jpeg" or file_extension==".png" or file_extension==".bmp"): #print("Processing: ", imageFolder + "/" + file) xml_path = os.path.join(source_voc_path, source_labels, filename+".xml") if os.path.exists(xml_path): image_path = os.path.join(imageFolder, file) labelName, labelXmin, labelYmin, labelXmax, labelYmax = getLabels(image_path, xml_path) img_bboxes = [] print(labelName, labelXmin, labelYmin, labelXmax, labelYmax) for i, label_want in enumerate(labelName): x = int(float(labelXmin[i])) y = int(float(labelYmin[i])) w = int(float(labelXmax[i]))-int(float(labelXmin[i])) h = int(float(labelYmax[i]))-int(float(labelYmin[i])) img_bboxes.append( (label_want, [x,y,w,h]) ) if(len(img_bboxes)>0): print(img_bboxes) makeLabelFile(filename, img_bboxes, image_path)
#想要取出的標記名稱,可多個。
labels_want = [“dog”, “cat”]
#例:”Labeled/VOC/VOC_Dataset/2007/VOCdevkit/VOC2007″
source_voc_path = “{VOC2012的path}”
#image圖檔的目錄名稱,例:JPEGImages
source_images = “{圖片檔path}”
#image標記檔的目錄名稱,例:Annotations
source_labels = “{標記檔path}”
#新建的dataset路徑,例:voc_coco_cars/
target_voc_path = “{新建dataset path}”
#新建dataset的圖檔目錄名稱,例:images/
target_images = “{圖檔目錄名稱}”
#新建dataset的標記檔目錄名稱,例:labels/
target_labels = “{標記檔目錄名稱}”
4. 執行 python pascalVOC_to_voc.py
下方我們以指定取出類別為「person」作為例子,在 pascalVOC_to_voc.py 程式執行結束後,便可見到所有類別為 person 的圖片及標記檔,皆已被取出並放置於指定的位置。

(圖片來源:曾成訓提供)

(圖片來源:曾成訓提供)
接著使用 LabelImg 工具檢視,確認每張圖片的標記皆有正確地被擷取,因此透過本程式,我們從 VOC dataset 取得了所有標記為 Person 的 image 及標記檔,數量有 14,721 張。

(圖片來源:曾成訓提供)

(圖片來源:曾成訓提供)
Pascal VOC Dataset 總共提供了 20 種類別的物件,除了範例的 Person,其它類別用如上的方式也能取得,只要修改 labels_want 參數的內容即可。
取得人體的部位標記
VOC 2011 起增加了所謂的「person layout」,也就是在標記檔中新增 head、hand、foot 三種人體標記(或稱 2-D Pose Annotation),透過 head、hand、foot 的位置,我們便能識別該人物目前的動作姿態。
這三種人體 annotation 位於標記檔的 part tag 底下,下方為圖檔 2011_000943.jpg,右側為透過其標記檔 2011_000943.xml 中的局部內容 ,可發現 tag 定義了 head、hand、foot 的位置區域。

2011_000943.jpg(圖片來源:曾成訓提供)
<part> <name>head</name> <bndbox> <xmin>153</xmin> <ymin>161</ymin> <xmax>192</xmax> <ymax>214</ymax> </bndbox> </part> <part> <name>hand</name> <bndbox> <xmin>79</xmin> <ymin>247</ymin> <xmax>102</xmax> <ymax>271</ymax> </bndbox> </part> <part> <name>hand</name> <bndbox> <xmin>230</xmin> <ymin>305</ymin> <xmax>264</xmax> <ymax>329</ymax> </bndbox> </part> <part> <name>foot</name> <bndbox> <xmin>195</xmin> <ymin>412</ymin> <xmax>221</xmax> <ymax>440</ymax> </bndbox> </part> <part> <name>foot</name> <bndbox> <xmin>89</xmin> <ymin>392</ymin> <xmax>126</xmax> <ymax>402</ymax> </bndbox> </part>
若要取得 VOC dataset 中的人體標記,請執行下方的 pascalVOC_to_peopleParts.py,所有的 2-D Pose Annotation 便會匯出在指定的路徑中。
pascalVOC_to_peopleParts.py
# -*- coding: utf-8 -*- import cv2 from shutil import copyfile import os, time import os.path from xml.dom import minidom #------------------------------------------- parts_want = ["head", "hand", "foot"] source_voc_path = "/DATA1/Datasets_download/Labeled/VOC/VOC_Dataset/2012/VOCdevkit/VOC2012/" source_images = "JPEGImages" source_labels = "Annotations" target_voc_path = "/DATA1/Datasets_mine/labeled/voc_people_parts" target_images = "images/" target_labels = "labels/" imgType = "jpg" xml_file = "xml_file.txt" object_xml_file = "xml_object.txt" def chkEnv(): if not os.path.exists(os.path.join(source_voc_path, source_images)): print("There is no source dataset in this path:", os.path.join(source_voc_path, source_images)) quit() if not os.path.exists(os.path.join(source_voc_path, source_labels)): print("There is no source dataset in this path:", os.path.join(source_voc_path, source_labels)) quit() if not os.path.exists(os.path.join(target_voc_path, target_images)): os.makedirs(os.path.join(target_voc_path, target_images)) print("Create the path:", os.path.join(target_voc_path, target_images)) if not os.path.exists(os.path.join(target_voc_path, target_labels)): os.makedirs(os.path.join(target_voc_path, target_labels)) print("Create the path:", os.path.join(target_voc_path, target_labels)) def getLabels(imgFile, xmlFile): labelXML = minidom.parse(xmlFile) labelName = [] labelXmin = [] labelYmin = [] labelXmax = [] labelYmax = [] totalW = 0 totalH = 0 countLabels = 0 #print(xmlFile) objects = labelXML.getElementsByTagName("part") for object in objects: id_list = [] tmpArrays = object.getElementsByTagName("name") for id, elem in enumerate(tmpArrays): if(str(elem.firstChild.data) in parts_want): labelName.append(str(elem.firstChild.data)) id_list.append(id) tmpArrays = object.getElementsByTagName("xmin") for id, elem in enumerate(tmpArrays): if(id in id_list): labelXmin.append(int(float(elem.firstChild.data))) tmpArrays = object.getElementsByTagName("ymin") for id, elem in enumerate(tmpArrays): if(id in id_list): labelYmin.append(int(float(elem.firstChild.data))) tmpArrays = object.getElementsByTagName("xmax") for id, elem in enumerate(tmpArrays): if(id in id_list): labelXmax.append(int(float(elem.firstChild.data))) tmpArrays = object.getElementsByTagName("ymax") for id, elem in enumerate(tmpArrays): if(id in id_list): labelYmax.append(int(float(elem.firstChild.data))) return labelName, labelXmin, labelYmin, labelXmax, labelYmax def writeObjects(label, bbox): with open(object_xml_file) as file: file_content = file.read() file_updated = file_content.replace("{NAME}", label) file_updated = file_updated.replace("{XMIN}", str(bbox[0])) file_updated = file_updated.replace("{YMIN}", str(bbox[1])) file_updated = file_updated.replace("{XMAX}", str(bbox[0] + bbox[2])) file_updated = file_updated.replace("{YMAX}", str(bbox[1] + bbox[3])) return file_updated def generateXML(imgfile, filename, fullpath, bboxes, imgfilename): xmlObject = "" for (labelName, bbox) in bboxes: #for bbox in bbox_array: xmlObject = xmlObject + writeObjects(labelName, bbox) with open(xml_file) as file: xmlfile = file.read() img = cv2.imread(imgfile) #print(os.path.join(datasetPath, imgPath, imgfilename)) cv2.imwrite(os.path.join(target_voc_path, target_images, imgfilename), img) (h, w, ch) = img.shape xmlfile = xmlfile.replace( "{WIDTH}", str(w) ) xmlfile = xmlfile.replace( "{HEIGHT}", str(h) ) xmlfile = xmlfile.replace( "{FILENAME}", filename ) xmlfile = xmlfile.replace( "{PATH}", fullpath + filename ) xmlfile = xmlfile.replace( "{OBJECTS}", xmlObject ) return xmlfile def makeLabelFile(filename, bboxes, imgfile): jpgFilename = filename + "." + imgType xmlFilename = filename + ".xml" #cv2.imwrite(os.path.join(datasetPath, imgPath, jpgFilename), img) xmlContent = generateXML(imgfile, xmlFilename, os.path.join(target_voc_path, target_labels, xmlFilename), bboxes, jpgFilename) file = open(os.path.join(target_voc_path, target_labels, xmlFilename), "w") file.write(xmlContent) file.close #-------------------------------------------- if __name__ == "__main__": chkEnv() i = 0 imageFolder = os.path.join(source_voc_path, source_images) for file in os.listdir(imageFolder): filename, file_extension = os.path.splitext(file) file_extension = file_extension.lower() if(file_extension == ".jpg" or file_extension==".jpeg" or file_extension==".png" or file_extension==".bmp"): #print("Processing: ", imageFolder + "/" + file) xml_path = os.path.join(source_voc_path, source_labels, filename+".xml") if os.path.exists(xml_path): image_path = os.path.join(imageFolder, file) labelName, labelXmin, labelYmin, labelXmax, labelYmax = getLabels(image_path, xml_path) img_bboxes = [] for i, label_want in enumerate(labelName): x = int(float(labelXmin[i])) y = int(float(labelYmin[i])) w = int(float(labelXmax[i]))-int(float(labelXmin[i])) h = int(float(labelYmax[i]))-int(float(labelYmin[i])) img_bboxes.append( (label_want, [x,y,w,h]) ) if(len(img_bboxes)>0): print(img_bboxes) makeLabelFile(filename, img_bboxes, image_path)
執行後,總共擷取了 664 張圖片及標記檔,再次使用 labelImg 工具驗證,會發現每張圖片中的人物都標記了三種人體部位,但若圖片中有很多人,VOC 並沒有每個人物皆標記,如下圖,最右側的人可能由於部份身體被遮住,因此略過不予標記。

(圖片來源:曾成訓提供)
但又有些圖片中的人物很適合標記卻也省略沒標,因此人物是否需要標記身體部位看來是隨機的,如下圖:

(圖片來源:曾成訓提供)
透過取得 head、hand、foot 三種身體標記,我們可以識別影像中的人物動作,例如舉手、站立、躺或坐、搖擺或倒立等,不過可惜的一點,就是 VOC 並不是針對圖片中所有的人物進行人體部位標記,除非我們自己將圖片中未標記的部份補足,否則這仍然屬於未完成標記的 dataset,無法直接進行訓練。
取得pose標記
在 VOC dataset 中,有的 Object 標記會有 pose 這個 tag(有的沒有),它有五種值:Rear、Left、Right、Frontal、Unspecified,代表了該物體正面的方向,例如下方這張圖片中的四個人,有三個朝向右方,一個朝向左方(以第一人稱觀點),因此在這張圖片的標記檔中,會有 pose 這個 tag 來定義這四個值。

(圖片來源:曾成訓提供)
一樣使用上方 pascalVOC_to_voc.py 的程式,注意一下其中的參數 pose,把想要抓取的 pose 填入即可(若 pose=[] 空值則代表全部抓取),VOC 共定義了 rear、left、right、frontal、unspecified 等五種 pose 值。
透過 pose 標記,我們便能訓練識別物品的方向或方位。下面我們執行程式抓取所有為 dog 標記的圖檔,並設定 pose = [“rear”, “left”, “right”, “frontal”],則會取得 835 張圖片及標記檔,例如下圖有兩個 labels,其 class 名稱在擷取後取名為 dog_Frontal。

(圖片來源:曾成訓提供)
下面這張的兩個 label 分別為 dog_Right 以及 dog_Left。

(圖片來源:曾成訓提供)
此外,交通工具的識別也能搭配方向資訊作出各種應用。只要設定擷取 car、bus、motorbike 這三種標記,並定義 pose = [“rear”, “left”, “right”, “frontal”]、補足未標記的部份,就能擁有一個可用以訓練辨識交通工具及其行進方向的 dataset 了:

(圖片來源:曾成訓提供)

(圖片來源:曾成訓提供)
(本文經作者同意轉載自 CH.TSENG 部落格、原文連結;責任編輯:賴佩萱)
- 【模型訓練】訓練馬賽克消除器 - 2020/04/27
- 【AI模型訓練】真假分不清!訓練假臉產生器 - 2020/04/13
- 【AI防疫DIY】臉部辨識+口罩偵測+紅外線測溫 - 2020/03/23
訂閱MakerPRO知識充電報
與40000位開發者一同掌握科技創新的技術資訊!