1、資料集下載
(1)wider_face 資料集網址為 http://shuoyang1213.me/WIDERFACE/index.html
下載以上幾項檔案(這裡推薦 google Drive 百度雲在沒有會員的情況下,下載太慢)
(2)將檔案解壓到各自獨立的資料夾
2、資料集簡介
WIDER FACE 資料集是一個人臉檢測基準(benchmark)資料集,圖片選取自 WIDER(Web Image Dataset for Event Recognition) 資料集。圖片數 32,203 張,人臉數 393,703 個,在大小(scale)、位置(pose)、遮擋(occlusion)等不同形式中,人臉是高度變換的。WIDER FACE 資料集是基於61個事件類別,每個事件類別,隨機選取訓練40%、驗證10%、測試50%。訓練和測試含有邊框(bounding box)真值(ground truth),而驗證不含。
這裡主要使用訓練集和驗證集,他們對應的標籤檔案分別為 wider_face_split/wider_face_train_bbx_gt.txt 和 wider_face_split/wider_face_val_bbx_gt.txt
在 wider_face_train_bbx_gt.txt 檔案中
資料如下所示:
0--Parade/0_Parade_marchingband_1_849.jpg
1
449 330 122 149 0 0 0 0 0 0
第一行代表圖片路徑
第二行是圖片中目標個數(人臉個數)
第三行是具體的圖片中人臉標註的相關引數(具體含義可以在 readme.txt 中看到)
從左到右的含義分別是 x1, y1, w, h, blur, expression, illumination, invalid, occlusion, pose
(1)x1, y1, w, h, 分別代表 左下點座標 及寬長
(2)blur:模糊程度,0——> 清晰 ,1——> 一般模糊 , 2——> 嚴重模糊
(3)expression: 表情 0——> 正常 , 1——> 誇張
(4)illumination:光源(應該是曝光程度)0——> 正常 , 1——>極度
(5)occlusion:遮擋 0——> 沒有遮擋 , 1——> 部分遮擋 , 2——> 嚴重遮擋
(6)pose: 姿勢 0——> 正常姿勢 , 1——非正常姿勢
(7)invalid: 無效圖片 0——否, 1——> 是
3、資料集轉換
YOLO v3 需要的 標籤格式為
0 0.498046875 0.292057761732852 0.119140625 0.1075812274368231 #type x y w h
從左到右的含義分別為 目標型別 (這裡只有一種型別,所以都是0 ) 目標框中心點的(x,y)座標 目標框的寬度和高度 (這裡的資料都是單位資料 即 x—— 中心點實際x / 圖片寬度 , y—— 中心點實際y / 圖片高度)
這裡可以直接把 wider_face 標籤轉成 yolo 標籤,也可以先轉成 voc 格式標籤再轉成 yolo 標籤。考慮到官方有將VOC 格式轉成 yolo 格式的程式碼 voc_label.py 於是先轉成 VOC 格式 的標註
(1)轉成VOC 格式
# -*- coding: utf-8 -*- import shutil import random import os import string from skimage import io headstr = """\ <annotation> <folder>VOC2007</folder> <filename>%06d.jpg</filename> <source> <database>My Database</database> <annotation>PASCAL VOC2007</annotation> <image>flickr</image> <flickrid>NULL</flickrid> </source> <owner> <flickrid>NULL</flickrid> <name>company</name> </owner> <size> <width>%d</width> <height>%d</height> <depth>%d</depth> </size> <segmented>0</segmented> """ objstr = """\ <object> <name>%s</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>%d</xmin> <ymin>%d</ymin> <xmax>%d</xmax> <ymax>%d</ymax> </bndbox> </object> """ tailstr = '''\ </annotation> ''' def writexml(idx, head, bbxes, tail): filename = ("Annotations/%06d.xml" % (idx)) f = open(filename, "w") f.write(head) for bbx in bbxes: f.write(objstr % ('face', bbx[0], bbx[1], bbx[0] + bbx[2], bbx[1] + bbx[3])) f.write(tail) f.close() def clear_dir(): if shutil.os.path.exists(('Annotations')): shutil.rmtree(('Annotations')) if shutil.os.path.exists(('ImageSets')): shutil.rmtree(('ImageSets')) if shutil.os.path.exists(('JPEGImages')): shutil.rmtree(('JPEGImages')) shutil.os.mkdir(('Annotations')) shutil.os.makedirs(('ImageSets/Main')) shutil.os.mkdir(('JPEGImages')) def excute_datasets(idx, datatype): f = open(('ImageSets/Main/' + datatype + '.txt'), 'a') f_bbx = open(('wider_face_split/wider_face_' + datatype + '_bbx_gt.txt'), 'r') while True: filename = f_bbx.readline().strip('\n') if not filename: break im = io.imread(('WIDER_' + datatype + '/images/' + filename)) head = headstr % (idx, im.shape[1], im.shape[0], im.shape[2]) nums = f_bbx.readline().strip('\n') bbxes = [] if nums=='0': bbx_info= f_bbx.readline() continue for ind in range(int(nums)): bbx_info = f_bbx.readline().strip(' \n').split(' ') bbx = [int(bbx_info[i]) for i in range(len(bbx_info))] # x1, y1, w, h, blur, expression, illumination, invalid, occlusion, pose if bbx[7] == 0: bbxes.append(bbx) writexml(idx, head, bbxes, tailstr) shutil.copyfile(('WIDER_' + datatype + '/images/' + filename), ('JPEGImages/%06d.jpg' % (idx))) f.write('%06d\n' % (idx)) idx += 1 f.close() f_bbx.close() return idx if __name__ == '__main__': clear_dir() idx = 1 idx = excute_datasets(idx, 'train') idx = excute_datasets(idx, 'val') print('Complete...')
目錄格式為
(2)VOC 格式轉成 yolo 需要的格式
將 上述步驟生成的 三個資料夾 即 Annotations ImageSets JPEGImages 放到之前編譯好的 \darknet-master\build\darknet\x64\data\voc\VOCdevkit\VOCface 目錄中
將voc_label.py 放入 \darknet-master\build\darknet\x64\data\voc\ 目錄下
開啟 voc_label.py 檔案
將7 、8左右的程式碼改成如下所示:
# sets=[('2012', 'train'), ('2012', 'val'), ('2007', 'train'), ('2007', 'val'), ('2007_test', 'test')]# sets=[('face', 'train'), ('face', 'val')]# # # classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"] classes = ["face"]
import xml.etree.ElementTree as ET import pickle import os from os import listdir, getcwd from os.path import join # sets=[('2012', 'train'), ('2012', 'val'), ('2007', 'train'), ('2007', 'val'), ('2007_test', 'test')]# sets=[('face', 'train'), ('face', 'val')]# # # classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"] classes = ["face"] def convert(size, box): dw = 1./size[0] dh = 1./size[1] x = (box[0] + box[1])/2.0 y = (box[2] + box[3])/2.0 w = box[1] - box[0] h = box[3] - box[2] x = x*dw w = w*dw y = y*dh h = h*dh return (x,y,w,h) def convert_annotation(year, image_id): in_file = open('VOCdevkit/VOC%s/Annotations/%s.xml'%(year, image_id)) out_file = open('VOCdevkit/VOC%s/labels/%s.txt'%(year, image_id), 'w') tree=ET.parse(in_file) root = tree.getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) for obj in root.iter('object'): difficult = obj.find('difficult').text cls = obj.find('name').text if cls not in classes or int(difficult) == 1: continue cls_id = classes.index(cls) xmlbox = obj.find('bndbox') b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text), float(xmlbox.find('ymax').text)) bb = convert((w,h), b) out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n') if __name__=='__main__': wd = getcwd() for year, image_set in sets: if not os.path.exists('VOCdevkit/VOC%s/labels/' % (year)): os.makedirs('VOCdevkit/VOC%s/labels/' % (year)) image_ids = open('VOCdevkit/VOC%s/ImageSets/Main/%s.txt' % (year, image_set)).read().strip().split() list_file = open('%s_%s.txt' % (year, image_set), 'w') for image_id in image_ids: list_file.write('%s/VOCdevkit/VOC%s/JPEGImages/%s.jpg\n' % (wd, year, image_id)) convert_annotation(year, image_id) list_file.close()
執行 voc_label.py 結束後 將會在 voc 目錄下生成 face_train.txt 和 face_val.txt
至此前期資料準備工作完成。
4、修改配置檔案
(1)配置 cfg 檔案
將 darknet-master\build\darknet\x64\cfg\yolov3.cfg 檔案 複製一份 並重新命名為 yolov3-obj.cfg
開啟 yolov3-obj.cfg 將 第三行第四行註釋掉 將第七行和第八行註釋取消
將 batch 設為 batch=64 (第6行)
將 subdivisions 設為 subdivisions=8 (第7行)
如果顯示卡記憶體較小(即後面執行時報 out of memory 的錯時) 可以 將 batch 改成 32 16 8 等 (保證 batch 是 subdivisions 的整數倍),同時取消多尺度訓練 即 設定 random = 0 ( 第 615、701、788 行 )
將 max_batches 改為 max_batches = 2000 (第20行)max_batches 的數量為檢測的目標數 * 2000
將 steps 改為 steps=1600,1800 (第22行)steps =max_batches *0.8 ,0.9
將 classes 改為 classes =1 (第 610 、696、783 行)
將 filters 改為 filters =18 (只改三個 yolo 層的上一層的 filters 即 第 603、689 、776 行 )
(2)配置 obj.data 和 obj.names 檔案
可以 複製 voc.data 和obj.names 檔案並重新命名,也可以自己新建兩個檔案
obj.data 檔案中 內容為
classes= 1
train = data/voc/face_train.txt
valid = data/voc/face_val.txt
#difficult = data/difficult_2007_test.txt
names = data/obj.names
backup = backup/
obj.names 的內容為 face (只有這一行)
face
(3)配置 \darknet-master\Makefile 檔案 (在有 GPU 和 CUDNN 的情況下)
將第 1 行 GPU=0 改成 GPU=1
將第 2 行 CUDNN=0 改成 CUDNN=1
將第 58 行 改為 NVCC=C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v10.0/bin (自己的cuda 安裝目錄)
將 88 —— 108 行的內容改成如下所示 (即 將對應目錄 改成)
ifeq ($(GPU), 1)
COMMON+= -DGPU -I/C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v10.0/include
CFLAGS+= -DGPU
ifeq ($(OS),Darwin) #MAC
LDFLAGS+= -L/usr/local/cuda/lib -lcuda -lcudart -lcublas -lcurand
else
LDFLAGS+= -L/C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v10.0/lib/x64 -lcuda -lcudart -lcublas -lcurand
endif
endif
ifeq ($(CUDNN), 1)
COMMON+= -DCUDNN
ifeq ($(OS),Darwin) #MAC
CFLAGS+= -DCUDNN -I/usr/local/cuda/include
LDFLAGS+= -L/usr/local/cuda/lib -lcudnn
else
CFLAGS+= -DCUDNN -IC:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v10.0/include
LDFLAGS+= -L/C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v10.0/lib/x64 -lcudnn
endif
endif
(4)下載 預訓練檔案 https://pjreddie.com/media/files/darknet53.conv.74 放到 \darknet-master\build\darknet\x64 目錄中
5、開始訓練
在 \darknet-master\build\darknet\x64 目錄下開啟 powershell
執行命令 ./darknet.exe detector train data/obj.data cfg/yolov3-obj.cfg darknet53.conv.74 開始訓練
如果報 CUDA Error: out of memory
則 將 batch 改成 32 16 8 等 (保證 batch 是 subdivisions 的整數倍),同時取消多尺度訓練 即 設定 random = 0 ( 第 615、701、788 行 ) (我是都改成 8 才可以)
6、訓練過程中的 輸出引數解釋
表示所有訓練圖片中的一個批次(batch),批次大小的劃分根據在cfg/yolov3-obj.cfg中設定的, 批次大小的劃分根據我們在 .cfg 檔案中設定的subdivisions引數。在我使用的 .cfg 檔案中 batch = 8 ,subdivision = 8,所以在訓練輸出中,訓練迭代包含了8組(8組 Region 82, Region 94, Region 106),每組又包含了1張圖片,跟設定的batch和subdivision的值一致。( 也就是說每輪迭代會從所有訓練集裡隨機抽取 batch = 8 個樣本參與訓練,所有這些 batch 個樣本又被均分為 subdivision = 8 次送入網路參與訓練,以減輕記憶體佔用的壓力)
(1) Region 82 ,Region 94 , Region 106 代表三個 訓練尺度 82 為最大尺度 用來預測較小目標, 106 為最小尺度 用來預測較大目標,94 為 中間尺度 在每個尺度 中的資料 會出現大量的 nan 資料 是正常現象,只有迭代的 avg loss 出現 nan 值才說明訓練出錯。
(2)Avg IOU:表示在當前subdivision內的圖片的平均IOU,代表預測的矩形框和真實目標的交併比 越接近1 越好
(3)Class:標註物體分類的正確率, 期望該值趨近於1;
(4)Obj:越接近 1 越好
(5)No Obj:越來越小,但不為 0
(6).5R:以IOU=0.5為閾值時候的recall; recall = 檢出的正樣本/實際的正樣本
(7).75R: 以IOU=0.75為閾值時候的recall; recall = 檢出的正樣本/實際的正樣本
(8)count:count後的值表示所有的當前subdivision圖片(本例中一張)中包含正樣本的圖片的數量。
(9)最後一行
11:指當前訓練的迭代次數
640.579651:總體的 loss
647.46337 avg loss :平均的loss 在這個數字到達 0.05-3 之間 可以停止訓練(當該數字 變化趨於平穩,波動不大時停止 )
0.00000 rate: 代表當前的學習率,在.cfg檔案中定義了它的初始值和調整策略。剛開始出現的值很有可能時 0 是正常情況
3.38700 seconds:當前批次的訓練時間
88 images:代表已參與訓練的圖片的數量
7、訓練完成與測試
本次訓練用的是破顯示卡(750 ti),訓練不到兩小時就我就停下了 avg loss 在 3.8 左右,測試下訓練效果
將cfg 檔案的 batch 和 subdivisions 換成 1
開啟 powershell
輸入命令 ./darknet.exe detector test data/obj.data cfg/yolov3-obj.cfg backup/yolov3-obj_last.weights -i 0 -thresh 0.25
放幾張從網上隨便找的照片,測試結果。初步結果還可以