简介:一套基于Python与机器学习技术的书籍内容文本识别项目源码,面向希望学习光学字符识别、模型部署及Web后台开发的开发者与学生。项目完整集成了模型训练与识别代码、Flask后台服务和数据库管理,识别出的文本可自动写入数据库,实现从书籍图像到可检索文字的完整链路。压缩包共96个文件,大小约97.08MB,主要包含Python脚本、HTML/CSS/JS前端资源、SQL数据库脚本、txt样本语料以及CNN模型权重等,目录划分清晰,便于按模块查阅。已有88人学习下载。借助源码、测试脚本和分类书籍文本,可快速理解深度学习文本识别模型的构建过程、Flask接口如何接收并调用识别服务,以及数据库表结构设计与数据持久化方法,也适合在此基础上进行二次训练与功能扩展。
1. 一张书页图片到一条数据库记录:这本书籍内容文本识别项目到底解决了什么
纸质书扫描件、手机拍的页面照片、出版社给的 PDF 书影,这些内容想变成能检索、能编辑、能入库的文本,最省人力的路子不是让录入员逐字敲,而是打通一条“图片 → 机器学习识别 → Flask 后台 → 数据库”的完整管线。这个 Python 项目做的就是这件事:识别引擎把页面上的文字块检测出来并转成文本,Flask 提供上传与查询接口,识别出的文本连同页码、置信度一起写进数据库,后续做知识库检索、内容校对、全文搜索都直接从库里取数,不用再回看图片。这个方案适合正在做数字图书馆、个人知识库或内部 OCR 工具链的人;如果你是刚完成机器学习入门、想学怎么用 Flask 部署模型的开发者,按这套代码改也能少走弯路。下面从引擎选型拆到排障,每步给直接可复制的实现。
2. 识别引擎与管线先立住:书籍文本识别不是把图丢给 OCR 那么简单
2.1 引擎怎么选:PaddleOCR、EasyOCR、Tesseract 的实际差距
你手头有三类开源识别引擎可选。PaddleOCR 是百度开源的中文 OCR 工具链,检测、方向分类、识别三个模型都是现成的,对中文和复杂版面支持最好,缺点是 PaddlePaddle 这套深度学习框架体积不小,安装时要注意 Python 版本与 CUDA 的兼容关系。EasyOCR 基于 PyTorch,pip install完就能跑,支持语言多,但识别速度比 PaddleOCR 慢,对长文本和双栏版面的稳定性也弱一些。Tesseract 是老牌开源 OCR,轻量、部署简单,但中文识别准确率和版面理解能力明显不如前两者,适合只识别印刷清晰的英文或简单中文场景。
选型上我的建议很明确:书籍场景默认选 PaddleOCR。原因有三条。第一,书籍文本的主力是中文,PaddleOCR 的中文识别准确率在开源方案里属于第一梯队。第二,它自带文本检测模型,能输出每个文字块的坐标和置信度,这是后面按页码、按块存数据库的基础;第三,它对方向分类(竖排、倒置页面)有现成支持,技术书和古籍扫描件经常出现这类情况。如果你只是临时识别几十页、不要求精细排版,用 EasyOCR 也行;但我不太推荐 Tesseract 做中文书籍,语言包与排版顺序的坑会花掉你大量时间。
2.2 识别管线四环节:预处理、检测、识别、后处理顺序不能乱
机器学习模型做得再准,也架不住输入图片质量差。我把书籍识别的标准管线拆成四段:预处理、文本检测、文本识别、后处理拼接。预处理做的是灰度化、二值化、去噪和倾斜校正。手机拍的书页经常有阴影和透视变形,中值滤波能去掉扫描噪点,OTSU 大津法自动计算二值化阈值,把文字和背景分开,方向分类器则负责把歪的页面转正。这些步骤不是玄学,它们直接影响检测模型能不能框出完整的文字块。
# preprocess.py import cv2 def preprocess_page(image_path: str) -> str: """书籍页面预处理:放大、去噪、二值化,返回新图片路径。""" img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) h, w = img.shape # 低分辨率图先放大,宽度目标 2000 像素 if w < 2000: scale = 2000 / w img = cv2.resize(img, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC) # 中值滤波去扫描噪点,ksize 必须是奇数 img = cv2.medianBlur(img, 3) # OTSU 自动算阈值,二值化输出黑白图 _, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) out_path = image_path.rsplit(".", 1)[0] + "_pre.png" cv2.imwrite(out_path, binary) return out_path这段代码有四个参数值得你按自己扫描件实际情况调整。2000是目标宽度,普通 300 DPI 的 A4 扫描件宽度在 2400 像素以上,不需要放大;手机照片常常只有 1200 像素左右,放大到 2000 后识别率提升明显。medianBlur的核大小3对文字清晰的书页足够,如果原图噪点很重可以改成5,但核太大会把细笔画磨掉。THRESH_OTSU会自动算阈值,适合背景均匀的扫描页;如果页面有严重阴影,还要再加一步顶帽变换才能分开背景。预处理完的图片建议单独存一份,方便回头对比是预处理的问题还是模型的问题。
文本检测负责在图片里找出“哪里有字”,输出一串坐标框;文本识别负责把每个框里的图像变成字符串;后处理把识别结果按阅读顺序拼成段落。PaddleOCR 的ocr()接口把检测和识别串在一起,但你要清楚它返回的数据结构:每个元素是[坐标框, (文本, 置信度)],坐标框是四个顶点的 xy 坐标,置信度是 0 到 1 的浮点数。这组数据不能直接丢进数据库,得先做排序和过滤——很多人第一次写 OCR 脚本时不知道还有这一步,入库后按页码抽文本时才发现段落顺序全乱了。
2.3 版面分析:小说容易识别、技术书难,难点在阅读顺序
书籍不是只有一种版面。纯文本小说是单栏正文,检测完按从上到下、从左到右排,就能得到和原书一致的段落。技术书和杂志就麻烦了:标题、正文、代码块、图表注释混在一起,检测模型按坐标输出文字块时,常常先输出图片注释、再输出正文,甚至把左右栏内容交叉串联。这个问题的根因不是识别模型不够好,而是没有给输出施加“阅读顺序”约束。
我一般用两种办法解决。第一种是后处理排序:拿到所有文字块后,按块中心点的 y 坐标从大到小排,页面坐标系里 y 向下增大,所以 y 大的是靠下的块;y 相近的块再按 x 从小到大排,这样能恢复多数单栏和双栏版面的阅读顺序。第二种是直接用 PaddleOCR 的版面分析能力,对小标题、页眉页脚等区域做分类,成本高,通常只有要做精确版式还原的产品才投入。对大多数书籍数字化场景,后处理排序已经够用。
# sort_blocks.py def sort_blocks(raw_result): """把 PaddleOCR 的原始输出按阅读顺序排序。""" blocks = [] for item in raw_result: box = item[0] # 四个顶点坐标 text, conf = item[1] # 文本和置信度 xs = [p[0] for p in box] ys = [p[1] for p in box] blocks.append((sum(ys) / 4, # 中心 y sum(xs) / 4, # 中心 x text, conf)) blocks.sort(key=lambda b: (b[0] // 20, b[1])) # 按 y 分行,组内按 x 排 return [(b[2], b[3]) for b in blocks]排序函数里b[0] // 20是个经验值:同一行文字块的中心 y 差距通常在 20 像素以内,整除 20 能让同一行的块分到同一组。这个系数和字体大小强相关,小五号字和标题的差距就很大;如果你的页面字号跨度大,改成按行高自适应分组更稳。排序后返回的列表里只剩文本和置信度,方便直接传给下一步写库。这个排序逻辑对绝大多数横排的书籍已经够用,竖排古籍不适用,需要另按 x 从右往左处理。
3. 建库、落库、搭 Flask:识别结果怎么从内存走进数据库
3.1 数据库表设计:用 MySQL 存识别结果的可抄作业方案
识别结果至少要存住五类信息:书籍标识、页码、文字块序号、文本内容、置信度。为什么页码和块序号要单独成字段?因为用户后续要“按页提取”“按块校对”,如果只存一个大长文本,定位成本会非常高。下面这张建表 SQL 是我常用的结构,字符集必须用utf8mb4,具体原因放避坑章讲。
CREATE DATABASE IF NOT EXISTS book_ocr DEFAULT CHARACTER SET utf8mb4; USE book_ocr; CREATE TABLE IF NOT EXISTS book_text ( id INT AUTO_INCREMENT PRIMARY KEY, book_id VARCHAR(64) NOT NULL COMMENT '书籍唯一标识,可用 ISBN', page_no INT NOT NULL COMMENT '页码,从 1 开始', block_no INT NOT NULL COMMENT '页内文字块序号,从 0 开始', content TEXT NOT NULL COMMENT '识别出的整段文本', confidence FLOAT NOT NULL COMMENT '该块识别置信度,范围 0~1', need_check TINYINT NOT NULL DEFAULT 0 COMMENT '置信度低于阈值时置 1,供人工校对', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_book_page (book_id, page_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这个表设计有两个关键点。book_id我建议放 ISBN 或统一编号,而不是自增主键,因为批处理同一本书的多页时,按书检索的频率远高于按单条记录检索。idx_book_page是复合索引,支撑两种高频查询:按书取全部内容、按页码取单页文本。书籍数字化场景几乎只有这两种查询,不需要再加别的索引。need_check字段是给置信度过滤预留的标记位,进阶章节会用到。TEXT 类型足够存一个文字块的内容,单页文本量再大也不会超过这个长度。
3.2 识别并写入数据库:同步版主流程代码
下面是一段可以直接跑通的同步识别脚本。它读取单张书页图片,调用 PaddleOCR 识别,然后把每个文字块连同元数据插入 MySQL。这里先保留pymysql直连方式,不引入 ORM,目的是让你看清数据是怎么流动的;ORM 版本放到下一小节。
# sync_ocr_to_db.py import pymysql from paddleocr import PaddleOCR DB_CONFIG = { "host": "127.0.0.1", "port": 3306, "user": "root", "password": "your_password", "database": "book_ocr", "charset": "utf8mb4", } def recognize_page_to_db(ocr, image_path: str, book_id: str, page_no: int) -> int: """识别单页图片并把文字块写入数据库,返回写入条数。""" result = ocr.ocr(image_path, cls=True) conn = pymysql.connect(**DB_CONFIG) written = 0 try: with conn.cursor() as cursor: for block_idx, item in enumerate(result): content = item[1][0].strip() confidence = float(item[1][1]) if not content: continue cursor.execute( "INSERT INTO book_text " "(book_id, page_no, block_no, content, confidence) " "VALUES (%s, %s, %s, %s, %s)", (book_id, page_no, block_idx, content, confidence), ) written += 1 conn.commit() except Exception: conn.rollback() raise finally: conn.close() return written if __name__ == "__main__": ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False) n = recognize_page_to_db(ocr, "./sample_page.jpg", "978-7-0000-0000-0", 1) print(f"写入 {n} 条文字块")代码里有三个参数需要你按实际拿捏。use_angle_cls=True表示开启方向分类器,能照顾歪着拍的页面和竖排书页,代价是识别时间增加;如果输入是排版端正的扫描 PDF,可以关掉省时间。lang="ch"指定中文模型,PaddleOCR 首次运行会自动下载对应模型文件,离线机器要提前准备。cls=True写在ocr()调用里,才是把方向分类应用到这次识别;如果这句忘写,即使初始化时开了分类器,实际也不会走分类流程。写入部分我特意做了两件事:strip()清掉文本首尾空白,空文本块直接跳过,这两个细节能避免库里出现大量空白字符串,影响后续全文检索。
3.3 批量识别一本书:按目录遍历并保持页码顺序
单页识别跑通后,批量处理是顺理成章的需求。常见的目录组织方式是把一本书的扫描页放在一个文件夹里,文件名带页码,比如book1_001.jpg、book1_002.jpg。遍历时要注意字符串排序问题:os.listdir返回的文件名顺序是字典序,2.jpg会排在10.jpg前面,所以页码要用正则从文件名里抠出来转成 int 再排。
# batch_recognize.py import os import re from paddleocr import PaddleOCR from preprocess import preprocess_page from sync_ocr_to_db import recognize_page_to_db def run_batch(book_id: str, page_dir: str): """批量识别一个目录下的所有书页图片。""" ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False) files = [f for f in os.listdir(page_dir) if f.lower().endswith((".jpg", ".jpeg", ".png"))] files.sort(key=lambda f: int(re.search(r"\d+", f).group())) for idx, filename in enumerate(files, start=1): image_path = os.path.join(page_dir, filename) processed_path = preprocess_page(image_path) try: n = recognize_page_to_db(ocr, processed_path, book_id, idx) print(f"{filename} -> {n} blocks") except Exception as exc: print(f"{filename} -> 失败: {exc}")这里用re.search(r"\d+", f)提取第一个连续数字串作为排序依据。文件名如果包含书籍编号和页码两段数字,这个方法会取到前者,所以更稳的写法是严格匹配_(\d+)\.这样的模式。enumerate(files, start=1)让页码从 1 开始连续编号,和表结构里的page_no对齐。单页失败了记下日志继续跑,不要中断整本书,这是批处理脚本的基本素养。
3.4 用 SQLAlchemy 改造写入:为 Flask 后台铺路
直连方式适合脚本和批处理,Flask 后台我更推荐换成 SQLAlchemy,连接池、事务和模型定义都有人管,不用自己拼 SQL。下面把写入逻辑改成 ORM 版本。
# models.py from sqlalchemy import create_engine, Column, Integer, String, Text, Float from sqlalchemy.orm import declarative_base, sessionmaker engine = create_engine( "mysql+pymysql://root:your_password@127.0.0.1:3306/book_ocr?charset=utf8mb4", pool_size=5, echo=False, ) Base = declarative_base() class BookText(Base): __tablename__ = "book_text" id = Column(Integer, primary_key=True, autoincrement=True) book_id = Column(String(64), nullable=False, index=True) page_no = Column(Integer, nullable=False) block_no = Column(Integer, nullable=False) content = Column(Text, nullable=False) confidence = Column(Float, nullable=False) Session = sessionmaker(bind=engine) def insert_blocks(book_id: str, page_no: int, blocks: list[dict]) -> int: """blocks 是 [{'content': ..., 'confidence': ...}, ...] 的列表。""" session = Session() try: rows = [BookText( book_id=book_id, page_no=page_no, block_no=i, content=b["content"], confidence=b["confidence"], ) for i, b in enumerate(blocks) if b["content"]] session.add_all(rows) session.commit() return len(rows) except Exception: session.rollback() raise finally: session.close()SQLAlchemy 版本的核心好处是session统一管理事务,避免手动cursor.execute和conn.commit的遗漏。pool_size=5是连接池大小,多线程并发识别时这个值要跟着 Flask 的并发数调整:连接池太小会报连接耗尽,太大对 MySQL 是负担。echo=False平时关掉 SQL 日志,排查问题时改成True,能看到每一条实际执行的语句。Base = declarative_base()的写法在 SQLAlchemy 1.4+ 是标准姿势,老项目里看到的declarative_base()也是同一个来源。
4. Flask 后台把识别服务暴露成接口:接口设计、异步任务与部署
4.1 两个接口的分工:上传和识别要拆开
做成后台服务后,得给调用方提供两个接口:POST /upload负责接收上传的图片文件并保存到临时目录,POST /recognize负责触发识别并把结果入库。为什么拆成两个?如果合成一个接口,上传大文件时连接要一直保持到识别结束,用户体验差且容易超时。拆开后,上传接口只做文件接收,识别接口只看图片路径,职责清晰,也方便后面接任务队列。
接口返回统一用 JSON,至少包含task_id和status两个字段。task_id是这次识别任务的唯一标识,调用方拿着它去查状态;status有pending、running、done、failed四种流转。为什么不直接返回识别文本?原因和上面一样,识别是重计算,读取结果应该由单独的查询接口负责,而不是让识别接口承载全部职责。
4.2 长任务处理:同步识别会把 Flask 请求卡死
如果你把识别直接写在视图函数里,第一个调用会把 Flask 的工作线程占住几秒甚至几十秒,并发一上来整个服务就不可用了。常见做法是把识别丢到后台线程或任务队列。生产环境我会用 RQ 或 Celery 配 Redis,但小项目用一个线程池就能先跑通。下面是一段可运行的简化实现。
# app.py import os import threading import uuid from flask import Flask, request, jsonify from paddleocr import PaddleOCR from models import insert_blocks app = Flask(__name__) app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 上传上限 16MB UPLOAD_DIR = "./uploads" os.makedirs(UPLOAD_DIR, exist_ok=True) ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False) OCR_LOCK = threading.Lock() TASKS = {} def run_task(task_id: str, image_path: str, book_id: str, page_no: int): TASKS[task_id]["status"] = "running" try: with OCR_LOCK: result = ocr.ocr(image_path, cls=True) blocks = [ {"content": item[1][0], "confidence": float(item[1][1])} for item in result ] insert_blocks(book_id, page_no, blocks) TASKS[task_id]["status"] = "done" except Exception as exc: TASKS[task_id]["status"] = "failed" TASKS[task_id]["error"] = str(exc) @app.route("/upload", methods=["POST"]) def upload(): file = request.files.get("file") if file is None: return jsonify({"error": "no file"}), 400 task_id = uuid.uuid4().hex save_path = os.path.join(UPLOAD_DIR, f"{task_id}_{file.filename}") file.save(save_path) book_id = request.form.get("book_id", "unknown") page_no = int(request.form.get("page_no", "0")) TASKS[task_id] = {"status": "pending", "image_path": save_path} thread = threading.Thread( target=run_task, args=(task_id, save_path, book_id, page_no), daemon=True, ) thread.start() return jsonify({"task_id": task_id, "status": "pending"}), 202 @app.route("/task/<task_id>") def task_status(task_id: str): task = TASKS.get(task_id) if task is None: return jsonify({"error": "task not found"}), 404 return jsonify({"task_id": task_id, "status": task["status"]}) if __name__ == "__main__": app.run(debug=False, port=5000)这段代码有几个关键点。OCR_LOCK这个全局锁很必要,因为 PaddleOCR 的ocr.ocr()调用不是线程安全的,多个线程同时识别会互相干扰甚至报错;加了锁之后并发识别能力归零,但换来的是稳定。想要真正并发,就得按线程各建一个 OCR 实例,机器内存够就上,CPU 不够还是会排队。TASKS字典存在进程内存里,只适用于单进程部署;用多 worker 启动时,请求可能落到不同 worker,TASKS互相不可见,状态查询会 404,所以生产环境必须换成 Redis 存任务状态。threading.Thread(daemon=True)用守护线程执行识别,主进程退出线程一起结束;识别中如果进程被重启,任务会丢,这也是要接持久化队列的原因。
4.3 Flask 部署前要改的默认配置:不只是 debug=False
本地调试没问题,到部署环节有三个默认配置必须动。第一个是MAX_CONTENT_LENGTH,Flask 默认不限制上传大小,不设上限会被大文件拖垮,按书页图片规格设成 16MB 或 32MB 即可,超过上限的请求会直接返回 413。第二个是debug,生产必须debug=False,调试器会泄露代码与调用栈信息。第三个是不要用 Flask 自带的app.run()顶在线服务,那个内置 server 性能差且不稳定。常见做法是用 waitress 启动:from waitress import serve; serve(app, host="0.0.0.0", port=5000),Windows 和 Linux 都能跑,部署省心。
上传目录和数据库连接串也要提前规划。uploads目录不能放在程序目录里,否则重启或更新代码会被清掉,我一般写成绝对路径,通过环境变量注入。数据库连接串同样走环境变量,不要把密码写进源码。还有一点容易被忽略:MySQL 的max_allowed_packet默认值只有 4MB,传大图时插入 TEXT 字段可能会报 packet too large,需要调大这个参数,或者限制单张上传图片的大小。
5. 书籍文本识别避坑指南:六个现场与对应修复手段
5.1 识别结果全乱码,英文字母和数字都错
现象:识别出的中文全是无关字符,英文单词字母错乱。原因:最常见的是没装对应语言模型。Tesseract 默认只带英文,识别中文需要额外下载chi_sim.traineddata放进tessdata目录;PaddleOCR 如果lang写错或首次自动下载模型失败,也会输出垃圾文本。解决:确认引擎的语言包路径,初始化时显式设置lang="ch",第一次跑通前不要开show_log=False,让日志把模型加载情况打出来。
5.2 中文漏字严重,长段文本丢了一半
现象:短标题识别正常,长段落漏行、漏字。原因:输入图片分辨率不够。手机拍的书籍照片常见,文字在低分辨率下连成一片,检测框框不住完整的文字块,后面的识别自然残缺。解决:预处理阶段把图片放大到宽度不低于 2000 像素,或扫描时直接用 300 DPI。识别前先肉眼确认文字边缘清晰,再进模型。这一步的性价比远高于调模型参数。
5.3 代码块和公式识别率特别低
现象:技术书里代码和正文混排,代码部分的缩进、括号、下划线全被识别成奇怪的字,公式完全是乱码。原因:OCR 模型对印刷体中文和英文效果好,但对代码的等宽字体、下划线、大括号、数学符号支持很差。解决:版面分析阶段把代码块切出来单独处理,代码块要么放弃自动识别交给人工,要么用专门的代码 OCR 方案;公式部分建议直接留空并标记need_check=1,不要让错误文本进库污染检索结果。
5.4 数据库里读出的中文全变成了问号
现象:MySQL 表里能查到数据,但字段值是?????。原因:建库或建表字符集不是utf8mb4,或者连接串没带charset=utf8mb4。MySQL 默认的latin1存不了中文,写入时就转成了问号。解决:建表语句按 3.1 节写;连接参数显式加charset="utf8mb4";已经建错的库可以用ALTER TABLE book_text CONVERT TO CHARACTER SET utf8mb4;修复。注意这条语句只改表结构,已入库的乱码问号救不回来,所以在一开始建库就做对才是真正的后悔药。
5.5 Flask 接口一调就超时,前端一直转圈
现象:上传图片后请求十几秒不返回,前端等不到结果。原因:识别是 CPU 密集型计算,同步跑在 Flask 视图里会把 worker 占住,并发一高整个服务不可用。解决:按 4.2 节改成任务队列加状态轮询。如果已经用了线程仍慢,看 CPU 和内存是否打满,PaddleOCR 在 CPU 机器上单页耗时通常在 3 到 10 秒,属于正常水平;要提速得换 GPU 推理,或者换更轻量的识别模型。
5.6 双栏或多栏版面识别顺序错乱
现象:A 栏第二行排到了 B 栏第一行前面,整体文本没法读。原因:PaddleOCR 输出的文字块顺序按检测框遍历顺序,不是按阅读顺序。解决:按 2.3 节的sort_blocks函数做后处理排序,先按块中心 y 分行,再组内按 x 排序。如果是中文竖排古籍,排序规则要反过来,按 x 从右往左;这时use_angle_cls=True的方向分类器能帮上忙,但最终输出顺序仍要靠自己整理。
6. 进阶:用置信度量化识别质量,别让脏文本悄悄入库
识别服务上线后,用户最常问的问题就是:你凭什么说识别得准?单看几页样本没有说服力,得有一个可复用的量化手段。PaddleOCR 为每个文字块返回置信度,这就是现成的质量信号。我习惯的做法是先定一个阈值,比如 0.85,低于阈值的文本块不直接入库,而是打上need_check=1标记,之后用脚本把它抽出来交给人工校对。
# checker.py from sqlalchemy import create_engine, text engine = create_engine( "mysql+pymysql://root:your_password@127.0.0.1:3306/book_ocr?charset=utf8mb4" ) with engine.connect() as conn: conn.execute(text( "UPDATE book_text SET need_check = " "CASE WHEN confidence < 0.85 THEN 1 ELSE 0 END" )) rows = conn.execute(text( "SELECT id, page_no, block_no, content, confidence " "FROM book_text WHERE need_check = 1 " "ORDER BY confidence ASC LIMIT 200" )).fetchall()阈值不要拍脑袋定。我会先随机抽 30 个文字块,把置信度和人工校对结果放在一起对比,看看这个阈值以下到底有多少比例是错字,再决定用 0.8 还是 0.9。有些扫描件质量差,阈值定高了会抽出几百个待校对块,这是正常现象,说明源头图像质量需要提升,而不是模型有问题。另一个有价值的验证手段是抽检脚本:随机取每页某个文字块,把识别文本和原图区域拼成对比图,人眼扫一遍就能判断这页是否达到交付标准。
这个习惯我做了很久:每次全量识别前先跑 30 页样本,算平均置信度和错字率;错字率超过千分之一就回头调分辨率或换预处理策略,而不是盲目重跑全量。置信度只是代理指标,真正要紧的是最终错字率,两者结合才能把质量关把住。等你把这条“图片入库、置信度打标、按标记校对”的链路跑顺,书籍内容文本识别就不再是黑匣子,而是可量化、可复盘、可持续优化的工具链了。希望这些方案和踩过的坑能帮到你,让书籍数字化的路走得稳一点。
本文还有配套的精品资源,点击获取