【影像處理】從VOC Dataset擷取需要的物件

作者:曾成訓(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.txtxml_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 部落格、原文連結;責任編輯:賴佩萱)

曾 成訓

Author: 曾 成訓

人到中年就像沒對準的描圖紙,一點一點的錯開,我只能當個Maker來使它復位。

Share This Post On

發表