識(shí)別實(shí)戰(zhàn):689張數(shù)據(jù)集處理與訓(xùn)練全流程解析)
簡(jiǎn)介面向水位檢測(cè)儀器讀數(shù)識(shí)別的 YOLO 系列目標(biāo)檢測(cè)數(shù)據(jù)集已按訓(xùn)練集、驗(yàn)證集、測(cè)試集劃分完畢并內(nèi)置 data.yaml 配置文件兼容 yolov5、v7、v8、v9、v10、v11 等主流版本可直接進(jìn)入訓(xùn)練流程適合正在學(xué)習(xí)目標(biāo)檢測(cè)、準(zhǔn)備算法實(shí)驗(yàn)或畢設(shè)項(xiàng)目的讀者快速上手省去從零采集圖像和人工標(biāo)注的準(zhǔn)備工作。壓縮包共包含 2000 個(gè)文件整體約 34.8MB其中 621 張 JPG 原圖、689 個(gè) TXT 標(biāo)簽和 689 個(gè) XML 標(biāo)簽TXT 為 YOLO 格式類別索引與歸一化中心點(diǎn)、寬高信息齊全XML 為 VOC 格式兩種標(biāo)簽分別存放便于在兩種標(biāo)注體系間對(duì)照或遷移另附 1 個(gè) YAML 數(shù)據(jù)集配置文件可直接被 YOLO 系列框架讀取。內(nèi)容以水位檢測(cè)儀器上的讀數(shù)顯示為檢測(cè)目標(biāo)貼合工業(yè)監(jiān)測(cè)、儀表讀數(shù)識(shí)別等實(shí)際場(chǎng)景適合驗(yàn)證模型在讀數(shù)識(shí)別任務(wù)中的檢測(cè)精度與泛化能力。目前已有 231 人瀏覽學(xué)習(xí)是開(kāi)展 YOLO 系列算法訓(xùn)練、模型驗(yàn)證與讀數(shù)識(shí)別預(yù)研時(shí)可復(fù)用的數(shù)據(jù)集資源。1. 689張帶標(biāo)簽圖像與YOLO水位表讀數(shù)識(shí)別的最小可行起點(diǎn)很多做水利信息化、智慧灌區(qū)和工業(yè)現(xiàn)場(chǎng)抄表的人拿到這類“水位檢測(cè)儀器讀數(shù)數(shù)據(jù)集”的第一反應(yīng)是趕緊拿來(lái)訓(xùn)練一個(gè)YOLO模型把表盤(pán)上的數(shù)字自動(dòng)讀出來(lái)。這個(gè)zip的價(jià)值正在于此——689張圖像不是幾千上萬(wàn)的通用數(shù)據(jù)集但勝在領(lǐng)域單一、標(biāo)簽齊全包含號(hào)碼、測(cè)量、最大值這些與讀數(shù)強(qiáng)相關(guān)的標(biāo)注維度。它的適用場(chǎng)景很明確你想低成本驗(yàn)證“基于yolo算法的表計(jì)讀數(shù)識(shí)別”這個(gè)方向可不可行先把這套數(shù)據(jù)跑通再疊加自己的現(xiàn)場(chǎng)圖像做增量訓(xùn)練。適合正在做目標(biāo)檢測(cè)入門、工業(yè)視覺(jué)和物聯(lián)網(wǎng)抄表方案的工程師也適合剛接觸數(shù)據(jù)集落地的新手——因?yàn)閳D像規(guī)模不大整個(gè)流程從解壓到訓(xùn)練出可用模型一兩天就能走完。2. 拿到zip先做數(shù)據(jù)體檢解壓、盤(pán)點(diǎn)、標(biāo)簽語(yǔ)義確認(rèn)2.1 解壓前先看包文件嵌套與中文路徑的坑壓縮包這層最容易出問(wèn)題。不要直接雙擊解壓先看一下包內(nèi)結(jié)構(gòu)。命令行里用unzip -l列目錄Windows 下用 7-Zip 的“打開(kāi)內(nèi)部壓縮包”也是一樣的效果。我見(jiàn)過(guò)很多人在這一步翻車解壓后產(chǎn)生兩級(jí)同名目錄yolo算法-水位檢測(cè)儀器讀數(shù)數(shù)據(jù)集又套一層同名目錄YOLO訓(xùn)練腳本里寫(xiě)相對(duì)路徑就全亂掉還有一部分圖像文件名帶中文Python 的cv2.imread在 Windows 默認(rèn)編碼下讀不出中文路徑報(bào)錯(cuò)卻指向下一行代碼。# 先看壓縮包頂層結(jié)構(gòu)別急著解壓 unzip -l yolo算法-水位檢測(cè)儀器讀數(shù)數(shù)據(jù)集-689張圖像帶標(biāo)簽-號(hào)碼-測(cè)量-最大值.zip | head -30 # 解壓到純英文路徑避免中文目錄導(dǎo)致OpenCV讀取失敗 mkdir -p /data/water_meter_dataset unzip yolo算法-水位檢測(cè)儀器讀數(shù)數(shù)據(jù)集-689張圖像帶標(biāo)簽-號(hào)碼-測(cè)量-最大值.zip -d /data/water_meter_dataset這段命令的思路是用unzip -l觀察包內(nèi)是否有頂層文件夾head -30只看前30條就夠了核心是確認(rèn)圖像和標(biāo)簽是不是在同一個(gè)根目錄下解壓時(shí)把目標(biāo)目錄設(shè)置成/data/water_meter_dataset這種純英文路徑是在提前消除編碼隱患。如果包內(nèi)確實(shí)有一層外層目錄解壓后你得到的就是/data/water_meter_dataset/水位檢測(cè)儀器讀數(shù)數(shù)據(jù)集/...后面所有腳本記得多帶一層即可。2.2 圖像與標(biāo)簽的數(shù)目盤(pán)點(diǎn)用幾條命令查清家底解壓之后先別訓(xùn)練先回答三個(gè)問(wèn)題圖像有多少?gòu)?、?biāo)簽有多少個(gè)、兩者能不能一一對(duì)上。689張圖像是標(biāo)題宣稱的數(shù)據(jù)量實(shí)際到手可能多幾張少幾張也可能圖像689張但標(biāo)簽只有670份——漏標(biāo)、空標(biāo)簽在打包過(guò)程中很常見(jiàn)。這里用 find 和 wc 組合做一次快速體檢。# 統(tǒng)計(jì)圖像文件數(shù)量jpg/png都要算 find /data/water_meter_dataset -type f \( -name *.jpg -o -name *.png \) | wc -l # 統(tǒng)計(jì)標(biāo)簽文件數(shù)量常見(jiàn)擴(kuò)展名是xml/txt/json find /data/water_meter_dataset -type f \( -name *.xml -o -name *.txt -o -name *.json \) | wc -l # 列出前10個(gè)標(biāo)簽文件名看清到底是xml還是txt find /data/water_meter_dataset -type f -name *.xml | head -10習(xí)慣上先把圖像和標(biāo)簽各自的數(shù)量列出來(lái)再比對(duì)文件名集合。數(shù)量對(duì)不上時(shí)不要急著懷疑數(shù)據(jù)集有問(wèn)題先看是不是有非圖像文件混在標(biāo)簽?zāi)夸浝?。更穩(wěn)妥的做法是直接用 Python 做一次文件名求差集把“有圖無(wú)標(biāo)”和“有標(biāo)無(wú)圖”的文件名各自列出來(lái)。數(shù)據(jù)集的坑多集中在文件層這一步花十分鐘后面少踩三個(gè)坑。來(lái)看看一套合格的數(shù)據(jù)集內(nèi)部長(zhǎng)什么樣下面這張核查表可以對(duì)照著手動(dòng)檢查。檢查項(xiàng)期望結(jié)果異常處理方法圖像格式j(luò)pg或png統(tǒng)一混有bmp/tif時(shí)用Pillow批量轉(zhuǎn)成jpg注意質(zhì)量參數(shù)標(biāo)簽格式全部同一種格式混用xml和txt時(shí)按格式分別解析不要強(qiáng)行合并文件名對(duì)應(yīng)圖像與標(biāo)簽同名后綴不同用Python對(duì)文件名取差集單獨(dú)移出缺失文件圖像分辨率不低于640x640過(guò)小的圖直接刪掉或標(biāo)注后作為增強(qiáng)數(shù)據(jù)標(biāo)簽內(nèi)容每份至少一個(gè)目標(biāo)空標(biāo)簽文件單獨(dú)歸類后面用腳本處理2.3 標(biāo)簽語(yǔ)義確認(rèn)“號(hào)碼”“測(cè)量”“最大值”到底標(biāo)在哪文件名里那串“號(hào)碼-測(cè)量-最大值”不能猜。把標(biāo)簽文件打開(kāi)看幾個(gè)樣本確認(rèn)這三個(gè)詞是類別名、屬性字段、還是只存在于文件名里的描述信息。下面這段 Python 代碼用標(biāo)準(zhǔn)庫(kù)解析 XML 標(biāo)簽打印出每個(gè)目標(biāo)的類別和邊界框坐標(biāo)。import xml.etree.ElementTree as ET from pathlib import Path # 以解壓后的目錄為例手動(dòng)指定一個(gè)xml樣本 xml_path Path(/data/water_meter_dataset/labels/000001.xml) if xml_path.exists(): tree ET.parse(xml_path) root tree.getroot() # 圖像尺寸在annotation里記錄后用于歸一化計(jì)算 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) print(f圖像尺寸: {img_w}x{img_h}) # 遍歷object節(jié)點(diǎn)輸出類別名和像素坐標(biāo) for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) print(f類別: {name}, 框: ({xmin:.1f}, {ymin:.1f}) - ({xmax:.1f}, {ymax:.1f}))這段代碼的邏輯很直白先讀size拿到原圖寬高這是后面計(jì)算歸一化坐標(biāo)的必要輸入再遍歷object節(jié)點(diǎn)把類別名和邊界框打出來(lái)。跑完幾個(gè)樣本后你會(huì)發(fā)現(xiàn)“測(cè)量”和“最大值”很可能不是檢測(cè)類別而是表計(jì)的量程信息“號(hào)碼”則可能是儀表編號(hào)文本也可能是一張圖里多個(gè)表盤(pán)實(shí)例的編號(hào)。如果是前者那“測(cè)量/最大值”只是附帶的業(yè)務(wù)元數(shù)據(jù)不需要訓(xùn)練如果是后者這幾個(gè)詞會(huì)出現(xiàn)在類別列表里就必須在 yaml 里全部定義。判斷標(biāo)準(zhǔn)就一條去找標(biāo)簽文件里name字段的實(shí)際取值只有一個(gè)類別就寫(xiě)一個(gè)nc1有三個(gè)就寫(xiě)nc3其他都是多余的。3. 從原始標(biāo)簽到Y(jié)OLO訓(xùn)練格式格式轉(zhuǎn)換與數(shù)據(jù)集劃分3.1 為什么YOLO非要txt歸一化坐標(biāo)背后的邏輯VOC 格式的 XML 標(biāo)簽在公開(kāi)數(shù)據(jù)集里最常見(jiàn)但 YOLO 訓(xùn)練器只認(rèn)純文本的 txt 標(biāo)簽每行一個(gè)目標(biāo)格式為class_id x_center y_center width height。最關(guān)鍵的是坐標(biāo)必須是歸一化的也就是說(shuō)所有值都要除以圖像寬高落在 0 到 1 之間。這樣做的原因是 YOLO 在訓(xùn)練時(shí)會(huì)自動(dòng)把輸入圖縮放到固定尺寸例如 640x640歸一化坐標(biāo)可以保證無(wú)論輸入大圖還是小圖標(biāo)注框都能正確映射到縮放后的特征圖上。而 XML 里的坐標(biāo)是像素值直接拿絕對(duì)值去算換一張分辨率不同的圖像就全錯(cuò)位了。這就是每份 YOLO 數(shù)據(jù)集必須有一個(gè)“格式轉(zhuǎn)換”環(huán)節(jié)的原因。另一個(gè)容易被忽略的點(diǎn)是很多公開(kāi)的 xml 標(biāo)簽文件根本沒(méi)有標(biāo)簽節(jié)點(diǎn)也就是只有object的空文件這種情況不能直接轉(zhuǎn)為空白 txt 了事需要單獨(dú)處理。3.2 轉(zhuǎn)換腳本批量把XML轉(zhuǎn)成YOLO格式txt并清洗空標(biāo)簽下面的腳本按最常見(jiàn)的 VOC XML 格式寫(xiě)假設(shè)標(biāo)簽文件在labels_xml目錄圖像在images目錄。轉(zhuǎn)換時(shí)除了歸一化坐標(biāo)還會(huì)過(guò)濾掉difficult1的模糊目標(biāo)并跳過(guò)解析失敗的標(biāo)簽文件。import xml.etree.ElementTree as ET from pathlib import Path # 類別映射以實(shí)際標(biāo)注為準(zhǔn)這里給一個(gè)示例假設(shè)只有讀數(shù)字符區(qū) label_map { meter_reading: 0, # 表盤(pán)讀數(shù)區(qū)域 meter_number: 1, # 儀表編號(hào) } xml_dir Path(/data/water_meter_dataset/labels_xml) txt_dir Path(/data/water_meter_dataset/labels_txt) txt_dir.mkdir(exist_okTrue) for xml_file in xml_dir.glob(*.xml): try: tree ET.parse(xml_file) root tree.getroot() except ET.ParseError: # 標(biāo)簽文件損壞時(shí)跳過(guò)并打印出文件名方便排查 print(f解析失敗: {xml_file.name}) continue size root.find(size) if size is None: continue img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): # 跳過(guò)difficult標(biāo)注避免模糊樣本干擾訓(xùn)練 diff obj.find(difficult) if diff is not None and int(diff.text) 1: continue name obj.find(name).text if name not in label_map: print(f未知類別: {name} 在 {xml_file.name}) continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 歸一化坐標(biāo)并剔除寬高為0的異常標(biāo)注 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 if w 0 or h 0: continue lines.append(f{label_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) # 文本文件與圖像同名寫(xiě)在txt_dir下 txt_path txt_dir / f{xml_file.stem}.txt if lines: txt_path.write_text(\n.join(lines) \n) else: # 沒(méi)有有效標(biāo)簽的空文件也要寫(xiě)出來(lái)YOLO用它作為背景負(fù)樣本 txt_path.write_text()轉(zhuǎn)換腳本有三個(gè)關(guān)鍵設(shè)計(jì)一是label_map必須提前從上一章的標(biāo)簽語(yǔ)義確認(rèn)中拿到類別名不對(duì)應(yīng)會(huì)在轉(zhuǎn)換階段就報(bào)“未知類別”而不是等到訓(xùn)練才炸二是空 txt 文件是合法的在 YOLO 里它表示“這張圖沒(méi)有需要檢測(cè)的目標(biāo)”訓(xùn)練時(shí)會(huì)被當(dāng)作背景圖參與 loss 計(jì)算不用刪除三是w 0 or h 0的過(guò)濾操作這種異常標(biāo)注在人工標(biāo)注數(shù)據(jù)里真實(shí)存在不篩掉會(huì)讓 loss 變成 NaN。腳本跑完后再用find labels_txt -name *.txt | wc -l統(tǒng)計(jì)一次和圖像數(shù)量比對(duì)差距應(yīng)控制在極小范圍內(nèi)。3.3 劃train/val并生成data.yaml分組比隨機(jī)更可靠數(shù)據(jù)集劃分最忌諱粗暴隨機(jī)。水位表計(jì)數(shù)據(jù)通常涉及多臺(tái)設(shè)備多次拍攝如果同一臺(tái)設(shè)備的 3 張圖像分別落在訓(xùn)練集和驗(yàn)證集驗(yàn)證就變成“開(kāi)卷考試”模型看到過(guò)同一場(chǎng)景的不同角度得分虛高。正確的做法是按設(shè)備 ID 或拍攝編號(hào)分組整組劃入訓(xùn)練或驗(yàn)證。文件名里如果帶編號(hào)可以解析出來(lái)作為分組鍵。import random import shutil from pathlib import Path img_dir Path(/data/water_meter_dataset/images) txt_dir Path(/data/water_meter_dataset/labels_txt) train_img Path(/data/water_meter_dataset/images/train) train_txt Path(/data/water_meter_dataset/labels_txt/train) val_img Path(/data/water_meter_dataset/images/val) val_txt Path(/data/water_meter_dataset/labels_txt/val) for d in [train_img, train_txt, val_img, val_txt]: d.mkdir(parentsTrue, exist_okTrue) # 假設(shè)文件名格式為 device_001_frame01.jpg按設(shè)備分組 files list(img_dir.glob(*.jpg)) groups {} for f in files: device_id f.stem.split(_)[1] # 提取001作為分組鍵 groups.setdefault(device_id, []).append(f) all_files [] for device_id, imgs in groups.items(): random.shuffle(imgs) # 每臺(tái)設(shè)備80%的圖像進(jìn)訓(xùn)練20%進(jìn)驗(yàn)證 split_idx int(len(imgs) * 0.8) all_files.append((imgs[:split_idx], train)) all_files.append((imgs[split_idx:], val)) # 按組移動(dòng)文件標(biāo)簽與圖像同步遷移 for subset, split in all_files: for img_path in subset: txt_path txt_dir / f{img_path.stem}.txt if split train: shutil.copy(img_path, train_img) shutil.copy(txt_path, train_txt) else: shutil.copy(img_path, val_img) shutil.copy(txt_path, val_txt)按設(shè)備分組的邏輯是split(_)[1]提取出設(shè)備編號(hào)整組一起劃分保證同一臺(tái)表計(jì)只出現(xiàn)在一個(gè)集合里。這種做法的代價(jià)是可能造成某個(gè)類別在訓(xùn)練集和驗(yàn)證集數(shù)量不均衡——比如設(shè)備 003 恰好全是“測(cè)量值”類別而它全分到了驗(yàn)證集。所以分組后要看一眼兩個(gè)集合里各類別的數(shù)量偏差大就調(diào)一下分組粒度。最后一步是寫(xiě)data.yaml這是 YOLO 訓(xùn)練器讀取數(shù)據(jù)配置的入口。# data.yaml path: /data/water_meter_dataset train: images/train val: images/val nc: 2 names: 0: meter_reading 1: meter_numberpath是數(shù)據(jù)集根目錄train和val是相對(duì)路徑nc必須和names里的數(shù)量嚴(yán)格一致。這里最容易犯的錯(cuò)是nc寫(xiě)成目標(biāo)的種類數(shù)而不是類別的最大編號(hào)加一如果你的類別編號(hào)是 0 和 1那nc2沒(méi)錯(cuò)但如果標(biāo)簽里有類別 5nc至少要寫(xiě) 6否則訓(xùn)練器直接報(bào) class id out of range。4. 用YOLOv8跑通訓(xùn)練選模型、調(diào)參、盯損失曲線4.1 模型選型與最小訓(xùn)練命令689 張圖像屬于典型的小規(guī)模專項(xiàng)數(shù)據(jù)集模型選型不需要一上來(lái)就上 YOLOv8x。我一般會(huì)用yolov8s起步它在精度和訓(xùn)練速度之間最平衡如果你只想驗(yàn)證流程yolov8n更快精度略低但足以判斷數(shù)據(jù)集是否靠譜。安裝依賴和啟動(dòng)訓(xùn)練的命令如下# 安裝ultralytics庫(kù)自動(dòng)帶上yolo命令行工具 pip install ultralytics # 用yolov8s預(yù)訓(xùn)練權(quán)重啟動(dòng)訓(xùn)練 yolo detect train datadata.yaml modelyolov8s.pt epochs120 imgsz640 batch16 device0這里解釋一下modelyolov8s.pt的含義它是 COCO 數(shù)據(jù)集上預(yù)訓(xùn)練好的模型內(nèi)部帶著 coco80 那 80 個(gè)類別的輸出頭但訓(xùn)練器在加載時(shí)會(huì)自動(dòng)把輸出層替換成你data.yaml里nc2的結(jié)構(gòu)所以類別不一致不需要你手動(dòng)改任何代碼。預(yù)訓(xùn)練權(quán)重復(fù)用的是前幾層提取通用特征的能力對(duì)這個(gè)小數(shù)據(jù)集特別重要——從零訓(xùn)練 689 張圖基本必過(guò)擬合。如果你在 PyCharm 這類 IDE 里操作直接把這兩條命令放進(jìn)終端窗口跑就行和命令行沒(méi)有區(qū)別只是注意工作目錄要切換到data.yaml所在的文件夾。4.2 必調(diào)參數(shù)imgsz、batch、epochs與增強(qiáng)開(kāi)關(guān)小數(shù)據(jù)集訓(xùn)練參數(shù)不能照搬 COCO 的大路配置。imgsz 影響最直接水位表盤(pán)在畫(huà)面里通常占比較大640 足夠但如果你的目標(biāo)是識(shí)別表盤(pán)上的小字讀數(shù)數(shù)字區(qū)域可能只有幾十個(gè)像素建議把 imgsz 提到 960代價(jià)是顯存占用約翻倍。batch 默認(rèn)是自動(dòng)模式8GB 顯存我建議手動(dòng)固定到 16。epochs 對(duì)這種規(guī)模設(shè) 120 比較合適開(kāi)啟早停后它會(huì)自己停止。參數(shù)建議值說(shuō)明modelyolov8s.pt小數(shù)據(jù)集用s起步驗(yàn)證完畢后可換m提高精度imgsz640數(shù)字很小、需要摳細(xì)節(jié)時(shí)調(diào)到960batch168G顯存以下建議不超過(guò)16OOM時(shí)降到8epochs120配合早停實(shí)際通常在80輪左右收斂patience20驗(yàn)證集指標(biāo)連續(xù)20輪不升即停止mosaic0.5表盤(pán)數(shù)字密集時(shí)mosaic會(huì)把目標(biāo)切成碎片建議調(diào)低cos_lrTrue余弦退火調(diào)度后半段收斂更平穩(wěn)這里重點(diǎn)說(shuō) mosaic。YOLOv8 默認(rèn)開(kāi)啟 mosaic它把四張圖拼成一張訓(xùn)練能顯著提升泛化但水位表是強(qiáng)結(jié)構(gòu)目標(biāo)拼接后經(jīng)常把讀數(shù)字符切成一半讓模型學(xué)到“數(shù)字?jǐn)嚅_(kāi)了也算一個(gè)目標(biāo)”。這類現(xiàn)場(chǎng)經(jīng)驗(yàn)很少寫(xiě)進(jìn)文檔只能靠損失曲線觀察——如果訓(xùn)練集的 cls_loss 一直降不下來(lái)試著把mosaic0.5或直接關(guān)掉。4.3 盯住yolo損失函數(shù)什么情況算健康什么情況算翻車訓(xùn)練目錄下會(huì)自動(dòng)生成results.png和results.csv里面有三個(gè) loss 曲線對(duì)應(yīng) yolo 損失函數(shù)的三個(gè)組成部分box_loss 負(fù)責(zé)框的位置和大小回歸cls_loss 負(fù)責(zé)類別分類dfl_loss 是分布焦點(diǎn)損失負(fù)責(zé)邊框的精細(xì)調(diào)整。健康訓(xùn)練的曲線特征很典型前 20 個(gè) epoch 三條 loss 直線下降之后轉(zhuǎn)入緩慢下降期驗(yàn)證集曲線與訓(xùn)練集保持同步如果驗(yàn)證集的 box_loss 在某個(gè) epoch 開(kāi)始掉頭向上訓(xùn)練集還在降那就是過(guò)擬合的信號(hào)。# 查看訓(xùn)練輸出目錄results.png就是損失曲線圖 ls runs/detect/train/ # 打開(kāi)結(jié)果csv用任意工具看最后幾行 tail -5 runs/detect/train/results.csv看曲線的時(shí)候還要配合兩個(gè)指標(biāo)mAP50 和 mAP50-95。mAP50 達(dá)到 0.7 以上說(shuō)明框基本打準(zhǔn)了mAP50-95 更能反映框的精細(xì)度——如果前者高后者低說(shuō)明框的位置“差不多但不精確”這在讀數(shù)字任務(wù)里是致命的因?yàn)閿?shù)字識(shí)別需要精確的裁剪區(qū)域。另外訓(xùn)練結(jié)束后打開(kāi)confusion_matrix.png看一下“測(cè)量值”和“編號(hào)”這兩個(gè)類別有沒(méi)有互相混淆。這類業(yè)務(wù)相關(guān)的類別人眼里分得很清模型卻容易搞混一旦混淆矩陣?yán)锍霈F(xiàn)明顯的非對(duì)角線亮塊要么增加對(duì)應(yīng)樣本要么考慮合并類別。5. 水位讀數(shù)數(shù)據(jù)集的五個(gè)踩坑點(diǎn)現(xiàn)象、原因、解決5.1 現(xiàn)場(chǎng)圖mAP不低卻框不準(zhǔn)問(wèn)題出在成像差異現(xiàn)象驗(yàn)證集上 mAP50 有 0.85看起來(lái)很不錯(cuò)把模型部署到現(xiàn)場(chǎng)去測(cè)同一個(gè)表盤(pán)就是框不準(zhǔn)數(shù)字要么框大要么框小。 原因數(shù)據(jù)集的圖像大多是近距離、順光拍攝的現(xiàn)場(chǎng)是隔著防護(hù)網(wǎng)、逆光甚至帶玻璃反光拍的成像差異比想象中大得多。 解決不要只在數(shù)據(jù)集上磨參數(shù)。從現(xiàn)場(chǎng)截幾十張孤立的圖按 YOLO 格式手工標(biāo)一版混入原數(shù)據(jù)集重新訓(xùn)練。這個(gè)增量訓(xùn)練的成本很低但效果立竿見(jiàn)影。推理時(shí)還得檢查預(yù)處理確保預(yù)測(cè)命令里imgsz和訓(xùn)練時(shí)一致YOLO 用的 letterbox 會(huì)補(bǔ)邊而不是拉伸如果被人為改成 resize框的坐標(biāo)就會(huì)偏移。5.2 class id out of range類別編號(hào)沒(méi)對(duì)齊現(xiàn)象訓(xùn)練剛開(kāi)始就彈AssertionError: class id out of range。 原因標(biāo)簽 txt 里出現(xiàn)了大于等于nc的類別編號(hào)常見(jiàn)于類別從 1 開(kāi)始編號(hào)但 yaml 里nc1只留了一個(gè)位置或者數(shù)據(jù)集中混入了其他數(shù)據(jù)集的標(biāo)簽文件。 解決先用一條命令掃出標(biāo)簽里的最大類別編號(hào)# 統(tǒng)計(jì)所有txt標(biāo)簽里最大類別編號(hào)確保小于nc awk {if($1max) max$1} END {print max class id:, max} labels_txt/*.txt如果最大值是 2但data.yaml里nc2那就是編號(hào) 2 溢出有效編號(hào)只能是 0 和 1。要么改 yaml 把nc3要么重新寫(xiě)標(biāo)簽把類別映射到 0 和 1。別嫌這個(gè)檢查麻煩在打包傳輸?shù)臄?shù)據(jù)集里混入一兩個(gè)不匹配文件是常有的事。5.3 模型只認(rèn)豎裝表盤(pán)數(shù)據(jù)增強(qiáng)和劃分的雙重責(zé)任現(xiàn)象訓(xùn)練集里 90% 的表盤(pán)是豎裝橫裝樣本只有幾十張結(jié)果模型對(duì)橫裝表盤(pán)漏檢率極高。 原因數(shù)據(jù)集天然不平衡加上默認(rèn)增強(qiáng)里旋轉(zhuǎn)角度很小橫裝模式特征沒(méi)被補(bǔ)出來(lái)。 解決劃分?jǐn)?shù)據(jù)集時(shí)就要按朝向分組保證驗(yàn)證集里也有橫裝樣本如果橫裝樣本實(shí)在少90 度旋轉(zhuǎn)復(fù)制一份加進(jìn)訓(xùn)練集。但要注意旋轉(zhuǎn)后的標(biāo)簽坐標(biāo)要跟著變換不能直接把圖像轉(zhuǎn)了而標(biāo)簽不轉(zhuǎn)否則就是白白給模型喂錯(cuò)誤信息。手工實(shí)現(xiàn) 90 度旋轉(zhuǎn)的數(shù)據(jù)增強(qiáng)腳本不復(fù)雜但計(jì)算坐標(biāo)時(shí)要小心寬高互換。5.4 EXIF旋轉(zhuǎn)讓標(biāo)簽錯(cuò)位看圖軟件和訓(xùn)練器各讀各的現(xiàn)象訓(xùn)練時(shí) loss 正常但可視化預(yù)測(cè)時(shí)發(fā)現(xiàn)框整體偏轉(zhuǎn) 90 度而且只發(fā)生在部分圖像上。 原因手機(jī)或部分工業(yè)相機(jī)拍的照片帶 EXIF 方向信息看圖軟件會(huì)自動(dòng)旋轉(zhuǎn)顯示但 OpenCV 的imread默認(rèn)忽略 EXIF訓(xùn)練器拿到的實(shí)際圖像和標(biāo)注工具的“所見(jiàn)”不一致。 解決在標(biāo)注階段就把方向固話。先批量把圖像轉(zhuǎn)正再開(kāi)始標(biāo)注如果數(shù)據(jù)集已經(jīng)打包好寫(xiě)一個(gè)預(yù)處理腳本對(duì)帶 EXIF 方向標(biāo)記的圖像執(zhí)行旋轉(zhuǎn)并去除方向標(biāo)記字段from PIL import Image, ExifTags for img_path in image_list: img Image.open(img_path) exif img.getexif() orientation exif.get(274) # 274是方向標(biāo)記的標(biāo)簽id if orientation 6: img img.rotate(-90, expandTrue) elif orientation 8: img img.rotate(90, expandTrue) img.save(img_path)這個(gè)坑隱蔽在“部分圖像”上只影響少數(shù)文件但一粒老鼠屎就能把訓(xùn)練集攪渾。處理完后再跑一次訓(xùn)練錯(cuò)位問(wèn)題就消失了。5.5 XML空標(biāo)簽與負(fù)樣本漏標(biāo)文件到底怎么處理現(xiàn)象標(biāo)簽文件數(shù)量對(duì)得上但訓(xùn)練日志里顯示某幾張圖的 cls_loss 異常高打開(kāi)一看xml 標(biāo)簽文件內(nèi)容為空或只有根節(jié)點(diǎn)沒(méi)有任何object。 原因打包數(shù)據(jù)集時(shí)把一些漏標(biāo)的圖像也放了進(jìn)來(lái)xml 文件被創(chuàng)建但沒(méi)寫(xiě)入內(nèi)容。 解決用 3.2 的轉(zhuǎn)換腳本時(shí)空 xml 會(huì)被跳過(guò)去而對(duì)應(yīng)的 txt 文件為空???txt 在 YOLO 里合法會(huì)被當(dāng)負(fù)樣本參與訓(xùn)練防止模型在無(wú)目標(biāo)區(qū)域亂框這不一定是壞事。但你得分清是“故意放的背景圖”還是“漏標(biāo)的真樣本”——如果是后者這張圖里的表盤(pán)就被模型學(xué)成了背景推理時(shí)遇到相似表盤(pán)會(huì)漏檢。最穩(wěn)妥的辦法是轉(zhuǎn)換后人工抽查幾十個(gè)空 txt 文件確認(rèn)對(duì)應(yīng)圖像確實(shí)沒(méi)有目標(biāo)。6. 最后一公里檢測(cè)框如何變成真實(shí)讀數(shù)6.1 先排序再識(shí)別把檢測(cè)框拼成讀數(shù)串模型輸出的是一個(gè)個(gè)檢測(cè)框但它不會(huì)告訴你讀數(shù)是多少。要把框變成數(shù)字常規(guī)做法是先用 YOLO 檢測(cè)出讀數(shù)字符區(qū)域再按 x 坐標(biāo)從左到右排序裁剪出每張數(shù)字圖送入 OCR 模塊識(shí)別。這個(gè)流程比端到端的讀數(shù)識(shí)別更靈活訓(xùn)練成本也低得多。from ultralytics import YOLO from PIL import Image model YOLO(runs/detect/train/weights/best.pt) img Image.open(現(xiàn)場(chǎng)圖.jpg) results model.predict(img, conf0.35, imgsz640) boxes results[0].boxes.xyxy.cpu().numpy() clss results[0].boxes.cls.cpu().numpy().astype(int) names model.names # 只取讀數(shù)區(qū)域的框按x坐標(biāo)排序 reading_boxes [] for b, c in zip(boxes, clss): if names[c] meter_reading: reading_boxes.append(b) reading_boxes.sort(keylambda x: x[0]) # 從左到右 # 逐個(gè)裁剪并送OCR這里簡(jiǎn)化為保存裁剪圖 for i, b in enumerate(reading_boxes): xmin, ymin, xmax, ymax [int(v) for v in b] crop img.crop((xmin, ymin, xmax, ymax)) crop.save(fcrops/digit_{i}.jpg)排序這一步看著簡(jiǎn)單實(shí)際上 Python 的sort默認(rèn)按元組第一個(gè)元素排正好符合讀數(shù)從左到右的順序。但如果表盤(pán)是多行數(shù)字比如測(cè)量值在上、最大值在下光按 x 排序會(huì)把兩行混在一起。這時(shí)候要加一個(gè)按 y 坐標(biāo)聚類分行的邏輯先在sort(keylambda x: (x[1] // 50, x[0]))這種寫(xiě)法里分行再組串。6.2 用“最大值”做量程校驗(yàn)與多幀投票檢測(cè)和識(shí)別都跑通之后還要利用標(biāo)簽里“最大值”這個(gè)信息給結(jié)果兜底。每個(gè)表盤(pán)都有量程比如最大量程 10 米那么識(shí)別出的讀數(shù)理論范圍是 0 到 10。如果輸出的讀數(shù)是 12.5一定是識(shí)別錯(cuò)了——可能是小數(shù)點(diǎn)位置錯(cuò)可能是數(shù)字 1 被誤讀成 7。常見(jiàn)的兜底策略是讀數(shù)超過(guò)量程的 1.05 倍時(shí)重新推理一次連續(xù)兩次超限就丟棄該幀等下幀再試。多幀識(shí)別后取中位數(shù)作為最終讀數(shù)能有效過(guò)濾掉偶發(fā)誤檢。這類數(shù)據(jù)集的真正價(jià)值不在于訓(xùn)練出一個(gè) 100% 完美的模型而在于幫你把一個(gè)完整的數(shù)據(jù)處理鏈路跑通數(shù)據(jù)集解壓、標(biāo)簽清洗、格式轉(zhuǎn)換、訓(xùn)練調(diào)參、誤差兜底。我在現(xiàn)場(chǎng)做過(guò)類似的項(xiàng)目模型在辦公室演示環(huán)境里跑得很漂亮一上工地每隔半小時(shí)錯(cuò)一張最后發(fā)現(xiàn)是太陽(yáng)斜射造成表盤(pán)玻璃反光。后來(lái)在程序里加了“讀數(shù)超過(guò)量程就連續(xù)重試 3 幀”的規(guī)則問(wèn)題才徹底消失。這類血淚經(jīng)驗(yàn)很難靠數(shù)據(jù)集自帶文檔避開(kāi)只能在實(shí)際部署中一點(diǎn)點(diǎn)補(bǔ)——希望幫你在這個(gè)方向少踩幾個(gè)坑。本文還有配套的精品資源點(diǎn)擊獲取