资讯详情

资讯详情

数字识别数据集:1000张图与VOC/COCO/YOLO标签格式转换及YOLO训练

简介这份资源面向计算机视觉入门与目标检测实践者提供一套真实场景下的YOLO数字识别数据集可用于训练和验证数字检测模型适合课程设计、算法练手及小目标检测实验。压缩包共约2000个文件整体14.07MB包含1000张标注图片及对应的vocxml、cocojson和yolotxt三种格式标签另附yaml配置文件、划分脚本与教程页面标签由labelimg标注质量较高可直接接入YOLO系列训练流程。资源还提供训练集、验证集、测试集划分脚本以及Windows与Linux环境搭建、训练案例教程方便按需重新划分数据并复现训练过程。目前已有464人学习下载适合希望快速获得规范标注数据、减少数据准备成本并跑通检测流程的读者参考使用。1. 数字识别数据集到底解决什么问题从 1000 张图到三种标签格式做表计读数、车牌尾号、票据编号、工业仪表数码管识别的人几乎都卡在同一个地方模型结构抄得到权重下得到唯独数据凑不齐。网上开源的检测数据集大多是 COCO 那 80 类里面没有你要的「0-9」这种细粒度数字类别自己拿手机拍几百张标注格式又对不上训练脚本。这个标题里的资源包本质就是把这条最耗时的链路一次性铺平1000 张数字图片、VOC/COCO/YOLO 三套标签、一份数据集划分脚本外加训练教程。它适合三类人刚入门 YOLO、想找一个类别少但真实的数据集跑通全流程的新手做 OCR 前置检测、需要单独定位数字框的工程师以及想验证自己改进的损失函数比如 NWD 这类在密集小目标上效果的人。数字识别是个很好的试炼场——类别只有 10 个但目标小、排列密、背景杂正好能暴露模型和训练配置的真实水平。下面我按「数据长什么样 → 三种格式怎么互转 → 划分脚本怎么写 → 训练怎么配 → 坑在哪」的顺序讲透。2. 三种标签格式的差异与互转VOC、COCO、YOLO 到底该用哪个2.1 先搞清楚三种格式各自描述了什么VOC 格式是 XML一张图一个文件核心节点是object里的name和bndbox的xmin/ymin/xmax/ymax坐标是绝对像素值。它的好处是可读性强、工具兼容广LabelImg 默认就吐这个坏处是文件多、解析慢1000 张图就是 1000 个 XML。COCO 格式是一个大 JSON用images、annotations、categories三个数组把整份数据集串起来bbox是[x, y, width, height]且为绝对像素。它适合做多任务检测分割关键点和跨框架评测但单文件体积大改一个框要动整个 JSON。YOLO 格式是每张图配一个同名.txt每行class_id cx cy w h全部归一化到 0-1。它最轻、读取最快是 Ultralytics 系列训练脚本的默认输入。缺点是脱离图片就看不懂坐标类别靠classes.txt或data.yaml的names顺序对齐。格式存储形态坐标类型类别表达典型用途VOC每图一个 XML绝对像素标签内字符串LabelImg、老版检测框架COCO单个 JSON绝对像素categories 数组评测、多任务、pycocotoolsYOLO每图一个 txt归一化 0-1行首整数 idUltralytics 训练2.2 VOC 转 YOLO归一化和类别映射是两个必踩点拿到 VOC 的 XML 后转 YOLO核心就两步读图片宽高做归一化把类别名映射成从 0 开始的整数 id。下面这段脚本我一般直接放项目根目录跑。import os import xml.etree.ElementTree as ET from PIL import Image # 类别顺序必须固定训练时的 data.yaml 要和这里完全一致 CLASSES [str(i) for i in range(10)] # 0~9 CLASS_TO_ID {name: idx for idx, name in enumerate(CLASSES)} def voc_to_yolo(xml_dir, img_dir, out_dir): os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() # 图片名从 XML 里取避免和文件名对不上 img_name root.find(filename).text img_path os.path.join(img_dir, img_name) w, h Image.open(img_path).size # 用真实图片尺寸别信 XML 里的 size lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in CLASS_TO_ID: continue # 类别不在表里就跳过防止 id 越界 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) # 归一化并转成中心点宽高 cx (xmin xmax) / 2.0 / w cy (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{CLASS_TO_ID[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(os.path.join(out_dir, img_name.rsplit(., 1)[0] .txt), w) as f: f.write(\n.join(lines))逻辑说明CLASSES的顺序决定了class_id一旦训练中途改动之前训的权重就废了这是血泪经验。归一化用的是PIL读出的真实宽高而不是 XML 里size节点——很多标注工具写进去的尺寸和实际图片不一致直接信它会得到一堆越界框。参数上:.6f保留六位小数足够YOLO 内部还会再处理。2.3 COCO 转 YOLO注意 bbox 是左上角加宽高COCO 的bbox是[x_min, y_min, width, height]不是[x1,y1,x2,y2]这是最容易翻车的地方。转换时先算x_max x_min width再走和上面一样的归一化。import json from PIL import Image def coco_to_yolo(json_path, img_dir, out_dir): os.makedirs(out_dir, exist_okTrue) data json.load(open(json_path, r, encodingutf-8)) # 建立 image_id - 文件信息 的索引 img_info {img[id]: img for img in data[images]} # 建立 category_id - 连续 id 的映射COCO 的 id 常常不连续 cat_ids sorted(c[id] for c in data[categories]) cat_map {cid: i for i, cid in enumerate(cat_ids)} grouped {} for ann in data[annotations]: grouped.setdefault(ann[image_id], []).append(ann) for img_id, anns in grouped.items(): info img_info[img_id] w, h info[width], info[height] lines [] for ann in anns: x, y, bw, bh ann[bbox] cx (x bw / 2) / w cy (y bh / 2) / h lines.append(f{cat_map[ann[category_id]]} {cx:.6f} {cy:.6f} {bw/w:.6f} {bh/h:.6f}) with open(os.path.join(out_dir, info[file_name].rsplit(., 1)[0] .txt), w) as f: f.write(\n.join(lines))关键点在cat_mapCOCO 的category_id经常是 1、3、5 这种跳号直接拿来当 YOLO 的类别 id 会导致类别数对不上、训练时报索引错误。这里用排序后重新编号保证从 0 连续。另外width/height优先用images里的字段如果缺失再回退到读图。2.4 反向转换什么时候需要训练完要拿pycocotools算 mAP或者要把结果喂给只认 VOC 的老工具时就得反向转。YOLO 转 COCO 时把归一化坐标乘回宽高再补上area和iscrowd字段即可YOLO 转 VOC 则把中心点还原成xmin/ymin/xmax/ymax。反向转换的坑是浮点误差累积建议乘回宽高后做一次max(0, min(coord, size))的裁剪避免出现负坐标或超出边界的框。3. 划分脚本怎么写训练集、验证集、测试集的比例与随机种子3.1 为什么不能手动分1000 张图手动分一是累二是容易把同一场景的连续帧分到不同集合里造成数据泄漏——验证集精度虚高上线就掉。正确做法是脚本随机划分并固定随机种子保证每次跑结果一致方便复现和对比实验。3.2 一个可直接用的划分脚本import os import random import shutil def split_dataset(img_dir, label_dir, out_root, ratios(0.8, 0.1, 0.1), seed42): random.seed(seed) # 固定种子保证可复现 # 只保留同时有图片和标签的样本避免半残数据 names [f.rsplit(., 1)[0] for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .png, .jpeg))] names [n for n in names if os.path.exists(os.path.join(label_dir, n .txt))] random.shuffle(names) n len(names) n_train int(n * ratios[0]) n_val int(n * ratios[1]) splits { train: names[:n_train], val: names[n_train:n_train n_val], test: names[n_train n_val:] } for split, items in splits.items(): img_out os.path.join(out_root, images, split) lbl_out os.path.join(out_root, labels, split) os.makedirs(img_out, exist_okTrue) os.makedirs(lbl_out, exist_okTrue) for name in items: # 图片扩展名可能不统一遍历匹配 for ext in (.jpg, .png, .jpeg): src os.path.join(img_dir, name ext) if os.path.exists(src): shutil.copy(src, os.path.join(img_out, name ext)) break shutil.copy(os.path.join(label_dir, name .txt), os.path.join(lbl_out, name .txt)) print(ftrain{len(splits[train])} val{len(splits[val])} test{len(splits[test])})逻辑说明先做「图片-标签配对」过滤这一步能提前发现标注漏掉的样本。seed42是习惯值换成任何固定整数都行关键是别用系统时间。比例上 8:1:1 适合 1000 张这个量级如果数据更少可以调成 7:2:1 给验证集多留点。参数ratios用元组传入方便按需调整。3.3 划分完必须做的一致性检查划分只是搬文件真正决定训练能不能跑的是标签和图片是否严格对应。我一般会加一段校验遍历labels/train下每个 txt确认对应图片存在再逐行解析检查class_id是否在[0, 9]内、四个坐标是否都在 0-1 之间。任何一条越界训练时要么报错要么静默学歪早查早省事。def check_labels(label_dir, num_classes10): bad [] for f in os.listdir(label_dir): if not f.endswith(.txt): continue for i, line in enumerate(open(os.path.join(label_dir, f))): parts line.strip().split() if len(parts) ! 5: bad.append((f, i, field_count)) continue cid int(parts[0]) coords [float(x) for x in parts[1:]] if cid 0 or cid num_classes: bad.append((f, i, class_out_of_range)) if any(c 0 or c 1 for c in coords): bad.append((f, i, coord_out_of_range)) return bad返回的bad列表为空才算干净。这一步花不了几秒但能挡掉后面几小时的无效训练。4. 用这份数据集跑通 YOLO 训练配置、命令与参数4.1 data.yaml 怎么写Ultralytics 系列训练入口认的是data.yaml路径、类别数、类别名三样必须和划分后的目录结构对上。path: ./digit_dataset # 数据集根目录 train: images/train val: images/val test: images/test nc: 10 # 类别数数字 0-9 names: [0,1,2,3,4,5,6,7,8,9]nc和names长度必须一致且顺序要和转换脚本里的CLASSES完全一样。路径用相对路径时path是基准train/val/test相对它拼接。4.2 训练命令与关键参数yolo detect train \ data./digit_dataset/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/digit \ nameexp1参数说明model选 nano 版先跑通数字识别类别少小模型往往够用不够再换 s/m。imgsz640是默认值如果数字目标特别小可以提到 960 或 1280但显存和速度要权衡。lr00.01是 SGD 的常见起点用 AdamW 的话降到 0.001 量级。patience20表示 20 轮没提升就早停省时间。batch按显存调爆显存就减半。4.3 训练过程中该盯哪些指标box_loss和cls_loss是训练损失正常应该稳步下降如果cls_loss震荡不降多半是学习率太大或标签有噪声。验证侧的mAP50和mAP50-95才是真实水平数字识别这种小目标mAP50-95通常比mAP50低不少别只看前者。混淆矩阵里重点看相邻数字有没有互认比如 6 和 8、1 和 7这是数字识别的典型难点。4.4 推理和导出yolo detect predict modelruns/digit/exp1/weights/best.pt source./test_imgs conf0.25 yolo export modelruns/digit/exp1/weights/best.pt formatonnx opset12conf0.25是置信度阈值数字密集场景可以调到 0.4 减少误检。导出 ONNX 时opset12兼容性较好部署到推理引擎前记得用onnxsim简化一遍。5. 避坑与排查数字识别数据集最容易翻车的 5 个地方5.1 类别 id 不连续导致训练报索引错误现象训练启动即报IndexError或类别数不匹配。原因COCO 的category_id跳号或 VOC 类别名映射时漏了某一类。解决转换后统一做一次 id 重映射保证从 0 连续到nc-1并用第 3 章的校验脚本扫一遍。5.2 归一化坐标越界模型学出框到图外现象训练损失能降但预测框经常贴边或超出图像。原因XML 里的size和真实图片尺寸不一致或标注时框本身就画出了边界。解决归一化一律用PIL读真实尺寸转换后对坐标做clip(0,1)再用校验脚本查越界。5.3 图片和标签文件名对不上静默丢样本现象训练集数量比预期少或某些图永远学不会。原因图片是.JPG大写扩展名标签是.txt脚本按小写匹配时漏掉。解决统一扩展名或在匹配时做lower()处理划分脚本里加配对过滤把没有标签的图直接排除并打印出来。5.4 验证集精度虚高上线就掉现象mAP50很高实际场景误检漏检严重。原因划分时同一场景的连续帧被分到训练和验证两侧造成数据泄漏。解决按场景或按拍摄批次分组后再划分而不是纯随机如果数据来自视频抽帧先按视频分组再分集合。5.5 小目标数字漏检严重现象大数字能检出小数字成片漏。原因imgsz太小小目标下采样后特征几乎消失或 anchor 尺度不匹配。解决提高imgsz到 960/1280开启mosaic增强必要时用带 P2 层的模型结构损失函数上可以试 NWD 这类对小目标更友好的度量。6. 把数字识别做扎实的一个进阶习惯先看数据再调模型我踩过最深的坑是一上来就换模型、改损失函数折腾一周发现是标签里有一批框画歪了。后来养成一个习惯任何数据集到手先花半小时做可视化——把标签框画回原图随机抽 50 张拼成网格看一遍。数字识别尤其如此因为数字小、排列密标注时手一抖框就偏肉眼不查根本发现不了。import cv2 import os import random def visualize(img_dir, label_dir, out_path, num50): names [f for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .png))] random.shuffle(names) tiles [] for name in names[:num]: img cv2.imread(os.path.join(img_dir, name)) h, w img.shape[:2] lbl os.path.join(label_dir, name.rsplit(., 1)[0] .txt) if os.path.exists(lbl): for line in open(lbl): cid, cx, cy, bw, bh line.split() cx, cy, bw, bh map(float, (cx, cy, bw, bh)) # 归一化坐标还原成像素 x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, cid, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) tiles.append(cv2.resize(img, (320, 320))) # 拼成 10x5 网格 rows [cv2.hconcat(tiles[i:i10]) for i in range(0, len(tiles), 10)] grid cv2.vconcat(rows) cv2.imwrite(out_path, grid)这段代码把标签框和类别画回图上拼成一张大图。跑完打开看一眼框歪的、类别标错的、漏标的基本一眼就能揪出来。参数num控制抽样数量50 张够覆盖大部分问题out_path建议放在项目根目录方便找。另一个习惯是固定一套「基线配置」yolov8n imgsz640 epochs100 seed42任何新想法都先在这套基线上跑只改一个变量。数字识别这种任务很多时候提升不来自花哨的改进而来自把数据清洗干净、把imgsz调对、把类别顺序理顺。模型是黑匣子但数据不是先把能看见的部分做对再谈玄学调参。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →