YOLOv8实战:甲骨文拓片单字分割识别全流程
发布时间:2026/10/1 8:53:50来源:尧图网络
简介面向甲骨文数字化保护与计算机视觉研究的完整YOLOv8项目源码包专门解决原始拓片图像中的甲骨文单字分割与识别问题。项目按目标检测与字符识别两个阶段组织先利用YOLOv8检测模型框定文字矩形区域再利用图像分类模型判断字形对应的具体字符同时提供Flask后端推理接口与Web UI可视化页面便于直观展示和交互测试。该设计将检测与分类任务解耦便于分别调参与验证适合作为毕业设计、学科竞赛或相关人工智能课题的参考实现也帮助初学者理解两阶段模型在实际场景中的串联方式。压缩包共13个文件整体约105KB文件类型涵盖Python后端脚本、前端HTML/CSS/JS页面、环境依赖配置和简要说明文档等目录简洁、依赖清晰能快速搭建可运行的演示环境。目前已有74人学习下载配合源码注释与文档说明可帮助使用者快速掌握从模型调用到服务封装的完整流程节省项目开发与调试时间。1. YOLOv8 在甲骨文拓片单字分割识别里是主角但不是终点拿到这个比赛项目压缩包时大多数人第一反应是“里面是不是有现成的模型能直接跑”打开之后才发现核心是源码文档说明资料。这里面的技术主线很清楚用 YOLOv8 对甲骨文原始拓片图像做单字分割识别也就是先定位出每个甲骨文字的位置和轮廓再判断它属于哪个字形。这个任务不是通用 OCR它面对的是墨拓噪声、字形粘连、残损断裂的文物图像YOLOv8 负责的是最重的检测与分割环节识别则要靠后续的分类网络或模板匹配去完成。适合做这件事的人是研究古籍数字化、文物图像挖掘的工程师和打 OCR/古籍类比赛的学生技术底座是 Ultralytics YOLOv8你要拿到的是“能复现训练和推理”的完整链路而不是一个黑匣子模型。2. 单字分割识别为什么绕不开 YOLOv8选型逻辑与网络结构2.1 拓片图像不是普通场景文字任务定义与难点甲骨文原始拓片和自然场景里的路牌、发票文本完全是两类东西。拓片是通过墨拓工艺把甲骨上的刻痕转移到纸面上得到的图像通常存在三类干扰一是纸张纹理和墨迹深浅不均导致字迹与背景的对比度不稳定二是甲骨本身有裂纹、残损一个完整的字可能被物理裂缝切成两段三是拓片上的文字排列并不像现代文档那样横平竖直经常歪斜、大小不一甚至多个字粘连在一起。把这些难点落到技术动作上“单字分割识别”这个标题本身就暗示了两件事。分割指的是实例级分割要把每个甲骨文字作为一个独立实例从拓片里“抠”出来给到它的像素级掩码识别指的是拿到掩码之后再根据字形特征判定它属于哪个字或哪个序号。所以整个任务的输出不是整张图的文字序列而是“单字掩码类别标签”的结构化结果。比赛项目的源码通常不会把这两步做成一个端到端模型而是用 YOLOv8 做检测/分割再用分类支路或外部匹配做识别。这就要求你在跑源码时先看清它的推理脚本是从模型输出中直接取掩码还是把检测框裁下来再走一遍分类。2.2 检测、分割还是两者同时做YOLOv8-seg 对比传统方案如果你以前处理过文档图像里的印章或手写文字可能习惯用“连通域分析轮廓提取”的方式分离单字。对拓片来说这个套路基本会翻车因为墨迹深浅不均会造成同一个字断裂成多个连通域或者两字粘连导致连通域合并。两阶段的 Mask R-CNN 能同时产出检测框和掩码精度确实高但在比赛场景里速度不够友好而且训练时对小尺寸单字收敛较慢。YOLOv8-seg 属于单阶段实例分割一次前向同时输出边界框、类别和掩码系数在速度和精度之间取了一个很现实的中点。我一般会拿 YOLOv8-seg 和 Mask R-CNN 做对比表格如下方案掩码质量小目标表现推理速度训练门槛传统连通域极差差快无需训练Mask R-CNN高中慢高YOLOv8-seg中高好快低YOLOv8 分类头高(二次识别)好中低YOLOv8-seg 的掩码是基于检测框内特征生成的而不是像 Mask R-CNN 那样在 RoI 上做全分辨率分割所以掩码边缘会略显粗糙。但甲骨文单字识别更看重字形整体结构不是像素级边缘这个粗糙度通常可以接受。真正让我选择 YOLOv8 的原因是小目标能力和训练生态它的检测头对不同尺度特征做了解耦配合 PAN-FPN 结构对小尺寸文字比较友好同时 Ultralytics 仓库把数据加载、增强、训练、导出一整套流程都封装好了省去自己写训练的体力活。2.3 从源码里看 YOLOv8 如何处理单字边界如果你打开项目源码里的模型定义文件会看到 YOLOv8 不是堆叠多阶段 RoI 的结构。骨干网络用 C2f 模块提取不同感受野的特征接着经过 SPPF 池化增强全局信息再交给 PAN-FPN 做多尺度融合。到这里为止和 YOLOv5 差别不明显但 YOLOv8 把检测头换成了解耦头分类和回归分别走两条卷积分支这让模型在相近字形之间做类别区分时压力小很多。分割功能则是在检测头旁边加了额外的 mask 分支输出一组 mask 系数和检测框内的特征相乘后得到实例掩码。从落地角度看你不需要逐层手推这些结构但要理解一个关键行为YOLOv8-seg 的掩码质量严重依赖于检测框是否框准。如果预测框比实际字形大很多掩码会带上周围背景纹理如果框太小字典边缘的字形会被截断。所以在训练自己的数据时标注框尽量贴紧字形外接矩形不要图省事框一大片空白。顺着网络结构图去看源码里的 head 改进点时你会发现大多数比赛代码改动集中在 mask 分支的采样方式和 loss 权重这两处参数直接影响单字轮廓的精细程度。3. 把原始拓片变成 YOLOv8 能吃的训练集预处理、标注与数据划分3.1 原始拓片预处理二值化与分块原始拓片往往是整张高分辨率扫描图宽度可能上万像素。YOLOv8 训练时会把输入缩放到固定尺寸比如 640 或 1024如果直接把整张拓片缩进去单个甲骨文字会被压成几个像素检测头根本看不到字形结构。正确做法是先做分块切割让每个小图块里保留足够分辨率的单字然后再进入标注和训练流程。我的常用预处理流程分三步先做背景抑制再分块最后生成训练所需的图像列表。背景抑制不一定要做成强二值化因为二值化容易破坏残损字的笔画连贯性更稳妥的是用自适应阈值加形态学操作把非文字纹理弱化。分块时设置 overlap 重叠区域避免一个文字被硬生生切成两块。下面的代码能完成分块并输出块坐标信息import cv2 import numpy as np import json def preprocess_and_split(scan_path, block_size1024, overlap128): img cv2.imread(scan_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(fcannot read image: {scan_path}) # 自适应阈值增强对比度保留笔画断裂处的弱边缘 binary cv2.adaptiveThreshold( img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 51, 15 ) # 开运算去掉孤立噪点闭运算连接同一字内的断裂笔画 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) binary cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 按 overlap 滑窗切块 h, w binary.shape blocks [] for y in range(0, h, block_size - overlap): for x in range(0, w, block_size - overlap): y_end min(y block_size, h) x_end min(x block_size, w) crop binary[y:y_end, x:x_end] # 过滤掉完全没有文字内容的空白块 if crop.sum() 500: continue # 补边到统一尺寸方便后续直接喂给模型 pad_y block_size - crop.shape[0] pad_x block_size - crop.shape[1] crop_padded cv2.copyMakeBorder( crop, 0, pad_y, 0, pad_x, cv2.BORDER_CONSTANT, value0 ) blocks.append({ image: crop_padded, offset: [x, y], size: [x_end - x, y_end - y] }) return blocks # 使用示例分块并保存 blocks preprocess_and_split(scan001.png, block_size1024, overlap128) for i, b in enumerate(blocks): cv2.imwrite(fblocks/scan001_{i}.png, b[image])这段代码里有几个参数要特别注意。block_size决定每个训练样本的尺寸一般取 640、1024 或 1280。如果拓片里的文字普遍较小1024 能保留更多笔画细节但会显著加大显存占用如果单字较大640 反而更容易让小目标策略生效。overlap不能太大推荐 100-150太大会让同一文字出现在过多图块里变相加重训练样本的重复性。crop.sum()的阈值 500 是根据二值化后白色像素总数估计的不同扫描仪的墨色深浅不一样这个阈值要按实际图块调整否则会漏掉笔画稀疏的大字。3.2 标注格式转换LabelMe JSON 转 YOLO 分割 txt比赛源码里的文档说明一般会告诉你标注格式是 YOLO 还是 VOC。YOLO 分割格式的标注文件是 txt每行是一个目标class_id x1 y1 x2 y2 ... xn yn坐标值全部归一化到 0-1。手工标注时大家习惯用 LabelMe 或 CVAT 画多边形导出的是 JSON。直接把 JSON 喂给 YOLOv8 是不行的Ultralytics 只认它自己的 txt 路径所以要写一个转换脚本。import json import os def labelme_to_yolo_seg(json_path, output_path, class_map, img_h, img_w): 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: print(f跳过未定义类别: {label}) continue class_id class_map[label] points shape[points] # YOLO 分割需要多边形顶点顺序闭合 norm_points [] for x, y in points: norm_x max(0.0, min(1.0, x / img_w)) norm_y max(0.0, min(1.0, y / img_h)) norm_points.append(f{norm_x:.6f} {norm_y:.6f}) # 去掉最后一个与起点重复的点避免影响面积计算 if norm_points[0] norm_points[-1]: norm_points norm_points[:-1] line f{class_id} .join(norm_points) lines.append(line) with open(output_path, w, encodingutf-8) as f: f.write(\n.join(lines)) # 使用示例单张标注转换 class_map {甲: 0, 骨: 1, 文: 2} # 按实际标注类别修改 labelme_to_yolo_seg( annotations/scan001_block0.json, labels/scan001_block0.txt, class_map, img_h1024, img_w1024 )转换脚本里最容易出错的是坐标归一化和多边形顶点顺序。LabelMe 标注的坐标是原始像素坐标必须除以当前图块的宽高而且 YOLOv8 对分割多边形内部做了网格采样顶点顺序不一致会导致掩码错乱。另外要注意如果图块边缘在分块时补了黑边那么标注坐标也要依照补边后的图像尺寸来归一化这就是我在预处理里保存offset和size的原因转换标注前先根据偏移量把原始标注坐标平移到补边后的坐标。3.3 数据集划分与样本均衡很多人用 YOLOv8 训练自己的数据集时习惯直接把所有图像随机按 8:1:1 分成训练/验证/测试集这在甲骨文拓片上是个坑。同一张原始拓片分出来的多个图块内容高度相似随机划分会让验证集和训练集来自同一张拓片最终测试分数虚高。正确做法是按拓片编号划分比如 scan001、scan002 的全部图块进训练集scan003 的全部图块进验证集保证验证集能看到没见过的字形书写风格。import glob import random def split_by_scan(image_paths, train_ratio0.8, val_ratio0.15, seed42): # 从文件名中提取拓片编号例如 scan001_block0.png - scan001 scan_groups {} for path in image_paths: fname os.path.basename(path) scan_id fname.split(_)[0] # 按实际命名规则调整 scan_groups.setdefault(scan_id, []).append(path) all_scans list(scan_groups.keys()) random.seed(seed) random.shuffle(all_scans) train_scans all_scans[:int(len(all_scans) * train_ratio)] val_scans all_scans[len(train_scans):len(train_scans) int(len(all_scans) * val_ratio)] test_scans all_scans[len(train_scans) len(val_scans):] return ( [p for s in train_scans for p in scan_groups[s]], [p for s in val_scans for p in scan_groups[s]], [p for s in test_scans for p in scan_groups[s]] ) # 使用示例 all_images glob.glob(blocks/*.png) train, val, test split_by_scan(all_images, train_ratio0.8, val_ratio0.15) print(ftrain{len(train)}, val{len(val)}, test{len(test)})样本均衡问题也一样重要。甲骨文单字的类别分布极不平衡高频字可能出现几百次生僻字只有几个样本。YOLOv8 自带了 mosaic、copy-paste 等增强但类别不均衡时仅靠增强不够我通常会在数据配置里给每个类别设置weight或者在 loss 项里提高低频类的分类权重。比赛源码的文档说明如果提到了类别映射表你要先确认低频类是否被合并成“未知字”处理这会影响模型输出类别的可解释性。4. 训练 YOLOv8 甲骨文单字分割模型环境、命令与参数调优4.1 Ubuntu 20.04 搭建 YOLOv8 环境CPU 与 GPU 的现实差别很多拿到比赛源码的同学第一关就死在环境上。源码里的 requirements.txt 通常包含 opencv、numpy、torch、ultralytics 这几个大头。在 Ubuntu 20.04 上搭建 CPU 版本可以用下面的命令快速完成# 创建 Python 3.10 虚拟环境 conda create -n oracle python3.10 -y conda activate oracle # 安装 CPU 版 PyTorch注意不要装 CUDA 版 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu # 安装 Ultralytics 和其余依赖 pip install ultralytics opencv-python matplotlib pandas tqdm # 验证安装 yolo detect predict modelyolov8n.pt sourcetest.jpgCPU 版可以用来跑通前向推理和少量训练但真要训练甲骨文分割模型会慢到怀疑人生。如果你的机器是 GTX 1660Ti 这类中低端显卡建议直接装 CUDA 版 PyTorch并把模型尺寸降到yolov8n-seg或yolov8s-seg。1660Ti 的 6GB 显存跑yolov8s-seg在 640 分辨率下大约能塞 batch size 8跑 200 轮需要几个小时属于可接受范围。千万别默认用yolov8l-seg还没跑到 10 轮就会 OOM。4.2 用 Ultralytics 包启动训练命令、参数含义与调整策略进入正题。项目源码的文档说明一般会写两套启动训练的方式命令行和 Python API。命令行更直观适合快速验证参数Python API 方便你改回调、记录日志、加载自定义数据集。以官方数据集格式为准你的训练数据需要符合目录结构datasets/ oracle/ images/ train/ val/ labels/ train/ val/然后创建一个 YAML 配置文件oracle.yamlpath: ./datasets/oracle train: images/train val: images/val # 类别名列表顺序必须和标注文件里的 class_id 一一对应 names: 0: jiagu1 1: jiagu2 2: jiagu3 3: unknown之后用以下命令启动训练yolo segment train \ modelyolov8s-seg.pt \ dataoracle.yaml \ epochs200 \ imgsz1024 \ batch8 \ lr00.005 \ lrf0.01 \ patience50 \ workers4 \ cacheTrue \ projectruns/oracle \ nameseg_s_1024参数说明如下参数值作用与调整逻辑epochs200甲骨文字形细碎训练收敛比自然场景慢建议不低于 150 轮imgsz1024决定输入分辨率单字小就调大显存不够就用 640 配多尺度增强batch8受显存限制6GB 显卡建议 8多卡可加到 16lr00.005初始学习率比默认 0.01 低一半减少初始震荡lrf0.01余弦退火最低学习率比例1/100 比较稳妥patience50验证集指标 50 轮不提升就早停保护时间cacheTrue把训练图缓存到内存加快数据读取磁盘够用就开这些参数到底怎么理解imgsz是最关键的一个。我建议在 1024 和 640 各跑一小段对比一下如果 1024 的验证 mAP 明显高于 640说明小字细节确实需要高分辨率来支撑那就继续加如果差距不到 1 个点说明字形大小不是主要瓶颈维持 640 能省很多训练时间。patience在比赛场景里要小心早期 mAP 可能长时间不动然后突然上升50 轮可以但别设成 20。4.3 画损失函数曲线图判断收敛状态和过拟合训练完一轮后Ultralytics 会在runs/oracle/seg_s_1024/results.csv里记录每个 epoch 的 loss 和指标。用脚本画曲线是最直观的判断方式import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/oracle/seg_s_1024/results.csv) # 检查 CSV 列名一般包含 train/box_loss, train/cls_loss, train/dfl_loss, train/mask_loss, metrics/mAP50(B) 等 plt.figure(figsize(12, 6)) plt.subplot(1, 2, 1) plt.plot(df[train/box_loss], labelbox_loss) plt.plot(df[train/cls_loss], labelcls_loss) plt.plot(df[train/dfl_loss], labeldfl_loss) plt.plot(df[train/mask_loss], labelmask_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.title(Train Loss) plt.subplot(1, 2, 2) plt.plot(df[metrics/mAP50(B)], labelmAP50(B)) plt.plot(df[metrics/mAP50-95(B)], labelmAP50-95(B)) plt.xlabel(epoch) plt.ylabel(metric) plt.legend() plt.title(Validation Metrics) plt.tight_layout() plt.savefig(loss_curve.png, dpi150)判断标准不是看训练 loss 降得多低而是看验证集 mAP 和平滑度。甲骨文数据样本少经常出现训练 loss 一路下降、验证 mAP 却在一二十轮就停滞的情况这是典型的过拟合。解决办法第一优先是加数据增强把mosaic、mixup的强度提高第二是降低模型复杂度从yolov8m-seg退回yolov8s-seg第三才是提前停止。如果你画出的曲线出现验证 mAP 突然骤降再回升的锯齿波多半是验证集里混入了和训练集同源的图块回到第三章按拓片划分数据去检查。5. 甲骨文单字分割识别训练与推理中的 5 个典型坑与排查思路5.1 同一片拓片上字大小悬殊小字漏检严重现象是 mAP 看起来不低但推理大图时小字一个都检不出只有中等以上大小的字能框出来。原因在于 YOLOv8 的默认锚框策略是面向自然场景设计的甲骨文单字尺寸分布和 COCO 数据集差别很大。分块之后小字可能只占 32x32 像素以下模型下采样多次后特征已经丢失。解决方式有两个方向一是把imgsz从 640 提到 1024 甚至 1280让每个字占的像素更多二是调整数据增强里的scale参数默认 0.5 的随机缩放会随机缩小图像对本来就很小的文字雪上加霜。我用的是把scale0.3并关闭mosaic因为 mosaic 把四张小图拼接后每张图的尺度进一步被压缩。5.2 两个粘在一起的甲骨字被框成一个实例掩码分不开现象是检测框恰好覆盖了两个字掩码输出是一整块后端的识别分类无从下手。原因通常是标注阶段就没有拆开粘连字。标注员为了省事直接用一个多边形把两字圈在一起YOLOv8 学习到的就是“两个字等于一个实例”。解决方法是回炉标注把粘连处单独画出边界。如果实在无法重新标注可以在预处理阶段用形态学腐蚀把粘连处的细连接断开但要注意别把正常笔画也腐蚀没了。还有一种情况是模型推理时产生的预测框过大这时可以调低 nms 的iou阈值让重叠框更容易分离。5.3 背景纹理被误检成单字precision 很低现象是没文字的地方被框成文字尤其在拓片边缘、折痕和纸纤维较粗的区域。原因是训练集里的负样本太少。YOLOv8 的检测头天然会倾向输出“有目标”的预测如果你只标注了正样本模型会从正样本周围自动采样背景作为负样本但拓片背景比自然场景复杂得多必须手动加难负样本。解决方式是按 2:1 到 3:1 的比例混入一些完全没有任何甲骨文字的空白图块并标注为空图让模型学会在这些纹理上输出低置信度。推理时也应该把conf调到 0.35 以上而不是默认的 0.25。5.4 训练到一半 loss 变成 NaN模型权重全部报废现象是训练前几十轮正常某一轮开始 box_loss、cls_loss 全部显示 nan之后验证 mAP 归零。原因多半是学习率过大导致梯度爆炸也有可能是数据里有全零图块或标注坐标越界。解决第一步是检查标注文件把所有超过 1.0 或为负数的坐标打印出来第二步是把lr0降到 0.002 并加上weight_decay0.0005如果还不行检查是不是用了cacheTrue后数据加载异常把 cache 关掉重试。我在实际比赛里遇到过的是 dataloader 的num_workers开太多导致共享内存崩溃表现为 loss 卡死而不是 NaN这时候降workers2就能解决。5.5 比赛源码包里的 zip 解压乱码或目录对不上训练路径直接报错现象是在 Windows 上解压 zip 后训练时找不到datasets/oracle/images/train。原因有两个一是 zip 包在 Windows 下解压因为字符编码问题出现乱码目录名尤其当包内注释是 UTF-8 而系统是 GBK 时二是项目文档里写的数据集路径是绝对路径换机器后没改 YAML。解决方式是不用 Windows 自带解压用 7-Zip 或 Python 的zipfile指定 UTF-8 解码然后打开oracle.yaml把path改成相对路径或者用$PWD拼接。顺手提醒一句源码包里如果是存在加密 zip 或者伪加密文件工具会提示密码错误这时候先把 zip 的伪加密标志去掉用 7-Zip 打开看注释里的说明别花时间去猜密码。6. 从训练结果到比赛提交滑窗推理、后处理与最终验证6.1 对超大拓片做滑窗推理并合并边界框训练完成后比赛测试集给的还是整张大拓片。直接拿模型输入整图是不可能的要回到第三章的分块逻辑但这次分块后还要把每个块上的预测坐标转换回原图坐标再把重叠区域的重复检测框合并。下面这个脚本是最常用的合并方式import numpy as np def merge_boxes_into_full_image(blocks_predictions, offsets, iou_thr0.5): blocks_predictions: list of dicts, each contains boxes (list [x1,y1,x2,y2] relative to block) and scores offsets: list of [offset_x, offset_y, block_w, block_h] for each block all_boxes [] all_scores [] for pred, off in zip(blocks_predictions, offsets): off_x, off_y, block_w, block_h off for box, score in zip(pred[boxes], pred[scores]): x1, y1, x2, y2 box # 从块内坐标平移到原图坐标 abs_box [x1 off_x, y1 off_y, x2 off_x, y2 off_y] all_boxes.append(abs_box) all_scores.append(score) boxes np.array(all_boxes, dtypenp.float32) scores np.array(all_scores, dtypenp.float32) # 简单的贪心 NMS按置信度降序合并重复框 keep [] order scores.argsort()[::-1] while order.size 0: i order[0] keep.append(i) if order.size 1: break rest order[1:] iou compute_iou(boxes[i], boxes[rest]) # 把重叠超过阈值的框去掉 order rest[iou iou_thr] return boxes[keep], scores[keep] def compute_iou(box_a, boxes_b): x1 np.maximum(box_a[0], boxes_b[:, 0]) y1 np.maximum(box_a[1], boxes_b[:, 1]) x2 np.minimum(box_a[2], boxes_b[:, 2]) y2 np.minimum(box_a[3], boxes_b[:, 3]) inter np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1) area_a (box_a[2] - box_a[0]) * (box_a[3] - box_a[1]) area_b (boxes_b[:, 2] - boxes_b[:, 0]) * (boxes_b[:, 3] - boxes_b[:, 1]) union area_a area_b - inter return inter / union这里的核心是iou_thr的设置。重叠块里的同一字形可能产生两个高置信度框阈值设到 0.5 就能滤掉重复如果设到 0.8两个框依然会同时保留导致最终提交结果里一个字形被标注两次。建议按验证集真实标注做一次评测统计切块位置导致的重复框比例再决定阈值。6.2 置信度阈值与 NMS 参数在 precision 和 recall 之间取舍比赛提交阶段往往有严格的评测规则有的看检测框是否命中有的看分割掩码重叠度。如果你的模型在验证集上 mAP50 已经达到 0.8 以上但提交分数不理想大概率是置信度阈值没调好。我通常会做一个网格搜索在验证集上跑conf从 0.1 到 0.6、iou从 0.3 到 0.6 的笛卡尔积画出 PR 曲线和 F1 曲线。对甲骨文任务我一般选择 F1 最大的点作为提交阈值因为漏检一个生僻字比多检一个背景的代价更大。如果你发现验证集上阈值 0.3 时 F1 最高但测试集上有明显的无字背景干扰那就保守地把阈值提到 0.4比赛宁可丢 recall 也不能让背景污染结果。6.3 用测试集做最终验证把掩码转成提交格式检测框合并完毕最后一步是输出比赛要求的提交格式。如果比赛要的是 JSOn 多边形或 RLE 掩码你需要把像素级掩码从块坐标平移到原图坐标然后缩放回原始分辨率。这里有一个经常踩的坑测试集图像分辨率若和训练分块分辨率不一致掩码坐标要先按照测试图的宽高比做缩放否则提交后掩码位置整体偏移。import pycocotools.mask as mask_util def mask_to_rle(mask): # mask: 二值 numpy 数组 0/1 rle mask_util.encode(np.asfortranarray(mask)) # 转成 COCO 比赛常用格式 return {counts: rle[counts].decode(ascii), size: rle[size]}验证最终结果时有一个好习惯把预测掩码画回原图上用半透明叠加方式逐张翻看。我每次都会看三样东西——漏检集中在哪类字形、掩码边界是否吃掉关键笔画、有没有把拓片边缘的墨晕当成字。这个环节看起来费时间但通常能发现训练阶段没暴露的分布差异比如测试拓片纸质更黄、裂纹更密这些信息比指标本身更能帮你决定要不要再做一轮数据增强。画完最后一版结果我会习惯性地把训练和推理的所有配置写进一个 README 文件记录数据划分方式、模型参数、阈值选择依据。比赛复盘时要回看这串配置才不会对着一个跑崩的模型发愣。这套 YOLOv8 的流程我用了好几回从自然场景切到文物图像最深的体会是网络结构不是瓶颈数据标注质量和对阈值的敏感度才是。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网