據(jù)集解析:VOC/YOLO雙格式與訓練避坑指南)
我今年接了好幾個車載視覺相關(guān)的項目每次聊到“道路拋灑物檢測”這個方向?qū)Ψ降谝痪湓捇径际菃栍袥]有現(xiàn)成的數(shù)據(jù)集可以用來做預訓練或者評測確實這個場景有點尷尬——通用目標檢測數(shù)據(jù)集里幾乎沒有專門針對拋灑物的即便有類別和格式往往也是亂七八糟。所以當我看到這份“道路拋灑物數(shù)據(jù)集4521張VOCYOLO格式”時第一反應(yīng)是終于有一份能直接用來干活的數(shù)據(jù)了。這篇文章不打算做那種“數(shù)據(jù)集說明文檔”式的復讀我想從實際落地的角度把這份數(shù)據(jù)集從目錄結(jié)構(gòu)、標注格式、轉(zhuǎn)換腳本到訓練前的檢查清單完整拆一遍。順便把我在類似數(shù)據(jù)集上踩過的坑和驗證方法也一并寫出來——當你手里拿到一份標注好的數(shù)據(jù)集真正的功課才剛剛開始。1. 為什么道路拋灑物檢測是“看著簡單做起來棘手”的視覺任務(wù)1.1 拋灑物檢測與通用目標檢測的差距在哪路面上的拋灑物聽起來就是“在圖像里框出物體”好像和行人檢測、車輛檢測沒什么本質(zhì)區(qū)別。但真正做進去就會發(fā)現(xiàn)這個任務(wù)是典型的高難度垂直場景。第一個難點是目標形態(tài)極度不固定。一輛貨車上掉下來的紙箱、輪胎皮、木板、塑料袋、散落的砂石它們在視覺上沒有統(tǒng)一的外觀。你不可能像檢測行人那樣依靠“有頭有肩”的先驗結(jié)構(gòu)拋灑物更多是“路面背景上的一塊異樣物體”。很多時候連人眼都要盯著看好一會才能確認那是不是拋灑物更別說模型了。第二個難點是目標尺度普遍偏小。在高速或者快速路的視頻畫面里拋灑物真正占據(jù)的像素往往只有幾十個甚至十幾個像素。小目標檢測本身是所有檢測任務(wù)里最難啃的骨頭之一而拋灑物恰好扎堆在小尺度區(qū)間。第三個難點是背景干擾。瀝青路面的裂縫、陰影、輪胎剎車印、水漬反光這些視覺特征和某些拋灑物在紋理、顏色上高度接近。模型很容易把路面的老化裂紋當成物體框出來或者反過來把一塊黑色的輪胎皮當成路面的一部分放過去。1.2 這種數(shù)據(jù)集能支撐什么方向的研究與應(yīng)用拋灑物檢測的落點其實很明確高速公路監(jiān)控、隧道內(nèi)的異物檢測、養(yǎng)護巡邏車的車載輔助識別。前兩年這個需求更多是“錦上添花”但現(xiàn)在已經(jīng)變成一種偏剛需的能力——畢竟拋灑物導致的事故率擺在那里無論是路政養(yǎng)護還是車路協(xié)同大家都在想辦法把發(fā)現(xiàn)時間從“人工巡查發(fā)現(xiàn)”壓縮到“攝像頭自動識別”。從算法研究的角度這類數(shù)據(jù)也能支撐不少有意思的方向。比如小目標檢測的評測或者在強光照、逆光、夜間條件下檢測魯棒性的研究。還有一些團隊會用這類數(shù)據(jù)做“異常事件檢測”——不是識別某個具體類別而是檢測路面上“出現(xiàn)了不應(yīng)該出現(xiàn)的東西”。所以這份4521張的數(shù)據(jù)集放在通用檢測領(lǐng)域不算大但在拋灑物這種垂直場景里已經(jīng)是能支撐模型訓練和評估的規(guī)模。關(guān)鍵在于拿到之后怎么用、怎么驗證、怎么避開格式轉(zhuǎn)換和標注噪聲這些坑。2. 數(shù)據(jù)集結(jié)構(gòu)拆解VOC與YOLO兩套目錄其實是一份標注的兩副面孔2.1 VOC風格目錄長什么樣拿到數(shù)據(jù)集的第一件事不是打開圖片看而是先把目錄結(jié)構(gòu)摸清楚。VOC格式的目錄組織方式來自一個很經(jīng)典的公開數(shù)據(jù)集結(jié)構(gòu)它的核心是圖片和標注文件分離存放再由索引文件控制哪些圖片參與訓練或驗證。一個標準的VOC風格目錄大致是這樣的dataset_root/ ├── JPEGImages/ │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... ├── Annotations/ │ ├── 000001.xml │ ├── 000002.xml │ └── ... └── ImageSets/ └── Main/ ├── train.txt ├── val.txt └── test.txtJPEGImages放原始圖片Annotations放對應(yīng)的XML標注文件ImageSets/Main下的txt文件記錄訓練、驗證、測試的文件名列表。文件名的對應(yīng)關(guān)系很直接000001.jpg對應(yīng)000001.xml如果這兩邊數(shù)量對不上或者名字對不上后面訓練時就會報錯。2.2 YOLO風格目錄長什么樣YOLO格式的數(shù)據(jù)集目錄則是另一套邏輯通常長這樣dataset_root/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml注意這里的差異YOLO風格不是用txt索引文件來劃分數(shù)據(jù)而是直接把圖片和標注分別扔進train/val/test子目錄。images下放圖labels下放同名、同路徑的txt標注文件。data.yaml是配置文件里面寫清楚訓練集、驗證集路徑以及類別名列表?!癡OCYOLO格式”的含義就是同一批標注內(nèi)容同時提供了XML和TXT兩套表示。有時候兩套標注是獨立維護的有時候是轉(zhuǎn)換生成的這個不確定所以拿到手第一步就應(yīng)該做兩種格式的交叉驗證。2.3 4521張圖片的規(guī)模與數(shù)據(jù)劃分從規(guī)模來推斷4521張對道路拋灑物任務(wù)來說處于“實用級”。如果按常見的8:1:1比例劃分訓練集大概3600張左右驗證集和測試集各四五百張。應(yīng)對單類或者少量類別的檢測任務(wù)這個量是夠訓練出可用模型的但如果類別很多、每類樣本極不均衡那訓練時就要靠加權(quán)采樣之類的技巧來彌補。我一般拿到這種規(guī)模的數(shù)據(jù)集會做兩件事。第一把所有圖片和標注按文件名排序做一個全量的對應(yīng)關(guān)系核對確保沒有孤兒圖片或者孤兒標注。第二把劃分后的txt文件全部讀一遍確認里面記錄的是文件名還是完整路徑——這兩種寫法在后續(xù)寫數(shù)據(jù)加載邏輯時完全不同。3. VOC標注里最容易看走眼的幾個細節(jié)3.1 讀懂一個標準XML的全部字段VOC格式的XML標注文件結(jié)構(gòu)清晰但字段瑣碎。隨手打開一份大概是這樣的annotation folderJPEGImages/folder filename000001.jpg/filename source databaseUnknown/database /source size width1920/width height1080/height depth3/depth /size segmented0/segmented object nametire/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin1356/xmin ymin784/ymin xmax1512/xmax ymax902/ymax /bndbox /object /annotation有幾個字段必須重視。filename有時候會帶著子目錄前綴有時候只有文件名以它為基準和圖片目錄配對時格式不一致就會出問題。size里的寬高非常關(guān)鍵和圖片實際尺寸不符時會造成坐標錯亂。object這個節(jié)點是真正意義上的標注本體。name是類別名bndbox里是目標的邊界框四個數(shù)值都是像素坐標左上角的(xmin, ymin)和右下角的(xmax, ymax)。這個坐標系是圖片自身的像素坐標系原點在左上角。3.2 哪些字段會影響訓練結(jié)果truncated和difficult這兩個標記很多人會忽略但它們在不同訓練框架里的處理邏輯完全不同。truncated表示目標是否被圖像邊緣截斷。路面上很大一部分拋灑物其實是“半截”狀態(tài)比如一個大木板只有一部分出現(xiàn)在畫面里所以這類數(shù)據(jù)在真實場景中占比不低。difficult表示目標是否難以辨認。在部分VOC時代的檢測框架里difficult為1的樣本在計算mAP時會被排除。但在現(xiàn)代深度學習訓練里很多框架根本不看這個字段而是把所有非背景的框全部拿來參與訓練。問題就出在這如果你用的框架會讀取difficult并做處理而另一套工具不讀同一個XML在兩套環(huán)境下就會產(chǎn)生完全不同的訓練樣本集合。所以建議拿到VOC格式的數(shù)據(jù)后先寫個小腳本統(tǒng)計一下所有XML里truncated和difficult的取值分布確認這批數(shù)據(jù)里困難樣本的占比。如果是像我之前遇到的那個項目訓練過程loss波動特別大回頭一查罪魁禍首就是一大批difficult1的樣本它們把小目標檢測結(jié)果攪得亂七八糟。4. VOC轉(zhuǎn)YOLO的完整操作與反向驗證4.1 轉(zhuǎn)換公式從像素坐標到歸一化中心坐標很多數(shù)據(jù)集的VOC和YOLO格式并不是同步生成的有的甚至不是同一批人標的。所以拿到手最穩(wěn)妥的做法是自己把VOC轉(zhuǎn)成YOLO再和已有的YOLO標注逐行比對確認兩邊一致。轉(zhuǎn)換的數(shù)學公式很基礎(chǔ)但單位搞錯就會全線崩盤。VOC的bndbox是像素坐標YOLO的txt里需要的是歸一化后的中心點坐標和寬高公式如下x_center (xmin xmax) / 2.0 / width y_center (ymin ymax) / 2.0 / height box_width (xmax - xmin) / width box_height (ymax - ymin) / height這里width和height必須來自XML里的size節(jié)點而不是代碼里猜測的圖片尺寸。雖然兩者應(yīng)該相等但為了防止XML和實際圖片尺寸不一致我最常用的是直接從XML讀取寬高——既然標注是在那套尺寸坐標系里畫的轉(zhuǎn)換時就用那套坐標系。4.2 一個可以直接跑的轉(zhuǎn)換腳本下面這個腳本是我在幾個項目里改來改去留下來的版本夠用且不啰嗦。它遍歷指定目錄下所有XML文件為每個XML生成對應(yīng)的txt文件并自動創(chuàng)建一個classes.txt用于記錄類別順序。import os import xml.etree.ElementTree as ET from glob import glob def voc_to_yolo(xml_file, output_dir, classes_map): tree ET.parse(xml_file) root tree.getroot() base os.path.basename(xml_file).replace(.xml, .txt) lines [] # size字段是轉(zhuǎn)換的關(guān)鍵基準必須和圖片實際分辨率一致 size root.find(size) width float(size.find(width).text) height float(size.find(height).text) for obj in root.iter(object): name obj.find(name).text if name not in classes_map: classes_map[name] len(classes_map) class_id classes_map[name] bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 做一次越界檢查坐標不能超出圖片范圍 if xmin 0 or ymin 0 or xmax width or ymax height: print(fWarning: {xml_file} box out of range: {xmin},{ymin},{xmax},{ymax}) x_center (xmin xmax) / 2.0 / width y_center (ymin ymax) / 2.0 / height box_w (xmax - xmin) / width box_h (ymax - ymin) / height lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(os.path.join(output_dir, base), w) as f: f.write(\n.join(lines) \n) xml_dir Annotations yolo_label_dir labels os.makedirs(yolo_label_dir, exist_okTrue) classes_map {} for xml_file in glob(os.path.join(xml_dir, *.xml)): voc_to_yolo(xml_file, yolo_label_dir, classes_map) with open(classes.txt, w) as f: f.write(\n.join(classes_map.keys()))有幾個坑我在代碼注釋里標了但還想單獨強調(diào)一下。第一width和height必須用XML里的值不要用代碼猜。第二當某個框的xmin出現(xiàn)負數(shù)或者xmax超過圖片寬度時這份標注要么是注釋坐標本身有錯誤要么是這張圖片被resize過而沒有更新XML。無論哪種情況都需要單獨處理不能直接往下游用。4.3 驗證是否轉(zhuǎn)換成功畫框和反向比對轉(zhuǎn)換完不是看一眼就說“好了”。我每次都會做兩層驗證。第一層是可視化驗證。選十幾張圖用OpenCV把txt里的歸一化坐標換算回像素坐標畫框后和原圖疊加。這一步主要看人眼直覺框是不是貼合目標、有沒有張冠李戴。第二層是數(shù)值比對。如果數(shù)據(jù)集里已經(jīng)自帶YOLO標注就寫個簡單腳本把轉(zhuǎn)換出的結(jié)果和原始TXT做逐行比較。因為浮點數(shù)精度問題直接比較相等可能太嚴格我通常用“每個框的重疊度大于0.98”作為一致的標準。兩套標注只有像素級差異的話基本可以接受。5. 訓練前的檢查清單避免在啟動訓練后才發(fā)現(xiàn)問題5.1 文件名對應(yīng)關(guān)系與空標簽檢查很多人上來直接跑訓練然后撞見“No labels found in xxx.jpg”這類報錯再回頭排查幾個小時就沒了。所以我把訓練前的檢查整理成了一張可以照著執(zhí)行的清單。第一項圖片與標簽一一對應(yīng)。對VOC目錄來說就是JPEGImages下的文件名集合與Annotations下的文件名集合完全一致對YOLO目錄來說就是images子目錄下的文件名集合與labels子目錄下的文件名集合完全一致。有任何一個文件只有圖片沒有標注或者只有標注沒有圖片都要先處理掉。第二項空標簽文件。YOLO訓練里一個正常的目標檢測項目中圖片里可能確實沒有任何目標這時候txt文件為空。如果你的任務(wù)要求每張圖都至少有一個目標那空標簽就意味著漏標。但拋灑物場景存在真實的負樣本圖片也就是路面上沒有拋灑物的正常畫面這些是有用的負樣本訓練空標簽是合理的要看數(shù)據(jù)集的初衷。第三項損壞圖片檢測。用PIL或者OpenCV批量讀取一遍所有圖片捕獲“不能解碼”的異常。出現(xiàn)損壞圖像時直接把該圖對應(yīng)的標注一并移出數(shù)據(jù)集不要抱僥幸心理。5.2 類別統(tǒng)計與目標數(shù)量統(tǒng)計接下來我要統(tǒng)計的目標分布是我判斷這個數(shù)據(jù)集質(zhì)量的重要指標。import os from collections import Counter label_dir labels counter Counter() empty_files [] total_boxes 0 for txt_file in os.listdir(label_dir): filepath os.path.join(label_dir, txt_file) with open(filepath, r) as f: lines f.readlines() if len(lines) 0: empty_files.append(txt_file) continue for line in lines: parts line.strip().split() if len(parts) 5: class_id parts[0] counter[class_id] 1 total_boxes 1 else: print(fBad line in {txt_file}: {line.strip()}) print(類別統(tǒng)計:, dict(counter)) print(總目標數(shù):, total_boxes) print(空標簽數(shù)量:, len(empty_files))這里有幾個經(jīng)驗判斷可以參考。如果總目標數(shù)除以總圖片數(shù)遠小于1比如平均每張圖連0.5個目標都沒有那這些圖片大部分是純背景如果遠大于3說明場景里拋灑物密集程度很高。這兩種數(shù)據(jù)分布訓練出來的模型行為完全不同。還需要檢查類別編號的連續(xù)性。YOLO格式里類別編號必須是從0開始的連續(xù)整數(shù)如果有類別列表是[tire,board,stone]那編號就是0、1、2對應(yīng)txt每行開頭的數(shù)字。如果classes.txt里寫了3個類別但txt里出現(xiàn)了編號5那轉(zhuǎn)換腳本一定有問題。5.3 一次性檢查圖片尺寸和分辨率分布這個檢查容易被忽略。磚頭大小的拋灑物在1080p畫面里可能占50x50像素但在720p畫面里可能只剩30x30像素訓練時模型看到的特征信息量完全不同。如果這批數(shù)據(jù)的圖片分辨率不統(tǒng)一標注的絕對像素坐標也缺少可比性。寫個腳本遍歷所有圖片統(tǒng)計寬高的分布。如果發(fā)現(xiàn)大部分是1920x1080少部分是1280x720甚至更低建議要么統(tǒng)一resize到一個固定的訓練尺寸要么在訓練配置里把輸入分辨率設(shè)到接近最小圖片的合理值避免代碼在resize時把標注坐標也改變了。6. 訓練和實際部署時的經(jīng)驗補充6.1 針對這種規(guī)模的訓練配置思路關(guān)于訓練參數(shù)我不直接給一組死數(shù)據(jù)因為類別數(shù)、圖片分辨率不同最優(yōu)值也不同。但有幾個經(jīng)驗原則是通用的。對于4521張這個規(guī)模訓練時不需要像用百萬級數(shù)據(jù)那樣開很大的epoch。我常用的做法是初始設(shè)100到150個epoch配合早停。如果loss在20到30個epoch后不再下降就停掉不要硬跑。輸入尺寸方面如果數(shù)據(jù)集中存在大量小目標就不要把訓練尺寸壓得太低。把訓練尺寸定在960甚至1280會顯著提高小目標的召回率代價是顯存吃緊、訓練變慢。我的建議是可以先用640跑通全流程確認沒有bug之后再往上調(diào)尺寸。數(shù)據(jù)增強要克制。對拋灑物這個場景HSV色彩抖動和輕微的尺度抖動比較有用因為光照和拍攝距離對路面影響很大。但過度使用翻轉(zhuǎn)和旋轉(zhuǎn)要小心——當然拋灑物的形態(tài)不像文字那樣有強方向性翻轉(zhuǎn)基本安全但旋轉(zhuǎn)超過30度之后目標會變得很假模型在真實場景里反而吃虧。6.2 部署端的思考處理好“漏檢”與“誤報”的平衡訓練完模型真正落地時還有一道坎。道路拋灑物檢測的部署位置大多是路側(cè)監(jiān)控或養(yǎng)護車車載攝像頭這兩種場景對誤報的容忍度差異很大。路側(cè)監(jiān)控視野固定算法誤報可以在時間維度上做平滑過濾比如連續(xù)多幀都出現(xiàn)同個位置的檢測框才觸發(fā)告警。車載場景畫面一直在動上一幀和下一幀的空間關(guān)聯(lián)弱多幀確認的閾值和策略要重新設(shè)計誤報閾值往往要調(diào)得更高一些寧可放過一些不確定的目標也不能每過一輛車就響一次警報。另外部署模型時優(yōu)先考慮輕量版本。拋灑物檢測通常是輔助功能和車輛檢測、車牌識別等任務(wù)共用算力。如果主干網(wǎng)絡(luò)太重會擠占其他模塊的資源。我的經(jīng)驗是主干能小則小主要通過提高輸入分辨率和合理的訓練策略來保精度而不是靠模型體量硬扛。這也是這類檢索定位評測數(shù)據(jù)集的價值所在——可以作為權(quán)衡模型輕量化和精度的標準基準。最后再分享一個個人習慣拿到任何一份VOC和YOLO雙格式的數(shù)據(jù)集我第一件事不是直接訓練而是把兩套格式做交叉校驗然后畫幾十張可視化圖出來把每一類拋灑物都人工掃一遍。雖然這步要花掉一兩個小時但它能幫你提前發(fā)現(xiàn)類別標簽分配錯誤、框偏移、漏標、誤標這些后續(xù)最難定位的問題。這個習慣幫我省下的排錯時間遠比檢查本身多得多。