ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

0-9数字图像检测数据集制作与YOLO训练全流程实战

0-9数字图像检测数据集制作与YOLO训练全流程实战 简介面向目标检测入门与课程实训的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到9classes 文件的顺序就是模型输出的映射这类数据集最常见的坑在类别定义上。数字检测的类别就是 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_id0也就是数字 0第 1 个元素对应数字 1依次类推。如果有人在标注工具里把数字 0 定义成了 zero 而不是 0或者把类别顺序写成了 1 到 10训练时模型会学到错位映射推理输出也会跟着乱。参数说明train和val写绝对路径最稳相对路径在切换工作目录后容易报错。nc10是数字检测的固定值不要因为加了背景类就把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_okTrue)。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如果都不是优先写一个字段映射脚本而不是手工改文件。工具导出格式适合场景注意点labelImgVOC XML / YOLO txt单人小规模、快速上手最新版本内置了 YOLO 模式labelmeJSON需要多边形标注时数字检测用矩形框就够不必用它X-AnyLabelingVOC / 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_thr0.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_thr0.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 \ modelyolov8n.pt \ imgsz640 \ epochs100 \ batch16 \ lr00.01 \ project./runs/detect逻辑说明modelyolov8n.pt会从 Ultralytics 自动下载预训练权重。数字检测任务建议从yolov8n或yolov8s开始模型小、收敛快1000 张图足够微调不需要直接上yolov8l。epochs100配合早停机制validation mAP 连续 50 轮不涨就会自动停止。参数说明imgsz640是输入分辨率。如果数据集的图片尺寸普遍在 200 到 400 像素之间比如摄像头裁切出的表计图可以降到imgsz512训练速度更快且不会损失太多精度。batch16在 16GB 显存上跑得动显存小就改 8。lr00.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 度就是 92 旋转 180 度会接近 5直接做rotate180会让模型在两个类别之间摇摆。水平翻转同样危险虽然数字本身不随翻转改变但真实场景里表计数字的位置语义会被破坏尤其当数字跟着设备左右布局时翻转后标签语义就不再准确。能放心用的是强度扰动类增强亮度、对比度、高斯模糊、随机缩放、平移和裁剪。这些增强模拟的是摄像头在不同光照和距离下的成像差异不改变数字本身的拓扑结构。Mosaic 增强在 YOLOv8 里默认开启把四张图拼在一起训练对小目标密集场景很有效但对数字检测来说 Mosaic 会把数字缩小建议保持默认强度或稍微调低mosaic0.5。yolo detect train data./dataset/data.yaml \ modelyolov8n.pt \ imgsz640 \ epochs100 \ batch16 \ mosaic0.5 \ flipud0.0 \ fliplr0.0 \ hsv_h0.015 \ hsv_s0.5 \ hsv_v0.4逻辑说明flipud和fliplr显式置 0禁止上下和水平翻转这是数字检测和其他目标检测最大的增强差异。hsv_h0.015是色调扰动只做微调过大的色相偏移会让红色数字变成蓝色干扰真实场景的颜色特征。hsv_s和hsv_v控制饱和度和明度扰动对应光照变化数值可以按场景自由调工业暗光场景把hsv_v调到 0.6 能提升泛化。参数说明mosaic0.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_id10而nc10只允许 0 到 9。常见来源是标注工具自定义了一个 背景 类别或者 VOC 转 txt 时classes列表长度和 data.yaml 的names不一致。解决跑一遍第 3 章的检查脚本找到[非法类别]对应的 XML把多余类别删掉或改成正确的数字类别然后重新转换一遍 txt不要在 YOLO 训练代码里硬改nc11去迁就脏标签。5.3 现象训练不报错但推理时所有框都错位半个图像宽度原因图片和标签尺寸不匹配。比如数据集里 PNG 是 640x640标注时工具读到的也是 640x640但训练脚本里imgsz512会把图缩放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(datadataset/data.yaml, imgsz640, batch16) print(fmAP50: {results.box.map50:.4f}, mAP50-95: {results.box.map:.4f}) for cls_id, name in results.names.items(): print(fclass {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 章的检查脚本最后才谈训练。这个流程帮我挡掉过不少因为标签错位导致的深夜排查。希望帮到你祝你的数字检测模型一次跑通。本文还有配套的精品资源点击获取
返回列表