據(jù)集:VOC與YOLO雙格式解析及訓練前避坑指南)
簡介RSNA肺炎檢測數(shù)據(jù)集面向醫(yī)學影像目標檢測方向的學習者與開發(fā)者圍繞RSNA胸部影像場景整理數(shù)據(jù)規(guī)模為6012張圖片、單一類別meat、9555個目標框適合直接用于YOLO、Faster R-CNN等主流檢測框架的訓練與評估。數(shù)據(jù)提供Pascal VOC與YOLO兩種格式的標注描述便于在主流檢測框架間直接復用所有標注由labelImg按矩形框完成規(guī)則統(tǒng)一。資源包采用7z壓縮大小約886.78MB文件總數(shù)為2000主要包含1999個VOC格式XML標注文件與1個TXT使用說明文件可配套6012張影像完成目標檢測實驗壓縮包內文件以標注為主體便于使用者快速了解數(shù)據(jù)構成并按需劃分訓練集和驗證集。目前已有594人學習瀏覽適合有一定深度學習基礎、需要標準醫(yī)學影像目標檢測數(shù)據(jù)集的開發(fā)者快速上手。需注意數(shù)據(jù)集僅保證標注準確、格式合理不對訓練所得模型或權重文件精度作任何承諾。1. RSNA肺炎檢測數(shù)據(jù)集VOCYOLO雙格式、6012張圖、9555個框拿來就能直接用如果你在網(wǎng)上搜“RSNA肺炎檢測數(shù)據(jù)集”大概率會看到一堆涉及DICOM和肺結節(jié)檢測的原始CT資料但真正拿到就能訓練的往往不是原始影像。我這次拆的這份資源是一個已經(jīng)被預處理并標注好的數(shù)據(jù)集6012張JPG圖片配套6012個Pascal VOC XML和6012個YOLO TXT標注文件總共9555個矩形框類別只有1個——標注工具里寫的是meat。說實話第一眼看到類名我也愣了一下但核對了圖片和框以后發(fā)現(xiàn)它是RSNA肺炎病灶/實變區(qū)域只是原作者在labelImg里沿用了一個預設標簽名。這份資源適合誰適合想跳過標注、直接驗證YOLO各版本效果的人也適合做肺炎檢測入門、遷移學習的工程人員。接下來我會從文件結構講到轉換腳本、訓練前的坑。2. VOC與YOLO雙格式的底細目錄結構、XML字段和歸一化坐標2.1 目錄結構同名文件和三種后綴的對應關系一打開壓縮包你會看到這是一個大平鋪目錄沒有常見的images/、labels/、Annotations/三層結構。所有文件都在一個根目錄下文件名統(tǒng)一是firc_rsna_帶編號比如firc_rsna_1010.jpg、firc_rsna_1010.xml、firc_rsna_1010.txt。文件名的數(shù)字其實就是原RSNA數(shù)據(jù)集的某個編號用途不大關鍵在于三者后綴一一對應。文件類型數(shù)量作用.jpg6012訓練圖片RGB通道尺寸不一.xml6012Pascal VOC格式標注含絕對坐標.txt6012YOLO格式標注含歸一化坐標另外根目錄還有一個使用前必讀.txt里面通常寫明格式說明、標注工具和免責聲明。注意YOLO txt文件里只有類別和坐標沒有圖片路徑。也就是說它“不包含分割路徑的txt文件”不像有些數(shù)據(jù)集會在每行開頭寫images/img_0001.jpg這里就是純數(shù)字。這意味著你要自行決定怎么給YOLO訓練組織目錄。如果你用ls看數(shù)量會發(fā)現(xiàn)jpg 6012、xml 6012、txt 6012完全一致說明這張數(shù)據(jù)集里每一張圖都標了框沒有純背景圖??偪驍?shù)9555類別唯一meat折合平均每張圖有1.59個框。很多RSNA病灶不止一個實變區(qū)這很合理。2.2 VOC XML從filename到bndbox的字段逐項解讀隨便挑一個xml打開比如firc_rsna_1010.xml內容長這樣annotation folderRSNA/folder filenamefirc_rsna_1010.jpg/filename pathD:/datasets/rsna/firc_rsna_1010.jpg/path source databaseUnknown/database /source size width1024/width height1024/height depth3/depth /size segmented0/segmented object namemeat/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin210/xmin ymin95/ymin xmax486/xmax ymax355/ymax /bndbox /object /annotation上面是針對結構做的示例展示實際數(shù)值以壓縮包解出來的為準。這個格式就是labelImg默認保存的Pascal VOC格式。幾個關鍵字段filename圖片文件名必須和實際jpg完全一致訓練腳本會拿它拼接圖片路徑。size寬高和通道數(shù)YOLO歸一化時要用它。object每個目標一個塊。name是類別名這里全為meatdifficult為1時可忽略這里基本都是0。bndbox矩形框的左上角和右下角絕對像素坐標。注意xmax、ymax是包含在框內的計算寬高時一般用xmax - xmin不需要加1OpenCV畫框時也一樣。這份數(shù)據(jù)里一個xml可能包含多個object因為同一張圖有多個病灶區(qū)域時需要畫多個框。讀取時要把每一個都解析出來不能只看第一個。2.3 YOLO TXT歸一化坐標的計算與還原再打開對應的firc_rsna_1010.txt內容類似0 0.3398 0.2197 0.2695 0.2539這是按上面xml的框換算出來的。五個數(shù)字分別代表類別ID、歸一化后的中心點x、中心點y、框寬度w、框高度h。類別ID從0開始所以0對應meat后續(xù)如果有多個類別就是0、1、2……。歸一化的公式很簡單x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height box_width (xmax - xmin) / width box_height (ymax - ymin) / height用上面VOC示例數(shù)據(jù)算一次圖片寬高1024×1024框210,95,486,355那么x_center就是0.3398y_center是0.2197寬高分別是0.2695和0.2539正好對應txt里的內容。當你用YOLO訓練時模型讀入的就是這些歸一化數(shù)字。還原成絕對坐標只需要乘回去xmin int((x_center - box_width / 2) * width) ymin int((y_center - box_height / 2) * height) xmax int((x_center box_width / 2) * width) ymax int((y_center box_height / 2) * height)注意這里存在精度損失txt只保留四位小數(shù)畫框驗證時會有1~2像素誤差屬于正常。如果保留6位小數(shù)誤差更小。這份數(shù)據(jù)集的txt應該是保留了一定精度但還是建議驗證時直接用xml還原絕對坐標最穩(wěn)。很多老訓練腳本只吃VOC新框架往往只要YOLO。雙格式意味著你不需要再轉換但由此帶來的一個坑就是如果兩份標注出現(xiàn)不一致你以哪份為準后文會講到。2.4 用bash快速核對文件數(shù)量和類別分布解壓后第一步我建議先用命令行做個快速體檢確認文件數(shù)沒少ls *.jpg | wc -l ls *.xml | wc -l ls *.txt | wc -l三條命令分別輸出jpg、xml、txt的數(shù)量正常都應該是6012。如果某個數(shù)字偏少說明解壓不完整后面所有訓練流程都要停下來。接著可以統(tǒng)計一下類別標簽是否有異常grep -h name *.xml | sort | uniq -c正常會看到只有一行6012 namemeat/name注意因為一個xml可能有多個object總數(shù)會是9555但名稱只會是meat。如果出現(xiàn)別的類名說明標注時混入了其他類別需要用后文的class_map統(tǒng)一清洗。這種極簡檢查成本不到十秒能避免后面帶著錯誤標注跑一整天。3. 從VOC到YOLO再劃分訓練集轉換腳本、隨機種子與邊界排查3.1 既然給了雙格式為什么還要自己轉換你可能會想“txt都有了不用轉換直接開訓。” 實際工程里這不夠。這三個場景很常見想把類別meat改成pneumonia或中文名需要同時改xml和txt。只改txt很簡單但改完兩邊不一致后面驗證會互相打架。想按自己的比例劃分train/val/test而且想讓每個類別在劃分后比例不過度傾斜。想重新生成YOLO txt避免原始txt因xml尺寸記錄錯誤而整體偏移。所以我把轉換腳本當作一個“清洗動作”而不是先去轉換。常見做法是讀取xml解析出標準VOC結構再重新生成txt。這樣能保證兩份標注的源頭是同一個。3.2 Python腳本XML解析、坐標歸一化與TXT生成下面是一個單文件轉換腳本輸入一個xml路徑輸出對應的yolo txt。我在多個數(shù)據(jù)集上用過類似結構。import xml.etree.ElementTree as ET import os def voc_xml_to_yolo_txt(xml_path, txt_path, class_map): tree ET.parse(xml_path) root tree.getroot() # 取圖片真實尺寸 size root.find(size) width int(size.find(width).text) height int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_map: continue # 跳過未映射的類別 class_id class_map[name] bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 歸一化 x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height box_w (xmax - xmin) / width box_h (ymax - ymin) / height # 鉗制到[0,1]避免標出圖外 x_center min(max(x_center, 0), 1) y_center min(max(y_center, 0), 1) box_w min(box_w, 1 - x_center) box_h min(box_h, 1 - y_center) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) if __name__ __main__: class_map {meat: 0} # 只有一個類別ID為0 voc_xml_to_yolo_txt(firc_rsna_1010.xml, firc_rsna_1010.txt, class_map)邏輯說明用ElementTree快速解析XML不依賴第三方庫。先拿到size算歸一化基準再遍歷每個object把name映射成類別ID。計算中心點和寬高時全部用float參與避免整數(shù)除法的精度損失。最后三行是邊界保護用于處理標注框超界的情況。這種超界在這一類數(shù)據(jù)集里偶爾會出現(xiàn)生成后最好再反向驗證一遍。參數(shù)說明class_map決定了類別順序如果將來換成多類別把字典按自己的規(guī)則填即可生成txt時第一列會自然從0遞增。文本保留6位小數(shù)比常見的4位更穩(wěn)雖然實際差異不大。我用min(max(...))作鉗制時要注意一個極端如果原框中心點越界太夸張鉗制后框會變成另一個位置所以最好在鉗制之前先打印警告而不是靜默吞掉。更穩(wěn)妥的做法是先判斷xmin 0 and ymin 0 and xmax width and ymax height不滿足就跳過兼顧后續(xù)統(tǒng)計。很多剛接觸YOLO的人收到整數(shù)坐標就會懷疑“位置怎么變了”其實沒有只是被歸一化了。這個腳本跑完以后生成的txt與原始txt在數(shù)值上應該高度吻合只存在進位誤差。3.3 劃分數(shù)據(jù)train/val/test 比例與穩(wěn)定隨機種子有了清洗后的標注文件下一步要把數(shù)據(jù)集分到images/train、images/val、images/test以及對應的labels/目錄。YOLO官方Ultralytics等框架通常要求圖片和標簽放在平行目錄里。常見做法是先收集所有jpg按文件名stem找對應txt然后隨機打亂。import os import random import shutil src_root ./rsna_dataset # 解壓后的根目錄 dst_root ./yolo_dataset # 輸出目錄 split_ratio {train: 0.8, val: 0.1, test: 0.1} random.seed(42) # 找出所有jpg文件確保都有對應的xml和txt jpegs [f for f in os.listdir(src_root) if f.endswith(.jpg)] valid [] for jpg in jpegs: stem os.path.splitext(jpg)[0] xml_path os.path.join(src_root, stem .xml) txt_path os.path.join(src_root, stem .txt) if os.path.exists(xml_path) and os.path.exists(txt_path): valid.append(stem) random.shuffle(valid) # 計算邊界 n len(valid) n_train int(n * split_ratio[train]) n_val int(n * split_ratio[val]) for split_name, stems in [ (train, valid[:n_train]), (val, valid[n_train:n_train n_val]), (test, valid[n_train n_val:]) ]: img_out os.path.join(dst_root, images, split_name) lbl_out os.path.join(dst_root, labels, split_name) os.makedirs(img_out, exist_okTrue) os.makedirs(lbl_out, exist_okTrue) for stem in stems: shutil.copy(os.path.join(src_root, stem .jpg), os.path.join(img_out, stem .jpg)) shutil.copy(os.path.join(src_root, stem .txt), os.path.join(lbl_out, stem .txt))邏輯說明先做一次“雙保險”校驗只有jpg、xml、txt三者齊全才進入劃分。random.seed(42)固定了偽隨機順序保證每次執(zhí)行得到相同劃分這是訓練可復現(xiàn)的基礎。注意我用的是復制而不是移動這樣原壓縮包解出來的原始數(shù)據(jù)還保留著出了錯有后悔藥。如果你磁盤緊張用os.replace改成移動也可以但建議先在副本上測試。邊界處的n_train n_val是val的結束下標最后一個valid[n_trainn_val:]就是剩下的test由于比例都是0.1幾乎不會丟樣本。參數(shù)說明split_ratio可以按數(shù)據(jù)集規(guī)模調整。6012張圖0.8/0.1/0.1會得到4809/601/602左右val/test略少對單類任務已經(jīng)夠用。如果要做K折實驗把這里改成循環(huán)種子換成不同值即可。注意random.seed要放在shuffle前面才有效放在后面等于沒設置。另外由于這份數(shù)據(jù)只有一個類別按純隨機劃分基本能保證類別均衡。如果換成多類別最好再用sklearn.model_selection.train_test_split做stratify。這里就不展開了。3.4 為YOLO訓練準備config.yaml劃分完目錄訓練前還需要一個yaml配置。Ultralytics的YOLO訓練會讀取這個文件定義數(shù)據(jù)路徑、類別數(shù)量和類別名。path: ./yolo_dataset train: images/train val: images/val test: images/test nc: 1 names: [meat]邏輯說明path填上面腳本生成的dst_roottrain、val、test填相對path的圖片目錄。yolo會自動在同一級目錄下找同名的labels目錄所以images/train旁邊的labels/train不需要額外配置。nc必須是1names長度與nc一致。如果你把類別名改成了pneumonia這里就要同步寫[pneumonia]否則訓練時的類別ID和顯示名會錯位。這樣做的意義是讓標注和訓練完全解耦即使txt里第一列是0在模型意義下它依然是meat或你定義的第一個類別。4. 避坑手冊從解壓到訓練前的五個常見問題4.1 文件名對不上導致訓練直接報錯現(xiàn)象用YOLO框架訓練時日志里出現(xiàn)大量image not found或label not found檢查后發(fā)現(xiàn)有些jpg沒有同名txt有些txt沒有同名jpg。原因原始壓縮包內文件名前綴是firc_rsna_下載工具或解壓軟件有時會把超長文件名截斷或Windows資源管理器對特殊字符處理導致改名。也有用戶自己用腳本重命名圖片但沒有同步重命名標簽。解決解壓后第一時間跑一個校驗腳本用stem把三個后綴配對缺少任何一個就單獨挑出來。合理做法是把異常的挑出來但如果你只是想快速開訓直接過濾掉這些文件即可。import os bad [] for f in os.listdir(.): if f.endswith(.jpg): stem os.path.splitext(f)[0] if not (os.path.exists(stem.xml) and os.path.exists(stem.txt)): bad.append(stem) print(異常文件數(shù):, len(bad)) print(bad[:10])這個腳本會列出缺配對的stem。我遇到過一次解壓中途報“文件已存在”的提示原因是我之前解壓過一部分到同一目錄后綴沒覆蓋完全最終導致配對混亂。從那以后我都先刪干凈再解壓。4.2 類別名“meat”讓人誤以為標注錯誤現(xiàn)象用labelImg打開VOC標注看到類別名是meat再去看RSNA原始定義覺得牛頭不對馬嘴懷疑數(shù)據(jù)集作者標錯了。原因meat是作者在labelImg預設類別里順手填的一個標簽名不是其真實語義。RSNA肺炎數(shù)據(jù)集里需要檢測的目標通常是肺部實變/毛玻璃影區(qū)域所以框都框在肺部。只要框的位置在肺部區(qū)域就沒有問題。解決不需要改數(shù)據(jù)在訓練配置里把names寫成[meat]即可或者用第3章的class_map批量改名。我一般更建議改成pneumonia或lung_opacity方便以后和別人協(xié)作但改完注意txt也要同步改不要只改一半。4.3 xml尺寸與圖片真實尺寸不一致造成框偏移現(xiàn)象用xml里的絕對坐標畫框框的位置整體偏到一側或大小不對。原因xml里的size是標注工具當時讀到的尺寸如果作者在修改圖片縮放后沒有重新讀取xml保留的還是舊寬高。另一種情況是某個大目錄下混入了不同尺寸的圖片但xml套用了統(tǒng)一模板。解決統(tǒng)一以真實圖片為準。用PIL讀取img.shape打印出和size不一致的樣本。重新計算歸一化坐標時不要直接用xml里的寬高而是用真實寬高。注意如果你的圖片統(tǒng)一被縮放到固定尺寸那么必須先把圖片縮放到相同大小再生成txt或者干脆讓訓練時保留原始尺寸。import os import cv2 import xml.etree.ElementTree as ET for f in os.listdir(.): if not f.endswith(.xml): continue stem os.path.splitext(f)[0] img cv2.imread(stem .jpg) if img is None: continue real_h, real_w img.shape[:2] root ET.parse(f).getroot() xml_w int(root.find(size/width).text) xml_h int(root.find(size/height).text) if (real_w, real_h) ! (xml_w, xml_h): print(尺寸不一致:, stem, 真實, (real_w, real_h), xml, (xml_w, xml_h))運行后會列出所有可疑文件。如果數(shù)量很少可以直接用人工修正如果數(shù)量過半就要回到轉換腳本按真實尺寸重新算。4.4 7z解壓大文件時的數(shù)據(jù)完整性問題現(xiàn)象解壓到一半報Unexpected end of data或者解壓完數(shù)量統(tǒng)計少了幾百個文件。原因壓縮包下載不完整或者網(wǎng)盤下載工具沒有正確保留7z格式。7z大文件損壞很常見這篇正好是.7z格式。解決下載后先校驗大小用7-Zip的7z t命令測試完整性7z t RSNA肺炎檢測數(shù)據(jù)集VOCYOLO格式6012張1類別.7z如果有任何CRC錯誤基本上只能重新下載不要指望修復。解壓時優(yōu)先用7-Zip而不是Windows自帶的“壓縮文件夾”因為后者對7z支持有限容易靜默解壓出空目錄。另外下載文件名如果被瀏覽器改成了(1).7z先改回原名再測試避免工具識別格式錯誤。4.5 只有一個類別時txt第一列的0帶來的歧義現(xiàn)象訓練配置里nc1但日志有時輸出class 0 has no labels或者驗證時mAP一直為0。原因很多人把class_id0當成背景ID而在YOLO里背景不在標注行里0就是第一個類別。如果誤把nc設成2或者把names寫成了[background, meat]模型會認為有兩類但數(shù)據(jù)里只有0導致1類標簽為空。解決把nc保持為1names只要一個元素即可。如果你真要加背景類那么數(shù)據(jù)里的0要改成1同時把所有框的類別ID重新映射。合理的習慣是看到txt里第一列全是0就說明只有一個類別這是單類數(shù)據(jù)集正常現(xiàn)象不要亂改。5. 訓練前花兩分鐘遍歷核查一個畫框與統(tǒng)計的小腳本無論轉換腳本寫得多嚴謹訓練前最好還是用樸素方法驗證一遍。我習慣的做法是隨機抽10張圖片把xml里的絕對坐標畫到jpg上另存到checks/目錄同時統(tǒng)計每張圖的框數(shù)和面積分布。這樣能一眼看出標注是否偏移、是否有明顯錯標。5.1 快速核查腳本10張圖看全局import os import cv2 import numpy as np import xml.etree.ElementTree as ET src_root ./rsna_dataset checks_dir ./checks os.makedirs(checks_dir, exist_okTrue) for idx, f in enumerate(sorted(os.listdir(src_root))): if not f.endswith(.xml): continue if idx 10: break xml_path os.path.join(src_root, f) stem os.path.splitext(f)[0] img_path os.path.join(src_root, stem .jpg) root ET.parse(xml_path).getroot() img cv2.imread(img_path) if img is None: print(圖片讀取失敗:, img_path) continue h, w img.shape[:2] for obj in root.iter(object): bb obj.find(bndbox) xmin int(float(bb.find(xmin).text)) ymin int(float(bb.find(ymin).text)) xmax int(float(bb.find(xmax).text)) ymax int(float(bb.find(ymax).text)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 0, 255), 2) cv2.putText(img, meat, (xmin, ymin - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) out_path os.path.join(checks_dir, stem _check.jpg) cv2.imwrite(out_path, img) print(已保存核查圖片:, out_path)邏輯說明通過cv2讀取真實圖片用xml里的絕對坐標畫紅色矩形框。這里要注意cv2.rectangle的坐標順序是左上、右下正好對應VOC的bndbox。如果框在圖中能貼合病灶區(qū)的邊緣說明標注基本可信如果框跑到圖外或者明顯錯位就要回到錯誤目錄重新生成標注。這個腳本只抽查前10個xml便于快速人工看。如果你想全量生成把idx 10的循環(huán)條件去掉即可但6012張圖全部輸出會比較占空間。參數(shù)說明顏色(0,0,255)是紅色文字標簽可以改成你最后使用的類別名。如果圖片較大畫了框以后保存的jpeg體積會增加檢查完沒發(fā)現(xiàn)問題就刪掉整個目錄不用保留。注意中文路徑下cv2.imwrite可能報錯輸出文件名建議用純英文stem。這個方法投入的時間不超過三分鐘但能扼殺掉絕大部分“標簽格式?jīng)]問題但訓練崩了”的玄學問題。最后一次用這個數(shù)據(jù)集時我抽查到第7張圖發(fā)現(xiàn)xml里的xmin變成0追溯原因仍然是前一份xml尺寸錯誤。從那以后我每次下載別人標注數(shù)據(jù)集都會先強制走一遍這個遍歷核查流程確認框形和統(tǒng)計都正常后才交給訓練腳本。希望幫到你。本文還有配套的精品資源點擊獲取