
简介基于YOLOv8的古籍保护系统是一套面向毕业设计与课程设计的完整项目资源覆盖目标检测、深度学习与人工智能典型应用场景适合计算机、人工智能、通信工程、自动化、电子信息等专业的在校学生、教师或企业员工学习也适合作为课程设计、大作业及项目初期立项演示。项目集成了可直接运行的Python源码、完整数据集、可视化交互界面和部署教程代码经过测试全部通过能够输出核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果以及标签分布图用可视化的方式直观呈现模型性能充分满足毕设答辩的成果展示要求。资源压缩包共97个文件以70个Python脚本为主另有4份预训练模型权重、XML配置、说明文档、界面图标和一段真实检测演示视频整体约24.21MB轻量便捷、下载后即可按说明文档开始部署。目前已有56人学习下载除直接运行外还可在现有代码基础上更换数据集、调节参数快速迁移到其他目标检测任务兼顾学习与二次开发。1. 一套能看清古籍损伤的YOLOv8检测系统到底在帮你看什么做古籍数字化和修复的人最头疼的不是翻页扫描而是把书页上那些虫蛀孔洞、水渍痕迹、破损撕裂、墨迹粘连的位置一张张标出来。传统做法靠人眼逐页看看一页两页还行碰上几百页的善本眼睛会花标准还会漂。用YOLOv8训练一个古籍保护检测模型本质上是把这套人工巡检变成框选任务模型在扫描图上直接输出损伤区域的类别和坐标检查人员只要核对框准不准、有没有漏效率能差出好几倍。这个方向特别适合拿来做毕设或课程设计——它有一个非常明确的视觉任务、一个不冷门的模型、一套可以落地的界面和数据集做成系统后演示效果直观写论文也好找创新点。下面这套方案就按我实际跑通这类项目的思路来拆。2. 为什么选YOLOv8做古籍损伤检测从C2f到Anchor-Free的取舍2.1 古籍场景下YOLOv8的检测目标虫蛀、水渍、破损、墨迹粘连古籍书页的损伤可以归纳成几大类检测目标不复杂但每一个都有各自的形状规律。虫蛀孔洞通常是小而圆的暗色区域直径可能只有十几个像素属于典型的小目标水渍是边界模糊的大片色块颜色偏黄褐色形状不规则破损撕裂是纸张边缘的缺口往往和书页边缘连在一起框的位置会贴着图像边界墨迹粘连则是文字笔画糊成一团区域形状细长。做数据集标注之前先把类别定死我一般用四个类wormhole、waterstain、tear、inkblot。类定多了模型学不过来定少了修复部门又没法用。这四个类对YOLOv8提出的要求不太一样。虫蛀孔洞需要模型对高频纹理敏感水渍需要模型吃透大感受野下的颜色分布破损撕裂则考验模型对边缘上下文的理解。好在YOLOv8的骨干网络C2f模块在不同尺度上都保留了足够的梯度流浅层特征图能守住小目标深层特征图能覆盖水渍这种large区域一个模型同时处理四类问题在实践里是够用的。2.2 标签设计与标注规范处理数据集用于YOLOv8训练的基本盘古籍书页的标注规范和自然场景有区别。自然场景里人、车这类目标有清晰边界标注员很容易对齐古籍损伤区域边界模糊尤其是水渍边缘是渐变的不同人画出来的框可能差很多。我的做法是定一条硬规则水渍只框最深色的核心区不框整个扩散区扩散区连标注员自己都说不准模型学出来也是噪声。虫蛀孔洞必须一个洞一个框不能把连成串的几个小洞合并成一个框否则等于教模型把多个目标当成一个。标注工具我习惯用LabelImg导出Pascal VOC格式的XML再转成YOLO格式的txt。如果你手上已经有Labelme标注的JSON也可以写脚本转转换逻辑是一样的读多边形顶点算外接矩形的中心点坐标、宽和高再除以图片宽高做归一化。下面这段脚本是我常用的转换方式兼容LabelImg的XML和Labelme的JSON两种来源。import os import xml.etree.ElementTree as ET import json from glob import glob def voc_xml_to_yolo(xml_path, out_txt_path, class_names): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): cls obj.find(name).text if cls not in class_names: continue cls_id class_names.index(cls) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 归一化到0~1区间 x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines)) def labelme_json_to_yolo(json_path, out_txt_path, class_names): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] lines [] for shape in data[shapes]: cls shape[label] if cls not in class_names: continue cls_id class_names.index(cls) points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] xmin, xmax min(xs), max(xs) ymin, ymax min(ys), max(ys) # Labelme是多边形取外接矩形并做边界裁剪 x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines)) class_names [wormhole, waterstain, tear, inkblot] # VOC格式目录示例 for xml_file in glob(labels_xml/*.xml): txt_path xml_file.replace(labels_xml, labels_txt).replace(.xml, .txt) voc_xml_to_yolo(xml_file, txt_path, class_names)这段脚本里有几个参数值得注意。归一化坐标是YOLO系列的硬性要求网络输出层预测的是相对网格的偏移量训练时会把标注框映射到特征图的网格上如果直接用像素坐标会让损失函数在数值量级上失衡。类别索引必须和data.yaml里的顺序一致顺序错了训练不会报错但mAP永远上不去这是新手最容易踩的无声坑。另外Labelme转出来的外接矩形可能略微超出图像边界YOLO训练时虽然能容忍少量越界但最好用numpy的clip函数把坐标限制在0到1之间省得后面评估时出现负数坐标影响可视化。2.3 训练参数含义epochs、imgsz、batch这些值怎么定YOLOv8训练参数是项目里最容易被照抄出错的部分。epochs不是越大越好古籍损伤类别少、背景相对单一通常100到150个epoch就能收敛硬跑到300个epoch只会把时间耗在抖动上。imgsz默认是640但古籍扫描件通常是300dpi以上的大图直接用640训练会丢失虫蛀孔洞这种小目标信息。我的做法是先统计标注框的像素面积分布如果大量框的宽高在20像素以下就把imgsz提到960或1280代价是显存占用上升GTX 1660Ti这类6GB显存的卡只能跑batch 4到6。batch大小直接影响BN层的统计量。YOLOv8的BatchNorm依赖batch内样本的均值和方差batch太小统计量不稳loss曲线会像锯齿一样抖。显存不够时的常见做法是用梯度累积ultralytics框架里没有直接暴露accumulate参数可以用训练回调自己实现或者干脆换yolov8n这种轻量模型换显存空间。学习率一般不用动默认的SGD配合cosine衰减在这类小数据集上表现稳定手动调lr反而容易让损失发散。3. 从零跑通训练链路数据集划分、data.yaml与损失曲线验证3.1 数据集目录结构与训练入口YOLOv8训练要求数据按固定目录结构组织这直接决定了后面能不能一键跑起来。我见过太多人把图片和标签堆在一个文件夹里训练时报no labels found然后开始怀疑框架有问题。标准结构是images下分train和val两个子目录labels下同样分train和val每个子目录里的文件名必须一一对应后缀不同没关系但主名必须完全相同。dataset/ ├── images/ │ ├── train/ │ │ ├── page_001.jpg │ │ └── page_002.jpg │ └── val/ │ └── page_050.jpg ├── labels/ │ ├── train/ │ │ ├── page_001.txt │ │ └── page_002.txt │ └── val/ │ └── page_050.txt └── data.yamldata.yaml是整个训练入口的配置文件内容是路径和类别映射。ultralytics支持相对路径和绝对路径我建议写绝对路径因为换机器跑的时候相对路径经常因为当前工作目录不同而失效。下面是我最常用的data.yaml模板# data.yaml path: /home/user/dataset # 数据集根目录绝对路径最稳 train: images/train # 训练集图片相对path的路径 val: images/val # 验证集图片相对path的路径 names: 0: wormhole 1: waterstain 2: tear 3: inkblot划分数据集时有个容易被忽略的点同一本书的扫描页只能分到同一侧。古籍书页之间背景和纸张纹理高度相似如果把同一本书的页面既分进train又分进val验证集会虚高因为模型已经见过同源的纹理分布了。我一般按书册划分一册书的所有页面要么全部进训练集要么全部进验证集。3.2 最小训练命令与损失曲线观察环境配置是第一个门槛。ultralytics的安装很简单CPU版本也能跑只是速度慢。Ubuntu 20.04上搭CPU环境的话先确认Python版本在3.8到3.11之间然后直接pip安装不需要额外装CUDA相关的东西# 创建虚拟环境避免污染系统Python python3 -m venv yolo_env source yolo_env/bin/activate pip install ultralytics安装完成后训练命令一句话就能启动。如果你是第一次跑我建议先用yolov8n这版最小的模型把整个链路走通确认数据没问题后再换yolov8s或yolov8m提升精度。# 用YOLOv8n做数据验证跑20个epoch确认流程通畅 yolo detect train datadataset/data.yaml modelyolov8n.pt epochs20 imgsz640 batch8 device0这段命令里device0表示用第一张显卡CPU机器改成devicecpu。跑通后去runs/detect/train目录看results.png这张图里有box_loss、cls_loss、dfl_loss三条训练曲线的收敛趋势还有验证集上的precision和recall曲线。正常的收敛状态是训练末段loss曲线进入平台期不再有明显下降如果验证集loss在某个epoch后开始反弹训练loss还在降说明模型开始过拟合了。据我的经验这类小数据集在epoch 60到80之间最容易出现过拟合拐点需要提前用早停回调把best.pt定在验证loss最低的位置。3.3 画损失函数曲线图用图表证明模型训练到位ultralytics训练过程中会实时把每个epoch的指标写进runs/detect/train/results.csv做毕设的同学拿这个表直接画图就可以放到论文里。但很多人不知道这个文件还能复用于分析训练异常。我一般会自己写一段画图脚本把box_loss单独拎出来看细节import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/train/results.csv) # 取训练和验证的box_loss列 train_loss df[train/box_loss] val_loss df[val/box_loss] epochs df[epoch] plt.figure(figsize(8, 5)) plt.plot(epochs, train_loss, labeltrain box_loss, linewidth1.5) plt.plot(epochs, val_loss, labelval box_loss, linewidth1.5) plt.xlabel(epoch) plt.ylabel(box_loss) plt.legend() plt.grid(alpha0.3) plt.savefig(loss_curve.png, dpi200)画完图主要看三个信号。第一train和val两条曲线如果从头到尾贴合得过于紧密说明数据集划分可能有问题模型从验证集里提前学到了信息。第二val曲线在训练中段大幅震荡常见原因是batch太小导致BN层统计波动把batch调大一倍再看。第三val曲线下降后再次上升就是典型的过拟合此时应该把epochs回调到拐点附近而不是加大模型。古籍扫描件的噪声比较多我通常还会把训练集做轻度的马赛克增强也就是ultralytics默认开启的mosaic它能强迫模型学到更多背景上下文。4. 可视化界面与部署把模型变成能给人演示的系统4.1 界面选型PySide6还是Streamlit古籍保护系统要拿给人看界面不能只是命令行输出几个框。做毕设最常见的两个选择是PySide6桌面程序和Streamlit网页应用各有各的适用场景。如果答辩现场需要离线演示、需要打开本地摄像头实时检测PySide6更稳打包成exe后不依赖浏览器环境如果导师和评委更习惯在线操作而且你还想展示不同古籍书页的检测结果对比Streamlit开发速度快得多代码量只有PySide6的三分之一。我自己的选择逻辑是看数据集推理结果要展示什么。如果你的系统只做单张图片检测和批量文件夹检测Streamlit足够它内置的文件上传组件和图片对比展示组件省掉大量前端布局时间。如果你还要做视频流实时检测那必须上PySide6配合OpenCV读摄像头帧在QGraphicsView上叠画检测框交互体验才跟得上。下面这个示例是PySide6的最小骨架演示了核心的加载模型和画框流程你可以基于它扩展文件选择按钮和结果统计面板。4.2 推理封装与界面代码骨架界面代码的核心是把YOLOv8的推理结果从ndarray转换成能在Qt控件上绘制的格式。ultralytics的Results对象里封装了boxes、masks等属性boxes.data是[N,6]的张量列分别是x1、y1、x2、y2、confidence、class_id。把这个张量转成Python列表后用QPainter在QPixmap上画矩形框和标签文字同时注意图片在Qt里显示要转成QImage。import sys import cv2 import numpy as np from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QFileDialog from PySide6.QtGui import QPixmap, QImage, QPainter, QPen, QFont from PySide6.QtCore import Qt from ultralytics import YOLO class DetectWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(古籍损伤检测) self.label QLabel(self) self.setCentralWidget(self.label) # 加载训练好的权重 self.model YOLO(runs/detect/train/weights/best.pt) def open_and_detect(self): path, _ QFileDialog.getOpenFileName(self, 选择古籍图片, , Images (*.jpg *.png)) if not path: return # 推理时用conf和iou控制输出框质量 results self.model.predict(path, conf0.3, iou0.45, imgsz960) img cv2.imread(path) # yolov8输出的是RGB顺序qt显示需要BGR这里转回 img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 遍历结果画框 for r in results: boxes r.boxes if boxes is None: continue for box in boxes: x1, y1, x2, y2 box.xyxy[0].tolist() conf float(box.conf[0]) cls_id int(box.cls[0]) name self.model.names[cls_id] cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (255, 0, 0), 2) cv2.putText(img, f{name} {conf:.2f}, (int(x1), int(y1) - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2) height, width, channel img.shape qimg QImage(img.data, width, height, channel * width, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimg).scaled( self.label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) if __name__ __main__: app QApplication(sys.argv) win DetectWindow() win.show() sys.exit(app.exec())界面代码里有两个参数直接影响演示效果。conf阈值默认是0.25古籍损伤场景噪声多、目标对比度低0.25会输出大量低置信度假框尤其水渍这类边界模糊的目标我通常会调到0.3到0.4之间。iou阈值影响重叠框的合并古籍书页上虫蛀孔洞密集分布时相邻两个洞的检测框容易重叠iou设0.45比较合适设太高会把两个相邻目标合并成一个设太低又会把同一个目标重复画两个框。4.3 模型导出ONNX与CPU部署训练完的best.pt是PyTorch格式演示环境如果只有CPU直接跑Pt文件也能用但速度一般且依赖PyTorch完整环境。更轻量的做法是导出ONNX格式用onnxruntime推理部署包体积能小几百兆。# 导出ONNX固定输入尺寸为960提升小目标检测精度 yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz960 opset12导出后默认生成best.onnx同一个目录下。用onnxruntime推理时输出是一个[1, 4, 8400]的张量这里的8400是YOLOv8在不同尺度特征图上预测框的总数。解析这个输出有点绕建议直接用ultralytics的YOLO类加载onnx文件from ultralytics import YOLO # 加载onnx格式模型 model YOLO(runs/detect/train/weights/best.onnx) # 推理接口和pt格式完全一致 results model.predict(test_page.jpg, conf0.3, iou0.45) boxes results[0].boxes print(boxes.xyxy, boxes.conf, boxes.cls)如果你要部署到RK3588这类边缘盒子ONNX还要再转成RKNN格式转换时要注意量化校准用的图片要从验证集里选不能从训练集选否则量化后的精度损失会被低估。这部分属于进阶部署毕设阶段一般不用碰。5. 古籍检测避坑五条从标注到部署的血泪经验5.1 现象小目标漏检严重虫蛀孔洞几乎全部没框出来训练完拿到验证集上一跑发现大的水渍和破损都检测出来了但虫蛀孔洞的召回率只有10%出头这是古籍检测里最常见的翻车。原因有两个一是imgsz设了640虫蛀孔洞在原图里只有10到20像素缩放到640后只剩几个像素特征图上一个网格都占不满二是数据集里小目标占比本来就少模型把注意力全放在了大目标上。解决方法是把imgsz提到960或1280同时调整ultralytics的anchor设置但YOLOv8本身是anchor-free设计调整anchor意义不大更有效的是在数据层面做处理。我试过最管用的办法是裁剪放大把原图按512x512滑窗切块每块独立训练和推理小目标在切块后的图片里占比翻了几倍这个方案比单纯调参效果明显得多。代价是推理时间成倍增加需要批量检测时可以把滑窗步长设大一点做折中。5.2 现象数据检查时图片数量正常训练时却报labels not found训练第一条命令就报错大概率是数据集目录结构没对齐。常见错误是images/train里图片后缀是.jpglabels/train里文件后缀是.txt但文件名主名不匹配或者图片是.jpg而标签是.jpegultralytics按主名查找标签文件时会漏。检查方法很简单# 统计train目录下图片主名和标签主名的差异 ls images/train | sed s/\.[^.]*$// | sort img_names.txt ls labels/train | sed s/\.[^.]*$// | sort lab_names.txt diff img_names.txt lab_names.txt如果diff输出为空说明文件名匹配正常问题出在data.yaml里的path或train路径配置。再有一种隐蔽情况是图片文件本身损坏OpenCV读不出来ultralytics会跳过并警告但如果坏图占比超过一定比例训练数据量会大幅缩水模型精度上不去你还没察觉。5.3 现象训练loss降到很低但验证集的mAP50就是不涨这是典型的过拟合到训练集噪声上的表现。古籍扫描件有大量背景纹路、纸张纤维纹理模型学到的是哪个区域的纹理像标注区而不是损伤区域的语义特征。解决方向有两个数据增强和调低模型容量。ultralytics默认开启mosaic和hsv变换但古籍损伤检测不需要颜色抖动纸张颜色变化应该由模型适应而不是通过增强抹平。我会在超参数里关闭hsv_h、hsv_s、hsv_v把增强集中到几何变换上。模型容量方面yolov8n如果已经开始过拟合继续加大到yolov8x只会更糟正确做法是加验证集数据或者用迁移学习冻结骨干网络的前几层让模型优先保留ImageNet学到的通用纹理特征。5.4 现象CPU推理一张大图要好几秒界面卡到没法演示古籍扫描件动辄3000x4000像素CPU用imgsz1280推理一张要5秒以上演示时体验很差。这个问题的根源不是模型本身慢而是输入图太大。我的做法是分级处理先用imgsz640快速跑一遍全图筛出有损伤的区域再把高置信度区域裁剪出来用imgsz960重新推理一遍精修框坐标。这样大部分无损伤页面在0.5秒内就能返回结果有损伤的页面多花1秒左右整体演示流畅度提升非常明显。代价是代码逻辑变复杂但毕设答辩时快速浏览精准复检这套流程很有说服力。5.5 现象PySide6界面打开后选择图片直接闪退闪退原因通常是QImage构造时参数没配对。YOLOv8推理出来的图像是RGB顺序但OpenCV的imread读出来是BGR如果直接把BGR数据当成RGB传给QImage轻则颜色错乱重则内存访问越界崩溃。解决方法是构造QImage前先转换色彩空间代码片段在4.2节里已经处理了。另一个闪退原因是用了scaled之后把QPixmap直接设置给QLabel而QLabel的size是0Qt里对0大小的控件设置图片会触发除零异常需要先setGeometry给窗口设置初始大小。6. 让古籍检测更稳的进阶操作多尺度推理与自查混淆矩阵6.1 多尺度推理把漏检率再压一截的实战技巧YOLOv8默认推理只在单尺度上做预测但古籍书页的损伤目标尺度差异极大虫蛀孔洞和整页水渍可能同时出现。多尺度推理的做法是跑多个imgsz把结果合并ultralytics的predict接口直接支持这个能力# 同时用640和960两个尺度推理augment开启测试时增强 yolo detect predict modelbest.pt sourcetest_pages/ imgsz640 augmentTrue save_txtTrueaugmentTrue会把水平翻转、缩放、平移全部加进推理过程多个增强结果做投票合并最终输出框的置信度会被平均。这个方法对小目标召回率的提升效果接近2到3个百分点但推理时间要乘上增强次数我只建议在评估指标时用或者对重点书页做复检时开起来。日常批处理还是关掉 augment 保持速度。6.2 用混淆矩阵核对模型到底错在哪训练结束后不要只看mAP打开混淆矩阵看一眼模型是怎么错的。ultralytics会自动生成混淆矩阵图在runs/detect/train/confusion_matrix.png里。需要重点观察两类错误水渍被误检成inkblot以及tear和wormhole之间的混淆。from ultralytics.utils.metrics import ConfusionMatrix import numpy as np # 用val集做预测构造混淆矩阵 from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.val(datadataset/data.yaml) # 混淆矩阵存在model.val的metric里 cm results.confusion_matrix.matrix np.set_printoptions(precision2, suppressTrue) print(cm)混淆矩阵输出是一个A x A的矩阵最后一行和最后一列是background类。如果background列里集中了大量高置信度框说明模型把纸张纹理这张背景当成了损伤目标此时应该检查标注时有没有把歧义区域标进去或者提高conf阈值。矩阵里cross-diagonal的数值居高不下则说明两个类别的feature分不开比如水渍和墨迹粘连在颜色和纹理上高度相似这种时候靠网络结构调整已经很难解决问题了更实际的做法是合并类别让系统只输出存在损伤和损伤类型不确定两个级别反而更贴合修复工作的实际需求。这套方案的落地路径总结下来就是标注规范定好训练参数整明白界面能画出框部署不依赖复杂环境。我在做类似项目时吃过最大的亏就是在标注阶段没统一标准导致返工三次重标数据集。如果你自己标注开工前一定要把每个类别的边界规则写清楚宁可多标几十张图也不要让标注员靠感觉画框。希望帮到你。本文还有配套的精品资源点击获取