简介:面向甲骨文数字化与计算机视觉方向学习者,这份资源提供了基于YOLOv8的原始拓片图像单字分割识别完整赛题实现。项目按两阶段设计:先以YOLOv8目标检测定位甲骨文字符矩形区域,再通过YOLOv8分类模型判断具体字形,并配有Flask后端推理接口和Web UI可视化,便于演示与二次开发。资源共13个文件,以css/js/html前端资源、Python脚本与toml配置、Markdown说明文档为主体,包体约105KB,结构紧凑,适合毕业设计、比赛复现及古文信息处理入门实践。压缩包内含源码、说明文档及界面截图,可帮助理解目标检测与分类的工程衔接、接口调用方式和前端展示逻辑;现有74人学习浏览,对希望快速搭建甲骨文识别演示系统或扩展自有数据集的开发者有直接参考价值。
1. 甲骨文原始拓片图像单字分割识别:这包 YOLOv8 比赛源码解决的是什么
把一张满是裂纹、墨色斑驳和钙化点的甲骨拓片图像交给算法,要求把每个单字挨个框出来并标上字种,是近几年古籍数字化比赛里反复出现的一类题目。标题这包“基于YOLOv8”的源码,核心就是把 YOLOv8 当作单字分割识别的主干模型——既负责把原始拓片上的字从背景噪声中分离出来,也负责给出每个单字的分类结果;“比赛源码 + 文档说明资料”则说明包里包含训练代码、推理脚本、参数配置和项目文档,属于一个能直接跑的 baseline。它适合三类人:备战古文字图像比赛的学生、做文物数字化系统的工程师,以及想用 YOLOv8 处理特殊文字目标的新手。下面的流程按数据、训练、避坑、提交四段展开,照着走就能复现一套完整的比赛方案。
2. 为什么普通 OCR 在拓片上直接失效:YOLOv8 的三个设计接得住这活
2.1 拓片图像的三道坎:裂纹、墨色不均和目标粘连
常规文档 OCR 的第一步是二值化和连通域分析,这招在甲骨拓片上一用就翻车。拓片上的字是刻痕在墨色底上的留白或凸起,天然带着三种普通预处理管线处理不动的问题:第一条,裂纹和笔画长得很像,连通域会把一个字断成好几个碎片;第二条,墨色在整张图像上分布极不均匀,同一张拓片里有的字浓得像墨块,有的字淡到和纸底几乎同色,固定阈值二值化之后字形边缘漂移非常严重;第三条,大小字粘连、字间距不稳定、单字姿态有旋转,版面分析很难稳定地切开每个字。
如果继续用传统图像处理,就得针对每一类拓片人工调阈值、调形态学核大小、调连通域过滤规则,换一张拓片就失效。这类比赛题目特意强调“原始拓片图像”,意思就是给你的是未经修图的照片,算法必须在噪声里把文字整体抓出来。YOLOv8 这类检测分割模型在这三点上比传统管线稳定得多,因为网络是在大量“裂纹 + 笔画 + 墨迹”组合样本上训练出来的,学到的是字形的高层结构特征,而不是局部的二值模式。
2.2 YOLOv8 网络结构里和古文字最相关的三个部位
做比赛不要求把 YOLOv8 每个卷积层都背下来,但有三个结构设计和这个任务强相关。
第一个是 C2f 构成的主干。C2f 是 YOLOv8 对 CSP 结构的改进,梯度在主干里有更丰富的分支流动,浅层和中层特征保留得比早期版本更充分。甲骨文字形细节弱、边缘信息少,主干太深太宽反而会钝化小字特征,C2f 这种结构能在保持足够深度的同时不丢掉细边缘。
第二个是 PAN-FPN 多尺度融合。它把高层的语义信息通过上采样回传给大特征图,再把大特征图的细节下传,让不同尺度输出都同时带上语义和细节。拓片里的字从十几像素到几百像素不等,同一个模型要同时召回大字和小字,多尺度特征融合决定了小字这一路能不能保住。
第三个是解耦检测头和分割掩码分支。检测框回归和类别预测被拆成两个分支,不再互相拉扯。而 YOLOv8-seg 版本在检测头基础上额外接了一个掩码分支,每个检测框都能输出一张对应单字区域的像素掩码。这个掩码分支本质上是在框内做细化,不是慢速的全图实例分割,但对单字切分来说,框加掩码的组合已经把字符和背景分开了。
2.3 单字分割识别选 YOLOv8-seg 还是 YOLOv8-detect
初学者最容易卡在这个选择上:标题写“分割识别”,包里跑的到底是 detect 还是 seg?
如果比赛的提交格式只要求矩形框加类别 ID,YOLOv8 目标检测模型就够了。但甲骨拓片有一个现实问题——字间距经常比字的笔画宽度还小,矩形框稍微偏一点就会把相邻字的笔画包进来,后续识别阶段等于每张图都掺了噪声。所以我一般建议直接上 YOLOv8-seg,用掩码裁掉邻字干扰,再缩放交给分类部分。这也是多数这类比赛最终方案的通用选择:矩形框没有像素级别的边缘,切出来的单字图干净程度远不如掩码裁剪。
实际落地还有一个两阶段思路可以备选:第一步先用单类别的 YOLOv8-seg 把“所有字”的位置和掩码分割出来,第二步把掩码区域抠图缩放到统一尺寸,交给一个独立的分类网络做字种识别。什么时候用两阶段?当比赛的类别数量超过 200 类,或者低频类别的样本数严重不够时,两阶段比一步到位的多类别 YOLOv8-seg 更容易调好。如果类别数在几十到一两百,且每类样本量都还说得过去,直接用多类别的 YOLOv8-seg 一步输出“掩码 + 字种”最省事。我的血泪经验是:开赛第一天先定任务格式,再决定用单类还是多类,不要等数据标完了再回头改标签结构。
3. 处理数据集用于 YOLOv8 训练:LabelMe 标注与格式转换的完整流程
3.1 标注规范:哪些算字、哪些噪声不算,以及负样本怎么保留
比赛给的原始拓片图像往往混着专家标记的释文,标注时第一步是把图像和对应的字表对齐。标注工具我一般用 LabelMe,它直接输出 JSON,里面存了每个多边形的顶点坐标和标签名,转换到 YOLOv8-seg 格式最方便。
标注规范里最容易出问题的是“哪些边缘算字”。我的做法是三条硬规则。第一,只有通过释文核对、能明确对应到某个甲骨文字的闭合区域才标,模棱两可的笔画不标;第二,钙化裂纹带来的细长高亮区域不管多像字痕都不标,这类区域交给模型当作背景去学;第三,一个字即使有断裂笔画,也要用闭合多边形把断裂部分整个包进来,不要拆成两个目标。多边形顶点数量按字形复杂度走,简单字 8 个点就够,复杂字可以到 20 个点,顶点太多反而会让归一化坐标在训练时抖动。
另外有一个常被忽略的点:完全没有字的背景图像也要保留,而且对应生成空的标签文件。YOLOv8 会把“图像有文件、但没有对应标注”的图像当成负样本参与训练,这对压制裂纹和纸纹的误检非常有效。我处理过的拓片数据集里,负样本占比加到 5% 左右,假阳性明显下降。
提示:LabelMe 的 JSON 里每个 shape 的 label 字段,建议直接写成比赛字表里的类别名。不要用“1”“2”这种临时编号存,后面改起来容易漏。
3.2 LabelMe JSON 转 YOLOv8-seg 格式:可直接复制的 Python 脚本
YOLOv8-seg 训练需要每个图像对应一个同名.txt文件,每一行代表一个目标,格式是:class_id x1 y1 x2 y2 ... xn yn,所有坐标按图像宽高归一化到 0 到 1 之间。下面这个脚本可以把 LabelMe JSON 批量转成这种格式。
import json import glob import os def labelme_to_yolo_seg(json_path, out_dir, classes): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_w = data["imageWidth"] img_h = data["imageHeight"] lines = [] for shape in data["shapes"]: label = shape["label"] if label not in classes: continue cls_id = classes[label] # 每个多边形顶点都转成归一化坐标,x / 宽,y / 高 norm = [] for x, y in shape["points"]: norm.append(str(round(x / img_w, 6))) norm.append(str(round(y / img_h, 6))) lines.append(f"{cls_id} " + " ".join(norm)) base = os.path.splitext(os.path.basename(json_path))[0] out_path = os.path.join(out_dir, base + ".txt") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) # 类别映射表:按比赛字表替换成你自己的类别名 classes = {"甲": 0, "乙": 1} os.makedirs("yolo_labels", exist_ok=True) for json_path in glob.glob("labelme/*.json"): labelme_to_yolo_seg(json_path, "yolo_labels", classes)脚本逻辑很简单:读 JSON → 取图像宽高 → 把每个多边形的点归一化 → 拼成一行写入 txt。核心参数就是classes这个字典,键是 LabelMe 里的标签名,值是对应的类别 ID,转换前务必和比赛字表逐字核对一遍,一旦后面训练完发现类别错位,那可真叫一个黑匣子,排错能排到你怀疑人生。
注意两点:第一,LabelMe 的imageWidth和imageHeight必须和实际图像一致,否则坐标全部错位,训练时 mAP 会异常低;第二,如果某个 JSON 里没有任何 shapes,脚本会生成一个空 txt,这是正常现象,对应负样本。
3.3 数据集划分与 YAML 配置文件写法
转换完成后,目录结构按 YOLO 惯例整理成下面这样:
dataset/ images/ train/ val/ labels/ train/ val/图像和标签文件名要一一对应,后缀不同。划分数据集时我的习惯是按“拓片来源”分,而不是随机分。因为同一张拓片切出来的多个子图风格极像,随机划分会把同源图像同时塞进训练集和验证集,验证分数虚高。按拓片编号划分,验证集分数才有参考意义。
YOLOv8 的配置文件是一个 YAML,内容如下:
path: /home/user/oracle_dataset train: images/train val: images/val nc: 2 names: 0: "甲" 1: "乙"path是数据集根目录的绝对路径,train和val是相对path的图像目录。训练时会自动在同级目录下找对应的labels目录。nc是类别总数,names必须和转换脚本里的classes字典完全一致。这个 YAML 是整个训练流程最容易出错的地方,路径写错或者 names 顺序不对,训练能跑起来但结果完全不对,别问我怎么知道的。
4. 在 Ubuntu 20.04 搭建 YOLOv8 环境并训练:CPU 跑通流程,GPU 调优参数
4.1 环境搭建:CPU 和 GPU 两条路径的取舍
Ubuntu 20.04 上搭 YOLOv8 环境,核心依赖就是 PyTorch 和 ultralytics 包。常见做法是先建一个独立的 conda 环境,然后安装:
conda create -n yolo python=3.10 -y conda activate yolo pip install ultralyticsCPU 场景下不需要手动装 CUDA 版 PyTorch,默认的 PyTorch 装完会在 CPU 上自动降级运行。CPU 环境能训练到什么程度?我的体感是:小规模数据、几百张图、图像缩到 640 分辨率的前提下,yolov8n-seg 这种小模型可以过夜训练;但甲骨文拓片如果按原始分辨率 2000 像素以上去做 1280 输入,CPU 训练一轮可能就要几十分钟,这时候不要硬扛,还是得上 GPU。
不过 CPU 环境有一个不可替代的价值:跑通流程。标注、格式转换、训练、推理、提交格式验证,整套代码先在 CPU 上用最小数据集跑一遍,确认脚本没有 bug,再花钱租 GPU 跑正式训练。这样能省掉大量 GPU 试错成本,是成本最低的后悔药。GPU 环境安装则是一行pip install ultralytics加 CUDA 驱动正常就完事,重点不在安装,而在确认torch.cuda.is_available()返回 True。
python -c "import torch; print(torch.cuda.is_available())"如果返回 False,一般不是 ultralytics 的问题,而是 PyTorch 和 CUDA 驱动版本不匹配。先查nvidia-smi的 CUDA Driver 版本,再决定是否要装对应版本的 PyTorch。
4.2 训练命令与关键参数:imgsz、batch、epochs、patience 的作用边界
环境就绪后,用一份训练命令启动,以单字分割模型为例:
yolo segment train \ data=oracle.yaml \ model=yolov8s-seg.pt \ imgsz=1280 \ batch=8 \ epochs=150 \ patience=25 \ device=0 \ lr0=0.01 \ workers=4 \ seed=42model=yolov8s-seg.pt表示用 COCO 预训练权重做起点,这比随机初始化收敛快得多,古文字这种小众数据尤其需要预训练权重兜底。imgsz=1280是最关键的一个参数,因为拓片的小字在被缩到 640 后可能只剩十几个像素,特征几乎消失。显存够就尽量往 1280 或 1536 走,显存不够就降 batch 而不是降 imgsz。
| 参数 | 建议值 | 作用说明 |
|---|---|---|
| imgsz | 640 / 1280 / 1536 | 输入图像边长,小字多就调大,显存不够时优先降 batch |
| batch | 4 / 8 / 16 | 每次迭代样本数,和显存强相关,imgsz 翻倍时 batch 通常要减半 |
| epochs | 100 到 200 | 总轮数,配 patience 早停,不用贪大 |
| patience | 20 到 30 | 验证指标连续多少轮不涨就自动停 |
| lr0 | 0.01 默认 | 初始学习率,数据量小时不建议改成大值 |
| device | 0 / cpu | 训练设备,多卡可写 0,1 |
| workers | 4 到 8 | 数据加载进程数,CPU 加载瓶颈时调大 |
学习率策略 YOLOv8 默认的 SGD 加余弦退火就够用,不需要手动调 schedule。训练命令里还有几个增强参数值得关注:degrees控制随机旋转角度,甲骨文字方向是有意义的,我一般把degrees限制在 5 到 10 度,反向旋转 180 度会让某些对称字失去判据;scale控制图像缩放增强,不要设太大,默认 0.5 在单字切分场景已经足够。
4.3 损失曲线怎么看:什么时候该停手
训练结束后,runs/segment/train/xxx/目录下会生成results.png,里面画了训练集和验证集的 box_loss、cls_loss、seg_loss、dfl_loss 曲线。这是判断训练是否健康的主要依据。
我的判断规则很简单。第一,训练集 loss 下降、验证集 loss 跟着下降,正常训练,不用干预。第二,训练集 loss 持续下降但验证集 loss 先降后升,说明模型开始背训练集了,看 patience 触发早停即可。第三,训练一开始验证集 loss 就震荡不降,多半是数据格式问题,回去检查标签和 YAML,而不是调学习率。第四,验证集 mAP50 和 mAP50-95 之间有差距很正常,但 mAP50 高、mAP50-95 低,说明掩码边缘质量差,这类问题要回到 imgsz 和标注多边形精细度上去找原因。
模型权重会自动保存两个文件:best.pt是验证集指标最优的权重,last.pt是最后一轮权重。比赛提交用best.pt,不要因为好奇去试last.pt,除非你想体验分数跌一大截的滋味。
5. 避坑排查:甲骨文单字分割训练里容易血本无归的 5 个问题
5.1 训练报 CUDA out of memory
现象:训练没跑几步,终端直接报torch.cuda.OutOfMemoryError,显存占用率拉满。
原因:imgsz 和 batch 组合超出显存容量。甲骨文拓片原始分辨率高,只要 imgsz 拉到 1280,batch 稍微给大一点,12GB 显存瞬间就满了。
解决:按优先级做三件事。第一,把 batch 降到 4 甚至 2,这是最直接的止损;第二,把模型从yolov8x-seg.pt降到yolov8s-seg.pt或yolov8n-seg.pt,参数量对显存的影响远大于 batch;第三,检测一下是否开启了 AMP,ultralytics 默认是自动混合精度,如果自定义脚本里关了,重新打开能省近一半显存。如果都试完还炸,那就只能降 imgsz 到 960 或 640,但要做好小字召回率下降的心理准备。
5.2 模型把钙化裂纹识别成字
现象:推理结果里一半检测框落在裂纹、纸纹或钙化斑块附近,真正的字反而漏掉一部分。
原因:标注阶段把大量边缘噪声也当成了字形轮廓,数据集给模型传递了“裂纹也是一种字”的错误信号。这比模型本身的问题更可怕,因为噪声模式五花八门,模型会学出一整套假特征。
解决:先回头清洗标签,把模棱两可的多边形删掉,宁缺毋滥。然后在训练集里加入更多没有文字的负样本图像,让模型知道“没有目标”也是一种正常输出。最后在后处理阶段加一道过滤,按掩码面积、多边形复杂度或置信度阈值把碎片状检测结果滤掉。这三步做完,假阳性通常能压掉一半以上。
5.3 小尺寸单字大面积漏检
现象:训练集里字数少的拓片检测效果还好,字多且小的拓片几乎全漏,验证集的 recall 惨不忍睹。
原因:原始拓片被缩放到 imgsz 之后,小字在特征图上的响应区域还不到几个像素,PAN-FPN 的浅层特征再强也救不回来。另外一个隐蔽原因是训练增强里的scale参数,它会随机缩放图像,小字被缩得更小后直接变成噪声。
解决:首选把 imgsz 提到 1280 或 1536,这比任何模型结构改进都直接。如果显存不允许,把原始拓片按固定网格切块,切成 640x640 的重叠图块训练,推理时同样切块再合并结果——注意图块之间留 10% 重叠,避免单字恰好在切缝上被截断。增强参数里把scale调小到 0.3 左右,减少模型看到“更小字”的几率。
5.4 类别不均衡:高频字垄断训练损失
现象:损失曲线看起来正常,但混淆矩阵显示,出现频率高的字类识别准确率很高,低频字类几乎全被分错。
原因:YOLOv8 的分类损失对每个类别平等看待,低频类别的正样本数量太少,网络干脆把所有预测都推向高频类别。甲骨文字类天然就是长尾分布,常用字出现几百次,冷僻字可能只有个位数样本。
解决:如果有多类硬训这条路走不通,就切回两阶段方案——第一阶段只用单类分割把所有字切出来,不涉及字种分类;第二阶段在裁剪后的单字图上训练分类网络,对低频类别做重复采样和数据增强,把每类的样本数尽量拉平。如果比赛限定必须用单模型一步到位,那就把低频类的样本以缩放、平移、光照扰动的方式复制进训练集,跑出来的效果通常比硬训好一些,但别指望能完全弥补数据不足。
5.5 验证集 mAP 很高、比赛测试集分数低
现象:离线验证集上 mAP50 能到 0.9 以上,提交到比赛平台的测试集分数却掉了一大截。
原因:最常见的是域偏移。比赛测试集的拓片来源、纸张颜色、光照条件和你的验证集不一致,模型在验证集上的高分是“背”出来的。另一个常见原因是训练和推理的 imgsz 不一致,比如训练用 1280,推理时图省事改成 640,性能自然往下掉。
解决:第一,复现评估时严格用同一套 imgsz 和 conf 阈值;第二,把验证集按光照和纸底颜色分成几个子集分别统计 mAP,找出掉分的子集,针对它做归一化或增强;第三,比赛前把测试集的少量曝光样本拿出来做灰度归一化统计,如果和训练集灰度分布差太远,就加一层 CLAHE 或直方图匹配预处理。最后的后悔药是测试时增强,推理打开 TTA 通常能稳涨一两个点,后面会说。
6. 比赛提交前的最后一步:推理脚本、TTA 和提交格式核对
推理阶段的代码需要和训练配置严格对齐。下面是一个典型的批量预测脚本:
from ultralytics import YOLO import json model = YOLO("runs/segment/train/best.pt") results = model.predict( source="data/test", imgsz=1280, conf=0.25, iou=0.5, augment=True, # TTA,速度慢但分数更稳 save_txt=False, ) submission = [] for idx, r in enumerate(results): for box in r.boxes: submission.append({ "image_id": idx, "category_id": int(box.cls[0]), "score": round(float(box.conf[0]), 4), "bbox": [round(float(v), 2) for v in box.xyxy[0].tolist()], }) with open("submission.json", "w", encoding="utf-8") as f: json.dump(submission, f, ensure_ascii=False, indent=2)augment=True即测试时增强,YOLOv8 会对输入做多尺度变换后合并预测,比赛冲分阶段可以开着,但推理耗时翻倍,如果比赛有时间限制就要权衡。conf=0.25是一个折中阈值,如果发现假阳性偏高就提到 0.4,发现漏检偏多就往 0.15 降——这个值每次比赛都不一样,一定要在验证集上扫一遍再定。
提交前还有一道比算法更重要的核对:确认比赛要求的字段名和坐标格式。有的比赛要求矩形框加类别,有的要求掩码,有的要求类别 ID 从 1 开始而不是 0。这套源码里虽然带了提交脚本,但每次换比赛都要把category_id的偏移、bbox是 xyxy 还是 xywh、坐标是否要还原到原始分辨率这三件事逐项对一遍。提交格式看似不起眼,却是整场比赛里最不该丢分的地方。
做这类比赛做到最后,我最大的习惯是:每跑完一个实验,把训练命令、数据划分、conf 阈值和结果分数记在一个日志文件里。模型参数和结果都是玄学,只有记录下来的实验序列才是真正能复用的资产。希望这篇流程能帮你在甲骨文单字分割识别这个方向上少走几段弯路,拿到一个对得起标注成本的结果。
本文还有配套的精品资源,点击获取