:VOC轉YOLO與YOLO訓練全解析)
簡介NEU-DET 鋼材表面缺陷檢測數(shù)據(jù)集是一份面向工業(yè)視覺檢測與深度學習目標檢測任務的標準資源適合入門至進階的算法工程師、科研人員及競賽選手用于模型訓練與評估。資源包含 1799 張 JPEG 原圖并提供 Pascal VOC 格式 XML 與 YOLO 格式 TXT 兩套標注文件覆蓋 crazing、inclusion、patches、pitted_surface、rolled-in_scale、scratches 共 6 個典型缺陷類別總標注框數(shù)達 4186 個各類別框數(shù)分布清晰可直接用于 YOLO、Faster R-CNN、SSD 等常見檢測框架。數(shù)據(jù)均采用 labelImg 工具以矩形框精細標注便于用戶快速開展缺陷識別、數(shù)據(jù)增強或預訓練微調(diào)實驗。壓縮包共 2000 個文件主要包括 1799 個 xml 標注文件與 201 個 txt 標注及說明文件整體大小 63.1MB體積精簡、解壓即用目錄組織簡潔能大幅節(jié)省數(shù)據(jù)預處理時間。目前已有 1157 人學習使用是一份經(jīng)過驗證的高質(zhì)量工業(yè)缺陷檢測入門數(shù)據(jù)集。1. NEU-DET鋼材表面缺陷檢測數(shù)據(jù)集1799 張圖到底夠不夠用做鋼材表面缺陷檢測的人大概率繞不開 NEU-DET鋼材表面缺陷檢測數(shù)據(jù)集。它用 1799 張帶標注圖像覆蓋了六類最常見的鋼板表面缺陷同時給出 VOC 和 YOLO 兩種格式是驗證檢測算法、演練訓練流程、練習格式轉換時最順手的起點。很多人第一眼嫌它小真正用起來才發(fā)現(xiàn)標注規(guī)范、類別清晰、解壓后不用做太多清洗就能喂給訓練腳本。這個數(shù)據(jù)集的定位不是生產(chǎn)級數(shù)據(jù)而是“讓檢測模型先跑起來”的種子數(shù)據(jù)。想驗證 YOLO 系列在工業(yè)缺陷上的效果、想跑通一套從標注到訓練的完整鏈路、想把自己的數(shù)據(jù)集整理成 VOC/YOLO 格式拿它做對照最合適。我最初在模擬項目X里接觸目標檢測靠的就是這條鏈路入門。下面從格式拆解開始講到訓練配置和踩坑盡量讓你拿到壓縮包后一個晚上就能跑通第一個模型。2. 拆開 NEU-DETVOC 與 YOLO 雙格式的目錄結構和六類缺陷網(wǎng)上流傳的 NEU-DET 轉存包很多解壓后布局可能不太一樣但核心就兩塊VOC 風格的圖像加 XML 標注以及 YOLO 風格的 images/labels 目錄。先把這兩種格式各自長什么樣搞清楚后面轉換和訓練時才不會看錯文件。2.1 六類缺陷的形態(tài)與檢測難點數(shù)據(jù)集里的六類缺陷分別是 crazing、inclusion、patches、pitted_surface、rolled_in_scale、scratches常見版本里圖像尺寸基本在 200×200 左右內(nèi)容以灰度紋理為主。先記下每類的視覺特征因為后面評估結果時你會發(fā)現(xiàn)模型在哪一類上翻車往往從圖像形態(tài)上就能預判。XML 中的 name常見叫法視覺特征檢測難點crazing裂紋/網(wǎng)狀裂紋細密紋路灰度對比弱像背景紋理邊界不清inclusion夾雜物塊狀或條狀暗區(qū)與基材灰度接近patches斑塊片狀灰度異常邊緣不規(guī)則面積大但特征模糊pitted_surface麻點/凹坑密集小點狀凹陷目標小且密集rolled_in_scale氧化皮鱗片狀亮/暗區(qū)域形狀多變反光干擾scratches劃痕細長三角形狀條紋長寬比極大容易漏斷這六類不是隨便拍的基本覆蓋了熱軋、冷軋產(chǎn)線上最常引起客訴的缺陷類型。所以在做工業(yè)質(zhì)檢項目原型驗證時它比很多通用目標檢測數(shù)據(jù)集更貼近真實場景。另一個實際好處是類別少訓練迭代周期短適合用來調(diào)參。2.2 VOC 格式XML 標注里哪些字段真正決定訓練VOC 格式的核心是 JPEGImages 里的原始圖像加 Annotations 里同名 XML。典型目錄布局如下解壓后根目錄/ ├── VOCdevkit/ │ └── NEU-DET/ │ ├── Annotations/ # 每張圖對應一個 .xml │ ├── JPEGImages/ # 原始圖像jpg/png 常見 │ └── ... └── labels/ # 常見 YOLO 布局之一 ├── images/ └── labels/打開任意一個 XML結構基本是這種風格annotation folderNEU-DET/folder filenamexxx.jpg/filename size width200/width height200/height depth3/depth /size object namecrazing/name bndbox xmin10/xmin ymin20/ymin xmax180/xmax ymax190/ymax /bndbox difficult0/difficult /object /annotation對訓練真正有用的字段只有 filename、size 和 object 里的 name、bndbox。folder、difficult 這些在大多數(shù)檢測框架里都會被忽略。size 里的寬度和高度最容易被忽略但 VOC 轉 YOLO 時坐標換算要靠它后面會專門講。檢查數(shù)據(jù)集時我會隨手確認兩件事每張圖是否都有同名 XML每個 XML 里 bndbox 四個值是否為整數(shù)且沒超出 0 到 200 的范圍。超出范圍的框基本是標注誤操作訓練時輕則警示重則讓 loss 變成 NaN。很多轉存包的問題不在數(shù)量而在個別標注文件被轉存工具截斷。2.3 YOLO 格式歸一化 txt 的換算邏輯與類別 idYOLO 格式的標注不是 XML而是一個和圖像同名的 txt 文件。每一行代表一個目標框格式是 class_id cx cy width height全部歸一化到 0~1 之間。舉個直觀例子2 0.4750 0.5250 0.8500 0.4250這行表示該圖像里有一個類別 id 為 2 的目標中心點在圖像的 (0.475, 0.525) 處寬度約 0.85高度約 0.425。歸一化的好處是訓練時和圖像實際尺寸無關無論輸入是 320 還是 640標注都不會因為這個盒子被縮放而需要重算。類別 id 完全依賴你定義的順序。NEU-DET 在 YOLO 格式包里最常用的映射是0 crazing 1 inclusion 2 patches 3 pitted_surface 4 rolled_in_scale 5 scratches這個順序不是 XML 里的 name 排字典序而是打包者自己定的。拿到包后先打開兩個 txt 和對應 XML 對照一遍確認 cats 順序再動手否則模型訓練到一半你會發(fā)現(xiàn)預測框的標簽和圖像內(nèi)容對不上那時候改映射表等于重訓一次。VOC 和 YOLO 雙格式同時存在時我習慣以 XML 內(nèi)容為基準去校驗 txt而不是反過來。3. 動手前先做三件事完整性校驗、VOC 轉 YOLO 與分層劃分拿到壓縮包先別急著訓練。NEU-DET 因為被多次轉存常見問題是標簽文件缺失、類別名大小寫不一致、YOLO 標注和 VOC 標注對不上。我一般先做三件事總共不到十分鐘能避免后面浪費幾小時。3.1 用一段腳本查清 1799 張圖與標注的缺口先寫個腳本統(tǒng)計圖像和標注數(shù)量并檢查每張圖是否有對應標注。路徑按自己解壓的實際目錄改import xml.etree.ElementTree as ET from collections import Counter from pathlib import Path img_dir Path(JPEGImages) xml_dir Path(Annotations) imgs {p.stem for p in img_dir.glob(*.jpg)} xmls {p.stem for p in xml_dir.glob(*.xml)} print(圖片數(shù)量:, len(imgs)) print(標注數(shù)量:, len(xmls)) print(缺失標注的圖:, len(imgs - xmls)) print(缺失圖片的標注:, len(xmls - imgs)) class_counter Counter() for xml_file in xml_dir.glob(*.xml): root ET.parse(xml_file).getroot() for obj in root.iter(object): name obj.findtext(name) if name: class_counter[name.strip().lower()] 1 print(class_counter.most_common())這段腳本用 stem 去掉擴展名后做集合差能同時找出“有圖沒標注”和“有標注沒圖”兩種情況。類別統(tǒng)計里把 name 轉成小寫再計數(shù)是因為不同轉存包里的類別名可能出現(xiàn) Crazing 與 crazing 混用的情況。如果發(fā)現(xiàn) 1799 張圖但有 1798 個 XML別覺得差一張無所謂直接找到那個沒有 XML 的圖名后面轉換和訓練時它一定是第一個報錯的文件。如果包里帶的是 YOLO 格式就把上面邏輯換成 images 目錄和 labels 目錄對比并用 txt 里第一列統(tǒng)計類別。沒有對應 txt 的圖像在 YOLO 訓練時會被靜默跳過可能整個類別都被跳過而你完全沒察覺。3.2 VOC 轉 YOLO坐標歸一化腳本與幾個邊界坑即使壓縮包里已經(jīng)給了 YOLO 格式我仍建議自己做一遍轉換。原因很現(xiàn)實轉存包的 labels 可能和 images 不匹配也可能類別順序和你準備用的訓練框架不一致。自己轉換能確保標注鏈路是可控的。import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image ANNO_DIR Path(Annotations) IMG_DIR Path(JPEGImages) LABEL_DIR Path(labels) LABEL_DIR.mkdir(exist_okTrue) CLASS_NAMES [crazing, inclusion, patches, pitted_surface, rolled_in_scale, scratches] def convert_one(xml_file): root ET.parse(xml_file).getroot() lines [] for obj in root.iter(object): name obj.findtext(name).strip().lower() if name not in CLASS_NAMES: continue cls_id CLASS_NAMES.index(name) box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) cx ((xmin xmax) / 2) / img_w cy ((ymin ymax) / 2) / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h cx, cy, bw, bh [max(0.0, min(v, 1.0)) for v in (cx, cy, bw, bh)] lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) return lines for xml_file in ANNO_DIR.glob(*.xml): with Image.open(IMG_DIR / (xml_file.stem .jpg)) as im: img_w, img_h im.size lines convert_one(xml_file) (LABEL_DIR / (xml_file.stem .txt)).write_text(\n.join(lines), encodingutf-8)這里最關鍵的是 img_w、img_h 用 PIL 從圖像本身讀取而不是直接信任 XML 里的 size。轉存過程中圖像經(jīng)常被重新保存或縮放一旦實際尺寸和 XML 不一致按錯誤尺寸歸一化出的坐標全是偏的但這種偏移在視覺上又不太容易發(fā)現(xiàn)。clip 到 0~1 是雙刃劍。bndbox 越界時把它截斷到邊界能保住訓練不報錯但也掩蓋了標注問題。我建議先不 clip 跑一遍如果 txt 里出現(xiàn)小于 0 或大于 1 的值把對應的 XML 文件名打出來逐個檢查是標注確實越界還是圖像被壓縮過。3.3 按類別分層劃分 train/val防止驗證集缺類1799 張圖聽起來不少但如果隨機劃分六個類別里數(shù)量少的類可能被分到驗證集只有幾張導致那一類在訓練集里完全沒被充分學習。正確做法是按類別分層采樣確保每個類別在訓練集和驗證集的比例基本一致。import random from pathlib import Path from collections import defaultdict random.seed(42) img_files list(Path(images).glob(*.jpg)) label_dir Path(labels) def classes_of(img_path): label_path label_dir / (img_path.stem .txt) if not label_path.exists(): return [] with open(label_path) as f: return sorted({line.split()[0] for line in f if line.strip()}) strata defaultdict(list) for img_path in img_files: cls_list classes_of(img_path) key cls_list[0] if cls_list else empty strata[key].append(img_path) train_ratio 0.8 train_files, val_files [], [] for key, paths in strata.items(): random.shuffle(paths) split int(len(paths) * train_ratio) train_files.extend(paths[:split]) val_files.extend(paths[split:]) print(train:, len(train_files), val:, len(val_files))這段腳本把同一圖像里的多個類別去重后取第一個作為分層鍵。單張圖只有一個缺陷的 NEU-DET 里足夠用。如果發(fā)現(xiàn) empty 分組里有文件說明標注缺失回到 3.1 去查原因。劃分完成后把 train_files 和 val_files 分別按 YOLO 目錄結構放好下一章直接用。4. 用 YOLO 在 NEU-DET 上完成訓練配置文件、命令與評估要點數(shù)據(jù)準備好之后訓練本身反而最順利。用公開的 YOLO 系列框架只要目錄結構和 yaml 沒問題一條命令就能跑起來。重點在于配置文件怎么寫、超參數(shù)怎么選、結果到底看什么指標。4.1 搭建標準目錄結構和 data.yaml按 YOLO 約定數(shù)據(jù)集目錄長這樣dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/訓練前把 3.3 劃分出的文件對應移動或軟鏈進去。labels 下每個 txt 的文件名必須和 images 下同名圖像一致框架是通過 stem 匹配的文件名對不上再好的標注也找不到。下面是 data.yaml# neu_det.yaml path: /path/to/dataset train: images/train val: images/val nc: 6 names: 0: crazing 1: inclusion 2: patches 3: pitted_surface 4: rolled_in_scale 5: scratchespath 建議寫絕對路徑因為相對路徑在不同工作目錄下行為不一致。names 的順序必須和 3.2 里 CLASS_NAMES 完全一致這是 NEU-DET 容易出現(xiàn)標簽錯亂的最主要原因。下載后如果壓縮包自帶的 data.yaml 里順序和標注不一致以標注文件實際的第一列為準。4.2 訓練命令與關鍵超參數(shù)從預訓練權重開始最省事的訓練命令是這樣yolo detect train dataneu_det.yaml modelyolov8n.pt \ epochs300 imgsz640 batch16 device0 cacheTrueNEU-DET 只有 1799 張圖epochs 至少要給到 200 以上否則模型還沒收斂就到頭了。imgsz 我建議 448 或 640不建議降到 320。六個類別里 pitted_surface 和 scratches 屬于典型的細小目標圖像本身只有 200×200再降分辨率會讓麻點和劃痕的像素特征進一步丟失。預訓練權重非常關鍵。從零訓練一個小數(shù)據(jù)集收斂慢而且 mAP 通常不及預期。用在大規(guī)模自然圖像上預訓練過的權重做初始化等于讓模型把“特征提取的基本功”帶過來只學 NEU-DET 的領域差異這在實際實驗中能明顯縮短訓練時間。batch 大小根據(jù)顯存調(diào)整16 在常見顯卡上都能跑batch 太大時 BN 層統(tǒng)計更容易波動。還可以順手打開 cacheTrue把圖像緩存到內(nèi)存數(shù)據(jù)集小所以內(nèi)存壓力不大但能顯著加快每個 epoch 的加載速度。4.3 評估到底看什么單類 AP 比總 mAP 更能發(fā)現(xiàn)問題訓練完后用驗證集評估yolo detect val dataneu_det.yaml \ modelruns/detect/train/weights/best.pt輸出里除了 mAP50 和 mAP50-95一定要看每一類的 Precision、Recall 和 AP。NEU-DET 的 mAP50 普遍不低但這個總分很容易掩蓋單類問題。我見過不少訓練結果總分到了 0.8 以上但 crazing 的 AP 只有 0.3因為 crazing 在驗證集里就幾十張即使全錯對總分影響也很小。更直接的做法是跑一輪批量預測yolo detect predict modelruns/detect/train/weights/best.pt \ sourcedataset/images/val預測結果會保存成帶框的圖片。快速翻一遍這些圖重點看兩類漏檢的真實目標和把背景紋理誤檢成缺陷的假正例。這兩個信息比任何指標都能說明模型下一步該怎么調(diào)。5. NEU-DET 使用中的五大坑與排查思路這一章都是實際跑數(shù)據(jù)時大概率會遇到的問題按現(xiàn)象到原因到解決來寫方便你直接對照。5.1 1799 張圖訓練后 loss 很低但驗證 mAP 上不去過擬合現(xiàn)象訓練集上的 loss 降得很漂亮但驗證集的 mAP 訓練到一半就停滯甚至下降。原因很簡單數(shù)據(jù)量太小模型把訓練集的特征背下來了沒有泛化能力。尤其是網(wǎng)絡層數(shù)較深時這種現(xiàn)象更明顯。解決優(yōu)先加大數(shù)據(jù)增強crazing 和 scratches 這類目標對翻轉、旋轉不敏感可把 fliplr 開到 0.5mosaic 開滿其次用預訓練權重而不是從零訓練再不行就換更小的模型比如直接上 n 系列而不是一上來就用 s 或 m。最后把 epochs 拉長配合早停讓它自己停在泛化最好的位置。5.2 crazing 與 patches 的 Precision 長期為 0類別不均衡與標注粒度現(xiàn)象訓練完看 per-class 指標這兩類的 AP 明顯低于其他四類有時 Precision 直接是 0。原因是這兩個類別的樣本量在數(shù)據(jù)集中偏少而且 crazing 的網(wǎng)狀裂紋和背景紋理高度相似模型傾向把所有帶紋理的區(qū)域都當成背景。解決思路分兩步先看統(tǒng)計確認哪一類樣本最少如果少到幾十張級別考慮對這種缺陷做離線復制增強把框內(nèi)區(qū)域隨機貼到背景圖合成新樣本然后檢查標注框是否太大crazing 的紋理區(qū)域往往是大框套著細小裂紋標注框過大會讓模型學到大量背景。減小這類框的粒度往往比加數(shù)據(jù)更有效。5.3 轉換后提示 no labels found坐標越界與目錄層級現(xiàn)象訓練命令一跑日志提示某個圖像沒有找到對應 labels或者直接報錯找不到標簽。原因通常是兩種一是圖像和 txt 不在預期的 labels/train 目錄下框架默認在 images 同級找 labels目錄層級錯了就找不到二是轉換出來的 txt 里坐標越界比如框的寬高算出來是負數(shù)部分框架會認為這是非法標注。解決先用 3.1 的校驗邏輯檢查每個 txt 是否有有效內(nèi)容再確認 labels 下的文件和 images 下文件一一對應最后打開一個 txt 看數(shù)字是否都在 0~1 區(qū)間如果出現(xiàn)負值回到轉換腳本檢查 xmax 是否小于 xmin這種問題常見于標注框在被壓縮后翻轉的情況。5.4 預測類別張冠李戴類別 id 順序不一致現(xiàn)象模型訓練沒有報錯也收斂了但預測出來的框類別和圖像內(nèi)容對不上比如把氧化皮預測成夾雜物。終極原因只有一個訓練時 CLASS_NAMES 的順序和 data.yaml 里 names 的順序不一致。VOC 轉 YOLO 時你按一個順序給類別編號訓練時框架按照另一個順序讀取類別名同一個數(shù)字在兩個文件里代表不同缺陷。解決把兩張表并排打印出來人工核對。我習慣把類別順序?qū)戇M一個固定文件轉換腳本、data.yaml、訓練輸出三處都從這個文件讀取徹底消滅手寫不一致的可能。5.5 訓練效果換到產(chǎn)線現(xiàn)場就失效域差異現(xiàn)象NEU-DET 上 mAP 很高但拿到自己現(xiàn)場的鋼板圖像上一測掉點嚴重。原因是數(shù)據(jù)集本身來自特定產(chǎn)線、特定光照和特定拍攝條件灰度紋理風格比較單一而現(xiàn)場圖像的分辨率、角度、氧化程度都不一樣。這不算數(shù)據(jù)集的坑而是使用方式的問題。解決把 NEU-DET 當預訓練和流程驗證基準不要指望它替你覆蓋現(xiàn)場域。常見做法是加載在 NEU-DET 上訓練好的權重用現(xiàn)場采集的一兩千張真實缺陷圖做微調(diào)哪怕標注粗糙一些效果也遠好于直接用 NEU-DET 權重做推理。6. 不換模型也能漲點的三個 NEU-DET 進階技巧數(shù)據(jù)只有 1799 張但有些技巧能在不換網(wǎng)絡結構的前提下把 mAP 再往上推一點。下面三個是我常用的。第一個是偽標注自訓練。用訓練好的 best.pt 對同一來源但未標注的圖像做推理把置信度高的預測框直接當標注加入訓練集再訓一輪。要注意只挑選置信度大于 0.9 且面積合理的框人工掃一眼抽查幾張。這個方法在小數(shù)據(jù)集上通常能帶來穩(wěn)定提升相當于用模型自己擴充了訓練數(shù)據(jù)。第二個是灰度增強副本。NEU-DET 圖像本質(zhì)是灰度紋理但很多訓練框架對灰度圖會做三通道復制本身沒有額外信息。我習慣把 CLAHE 或直方圖均衡化后的圖像作為第二個版本和原始圖混合進訓練強迫模型對光照和對比度變化更魯棒。操作上不需要改代碼提前跑個批處理生成增強副本放進 images/train 即可。第三個是推理時多尺度融合。因為原始圖像是 200×200輸入 640 本身已經(jīng)是放大三倍多模型對尺度變化比較敏感。推理時可以用 TTA 或三尺度推理對三次預測結果做 NMS 合并。這一招對小目標類別特別有效代價是推理時間增加離線批量檢測場景完全劃得來。就這個數(shù)據(jù)集而言我最后悔的事情是早期只看總 mAP直到某次把 best.pt 放到現(xiàn)場圖像上才發(fā)現(xiàn) crazing 全線漏檢。后來養(yǎng)成習慣每次訓練完先翻每個類的壞例圖再決定要不要調(diào)參。這個習慣幫我少走了很多彎路希望也能幫到你。本文還有配套的精品資源點擊獲取