中国象棋检测数据集VOC转YOLO训练实战:300张图也能训出可用模型
发布时间:2026/10/11 22:32:24 锦皓数字建站

简介中国象棋检测数据集面向目标检测与棋类识别等应用场景提供三百张棋盘图像的完整标注标签体系覆盖黑红双方的十二种棋子类别适用于模型训练、格式转换练习与算法验证。压缩包共包含九百零二个文件其中有三百张JPG原图、三百个Pascal VOC格式的XML标注文件和三百个YOLO格式的TXT标注文件另有少量说明文本总大小约38.74MB结构清晰、目录规整便于直接解压使用。数据集目前已有三百一十八人学习或下载适合初学者和进阶开发者快速获取规范数据。借助VOC与YOLO双格式标注用户可对比理解两种标注体系的异同并能无缝接入常见检测框架进行训练省去自行采集与标注的时间成本同时清晰的类别命名也便于针对特定棋子做精细化调优。1. 为什么只靠 300 张象棋图我能把一个检测模型训练到能用的地步做棋盘识别、棋谱直播或机器对弈项目最容易被卡住的一步不是算法而是数据。中国象棋的棋子不像行人检测那样有海量公开资源自己拍照标注一套又花时间所以看到这份「中国象棋检测数据集VOCYOLO格式300张12类别.7z」时我的第一反应是能用一个下午把物体检测的完整流程跑通。12 个类别正好覆盖红黑双方的常见兵种300 张图听起来少但在目标检测任务里只要类别不杂、目标尺寸相对规整配合预训练权重和合理的数据增强完全能训练出可用的 640 分辨率模型。这篇笔记就把我拿到这份数据集之后的整个处理过程写清楚从解压格式开始到 VOC 与 YOLO 标注互转、训练参数设置再到小数据集最容易踩的五个坑全部按可复现的命令和脚本走。2. 先弄清这份数据集的两套格式VOC 的 XML 和 YOLO 的 TXT 在表达同一件事2.1 VOC 里每个 XML 文件其实是一张图片的完整答案Pascal VOC 格式是目标检测算法里历史最悠久的标注方式每张 JPEG 图片对应一个同名 XML 文件。打开一个 XML会看到folder、filename、size和一堆object节点。每个object里记录一个目标name指的是该棋子类别bndbox里的xmin / ymin / xmax / ymax是左上角和右下角的绝对像素坐标。比如车被标注成namecar/name还是namerook/name取决于这份数据集的类别命名约定这也是转换格式前必须先检查的一点永远不要默认name标签的顺序和你想的一样。在处理这份 300 张的数据集时我一般先把所有 XML 里出现过的name去重列出全部类名。这个列表就是后面 YOLO 训练时data.yaml里names的唯一依据。若漏看了一个类名训练时就会出现类别 ID 对不齐、mAP 异常低甚至 loss 震荡的问题。严格来说这一步不是算法活却是后面所有工作的地基。2.2 YOLO 的 TXT 与 VOC 的最大差异绝对坐标变成了归一化坐标YOLO 格式每张图片对应一个同名 TXT 文件每一行内容是class_id center_x center_y width height其中坐标全部除以图片宽高做了归一化取值 0 到 1。训练时 YOLO 会直接读取这些归一化值所以转换脚本里的圆弧率一定要正确。容易翻车的点有两个一是 VOC 的bndbox是绝对像素值转 YOLO 时要记得除以width、height二是有些 XML 里图片的宽高是width: 720 height: 480但实际 JPEG 是640x480训练时图像尺寸不一致检测框就会整体偏移。稳妥的做法是先读取图片真实尺寸别信任 XML 里写的size字段。YOLO 的类别 ID 是个整数从 0 开始与data.yaml里的names列表下标挂钩。如果 XML 里识别出的class_names排序是[b, c, m, p, r, x...]其中b是兵、r是车那 YOLO 里车可能对应 ID 4视觉上同样的棋子VOC 和 YOLO 的表达方式完全不同但语义要完全一致这就是转换脚本存在的意义。2.3 12 个类别的常见命名方式和你要不要拆分红黑中国象棋的棋子按兵种与红黑常见做法是拆成 14 个类别红车、黑车都单独成类但不少公开数据集会压缩成 12 类把车、马、炮、兵、卒这类跨阵营同兵种合并再补一个“棋盘背景类”或“空位类”凑成 12。如果你拿到的是 12 类数据集训练前要看清楚names列表里怎么命名。比如说class0: 帅、class1: 仕、class2: 相、class3: 车、class4: 马、class5: 炮、class6: 兵黑方则是class7: 将class8: 士、class9: 象、class10: 卒再加一个class11: 空。但这种分配只是猜的实际情况要以解压后 XML 里的真实name为准所以我第一件事永远是统计类别而不是直接开训。2.4 为什么 300 张图的 12 类目标转换脚本最好先跑一遍冷启动徒手处理标注格式从来不是一遍能成功的事情我第一次跑 VOC 转 YOLO 时也中途翻车遇到xmax比xmin小、XML 里有difficult1的样本被错误算进 YOLO 训练范围等情况。一般做法是把转换脚本设计成幂等、可重复运行的每次都在终端打印“解析到多少张图、跳过多少个无效目标”。整个流程里读 XML、画框、统计类别的脚本可以先跑通再训练不能在转换阶段省事。3. 用 Python 把 VOC 转 YOLO一套能直接跑的转换脚本和四个边界坑3.1 先写一个能复制的 XML 解析脚本通常我会先建一个voc2yolo.py把 XML 路径、图片路径和输出路径做成命令行参数。如果碰到一份大压缩包解压后层级不统一先用find或glob把 XML 列表拉出来路径带不带中文、带不带空格都会影响后面处理。下面是一个简洁、可直接跑的版本适合快速转换。import os import xml.etree.ElementTree as ET from glob import glob # 一份包含类别名到ID映射的字典需要手动按 XML 中出现的 name 调整 CLASS_MAP { 帅: 0, 仕: 1, 相: 2, 车: 3, 马: 4, 炮: 5, 兵: 6, 将: 7, 士: 8, 象: 9, 卒: 10 } voc_root ./VOCdevkit/VOC2012 # 按实际解压路径改 img_dir os.path.join(voc_root, JPEGImages) xml_dir os.path.join(voc_root, Annotations) out_dir ./labels os.makedirs(out_dir, exist_okTrue) xml_list glob(os.path.join(xml_dir, *.xml)) skip_count 0 for xml_path in xml_list: tree ET.parse(xml_path) root tree.getroot() filename root.find(filename).text # 优先读取 XML 的 width/height但后面会校验 size_node root.find(size) img_w int(size_node.find(width).text) img_h int(size_node.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASS_MAP: class_id len(CLASS_MAP) # 让新类别排在后面避免程序中断 CLASS_MAP[name] class_id else: class_id CLASS_MAP[name] bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 边界修正防止 xmin xmax 或越界 if xmax xmin or ymax ymin: skip_count 1 continue x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h # 裁剪到 0~1 之间避免归一化后框超出画面 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) box_w min(max(box_w, 0.0), 1.0) box_h min(max(box_h, 0.0), 1.0) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) # 输出 TXT 与图片同名 base os.path.splitext(filename)[0] with open(os.path.join(out_dir, base .txt), w, encodingutf-8) as f: f.write(\n.join(lines)) print(f转换完成共 {len(xml_list)} 个 XML跳过无效框 {skip_count} 个) print(CLASS_MAP)逻辑说明先遍历每个 XML取出每个 object 的类别与 bndbox再把绝对坐标换算成中心点和宽高的归一化值。脚本里用min/max做了边界裁剪避免个别标注出画面导致训练报错。CLASS_MAP是手工初值若 XML 里出现字典里没见过的类别我会打印出来确认后补进映射而不是直接让它排到后面因为类别顺序一旦排错和data.yaml的names就对不上了。3.2 别只转格式还要画个框验证转换结束不是终点我给所有转换后的样本随机抽 20 张画框可视化检查。原因很简单数字坐标可以自洽但和实际棋子能不能对上眼睛看一遍比跑指标更直接。import cv2 import os # 根据图片和标签画出检测框 img_dir ./VOCdevkit/VOC2012/JPEGImages label_dir ./labels sample_names [000001, 000003, 000005] # 按实际文件名改 for name in sample_names: img cv2.imread(os.path.join(img_dir, name .jpg)) h, w img.shape[:2] with open(os.path.join(label_dir, name .txt), r, encodingutf-8) as f: lines f.readlines() for line in lines: parts line.strip().split() class_id int(parts[0]) xc, yc, bw, bh map(float, parts[1:]) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(class_id), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(fcheck_{name}.jpg, img)这段脚本做的事比较简单读回 TXT乘以图片宽高还原矩形画绿框并标注类别 ID。我一般会在屏幕上肉眼扫一遍重点看三个地方棋子互相遮挡时框是不是把两颗棋框在一起了框是不是完全不贴合目标以及某些类别 ID 能不能对上棋子本身。只要有一组图明显不对马上回查 VOC 转换逻辑这比训完再排查省时间得多。3.3 常见边界坑XML 缺尺寸、图片和标注不同名、类别名有空格和大小写数据集的制作者未必是按同一套工具链整理的我碰到过的异常主要有size节点被漏掉或宽高为 0此时不要用默认值硬转直接用cv2.imread读一次图片拿真实宽高XML 文件名和 JPEG 文件名后缀大小写不一致比如标签是a.XML图片是a.jpg最好统一用小写后缀再跑glob还有 Windows 上压缩包解压后目录名带1或空格脚本拼接路径时会读不到文件。整体思路是多做防御式编程转换脚本要能在异常发生时跳过并打印原因而不是直接崩掉。3.4 训练集和验证集的划分直接按文件名比例切小数据集不建议用随机乱序后直接split因为棋盘照片可能是按场景顺序拍的前 250 张全是红方视角、后 50 张全是黑方视角简单的随机切包可能会出现验证集只有单一视角的问题。一般做法是先用文件名 hash 或随机种子做分层划分尽量保证每个类别在每个集合里都有。最省事的方式是使用sklearn的train_test_split也可以直接写下标采样。import os import random random.seed(42) img_dir ./VOCdevkit/VOC2012/JPEGImages images [f.split(.)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)] random.shuffle(images) val_ratio 0.2 val_count int(len(images) * val_ratio) train_files images[val_count:] val_files images[:val_count] with open(train.txt, w) as f: for name in train_files: f.write(f./images/{name}.jpg\n) with open(val.txt, w) as f: for name in val_files: f.write(f./images/{name}.jpg\n) print(ftrain: {len(train_files)}, val: {len(val_files)})seed(42)固定随机种子保证每次划分结果一致这在调参时非常有用。第 4 章训练时data.yaml会直接引用train.txt和val.txt所以文件里写的是图片路径而不是标签路径YOLO 会自动在相同目录或者labels同目录兄弟路径下找你刚才生成的 TXT。4. 用 YOLO 训练这套象棋数据从最小命令到三个必调的参数4.1 骨架选择用 nano 还是 small先算算你的显存象棋检测里的目标小、数量多、类别也只有 12 个不需要 heavy 模型直接把所有 GPU 吃完。我的建议是第一轮验证跑nano比如yolov8n.pt因为一次完整训练耗时短能快速发现数据问题。如果最终指标差一点再上small。300 张图训练 100 轮nano 在一张消费级显卡上通常只要十几分钟small 也只是把时间拉长到半小时到一小时。这是典型的“小数据不值得大模型”的场景复杂的模型在小数据上更容易过拟合而不是泛化更好。同时建议使用 YOLOv8 或 YOLOv5 这类开箱即用框架。经典一句yolo detect train datachess.yaml modelyolov8n.pt epochs100 imgsz640 batch16执行前先建一个chess.yaml文件里面写好三类信息path指到压缩包解压后固定放图像的根目录train和val指向刚才生成的train.txt和val.txtnames列表顺序必须与CLASS_MAP一致。names写错是训练时不报错但成绩极差的常见原因因为训练和推理的类别映射会错位但 loss 依然可以下降。4.2 最影响结果的两个参数imgsz和batchimgsz确定分辨率训练和推理统一即可。象棋棋子本身小如果把整张棋盘照片缩到 320 分辨率喂给模型很多棋子会小于 10 像素特征基本糊成一团。反过来用 640 或 768 会好很多但显存占用直线上升。batch在显存允许前提下尽量大因为它直接决定一个 epoch 里梯度更新次数的稳定性。具体调参时用一张 8GB 显存的卡imgsz640配batch16通常能跑起来。如果 OOM就把batch降到 8同时开cacheTrue让数据缓存到内存减少磁盘读取抖动。不要上来就把epochs拉到 300小数据集会在 100 轮以后出现过拟合表现为训练 loss 极低但验证 mAP 不再上升。4.3 数据增强在小数据集上是主要增益来源YOLOv8 默认开了hsv_h、hsv_s、hsv_v、translate、scale、fliplr等增强。对棋盘照片来说左右翻转会让文字产生阅读顺序变化在训练时反而可能把一些棋子弄混我一般会把fliplr降到 0.3 甚至 0.1。同时可以调高scale0.3模拟棋子大小不一致调低degrees因为棋盘照片很少出现大幅旋转但轻微旋转 5~10 度是常态。这些参数都在训练命令里以augment相关的超参传入具体写法取决于框架版本可以先看框架文档确认。我一般还会额外加一点点mosac在 300 张小图上mosaic 可以把四张棋盘拼成一张让模型看到更多棋盘边缘被截断的情况防止推理时棋子贴近边角就漏检。如果mosaic1.0在最后 10 轮不关闭可能让模型对真实棋盘布局不适应所以 tune 的时候记住大增强只用于前半段最后稳住就用接近原始分布的图片。4.4 训练后的验证指标怎么读只看 mAP 会漏掉细颗粒问题训练结束后框架会打印mAP50和mAP50-95。对于象棋重点关注mAP50就行因为这类检测并不需要像素级严谨定位框不贴得很紧也可以接受。mAP50 在 0.9 以上是小数据集健康的信号如果掉到 0.7 以下大概率是某些类别漏标、样本数量太少或目录里混进了背景复杂的图片。此时要打开val_batch0_pred.jpg这类可视化输出看每张图片上有哪些误检。象棋容易误检的往往是相与象、仕与士两者外形高度相似转格式时如果这两类分别占了一半样本就容易区分不开。更精准的判断方式是把混淆矩阵图画出来看哪些类别互相污染最严重再回去补样本或调整类别定义。4.5 导出部署从 PyTorch 到 ONNX踩一遍格式坑训练完的模型实际用起来一般需要导出成 ONNX 或者 TensorRT在推理机上用 TensorRT 跑 FP161080p 25 帧每秒的实时识别对 640 分辨率输入是可以做到的但具体路数取决于显卡和引擎优化程度不是随便一个板子都能跑满。导出命令本身没什么花活yolo export modelbest.pt formatonnx imgsz640 dynamicTruedynamicTrue允许输入宽高动态变化方便上线时兼容不同分辨率但动态维度会让 TensorRT 优化打折如果最终输入尺寸固定导出时直接写死imgsz640反而更快。我自己导出 ONNX 后的习惯是用onnxruntime或 TensorRT 写个推理 demo拿没训练过的现场照片跑一遍确认部署环境正常再交给业务方。5. 常见问题与避坑中国象棋小数据集训练里的 5 个典型现场5.1 训练 loss 一直降但验证 mAP 一直是 0现象训练日志里 box loss 和 cls loss 都在下降但每个 epoch 结束后验证集 mAP 始终约等于 0。原因最常见的不是模型问题而是data.yaml的names顺序和转换脚本生成的class_id不一致。比如转换脚本里车的 ID 是 3但names列表里第 4 个元素却写成了马。这样模型学到的特征和标签完全错位训练 loss 照样下降但 mAP 不可能好。解决统一类别 ID。把voc2yolo.py打印出来的CLASS_MAP直接复制进data.yaml不允许手工抄写排序并重新生成标签做一次可视化检查。训练前写一个小脚本读取一张图的 TXT 和data.yaml做交叉验证。5.2 控制台警告 “found no labels in …” 但不报错现象训练启动后大量 warning 提示某些图片没有找到标签文件但训练仍能继续最终结果极差。原因YOLO 的标签目录规则要求如果有images/train/xxx.jpg标签就在labels/train/xxx.txt或者通过train.txt里的路径推导相邻 labels 目录。如果图片在JPEGImages下而标签输出到了labels根目录路径不对就找不到。还有就是解压后文件名带.jpeg或.png但 XML 里写的却是.jpg导致标签写成了不存在的图片名。解决养成固定目录结构的习惯。把图片复制到dataset/images/train、dataset/images/val标签对应放dataset/labels/train、dataset/labels/val然后train.txt里写相对路径。不要在层级结构上搞太多花活。5.3 相和象、仕和士识别互相串现象验证集上帅、车、马、炮、兵都挺好但相经常被标成象士偶尔标成仕。原因这两对棋子外观几乎一样差别只在文字写法但数据集标注时若身份边界不清晰或者该类别的训练样本数偏少模型很难学到稳定特征。棋盘上“相”和“象”在同一阵营不会同时出现所以模型容易被文字笔画影响。解决优先检查类别是否应该合并。如果实际业务不需要区分红黑的具体文官棋子把相、象合并成同一个类别仕、士也合并12 类变小类反而提升整体 mAP。若业务必须区分就针对性多拍一些该类别的样本用旋转和亮度增强做扩充再单独看混淆矩阵验证是否好转。5.4 验证集很好一换到实拍照片就漏检现象在验证集上的 mAP 很高但把手机随手拍的一张棋盘扔给模型要么检不出要么框偏。原因数据集里 300 张图片可能来自固定的拍摄机位光照、角度、棋盘材质都很单一而实拍场景的光线变化极大。模型学到的不是“棋子”的抽象概念而是这 300 张图的色调和纹理分布。解决训练时打开更多增强特别是把亮度、对比度、高斯噪声都加一点同时在模型验收时规定必须拍 10 张训练集之外的真实照片跑推理以这些现场照片的主观识别效果为准而不是只看官方验证集数字。也可以用这些现场样本补充训练集凑到 400~500 张。5.5 7z 压缩包在 Linux 解压后中文文件名的锅现象压缩包里带了中文标签名或中文目录名Linux 上用7z x解压后Python 脚本报 UnicodeEncodeError。原因Windows 中文环境下创建的压缩包使用 GBK 编码Linux 默认 UTF-8解压后文件名变乱码或无法识别。另一个常见问题是压缩包解压出多个顶层文件夹脚本路径写错找不到文件。解决解压后先用7z l 文件名.7z看一遍压缩包顶层结构确认是否有嵌套目录再统一用rename或 Python 把中文路径改为拼音或数字编号避免后端环境字符集坑。流程上先解压、再整理、最后写脚本不要直接对着一个压缩包写绝对路径。6. 让 300 张图发挥出 1200 张的效果增强策略、复现检查和一点强迫症习惯小数据集的训练本质是和数据过拟合对抗所以最后这一章聊聊怎么把 300 张图的潜力榨干净。首先训练时我一般会关掉或降低左右翻转。象棋棋子在棋盘上会有固定的文本朝向虽然模型未必依赖文字方向但翻转后棋盘上的“楚河汉界”和棋子位置关系会被破坏。相反我会把hsv_h增加一点模拟不同灯光下木色棋盘的颜色偏移因为象棋项目最常遇到的光照变化就是室内灯光冷暖差异。另外把translate调大到 0.1可以模拟棋盘在画面中偏左偏右的情况这对“棋子接触图像边缘”的鲁棒性很有用。然后是混合增强。Mosaic 对小数据集特别有效四张图拼一起可以让模型学到局部遮挡时棋子的不完整形态。但如果棋盘照片中棋子排列紧密拼图时跨图边界容易把两个半颗棋子拼成一颗完整的假棋子反而引入噪声。我一般把mosaic0.4左右不全程打开前 60% 轮次开着最后 40% 关闭让模型在真实分布上收敛。另一招是随机擦除把棋盘中的某个交叉点区域用灰色块遮掉强制模型不过度依赖某个局部的颜色特征。复现检查也是小数据集训练里必须保持的习惯。每次跑实验除了固定random_seed我还会把data.yaml、训练命令、CLASS_MAP、最终指标记录在一个文本文件里与模型权重放在一起。300 张图的训练很快很容易同一晚反复调整参数如果中间有一次类别 ID 改动了却没记录下来第二天比对结果时就会迷失方向。最后还有一个容易被忽略的点不用太早追求高 mAP整个框架先跑通再迭代优化。第一次训练可能只跑 50 轮拿到的模型已经足够验证从图片到训练再到导出的整条链路。链路通了后再增加 epochs、调小学习率观察验证集的提升幅度。如果一次训练加了新增强后 mAP 反而下降不用急着删增强先看是不是该类别本身样本太少。小数据集的训练往往就这样三分算法七分数据审查。我自己处理这类小数据集时有个习惯训练结束后不过夜立刻把最佳权重拖到推理脚本里对着现场拍的畸形图、边框切到半颗棋子的图做一次测试。因为很多指标上的翻车并不在 loss 和 mAP 里体现而在一次肉眼可见的误检里暴露出来。希望这次梳理能帮新入局的工程师少走点弯路。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。