ARTICLE DETAIL

资讯详情

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

YOLOv8手写数字检测实战:从数据集制作到OCR落地全攻略

YOLOv8手写数字检测实战:从数据集制作到OCR落地全攻略 简介面向手写数字识别与分类的YOLOv8目标检测项目集成了标注数据集、训练配置与训练好的模型适用于光学字符识别OCR和数字图像处理任务内含分步使用教程可帮助学习者从数据准备到模型训练快速上手。压缩包共2000个文件由1985个xml标注文件、13个md使用说明、1个yaml配置文件和1个txt说明文件组成整体大小331.69MB。数据集包含4103张已划分好train/val/test的图像标注类别覆盖数字0至9共十个数字附带的data.yaml可直接用于YOLOv5、v8、v9、v10、v11、v12等算法训练同时提供训练好的模型与可视化参考链接便于验证识别效果并加深对OCR检测流程的理解。目前已有96人学习下载适合具备一定深度学习基础、希望实践目标检测或OCR方向的开发者使用。1. 这包到底做了什么YOLOv8 动手写数字和纯分类不是一回事很多人一提手写数字识别脑海里还是 MNIST 那种“一张图输出一个数字”的分类任务。但这个标题里写的是 ultralytics-yolov8-pred-digits强调“检测光学字符识别”方向完全不同它要用 YOLOv8 在一张图里同时定位和分类多个手写数字。模型输出的是每个数字的边界框、类别和置信度而不是单个标签。这个方案适合答题卡、票据、快递单号这一类场景也适合想把目标检测应用到 OCR 方向的人。这个方向的价值在于三件事可用的数据集、训练好的模型、以及一条从环境搭建到推理都能自己复现的路线。2. 把 ultralytics 环境跑通CPU 也能用的最小安装和第一次推理2.1 Ubuntu 20.04 搭 CPU 版 YOLOv8pip 装还是源码装很多人一看到 YOLOv8 就以为必须要有 NVIDIA 显卡其实手写数字检测对显存要求不高CPU 也能跑只是训练慢一点。标题里这套东西如果用普通笔记本做过验证大概率就是 CPU 训练出来的小模型。我推荐先用 Python 虚拟环境隔离避免把系统 Python 搞坏尤其是 Ubuntu 20.04 上通常自带的 Python 3.8 和 3.10 混在一起直接 pip 装容易把包装进系统目录。python3 -m venv yolodigits source yolodigits/bin/activate pip install --upgrade pip pip install ultralytics安装完成后验证一下版本python -c import ultralytics; print(ultralytics.__version__)这里的逻辑是ultralytics这个包会同时拉入 torch、torchvision、opencv-python 等依赖。CPU 环境直接 pip 安装即可默认会装 CPU 版 torch如果你之前手动装过 CUDA 版 torch建议先卸载再装否则会出现“torch 正常 import跑模型时突然报 device 错误”的诡异问题。很多刚接触 YOLOv8 的人在这一步就放弃了其实十有八九是 torch 版本和 ultralytics 的预期不一致。提示Ubuntu 20.04 上如果 Python 是 3.8ultralytics 新版还支持如果系统里同时有多个 Python建虚拟环境时用python3.10 -m venv更稳避免 libpython 版本冲突。那什么时候不推荐 pip 装当你要改 YOLOv8 源码比如改 Head 结构、自定义损失函数时才需要git clone然后pip install -e .。但只是训练和推理手写数字pip 包完全够用没必要给自己增加维护负担。我见过有人为了学原理把源码整个拉下来结果连依赖都装不顺反而忘了先跑通最小流程。2.2 用 COCO 权重先跑一次预测理解检测输出结构环境装好后的第一件事不是急着训练而是先拿官方yolov8n.pt跑一张普通图片确认整个管线能通。这个权重在 COCO 上训过能检测 80 类物体里面没有手写数字但可以帮助我们理解 YOLOv8 的预测输出格式。yolo predict modelyolov8n.pt sourcebus.jpg imgsz640 conf0.25命令会从官方来源自动下载权重到~/.cache/ultralytics/weights/。如果下载慢可以手动下载后放在当前目录。跑完后输出图片默认保存在runs/detect/predict/。说下命令的用意model指定权重文件source可以是图片、视频或目录imgsz强制输入尺寸conf是置信度阈值。用 Python API 更灵活尤其是后面要接 OCR 管线时我习惯这么写from ultralytics import YOLO model YOLO(yolov8n.pt) results model(bus.jpg, imgsz640, conf0.25) boxes results[0].boxes print(boxes.xyxy) # 左上角和右下角坐标格式 [x1, y1, x2, y2] print(boxes.conf) # 置信度 print(boxes.cls) # 类别索引这段代码的关键是把results[0].boxes看作一个对象它有三个常用属性xyxy是目标框坐标单位是像素conf是每个框的置信度cls是类别索引对应 COCO 的 80 类。对新手来说最容易踩的坑是只打印results[0].plot()那是画好框的图片真正要用的坐标信息反而没拿到。如果要在数字检测里做业务逻辑一定要从xyxy取坐标而不是去解析画好框的图片。2.3 为手写数字调推理参数imgsz、conf、iou 在数字上怎么设COCO 演示没问题后回到手写数字场景。数字和行人、汽车不一样它尺寸小、数量多、密集排列推理参数不能直接照搬。imgsz对目标检测影响很大。手写数字一般是扫描件原图可能很大YOLOv8 会先做 letterbox把图等比缩放到imgsz。设太小数字缩没了设太大CPU 慢到不能接受。我的经验值是 640单张 A4 扫描图上的身份证号、银行承兑汇票上的金额数字640 都能覆盖。如果数字非常小比如 20x20 像素可以调成 960代价是 CPU 推理时间翻倍。conf阈值决定保留多少框。手写数字背景一般干净0.25 够用但遇到圆珠笔笔迹浅、连笔多的数字和背景对比度低模型置信度会降到 0.1 左右。这时候把阈值调到 0.1先拿到框再在后处理里排除低质量结果而不是在一开始就把它丢掉。iou是 NMS 的阈值。数字之间基本不重叠所以保持 0.5 或 0.6 即可。如果扫描件里数字有笔画交叉或者手写数字歪斜导致框互相压着就把 iou 调到 0.2~0.3能减少保留重复框。| 参数 | 默认值 | 数字场景建议 | 原因 | | imgsz | 640 | 640~960 | 小目标需要尽量保留分辨率 | | conf | 0.25 | 0.1~0.3 | 手写字符置信度波动大 | | iou | 0.7 | 0.5~0.6 | 数字框重叠少不需要太激进的抑制 |注意这里说的是推理参数。训练时会自动用训练集的imgsz预测时如果和训练不一致精度会下降所以训练和预测尽量保持同一分辨率。3. 手写数字数据集准备从 MNIST 到 YOLO 格式的转换与检查3.1 MNIST / EMNIST 转为 YOLO 格式合成检测框的脚本MNIST 是单通道 28x28 图像它只有图像和标签没有目标检测框。要拿它来训 YOLOv8一种常用做法是把多个 MNIST 样本贴到一张大图上同时生成每个数字的框。这样既借鉴了 MNIST 的标注质量又能让模型学到“一张图里有多个数字”的检测能力。我一般会这样写一个合成脚本import cv2 import numpy as np import random def paste_digit(canvas, digit_img, box_size(40, 40)): # 将 28x28 数字缩放到 box_size并贴到画布随机位置 h, w box_size digit_resized cv2.resize(digit_img, (w, h)) x random.randint(0, canvas.shape[1] - w) y random.randint(0, canvas.shape[0] - h) canvas[y:yh, x:xw] np.maximum(canvas[y:yh, x:xw], digit_resized) x_center (x w / 2) / canvas.shape[1] y_center (y h / 2) / canvas.shape[0] box_w w / canvas.shape[1] box_h h / canvas.shape[0] return [x_center, y_center, box_w, box_h], x, y, w, h这段脚本的核心逻辑是先把digit_img缩放到 40x40放在画布随机位置然后生成 YOLO 格式的归一化框。np.maximum是为了避免数字叠加时直接覆盖掉背景。这里有一个容易被忽略的点MNIST 的像素值只有 0~255缩放前最好做一次灰度拉伸否则贴到大画布上数字会偏淡训练时模型学到的边缘特征不明显。MNIST 合成只是其中一种方案。如果标题里的数据集包里已经提供了检测标注的图片就直接跳过合成用ultralytics自带的标签校验功能检查即可。如果用的是 EMNIST注意它是一个字母数字混合数据集要过滤出数字类否则标签索引和 YOLO 的类别定义会对不上。3.2 用 labelme 标注自己收集的数字图片并转成 YOLO 格式如果没有现成数据集或者要累积自己的业务数据手工标注是绕不开的。labelme是最容易上手的标注工具画矩形框很快。pip install labelme labelmelabelme 默认保存成 JSON 格式每个矩形框记录一个label字符串。我们把它转成 YOLO 的 txtimport json import os label_to_id {str(i): i for i in range(10)} def convert_labelme_to_yolo(json_path, target_dir): with open(json_path, r, encodingutf-8) as f: data json.load(f) image_w, image_h data[imageWidth], data[imageHeight] lines [] for shape in data[shapes]: label shape[label] if label not in label_to_id: continue # labelme 矩形框给出两点坐标 x1, y1 shape[points][0] x2, y2 shape[points][1] x1, x2 min(x1, x2), max(x1, x2) y1, y2 min(y1, y2), max(y1, y2) x_center ((x1 x2) / 2) / image_w y_center ((y1 y2) / 2) / image_h box_w (x2 - x1) / image_w box_h (y2 - y1) / image_h lines.append(f{label_to_id[label]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) txt_path os.path.join(target_dir, os.path.basename(json_path).replace(.json, .txt)) with open(txt_path, w) as f: f.write(\n.join(lines))说明一下YOLO 的标签文件与图片同名放在labels/下图片放在images/下。转换时要注意imageWidth和imageHeight必须是原始图片尺寸如果标注前做过缩放保存 JSON 时用的是缩放图但训练时加载原图框就全偏了。这个坑我踩过转换出来的框盖不住数字损失函数看着在降但精度永远是零就是因为尺寸不匹配。3.3 数字图像处理数据增强与预处理让模型看到更“干净”的数字手写数字检测的难点不是类别多而是同一个数字在不同人笔下差异巨大。7 可能带一横1 可能带弯钩0 可能写成小写字母 o。做数据集的时候我们应该把“采集到的原始图像”处理成更符合检测模型输入的形式。常见的处理链是灰度化去掉彩色干扰高斯去噪扫描纹路和纸张颗粒自适应阈值二值化把数字和背景分开形态学闭运算把断笔连起来。这一步可以用 OpenCV 快速完成import cv2 img cv2.imread(scan.jpg, cv2.IMREAD_GRAYSCALE) img_blur cv2.GaussianBlur(img, (5, 5), 0) _, img_bin cv2.threshold(img_blur, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) img_close cv2.morphologyEx(img_bin, cv2.MORPH_CLOSE, kernel, iterations1)这段代码产出的二值图可以直接用来可视化检查也可以作为数据增强的一部分。但不要把所有训练图都强制二值化再喂给 YOLO因为 YOLO 自己会在训练时做色彩增强提前二值化反而会丢失灰度层次让模型对铅笔浅迹更敏感。再说增强参数。如果使用albumentations我会这样配import albumentations as A transform A.Compose([ A.Rotate(limit15, p0.8, border_mode0, value0), A.ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit0, p0.5), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.5), A.GaussNoise(var_limit(10, 30), p0.2), ])特别提醒手写数字旋转不要超过 15~20 度否则 6 和 9、7 和 1 在视觉上几乎无法区分。旋转适当时模型对倾斜数字的鲁棒性会提高旋转过大反而让测试分数往下掉。这是数字图像处理里最容易“好心办坏事”的一个增强点。4. 用 ultralytics 训练手写数字模型模型选择与参数调优4.1 选 yolov8n 还是 yolov8s小目标数字的容量权衡YOLOv8 有 n/s/m/l/x 五个版本。手写数字总共 10 类特征简单不需要把模型拉得很大。我一般建议从yolov8n开始推理速度快、CPU 也能勉强训练精度损失在数字场景下并不明显。下面是一个简单对比| 模型 | 参数量(M) | CPU 训练速度 | 精度表现 | | yolov8n | ~3.2 | 较快 | 数字分类够用 | | yolov8s | ~11.2 | 慢 | 对模糊、低对比度更稳 | | yolov8m | ~25.9 | 很慢 | 一般不推荐 |如果你的图片里数字清晰、笔画规整n 就够了。如果输入是手机拍到的票据光线不均、透视倾斜那就换 s。注意 m/l/x 在 CPU 上训练一轮几十张图可能都要几分钟除非你有 GPU否则不建议碰。数字检测的核心资产是数据和标签质量模型容量反而是次要的。4.2 训练脚本data.yaml 的写法和关键参数训练 YOLOv8 分两步。先准备数据描述文件digits.yamlpath: ./datasets/digits # 数据集根目录 train: images/train val: images/val names: 0: 0 1: 1 2: 2 3: 3 4: 4 5: 5 6: 6 7: 7 8: 8 9: 9这里的path是相对路径或绝对路径。train和val是图片目录相对 path 的路径YOLO 会自动去同级的labels目录找标签。最容易出错的是names顺序如果标注脚本里把“0”放在索引 1训练时却把它放在索引 0结果就是模型学了半天预测结果全部错位。所以训练前最好先跑一次数据可视化校验。然后执行训练yolo detect train modelyolov8n.pt datadigits.yaml epochs60 imgsz640 batch8 devicecpu projectrun_digits nameexp1拆解一下这几个参数modelyolov8n.pt这里填预训练权重。可以是官方 COCO 权重也可以先用yolov8n.yaml从零开始训练。建议用 COCO 权重做迁移学习虽然 COCO 没有数字但模型已经学会了边缘和纹理特征能减少训练轮数。epochs60手写数字集通常不大60 轮足够。如果训练损失还在明显下降继续加imgsz640训练输入尺寸尽量和预测一致batch8CPU 内存小batch 太大容易内存爆炸。8 是安全值devicecpu没有 GPU 时明确指定免得它去试探 CUDA 然后报错。用 Python API 也可以实现同一件事并且能拿到训练过程中每个 epoch 的损失指标from ultralytics import YOLO model YOLO(yolov8n.pt) results model.train( datadigits.yaml, epochs60, imgsz640, batch8, devicecpu, projectrun_digits, nameexp1, lr00.001, cos_lrTrue, )这里加了lr00.001。很多新手直接用默认学习率 0.01在自定义数据集上容易梯度爆炸。尤其我们只做 10 类数字学习率小一点更稳。cos_lrTrue让学习率按余弦曲线下降长时间训练能避免尾部震荡训练损失后期会平滑很多。4.3 训练过程的 4 张图怎么看损失、精确率、召回率与混淆矩阵训练结束后run_digits/exp1/下会生成results.png和confusion_matrix.png。不要只盯着总损失要看分开的指标train/box_loss目标框回归损失。数值从 1.x 降到 0.2~0.3 是正常的train/cls_loss分类损失。数字场景 10 类应该降到 0.1 以下metrics/precision(B)精确率检测出的框里有多少是真正的数字metrics/recall(B)召回率图像里所有数字有多少被框出来了。典型场景是精确率 0.95、召回率 0.6。这代表模型“框得很准但漏了很多”。原因可能是 conf 阈值设置过高或者训练数据中每张图数字数量太多模型只能记住一部分。我会先用yolo detect val验证原始精度再调整conf而不是直接怀疑模型没训练好。如果 val loss 在 20 轮后开始反弹而 train loss 还一直降就是过拟合。解决办法依次是增加数据增强、减少 epochs、换更小的模型s 改 n。反过来如果 train loss 和 val loss 都降不到低位可能是标签本身有错误这时应该回看标注可视化。5. 手写数字识别落地避坑训练、推理和 OCR 里的 4 个典型问题5.1 把“7”识别成“1”目标框松动和标签噪声怎么处理现象训练集准确率很高但在真实扫描件上总是把“7”识别成“1”而且每张图都稳定错不是偶发。原因训练集里的“7”大多没有横笔而实际场景里很多人在“7”中间加一横模型看到带横的字符时特征匹配到了“1”的竖线。另一种常见原因是目标框太紧把“7”的横划截在框外模型看不到完整字形。解决第一步检查标签框是否完全包住数字的墨迹。我一般写一个小脚本把标注框和原图画在一起人眼扫一遍。第二步收集带横笔的“7”样本至少 50 张合成到训练集。第三步如果短期拿不到新数据可以在后处理里对“7”和“1”做修正数字“7”通常有左上角的水平笔画而“1”在这个位置没有。这个判断用 5x5 的核在原图上做形态学角点检测就能完成不用重新训练。5.2 CPU 推理很慢预处理变成了黑匣子现象在 Ubuntu 20.04 CPU 机器上跑一张 6000x4000 的扫描图要十几秒其中模型只占两秒大部分时间不知道去哪了。原因YOLO.predict()默认会对大图做缩放OpenCV 在 CPU 上把 6000x4000 的图缩放再填充到 640这个操作本身就慢。另外如果你把整页扫描图直接送入模型YOLOv8 的 letterbox 会做大量图像复制内存带宽也吃紧。解决对于手写数字这种小目标常见做法是先做图像金字塔或者按区域切图。比如把扫描图用 OpenCV 按 640x640 的窗口滑动重叠 10%每个窗口单独检测。这样每个窗口内数字的实际像素面积更大模型推理更快召回率也更高。另一个技巧是别用halfTrueCPU 上半精度实际上不会省多少时间别指望它。5.3 训练 loss 突然出现 NaN梯度爆炸还是标签脏现象训练到第 5 轮时box_loss突然变成nan随后所有指标全部失效。原因最常见的不是学习率而是标签里有坐标越界的框。某些转换脚本在图像宽度高度不一致时误用imageWidth作为所有图片的高导致y_center 1。YOLO 的损失函数对越界的坐标算出一个离谱的梯度几十步内就把权重冲飞。解决训练前执行yolo detect train时YOLO 会自动跳过非法标签但不会告诉你跳过了多少。我习惯先写脚本读所有 txt检查每个值是否在 (0,1] 区间import os label_dir datasets/digits/labels bad_files [] for fname in os.listdir(label_dir): with open(os.path.join(label_dir, fname)) as f: lines f.read().strip().splitlines() for line in lines: parts line.split() if len(parts) ! 5: bad_files.append(fname length_error) else: values list(map(float, parts[1:])) if any(v 0 or v 1 for v in values): bad_files.append(fname range_error) print(bad_files)这段脚本的意图是找出所有“解析失败”和“越界”的标签文件。如果bad_files不为空基本不用怀疑就是它们导致 loss 变成nan。清洗完再训练。还有一种少见的 NaN 原因是 batch 里含全黑图片手写数字二值化后如果全图都是 0模型照样能算出损失但梯度可能异常。预处理时把所有全黑样本剔除。5.4 检测框把数字切成了两半后处理怎么合并现象模型对一串连续的数字只框住了一部分比如“1234”被识别成“1”、“23”、“4”或者干脆把相邻的两个数字框在一起。原因手写数字间距不统一当笔画粘连时检测框的 NMS 把两个数字合并成一个框而当单个数字笔画较窄时模型可能用两个框去覆盖一个数字的上下段。解决第一种用较低 confidence 较高 IOU 阈值先把所有框取出来再用“位置关系”做二次分类如果两个框左右相邻且高度重叠面积超过 60%就合并如果同一垂直位置有两个框取两者中更大的。第二种用形态学的水平投影法做字符分割把图片中每一列像素和不为 0 的区域切出来再逐块送进检测模型。这样检测模型就退化为分类器避开了检测框切错的问题。针对粘连字符推荐之后补一个基于连通域的分割步骤先二值化再计算每个连通域的外接矩形矩形内再用模型判断。6. 把模型接进 OCR 流程字符分割与置信度校准技巧在手写数字检测里模型只是中间一步前后各有一片传统图像处理的地盘。我最近常推荐一套组合原图转灰度Otsu 二值化再按连通域找候选区每个候选区缩放到 64x64 后直接丢给 YOLO 模型判断。这样做的优势是连通域切分天然解决了密集数字的“框错”问题YOLO 只负责对每个候选区做 0-9 分类。import cv2 import numpy as np from ultralytics import YOLO model YOLO(best.pt) # 训练好的数字检测权重 img cv2.imread(handwritten.png, cv2.IMREAD_GRAYSCALE) _, bin_img cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) contours, _ cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) digits for c in contours: x, y, w, h cv2.boundingRect(c) if w 5 or h 5: continue patch bin_img[y:yh, x:xw] patch cv2.resize(patch, (64, 64), interpolationcv2.INTER_NEAREST) patch_3c cv2.cvtColor(patch, cv2.COLOR_GRAY2BGR) result model.predict(patch_3c, conf0.35, imgsz64, verboseFalse) if len(result[0].boxes) 0: digits str(int(result[0].boxes.cls[0].item())) print(digits)这段代码里RETR_EXTERNAL只取外边界适合数字之间没有粘连的情况如果手写数字连笔严重横向投影更稳。interpolationcv2.INTER_NEAREST保留硬边缘避免二值图缩放后出现模糊。另外分类模式下imgsz64要描述清楚训练时模型在 640 的图上见到的是小尺寸数字现在输入是 64x64相当于把数字放大推理结果一般没问题。还有一个实用技巧是置信度校准检测模型在分类 6 和 9 时置信度往往都在 0.8 左右很难靠阈值分开。更可靠的是统计每个类别在验证集上的平均置信度松弛一个分位点作为“低置信度预警线”。比如“6”的 P10 置信度是 0.72那么低于 0.72 的“6”就自动标为需人工复核。这种校准比单纯调阈值更能让结果可信。我训练完手写数字模型一定会在真实场景里随机抽 20 张图把模型的预测框叠加输出一页一页看“离散点被当成数字”的情况而不是只看 mAP。数字检测的翻车往往是工程结构问题不一定是模型问题。把这些环节处理好这个标题下的方案才真正落地。希望帮到你。本文还有配套的精品资源点击获取
返回列表