據(jù)集VOC+YOLO雙格式:從標(biāo)注解讀到Y(jié)OLO訓(xùn)練避坑指南)
簡(jiǎn)介這份藥品包裝盒目標(biāo)檢測(cè)數(shù)據(jù)集面向計(jì)算機(jī)視覺(jué)入門(mén)者、算法研究及工業(yè)質(zhì)檢項(xiàng)目開(kāi)發(fā)者提供統(tǒng)一格式的真實(shí)場(chǎng)景圖像解決訓(xùn)練檢測(cè)模型時(shí)缺乏高質(zhì)量標(biāo)注數(shù)據(jù)的痛點(diǎn)。壓縮包共2000個(gè)文件包含1032張藥品包裝盒圖片、VOC格式xml標(biāo)注和YOLO格式txt標(biāo)注整體大小約27.95MB便于下載和部署同一圖像同時(shí)提供兩種格式標(biāo)注可在Pascal VOC與YOLO訓(xùn)練框架間無(wú)縫切換。數(shù)據(jù)集中僅含box一個(gè)類(lèi)別共1468個(gè)矩形框均由labelImg人工框選標(biāo)注規(guī)范。已有1285人學(xué)習(xí)/下載拿到后可自行劃分訓(xùn)練集與驗(yàn)證集用于YOLO、Faster R-CNN等模型訓(xùn)練也可服務(wù)于藥品包裝檢測(cè)、藥房自動(dòng)盤(pán)點(diǎn)、包裝缺損識(shí)別等場(chǎng)景。1. 藥品包裝盒數(shù)據(jù)集1032張雙格式標(biāo)注圖省掉從零標(biāo)框的幾天時(shí)間做藥品包裝盒識(shí)別時(shí)最耽誤時(shí)間的不是寫(xiě)模型而是標(biāo)數(shù)據(jù)。去過(guò)藥廠(chǎng)倉(cāng)庫(kù)的人都知道藥盒反光、瓶身和盒子挨得近、同類(lèi)包裝尺寸差異不大框稍微偏一點(diǎn)訓(xùn)練出來(lái)的效果就差很多。這份藥品包裝盒數(shù)據(jù)集正好解決這個(gè)問(wèn)題1032張圖片已經(jīng)全部標(biāo)注完成同時(shí)給出VOC和YOLO兩種格式按需取用就能直接進(jìn)訓(xùn)練流程。整理這份筆記時(shí)我把目錄結(jié)構(gòu)、坐標(biāo)格式、訓(xùn)練命令和踩過(guò)的坑都過(guò)了一遍結(jié)論是它適合兩類(lèi)人手頭有藥品檢測(cè)需求但不想從零標(biāo)框的開(kāi)發(fā)者以及想用一份干凈數(shù)據(jù)練習(xí)YOLO目標(biāo)檢測(cè)完整流程的入門(mén)者。下面按“先看結(jié)構(gòu)→再訓(xùn)練→后避坑”的順序展開(kāi)。2. 拆開(kāi)壓縮包前先搞懂VOC與YOLO標(biāo)注目錄、坐標(biāo)與類(lèi)別的三個(gè)細(xì)節(jié)2.1 目錄結(jié)構(gòu)里到底有什么JPEGImages、Annotations、labels 三件套很多從網(wǎng)上下數(shù)據(jù)集的人第一反應(yīng)是直接解壓找圖片和標(biāo)注文件然后隨便指定一個(gè)路徑丟給訓(xùn)練腳本。這個(gè)做法不是不能跑而是很容易讓標(biāo)注文件和圖片錯(cuò)位。我的習(xí)慣是先看目錄結(jié)構(gòu)再抽樣看兩個(gè)標(biāo)注文件最后才會(huì)動(dòng)訓(xùn)練命令。這份數(shù)據(jù)集既然同時(shí)給了VOC和YOLO兩種格式解壓后的目錄一般會(huì)沿用Pascal VOC時(shí)代留下來(lái)的組織方式data/ ├── JPEGImages/ # 全部圖片jpg格式 ├── Annotations/ # VOC格式標(biāo)注每張圖對(duì)應(yīng)一個(gè)xml ├── labels/ # YOLO格式標(biāo)注每張圖對(duì)應(yīng)一個(gè)txt └── ImageSets/Main/ # 劃分文件 ├── train.txt ├── val.txt └── test.txtJPEGImages放圖片Annotations放VOC的xmllabels放YOLO的txtImageSets/Main里是數(shù)據(jù)集劃分。需要注意不同發(fā)布者打包習(xí)慣不一樣有的把labels做成了扁平的labels/*.txt有的直接給出labels/train和labels/val。先不要急著開(kāi)始訓(xùn)練用命令數(shù)一下三個(gè)目錄文件數(shù)是否一致。ls JPEGImages | wc -l ls Annotations | wc -l ls labels | wc -l如果三個(gè)數(shù)字不一樣大概率存在漏標(biāo)或者多余的xml/txt這種情況訓(xùn)練時(shí)會(huì)直接報(bào)錯(cuò)。1032張圖對(duì)應(yīng)的xml和txt理論上都應(yīng)該是1032個(gè)但偶爾會(huì)因壓縮包傳輸丟文件出現(xiàn)數(shù)量不一致先在這一步確認(rèn)清楚。2.2 坐標(biāo)定義不同VOC的絕對(duì)框和YOLO的歸一化中心這個(gè)數(shù)據(jù)集叫“VOCYOLO格式”實(shí)際意思是同一種標(biāo)注用兩種文件表達(dá)。VOC格式存的是XML目標(biāo)框?qū)懗山^對(duì)像素坐標(biāo)YOLO格式存的是文本每行一個(gè)目標(biāo)內(nèi)容是類(lèi)別id加歸一化中心點(diǎn)坐標(biāo)和歸一化寬高。給個(gè)例子更直觀。annotation folderJPEGImages/folder filenameimg_0001.jpg/filename size width1920/width height1080/height depth3/depth /size object namemedicine_box/name bndbox xmin420/xmin ymin180/ymin xmax720/xmax ymax520/ymax /bndbox /object /annotation這是標(biāo)準(zhǔn)的Pascal VOC標(biāo)注img_0001.jpg對(duì)應(yīng)的YOLO標(biāo)簽文件img_0001.txt內(nèi)容只有一行0 0.296875 0.324074 0.156250 0.314815第一格0是類(lèi)別id后面四個(gè)數(shù)分別是 x_center (420 720) / 2 / 1920 0.296875 y_center (180 520) / 2 / 1080 0.324074 w (720 - 420) / 1920 0.15625 h (520 - 180) / 1080 0.314815四個(gè)坐標(biāo)全被歸一化到0到1之間不同分辨率的圖片就可以放進(jìn)同一個(gè)batch訓(xùn)練。很多人第一次看YOLO標(biāo)注會(huì)奇怪為什么只有一個(gè)框還要這么多位小數(shù)其實(shí)就是把絕對(duì)像素?fù)Q算過(guò)去的。如果出現(xiàn)x_center或?qū)捀叽笥?那就是標(biāo)注越界了后面第5章會(huì)詳細(xì)說(shuō)。2.3 一眼看清類(lèi)別與劃分用一段腳本統(tǒng)計(jì)標(biāo)簽在開(kāi)訓(xùn)之前我一般會(huì)先統(tǒng)計(jì)兩類(lèi)信息類(lèi)別數(shù)量以及每個(gè)類(lèi)別在train/val/test里的分布。這個(gè)數(shù)據(jù)集從標(biāo)題看是藥品包裝盒最常見(jiàn)的標(biāo)注是單個(gè)medicine_box類(lèi)但也不排除發(fā)布者把不同藥盒品牌標(biāo)成了多類(lèi)。在寫(xiě)data.yaml之前必須知道到底有幾個(gè)類(lèi)別否則nc寫(xiě)錯(cuò)訓(xùn)練直接報(bào)錯(cuò)。import glob from collections import Counter label_files glob.glob(labels/*.txt) cls_counter Counter() all_ids set() for path in label_files: with open(path) as fp: for line in fp: line line.strip() if line: cls_id int(line.split()[0]) all_ids.add(cls_id) cls_counter[cls_id] 1 print(類(lèi)別id集合:, sorted(all_ids)) print(類(lèi)別id - 目標(biāo)數(shù):, dict(cls_counter)) missing [] for img_path in glob.glob(JPEGImages/*.jpg): stem img_path.split(/)[-1].replace(.jpg, ) if not glob.glob(flabels/{stem}.txt): missing.append(stem) print(缺標(biāo)簽的圖片數(shù):, len(missing), missing[:5])這段腳本做的事情很簡(jiǎn)單遍歷所有l(wèi)abels/*.txt把每行第一個(gè)數(shù)字拿出來(lái)計(jì)數(shù)得到類(lèi)別id是哪些、每個(gè)id出現(xiàn)多少次。然后反過(guò)來(lái)查JPEGImages里的jpg是否都有同名txt有缺就打印出具體文件名方便后面定位。參數(shù)上要注意兩點(diǎn)。第一類(lèi)別id從0開(kāi)始是YOLO系約定如果統(tǒng)計(jì)結(jié)果里出現(xiàn){1: 1032}而沒(méi)有任何0說(shuō)明發(fā)布者把類(lèi)別id從1開(kāi)始編了訓(xùn)練前要把txt批量減1。第二腳本默認(rèn)labels是扁平目錄如果labels下還有train和val子目錄就把glob路徑改成labels//.txt。這里不要硬編碼絕對(duì)路徑方便在解壓目錄直接跑。3. 用YOLO訓(xùn)練藥盒檢測(cè)從數(shù)據(jù)重排到第一個(gè)best.pt3.1 把雙格式整理成YOLO標(biāo)準(zhǔn)目錄先用2.3的腳本確認(rèn)了類(lèi)別和標(biāo)簽完整性接下來(lái)要把文件挪成Ultralytics YOLO認(rèn)識(shí)的標(biāo)準(zhǔn)目錄。YOLOv5、YOLOv8和YOLO11的默認(rèn)數(shù)據(jù)布局都是datasets/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/如果你拿到的包已經(jīng)按這個(gè)布局組織直接跳到3.2。如果只有一個(gè)扁平的labels目錄就要按ImageSets/Main下的train.txt、val.txt、test.txt來(lái)重新放。這個(gè)步驟我一般用Python完成幾秒搞定。import os import shutil from pathlib import Path root Path(.) # 數(shù)據(jù)集解壓根目錄 split_map {train.txt: train, val.txt: val, test.txt: test} for split_file, subset in split_map.items(): split_path root / ImageSets / Main / split_file if not split_path.exists(): continue os.makedirs(root / images / subset, exist_okTrue) os.makedirs(root / labels / subset, exist_okTrue) with open(split_path) as fp: for line in fp: line line.strip() if not line: continue stem line.split(/)[-1] if stem.endswith(.jpg): stem stem[:-4] img_src root / JPEGImages / f{stem}.jpg lbl_src root / labels / f{stem}.txt if img_src.exists(): shutil.copy(img_src, root / images / subset / f{stem}.jpg) else: print(f[warn] 圖片不存在: {img_src}) if lbl_src.exists(): shutil.copy(lbl_src, root / labels / subset / f{stem}.txt) else: print(f[warn] 標(biāo)簽不存在: {lbl_src})這里有個(gè)容易踩的細(xì)節(jié)ImageSets/Main里的行內(nèi)容可能是純文件名img_0001也可能是帶路徑的JPEGImages/img_0001.jpg。代碼里用split(/)[-1]取最后一段再判斷是否以.jpg結(jié)尾就是為了兼容這兩種寫(xiě)法。shutil.copy是復(fù)制而不是移動(dòng)保留原目錄文件這樣后續(xù)想對(duì)比VOC不會(huì)破壞原始數(shù)據(jù)。磁盤(pán)空間緊張時(shí)可以把copy改成move。3.2 寫(xiě)data.yaml并檢查數(shù)據(jù)集train/val整理好后需要寫(xiě)一個(gè)data.yaml告訴訓(xùn)練器類(lèi)別名和路徑。放在數(shù)據(jù)集根目錄下我一般命名為medicine_box.yaml。# 數(shù)據(jù)集根目錄建議用絕對(duì)路徑避免不同機(jī)器工作目錄不一樣 path: /home/user/datasets/medbox train: images/train val: images/val # test 可留空也能在訓(xùn)練后單獨(dú)跑test test: images/test nc: 1 names: 0: medicine_box參數(shù)說(shuō)明path是根目錄train和val都是相對(duì)path的路徑。names的鍵從0開(kāi)始順序和標(biāo)簽txt里的類(lèi)別id嚴(yán)格對(duì)應(yīng)。如果你統(tǒng)計(jì)出來(lái)是多類(lèi)names里要按順序列全比如0: box_a, 1: box_b。寫(xiě)完先用一行命令驗(yàn)證數(shù)據(jù)集能否加載。yolo detect train datamedicine_box.yaml epochs0 batch1 device0epochs0是Ultralytics的一種快速檢查方式它不會(huì)真正訓(xùn)練只做數(shù)據(jù)集加載并打印train/val圖片數(shù)和類(lèi)別數(shù)。如果這里報(bào)錯(cuò)不要急著調(diào)模型先回頭查目錄結(jié)構(gòu)和yaml。提示多人協(xié)作時(shí)絕對(duì)路徑會(huì)導(dǎo)致別人機(jī)器上失效這種情況可以用相對(duì)path例如path: ./datasets/medbox。但相對(duì)路徑是從終端工作目錄解析容易踩坑所以單機(jī)訓(xùn)練我始終用絕對(duì)路徑。3.3 訓(xùn)練參數(shù)怎么定epochs、imgsz、batch 與預(yù)訓(xùn)練權(quán)重?cái)?shù)據(jù)集就緒接下來(lái)就是訓(xùn)練。以Ultralytics YOLO為例一行命令能跑yolo detect train datamedicine_box.yaml modelyolov8n.pt epochs100 batch16 imgsz640 device0我針對(duì)1032張這種量級(jí)的選參習(xí)慣是預(yù)訓(xùn)練權(quán)重從yolov8n或yolov8s開(kāi)始先用n跑通流程再看效果換sepochs默認(rèn)給100訓(xùn)練時(shí)會(huì)通過(guò)早停機(jī)制自動(dòng)截?cái)嗖挥脼榱藴悢?shù)去改imgsz保持640這是YOLO系列的默認(rèn)輸入尺寸如果藥盒在圖中比較小可以提高到960或1280但顯存占用和訓(xùn)練時(shí)間會(huì)明顯上漲batch在顯存允許范圍內(nèi)盡量大16G顯存跑yolov8n時(shí)batch16沒(méi)問(wèn)題6G顯存就改batch8。訓(xùn)練過(guò)程中終端除了每輪的mAP還會(huì)打印box_loss、cls_loss、dfl_loss這幾個(gè)yolo損失函數(shù)分量。很多人只看最后的mAP50其實(shí)觀察損失曲線(xiàn)更有用如果loss在前20輪不降反升大部分情況是學(xué)習(xí)率過(guò)大或數(shù)據(jù)集劃分有問(wèn)題如果loss降了但mAP不動(dòng)就要檢查是不是藥盒目標(biāo)本身太相似導(dǎo)致分類(lèi)學(xué)不動(dòng)。如果你想控制數(shù)據(jù)增強(qiáng)訓(xùn)練命令里可以加augment相關(guān)參數(shù)yolo detect train datamedicine_box.yaml modelyolov8s.pt epochs120 batch16 imgsz640 scale0.5 fliplr0.5 mosaic1.0scale控制隨機(jī)縮放范圍fliplr是水平翻轉(zhuǎn)概率mosaic是馬賽克增強(qiáng)。藥盒檢測(cè)通常不需要太多翻轉(zhuǎn)因?yàn)榘b盒上的文字方向有實(shí)際含義豎直方向翻轉(zhuǎn)反而會(huì)讓模型學(xué)到錯(cuò)誤特征。3.4 訓(xùn)練產(chǎn)物和損失曲線(xiàn)怎么看訓(xùn)練結(jié)束后在runs/detect/train目錄會(huì)生成weights/best.pt、weights/last.pt、results.png、confusion_matrix.png、args.yaml等一堆文件。最常用的是兩個(gè)權(quán)重best.pt是驗(yàn)證集效果最好的模型last.pt是最后一輪epoch的模型。日常推理和部署直接取best.pt不要為了“多訓(xùn)幾輪更好”就默認(rèn)用last.pt。last.pt更適合在斷點(diǎn)續(xù)訓(xùn)時(shí)作為預(yù)訓(xùn)練權(quán)重。results.png是訓(xùn)練過(guò)程的匯總圖從上到下分別是box_loss、cls_loss、dfl_loss、precision、recall、mAP50、mAP50-95??催@條曲線(xiàn)如果最后的mAP50在0.5以下大概率是標(biāo)注不全或類(lèi)別混淆如果mAP50能到0.8以上但mAP50-95掉得厲害說(shuō)明框沒(méi)有貼合邊界藥盒邊緣被壓線(xiàn)了。這些都是經(jīng)驗(yàn)值不同場(chǎng)景會(huì)有浮動(dòng)但能幫你在調(diào)參前快速定位問(wèn)題方向。驗(yàn)證時(shí)還可以看confusion_matrix.png理清哪兩類(lèi)容易互相誤檢。4. 反向使用VOC格式XML轉(zhuǎn)YOLO腳本與舊工程兼容方案4.1 為什么VOC格式到今天還有用雙格式數(shù)據(jù)集里YOLO txt顯然是新工程首選那XML還有必要留嗎有的而且用到的頻率不低。本地標(biāo)注工具如果不換labelImg導(dǎo)出就是VOC XML很多公司老代碼庫(kù)里的檢測(cè)pipeline只寫(xiě)了讀VOC的Loader比如基于Detectron2或Faster R-CNN的工程還有做數(shù)據(jù)審核的同事習(xí)慣在labelImg里打開(kāi)XML回看標(biāo)注。這種情況下直接把訓(xùn)練用的txt再轉(zhuǎn)回XML比重新標(biāo)注快得多。Pascal VOC這套XML標(biāo)準(zhǔn)雖然老但勝在結(jié)構(gòu)透明任何語(yǔ)言都能用標(biāo)準(zhǔn)庫(kù)解析不帶額外依賴(lài)。所以拿到“VOCYOLO格式”的數(shù)據(jù)集等于同時(shí)拿到了一條通往老框架和YOLO訓(xùn)練的通路。而反過(guò)來(lái)當(dāng)你從別的數(shù)據(jù)集拿到只有XML的標(biāo)注想用YOLO跑也需要把XML轉(zhuǎn)成txt。這個(gè)轉(zhuǎn)換不應(yīng)該靠手工重復(fù)下面給兩個(gè)小腳本第一步是從XML轉(zhuǎn)到Y(jié)OLO。4.2 XML轉(zhuǎn)YOLO txt一個(gè)不讀圖片的高效腳本用Python標(biāo)準(zhǔn)庫(kù)xml.etree.ElementTree解析XML圖片寬高直接從XML的size節(jié)點(diǎn)讀取不需要額外打開(kāi)圖片速度會(huì)快很多。import xml.etree.ElementTree as ET import glob import os CLASS_NAMES [medicine_box] # 需要和訓(xùn)練用的類(lèi)別順序保持一致 def xml_to_yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() img_w int(root.findtext(size/width)) img_h int(root.findtext(size/height)) stem os.path.splitext(os.path.basename(xml_path))[0] lines [] for obj in root.findall(object): name obj.findtext(name) if name not in CLASS_NAMES: continue cls_id CLASS_NAMES.index(name) bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) os.makedirs(out_dir, exist_okTrue) with open(os.path.join(out_dir, f{stem}.txt), w) as fp: fp.write(\n.join(lines)) for xml_file in glob.glob(Annotations/*.xml): xml_to_yolo(xml_file, labels_from_xml)這個(gè)腳本有幾個(gè)值得說(shuō)的點(diǎn)。findtext(size/width)能直接拿到size下的width文本比一層層find再.text省事bndbox里的xmin、ymin這些XML節(jié)點(diǎn)文本存的是字符串轉(zhuǎn)float后再做算術(shù)避免字符串拼接造成隱式錯(cuò)誤最后寫(xiě)入時(shí)小數(shù)位統(tǒng)一保留6位訓(xùn)練解析時(shí)不會(huì)因?yàn)榫葐?wèn)題丟邊界。CLASS_NAMES的順序必須和最終訓(xùn)練yaml里names順序完全一致否則框還是那些框但類(lèi)別標(biāo)簽全錯(cuò)位了。4.3 反過(guò)來(lái)把YOLO txt還原成VOC XML用于校對(duì)如果你想在labelImg里檢查YOLO標(biāo)注或者喂給只認(rèn)VOC的老框架就需要反向轉(zhuǎn)換。轉(zhuǎn)換公式是剛才的逆運(yùn)算def yolo_to_xml(txt_path, img_w, img_h, out_dir): stem os.path.splitext(os.path.basename(txt_path))[0] xml_lines [ annotation, f filename{stem}.jpg/filename, size, f width{img_w}/width, f height{img_h}/height, depth3/depth, /size ] with open(txt_path) as fp: for line in fp: parts line.strip().split() if not parts: continue cls_id, x_cent, y_cent, w, h parts xmin (float(x_cent) - float(w) / 2) * img_w ymin (float(y_cent) - float(h) / 2) * img_h xmax (float(x_cent) float(w) / 2) * img_w ymax (float(y_cent) float(h) / 2) * img_h # CLASS_NAMES 由外部傳入順序與訓(xùn)練時(shí)一致 xml_lines.append( object) xml_lines.append(f name{CLASS_NAMES[int(cls_id)]}/name) xml_lines.append( bndbox) xml_lines.append(f xmin{xmin:.0f}/xmin) xml_lines.append(f ymin{ymin:.0f}/ymin) xml_lines.append(f xmax{xmax:.0f}/xmax) xml_lines.append(f ymax{ymax:.0f}/ymax) xml_lines.append( /bndbox) xml_lines.append( /object) xml_lines.append(/annotation) with open(os.path.join(out_dir, f{stem}.xml), w) as fp: fp.write(\n.join(xml_lines))這段代碼要注意的是圖片寬高必須傳入因?yàn)閅OLO txt里只有歸一化坐標(biāo)沒(méi)有圖片尺寸信息。實(shí)際工程里圖片尺寸可以從JPEGImages下同名jpg用PIL讀出來(lái)但那樣會(huì)慢所以一般先用一次glob讀出所有圖片尺寸存成字典再批量轉(zhuǎn)換。另一個(gè)細(xì)節(jié)還原出來(lái)的xmin/ymin用取整輸出四舍五入到整像素labelImg讀進(jìn)去不會(huì)有問(wèn)題如果要給訓(xùn)練器用也可以保留小數(shù)不取整。有了這兩個(gè)轉(zhuǎn)換函數(shù)這套VOCYOLO雙格式數(shù)據(jù)集就能自由在舊工程和YOLO生態(tài)之間切換。偶爾需要新增數(shù)據(jù)時(shí)用labelImg標(biāo)一批XML再跑一遍4.2腳本轉(zhuǎn)成txt并入原有目錄即可。5. 避坑這類(lèi)數(shù)據(jù)集最常見(jiàn)的四個(gè)翻車(chē)點(diǎn)5.1 類(lèi)別數(shù)對(duì)不上訓(xùn)練報(bào) class index out of range 怎么查訓(xùn)練剛開(kāi)始不到兩分鐘就崩常見(jiàn)報(bào)錯(cuò)是AssertionError: class index ... out of range或者Invalid label file。原因基本是三類(lèi)data.yaml的nc和names沒(méi)有嚴(yán)格對(duì)齊labels目錄里混入了別的數(shù)據(jù)集txt類(lèi)別id超過(guò)了nc發(fā)布者把類(lèi)別id從1開(kāi)始計(jì)數(shù)導(dǎo)致最大id超出names長(zhǎng)度。解決方法是先跑2.3的統(tǒng)計(jì)腳本把整個(gè)labels目錄的類(lèi)別id列出來(lái)。你要確認(rèn)最小id是0最大id小于nc。然后打開(kāi)classes.txt和xml里的name比對(duì)確認(rèn)names順序不是按字母序而是按訓(xùn)練字典。我遇到過(guò)更隱蔽的情況xml里只有box一個(gè)類(lèi)但某個(gè)txt里混著一行舊的類(lèi)別id 5這種通常來(lái)自發(fā)布者合并數(shù)據(jù)集時(shí)漏刪文件直接用文件路徑定位后刪掉那一行即可。5.2 藥盒緊貼圖邊letterbox后框越界漏檢現(xiàn)象是訓(xùn)練正常但val圖片里靠邊的藥盒經(jīng)常丟框。原因是拍照時(shí)藥盒本身頂著圖邊緣XML里的xmax或ymax超過(guò)圖片寬高YOLO歸一化后變成1.0x訓(xùn)練時(shí)letterbox會(huì)在原圖周?chē)畛浠疫?.0的坐標(biāo)被映射到填充區(qū)模型學(xué)到的框就不準(zhǔn)了。解決分兩步。第一步檢查越界標(biāo)注python -c import glob, xml.etree.ElementTree as ET for f in glob.glob(Annotations/*.xml): r ET.parse(f).getroot() w, h int(r.findtext(size/width)), int(r.findtext(size/height)) for o in r.findall(object): b o.find(bndbox) xmax, ymax float(b.findtext(xmax)), float(b.findtext(ymax)) if xmax w or ymax h: print(f, xmax, ymax, w, h) 這段代碼會(huì)打印所有框坐標(biāo)越界的XML文件名和具體數(shù)值。第二步是處理把越界的xmax/ymax強(qiáng)制clip回圖片邊界再重新生成YOLO txt不要手工改XML。訓(xùn)練參數(shù)上fliplr降到0.3scale控制在0.3到0.8減少隨機(jī)縮放對(duì)邊緣目標(biāo)的破壞。藥盒這類(lèi)物體邊緣特征靠文字放大太多反而丟細(xì)節(jié)。5.3 數(shù)據(jù)泄漏train/val里出現(xiàn)幾乎相同的圖現(xiàn)象是訓(xùn)練時(shí)mAP50已經(jīng)0.95但val集只有0.6而且隨著epoch增加val曲線(xiàn)不再上升。原因是數(shù)據(jù)劃分批次時(shí)連續(xù)拍攝的同場(chǎng)景照片被拆去了train和val藥盒位置、光線(xiàn)都幾乎一樣訓(xùn)練時(shí)模型等于見(jiàn)過(guò)val圖片指標(biāo)虛高實(shí)際部署立刻掉點(diǎn)。解決方法是劃分前先做相似度去重。簡(jiǎn)單做法是用OpenCV的感知哈希每張圖縮成8x8算64位hash漢明距離小于5的視為重復(fù)圖只保留一張import cv2 import numpy as np import glob def phash(img_path, size8): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) resized cv2.resize(img, (size, size), interpolationcv2.INTER_LINEAR) return (resized resized.mean()).astype(np.uint8) def hamming(a, b): return np.count_nonzero(a ! b) imgs glob.glob(JPEGImages/*.jpg) hashes {p: phash(p) for p in imgs} for i in range(len(imgs)): for j in range(i 1, len(imgs)): if hamming(hashes[imgs[i]], hashes[imgs[j]]) 5: print(相似的圖片:, imgs[i], imgs[j])這段腳本復(fù)雜度O(n2)1032張圖沒(méi)問(wèn)題更大的數(shù)據(jù)集要先做索引。相似判斷可能把不同藥盒誤判成近似閾值5可以按實(shí)際情況調(diào)。排除重復(fù)圖后再把同一時(shí)間戳前綴或同序列號(hào)的文件分到同一集合沒(méi)有這類(lèi)元數(shù)據(jù)時(shí)隨機(jī)多分幾個(gè)val集合選均值最穩(wěn)定的一個(gè)。5.4 路徑和文件名惹的禍中文空格、相對(duì)路徑現(xiàn)象是Windows下壓縮包正常換到Linux服務(wù)器一跑就報(bào)Train dataset not found或圖片讀取為None。原因是解壓時(shí)Windows目錄名帶空格和中文yaml里path寫(xiě)的是反斜杠相對(duì)路徑對(duì)不上還有rar解壓偶爾生成._開(kāi)頭的AppleDouble文件被訓(xùn)練器當(dāng)成標(biāo)注文件。解決是三步走所有目錄改成純英文無(wú)空格yaml統(tǒng)一用正斜杠解壓后先跑一句find . -name ._* -delete清理多余文件再數(shù)圖片和標(biāo)簽數(shù)量缺少單個(gè)文件會(huì)直接報(bào)no labels found。注意有些系統(tǒng)解開(kāi)rar后文件權(quán)限是只讀建議chmod -R uw dataset一鍵開(kāi)放寫(xiě)權(quán)限細(xì)節(jié)不多但能折騰很久。6. 用訓(xùn)練結(jié)果做真實(shí)推理驗(yàn)證、調(diào)置信度與導(dǎo)出ONNX6.1 用best.pt跑驗(yàn)證集看不是只看mAP訓(xùn)練完不要急著把best.pt發(fā)給同事先用val集跑一遍yolo detect val datamedicine_box.yaml modelruns/detect/train/weights/best.pt batch8輸出會(huì)包含precision、recall、mAP50、mAP50-95同時(shí)把混淆矩陣和預(yù)測(cè)圖片寫(xiě)進(jìn)runs/detect/val目錄。我一般會(huì)重點(diǎn)看預(yù)測(cè)圖里藥盒之間的疊放案例藥店照片常出現(xiàn)盒子之間互相壓邊val結(jié)果圖能直觀看到是不是漏檢了壓住的那一盒這種預(yù)測(cè)圖比數(shù)字更容易發(fā)現(xiàn)問(wèn)題。如果mAP50超過(guò)0.85但val預(yù)測(cè)圖里還是有漏框大概率是標(biāo)注邊界太小回到數(shù)據(jù)集把緊貼的框重新clip一遍再訓(xùn)。6.2 藥盒推理時(shí)的置信度與IOU調(diào)整實(shí)際推理時(shí)用CLI或者Python接口都一樣關(guān)鍵是置信度不要一成不變。yolo detect predict modelbest.pt sourcetest_images/ conf0.25 iou0.45 saveTrue藥盒反光會(huì)造成部分框置信度偏低如果漏檢很多別急著換模型先調(diào)conf。我習(xí)慣把conf降到0.15同時(shí)把iou從0.45提到0.5讓相鄰的高置信度框不容易被合并藥盒之間的邊界更干凈。如果誤檢變多再把conf提回0.3即可。6.3 導(dǎo)出ONNX給TensorRT和邊緣設(shè)備復(fù)用最后一步是把訓(xùn)練好的權(quán)重導(dǎo)出成ONNXyolo export modelbest.pt formatonnx imgsz640 dynamicFalse simplifyTrue導(dǎo)出后得到一個(gè)best.onnx用onnxruntime可以在CPU/GPU上推理也可以轉(zhuǎn)成TensorRT engine跑服務(wù)。dynamicFalse會(huì)讓輸入尺寸固定為640部署時(shí)省心如果項(xiàng)目里圖片尺寸多變?cè)俑膁ynamicTrue。要注意的是導(dǎo)出ONNX后輸入張量的歸一化方式仍然是像素值除以255外部調(diào)用時(shí)需要保持同一套預(yù)處理不要重復(fù)歸一化。我從那以后每次拿到現(xiàn)成數(shù)據(jù)集都不會(huì)直接開(kāi)訓(xùn)而是先花半小時(shí)把目錄結(jié)構(gòu)、標(biāo)簽統(tǒng)計(jì)和劃分完整性過(guò)一遍確認(rèn)沒(méi)問(wèn)題再寫(xiě)訓(xùn)練命令。這個(gè)習(xí)慣幫我省了很多翻車(chē)的時(shí)間尤其是在“VOCYOLO雙格式”這種看起來(lái)沒(méi)什么坑、實(shí)際上坑都在文件里的場(chǎng)景。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取