基于YOLOv8的古籍保护系统:从损伤检测到部署实战
简介基于YOLOv8的古籍保护系统是一套面向计算机相关专业学生、教师及企业员工的完整目标检测毕设项目针对古籍文献数字化保护场景设计功能完善、操作简单简单部署即可运行适用于毕设、课程设计、大作业或项目初期演示特别适合新手进阶。资源共97个文件以70个Python源码文件为核心覆盖模型训练、视频检测与可视化界面等模块另含12个pyc编译文件、4个pt模型权重、5个xml配置、2个txt说明文档及1个mp4演示视频压缩包整体24.21MB目录划分清晰从训练到推理均有对应脚本便于按需查阅。配套完整数据集、可视化页面和部署教程训练后可直接产出核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果及标签分布图为毕业设计答辩提供直观的数据支撑让评审对项目完整度与实用性信服。目前已有56人学习下载适合需要快速搭建深度学习目标检测系统或作为课程设计、毕业设计起点的开发者也便于在现有代码基础上二次修改扩展功能。1. 基于YOLOv8的古籍保护系统是什么它不是「识别文物」是给扫描件做损伤定位拿到《基于YOLOv8的古籍保护系统》这个标题第一反应容易想偏觉得是拿模型识别古籍里的文字、判断朝代、鉴别版本。实际做下来这套系统最核心的活儿只有一件——在古籍扫描图上定位破损区域。虫蛀、水渍、霉斑、撕裂、墨迹晕染这些损伤位置才是保护修复工作真正需要的输入。模型输出的是带置信度的检测框不是分类标签这意味着你拿到手的是一个典型的目标检测项目骨干网络是 YOLOv8配套有完整的标注数据、训练好的权重和一个可视化界面。对毕设或课程设计来说它的价值在于不用从零攒数据集不用自己搭前后端环境配好就能跑但你得真看懂数据怎么组织、模型怎么训、部署时哪些参数会坑你——这正是本文要拆开讲的东西。2. 给 YOLOv8 喂一张三四千万像素的古籍扫描图数据集构建的正确姿势古籍保护系统的数据环节最容易被低估。常见公开数据集都是自然图像一张图几百 KB 到几 MB而古籍扫描件通常 300 DPI 起步单页可能到 4000×6000 甚至更高。拿这样的图直接喂 YOLOv8显存直接爆掉小目标也根本学不到。所以第一步不是标注而是想清楚检测什么、怎么切块、标注粒度怎么定。2.1 检测目标设计与标注类别划分先定「损」还是先定「类」做古籍保护检测最常见的类别划分是破损裂口、缺损、虫蛀孔洞、水渍/霉斑、文字区域用于版面分析。我第一次做的时候把所有损伤统一定成 damage 一类省事但答辩被问住——「你的系统能区分虫蛀和水渍吗」答不上来。后来拆开标又发现另一个问题虫蛀样本极少几百张图里可能只有二三十张有虫蛀模型对这类别几乎学不动。我的建议是看数据量定类别总量低于 500 张标注图合并成 damage 一类把精力放在框的准确度上超过 1500 张再拆细类。毕设场景下合并类别 在论文里写清楚「细分类需扩充样本」是性价比最高的路径。另外标注时要把钤印印章、装订线、书口这些常见背景也标出来哪怕最后训练时不用这些类别也可以在难负样本挖掘时用它们来打压误检。2.2 用 LabelMe 标注古籍图像区域勾选与导出格式LabelMe 是这个方向最常见的标注工具不是因为功能强而是因为它导出的是 JSON里面保留的是多边形点集而不是直接给你一个矩形框。这在古籍场景很重要破损区域不规则直接用外接矩形会把大量完好的纸张背景包进去模型学到的是「这块区域颜色深」而不是「这块区域是损伤」。LabelMe 打开扫描图后用 Create Polygons 沿损伤边缘打点一个损伤区域至少 6~10 个点保存后得到和图片同名的 JSON 文件。{ version: 5.2.1, imagePath: page_0123.jpg, imageWidth: 4096, imageHeight: 6144, shapes: [ { label: damage, points: [[1240.5, 3012.3], [1288.1, 3056.9], [1377.4, 3101.2]], shape_type: polygon, group_id: null } ] }这段 JSON 是 LabelMe 导出的标准结构三个字段必须搞清楚imageWidth/imageHeight是原始图的宽高后面转 YOLO 格式时要拿它们做分母points是标注点的像素坐标直接来自你在画布上点的位置label是类别名默认叫 damage。注意imagePath只存文件名不带路径所以整理数据集时最好把图片和 JSON 放同一目录否则后续批量转换脚本找文件会莫名失败。2.3 把 LabelMe 标注转成 YOLO 格式归一化坐标的四个边界坑YOLOv8 训练时读的是 txt 标注每行格式是class x_center y_center width height全部是相对图片宽高的归一化值。所以必须写一段转换脚本把 JSON 里的多边形点集转成外接矩形再除以图片实际宽高。这步看着简单踩坑的人不在少数我列四个最常见的翻车点。import json import os def labelme_to_yolo(json_path, out_dir): with open(json_path, r, encodingutf-8) as f: data json.load(f) # 坑 1必须用 JSON 里的原始尺寸不能重新读一遍图片 img_w data[imageWidth] img_h data[imageHeight] lines [] for shape in data[shapes]: cls shape[label] pts shape[points] xs [p[0] for p in pts] ys [p[1] for p in pts] # 坑 2多边形转外接矩形直接用 min/max x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) # 坑 3外接矩形宽度可能为零竖线要做保护 if x_max - x_min 2 or y_max - y_min 2: continue # 转 YOLO 格式中心点 宽高全部归一化 x_center (x_min x_max) / 2 / img_w y_center (y_min y_max) / 2 / img_h w (x_max - x_min) / img_w h (y_max - y_min) / img_h # 坑 4归一化结果超过 1 或小于 0说明标注越界需要裁剪 x_center min(max(x_center, 0), 1) y_center min(max(y_center, 0), 1) w min(w, 1) h min(h, 1) lines.append(f{0} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_path os.path.join(out_dir, os.path.basename(json_path).replace(.json, .txt)) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines))这段脚本里 class 直接写死成了 0如果你的数据集有多类别需要维护一个label - index的映射字典类别名顺序和你后面data.yaml里names列表的顺序必须完全一致。另一个常见问题是 LabelMe 在标注超大图时会自动做缩放预览如果你从界面上抄坐标而不是从 JSON 里读坐标分母就是错的——这就是为什么上面代码强制从imageWidth/imageHeight取数不要自己拿cv2.imread去读图。2.4 切块策略古籍大图训练集生成与正负样本平衡转换完标注后面临的问题是图片太大YOLOv8 训练时imgsz一般取 640直接把整页缩到 640一个虫蛀孔洞可能就剩几个像素模型看都看不清。常见做法是把大图切成若干tile瓦片每个 tile 作为独立训练样本。我在项目里一般切 640×640overlap 设 20%这样单页 4096×6144 的扫描图可以产生约 50 个训练块。切块训练最容易被忽略的是正负样本比例。全图切块后大量 tile 落在纯背景区域——古籍纸张底色均匀没有任何损伤这些是负样本。如果全量塞给模型模型很快学会「没纹理就输出背景」导致检测时纸张边缘、折痕被误判成破损。我一般会把纯背景块过滤掉只保留包含标注框的块再额外留 10%~15% 的纯背景块作为负样本。这样模型既见过「没有目标的长什么样」又不会被负样本淹没。3. 训练 YOLOv8 古籍检测模型从 checkpoint 选择到损失函数曲线数据准备好进入训练环节。这个阶段最大的问题是参数怎么设。网上能搜到一堆 yolov8 训练自己的数据集的教程但人家拿的是自然图像、通用目标参数直接抄过来在古籍数据上不一定好使。这一章讲清楚预训练权重怎么选、训练参数为什么这样调、损失曲线到底看什么。3.1 用 COCO 预训练权重起步yolov8s 是最稳的起点YOLOv8 官方提供了 n / s / m / l / x 五档预训练权重底层backbone是在 COCO 上训好的。古籍页面和 COCO 的自然图像域差异非常大但迁移学习仍然有效——backbone 学到的边缘、纹理、局部对比度特征是通用的。选哪个型号核心看数据量。古籍保护系统这种项目完整数据集通常几百到一千多张标注图n 档太轻容易欠拟合l / x 档纯属浪费算力还容易过拟合。我一般先用 yolov8s.pt 跑通全流程验证数据管线和 loss 下降正常后再换 yolov8m 冲一轮精度。注意m 档的权重文件叫yolov8m.ptultralytics 库会自动从官方地址下载但国内网络环境下经常下到一半失败离线部署时提前准备好权重文件是常规操作。3.2 训练参数怎么设batch、imgsz、lr 与古籍页面的适配逻辑训练命令本身不复杂复杂的是参数背后的取舍。下面是配好data.yaml后的完整训练命令我用的是 ultralytics 库的标准写法。# data.yaml path: /path/to/guji_dataset train: images/train val: images/val nc: 1 names: [damage]yolo detect train \ modelyolov8s.pt \ datadata.yaml \ epochs150 \ imgsz640 \ batch8 \ lr00.005 \ optimizerAdamW \ seed42 \ workers2 \ device0imgsz640是最常用的输入尺寸。如果你的古籍扫描图里损伤目标较小可以试着提到 896 或 1024但显存压力会指数上升。lr0我从默认的 0.01 调到 0.005原因是古籍数据量小学习率太大容易震荡val loss 会在某个 epoch 突然飙上去拉不回来。batch的大小完全看显卡6GB 显存跑 640 分辨率 batch 取 8 基本是上限了想上 1024 分辨率就得降到 batch 2。optimizer我倾向直接用 AdamWSGD 在小数据集上收敛慢调整起来更费时间。下面是结合显存选参数的参考表这是个经验值不是硬性规定。显卡显存imgszbatch可跑模型备注6GBGTX 1660 Ti 级别6408yolov8s最稳的组合6GB8962yolov8s可跑但慢显存接近极限12GBRTX 3060 / 407064016yolov8m推荐组合12GB10244yolov8s小目标场景适用workers这个参数在 Windows 上特别容易出问题ultralytics 在 Windows 下多进程数据加载偶尔会卡死甚至闪退我在这上面翻过车后面避坑章节会详细说。另一个容易忽略的是seed42固定随机种子才能保证实验结果可复现——毕设答辩时导师问「你这个结果能不能复现」固定种子是最基本的交代。3.3 损失函数曲线怎么读train 和 val 的三种典型形态训练过程中ultralytics 会每隔一定 epoch 在runs/detect/train目录下生成results.png里面画了train/box_loss、train/cls_loss、train/dfl_loss、val/box_loss、val/cls_loss、val/dfl_loss以及metrics/precision、metrics/recall、metrics/mAP50、metrics/mAP50-95的曲线。很多人训练完直接看 mAP这是把结果当黑匣子的做法曲线形态才是判断训练是否正常的核心依据。三种典型形态要记住。第一种train loss 和 val loss 同步下降后期趋于平缓这是健康形态说明模型在正常学习。第二种train loss 持续下降但 val loss 在某个 epoch 后开始回升这是过拟合的特征处理办法是加数据增强、加大weight_decay或直接减小 epochs。第三种两个 loss 从一开始就震荡不停val loss 忽高忽低说明学习率太大或者数据管线有问题——先回看训练集图片和标注框是否正确显示。古籍数据量小过拟合是常态我一般练到 150 epoch 就会盯着 val loss 看一旦开始回升就在那个 epoch 附近取验证集上最好的权重不非得跑满。3.4 中断续训与权重导出别让训练进度白费训练跑一半断电、显存 OOM 导致进程被杀这是常态。ultralytics 会在训练目录下自动保存last.pt续训命令极简单yolo detect train modelruns/detect/train/weights/last.pt datadata.yaml epochs150。这个操作背后其实没丢多少进度优化器状态、学习率调度器lr_scheduler当前值都序列化在last.pt里了不是只存权重。训练结束目录里还会有best.pt这是 val 集上 mAP 最高的权重部署和推理都用它。我拿到项目的习惯是进去第一件事看weights文件夹里有没有best.pt没有的话说明作者没把训练产物放全得自己训或者用last.pt凑合。4. 把模型装进可视化界面PyQt5 桌面端与 Flask 网页端的两种选择标题里「可视化界面」是毕设答辩的关键加分项模型再好没有界面演示效果是大打折扣的。这一章给两条可行的路线PyQt5 桌面应用和 Flask 网页应用外加古籍检测特有的大图切块推理实现。4.1 推理脚本先跑通加载 best.pt 并对单张古籍图输出检测框不管最后用什么界面第一步永远是先写一个不带界面的推理脚本确认权重文件本身是好的。很多项目包里的权重是从别处拷来的best.pt文件存在但训练配置对不上直接加载会报错。from ultralytics import YOLO model YOLO(best.pt) results model.predict( sourcetest_page.jpg, conf0.25, iou0.45, imgsz640, devicecuda, # 没有 GPU 则改 cpu saveTrue, ) for r in results: boxes r.boxes print(f检测到 {len(boxes)} 个目标) for box in boxes: x1, y1, x2, y2 box.xyxy[0].tolist() conf float(box.conf[0]) cls int(box.cls[0]) print(f类别 {cls}, 置信度 {conf:.3f}, 框坐标 ({x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f}))这里conf0.25是置信度阈值低于这个值的框会被丢掉iou0.45是 NMS 的 IoU 阈值重叠度超过它算同一个目标。古籍检测场景下虫蛀这类小目标框往往互相靠近iou可以往 0.5 调来减少误删。如果发现输出框数量明显比标注多很多优先怀疑conf太低而不是模型坏了。4.2 界面选型PyQt5 适合答辩现场Flask 适合远程展示界面这块没有唯一的正确答案取决于你演示的场景。我把两种方案的区别列一下按自己的条件选。对比维度PyQt5 桌面端Flask 网页端演示场景本地开程序适合现场答辩手机 / 另一台电脑远程访问开发成本中等需要处理 Qt 布局较低写个 HTML 模板就行交互体验响应快拖拽文件即测需要上传有网络延迟依赖复杂度需要 PyQt5 库需要 Flask 模板渲染桌面端我用 PyQt5 时一般直接做一个最小可用布局左边按钮选图片中间 QLabel 显示检测结果图右边文本框输出检测数量和每个框的置信度。核心逻辑就是上一步推理脚本包一层 Qt 信号槽检测耗时操作放到线程里避免界面卡死。Flask 方案更轻把模型加载放在启动时路由里接收上传图片、调用 model.predict、把r.boxes的信息拼进返回结果前端用原生canvas画框。如果项目包里已经有现成界面先跑通默认端口看表现再决定要不要换。4.3 切块检测与坐标回投显示层必须处理的古籍大图问题界面接上模型后新问题立刻出现古籍扫描图 4000×6000 像素模型直接跑imgsz640推理时内部会把整图缩到 640小目标基本检测不到。解决思路和训练时一样——切成多个 tile 分别检测再把框坐标回投到原图重叠区域的重复框做合并。这步是古籍保护系统区别于通用检测项目的核心工程点。import cv2 import numpy as np from ultralytics import YOLO model YOLO(best.pt) def detect_large_image(img_path, tile_size640, overlap0.2, conf0.25, iou0.45): img cv2.imread(img_path) H, W img.shape[:2] all_boxes [] step int(tile_size * (1 - overlap)) for y in range(0, H, step): for x in range(0, W, step): # 最后一行/列可能不足 tile_size裁剪到边界 y2 min(y tile_size, H) x2 min(x tile_size, W) tile img[y:y2, x:x2] results model.predict(tile, confconf, iouiou, verboseFalse) for r in results: for box in r.boxes: x1, y1, x2b, y2b box.xyxy[0].tolist() # 坐标回投加上 tile 在原图中的偏移 all_boxes.append([ x x1, y y1, x x2b, y y2b, float(box.conf[0]), int(box.cls[0]) ]) # 合并重叠框直接用 NMS 处理回投后的全部框 if all_boxes: boxes_np np.array(all_boxes, dtypeobject) keep cv2.dnn.NMSBoxes( [[b[0], b[1], b[2] - b[0], b[3] - b[1]] for b in all_boxes], [b[4] for b in all_boxes], score_thresholdconf, nms_thresholdiou ) final_boxes [all_boxes[i] for i in keep.flatten()] else: final_boxes [] return final_boxes这个函数的三个参数是古籍检测的精华所在。tile_size640要和训练时的imgsz保持一致模型只在 640 分辨率下见过目标overlap0.2是相邻 tile 的重叠率这个值不能设成 0否则一个目标被切在边界上时就只能看到一半置信度暴跌导致漏检conf和iou要和训练时推理保持一致不然后面对比结果没有说服力。回投思路很简单tile 在整图中的偏移量是(x, y)tile 内部的框坐标加上这个偏移就是原图坐标。最后用cv2.dnn.NMSBoxes做合并非常关键因为重叠区域的目标会被重复检测出两次甚至三次。4.4 界面里几个答辩加分项批量检测与结果导出模型能跑、界面能显示离「完整系统」还差一步——批量处理和结果导出。古籍保护的实际场景是一次处理一整册扫描件不是一张一张手工测。在界面上加一个「批量检测」按钮遍历文件夹下所有图片对每张调用detect_large_image把检测框叠加到原图后另存到输出目录。再加一个「导出 CSV」按钮把每张图的文件名、检测框数量、每个框的坐标和置信度写进表格。这两项功能代码量不大但答辩时展示「系统能自动处理整册古籍并生成报告」会让项目完整度上一个台阶。5. 古籍保护系统部署避坑环境、显存、漏检与非极大值抑制的那些事这个方向从环境搭建到推理部署坑密集度是我见过的 CV 项目里数一数二的。下面几条都是踩过之后才总结出来的血泪经验按「现象 → 原因 → 解决」写清楚希望你能绕开。5.1 Windows 下训练卡死或进度条不动现象在 Windows 上跑yolo detect train开训几分钟后进度条停止不动CPU 占用却拉满最后只能强杀进程。原因ultralytics 默认workers2Windows 下多进程数据加载器使用 spawn 模式启动子进程和训练循环存在兼容问题导致数据加载线程死锁。解决训练命令加workers0或workers1如果项目包里的训练脚本写死了 workers 值直接改参数重跑。这个坑在 ubuntu20.04 这类 Linux 环境下不存在所以网上教程里很少提 Windows 上的这个问题只有自己踩过才知道。5.2 显存 OOMGTX 1660 Ti 级别显卡跑不起来现象6GB 显存显卡跑imgsz1024、batch8训练到第一个 epoch 直接CUDA out of memory。原因显存占用和imgsz成平方关系1024 分辨率下特征图占用是 640 的约 2.56 倍batch 翻倍又翻一倍6GB 根本扛不住。解决优先降 batch 到 2其次降imgsz到 640。另外把workers0也能省掉一点数据加载端的显存缓存。如果两个都要我建议保imgsz降 batch因为古籍检测小目标靠分辨率撑着批次大小影响的是训练稳定性对小数据集来说 batch 2 和 batch 8 的最终精度差距没那么大。5.3 转换好的标注全部偏移或归一化值超界现象labelme2yolo转换脚本跑完训练时用showTrue查看标注框发现所有框都堆在图片左上角或者中心点坐标大于 1。原因转换脚本里用cv2.imread()重新读了图片尺寸但 LabelMe 在标注超大图时会在界面里预览缩放JSON 里记录的坐标是对原图的坐标而cv2.imread()读出的尺寸可能因为图片损坏或通道数不同而不一致更常见的是直接抄了缩放后预览图的宽高当分母。解决代码里强制从 JSON 的imageWidth/imageHeight字段取分母不要自己猜。还有一个隐蔽版本同一批数据有的 JSON 里没写imageWidth字段脚本读出来 KeyError转的时候加一个data.get(imageWidth, None)保护缺失时再回退用cv2.imread。5.4 装订线和印章被检测成破损难负样本没给够现象模型在文字密集区域表现尚可但对装订线、钤印红色印章、书口折痕大量误检一个扫描页能出十几个假框。原因训练数据里没有包含这些「长得像损伤但实际不是」的背景样本模型只能根据局部纹理深浅做判断红色印章和深色霉斑在特征空间里确实很接近。解决收集一批只包含装订线、印章、折痕、墨迹的负样本图片不需要标注框放进训练集的 images 目录但 txt 标注文件留空。YOLOv8 会把这些当作 background 类别参与训练。这个操作在 ultralytics 里是原生支持的空 txt 不会报错。5.5 大图切块检测时重复框和漏检并存现象用detect_large_image跑整页古籍发现同一损伤区域被检出 3~4 个高置信度框位置几乎重叠而旁边一个更明显的损伤却什么都没有。原因overlap导致目标在相邻 tile 中被多次检测NMS 只做了框内合并漏检多半是目标横跨 tile 边界时被切碎每块只看到一半置信度达不到conf阈值。解决合并阶段用回投后 NMS这个代码已经在 4.3 节给出漏检问题把overlap从 0.1 提高到 0.2~0.3让目标至少有一部分完整落在一个 tile 内。overlap 提高的代价是计算量增加0.3 是性价比临界点。6. 用 PR 曲线和热力图给古籍检测模型「验明正身」答辩时最可靠的两张图项目做到能跑、界面能点还不算完。答辩时导师一定会问「你的模型到底准不准误检和漏检长什么样为什么这里会误检」只甩一个 mAP 数字是不够看的那只是把模型当黑匣子的表现这一章给两条最实用的验证路径。训练结束后runs/detect/val目录里会自动生成PR_curve.png和confusion_matrix.png。PR 曲线看的是不同置信度阈值下 Precision 和 Recall 的平衡曲线曲线下面积越大越好。古籍破损检测有个特殊性我一般更盯着曲线靠近右上角的部分也就是高 Recall 区间还保持着不算差的 Precision。原因很简单——修复工作里漏掉一个损伤区域的代价远高于多标一个假框所以模型应该往「宁可多检、不可漏检」的方向去调阈值这也意味着部署时conf不要设太高。confusion_matrix.png看的是哪些目标被误分到了哪个类别如果 background 行里有明显亮的格子说明上面 5.4 节的难负样本问题还没解决干净。如果想更进一步用热力图解释模型为什么在某个区域给出高置信度。YOLOv8 可以直接挂pytorch-grad-cam库里的 EigenCAM不需要改模型结构。from pytorch_grad_cam import EigenCAM from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget model YOLO(best.pt).model target_layers [model.model[-2]] cam EigenCAM(modelmodel, target_layerstarget_layers) cam_out cam(input_tensorimg_tensor.unsqueeze(0))[0, :]这里的model.model[-2]拿到的是 YOLOv8 检测头的倒数第二层卷积输出EigenCAM 会用它生成热力图叠加到原图上。红色区域就是模型「注意力集中」的地方如果热力图里的高亮区正好落在损伤位置上这就是最直观的辅助验证。要注意的是对 YOLO 这类多尺度检测头热力图的空间分辨率有限只能当辅助证据不能单靠它下结论。这个方向如果还想往前做常见的进阶路线是拆细类别把 damage 拆成 worm、stain、tear、或者从检测框升级到 YOLOv8-seg 分割出损伤的精确轮廓——修复工作真正需要的是边界形状不是外接框。我个人的习惯是每次训练完先看 PR 曲线和混淆矩阵这两张图再决定要不要调阈值重跑而不是盯着 mAP 数字看半天。这套流程适用于所有检测项目古籍保护只是其中一个场景。希望帮到你。本文还有配套的精品资源点击获取