ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

基于YOLOv8的甲骨文拓片单字分割识别实战

基于YOLOv8的甲骨文拓片单字分割识别实战 简介本资源为基于 YOLOv8 的甲骨文原始拓片图像单字分割识别模型源码包面向计算机视觉学习者、深度学习竞赛参与者及古文字数字化研究者解决甲骨文字形复杂、传统识别效率低的问题。项目将识别流程拆分为目标检测与字符识别两阶段先由 YOLOv8 检测模型圈出拓片中的文字矩形区域再借助分类模型判定字形对应的具体字符取材自 2024 年 MathorCup 数学应用挑战赛 B 题。压缩包共 16 个文件约 627KB以 10 个 Python 脚本为核心涵盖检测与分类的预测入口、数据处理及可视化绘制模块另附 yaml 配置、requirements 依赖说明、示例图片与操作手册文档目录按 detect、classify、utils 等模块划分便于按阶段阅读与复现。目前已有 440 人学习下载适合希望掌握 YOLOv8 检测与分类联合流程、积累跨学科项目经验的读者参考。1. 甲骨文拓片单字分割识别从一张模糊拓片到可检索字库手上拿到一批甲骨文拓片扫描件灰度、低对比、字迹和骨面纹理糊在一起这是做古文字数字化最头疼的起点。基于 YOLOv8 的甲骨文原始拓片图像单字分割识别模型要解决的就是把一张整版拓片里的单个甲骨文字自动框出来、分割出像素级掩码、再给出对应的识别结果。它适合三类人做古籍数字化的工程团队、想用真实冷门数据集练 YOLOv8 分割的算法同学、以及需要把甲骨文字形做成可检索字库的研究者。这件事的难点不在 YOLOv8 本身而在于拓片没有干净背景、字与字粘连、标注成本极高。下面按我实际落地的顺序把数据、训练、分割、识别、部署这条链路拆开讲清楚能抄的代码和参数我都放出来。2. 拓片数据怎么变成 YOLOv8 能吃的分割数据集2.1 先想清楚为什么用分割而不是纯检测甲骨文单字任务里检测框只能告诉你「这里有个字」但拓片字迹边缘和骨面裂纹交织框内往往混进大量非文字纹理。如果下游要做字形比对、拓片缀合或者字库矢量化必须拿到像素级掩码所以实例分割是刚需。YOLOv8 的-seg系列在检测头之外多了一个掩码分支输出masks和boxes两套结果正好匹配「先定位再抠形」的需求。选 YOLOv8 而不是 Mask R-CNN主要理由是拓片数据集通常只有几百到几千张YOLOv8 收敛快、显存占用低单张 2080Ti 甚至 1660Ti 都能跑起来对预算有限的项目更友好。2.2 标注格式labelme 转 YOLO-seg 的脚本拓片标注我一般用 labelme 画多边形因为甲骨文字形不规则矩形框根本贴不住。labelme 存出来是 JSONYOLOv8 分割要的是每行class x1 y1 x2 y2 ...的归一化坐标 txt。转换脚本如下import json import os from pathlib import Path # 类别映射甲骨文单字项目里通常先按字或未识别字粗分 CLASS_MAP {jiaguwen: 0} def labelme_to_yolo_seg(json_path, out_dir, img_w, img_h): with open(json_path, r, encodingutf-8) as f: data json.load(f) lines [] for shape in data[shapes]: label shape[label] if label not in CLASS_MAP: continue pts shape[points] # [[x1,y1],[x2,y2],...] # 归一化并限制在 0~1防止标注越界导致训练报错 norm [] for x, y in pts: norm.append(min(max(x / img_w, 0.0), 1.0)) norm.append(min(max(y / img_h, 0.0), 1.0)) line str(CLASS_MAP[label]) .join(f{v:.6f} for v in norm) lines.append(line) out_path Path(out_dir) / (Path(json_path).stem .txt) out_path.write_text(\n.join(lines), encodingutf-8) # 批量处理img_w/img_h 必须和原图一致否则掩码会整体偏移 for jp in Path(labels_json).glob(*.json): labelme_to_yolo_seg(jp, labels_seg, 1024, 1024)逻辑说明YOLOv8 分割标签要求多边形点按顺序排列且归一化到 0~1类别用整数索引。参数上img_w/img_h一定要取原图真实尺寸很多人直接写 640 导致掩码缩放错位。CLASS_MAP在甲骨文项目里建议先做单类等单字分类模型稳定后再拆多类否则每类样本太少分割头学不动。2.3 数据集目录与 data.yamlYOLOv8 对目录结构有固定约定图片和标签同名不同后缀放在 images/labels 下dataset/ ├── images/ │ ├── train/ # 训练图 │ └── val/ # 验证图 ├── labels/ │ ├── train/ # 对应 txt │ └── val/ └── data.yamldata.yaml内容path: /home/user/dataset train: images/train val: images/val nc: 1 names: [jiaguwen]参数说明nc是类别数单字分割阶段填 1names顺序必须和转换脚本里的CLASS_MAP一致。拓片数据量少时train/val 按 8:2 切且要保证同一版拓片的不同单字不要同时出现在训练和验证集否则验证指标虚高这是血泪经验。3. YOLOv8-seg 训练参数怎么设、损失怎么看3.1 环境搭建与预训练权重Ubuntu 20.04 上 CPU 版本也能跑通小数据集但训练建议至少一张 8G 显存以上的卡。安装命令conda create -n yolo8 python3.10 -y conda activate yolo8 pip install ultralytics labelme opencv-python # 验证安装 yolo checks预训练权重用yolov8n-seg.pt起步数据量上千后再换yolov8s-seg.pt。权重文件放到项目根目录训练时自动加载。CPU 版本训练速度极慢只适合调试代码是否跑通正式训练还是上 GPU。3.2 训练命令与关键参数yolo segment train \ modelyolov8n-seg.pt \ datadataset/data.yaml \ epochs200 \ imgsz1024 \ batch8 \ lr00.01 \ lrf0.01 \ patience30 \ mosaic0.5 \ degrees10 \ translate0.1 \ scale0.3 \ projectruns/seg \ namejiagu_v1参数说明imgsz1024是因为拓片分辨率高字迹细节在 640 下会丢batch8按显存调爆显存就降到 4lr00.01是初始学习率lrf0.01是最终学习率比例配合余弦退火patience30表示 30 轮无提升就早停防止过拟合mosaic0.5做马赛克增强但拓片本身纹理复杂mosaic 太高会让字迹更难辨认我一般不超过 0.5degrees/translate/scale是几何增强拓片扫描角度有偏差时有用。3.3 损失曲线与指标解读训练完在runs/seg/jiagu_v1/下有results.csv和results.png。重点看三条曲线train/seg_loss、val/seg_loss、metrics/mAP50-95(M)。分割任务里 mask 的 mAP 通常比 box 低 5~10 个点这是正常的。如果train/seg_loss持续下降而val/seg_loss抬头说明过拟合要么加数据要么把mosaic和degrees调低。画损失曲线的脚本import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/seg/jiagu_v1/results.csv) df.columns [c.strip() for c in df.columns] plt.plot(df[epoch], df[train/seg_loss], labeltrain_seg) plt.plot(df[epoch], df[val/seg_loss], labelval_seg) plt.xlabel(epoch) plt.ylabel(seg_loss) plt.legend() plt.savefig(seg_loss.png, dpi150)逻辑说明results.csv列名可能带空格先 strip 再取。看曲线时不要只盯 lossmetrics/mAP50(M)才是最终分割质量的判据拓片单字任务里 mAP50 能到 0.75 以上就算可用。4. 单字分割后处理从掩码到独立字形4.1 掩码提取与二值化YOLOv8 推理输出的是masks对象需要转成 numpy 再二值化。代码from ultralytics import YOLO import cv2 import numpy as np model YOLO(runs/seg/jiagu_v1/weights/best.pt) results model.predict(tuopian_001.jpg, imgsz1024, conf0.25, iou0.5) for r in results: if r.masks is None: continue masks r.masks.data.cpu().numpy() # shape: (n, h, w) for i, m in enumerate(masks): binary (m 0.5).astype(np.uint8) * 255 # 形态学去噪拓片掩码边缘常有毛刺 kernel np.ones((3, 3), np.uint8) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) cv2.imwrite(fchar_{i}.png, binary)参数说明conf0.25是置信度阈值拓片噪声多时可以提到 0.4 减少误检iou0.5控制重叠框合并m 0.5是掩码二值化阈值调高会让字形变瘦调低会粘连。形态学开运算用来去掉小噪点核大小 3x3 足够太大反而吃掉笔画。4.2 粘连字分割分水岭与投影法甲骨文里两个字贴在一起很常见YOLOv8 可能把它们检成一个实例。后处理用垂直投影找谷底切分def split_by_projection(binary): col_sum binary.sum(axis0) # 找投影谷底作为切分点 threshold col_sum.mean() * 0.3 splits [] in_gap False for x, v in enumerate(col_sum): if v threshold and not in_gap: in_gap True splits.append(x) elif v threshold: in_gap False return splits逻辑说明投影法适合左右结构的粘连字上下结构要换成行投影。threshold取均值的 0.3 倍是经验值拓片笔画细时可以降到 0.2。切分后每个子图再单独送识别模型不要指望分割模型一次解决所有粘连。5. 避坑与排查拓片项目里最容易翻车的五件事5.1 掩码整体偏移现象训练 loss 正常下降但推理时掩码和字迹错位半个字。原因标注时img_w/img_h用了缩放后尺寸和原图不一致。解决转换脚本里读原图cv2.imread拿真实尺寸或者用 labelme JSON 里的imageWidth/imageHeight字段。5.2 验证集指标虚高现象mAP50 到 0.9实际推理一塌糊涂。原因同一版拓片的单字被随机切分到 train 和 val模型记住了骨面纹理而非字形。解决按拓片编号分组切分同一版拓片整体进 train 或 val。5.3 小字被漏检现象大个字能检出小字全丢。原因imgsz太小或 anchor 尺度不匹配。解决把imgsz提到 1280或者在data.yaml里加overlap_maskTrue同时把conf降到 0.15 观察召回。5.4 训练中途显存爆掉现象前几十轮正常突然 OOM。原因mosaic 增强在后期关闭时 batch 内图像尺寸变化或者验证阶段缓存累积。解决固定batch不要用-1自动模式验证时加valFalse分阶段跑。5.5 识别模型和分割模型类别对不上现象分割出 5 个字识别结果只有 3 个。原因识别模型单独训练时类别索引和分割的CLASS_MAP不一致。解决两个模型共用一份classes.txt训练前用脚本校验索引映射。6. 把单字送进识别模型CRNN 微调与端到端串联分割只是第一步真正让拓片可用的是识别。我一般用 CRNN 做单字分类输入是分割出的二值字形图输出是字符编码。数据量少时先用分割掩码做数据增强把每个字旋转 ±10 度、加高斯噪声扩充 5 倍再训。CRNN 训练脚本核心部分import torch import torch.nn as nn class CRNN(nn.Module): def __init__(self, num_classes): super().__init__() self.cnn nn.Sequential( nn.Conv2d(1, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(128, 256, 3, padding1), nn.ReLU() ) self.rnn nn.LSTM(256, 128, bidirectionalTrue, batch_firstTrue) self.fc nn.Linear(256, num_classes) def forward(self, x): x self.cnn(x) # (B,256,H/4,W/4) B, C, H, W x.shape x x.permute(0, 3, 1, 2).reshape(B, W, C * H) x, _ self.rnn(x) return self.fc(x[:, -1, :]) # 取最后时间步做分类参数说明输入单通道灰度图num_classes是甲骨文字表大小常见项目里先做 500 类以内。LSTM 隐藏层 128双向拼接后 256。训练时lr1e-3batch64用 Adam。端到端串联时分割输出的每个掩码裁剪原图对应区域缩放到 32x32 送 CRNN置信度低于 0.6 的识别结果标为「待考释」不要强行输出。验证方法上我习惯留 10% 拓片做闭集测试另找 20 个未参与训练的字做开集测试。闭集准确率到 90% 以上、开集能到 60% 左右这个方案就值得继续投入。最后说个习惯每次改完数据增强或后处理参数先跑 20 张图的推理可视化肉眼看掩码贴合度比盯指标快得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表