據(jù)集與YOLO訓練實踐)
簡介面向電力設備視覺檢測方向的科研人員、工程師與目標檢測學習者這份文檔型資料圍繞一套4271張紅外圖像、14類電氣設備標注數(shù)據(jù)集展開重點服務變壓器及相關(guān)電力部件的識別與定位任務。資源以1個docx文檔交付約1006KB集中說明Pascal VOC與YOLO雙格式的組織方式、jpg圖片與xml/txt標注對應關(guān)系以及ACB、CT、Connection、LA、LBS、MCCB、PT、VCB等類別劃分和總框數(shù)11896的標注概況。文檔還交代labelImg矩形框標注規(guī)則、類別框數(shù)分布及精度不保證等使用說明便于讀者在訓練前核對數(shù)據(jù)規(guī)格、理解標簽含義并評估適用性。已有101人學習適合需要快速掌握該數(shù)據(jù)集結(jié)構(gòu)與檢測場景的讀者查閱。1. 4271 張紅外圖值多少工時一個巡檢識別項目的起步賬前年冬天幫一個做變電站巡檢的團隊看模型效果他們卡在一個很實在的地方現(xiàn)場熱像儀每天產(chǎn)出上千張圖人眼一眼能從熱像里挑出過熱的接頭模型換個站就崩。查下來問題不在骨干網(wǎng)絡而在訓練集只有三百多張自拍圖、類別只標了兩種。這類場景的解法往往落在數(shù)據(jù)側(cè)——一份標注齊全、類別覆蓋夠廣的紅外圖像數(shù)據(jù)集比換三個檢測頭都實在。這個電力場景電氣設備紅外圖像變壓器檢測數(shù)據(jù)集4271 張圖、14 類設備、11896 個矩形框VOC 與 YOLO 雙格式并存補的正是這塊短板。它適合兩類人正在做電力設備目標檢測、需要現(xiàn)成標注先把 baseline 跑起來的算法同學手上有紅外巡檢業(yè)務、想驗證自動識別可行性的工程團隊。2. 數(shù)據(jù)集拆解4271 張圖、14 類標簽與 11896 個框的分布拿到一個數(shù)據(jù)集我第一件事不是急著寫 train.py而是把包解開、數(shù)一遍文件、畫一張類別分布圖。數(shù)完心里就有底這活兒是能訓出東西還是會在長尾上翻車。這份數(shù)據(jù)集的骨架很規(guī)整圖片、VOC 標注、YOLO 標注三份文件數(shù)量完全對齊都是 4271說明沒有半途而廢的標注殘留這對后續(xù)寫轉(zhuǎn)換腳本省了很多事。2.1 文件組織jpg、xml、txt 三件套怎么擺壓縮包解開后是典型的 Pascal VOC 布局外加一份 YOLO 格式的平行目錄。里面不含 ImageSets 那種劃分路徑的 txt也就是說訓練集/驗證集的名單得自己生成這一點在動手前要清楚別指望拿現(xiàn)成的 train.txt。ir_power_dataset/ ├── JPEGImages/ # 4271 張 .jpg紅外偽彩或灰度渲染圖 │ ├── 000001.jpg │ └── ... ├── Annotations/ # 4271 個 VOC .xml與 jpg 同名 │ ├── 000001.xml │ └── ... ├── labels/ # 4271 個 YOLO .txt與 jpg 同名 │ ├── 000001.txt │ └── ... └── classes.txt # 14 行類別名行號即 class_id從 0 起判斷一個數(shù)據(jù)集能不能直接喂給訓練腳本我一般跑三行命令核對ls JPEGImages | wc -l、ls Annotations | wc -l、ls labels | wc -l三個數(shù)必須相等再用comm -3比一次文件名列表確認沒有孤兒文件。紅外數(shù)據(jù)集常見的一個毛病是同一設備同一角度連拍多張文件名帶(1)、(2)后綴轉(zhuǎn)換腳本里如果按stem拼路徑就會找不到對應的 xml。這份數(shù)據(jù)集名字規(guī)整但檢查這一步我從不省血淚經(jīng)驗是一次漏檢讓整個驗證集指標虛高 6 個點。2.2 14 個類別與長尾分布誰是大頭誰是稀有物種14 個類別的框數(shù)差了兩個數(shù)量級這是這份數(shù)據(jù)最需要提前知道的信息。按框數(shù)排下來class_id類別名含義框數(shù)占比0ACB空氣斷路器940.79%1CT電流互感器7906.64%2Conection連接器拼寫變體4713.96%3Connection連接器396133.30%4LA避雷器2672.24%5LBS負荷開關(guān)5484.61%6MCCB塑殼斷路器160013.45%7MOF計量/互感器類2732.29%8PF電力附件560.47%9PT電壓互感器6615.56%10VCB真空斷路器1441.21%11body柜身3803.19%12connection連接器小寫195216.41%13core鐵芯6995.88%—合計—11896100%表格里最扎眼的是Conection、Connection、connection三個條目——語義上都是連接器/接線端子卻因為標注時的拼寫和大小寫差異被拆成了三個類。PF只有 56 個框ACB94 個這兩個類在 640 輸入尺寸下如果目標本身還小訓到后期 AP 大概率是 0 或者個位數(shù)。我的判斷是先按原始 14 類跑一版當基線記錄每類 AP再決定要不要合并成 810 類而不是一上來就改標簽那樣出了問題分不清是數(shù)據(jù)的問題還是改造的問題。2.3 標注文件長什么樣xml 與 txt 的字段對照VOC 與 YOLO 表達的是同一個框只是坐標系不同。VOC 存絕對像素的左上、右下角點YOLO 存歸一化的中心點與寬高。理解這一層寫轉(zhuǎn)換腳本時就不會在/w和/h上搞反。annotation filename000123.jpg/filename size width640/widthheight480/heightdepth3/depth /size object nameConnection/name bndbox xmin210/xminymin96/yminxmax388/xmaxymax302/ymax /bndbox /object /annotation# 000123.txt —— 一行一個目標class_id cx cy w h全部歸一化到 0~1 3 0.467188 0.414583 0.278125 0.429167對應的換算關(guān)系是cx(xminxmax)/2/width、w(xmax-xmin)/width縱軸同理。這里有個容易忽略的點xml 里的size字段不一定和 jpg 實際尺寸一致有的標注工具在圖片縮放后沒回寫尺寸直接信 xml 會讓框整體偏移。穩(wěn)妥做法是轉(zhuǎn)換時用cv2.imread(...).shape讀實際尺寸和 xml 記錄的尺寸做一次比對不一致就報警并統(tǒng)一按實際尺寸換算。3. 跑通第一步VOC 轉(zhuǎn) YOLO 的轉(zhuǎn)換腳本與訓練配置數(shù)據(jù)集同時給了兩套格式理論上 YOLO 訓練只吃 txtVOC 那份是給 TensorFlow/Paddle 系或 MMDetection 用的。但我不建議直接拿包里的 txt 開訓——class_id 的分配順序你得自己確認一遍一旦和data.yaml里 names 的順序?qū)Σ簧嫌柧氄諛优躭oss 照樣降最后預測出來避雷器的位置標著柜身這種錯誤沒有報錯提示純純的黑匣子。3.1 類別映射別用 sorted() 偷懶定 class_id自己寫轉(zhuǎn)換腳本時第一步是把類別順序釘死成一份列表轉(zhuǎn) xml 和生成 yaml 都用這一份絕不在兩個地方各寫一遍。用sorted(set(names))自動推導看著聰明實際會引入大小寫排序問題——大寫字母全部排在小寫前面body和core會被擠到中間去和包內(nèi) txt 的編號完全錯位。# classes.py —— 全項目唯一的類別定義來源 CLASSES [ ACB, CT, Conection, Connection, LA, LBS, MCCB, MOF, PF, PT, VCB, body, connection, core, ] NAME2ID {name: idx for idx, name in enumerate(CLASSES)} ID2NAME {idx: name for name, idx in NAME2ID.items()}這份列表的順序是照抄數(shù)據(jù)集說明里給出的順序ACB是 0 號core是 13 號。如果決定合并那三個連接器類別只需要在這一個文件里改其他地方全部引用NAME2ID改完跑一次全量轉(zhuǎn)換即可不用滿項目找散落的數(shù)字。參數(shù)上唯一要盯的是enumerate的起點YOLO 的 class_id 從 0 開始而有些老腳本按 VOC 習慣從 1 開始差一位就是全盤錯。3.2 轉(zhuǎn)換腳本越界裁剪與退化框過濾真正容易出問題的地方在坐標上。紅外圖像邊緣經(jīng)常只拍到半個設備標注的人手一抖就把 xmax 標到了圖片寬度之外也有框被標成xmin xmax的一條線。這兩種情況不處理寫進 txt 后就是越界的歸一化值訓練時要么被框架靜默丟掉要么在計算 loss 時炸出 NaN。# voc2yolo.py import os import xml.etree.ElementTree as ET import cv2 from classes import NAME2ID def convert_one(xml_path, txt_path, img_path): h, w cv2.imread(img_path).shape[:2] # 以圖片實際尺寸為準 root ET.parse(xml_path).getroot() lines, dropped [], 0 for obj in root.iter(object): name (obj.find(name).text or ).strip() if name not in NAME2ID: # 不在白名單里的類別直接跳過 dropped 1 continue b obj.find(bndbox) x1, y1 float(b.find(xmin).text), float(b.find(ymin).text) x2, y2 float(b.find(xmax).text), float(b.find(ymax).text) # 越界裁剪先夾到圖像范圍內(nèi) x1, x2 max(0.0, min(x1, w)), max(0.0, min(x2, w)) y1, y2 max(0.0, min(y1, h)), max(0.0, min(y2, h)) bw, bh x2 - x1, y2 - y1 if bw 2 or bh 2: # 退化框?qū)捇蚋卟蛔?2 像素 dropped 1 continue cx, cy (x1 x2) / 2 / w, (y1 y2) / 2 / h lines.append(f{NAME2ID[name]} {cx:.6f} {cy:.6f} {bw/w:.6f} {bh/h:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) return len(lines), dropped if __name__ __main__: root_dir /data/ir_power_dataset total_box, total_drop, empty_img 0, 0, 0 for fn in os.listdir(f{root_dir}/Annotations): stem os.path.splitext(fn)[0] n, d convert_one( f{root_dir}/Annotations/{fn}, f{root_dir}/labels/{stem}.txt, f{root_dir}/JPEGImages/{stem}.jpg, ) total_box n; total_drop d empty_img (n 0) print(f寫入框 {total_box}丟棄 {total_drop}空標注圖 {empty_img})這段腳本干了三件事按白名單過濾類別、把越界坐標夾回圖內(nèi)、丟掉寬高不足 2 像素的退化框。跑完打印三個數(shù)字正常情況是寫入框數(shù)接近 11896、丟棄數(shù)為個位數(shù)如果丟棄數(shù)上百多半是某批圖的標注尺寸和圖片尺寸對不上需要回去查那批文件。empty_img也不能忽略標注工具過濾之后留下空 txt 的圖仍會被加載雖然不算負樣本錯誤但會讓一個 batch 的有效梯度變少我一般把空 txt 對應的圖一起從訓練名單里剔掉。想省事直接用包里現(xiàn)成的 txt 也行那就跳過這一步但至少抽樣把labels/里的 class_id 分布統(tǒng)計一次和上面的表對得上才放心。3.3 劃分與 data.yaml一次寫對后面都省事數(shù)據(jù)集沒帶 ImageSets切分得自己做。我固定用 8:1:1并且按類別分層抽樣保證PF、ACB這類小類在驗證集里至少有十幾個框否則算出來的 mAP 波動大得沒法看。# split.py —— 分層切分保證小類在驗證集也有樣本 import random, shutil from pathlib import Path from collections import defaultdict random.seed(42) root Path(/data/ir_power_dataset) stems [p.stem for p in (root / labels).glob(*.txt)] # 用每個文件里出現(xiàn)的類別作為分層鍵 bucket defaultdict(list) for s in stems: ids {line.split()[0] for line in open(root / labels / f{s}.txt) if line.strip()} bucket[tuple(sorted(ids))].append(s) train, val, test [], [], [] for key, items in bucket.items(): random.shuffle(items) n len(items) train items[: int(n * 0.8)] val items[int(n * 0.8): int(n * 0.9)] test items[int(n * 0.9):] for split, items in [(train, train), (val, val), (test, test)]: for sub in (images, labels): (root / splits / split / sub).mkdir(parentsTrue, exist_okTrue) for s in items: shutil.copy(root / JPEGImages / f{s}.jpg, root / splits / split / images / f{s}.jpg) shutil.copy(root / labels / f{s}.txt, root / splits / split / labels / f{s}.txt) print(len(train), len(val), len(test))分層鍵用的是這張圖里出現(xiàn)了哪些類別的組合而不是單一主類別效果比隨機切分穩(wěn)。代價是組合數(shù)量可能很碎樣本少的組合分出來只有一兩張這沒關(guān)系小類本來就要靠訓練時的采樣策略補切分只能保證不全落到訓練集里。# data.yaml path: /data/ir_power_dataset/splits train: train/images val: val/images test: test/images names: 0: ACB 1: CT 2: Conection 3: Connection 4: LA 5: LBS 6: MCCB 7: MOF 8: PF 9: PT 10: VCB 11: body 12: connection 13: corenames 的編號必須和classes.py逐行一致包括大小寫。檢查方法很土但有效隨便挑一張圖把它的 txt 第一行讀出來拿 class_id 去 yaml 里查名字再回頭看 xml 里對應 object 的 name能對上就說明整條鏈路沒錯位。# 先在 640 分辨率上跑一版基線別急著堆 epochs yolo detect train modelyolov8s.pt datadata.yaml imgsz640 epochs100 \ batch16 workers8 device0 patience30 close_mosaic10參數(shù)取值說明modelyolov8s.pt小模型先驗證數(shù)據(jù)可用性跑通再換 m/limgsz640紅外小目標多顯存允許可上 960 試一版batch168G 顯存的安全值OOM 就降到 8patience30連續(xù) 30 輪沒提升就早停避免過擬合小類close_mosaic10最后 10 輪關(guān)掉 mosaic讓框回歸收斂得更準第一版訓練我只關(guān)心兩件事loss 曲線是不是平滑下降以及val上的每類 AP 分布和框數(shù)分布是否一致。如果Connection的 AP 上到 0.9 而PF停在 0.01那是樣本量的問題不是模型的問題別在那調(diào)學習率白費時間。4. 訓練鏈路適配紅外圖像的數(shù)據(jù)加載與增強策略把數(shù)據(jù)塞進 YOLO 默認管線能跑但跑出來的模型往往在真實紅外圖上表現(xiàn)不穩(wěn)定原因多半出在增強上。可見光的增強配方搬到紅外場景有些操作是在幫倒忙——熱像圖的顏色是偽彩映射出來的不是物體本色HSV 擾動一上模型學到的可能是配色規(guī)律而不是設備形狀。4.1 紅外圖像的三個脾氣低對比度、偽彩失真、噪點第一是動態(tài)范圍窄。紅外相機的原始數(shù)據(jù)是溫度映射成 8 位圖之后柜體和背景的灰度差可能只有十幾個灰階肉眼看著一片糊模型的淺層卷積很難提出邊緣。第二是偽彩。同一臺設備在不同色帶設置下可以是紅黃也可以是藍紫若訓練集全是一種配色換臺設備就廢。第三是噪聲熱像儀的非均勻性和隨機噪點在低對比度區(qū)域特別明顯容易被誤檢成小目標。對應的策略我一般這么定用 CLAHE 提升局部對比度、隨機灰度化打散偽彩依賴、亮度/對比度擾動控制在 ±0.2 以內(nèi)、不用HueSaturationValue 這類強色彩增強。翻轉(zhuǎn)可以留水平翻轉(zhuǎn)垂直翻轉(zhuǎn)要慎用——電力柜里的斷路器、儀表安裝是有明確上下方向的翻過來就成了現(xiàn)實中不存在的樣本。4.2 自定義 Dataset把增強控制在合理范圍如果要用 Ultralytics 之外的框架Dataset 得自己寫。核心是三段讀圖統(tǒng)一成三通道、讀 txt 組 boxes、走 albumentations 管線并保證 boxes 跟著變換。# ir_dataset.py import cv2 import numpy as np import torch from torch.utils.data import Dataset import albumentations as A from albumentations.pytorch import ToTensorV2 train_tf A.Compose([ A.LongestMaxSize(max_size640), A.PadIfNeeded(640, 640, border_modecv2.BORDER_CONSTANT, value0), A.HorizontalFlip(p0.5), # 垂直翻轉(zhuǎn)對柜內(nèi)設備不合理關(guān)掉 A.RandomBrightnessContrast(0.2, 0.2, p0.5), # 模擬曝光差異幅度收窄 A.CLAHE(clip_limit2.0, tile_grid_size(8, 8), p0.3), # 拉開局部對比度 A.GaussNoise(var_limit(5.0, 20.0), p0.2), # 貼合熱像儀噪點 A.ToGray(p0.1), # 隨機去色降低偽彩依賴 ToTensorV2(), ], bbox_paramsA.BboxParams(formatyolo, label_fields[cls], min_visibility0.3)) class IRDataset(Dataset): def __init__(self, img_dir, lbl_dir, tf): self.img_dir, self.lbl_dir, self.tf img_dir, lbl_dir, tf self.names sorted(p.stem for p in img_dir.glob(*.jpg)) def __len__(self): return len(self.names) def __getitem__(self, idx): stem self.names[idx] img cv2.imread(str(self.img_dir / f{stem}.jpg)) # 偽彩圖按 BGR 讀 if img is None: raise FileNotFoundError(stem) cls, boxes [], [] for line in open(self.lbl_dir / f{stem}.txt): if not line.strip(): continue c, cx, cy, w, h line.split() cls.append(int(c)); boxes.append([float(cx), float(cy), float(w), float(h)]) out self.tf(imageimg, bboxesboxes, clscls) return { image: out[image], boxes: torch.tensor(out[bboxes], dtypetorch.float32).clamp(0, 1), labels: torch.tensor(out[cls], dtypetorch.long), }幾個參數(shù)的取舍值得說明。min_visibility0.3是裁剪/翻轉(zhuǎn)后框只剩不到三成面積就丟棄避免產(chǎn)生一堆幾乎看不見的殘框污染標簽ToGray(p0.1)概率不高目的是讓模型偶爾見到無顏色信息的輸入學會靠形狀判別CLAHE的clip_limit2.0是個保守值調(diào)到 4 以上會把噪聲也一起放大熱像圖的噪點被強化后很像是小目標反而拉低精度。如果直接用 Ultralytics 訓練等價做法是在命令行加hsv_h0.0 hsv_s0.0 hsv_v0.3 degrees0 fliplr0.5 flipud0.0把色彩相關(guān)的增強全關(guān)掉。提示紅外數(shù)據(jù)集如果混了灰度和偽彩兩種渲染務必確認讀圖時通道數(shù)一致。灰度圖用cv2.imread默認讀成三通道沒問題但用cv2.IMREAD_GRAYSCALE讀進來再送進需要 3 通道的骨干網(wǎng)絡會直接報維度錯誤。4.3 評估怎么讀每類 AP、混淆矩陣和漏檢圖只看一個 mAP50 是自欺欺人。這份數(shù)據(jù) 14 類里框數(shù)從 56 到 3961總 mAP 會被Connection和connection兩個大類拉到 0.8 以上看著漂亮實際PF、ACB全軍覆沒。我通常按這個順序看先拉每類 AP 表和框數(shù)表并排放找框數(shù)不少但 AP 極低的類這通常是標注質(zhì)量問題再看混淆矩陣重點看那三個連接器類別之間是否互相串——串得厲害就是拼寫分裂導致的最后抽 20 張驗證集原圖做可視化專門找漏檢紅外圖里設備密集堆疊的區(qū)域漏檢率明顯更高這類樣本要回流到訓練集。驗證命令跑完之后runs/detect/val/下會生成confusion_matrix.png、PR_curve.png和各分類的val_batchX_pred.jpg。我習慣把預測圖按文件名排序后快速翻一遍翻到哪個類標錯顏色就記下來比盯著數(shù)字有用。5. 避坑排查標簽拼寫、坐標越界與驗證集泄漏的 5 個坑這一節(jié)是整篇里我最想讓人先看的部分。上面那些步驟照著做基本不會出岔子真正耗時間的是跑起來之后指標不對勁、又找不到原因的場面。下面五條都是這類數(shù)據(jù)集上真實會撞見的按現(xiàn)象 → 原因 → 解決記下來。5.1 標簽層面的三個坑坑一三個連接器類別互相搶框混淆矩陣一片紅。現(xiàn)象是訓練能收斂但Conection、Connection、connection三類的 AP 都在 0.4 上下徘徊混淆矩陣里它們?nèi)齻€互相大量錯分同一張圖上的接線端子一會兒被預測成 2 號一會兒成 12 號。原因是標注階段不同人對同一個部件的寫法不統(tǒng)一本質(zhì)上是一個類被拆成了三份每份樣本量都不夠模型學不到穩(wěn)定特征。解決辦法是合并成一個類在classes.py里把這三項合成單個connector重跑轉(zhuǎn)換腳本同時注意合并后body/core的編號會前移data.yaml的 names 必須跟著改兩處一起改別只改一邊??佣柧毜揭话?loss 變 NaN或者某些框完全學不到。現(xiàn)象是前幾輪 loss 正常下降十幾輪之后突然炸成 NaN或者日志里出現(xiàn)大量被忽略的框。原因是原始 xml 里存在越界坐標和零面積框轉(zhuǎn)換時沒裁干凈歸一化后出現(xiàn)大于 1 的值或?qū)挾葹?0 的行。解決辦法就是 3.2 節(jié)那段腳本里的截斷邏輯轉(zhuǎn)換完再補一道校驗把所有 txt 讀一遍統(tǒng)計cx1 or cy1 or w0 or h0的行數(shù)正常應該是 0不為 0 就說明那批 xml 有問題回去看是哪個標注批次。坑三某張圖上有目標但 txt 是空的訓練時框全丟?,F(xiàn)象是驗證時發(fā)現(xiàn)明明有設備的圖模型一個框都不出。原因通常是標注工具里畫了框但沒保存或者類別名寫成了表里沒有的拼寫比如把MCCB寫成MCCB帶了尾部空格轉(zhuǎn)換腳本按白名單過濾時就靜默跳過了。解決辦法是在轉(zhuǎn)換腳本里加一行日志把未命中白名單的類別名連同文件名打印出來跑一次全量轉(zhuǎn)換就能把這個名單收齊另外strip()一定要加尾部空格是最隱蔽的一種。5.2 訓練與評估層面的兩個坑坑四驗證集指標高得離譜換到現(xiàn)場就崩。現(xiàn)象是 val 上 mAP50 輕松上 0.9拿去跑另一批現(xiàn)場圖連大類都認不全。原因是隨機切分把同一臺設備、同一角度、甚至是連拍的近似幀同時分進了訓練集和驗證集模型等于在背答案。解決方法是切分時按采集批次/設備編號分組同一組只進一個 split。如果文件名里沒有批次信息就用圖像相似度聚類把近鄰幀歸為一組再切。這一步多花半天能省掉現(xiàn)場調(diào)試的兩周??游逍☆悇e AP 長期為 0調(diào)參調(diào)不動。現(xiàn)象是PF56 個框、ACB94 個框從第一輪到最后一輪 AP 都是 0其他類正常。原因是這兩類樣本量太少且目標在圖中往往偏小標準的隨機采樣下每個 batch 里幾乎見不到它們。解決辦法分三步先確認這兩類的框確實存在且坐標正確用可視化腳本畫出來看再開啟類別加權(quán)采樣或過采樣小類圖片最后考慮提高輸入分辨率到 960讓小平米目標在特征圖上多占幾個像素。如果三步做完 AP 還是 0接受它——56 個框本來就不足以支撐一個獨立類別合并進語義相近的類更實際。注意這五條里坑一和坑四最容易被忽略因為它們的共同特征是訓練過程完全正常、沒有任何報錯。前者錯在標簽后者騙在切分兩者都不會在終端里給你一行紅字。6. 二次開發(fā)類別重劃、半自動預標注與難例回流把基線跑通只是起點真正落到業(yè)務里14 類這個粒度大概率要動。我一般的做法分三層推進每一步都用指標驗證不憑感覺。第一層是類別重劃。按業(yè)務需求把 14 類壓成設備大類 關(guān)鍵部件比如斷路器ACB/MCCB/VCB 合并、互感器CT/PT/MOF 合并、連接器三個拼寫合并、柜身、鐵芯、避雷器、負荷開關(guān)。合并后類別數(shù)從 14 降到 8 左右每個類的框數(shù)都在 300 以上PF這種 56 框的稀有類要么并入鄰近類要么單獨留出來做二階段分類。改造方式是只動classes.py里的映射表加一個RAW2MERGED字典轉(zhuǎn)換腳本里多查一次表原始 xml 一個字都不改隨時可以回退。改造項改造前改造后驗證方式類別粒度14 類8 類每類 AP 是否整體上移小類是否脫離 0輸入分辨率640960小目標召回率、顯存占用與推理耗時數(shù)據(jù)量4271 張4271 預標注復核新增樣本中人工修正比例是否逐步下降第二層是半自動預標注。用訓好的模型對未標注的現(xiàn)場圖做推理置信度閾值卡在 0.4 左右把預測框直接寫成 YOLO txt再人工過一遍只改錯的不畫新的。這一步能省掉標注成本的大頭但有個細節(jié)要注意預標注生成的 txt 里混著模型的系統(tǒng)性偏差如果全部無條件接受下一輪訓練只是在放大誤差。我的經(jīng)驗是每天抽 10% 的預標注結(jié)果做人工復核并統(tǒng)計修正率修正率降到 5% 以下再考慮擴大自動接受范圍。# prelabel.py —— 用已有權(quán)重給新圖打預標注低置信度直接丟不留給人工糾結(jié) from ultralytics import YOLO from pathlib import Path model YOLO(runs/detect/train/weights/best.pt) src, dst Path(/data/new_ir), Path(/data/new_labels) dst.mkdir(parentsTrue, exist_okTrue) for r in model.predict(sourcestr(src), imgsz960, conf0.4, iou0.5, streamTrue): stem Path(r.path).stem lines [] for b in r.boxes: c, x, y, w, h int(b.cls), *b.xywhn[0].tolist() lines.append(f{c} {x:.6f} {y:.6f} {w:.6f} {h:.6f}) (dst / f{stem}.txt).write_text(\n.join(lines))conf0.4是刻意壓低的值寧可多給人看幾個框也別漏掉真目標——漏掉的框人工根本發(fā)現(xiàn)不了多出來的框一眼就能刪。imgsz960要和訓練時保持一致尺寸不匹配會讓小目標直接消失。第三層是難例回流。把驗證集和現(xiàn)場測試里漏檢、誤檢的圖單獨存一個目錄每隔一輪訓練就補進去同時在data.yaml里加一組hard路徑做專項評估。這個循環(huán)跑上三五輪模型在現(xiàn)場的穩(wěn)定性提升通常比換更大的骨干網(wǎng)絡明顯。一個教訓我早期做這類改造時習慣一次性把類別、增強、分辨率全改完結(jié)果指標掉了也不知道是哪一項的責任只能全量回滾重來。從那以后我每次改數(shù)據(jù)集只動一個變量先跑 20 輪小實驗看趨勢確認有效再進全量訓練改動記錄寫在classes.py的注釋里隔兩個月回來也能看懂自己當時干了什么。希望幫到你。本文還有配套的精品資源點擊獲取