一、數(shù)據(jù)集介紹
本教程主要是利用YOLOv5算法實(shí)現(xiàn)對PCB板上的缺陷進(jìn)行檢測識(shí)別。使用的DeepPCB缺陷數(shù)據(jù)集中的所有圖像都是從線性掃描CCD獲得的,分辨率約為每1毫米48個(gè)像素,以上述方式從采樣圖像中手動(dòng)檢查
測試圖像的原始大小約為16k x 16k像素, 然后將它們裁剪成許多大小為640 x 640的子圖像,共1500張圖片,DeepPCB數(shù)據(jù)集中的部分圖片如下圖所示。
對于測試圖像中的每個(gè)缺陷,我們使用軸對齊的邊界框和一個(gè)類ID。如上所示,我們標(biāo)注了六種常見的PCB缺陷類型:open、short、mousebite、spur、pin-hole、spur。?
由于實(shí)際測試圖像中只有少數(shù)缺陷,我們根據(jù) PCB 缺陷模式在每個(gè)測試圖像上手動(dòng)論證一些人工缺陷,這導(dǎo)致每個(gè)640 x 640圖像中大約有3到12個(gè)缺陷。
PCB缺陷數(shù)如下圖所示。我們將1000 張圖像作為訓(xùn)練集,剩下的作為測試集。
二、構(gòu)建訓(xùn)練數(shù)據(jù)集?
1、先構(gòu)建數(shù)據(jù)集文件夾
本人按照VOC格式創(chuàng)建數(shù)據(jù)集,具體格式如下:
├── data │ ? ├── xml ?進(jìn)行 detection 任務(wù)時(shí)的標(biāo)簽文件,xml 形式,文件名與圖片名一一對應(yīng) │ ? ├── image ?存放.jpg 格式的圖片文件 │ ? ├── label ?存放label標(biāo)注信息的txt文件,與圖片一一對應(yīng) │ ? ├── txt ?存放原始標(biāo)注信息,x1,y1,x2,y2,type ├── dataSet(train,val,test建議按照8:1:1比例劃分) │ ? ├── train.txt ?寫著用于訓(xùn)練的圖片名稱 │ ? ├── val.txt ?寫著用于驗(yàn)證的圖片名稱 │ ? ├── trainval.txt ?train與val的合集 │ ? ├── test.txt ?寫著用于測試的圖片名稱
2、數(shù)據(jù)集格式轉(zhuǎn)換
原始的標(biāo)注信息是保存成txt文件,txt文件里面的每一行都包含一個(gè)標(biāo)注信息,格式為x1,y1,x2,y2,type,這里 (x1,y1) 和 (x2,y2) 是缺陷邊界框的左上角和右下角
type是匹配后的整數(shù) ID:0-background、1-open、2-short、3-mousebite、4-spur、5-copper、6-pin-hole。通過一下代碼進(jìn)行轉(zhuǎn)換:
import os import cv2 import time from xml.dom import minidom name_dict = {'0': 'background', '1': 'open', '2': 'short', ? ? ? ? ? ? '3': 'mousebite', '4': 'spur', '5': 'copper', '6': 'pin-hole'} def transfer_to_xml(pic, txt, file_name,xml_save_path): ? ?if not os.path.exists(xml_save_path): ? ? ? ?os.makedirs(xml_save_path,exist_ok=True) ? ?img = cv2.imread(pic) ? ?img_w = img.shape[1] ? ?img_h = img.shape[0] ? ?img_d = img.shape[2] ? ?doc = minidom.Document() ? ?annotation = doc.createElement("annotation") ? ?doc.appendChild(annotation) ? ?folder = doc.createElement('folder') ? ?folder.appendChild(doc.createTextNode('visdrone')) ? ?annotation.appendChild(folder) ? ?filename = doc.createElement('filename') ? ?filename.appendChild(doc.createTextNode(file_name)) ? ?annotation.appendChild(filename) ? ?source = doc.createElement('source') ? ?database = doc.createElement('database') ? ?database.appendChild(doc.createTextNode("Unknown")) ? ?source.appendChild(database) ? ?annotation.appendChild(source) ? ?size = doc.createElement('size') ? ?width = doc.createElement('width') ? ?width.appendChild(doc.createTextNode(str(img_w))) ? ?size.appendChild(width) ? ?height = doc.createElement('height') ? ?height.appendChild(doc.createTextNode(str(img_h))) ? ?size.appendChild(height) ? ?depth = doc.createElement('depth') ? ?depth.appendChild(doc.createTextNode(str(img_d))) ? ?size.appendChild(depth) ? ?annotation.appendChild(size) ? ?segmented = doc.createElement('segmented') ? ?segmented.appendChild(doc.createTextNode("0")) ? ?annotation.appendChild(segmented) ? ?with open(txt, 'r') as f: ? ? ? ?lines = [f.readlines()] ? ? ? ?for line in lines: ? ? ? ? ? ?for boxes in line: ? ? ? ? ? ? ? ?box = boxes.strip(' ') ? ? ? ? ? ? ? ?box = box.split(" ") ? ? ? ? ? ? ? ?x_min = box[0] ? ? ? ? ? ? ? ?y_min = box[1] ? ? ? ? ? ? ? ?x_max = box[2] ? ? ? ? ? ? ? ?y_max = box[3] ? ? ? ? ? ? ? ?object_name = name_dict[box[4]] ? ? ? ? ? ? ? ?if object_name != "background": ? ? ? ? ? ? ? ? ? ?object = doc.createElement('object') ? ? ? ? ? ? ? ? ? ?nm = doc.createElement('name') ? ? ? ? ? ? ? ? ? ?nm.appendChild(doc.createTextNode(object_name)) ? ? ? ? ? ? ? ? ? ?object.appendChild(nm) ? ? ? ? ? ? ? ? ? ?pose = doc.createElement('pose') ? ? ? ? ? ? ? ? ? ?pose.appendChild(doc.createTextNode("Unspecified")) ? ? ? ? ? ? ? ? ? ?object.appendChild(pose) ? ? ? ? ? ? ? ? ? ?truncated = doc.createElement('truncated') ? ? ? ? ? ? ? ? ? ?truncated.appendChild(doc.createTextNode("1")) ? ? ? ? ? ? ? ? ? ?object.appendChild(truncated) ? ? ? ? ? ? ? ? ? ?difficult = doc.createElement('difficult') ? ? ? ? ? ? ? ? ? ?difficult.appendChild(doc.createTextNode("0")) ? ? ? ? ? ? ? ? ? ?object.appendChild(difficult) ? ? ? ? ? ? ? ? ? ?bndbox = doc.createElement('bndbox') ? ? ? ? ? ? ? ? ? ?xmin = doc.createElement('xmin') ? ? ? ? ? ? ? ? ? ?xmin.appendChild(doc.createTextNode(x_min)) ? ? ? ? ? ? ? ? ? ?bndbox.appendChild(xmin) ? ? ? ? ? ? ? ? ? ?ymin = doc.createElement('ymin') ? ? ? ? ? ? ? ? ? ?ymin.appendChild(doc.createTextNode(y_min)) ? ? ? ? ? ? ? ? ? ?bndbox.appendChild(ymin) ? ? ? ? ? ? ? ? ? ?xmax = doc.createElement('xmax') ? ? ? ? ? ? ? ? ? ?xmax.appendChild(doc.createTextNode(str(x_max))) ? ? ? ? ? ? ? ? ? ?bndbox.appendChild(xmax) ? ? ? ? ? ? ? ? ? ?ymax = doc.createElement('ymax') ? ? ? ? ? ? ? ? ? ?ymax.appendChild(doc.createTextNode(str(y_max))) ? ? ? ? ? ? ? ? ? ?bndbox.appendChild(ymax) ? ? ? ? ? ? ? ? ? ?object.appendChild(bndbox) ? ? ? ? ? ? ? ? ? ?annotation.appendChild(object) ? ? ? ? ? ? ? ? ? ?with open(os.path.join(xml_save_path, file_name + '.xml'), 'w') as x: ? ? ? ? ? ? ? ? ? ? ? ?x.write(doc.toprettyxml()) ? ? ? ? ? ? ? ? ? ?x.close() ? ?f.close() if __name__ == '__main__': ? ?t = time.time() ? ?print('Transfer .txt to .xml...ing....') ? ?txt_folder = 'data/PCBDatasets/txt' ? ?txt_file = os.listdir(txt_folder) ? ?img_folder = 'data/PCBDatasets/image' ? ?xml_save_path = 'data/PCBDatasets/xml/' ? ?for txt in txt_file: ? ? ? ?txt_full_path = os.path.join(txt_folder, txt) ? ? ? ?img_full_path = os.path.join(img_folder, txt.split('.')[0] + '.jpg') ? ? ? ?try: ? ? ? ? ? ?transfer_to_xml(img_full_path, txt_full_path, txt.split('.')[0],xml_save_path) ? ? ? ?except Exception as e: ? ? ? ? ? ?print(e) ? ?print("Transfer .txt to .XML sucessed. costed: {:.3f}s...".format(time.time() - t))
3、訓(xùn)練集劃分代碼
主要是將數(shù)據(jù)集分類成訓(xùn)練數(shù)據(jù)集和測試數(shù)據(jù)集,默認(rèn)train,val,test按照比例進(jìn)行隨機(jī)分類,運(yùn)行后dataSet文件夾中會(huì)出現(xiàn)四個(gè)文件
主要是生成的訓(xùn)練數(shù)據(jù)集和測試數(shù)據(jù)集的圖片名稱,如下圖。同時(shí)data目錄下也會(huì)出現(xiàn)這四個(gè)文件,內(nèi)容是訓(xùn)練數(shù)據(jù)集和測試數(shù)據(jù)集的圖片路徑。
import os import random trainval_percent = 0.9 train_percent = 0.9 xmlfilepath = 'data/PCBDatasets/xml/' txtsavepath = 'data/PCBDatasets/dataSet/' total_xml = os.listdir(xmlfilepath) num = len(total_xml) list = range(num) tv = int(num * trainval_percent) tr = int(tv * train_percent) trainval = random.sample(list, tv) train = random.sample(trainval, tr) ftrainval = open('data/PCBDatasets/dataSet/trainval.txt', 'w') ftest = open('data/PCBDatasets/dataSet/test.txt', 'w') ftrain = open('data/PCBDatasets/dataSet/train.txt', 'w') fval = open('data/PCBDatasets/dataSet/val.txt', 'w') for i in list: ? ?name = total_xml[i][:-4] + ' ' ? ?if i in trainval: ? ? ? ?ftrainval.write(name) ? ? ? ?if i in train: ? ? ? ? ? ?ftrain.write(name) ? ? ? ?else: ? ? ? ? ? ?fval.write(name) ? ?else: ? ? ? ?ftest.write(name) ftrainval.close() ftrain.close() fval.close() ftest.close()
4、生成yolo格式的標(biāo)簽
主要是將圖片數(shù)據(jù)集標(biāo)注后的xml文件中的標(biāo)注信息讀取出來并寫入txt文件,運(yùn)行后在label文件夾中出現(xiàn)所有圖片數(shù)據(jù)集的標(biāo)注信息
# xml解析包 import xml.etree.ElementTree as ET import pickle import os # os.listdir() 方法用于返回指定的文件夾包含的文件或文件夾的名字的列表 from os import listdir, getcwd from os.path import join sets = ['train', 'test', 'val'] classes = ['open', 'short','mousebite','spur', 'copper', 'pin-hole'] # 進(jìn)行歸一化操作 def convert(size, box): ?# size:(原圖w,原圖h) , box:(xmin,xmax,ymin,ymax) ? ?dw = 1./size[0] ? ? # 1/w ? ?dh = 1./size[1] ? ? # 1/h ? ?x = (box[0] + box[1])/2.0 ? # 物體在圖中的中心點(diǎn)x坐標(biāo) ? ?y = (box[2] + box[3])/2.0 ? # 物體在圖中的中心點(diǎn)y坐標(biāo) ? ?w = box[1] - box[0] ? ? ? ? # 物體實(shí)際像素寬度 ? ?h = box[3] - box[2] ? ? ? ? # 物體實(shí)際像素高度 ? ?x = x*dw ? ?# 物體中心點(diǎn)x的坐標(biāo)比(相當(dāng)于 x/原圖w) ? ?w = w*dw ? ?# 物體寬度的寬度比(相當(dāng)于 w/原圖w) ? ?y = y*dh ? ?# 物體中心點(diǎn)y的坐標(biāo)比(相當(dāng)于 y/原圖h) ? ?h = h*dh ? ?# 物體寬度的寬度比(相當(dāng)于 h/原圖h) ? ?return (x, y, w, h) ? ?# 返回 相對于原圖的物體中心點(diǎn)的x坐標(biāo)比,y坐標(biāo)比,寬度比,高度比,取值范圍[0-1] def convert_annotation(image_id): ? ?''' ? ?將對應(yīng)文件名的xml文件轉(zhuǎn)化為label文件,xml文件包含了對應(yīng)的bunding框以及圖片長款大小等信息, ? ?通過對其解析,然后進(jìn)行歸一化最終讀到label文件中去,也就是說 ? ?一張圖片文件對應(yīng)一個(gè)xml文件,然后通過解析和歸一化,能夠?qū)?yīng)的信息保存到唯一一個(gè)label文件中去 ? labal文件中的格式:calss x y w h 同時(shí),一張圖片對應(yīng)的類別有多個(gè),所以對應(yīng)的bounding的信息也有多個(gè) ? ?''' ? ?# 對應(yīng)的通過year 找到相應(yīng)的文件夾,并且打開相應(yīng)image_id的xml文件,其對應(yīng)bund文件 ? ?in_file = open('data/PCBDatasets/xml/%s.xml' % (image_id), encoding='utf-8') ? ?# 準(zhǔn)備在對應(yīng)的image_id 中寫入對應(yīng)的label,分別為 ? ?#? ?out_file = open('data/PCBDatasets/label/%s.txt' % (image_id), 'w', encoding='utf-8') ? ?# 解析xml文件 ? ?tree = ET.parse(in_file) ? ?# 獲得對應(yīng)的鍵值對 ? ?root = tree.getroot() ? ?# 獲得圖片的尺寸大小 ? ?size = root.find('size') ? ?# 如果xml內(nèi)的標(biāo)記為空,增加判斷條件 ? ?if size != None: ? ? ? ?# 獲得寬 ? ? ? ?w = int(size.find('width').text) ? ? ? ?# 獲得高 ? ? ? ?h = int(size.find('height').text) ? ? ? ?# 遍歷目標(biāo)obj ? ? ? ?for obj in root.iter('object'): ? ? ? ? ? ?# 獲得difficult ?? ? ? ? ? ? ?difficult = obj.find('difficult').text ? ? ? ? ? ?# 獲得類別 =string 類型 ? ? ? ? ? ?cls = obj.find('name').text ? ? ? ? ? ?# 如果類別不是對應(yīng)在我們預(yù)定好的class文件中,或difficult==1則跳過 ? ? ? ? ? ?if cls not in classes or int(difficult) == 1: ? ? ? ? ? ? ? ?continue ? ? ? ? ? ?# 通過類別名稱找到id ? ? ? ? ? ?cls_id = classes.index(cls) ? ? ? ? ? ?# 找到bndbox 對象 ? ? ? ? ? ?xmlbox = obj.find('bndbox') ? ? ? ? ? ?# 獲取對應(yīng)的bndbox的數(shù)組 = ['xmin','xmax','ymin','ymax'] ? ? ? ? ? ?b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text), ? ? ? ? ? ? ? ? float(xmlbox.find('ymax').text)) ? ? ? ? ? ?print(image_id, cls, b) ? ? ? ? ? ?# 帶入進(jìn)行歸一化操作 ? ? ? ? ? ?# w = 寬, h = 高, b= bndbox的數(shù)組 = ['xmin','xmax','ymin','ymax'] ? ? ? ? ? ?bb = convert((w, h), b) ? ? ? ? ? ?# bb 對應(yīng)的是歸一化后的(x,y,w,h) ? ? ? ? ? ?# 生成 calss x y w h 在label文件中 ? ? ? ? ? ?out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + ' ') # 返回當(dāng)前工作目錄 wd = getcwd() print(wd) for image_set in sets: ? ?''' ? ?對所有的文件數(shù)據(jù)集進(jìn)行遍歷 ? ?做了兩個(gè)工作: 1.將所有圖片文件都遍歷一遍,并且將其所有的全路徑都寫在對應(yīng)的txt文件中去,方便定位 ?。玻瑫r(shí)對所有的圖片文件進(jìn)行解析和轉(zhuǎn)化,將其對應(yīng)的bundingbox 以及類別的信息全部解析寫到label 文件中去 ? ? 最后再通過直接讀取文件,就能找到對應(yīng)的label 信息 ? ?''' ? ?# 先找labels文件夾如果不存在則創(chuàng)建 ? ?if not os.path.exists('data/PCBDatasets/labels/'): ? ? ? ?os.makedirs('data/PCBDatasets/labels/') ? ?# 讀取在ImageSets/Main 中的train、test..等文件的內(nèi)容 ? ?# 包含對應(yīng)的文件名稱 ? ?image_ids = open('data/PCBDatasets/dataSet/%s.txt' % (image_set)).read().strip().split() ? ? ?list_file = open('data/PCBDatasets/%s.txt' % (image_set), 'w') ? ?# 將對應(yīng)的文件_id以及全路徑寫進(jìn)去并換行 ? ?for image_id in image_ids: ? ? ? ?list_file.write('data/PCBDatasets/image/%s.jpg ' % (image_id)) ? ? ? ?# 調(diào)用 ?year = 年份 ?image_id = 對應(yīng)的文件名_id ? ? ? ?convert_annotation(image_id) ? ?# 關(guān)閉文件 ? ?list_file.close()
label文件夾中某文件內(nèi)容如下:
三、修改配置文件
1、數(shù)據(jù)配置文件
首先需要在/yolov5-master/data文件夾中,新建一個(gè)PCBDetect.yaml文件,內(nèi)容設(shè)置如下:
train: data/PCBDatasets/dataSet/train.txt val: ?data/PCBDatasets/dataSet/val.txt test: data/PCBDatasets/dataSet/test.txt nc: 6 names: ['copper', 'mousebite', 'open', 'pin-hole', 'short', 'spur']
對yolov5-master/model文件夾中,對yolov5x.yaml(根據(jù)自己選擇的模型而定)文件內(nèi)容修改。
3、trian.py修改
主要用到的幾個(gè)參數(shù):–weights,–cfg,–data,–epochs,–batch-size,–img-size,–project,-workers
重點(diǎn)注意:–weights,–cfg,–data,其他的默認(rèn)即可(batch_size,workers根據(jù)自己電腦屬性進(jìn)行設(shè)置)。
四、訓(xùn)練及測試
1、訓(xùn)練
在完成上述所有的操作之后,就可以進(jìn)行訓(xùn)練,在命令窗口輸入python train.py即可以進(jìn)行訓(xùn)練。
2、測試
在訓(xùn)練完成后可以利用測試集對訓(xùn)練后的模型進(jìn)行測試,利用val.py文件進(jìn)行測試,主要修改一下地方:
測試完成后會(huì)輸出map、precision、recall等指標(biāo),具體如下圖所示:
P-R曲線如下圖所示:
同時(shí)也可以利用detect.py文件對測試集進(jìn)行測試,將檢測后的框繪制在圖像上,部分測試結(jié)果如下圖所示:
編輯:黃飛
?
評論