简介:面向目标检测入门与课程实训的0-9数字图像检测数据集,按YOLO格式整理,适配YOLOv5及后续系列模型训练。数据划分为训练集约1000张、验证集约100张、测试集约50张,每张图片均配有txt标签文件,标注采用x_centre、y_centre、w、h相对坐标,类别以class文本文件为准;另附show.py可视化脚本,可将检测框绘制回原图,便于检查标注质量。压缩包共2000个文件,以jpg图像与txt标签为主体,另含1个py脚本,整体约87.43MB,目录按YOLOV5习惯组织,下载后基本可无缝接入训练流程。图片素材来源多样,包含不同拍摄角度、水印背景与光照环境,有助于提升真实场景下的数字识别泛化能力。已有254人学习,适合数字识别、OCR前置检测、目标检测课程作业及YOLO框架实操练习使用。
1. 0-9数字图像检测数据集:1000张图能做什么,怎么做才不浪费
做数字识别的时候,很多人第一反应是端到端OCR或者图像分类,直接把图片裁出来丢进分类模型。等真上了产线才发现,表计上的数字带旋转、底板反光、相邻数字连在一起,分类器全崩。这恰恰是目标检测这条路的用武之地:先框出每个数字,再按框裁切或直接输出类别。0-9数字图像检测数据集(超过1000张图片和标签)要解决的正是这件事——为10个数字类别提供带边界框的训练样本,让模型先学会找数字、再判断是几。这类数据集特别适合三类人:刚入门想跑通检测全流程的新手、做工业表计和仪表读数落地的工程师、以及需要快速验证数据标注规范的团队。1000张图不算多,但数字检测的类别少、目标结构简单,只要划分、格式和增强处理得当,完全能训练出一个可以上手的检测模型。
2. 数字检测的数据集应该是什么样:类别定义、目录结构与格式转换
2.1 数字图像检测和通用目标检测的差异:为什么1000张够用一半
通用目标检测数据集动辄几万张,因为类别多、场景杂、目标尺度差异大。数字检测不一样:类别固定是10个,数字的形态变化只有字体、粗细、倾斜和模糊这几项,结构特征非常明确。所以 1000 张图、每张平均两到三个数字,实例数通常能到 2000 到 3000 个,这个规模做预训练权重微调是够的。但前提是实例数要均衡,10 个数字不能出现某类只有 50 个、另一类有 400 个的情况,否则模型会把高频类别学得更牢,低频类别在验证集上 AP 明显偏低。
另一个差异是检测跟分类的定位粒度不同。分类模型吃的是已经裁好的数字图,检测模型要从整张图里找数字框。这意味着训练样本里不能只有"干净的数字特写",还必须有带背景干扰、多数字同框、部分遮挡的图。如果原始数据集里全是白底黑字、数字居中,训练出来的模型换个场景大概率漏检。拿到数据后先按场景分类看一眼,比直接开训重要得多。
2.2 类别定义从0到9:classes 文件的顺序就是模型输出的映射
这类数据集最常见的坑在类别定义上。数字检测的类别就是 0、1、2、3、4、5、6、7、8、9 这十类,标注工具里通常也叫这个名字,但 YOLO 训练读取的类别编号不是字符串而是整数索引。data.yaml 里的names列表顺序,直接决定class_id的映射关系。
# data.yaml train: ./dataset/images/train val: ./dataset/images/val nc: 10 names: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']逻辑说明:nc是类别总数,必须和names的长度一致。names列表里第 0 个元素对应标注文件里的class_id=0,也就是数字 0;第 1 个元素对应数字 1,依次类推。如果有人在标注工具里把数字 0 定义成了 "zero" 而不是 "0",或者把类别顺序写成了 1 到 10,训练时模型会学到错位映射,推理输出也会跟着乱。
参数说明:train和val写绝对路径最稳,相对路径在切换工作目录后容易报错。nc=10是数字检测的固定值,不要因为加了背景类就把nc改成 11——背景类在 YOLO 里是隐式学习的,不需要显式标注。
2.3 把 VOC XML 转成 YOLO txt:转换脚本与坐标细节
标注工具如果导出的是 Pascal VOC 格式,每个 XML 文件对应一张图片,里面记录filename、图片宽高和每个目标的name、bndbox。YOLO 训练不认 XML,需要转成 txt,每行格式是class_id cx cy w h,四个坐标全部归一化到 0 到 1 之间。
import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_dir, classes): tree = ET.parse(xml_path) root = tree.getroot() # VOC 原始坐标是像素值,先读出图片尺寸用于归一化 img_w = int(root.find("size").find("width").text) img_h = int(root.find("size").find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in classes: print(f"[跳过未定义类别] {name}") continue cls_id = classes.index(name) bbox = obj.find("bndbox") # VOC 的 bndbox 是左上角和右下角两个点 x1 = float(bbox.find("xmin").text) y1 = float(bbox.find("ymin").text) x2 = float(bbox.find("xmax").text) y2 = float(bbox.find("ymax").text) # 中心点坐标换算成归一化值 w = x2 - x1 h = y2 - y1 cx = (x1 + w / 2.0) / img_w cy = (y1 + h / 2.0) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w / img_w:.6f} {h / img_h:.6f}") out_name = os.path.splitext(os.path.basename(xml_path))[0] + ".txt" with open(os.path.join(out_dir, out_name), "w") as f: f.write("\n".join(lines)) if __name__ == "__main__": voc_to_yolo("labels/0001.xml", "labels_txt", ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"])逻辑说明:XML 里size节点存的是原图宽高,如果图片在标注后被 resize 过,这里的宽高必须同步更新,否则归一化坐标全错。bndbox里四个值取出来先算宽高,再算中心点,这是 YOLO 和 VOC 两种格式最关键的习惯性差异——VOC 存角点,YOLO 存中心点和宽高。
参数说明:classes列表的顺序和前面 data.yaml 的names必须完全一致,这里写的是数字 0 到 9,转换出来的cls_id就从 0 到 9。输出目录如果不存在,脚本会直接报错,建议先os.makedirs(out_dir, exist_ok=True)。
2.4 按图划分 train/val/test:固定随机种子与类别均衡
1000 张图的划分方式直接影响验证指标的可信度。常见做法是 8:1:1,也就是 800 张训练、100 张验证、100 张测试。划分的单位是"图片",不是"目标实例",同一张图不能同时出现在训练集和验证集里,否则会数据泄露,验证集 AP 虚高。
import os import random import shutil random.seed(42) # 固定种子,保证每次切分结果一致 img_files = [f for f in os.listdir("images") if f.endswith(".jpg")] random.shuffle(img_files) n = len(img_files) train_files = img_files[:int(n * 0.8)] val_files = img_files[int(n * 0.8):int(n * 0.9)] test_files = img_files[int(n * 0.9):] for f in train_files: shutil.copy(os.path.join("images", f), os.path.join("dataset/train", f)) # val/test 同理,标签文件按同名复制即可逻辑说明:random.seed(42)固定随机种子后,每次跑脚本得到的切分结果相同,方便复现实验。shuffle是为了避免原始文件名按数字顺序排列时,前 80% 全是小数字、后 20% 全是大数字这种分布倾斜。
参数说明:如果数据集里每个图片包含数字的数量不均匀(比如有些图只有一个 5,有些图有六个数字),建议切分后统计一下各集合里每个类别的实例数。发现验证集里某个数字一个都没有,就先不做随机纯切分,改成按文件名或场景分层抽样,否则那个数字的验证 AP 永远是 0。
2.5 不够用的时候怎么补:合成数据和公开数据集的合并边界
1000 张图对数字检测来说是一个"能做但不宽裕"的规模。如果验证集 AP 一直上不去,优先考虑补充合成数据:用 OpenCV 把数字随机贴在自然背景或噪点背景上,随机换字体、缩放、旋转。合成数据能快速补足背景多样性和角度变化,但要注意合成分布和真实分布的差异,合成图占比不要超过总量的 30%,否则模型会学到"数字边缘过于干净"的假特征,真实场景里反而掉点。公开数字数据集和这里的数据合并也可以,但先检查类别定义和标注粒度,有些数据集把连续数字标成一个框,有的标成单个数字,混着用模型会分裂。
3. 标签标注实操:工具选型、边界规范和质量检查脚本
3.1 目标检测常用标注工具怎么选
数字检测的标注工作量不大,选工具的核心标准是"导出格式干净、操作简单、能批量检查"。常见做法是单人小数据集用 labelImg,它导出 VOC XML 和 YOLO txt 都直接支持,鼠标画框加快捷键标类别,学习成本最低。需要多人协作或做半自动预标注时,用 X-AnyLabeling 这类带模型辅助的工具会更高效,先跑一遍检测模型生成预标注框,人工只改错框,1000 张图一天能清完。
工具选型要避免"格式转换地狱"。比如在线标注平台导出的可能是一个大 JSON 或 ZIP 包,字段命名和本地工具不一致,转换脚本反而比标注还费时间。我的习惯是:先确认导出格式是不是标准 COCO 或 VOC,如果都不是,优先写一个字段映射脚本而不是手工改文件。
| 工具 | 导出格式 | 适合场景 | 注意点 |
|---|---|---|---|
| labelImg | VOC XML / YOLO txt | 单人小规模、快速上手 | 最新版本内置了 YOLO 模式 |
| labelme | JSON | 需要多边形标注时 | 数字检测用矩形框就够,不必用它 |
| X-AnyLabeling | VOC / COCO / YOLO | 半自动预标注、大批量 | 需要配置模型权重,机器要有 GPU 或能容忍 CPU 推理 |
3.2 数字框的标注边界规范:框多大、重叠怎么算
数字检测的边界框规范和通用目标检测不一样。通用物体允许框包含一部分上下文背景,因为类别判断需要环境信息;数字是纯结构符号,框太大把旁边的干扰线、表计指针包含进来,模型会误把那些噪声当成数字特征。反过来框太小裁掉数字的一角,类别判断直接出错。
我的标注规范是:矩形框紧贴数字可见区域的外轮廓,上下左右各留 2 到 3 个像素的余量,不要加额外的安全边距。对于连在一起的两个数字(比如表计上的 "25"),两个框可以紧挨,但必须有明确分界线,不允许两个框重叠超过 10%。数字斜排的时候,用带角度的框比轴对齐框更准——但这取决于标注工具,labelImg 原生不支持旋转框,就用轴对齐框框住数字的最小外接范围即可。
另外要专门处理"像数字的干扰物":斜杠/、字母 O、字母 l、小数点都容易让标注者手滑标成数字。发生这类错误不会立刻报错,但训练后 0 和 O 会互相抢类别。项目启动前,把难分样本单独挑出来放一个文件夹,找两个人各标一遍,不一致的框重新讨论,这是挡掉脏标签最有效的手段。
3.3 标注质量检查脚本:一次性揪出越界、错类和重叠
标注完成后,先跑一遍质量检查脚本再开始训练,比起训练到一半发现 loss 不降再回头查标签高效得多。检查的核心四件事:类别是否合法、坐标是否越界、宽高是否为负、两个框是否大面积重叠。
import xml.etree.ElementTree as ET import os def check_voc_label(xml_path, img_dir, classes, iou_thr=0.5): tree = ET.parse(xml_path) root = tree.getroot() img_file = root.find("filename").text if not os.path.exists(os.path.join(img_dir, img_file)): print(f"[缺图] {xml_path} -> {img_file}") return boxes = [] for obj in root.findall("object"): name = obj.find("name").text if name not in classes: print(f"[非法类别] {xml_path} -> {name}") bbox = obj.find("bndbox") x1 = float(bbox.find("xmin").text) y1 = float(bbox.find("ymin").text) x2 = float(bbox.find("xmax").text) y2 = float(bbox.find("ymax").text) if x1 >= x2 or y1 >= y2: print(f"[宽高非法] {xml_path} -> {x1},{y1},{x2},{y2}") if x1 < 0 or y1 < 0 or x2 < 0 or y2 < 0: print(f"[坐标越界] {xml_path} -> 负坐标") boxes.append((name, x1, y1, x2, y2)) for i in range(len(boxes)): for j in range(i + 1, len(boxes)): iou = calc_iou(boxes[i][1:], boxes[j][1:]) if iou > iou_thr: print(f"[重叠框] {xml_path} -> {boxes[i][0]}/{boxes[j][0]} IOU={iou:.2f}") def calc_iou(a, b): # 两个矩形相交面积 / 并集面积,越界框在此函数统一按边界裁剪 x1 = max(a[0], b[0]); y1 = max(a[1], b[1]) x2 = min(a[2], b[2]); y2 = min(a[3], b[3]) inter = max(0, x2 - x1) * max(0, y2 - y1) area_a = (a[2] - a[0]) * (a[3] - a[1]) area_b = (b[2] - b[0]) * (b[3] - b[1]) return inter / (area_a + area_b - inter)逻辑说明:脚本按 XML 逐个检查,[缺图]说明文件名对不上,通常是标注完又改过图片名;[非法类别]说明标注工具里出现了classes列表之外的类别名;重叠框检查用 IOU 大于 0.5 作为阈值输出告警,同一张图里两个数字连得很近时 IOU 会有 0.1 到 0.3 的波动,超过 0.5 基本是复制粘贴导致的重叠,需要人工确认。
参数说明:iou_thr=0.5是通用目标检测里经验用的阈值,数字检测因为框小且密集,可以放宽到 0.6,但仍要输出告警让人确认。跑完脚本后,把告警信息导出成文本,按 XML 文件名逐条修正,不要在训练脚本里用ignore关键字偷偷跳过脏样本,脏标注会污染整体特征学习。
4. 把 1000 张图喂进 YOLOv8 训练自己的数据集:从配置到增强
4.1 构造 data.yaml 并跑通 YOLOv8 训练流程
有了划分好的目录和转换好的 txt 标签,剩下的就是用 YOLOv8 拉起训练。先确认目录结构,标准做法是:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ └── val/图片和标签按数据集分开放,不要混在同一目录。YOLO 训练时自动到 labels 目录找同名 txt 文件,图片0001.jpg对应标签0001.txt。data.yaml 里已经写好了两个路径,这里再强调一个细节:test路径如果写在 data.yaml 里,训练完成后会额外跑一次测试集推理,速度慢且不是必须的,测试集可以在验证完模型后用独立脚本单独评估。
yolo detect train data=./dataset/data.yaml \ model=yolov8n.pt \ imgsz=640 \ epochs=100 \ batch=16 \ lr0=0.01 \ project=./runs/detect逻辑说明:model=yolov8n.pt会从 Ultralytics 自动下载预训练权重。数字检测任务建议从yolov8n或yolov8s开始,模型小、收敛快,1000 张图足够微调,不需要直接上yolov8l。epochs=100配合早停机制,validation mAP 连续 50 轮不涨就会自动停止。
参数说明:imgsz=640是输入分辨率。如果数据集的图片尺寸普遍在 200 到 400 像素之间(比如摄像头裁切出的表计图),可以降到imgsz=512,训练速度更快且不会损失太多精度。batch=16在 16GB 显存上跑得动,显存小就改 8。lr0=0.01是 YOLOv8 的默认初始学习率,微调场景通常不动。
4.2 训练前把 GT 框画回图上:可视化校验脚本
很多人拿到数据集就直接开训,训练完 loss 很漂亮但 val mAP 只有 0.2,回头查才发现标签坐标整体偏移了 50 像素。训练前把标注框画回原图抽查 20 到 30 张,是最便宜的后悔药。
import cv2 from glob import glob def visualize_yolo(img_path, label_path, classes): img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w = img.shape[:2] with open(label_path) as f: for line in f: parts = line.strip().split() cls_id, cx, cy, bw, bh = int(parts[0]), *map(float, parts[1:]) # 归一化坐标还原成像素坐标,注意四舍五入后要裁剪到图像边界内 x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) x1, y1 = max(0, x1), max(0, y1) x2, y2 = min(w - 1, x2), min(h - 1, y2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, classes[cls_id], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite("check_" + img_path.split("/")[-1], img) for img_path in glob("dataset/images/val/*.jpg")[:30]: label_path = img_path.replace("images", "labels").replace(".jpg", ".txt") visualize_yolo(img_path, label_path, ["0","1","2","3","4","5","6","7","8","9"])逻辑说明:逐行读取 txt 里的归一化中心点和宽高,还原成像素坐标画矩形。代码里max(0, y1 - 5)是防止文字画到图像外面去。check_前缀的输出图会直接写到当前目录,方便快速翻看。
参数说明:抽查的 30 张图不要只挑前 30 个文件,建议随机抽样,每类数字保证至少有一张。如果发现框整体偏向数字的左下角,或者框比数字大一圈,说明标注规范没统一,要回到第 3 章复核一遍标注,而不是靠训练去纠正。这一步我每次都会做,省下的调试时间远超画框的时间。
4.3 数字检测的增强策略:哪些增强能动、哪些不能动
数据增强在数字检测里是一把双刃剑。旋转是最大的坑:数字 6 旋转 180 度就是 9,2 旋转 180 度会接近 5,直接做rotate=180会让模型在两个类别之间摇摆。水平翻转同样危险,虽然数字本身不随翻转改变,但真实场景里表计数字的位置语义会被破坏,尤其当数字跟着设备左右布局时,翻转后标签语义就不再准确。
能放心用的是强度扰动类增强:亮度、对比度、高斯模糊、随机缩放、平移和裁剪。这些增强模拟的是摄像头在不同光照和距离下的成像差异,不改变数字本身的拓扑结构。Mosaic 增强在 YOLOv8 里默认开启,把四张图拼在一起训练,对小目标密集场景很有效,但对数字检测来说 Mosaic 会把数字缩小,建议保持默认强度或稍微调低mosaic=0.5。
yolo detect train data=./dataset/data.yaml \ model=yolov8n.pt \ imgsz=640 \ epochs=100 \ batch=16 \ mosaic=0.5 \ flipud=0.0 \ fliplr=0.0 \ hsv_h=0.015 \ hsv_s=0.5 \ hsv_v=0.4逻辑说明:flipud和fliplr显式置 0,禁止上下和水平翻转,这是数字检测和其他目标检测最大的增强差异。hsv_h=0.015是色调扰动,只做微调,过大的色相偏移会让红色数字变成蓝色,干扰真实场景的颜色特征。hsv_s和hsv_v控制饱和度和明度扰动,对应光照变化,数值可以按场景自由调,工业暗光场景把hsv_v调到 0.6 能提升泛化。
参数说明:mosaic=0.5的意思是每 50% 的概率使用 Mosaic 拼接。如果发现训练后期小数字的 AP 上不去,可以关闭 Mosaic(设为 0)再看,Mosaic 拼接后数字尺寸变小,模型对中等尺寸的数字学得更充分,但小尺寸数字可能被压缩到难以辨认。1000 张数据集的规模不大,增强翻倍到 2000 张跑了也很正常,关键是别在增强里把数字变形到连人都认不出的程度。
5. 避坑:数字检测数据集训练中的典型翻车现场
5.1 现象:loss 降得很顺利,但验证集 AP 只有 0.3,且 6 和 9 互相混
原因:训练集中数字 6 有 400 个实例,数字 9 只有 60 个,模型对 9 的特征学习不充分;更隐蔽的是标注阶段 6 和 9 被人为标反了一批。解决:先统计labels/train下每个cls_id出现次数,类别不均衡就做实例级重采样,9 号类别复制三到四份;再画混淆矩阵看具体是哪些类别互混,发现有规律地混(总是 6->9),大概率是标签错标。
5.2 现象:训练中途报错IndexError: index 10 is out of bounds
原因:txt 标签里出现了class_id=10,而nc=10只允许 0 到 9。常见来源是标注工具自定义了一个 "背景" 类别,或者 VOC 转 txt 时classes列表长度和 data.yaml 的names不一致。解决:跑一遍第 3 章的检查脚本,找到[非法类别]对应的 XML,把多余类别删掉或改成正确的数字类别,然后重新转换一遍 txt,不要在 YOLO 训练代码里硬改nc=11去迁就脏标签。
5.3 现象:训练不报错,但推理时所有框都错位半个图像宽度
原因:图片和标签尺寸不匹配。比如数据集里 PNG 是 640x640,标注时工具读到的也是 640x640,但训练脚本里imgsz=512会把图缩放,YOLO 能正确处理缩放后的坐标;真正的问题在于部分图片本身尺寸不统一(有的是 640、有的是 800),而标签的归一化坐标基于各自的原始尺寸,混在一起训练,模型学到的坐标系是乱的。解决:训练前用脚本把所有图片统一 resize 到同一尺寸,标签不用改(本来就是归一化的);如果用的是 VOC XML,转换 txt 时要按 XML 里的实际宽高归一化,不要用代码里写死的 640。
5.4 现象:模型把字母 O 当成 0、字母 l 当成 1,数值读错但 mAP 很高
原因:数据集里混入了含字母的样本(比如车牌里的省份简称、产品编号),标注者把 O 标成了 0。模型学到的是"圆环就是 0"的特征,不是"数字 0"本身。解决:这种问题出现在任务边界不清晰时,如果项目只做数字检测,把含字母的样本从数据集中直接剔除;如果产品需要同时处理字母和数字,正确做法是扩展类别到全字符集,而不是让模型在 10 类里硬分。
5.5 现象:val mAP0.5 到了 0.95,但换一个拍摄角度后漏检一半
原因:数据分布和真实场景脱节。1000 张图如果都来自同一个摄像头、同一个角度、同一块表计背景,模型学会的是"这个表计的数字长什么样",而不是"任意表计的数字长什么样"。解决:把测试集换成真实场景的样本(哪怕只有 50 张),看真实场景的 AP;然后往训练集里补充多角度、多光照的样本,或者用合成数据把数字贴到不同背景上做平衡。这一步决定了模型是"实验室准"还是"现场准",后者才是落地项目的验收标准。
6. 落地验证:用评估脚本看 mAP 和单类 AP,再用真实样本做最终检查
训练结束后,不要只看训练日志里最后一行 mAP。YOLOv8 的自带验证接口会输出每个类别的 AP,数字检测场景里单类 AP 比总均值重要得多——总 mAP 高可能是因为 0 和 1 这类易分数字拉高了均值,6 和 9 的实际 AP 可能刚过 0.5。
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.val(data="dataset/data.yaml", imgsz=640, batch=16) print(f"mAP50: {results.box.map50:.4f}, mAP50-95: {results.box.map:.4f}") for cls_id, name in results.names.items(): print(f"class {name}: AP50 = {results.box.ap50[cls_id]:.4f}")逻辑说明:results.box.ap50是一个数组,长度为类别数,按cls_id索引。逐类打印能清楚看到拖后腿的是哪个数字。如果某个数字的 AP50 明显低于均值,回到第 5 章检查该类的实例数和标签质量。另外调用model.val时注意data.yaml里的test路径,如果没配 test 就用 val 路径评估,指标会更保守但可复现。
单类 AP 确认没问题后,还有一个习惯值得坚持:拿 50 到 100 张完全没有参与训练的真实场景图片,人工标注后单独跑一遍推理脚本,对比预测框和人工框的位置误差与类别误差。数据集的 1000 张图再怎么划分,都只是内部验证,只有真实场景样本才能暴露光照、角度和字体变化带来的分布偏移。这一步发现问题后,把失败样本挑出来,按困难样本补充进训练集,再微调 20 到 30 轮,往往比增大训练集本身收益大得多。
我做数字检测项目养成的习惯是:拿到的任何数据集,第一件事永远是先跑可视化脚本看 GT 框,然后跑第 3 章的检查脚本,最后才谈训练。这个流程帮我挡掉过不少因为标签错位导致的深夜排查。希望帮到你,祝你的数字检测模型一次跑通。
本文还有配套的精品资源,点击获取