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