简介:这份《基于YOLOv8的古籍保护系统》资源包专为毕业设计、课程设计或相关深度学习项目开发而整理,面向计算机、人工智能等专业的学生与初学者。内容涵盖完整的YOLOv8目标检测源码、古籍标注数据集、可视化交互界面及部署说明,可用于快速搭建一套可演示、可扩展的古籍保护检测系统。压缩包共包含97个文件,以70个Python源码文件为核心,辅以模型权重文件(pt)、配置文件(xml)、说明文档(txt)和演示视频(mp4),整体大小24.21MB,结构清晰,便于按模块查阅和二次开发。系统界面支持直观展示检测结果,并能生成精确率-召回率曲线、混淆矩阵、F1分数曲线等核心指标图表,方便用于答辩或项目汇报。资源中的代码均已测试通过,下载后按README指引即可运行,已有56人学习下载,值得作为实战项目参考。
1. 古籍保护系统为什么选择 YOLOv8:先看清这套系统在检测什么
「古籍保护系统」听起来像博物馆里的高精尖项目,拆开看核心任务其实很朴素:把古籍扫描件上的文字行、印章、批注、污损区域自动框出来。YOLOv8 在这套系统里承担的就是这个目标检测任务,框完之后再交给后续的 OCR 识别、留档和检索环节。选 YOLOv8 而不是更复杂的二阶段模型,是因为古籍页面的检测目标尺寸相对固定、类别少、对推理速度有要求,而且部署链路最成熟。这套方案把源码、数据集、可视化界面和部署教程打包在一起,简单部署即可运行,特别适合拿来当毕设或课程设计的完整项目练手。
2. 构建古籍检测数据集:标注类别怎么定、JSON 怎么转 YOLO 格式
2.1 古籍检测任务拆解与数据选型
古籍扫描件和现代文档的版面差异很大:竖排文字、繁体字形、栏线、版框、渗墨、虫蛀痕迹,这些在目标检测里都是可以被建模的视觉特征。做古籍保护系统,最常见的检测任务有三个方向:文字行检测(为 OCR 做前置)、印章检测、污损区域检测(水渍、霉斑、破损)。毕设或课程设计不建议一上来做七八个类别,2 到 4 类最稳,既能体现系统完整度,又不至于让数据集标注量失控。
我自己做过一次这类项目,踩过最大的坑是「想做单字检测」。单字框数量大、标注累、而且竖排繁体字的单字框外接矩形不稳定,换一页扫描件就可能对不齐。后来改回文字行检测,整页扫描件一张图标注的框数从几百降到三四十,训练质量反而上去了。古籍左上角的竖排题签、卷首的牌记、正文里的双行小注,这些用行级框都能覆盖,后续 OCR 再按行去切分识别,链路是通的。
数据来源方面,常见做法是去公开数字馆藏找公版古籍的扫描图,优先选明清民国时期的刻本和抄本,页面干净、版权风险低。先挑 30 到 50 张整页扫描件人工标注,训一个小模型,然后用这个小模型对剩下的扫描件做预标注,人工修正后回填数据集,迭代效率能提升很多。这个「半自动标注」流程比纯手标节省至少一半时间,是古籍这类密集版面场景最实用的开工方式。
2.2 LabelMe 标注与 Label 映射规则
LabelMe 是古籍数据集标注最常用的工具,因为它的 JSON 结构里保留的是多边形点坐标,后处理自由度大。如果你的项目也包含界面端展示,LabelMe 的原生格式还方便做可视化校验。LabelImg 当然也能用,但矩形框在竖排文字行的细长形状上经常框不紧,所以我一般优先教同学用 LabelMe。
标注前先定好类别与 id 的映射表,这一步指定了,后面所有脚本都不用改。
| 类别名 | label id | 含义 | 标注要求 |
|---|---|---|---|
| text_line | 0 | 竖排文字行 | 外接矩形紧贴文字,高度可以略大于文字本身 |
| seal | 1 | 红色印章 | 只框印面,不框题跋墨迹 |
| stain | 2 | 渗墨、水渍、破损 | 边缘模糊时按肉眼可见区域框 |
标注完成后每个图片文件对应一个同名 JSON 文件,例如page_001.jpg对应page_001.json。JSON 里记录的是多边形点坐标,下一步必须统一转换成 YOLO 训练需要的 txt 格式。
2.3 LabelMe JSON 转 YOLO txt 转换脚本
YOLO 的标注格式是每行class_id x_center y_center width height,坐标全部归一化到 0 到 1 之间。转换的核心是读取多边形的外接矩形,然后做归一化。下面是完整的转换脚本。
import json import os from pathlib import Path def labelme_to_yolo(json_path, out_dir, class_map): """ 将 LabelMe 的 JSON 标注转换为 YOLO 格式的 txt 标注 json_path: LabelMe 输出的 JSON 文件路径 out_dir: 输出 txt 的目录 class_map: 类别名到数字 id 的映射,例如 {"text_line": 0, "seal": 1, "stain": 2} """ with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) # 读取图像宽高,LabelMe 在 JSON 里存的是 imageWidth / imageHeight img_w = data.get("imageWidth", 0) img_h = data.get("imageHeight", 0) if img_w == 0 or img_h == 0: print(f"[跳过] {json_path} 缺少图像尺寸信息") return lines = [] for shape in data["shapes"]: label = shape["label"] if label not in class_map: # 宁可报错也不要静默跳过,否则类别错乱很难排查 raise ValueError(f"{json_path} 中出现未定义类别: {label}") # points 是多边形顶点,取所有顶点中最小/最大的 x、y 作为外接矩形 pts = shape["points"] xs = [p[0] for p in pts] ys = [p[1] for p in pts] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) # 过滤掉宽或高小于 1 像素的框,这类框训练时会被忽略 if x_max - x_min < 1 or y_max - y_min < 1: continue # 归一化,并裁剪到 [0, 1] 区间,防止坐标越界 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 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) lines.append(f"{class_map[label]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") # 输出 txt 文件,文件名与 JSON 同名 out_path = Path(out_dir) / (Path(json_path).stem + ".txt") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) if __name__ == "__main__": class_map = {"text_line": 0, "seal": 1, "stain": 2} json_dir = "datasets/annotations" txt_dir = "datasets/labels" os.makedirs(txt_dir, exist_ok=True) for json_file in Path(json_dir).glob("*.json"): labelme_to_yolo(str(json_file), txt_dir, class_map)脚本的逻辑分三步:先读 JSON 里的图像宽高和多边形点,再求外接矩形并归一化,最后按 YOLO 格式写入 txt。几个参数值得注意:class_map必须和后面训练时data.yaml里的类别顺序完全一致;坐标裁剪到 [0,1] 区间是为了防止某些标注工具偶尔产生越界点;宽度或高度小于 1 像素的框直接丢弃,因为这类框在训练时根本不会贡献损失,留着反而可能让 loss 崩掉。转换完成后,建议随手统计一下每个 txt 的行数,如果发现某张图的框数异常偏少,多半是 JSON 读取出了问题。
2.4 数据增强与划分:别把缺样本全押在增强上
古籍扫描件有两个天然难题:页面明暗不均、纸张纹理干扰多。YOLOv8 自带的 HSV 增强和 Mosaic 已经能覆盖一部分,但针对古籍我还建议额外加两种增强:亮度对比度抖动和局部高斯模糊。前者模拟不同扫描仪和翻拍设备的效果差异,后者模拟页面污损区域对文字行的边缘干扰。数据增强的开源库很多,常见做法是把它做成离线增强脚本,在训练前把样本量扩到原来的 2 到 3 倍,再走正常的训练流程。
需要特别记住的一条「血泪经验」是:古籍竖排文字行不要做左右翻转。通用目标检测里翻转是标配增强,但文字是带方向的,左右翻转之后文字行方向反了,等于强行把模型往错误方向训练。我见过有同学加了翻转增强后 mAP 涨了几个点,结果拿到真实古籍页面上检测框位置全偏到栏线外,就是这个原因。如果确实想引入翻转,只做小幅度的旋转和裁剪,不要做镜像。
数据集划分上,我习惯按「整页图片」划分而不是按「标注框」划分。同一页扫描件的内容是连贯的,如果训练集和验证集里出现同一页的不同区域,mAP 会虚高,最后换到全新页面上立刻现原形。train / val / test 按 8:1:1 切分即可,测试集留出来最后统一评估,不要提前参与任何调参决策。这个划分比例对古籍这种样本量几百张的小数据集来说是够用的,样本量真的特别少时,先把增强力度提上去,而不是急着加更多网络结构。
3. 训练古籍检测模型:环境、参数与训练日志判读
3.1 环境搭建:CPU 与 GPU 两条路怎么选
拿到项目源码后第一件事不是跑训练,而是把环境搭好。我接触过不少同学卡在第一步,这里按 GPU 和 CPU 两条线分别说。GPU 环境适合有独立显卡的机器,GTX 1660 Ti 这类 6G 显存的卡也能跑,只要别用大模型就行。
# GPU 路线:创建 conda 环境并安装 PyTorch(CUDA 11.8 版本) conda create -n guji_yolo python=3.10 -y conda activate guji_yolo # 安装 PyTorch,注意这里安装的是 CUDA 11.8 版本,下什么版本看显卡驱动 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 ultralytics,YOLOv8 的训练、验证、导出都靠这个库 pip install ultralytics # 验证安装是否成功,能打印出模型结构说明环境没问题 python -c "from ultralytics import YOLO; print(YOLO('yolov8n.pt'))"CPU 路线就是把 PyTorch 换成本身自带的 CPU 版本,pip install torch torchvision不加--index-url即可,ultralytics 的安装完全一致。在 Ubuntu 20.04 上搭建 YOLOv8 的 CPU 环境,很多同学会遇到libGL.so.1缺失的问题,这是 OpenCV 依赖的系统库没装,后面避坑章节会专门讲。CPU 环境跑训练确实慢,但跑推理和界面演示完全够用,做毕设答辩演示不必非得有 GPU。
环境搭好后建议顺手固定依赖版本,把pip freeze > requirements.txt的结果提交到项目里,这样换机器部署时能一键还原环境。这是「简单部署即可运行」最容易被忽略的一部分。
3.2 数据配置与最小训练命令
训练前需要先写一个data.yaml,告诉 YOLOv8 训练集、验证集、类别信息放在哪。古籍数据集的项目里一般都有一个datasets/目录,里面按 images 和 labels 分开存放,结构如下。
# data.yaml:古籍检测数据集配置 path: ../datasets/guji # 数据集根目录,相对于该 yaml 文件所在位置 train: images/train # 训练集图片目录 val: images/val # 验证集图片目录 test: images/test # 测试集图片目录,训练时用不到,但评估时有用 nc: 3 # 类别总数 names: 0: text_line # 竖排文字行 1: seal # 印章 2: stain # 污损区域写data.yaml最容易翻车的是path用了绝对路径,换一台机器就得改一遍。这里建议用相对于 yaml 文件所在位置的相对路径,这样整个数据集目录拷到哪里都能直接训。类别顺序必须和 2.3 节转换脚本里的class_map保持一致,否则模型学到的和标注对不上,训练过程看起来正常但结果完全错乱。
训练命令本身很短,但参数组合有讲究。我第一次跑古籍数据时用的就是下面这条命令。
# 最小可用的训练命令 yolo detect train \ data=datasets/guji/data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=8 \ device=0 \ patience=20 \ workers=2 \ project=runs/train \ name=guji_v1参数含义拆开看:model=yolov8s.pt表示在 COCO 预训练权重基础上微调,比从头训练收敛快得多;epochs=100是常规起步值,古籍数据量小,一般 80 到 120 轮就能收敛;imgsz=640是 YOLOv8 的默认输入尺寸,但如果你的扫描件分辨率很高且文字行偏细,建议提到 960;patience=20表示连续 20 轮验证集指标没提升就早停,这个参数能避免周末没看训练导致过拟合白跑;workers=2是数据加载线程数,Windows 上太高容易报错,Linux 上可以到 4 到 8。batch主要看显存,6G 显存跑s模型用 8 颗没事,想稳一点就降到 4。
先别急着追求指标,我第一次拿到项目源码时习惯先用yolov8n.pt小模型训 20 个 epoch,目的是确认数据路径、标注格式、类别数量全部正确。这一步能跑通,再换s或m模型跑正式训练,能省下大量排查问题的时间。
3.3 训练日志怎么读:从 loss 到 mAP 的完整链路
训练开始后,很多同学只看每轮打印的 mAP50,这是最常见的误区。mAP 是结果不是原因,训练过程中真正需要盯的是 loss 曲线的趋势。YOLOv8 会在runs/train/guji_v1/目录下生成results.csv和results.png,里面记录了每一轮的 box_loss、cls_loss、dfl_loss 以及验证集上的 precision、recall、mAP50、mAP50-95。
判断模型状态有一套很实用的经验规则:如果train/box_loss持续下降而val/box_loss先降后升,说明开始过拟合了,应该提前停止或加大数据增强;如果两个 loss 都降不下去,且 mAP 在低位抖动,说明模型容量不够或者标注噪声太大,这时候换更大的模型不如回头清洗数据。古籍数据的标注质量参差不齐,尤其污损区域边界模糊,标注不一致导致的 loss 波动很常见,不用过度紧张。
YOLOv8 自带的results.png已经画好了曲线,但如果你想单独画损失函数曲线图放进论文或答辩 PPT,可以用下面的脚本自己画。
import pandas as pd import matplotlib.pyplot as plt # 读取训练日志,YOLOv8 会把每轮的指标写入 results.csv df = pd.read_csv("runs/train/guji_v1/results.csv") # 去掉列名首尾空格,方便按列名取数 df.columns = df.columns.str.strip() plt.figure(figsize=(10, 6)) # train_loss 画在左边子图,注意 YOLOv8 的列名格式 plt.subplot(1, 2, 1) plt.plot(df["epoch"], df["train/box_loss"], label="train_box_loss") plt.plot(df["epoch"], df["val/box_loss"], label="val_box_loss") plt.xlabel("epoch") plt.ylabel("loss") plt.legend() plt.title("Box Loss Curve") # mAP 画在右边子图,量纲和 loss 不同,所以要分开画 plt.subplot(1, 2, 2) plt.plot(df["epoch"], df["metrics/mAP50(B)"], label="mAP50") plt.plot(df["epoch"], df["metrics/mAP50-95(B)"], label="mAP50-95") plt.xlabel("epoch") plt.ylabel("mAP") plt.legend() plt.title("mAP Curve") plt.tight_layout() plt.savefig("loss_curve.png", dpi=200)画图脚本的逻辑很简单,但有两个参数细节值得注意。第一,results.csv是逗号分隔且列名包含空格,读取后必须 strip 列名,否则取数时报 KeyError;第二,loss 曲线和 mAP 曲线量纲不同,不要画在同一个纵轴上,否则 loss 的数值变化会被 mAP 的 0 到 1 区间压扁,看起来像一条直线。训练结束后best.pt是验证集指标最好的权重,last.pt是最后一轮权重,调参阶段用 best.pt,继续训练则用 last.pt。
3.4 模型选择与性能边界:n、s、m、l 怎么选
YOLOv8 系列从 n 到 x 共有五个规模,古籍保护系统完全用不到 x 级别,重点在 n、s、m 三个之间选择。选择依据就两条:标注框的细腻程度和推理设备的能力。
古籍扫描件上的文字行通常是细长形状,竖排文字行在 640 × 640 输入下大约只有 10 到 30 像素宽。yolov8n的 backbone 比较浅,对这种细条状目标的特征提取能力偏弱,容易漏检;yolov8m特征表达能力更强,但在 CPU 上推理速度会明显下降。我的实际选择建议是:数据量在 300 张以下用s,数据量充足做最终版用m,n只用来验证流程是否跑通。GTX 1660 Ti这类 6G 显存显卡跑s模型训练毫无压力,推理速度也是毫秒级,不用过度焦虑硬件配置。
输入尺寸imgsz对古籍这类小目标的影响比换模型更大。用 640 训练时,如果发现文字行漏检率偏高,可以把imgsz提到 960 或 1024 重新训练。代价是显存占用和训练时间约翻一倍,但古籍文字行的检测效果提升非常明显。这一步可以配合 YOLOv8 的网络结构图去理解:小目标在浅层特征图上的信息更丰富,输入图越大,浅层的特征保留越完整,检测头越容易捕捉到细长目标。
4. 把模型封装成可视化界面:推理逻辑与 PySide6 实现
4.1 可视化界面功能拆解:先想清楚用户操作什么
古籍保护系统的可视化界面不只是「显示一张图画几个框」那么简单。站在使用者的角度,打开界面后需要完成这些操作:加载训练好的模型权重、选择单张图片或一个文件夹、执行检测、调节置信度阈值过滤误检、查看检测结果并导出。导出的内容又分为带标注框的图片和结构化的坐标数据,后者会对接后续的 OCR 识别和档案录入。
功能拆解做得越细,界面代码越不容易返工。我把界面拆成三个区域:左侧是控制面板,包含模型选择、图片选择、置信度滑块、检测按钮;中间是图像显示区,实时渲染检测框和标签;底部是日志栏,显示当前识别的图片路径、检测到的目标数量、每类目标的分布。这个布局对汉字竖排的检测结果展示很友好,左侧参数调整后中间画面立刻刷新,答辩演示的观感也好。
4.2 推理模块:单图预测与批量预测的差异
推理模块是界面和模型之间的桥梁,建议单独抽成一个 Python 文件,UI 层只负责调用,不要在控件事件里直接写模型代码。这样以后换模型、换推理框架都不需要改动界面。
# inference.py:封装 YOLOv8 推理逻辑 from ultralytics import YOLO class GujiDetector: def __init__(self, weights_path: str, device: str = "0"): """ weights_path: 训练得到的 best.pt 路径 device: '0' 表示第一个 GPU,CPU 环境传 'cpu' """ self.model = YOLO(weights_path) self.device = device def predict(self, image_path: str, conf: float = 0.25, imgsz: int = 640): """ 对单张图片执行检测,返回标准化后的结果 """ results = self.model.predict( source=image_path, conf=conf, imgsz=imgsz, device=self.device, verbose=False, ) # results[0] 是单张图片的检测结果,boxes 里包含所有目标框 boxes = results[0].boxes detections = [] for box in boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() # 框的绝对坐标 conf_val = float(box.conf[0]) cls_id = int(box.cls[0]) detections.append({ "bbox": [x1, y1, x2, y2], "confidence": conf_val, "class_id": cls_id, }) return detections这个模块里有几个关键选择。verbose=False必须加上,否则每次推理都会往控制台刷一条日志,界面跑久了会显著卡顿。box.xyxy返回的是像素绝对坐标,不是归一化坐标,在界面上画框时直接用,方便得很。批量预测不需要自己写循环,ultralytics 的predict支持source传文件夹路径,它会自动遍历目录下所有图片,但返回的results是一个列表,遍历时要按图片顺序对应起来,这个细节在界面里尤其容易踩坑。我习惯在predict方法里只处理单张图片,批量逻辑放在 UI 层用循环驱动,这样单条进度能实时反馈到界面底部的日志栏。
4.3 核心问题:把推理放进子线程,界面才不卡死
可视化界面最典型的翻车现场是:点击「开始检测」按钮后,整个窗口白屏无响应,鼠标转圈,几秒钟后系统提示「程序未响应」。原因几乎可以锁定——推理跑在了 UI 主线程里。YOLOv8 的推理虽然只有几百毫秒,但在主线程里执行期间,Qt 的事件循环被阻塞,窗口自然就冻结了。解决办法是把推理放到 QThread 子线程,完成后通过信号把结果传回主线程更新界面。
# worker.py:在子线程中执行推理,避免界面卡死 from PySide6.QtCore import QThread, Signal class DetectWorker(QThread): # 定义两个信号:finished 传检测结果,error 传错误信息 finished = Signal(list) error = Signal(str) def __init__(self, detector, image_path: str, conf: float): super().__init__() self.detector = detector self.image_path = image_path self.conf = conf def run(self): # run 方法在子线程中执行,不会阻塞主线程的界面刷新 try: detections = self.detector.predict(self.image_path, conf=self.conf) self.finished.emit(detections) except Exception as e: self.error.emit(str(e))对应的主窗口里,按钮点击事件只做三件事:创建 Worker、连接信号、启动线程。不要在按钮回调里写任何模型推理代码。
# main_window.py:主窗口核心代码(节选) from PySide6.QtWidgets import QMainWindow, QPushButton, QSlider, QLabel, QFileDialog from PySide6.QtCore import Qt class MainWindow(QMainWindow): def __init__(self, detector): super().__init__() self.detector = detector self.worker = None # 保存 worker 引用,防止被垃圾回收 self.btn_detect = QPushButton("开始检测", self) self.btn_detect.clicked.connect(self.on_detect) self.slider_conf = QSlider(Qt.Horizontal, self) self.slider_conf.setRange(5, 95) # 置信度阈值 5% ~ 95% self.slider_conf.setValue(25) # 默认 25% def on_detect(self): # 选择图片,然后启动子线程 path, _ = QFileDialog.getOpenFileName(self, "选择古籍扫描件", "", "Images (*.png *.jpg *.jpeg)") if not path: return # 清空上一次的 worker,避免重复点击时线程冲突 if self.worker is not None and self.worker.isRunning(): return conf = self.slider_conf.value() / 100.0 self.worker = DetectWorker(self.detector, path, conf) self.worker.finished.connect(self.on_detect_finished) self.worker.error.connect(self.on_detect_error) self.worker.start() # start 之后 run 方法在子线程中执行 def on_detect_finished(self, detections): # 主线程中收到结果,此时可以安全地更新界面 self.draw_boxes(detections) def on_detect_error(self, message): print(f"检测出错: {message}")信号槽机制是 PySide6 让界面不卡死的关键:子线程算完结果通过finished信号发回主线程,主线程在事件循环里收到信号后才去更新界面。两个参数细节值得注意:self.worker必须作为实例属性保存,否则 Python 的垃圾回收会在函数返回后把 worker 销毁,导致线程异常终止;滑块的值是整数,除以 100 转成浮点数后才能作为置信度阈值传给模型。这套线程模型同样适用于批量检测时逐张展示进度条的场景。
4.4 从界面到系统:目录规划与源码组织
一个能交付的毕设项目,源码组织比模型本身更能决定答辩效果。古籍保护系统的项目结构我建议按下面的方式组织,训练代码、推理代码、界面代码、数据配置互不干扰。
guji_protect/ ├── README.md # 部署教程:环境安装 + 启动命令 ├── requirements.txt # 依赖清单,一键安装 ├── data.yaml # 数据配置 ├── datasets/ │ ├── images/ # 古籍扫描件 │ └── labels/ # YOLO 格式标注 ├── weights/ │ └── best.pt # 训练好的模型权重 ├── scripts/ │ ├── convert_json_to_yolo.py │ ├── train.py │ └── eval.py ├── ui/ │ ├── main_window.py │ └── worker.py ├── inference.py # 推理模块封装 └── app.py # 程序入口,双击运行README.md里部署教程的写法要遵循一个「陌生机器验证」原则:在另一台干净电脑上,照着 README 从头执行一遍命令,能跑通才算数。我见过很多项目的 README 是答辩前两小时赶出来的,漏掉了libGL.so.1这种系统依赖,最后只能在评委面前翻车。requirements.txt里建议把ultralytics、torch、PySide6、pandas、matplotlib这几个核心包列全,版本号可以不用锁死,但 Python 版本最好在 README 里标明。
5. 部署与跑通避坑清单:环境、数据、界面三类常见问题
5.1 环境与依赖的坑
现象:pip install ultralytics之后运行程序,报错ImportError: libGL.so.1: cannot open shared object file。
原因:ultralytics 会依赖 OpenCV,而 OpenCV 在 Linux 上需要系统的libgl1库。很多新装的 Ubuntu 20.04 系统没有预装这个库,Python 层面装再多包也解决不了。
解决:在终端执行sudo apt-get update && sudo apt-get install -y libgl1 libglib2.0-0,装完再运行程序就正常了。如果这台机器连编译工具都没有,顺手把build-essential也装上,避免后续装其他包时再踩一遍。
现象:CPU 环境下运行界面,点击检测后转圈三五秒才出结果,答辩演示体验很差。
原因:CPU 推理本身就慢,如果还用yolov8m或yolov8l模型,加上没有做任何推理优化,单张图 3 到 5 秒是常态。
解决:答辩演示换yolov8s甚至yolov8n权重,imgsz降到 480,延迟能压到 1 秒以内。也可以把模型导出成 ONNX 格式用 ONNX Runtime 跑推理,CPU 上提速非常明显,这个后面章节会讲。
5.2 训练与数据集格式的坑
现象:训练刚跑几个 step 就报CUDA out of memory,把batch从 8 降到 2 还是崩。
原因:imgsz设得太大,或者模型用了m级别以上,显存占用超出显卡容量。6G 显存跑yolov8m加imgsz=960,崩是很正常的,不是代码问题。
解决:先按「s模型 +imgsz=640+batch=4」跑通整个流程,确认数据没问题后再逐步加大。另外 YOLOv8 默认开启 Mosaic 增强,Mosaic 会把 4 张图拼在一起,显存占用大概是普通训练的 2 倍,batch调小是合理的应对手段。
现象:训练正常结束,mAP50 看着还行,但拿到完整古籍扫描页面上检测,文字行大量漏检,只检出一些碎片。
原因:训练时输入图片是 640 × 640 的缩放图,完整扫描件缩到 640 后,竖排文字行的宽度只剩几个像素,模型特征提取阶段就把这些小目标过滤掉了。
解决:一是训练时把imgsz提到 960 甚至 1024;二是做滑窗推理,把完整扫描页切成 640 × 640 的重叠块分别检测再合并结果。后者更稳定,但对界面代码要求高一些,适合作为进阶优化。
现象:新增了一个类别,重新训练后所有旧类别的框都识别成新类别,或者预测结果类别错乱。
原因:data.yaml里类别顺序改了,但标注文件的 label id 没同步改,模型输出层的类别映射和实际标注对不上。还有一种情况是旧标注缓存没有清掉,YOLOv8 用.cache文件缓存标注信息,改了标注后缓存还在用旧数据。
解决:以data.yaml的names顺序为唯一标准,重跑一遍 JSON 转 YOLO 脚本生成新的 txt;同时删除数据集目录下所有.cache文件再训练。这条坑排查起来非常隐蔽,我花了半天才定位到缓存上。
5.3 界面与推理的坑
现象:点击「开始检测」后窗口立刻白了,不停转圈,过几秒弹出「未响应」。
原因:推理直接写在按钮点击事件里,跑在了 Qt 主线程上,阻塞了界面事件循环。模型推理时间越长,白屏时间越久。
解决:按第 4 章的做法,把推理逻辑拆到QThread子线程中,通过信号把结果回传主线程。如果项目用了 Flask 或 FastAPI 做 Web 界面,则对应地使用异步任务队列,思路是一样的。
现象:程序在 Windows 上能跑,拷到 Linux 上打开图片时报错,提示图片路径不存在。
原因:项目中写死了 Windows 的路径分隔符\,或者图片路径里带中文,Ultralytics 与 OpenCV 在部分 Linux 环境下对非 ASCII 路径处理不稳定。
解决:项目所有路径统一用pathlib.Path拼接,迁移到 Linux 后自动适配/分隔符;训练数据和项目整体约定用英文目录,避免中文路径带来的未知问题。在程序入口处打印当前工作目录,排查路径问题时能省一半时间。
6. 交付前最后一步:验证指标、ONNX 导出与 OCR 联动扩展
6.1 验证指标检查清单
训练完不是直接交差,先用测试集做一轮完整验证再谈部署。测试集是训练阶段完全没见过的图片,在这上面跑的指标才有说服力。我习惯对照下面这张表逐项检查,缺一项就回去补。
| 检查项 | 工具 | 通过标准 |
|---|---|---|
| 逐类 AP | yolo detect val或脚本读取 val 结果 | 每个类别的 AP50 都高于 80% 视为及格 |
| 置信度阈值选择 | PR 曲线或 F1-confidence 曲线 | 在 F1 最高点附近取阈值,不要直接用默认 0.25 |
| 小目标漏检 | 在测试集上可视化预测结果 | 竖排文字行连续完整,无断行漏检 |
| 误检 | 查看标注框与预测框的 IoU 分布 | 印章误检成文字行、栏线误检成文字行的比例需低于 5% |
6.2 导出 ONNX 与 OCR 联动
部署到没有 GPU 的机器时,把模型导出成 ONNX 格式是性价比最高的加速手段。Ultralytics 自带导出功能,一条命令就能搞定。
# 导出 ONNX 格式模型,opset 12 兼容性好 yolo export model=weights/best.pt format=onnx imgsz=640 opset=12导出后的.onnx文件可以用 ONNX Runtime 加载,CPU 上推理速度比原生 PyTorch 快不少,而且完全不依赖 PyTorch 环境,部署时少装一堆依赖。
古籍保护系统的最终价值在识别而不只是检测。检测框拿到之后,把每个文字行区域裁剪下来,交给开源 OCR 引擎识别成文本,再连同坐标一起写入 JSON 或 CSV,就形成了完整的古籍数字化档案。这一步做进系统里,毕设的功能深度立刻拉开差距。我自己交付这类项目前有个固定习惯:找一台没装过任何 Python 包的干净机器,照着 README 从零执行一遍部署教程,全部命令复制粘贴能跑通,才敢说「简单部署即可运行」。这套系统的价值不在于模型结构多先进,而在于把数据标注、训练、界面、部署整条链路完整走通了一次,往里再补 YOLOv8 结构改进或小目标检测头也都是顺手的事。希望帮到你。
本文还有配套的精品资源,点击获取