ARTICLE DETAIL

资讯详情

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

基于YOLOv8的手写数字识别:目标检测、数据与OCR串接实战

基于YOLOv8的手写数字识别:目标检测、数据与OCR串接实战 简介面向计算机视觉与图像处理学习者的手写数字识别完整方案集成 YOLOv8 推理预测代码、标注数据集与训练好的模型权重适用于光学字符识别、数字图像处理、课程设计与毕业设计等场景兼顾入门实践与项目复用。全套资料约 331.69MB共 2000 个文件1985 个 xml 标注文件对应 VOC 格式另有 13 个 Markdown 教程文档、1 个 data.yaml 配置和 1 个说明 txt目录按数据集、模型、文档等模块组织便于检索。数据集包含 4103 张手写数字图像覆盖 09 十个类别已划分好 train/val/test 集同时提供 YOLO 格式 txt 标签可无缝用于 YOLOv5YOLOv12 系列模型训练。配套文档从数据组织、标注格式讲到训练参数调整与推理结果可视化配合博主提供的参考文章可快速复现整个识别流程训练好的模型可直接对图像进行数字识别预测省去从零训练的等待时间。已有 96 人学习使用适合需要快速上手目标检测、构建数字识别系统或进行 OCR 预处理研究的开发者获取。1. 拿 Ultralytics YOLOv8 做手写数字识别这个包解决的其实不是“分类”问题很多刚接触ultralytics-yolov8-pred-digits这类包的人第一反应是“手写数字识别不就是 MNIST 分类吗”。但实际项目里手写数字往往是一张表单上散落的多个数字你需要的不是一个类别标签而是每个数字的位置和类别。YOLOv8 目标检测正好能把定位和分类一起做掉所以压缩包里才会有“数据集”和“训练好的模型”配合ultralytics框架直接做光学字符识别OCR里的数字检测环节。这个方向适合三类人做表单自动录入的工程师、拿 CV 做毕业设计的学生、以及想把数字识别从简单分类升级成检测方案的人。下文按“数据准备 → 训练 → 推理 → 排错 → 进阶落地”的顺序把这个包背后的落地路径拆开讲清楚。2. 把数据集整成 YOLO 能吃的格式目录结构、MNIST 转检测框、增强取舍2.1 YOLOv8 的目标检测数据集长什么样ultralytics的 YOLO 训练数据不是把图片和标签混在一起就完事的。解压下载的数据集后先看根目录是不是这个结构digits/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── digits.yamlimages和labels必须同名同前缀图片是.jpg标签就是同名.txt。每张图片对应的标签文件里每一行代表一个目标格式是类别id x_center y_center width height注意这里是归一化坐标单位是“占整张图宽高的比例”不是像素值。比如一张 640×480 的图里有个数字“3”检测框左上角在 (160, 120)右下角在 (320, 240)那么换算成中心坐标就是cx(160320)/2/6400.375cy(120240)/2/4800.375宽高是(320-160)/6400.25(240-120)/4800.25标签行写3 0.375 0.375 0.25 0.25。很多人第一次转格式就栽在这里有的标注工具导出的是图片原始像素坐标有的导出成 VOC 的 XML如果不先统一成 YOLO 归一化格式训练时ultralytics会直接报坐标越界或者模型学出“框全在边缘”的奇怪结果。传统图像处理里的“分割-找轮廓-外接矩形”也能得到坐标但那种坐标要人工再归一化一次步骤越多越容易出错。2.2 MNIST 或普通单数字图片文件夹转成 YOLO 标签可直接用的脚本如果数据集本身就是 MNIST 风格的单数字图片或者每个文件名第一位就是数字类别比如3_001.jpg代表数字 3那不需要手工标注直接用脚本转。常见做法是把每张整图当成一个检测框因为图片主体就是单个数字import os from PIL import Image def convert_single_digit_dir(img_dir, out_img_dir, out_label_dir): 把单数字图片文件夹转成 YOLO 检测格式。 图片命名要求类别数字开头例如 7_001.jpg / 7_001.png。 os.makedirs(out_img_dir, exist_okTrue) os.makedirs(out_label_dir, exist_okTrue) for name in os.listdir(img_dir): if not name.lower().endswith((.jpg, .jpeg, .png)): continue cls name.split(_)[0] if not cls.isdigit(): continue img Image.open(os.path.join(img_dir, name)).convert(RGB) w, h img.size out_name os.path.splitext(name)[0] img.save(os.path.join(out_img_dir, name)) label_path os.path.join(out_label_dir, out_name .txt) with open(label_path, w, encodingutf-8) as f: f.write(f{int(cls)} 0.5 0.5 1.0 1.0\n) # 用法把 MNIST 转成图片目录后执行 # convert_single_digit_dir(mnist_png/train, digits/images/train, digits/labels/train)这个脚本的逻辑很直白读图片尺寸把类别从文件名里解析出来然后写一行类别 0.5 0.5 1.0 1.0。(0.5, 0.5, 1.0, 1.0)意味着检测框就是整张图因为单数字图片绝大多数情况下数字已经居中、铺满画面。这种转换只适合“一图一个数字”的数据集如果你的数据集是一张表单里有十几个数字就得用 Labelme、CVAT 或 Roboflow 逐框标注再导出成 YOLO 格式。用 Labelme 标注时导出格式往往是 JSON。常见做法是写一个转码脚本把shape_type为rectangle的标注点解析成 YOLO 归一化坐标。这里有一个很多人忽略的点Labelme 里手工画框很难保证完全贴合数字边缘框太松会导致模型学到多余背景框太紧又会裁掉数字笔画。我一般会在标注时让框边距比数字外轮廓大 25 个像素宁可“松一点”不要“紧到切字”。2.3 数据增强别把 6 和 9 转晕手写数字识别最大的坑不是模型结构而是数据分布太“干净”。MNIST 是黑白居中、背景纯净的真实表单却是白纸黑字、带网格线、有污渍、位置偏移。所以训练增强的重点不是花哨的 GAN 生成而是模拟真实书写和拍照环境。推荐用albumentations做在线增强训练时每一轮都能看到不同变体。以下是一组建议参数import albumentations as A transform A.Compose([ A.Rotate(limit15, border_mode0), # 轻微旋转超过 15 度容易让 6/9 混淆 A.RandomBrightnessContrast(p0.3), # 模拟不同扫描亮度 A.Blur(blur_limit3, p0.2), # 模拟轻微失焦 A.ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit10, p0.3), A.OneOf([ A.CoarseDropout(max_holes4, max_height8, max_width8, p0.3), A.GaussNoise(var_limit(10, 50), p0.3), ], p0.2), ])Rotate的limit15是第一道防线。手写数字里 6 和 9、2 和 7 本来就容易混淆旋转过大时 6 倒过来就是 99 倒过来就是 6。border_mode0表示旋转后空白区域填 0黑色对白底黑字的灰度扫描件来说填 255 白底会更合理但 YOLO 训练读的是 RGB需要把灰度图统一转成三通道后再增强别在灰度图上直接做归一化。CoarseDropout模拟数字上被笔画遮挡或缺墨的情况这个对实际扫描件非常有用因为很多手写数字自带断笔。另外要注意的是增强会改变检测框坐标。albumentations的BBoxParams必须设置formatyolo否则增强后的框和实际目标就对不上了训练出来的模型会出现“框偏移半格”的诡异现象。使用增强时训练时间会变长但 mAP50-95 通常能比不增强高 36 个点值得加。第一次跑建议先不开增强确认模型能正常训练后再加上否则一出问题你分不清是增强的锅还是数据格式的锅。3. 用 Ultralytics 跑通训练YAML、最小命令、六个必调参数3.1 环境搭建与最小训练命令ultralytics包目前迭代很快CPU 机器也能跑但手写数字检测建议至少用一个有 CUDA 的 GPU。GTX 1660 Ti 这类 6GB 显存的卡跑yolov8n完全够yolov8s也能勉强吃下。安装命令很简单pip install ultralytics yolo --help安装后先写数据集配置。这个 YAML 是训练的入口路径可以写绝对路径也可以写相对路径但注意ultralytics对路径敏感建议直接用绝对路径省得后续排查路径问题# digits.yaml path: /home/user/datasets/digits # 数据集根目录改成你自己的绝对路径 train: images/train val: images/val test: images/test nc: 10 names: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]nc是类别数代表 0 到 9 共 10 个数字。names列表的顺序就是训练时类别 id 的顺序0 对应01 对应1不能乱排如果names写错训练不会报错但推理时会出现“模型认为是 3实际输出是 5”的错位。命令行训练最省事一条命令就能启动完整流程yolo detect train \ data/home/user/datasets/digits.yaml \ modelyolov8n.pt \ epochs100 \ imgsz320 \ batch16 \ device0 \ patience10 \ project/home/user/runs \ namedigits_train这个命令会从yolov8n.pt加载 COCO 预训练权重然后用自己的手写数字数据集微调。yolov8n是 nano 版本模型文件小、推理快对数字这种类别少、目标小的任务精度和速度的平衡最好。如果追求更高精度可以换yolov8s.pt但要留意显存和训练时间。3.2 模型选型和参数含义这六个参数决定你能不能训练完训练参数不是越多越好真正决定“能不能跑完”和“模型能不能用”的主要是这六个epochs决定训练轮次。100 轮对数字识别足够因为任务是 10 分类且目标结构简单通常 30 轮以后 mAP50-95 就基本稳定。patience10表示验证集指标连续 10 轮不涨就自动早停这个参数能避免后期过拟合我一般会开着省电也省时间。batch受显存限制。6GB 显存跑imgsz320时batch16是安全的如果报 CUDA out of memory先把 batch 降到 8 或 4别一上来就改imgsz因为输入尺寸影响检测能力批量大小只是训练速度问题。imgsz320是训练输入边长。手写数字是小目标太低的imgsz会丢失笔画细节但imgsz640会让每张图的缩放计算量翻四倍训练时间暴涨。我习惯先用 320 跑通流程模型收敛后再用imgsz640续训几个 epoch精度能再涨一点。device0指定 GPUCPU 就写devicecpu但训练速度会非常感人准备等一小时跑一个 epoch 的心理预期。workers默认是 2 或者 8Windows 下 workers 开太大容易报 DataLoader worker 崩溃直接workers0最省心Linux 下可以开 4 或 8。从头训练一个 YOLOv8 不是不行但数字识别这种任务用yolov8n.pt预训练权重能更快收敛。ultralytics没有针对 MNIST 特化的预训练模型COCO 预训练权重提供的是通用特征提取能力再微调到数字上效果远好于随机初始化。3.3 看懂训练输出loss、P/R、mAP50-95 不看会吃亏训练时终端会实时打印指标box_loss、cls_loss、dfl_loss、precision、recall、mAP50、mAP50-95。外行只看 mAP50内行会同时盯box_loss和cls_loss。box_loss降不下去说明检测框位置学得不好大概率是标签框标歪了cls_loss降不下去说明数字类别混淆大概率是数据增强里旋转过大、或者 6/9、1/7 的样本不平衡。mAP50是 IOU 阈值 0.5 时的平均精度mAP50-95是多个 IOU 阈值的平均值后者更严格。手写数字检测场景里mAP50达到 0.98 不难但mAP50-95只有 0.85 也别慌因为数字框之间大量重叠、边缘情况多后者提高 0.01 都很值钱。训练结束后ultralytics会在project/name下生成权重文件。best.pt是验证集指标最好的权重last.pt是最后一轮权重。我永远只用best.pt因为手写数字任务后期很容易过拟合到训练集last.pt的验证指标通常已经下滑了。验证集指标是模型好坏的一手依据但不要只信命令行刷出来的那行数字要配合下一章的推理和排序一起看。4. 推理与光学字符识别串接按坐标把一串数字读出来4.1 用 predict() 单图推理conf、iou、max_det 怎么设训练好的best.pt可以直接加载推理。推理代码短但参数含义别忽略from ultralytics import YOLO model YOLO(runs/digits_train/weights/best.pt) results model.predict( sourcetest_form.jpg, conf0.25, iou0.45, imgsz320, max_det50, saveTrue, projectruns/infer, nameform_output )conf0.25表示置信度低于 25% 的检测框直接丢弃。手写数字有时笔画潦草模型置信度只有 0.3我建议验证阶段调低到conf0.05先看全貌确定没有漏检后再调回 0.3 以上。iou0.45是 NMS 的 IOU 阈值两个框重合超过 45% 就合并。如果一个数字被同时检出多个框且置信度都高可以把iou调到 0.3让重复框被更激进地合并。max_det50限制单张图最大检测目标数防止扫描件上污渍被误检成数字后输出几百个框。saveTrue会直接在项目目录生成带框的可视化图这是排查漏检最好的工具。4.2 把检测结果排成正确顺序y 坐标分行x 坐标排序目标检测的输出是无序的模型一次会返回所有框但 OCR 需要按阅读顺序把数字串起来。如果直接把结果按xyxy顺序拼接十有八九乱码。手写表单数字通常分布在多行一个简单的排序策略是先按 y 坐标分行再对每行按 x 坐标排序。import numpy as np def boxes_to_number(boxes, clss, scores, row_gap20): boxes: (N, 4) 的 xyxy 坐标 clss: 类别 id 数组 scores: 置信度数组 row_gap: 两个框中心 y 差值小于该值时认为是同一行 items [ ((y1 y2) / 2, x1, y1, x2, y2, c, s) for (x1, y1, x2, y2), c, s in zip(boxes, clss, scores) ] items.sort(keylambda t: t[0]) lines [] cur_line [items[0]] for item in items[1:]: if abs(item[0] - cur_line[-1][0]) row_gap: cur_line.append(item) else: lines.append(sorted(cur_line, keylambda t: t[1])) cur_line [item] lines.append(sorted(cur_line, keylambda t: t[1])) result_lines [] for line in lines: line_text .join(str(int(c)) for (_, _, _, _, _, c, _) in line) result_lines.append(line_text) return result_lines # 用法 r results[0] num_lines boxes_to_number( r.boxes.xyxy.cpu().numpy(), r.boxes.cls.cpu().numpy(), r.boxes.conf.cpu().numpy(), row_gap30, ) print(num_lines) # 例如 [123, 4567]这里row_gap30是经验值具体要看表单行距在图像上的像素距离。行距大的时候可以设 50行距小的时候设 15。判断“同一行”不只看 y 中心还要考虑框高度差异如果表单有倾斜直接按绝对 y 会分错行。更稳的方式是按 y 坐标排序后计算相邻框中心的 y 差如果差值大于当前行平均高度的 0.6 倍就换行。这个逻辑比较耐斜拍但代码开销也大。到手写数字识别这种规整场景row_gap固定值已经够用。4.3 验证模型好坏的硬标准单字符准确率和整串准确率目标检测的 mAP 高不代表整串识别正确率高。一张表单上 6 个数字即使每个数字定位都准只要一个数字识别错这串号码就是废的。所以我评测时看两个指标单字符准确率所有预测框中分类正确的比例和整串准确率一整个表单数字串完全正确的比例。通常整串准确率比单字符准确率低 510 个百分点。如果你的场景是身份证号、银行卡号这种不允许错位的数字串训练时就要重点增强容易混淆的0/O、1/I、2/Z。手写数字没有字母重点就是6/9、7/9、4/9可以额外多生成一些难例样本或在后处理里对低置信度的检测框人工复核。5. 常见问题排查手写数字项目翻车的 5 个现场5.1 现象MNIST 上训练得很好真实表单上基本漏检很多人拿到手写数字数据直接拿 MNIST 训练再拿到真实表单上一跑发现漏检率奇高。原因是 MNIST 是 28×28 单通道、数字居中、黑底白字而真实表单是几百像素宽、白底黑字、可能还有表格线。模型学到的背景特征是纯黑纯白一遇到网格线就懵。解决的办法是别把 MNIST 直接当最终训练集而是把 MNIST 样本“贴”到真实背景上做合成数据。用一张空白表单或纸面纹理图当背景把 MNIST 数字随机放到任意位置增强时加旋转、模糊、透视这样模型见到的背景分布覆盖了真实扫描件。你也可以先把包里的训练好的模型跑一遍看它能不能扛住合成背景不行再重新训练。数据增强里加RandomBrightnessContrast很有效因为真实扫描件的亮度不均匀模型必须对光照不敏感。5.2 现象mAP50-95 很高但串成字符串后顺序乱七八糟指标漂亮但串出来的数字不对问题出在后处理排序。YOLOv8 输出目标的顺序是模型推理的自然顺序不是阅读顺序。一张表格里数字从左到右排但检测框返回顺序可能是右先左换行后上一行的最后一个数字和下一行的第一个数字如果 y 坐标接近还会被排进同一行。解决方法是写一个更鲁棒的排序函数。先按 y 中心排序再用行间距阈值切分最后对每行按 x 升序排。如果表单本身有倾斜角度先做cv2.minAreaRect检测整张表单的角度再对图像做反向旋转最后再检测排序。这一步是光学字符识别里的经典前置处理传统数字图像处理流程里叫“版面倾斜矫正”。5.3 现象CPU 上训练一个 epoch 要几小时batch 降到 2 也扛不住ubuntu20.04上搭建的 CPU 环境跑yolov8m一个 epoch 跑几小时很正常因为这模型规模不适合纯 CPU 训练。即使降到yolov8nimgsz640也会拖垮训练。修改优先级是先换yolov8n.pt再imgsz320再batch8再cacheTrue缓存图片到内存。如果机器内存太小cacheTrue反而会撑爆内存此时改为cacheFalse并减少workers。GTX 1660 Ti 这种显卡跑yolov8n没问题但如果是纯 CPU别指望短时间训出可用模型M1/M2 Mac 的 Metal 加速可能还能跑x86 CPU 就老实降低输入尺寸、减少 epoch、用早停。5.4 现象训练时提示标签坐标越界或者模型学出来的框全偏到一侧打开标签文件如果里面出现负数坐标或大于 1 的宽高基本都是标注导出后没有归一化。常见原因是标注工具直接导出了基于原图的像素坐标而 YOLO 需要归一化。还有一种是带旋转标注框时导出的cx, cy, w, h是旋转外接矩形的参数而 YOLO 格式只支持轴对齐矩形二者坐标偏差很大。解决方式是训练前写脚本检查所有标签任何x_center width/2 1.0或y_center height/2 1.0的标签都打印出来并跳过对应样本。可以在训练命令里加plotsTrueultralytics会在验证时生成标注框可视化图一眼看出框是否贴合目标。5.5 现象检测时什么都检测不到一张图输出 0 个目标先别怀疑模型先降conf到 0.05 再测。很多数字写得潦草模型给出的置信度普遍在 0.3 以下默认阈值 0.25 会把它们全部过滤掉。还有一个容易被忽略的点predict的imgsz必须和训练时的输入尺寸一致或用更大值如果你用 320 训练却用 64 去推理模型几乎不可能检出。灰度图也有影响ultralytics会自动转 RGB但如果你的图片本身是单通道二值图模型可能不适应可以在推理前用 OpenCVcv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)显式转换。最后一个原因是图像里数字太小比如一张 4000×3000 的表单只占中心一小块直接整图推理容易漏检。此时应该先做cv2.resize或切片把数字区域放大到模型适合的尺度再交给predict。手写数字检测通常不是“把整张扫描件缩小后一眼看完”而是要保证每个字符在图像上的高度不小于 32 像素。6. 进阶把模型导出成 ONNX 再验证一次边界情况训练调参到差不多就可以考虑部署了。常见做法是导出成 ONNX再转到你实际的推理框架。RK3588 这类边缘设备上部署 YOLOv8也是这个路径先用yolo export导出再在板端用 RKNN 工具链转模型。导出本身很简单yolo export modelruns/digits_train/weights/best.pt formatonnx imgsz320 opset12导出后我习惯用onnxruntime跑一张专门的测试图这张图里要覆盖连笔数字、歪斜数字、以及 6 和 9 各一个。不是所有导出模型在 ONNX Runtime 下和 PyTorch 的结果完全一致有时因为坐标解码差异框的位置会偏移几个像素。这时候先看results[0].boxes.xyxy和 ONNX Runtime 的输出能不能对上对不上就在预处理里调整归一化和letterbox顺序。另一个验证技巧是写一个批量脚本把测试集里预测错误的图片单独存到一个error/目录不用等到全部跑完就打开看。我习惯每训练一版模型就用同一套包含 100 张手写样本的验证集跑一遍记录整串准确率和每张卡的错位情况如果新模型比旧模型只高了 0.1 个点但损失了 10 张卡片的整串准确率我就会回退到旧权重。这个“回头验证”的习惯帮我避免过很多次过拟合陷阱。手写数字识别不是一个换网络结构就能一劳永逸的项目它更依赖数据分布、后处理排序和阈值选择。把基础流程跑稳再谈部署希望帮到你。本文还有配套的精品资源点击获取
返回列表