戰(zhàn):501張圖達(dá)到99.3%準(zhǔn)確率)
簡介這份撲克牌識別數(shù)據(jù)集面向計(jì)算機(jī)視覺學(xué)習(xí)者與目標(biāo)檢測開發(fā)者用于訓(xùn)練可同時(shí)識別牌面數(shù)字與花色的模型適用于YOLO系列實(shí)戰(zhàn)、課程設(shè)計(jì)或小型競賽場景。資源包共1003個(gè)文件包含501張?jiān)糺pg圖像、501個(gè)同名txt標(biāo)注文件及1個(gè)yaml配置文件壓縮包約11.82MBtxt為YOLO v8格式的邊界框標(biāo)注yaml用于定義數(shù)據(jù)集路徑與類別jpg即原始牌面圖像整體結(jié)構(gòu)規(guī)整可直接接入訓(xùn)練流程。據(jù)描述該數(shù)據(jù)集在數(shù)字與花色識別任務(wù)上正確識別率可達(dá)99.3%樣本覆蓋多種牌面組合便于快速驗(yàn)證模型效果。目前已有112人學(xué)習(xí)下載適合希望以較小數(shù)據(jù)量完成端到端檢測實(shí)驗(yàn)、對比不同YOLO版本性能或作為數(shù)據(jù)增強(qiáng)與遷移學(xué)習(xí)起點(diǎn)的讀者參考使用。1. 撲克牌識別數(shù)據(jù)集501 張?jiān)紙D怎么撐起 99.3% 的識別率手里有一批撲克牌照片想讓模型同時(shí)認(rèn)出「數(shù)字」和「花色」這件事聽起來簡單做起來處處是坑。數(shù)字只有 13 類花色只有 4 類可一旦把兩者組合起來就是 52 類目標(biāo)再加上不同角度、光照、遮擋、牌面反光難度立刻上一個(gè)臺(tái)階。這個(gè)撲克牌識別數(shù)據(jù)集給了一個(gè)很具體的起點(diǎn)501 張?jiān)紙D按 yolo v8 格式標(biāo)注官方口徑的正確識別率可達(dá) 99.3%。對做目標(biāo)檢測落地的人來說這不是一個(gè)玩具數(shù)據(jù)集而是一個(gè)能快速驗(yàn)證「小樣本 多類別 細(xì)粒度」方案的試驗(yàn)田。它適合誰一是想跑通 yolo v8 訓(xùn)練自己數(shù)據(jù)集全流程的新手501 張圖規(guī)模不大單卡幾十分鐘就能出第一版權(quán)重二是做牌類游戲輔助、發(fā)牌機(jī)器人、桌面視覺統(tǒng)計(jì)的工程師需要一套能直接復(fù)現(xiàn)的基線三是研究細(xì)粒度檢測的人撲克牌的數(shù)字和花色在視覺上高度相似正好用來壓榨模型的特征分辨能力。接下來我會(huì)按「數(shù)據(jù)長什么樣 → 怎么轉(zhuǎn)成 yolo v8 能吃的格式 → 訓(xùn)練參數(shù)怎么設(shè) → 翻車點(diǎn)在哪 → 怎么把 99.3% 穩(wěn)住」這條線把能抄的步驟和參數(shù)都攤開。2. 先看清數(shù)據(jù)501 張?jiān)紙D的標(biāo)注結(jié)構(gòu)與類別設(shè)計(jì)2.1 撲克牌識別的兩類標(biāo)注思路單標(biāo)簽 52 類 vs 雙標(biāo)簽組合拿到撲克牌數(shù)據(jù)第一件事不是急著訓(xùn)練而是決定標(biāo)注體系。常見做法有兩種一種是把「紅桃 A」當(dāng)成一個(gè)獨(dú)立類別總共 52 類另一種是拆成數(shù)字和花色兩個(gè)維度檢測框只標(biāo)牌面再用兩個(gè)分類頭分別預(yù)測數(shù)字和花色。這個(gè)數(shù)據(jù)集走的是前一種思路標(biāo)注文件里直接給出 52 類中的某一類yolo v8 格式一行一個(gè)目標(biāo)格式是class_id x_center y_center width height坐標(biāo)全部歸一化到 0 到 1。為什么小樣本更適合單標(biāo)簽 52 類因?yàn)?501 張圖分?jǐn)偟?52 類平均每類不到 10 個(gè)樣本如果再做雙頭結(jié)構(gòu)每個(gè)頭的正樣本更少訓(xùn)練時(shí)容易某一維塌縮。單標(biāo)簽把數(shù)字和花色的組合當(dāng)成一個(gè)整體模型學(xué)的是「這張牌長什么樣」而不是「先認(rèn)數(shù)字再認(rèn)花色」在數(shù)據(jù)量不足時(shí)反而更穩(wěn)。代價(jià)是類別數(shù)多分類層參數(shù)量上升但對 yolo v8 這種 anchor-free 結(jié)構(gòu)來說52 類并不算負(fù)擔(dān)。提示如果你的場景里撲克牌種類固定且每類樣本能到 50 張以上雙標(biāo)簽組合會(huì)更靈活因?yàn)樾略鲆环N花色或數(shù)字時(shí)不用重新標(biāo)全部數(shù)據(jù)。501 張這個(gè)量級建議先按單標(biāo)簽 52 類跑通。2.2 yolo v8 標(biāo)注格式逐字段拆解與目錄組織yolo v8 的標(biāo)注文件是純文本每行代表一個(gè)目標(biāo)字段之間用空格分隔。以一張包含「黑桃 10」的圖為例標(biāo)注行可能是47 0.512 0.634 0.180 0.260。第一個(gè)數(shù)字 47 是類別索引對應(yīng)你data.yaml里 names 列表的第 48 個(gè)名字后面四個(gè)數(shù)分別是框中心 x、中心 y、框?qū)?、框高全部除以圖像寬高做歸一化。這里最容易翻車的是類別索引和 names 順序?qū)Σ簧嫌?xùn)練時(shí) loss 能降但推理出來全是錯(cuò)類。目錄組織上我一般會(huì)保持這樣的結(jié)構(gòu)讓 yolo v8 的默認(rèn) dataloader 直接認(rèn)poker_dataset/ ├── images/ │ ├── train/ # 訓(xùn)練圖約 400 張 │ └── val/ # 驗(yàn)證圖約 101 張 ├── labels/ │ ├── train/ # 與 train 圖同名的 .txt │ └── val/ └── data.yamldata.yaml里寫清路徑和類別名注意path用絕對路徑或相對于訓(xùn)練腳本的路徑train和val指向 images 下的子目錄yolo v8 會(huì)自動(dòng)去找同級的 labels。path: /home/user/poker_dataset train: images/train val: images/val nc: 52 names: 0: spade_A 1: spade_2 # ... 依次補(bǔ)齊 52 類 51: heart_K字段說明nc必須等于 names 的長度少一個(gè)都會(huì)在訓(xùn)練啟動(dòng)時(shí)報(bào)索引越界names 的順序必須和標(biāo)注文件里的 class_id 嚴(yán)格一致建議用腳本生成而不是手寫。501 張圖按 8:2 切分訓(xùn)練集 400 張、驗(yàn)證集 101 張這個(gè)比例在小樣本下比較穩(wěn)驗(yàn)證集太小會(huì)導(dǎo)致 mAP 波動(dòng)大太大又浪費(fèi)訓(xùn)練樣本。2.3 用腳本檢查標(biāo)注完整性三個(gè)必查項(xiàng)在訓(xùn)練之前我習(xí)慣跑一個(gè)檢查腳本把三類問題提前揪出來標(biāo)注文件缺失、坐標(biāo)越界、類別索引超出 nc。這三類問題在 501 張規(guī)模的數(shù)據(jù)里很常見尤其是從其他格式轉(zhuǎn)過來的時(shí)候。import os from pathlib import Path img_dir Path(poker_dataset/images/train) lbl_dir Path(poker_dataset/labels/train) nc 52 errors [] for img in img_dir.glob(*.jpg): lbl lbl_dir / (img.stem .txt) if not lbl.exists(): errors.append(f缺標(biāo)注: {img.name}) continue for line in lbl.read_text().strip().splitlines(): parts line.split() if len(parts) ! 5: errors.append(f字段數(shù)錯(cuò): {lbl.name} - {line}) continue cid int(parts[0]) x, y, w, h map(float, parts[1:]) if cid 0 or cid nc: errors.append(f類別越界: {lbl.name} cid{cid}) if not (0 x 1 and 0 y 1 and 0 w 1 and 0 h 1): errors.append(f坐標(biāo)異常: {lbl.name} - {line}) print(f共發(fā)現(xiàn) {len(errors)} 個(gè)問題) for e in errors[:20]: print(e)邏輯說明遍歷訓(xùn)練圖逐個(gè)找同名 txt檢查每行字段數(shù)、類別索引范圍、歸一化坐標(biāo)是否落在合法區(qū)間。參數(shù)上nc要和 data.yaml 一致坐標(biāo)檢查里寬高必須大于 0中心點(diǎn)允許等于 0 或 1貼邊目標(biāo)。跑完如果輸出 0 個(gè)問題說明標(biāo)注基本干凈如果有越界優(yōu)先懷疑轉(zhuǎn)換腳本把像素坐標(biāo)直接寫進(jìn)去了沒有除以圖像寬高。3. 從原始圖到 yolo v8 可訓(xùn)練轉(zhuǎn)換、劃分與增強(qiáng)配置3.1 把 VOC 或 COCO 標(biāo)注轉(zhuǎn)成 yolo v8 格式的腳本很多撲克牌數(shù)據(jù)最初是 VOC 的 XML 或 COCO 的 JSON要喂給 yolo v8 必須先轉(zhuǎn)。轉(zhuǎn)換的核心就一件事把絕對像素坐標(biāo)變成歸一化中心點(diǎn)加寬高。下面這個(gè)腳本處理 VOC 格式COCO 只需把讀取部分換成 json 解析邏輯一樣。import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image classes [spade_A, spade_2, ...] # 52 類順序固定 cls2id {c: i for i, c in enumerate(classes)} def voc_to_yolo(xml_path, img_path, out_path): tree ET.parse(xml_path) root tree.getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in cls2id: continue 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) xc (xmin xmax) / 2 / w yc (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls2id[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) out_path.write_text(\n.join(lines)) for xml in Path(voc_annotations).glob(*.xml): img Path(images) / (xml.stem .jpg) voc_to_yolo(xml, img, Path(labels) / (xml.stem .txt))邏輯說明先讀 XML 里的圖像寬高再對每個(gè) object 取 bbox 四個(gè)角點(diǎn)算中心點(diǎn)和寬高后除以寬高歸一化。參數(shù)上classes列表順序就是最終 class_id必須和 data.yaml 完全一致保留 6 位小數(shù)足夠yolo v8 內(nèi)部會(huì)再處理。轉(zhuǎn)換完務(wù)必用 2.3 的檢查腳本過一遍我見過太多因?yàn)?XML 里 name 拼寫不一致導(dǎo)致某類直接消失的情況。3.2 訓(xùn)練集驗(yàn)證集劃分別讓同一張牌的兩個(gè)角度分到兩邊501 張圖如果隨機(jī)切分很容易出現(xiàn)「同一張牌的不同拍攝角度」被分到訓(xùn)練和驗(yàn)證兩邊驗(yàn)證集 mAP 虛高實(shí)際部署就翻車。我的做法是按牌面組合分層抽樣先統(tǒng)計(jì)每類出現(xiàn)的圖片保證每個(gè)類別在驗(yàn)證集里至少有一張同時(shí)同一張物理牌的不同角度盡量只進(jìn)一邊。如果數(shù)據(jù)里同一張牌有多個(gè)角度建議按「牌」分組后再切分而不是按圖隨機(jī)。import random from pathlib import Path from collections import defaultdict random.seed(42) img_dir Path(poker_dataset/images/all) imgs list(img_dir.glob(*.jpg)) # 按類別分組這里簡化為按文件名前綴分組實(shí)際按標(biāo)注統(tǒng)計(jì) groups defaultdict(list) for img in imgs: key img.stem.split(_)[0] # 假設(shè)文件名含牌面標(biāo)識 groups[key].append(img) train, val [], [] for key, items in groups.items(): random.shuffle(items) split max(1, int(len(items) * 0.2)) val.extend(items[:split]) train.extend(items[split:]) print(ftrain{len(train)} val{len(val)})邏輯說明用文件名前綴模擬「同一張牌」的分組實(shí)際項(xiàng)目里應(yīng)該根據(jù)標(biāo)注內(nèi)容聚類。參數(shù)0.2是驗(yàn)證集比例seed42保證可復(fù)現(xiàn)。切分后把圖復(fù)制到 images/train 和 images/val標(biāo)注同步復(fù)制再核對一遍兩邊類別分布避免某類只在訓(xùn)練集出現(xiàn)。3.3 yolo v8 訓(xùn)練參數(shù)501 張圖該設(shè)多少 epoch 和 batch數(shù)據(jù)準(zhǔn)備好后用 ultralytics 的 yolo v8 訓(xùn)練命令行一行就能起。501 張圖屬于小樣本參數(shù)不能照搬 COCO 那套。yolo detect train \ datapoker_dataset/data.yaml \ modelyolov8n.pt \ epochs300 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ patience50 \ augmentTrue \ mosaic1.0 \ degrees10.0 \ hsv_h0.015 \ hsv_s0.7 \ hsv_v0.4 \ projectruns/poker \ namev8n_501參數(shù)說明modelyolov8n.pt用 nano 版501 張圖不需要大模型n 版在單卡上幾分鐘一個(gè) epochepochs300配合patience50驗(yàn)證集 50 輪不漲就早停避免過擬合imgsz640是 yolo v8 默認(rèn)撲克牌在圖中占比不小640 夠用如果牌很小可以提到 960batch16在 8G 顯存上穩(wěn)顯存大可加到 32lr00.01是初始學(xué)習(xí)率小樣本別設(shè)太大否則 loss 震蕩mosaic1.0開啟馬賽克增強(qiáng)對小樣本很關(guān)鍵能顯著提升泛化degrees10.0做小角度旋轉(zhuǎn)撲克牌擺放角度多變這個(gè)增強(qiáng)很值HSV 三參數(shù)控制顏色抖動(dòng)應(yīng)對不同光照。訓(xùn)練啟動(dòng)后重點(diǎn)看三個(gè)指標(biāo)box_loss是否穩(wěn)定下降、mAP50是否在 100 輪后進(jìn)入平臺(tái)、驗(yàn)證集cls_loss是否明顯高于訓(xùn)練集過擬合信號。如果 mAP50 卡在 0.8 上不去先別調(diào)模型回頭查標(biāo)注里有沒有類別混淆尤其是數(shù)字 6 和 9、方塊和紅桃這種視覺相近的。4. 99.3% 識別率背后的避坑與排查清單4.1 現(xiàn)象訓(xùn)練 mAP 很高推理卻把紅桃認(rèn)成方塊原因驗(yàn)證集和訓(xùn)練集來自同一批拍攝條件模型學(xué)到了背景或光照的捷徑而不是牌面本身的特征。花色里紅桃和方塊都是紅色形狀差異在低分辨率下被抹平模型靠顏色區(qū)分一旦換光照就崩。解決在增強(qiáng)里加大hsv_h和hsv_s讓顏色通道抖動(dòng)更劇烈逼模型看形狀同時(shí)把驗(yàn)證集換成不同光照下拍的圖哪怕只有幾十張也能暴露問題。如果條件允許把紅桃和方塊的樣本單獨(dú)抽出來做混淆矩陣看錯(cuò)分集中在哪幾類。4.2 現(xiàn)象某些類別 mAP 為 0訓(xùn)練日志里也沒報(bào)錯(cuò)原因這些類別的標(biāo)注 class_id 超出了nc或者 names 列表里名字拼寫和標(biāo)注不一致yolo v8 在加載時(shí)靜默跳過不報(bào)錯(cuò)。501 張圖里如果某類只有兩三個(gè)樣本更容易被忽略。解決跑 2.3 的檢查腳本重點(diǎn)看類別索引分布再用yolo detect val輸出每類 APAP 為 0 的類逐個(gè)核對標(biāo)注。我一般會(huì)統(tǒng)計(jì)每個(gè) class_id 的標(biāo)注框數(shù)量少于 5 個(gè)的類要么補(bǔ)數(shù)據(jù)要么合并到相近類。4.3 現(xiàn)象訓(xùn)練到 200 輪后驗(yàn)證 loss 開始上升mAP 反而降原因小樣本過擬合。501 張圖對 52 類來說平均每類不到 10 個(gè)樣本模型在 150 輪左右就記住了訓(xùn)練集之后開始背噪聲。解決把patience從 50 降到 30早停更敏感開啟dropoutyolo v8 里通過dropout0.1設(shè)置減小模型從 yolov8s 換回 yolov8n增強(qiáng)里把mosaic保持 1.0再加mixup0.1。如果還壓不住說明數(shù)據(jù)量確實(shí)是瓶頸考慮用同一批牌多拍幾個(gè)角度補(bǔ)到 800 張以上。4.4 現(xiàn)象推理時(shí)一張圖里多張牌漏檢靠近邊緣的牌原因yolo v8 對貼邊目標(biāo)的回歸能力弱標(biāo)注時(shí)如果框貼邊歸一化坐標(biāo)接近 0 或 1訓(xùn)練時(shí)容易被裁掉。另外imgsz太小邊緣目標(biāo)下采樣后特征消失。解決標(biāo)注時(shí)給貼邊目標(biāo)留 2 到 3 像素余量訓(xùn)練時(shí)把imgsz提到 960推理時(shí)降低conf閾值到 0.15 觀察是否召回如果召回但誤檢多再用 NMS 的iou參數(shù)壓。我一般會(huì)在推理腳本里把conf0.25, iou0.45作為起點(diǎn)根據(jù)實(shí)際漏檢情況微調(diào)。4.5 現(xiàn)象換一臺(tái)機(jī)器或換一批牌識別率從 99.3% 掉到 80%原因99.3% 是在特定數(shù)據(jù)集分布下測的牌面印刷、相機(jī)白平衡、桌面背景一變特征分布就漂移。這不是模型的問題是數(shù)據(jù)覆蓋度的問題。解決部署前用目標(biāo)場景的圖做一次小樣本微調(diào)哪怕只有 50 張凍結(jié) backbone 只訓(xùn) head學(xué)習(xí)率設(shè) 0.00120 輪就能拉回來。同時(shí)把推理時(shí)的預(yù)處理對齊訓(xùn)練配置比如訓(xùn)練用了 letterbox推理也要 letterbox別直接 resize否則長寬比一變框就偏了。5. 把 99.3% 復(fù)現(xiàn)出來驗(yàn)證腳本與一個(gè)提點(diǎn)技巧訓(xùn)練完拿到best.pt別急著信日志里的 mAP自己寫一個(gè)驗(yàn)證腳本按類別統(tǒng)計(jì)準(zhǔn)確率才能知道 99.3% 到底落在哪。下面這個(gè)腳本用 ultralytics 的 API 跑驗(yàn)證集輸出每類 AP 和整體混淆矩陣。from ultralytics import YOLO import numpy as np model YOLO(runs/poker/v8n_501/weights/best.pt) metrics model.val(datapoker_dataset/data.yaml, imgsz640, conf0.25, iou0.45) print(fmAP50: {metrics.box.map50:.4f}) print(fmAP50-95: {metrics.box.map:.4f}) # 每類 AP for i, ap in enumerate(metrics.box.ap50): if ap 0.9: print(f類別 {i} AP50{ap:.3f} 偏低需補(bǔ)數(shù)據(jù))邏輯說明model.val會(huì)按 data.yaml 的 val 路徑跑一遍返回的metrics.box.ap50是每類 AP 數(shù)組。參數(shù)conf0.25和iou0.45要和部署時(shí)一致否則驗(yàn)證結(jié)果沒有參考意義。跑完重點(diǎn)看 AP 低于 0.9 的類這些就是拉低整體 99.3% 的短板優(yōu)先補(bǔ)它們的樣本。一個(gè)提點(diǎn)技巧撲克牌的數(shù)字和花色在牌面上下兩端是對稱的如果模型對旋轉(zhuǎn)敏感可以在推理前做一次 TTA測試時(shí)增強(qiáng)把原圖和旋轉(zhuǎn) 180 度的圖各跑一次框合并后再 NMS。yolo v8 自帶augmentTrue參數(shù)推理時(shí)加上就能開啟 TTA代價(jià)是速度慢一倍但對小樣本模型通常能提 1 到 2 個(gè)點(diǎn)。我自己的習(xí)慣是只要部署端算力夠推理默認(rèn)開 TTA尤其是牌面可能倒著放的時(shí)候。最后說個(gè)血淚教訓(xùn)別在驗(yàn)證集上反復(fù)調(diào)參調(diào)到 99.9%然后拿這個(gè)數(shù)去匯報(bào)。501 張圖的驗(yàn)證集只有 101 張mAP 波動(dòng)一兩個(gè)點(diǎn)是常事真正靠譜的做法是留一批完全沒參與訓(xùn)練的圖做最終測試哪怕只有 30 張。我一般會(huì)把數(shù)據(jù)切成 train/val/test 三份test 只在最后跑一次跑完就封存這樣得到的數(shù)字才敢寫進(jìn)文檔。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取