OvisOCR2 输出段落顺序和原文对不上 / 问题出在版面还是图片质量?

文章导读
段落顺序和原文对不上时,先别急着调模型参数:判断的依据是同一页在只改图像条件时,顺序结果会不会跟着变。如果换成更清晰、更干净、切边更规整的图后乱序位置改变,问题偏向图像质量;如果顺序错乱的位置几乎不动,只是随版面复杂度出现,问题偏向版面阅读序推断。这两条线要分开跑,混在一起调通常越调越乱。
📋 目录
  1. 壹 固定一批有阅读序差异的样张
  2. 贰 写一个段落顺序对照脚本
  3. 叁 把图像质量变量单独跑一遍
  4. 肆 按版面类型归纳错乱触发条件
  5. 伍 给出降低错乱的输入预处理清单
A A

段落顺序和原文对不上时,先别急着调模型参数:判断的依据是同一页在只改图像条件时,顺序结果会不会跟着变。如果换成更清晰、更干净、切边更规整的图后乱序位置改变,问题偏向图像质量;如果顺序错乱的位置几乎不动,只是随版面复杂度出现,问题偏向版面阅读序推断。这两条线要分开跑,混在一起调通常越调越乱。

段落错乱大多先在版面上表现出来:双栏、侧栏注解、图文环绕三类页面最容易在阅读序上分叉;图像质量多数时候是放大器,把本来就脆弱的版面推向失败。可先固定一批人工标注过阅读序的样张,用对照脚本把错位打印成行号,再把分辨率、去噪分别作为单独变量各跑一遍。只有顺序结果随图像变量改变时,才把原因归到图片质量上;否则按版面类型处理。

固定一批有阅读序差异的样张

目的是让顺序问题可复现。从业务文档里挑出三类页面各若干页:等宽双栏(含跨栏标题)、正文加侧栏注解或边注、图片被文字环绕的图文混排页。每页同时保存原图和一份人工标注文件,标注内容就是原文段落按阅读顺序逐行抄写,一行一段,不要改写措辞、不要合并段落——标注文本必须能和解析输出逐段对上,否则后面的比对全是噪音。

目录建议这样放,方便脚本按编号配对:

samples/
  page_017.png        # 原图,保留原始扫描分辨率
  page_017.meta.txt   # 记录:页宽像素、栏数、DPI、版面类型
  gold/page_017.txt   # 人工标注的阅读顺序,一行一段

.meta.txt 里的页宽和栏数是后面归纳触犯条件的唯一依据,随手记一下比事后回看图片可靠。验证方式:随机抽两页,按标注顺序朗读一遍,确认标注本身没有跳段。

写一个段落顺序对照脚本

目的是把“看着乱”变成可打印的行号。脚本只依赖两样东西:解析输出的段落文本与段落坐标,以及人工标注的顺序。下面的骨架用占位字段名 blocks/text/bbox,请替换成你实际导出文件里的字段;如果没有坐标,把 bbox 留空即可,文本比对仍然有效。

import json, sys

def load_parsed(path):
    data = json.load(open(path, encoding="utf-8"))
    blocks = data.get("blocks") or data.get("paragraphs") or []
    out = []
    for i, b in enumerate(blocks):
        out.append({
            "idx": i,
            "text": (b.get("text") or "").strip(),
            "bbox": b.get("bbox"),   # [x0,y0,x1,y1],没有就为 None
        })
    return out

def load_gold(path):
    return [l.strip() for l in open(path, encoding="utf-8") if l.strip()]

def norm(s):
    return "".join(s.split())

def check(parsed, gold, head=10):
    pos = {}
    for b in parsed:
        pos.setdefault(norm(b["text"]), b["idx"])
    seq = []
    for ln, g in enumerate(gold, 1):
        key = norm(g)
        idx = pos.get(key)
        if idx is None:  # 退化为前缀包含匹配
            cand = [b["idx"] for b in parsed if key[:head] and key[:head] in norm(b["text"])]
            idx = cand[0] if len(cand) == 1 else None
        if idx is None:
            print(f"[未匹配] 标注第 {ln} 段:{g[:24]}")
        seq.append((ln, idx))
    prev = None
    for ln, idx in seq:
        if idx is None:
            continue
        if prev is not None and idx < prev:
            print(f"[乱序] 标注第 {ln} 段 → 解析第 {idx} 段(上一段为解析第 {prev} 段)")
        prev = idx
    return seq

if __name__ == "__main__":
    parsed = load_parsed(sys.argv[1])
    gold = load_gold(sys.argv[2])
    check(parsed, gold)

运行方式与验证:python check_order.py out/page_017.json samples/gold/page_017.txt。先用一页你确认顺序完全正确的单栏页跑一遍,输出应当只有未匹配提示、没有乱序行;这就是基线。之后每页的乱序行号集合,就是后面归因时比较的对象。注意脚本不判断对错原因,只报位置,别在脚本里加推断逻辑。

把图像质量变量单独跑一遍

目的是确认顺序结果对图像条件是否敏感。对同一页生成几个变体再跑,保持版面不变,只改分辨率与去噪:

OvisOCR2 输出段落顺序和原文对不上 / 问题出在版面还是图片质量?
python - <<'PY'
from PIL import Image, ImageFilter
src = Image.open("samples/page_017.png").convert("L")
w, h = src.size
for scale, name in [(1.0, "r100"), (0.6, "r60"), (0.4, "r40")]:
    src.resize((int(w*scale), int(h*scale))).save(f"tmp/page_017_{name}.png")
src.filter(ImageFilter.MedianFilter(3)).save("tmp/page_017_denoise.png")
PY
for v in r100 r60 r40 denoise; do
  python check_order.py out/page_017_$v.json samples/gold/page_017.txt > log/order_$v.log
done
diff log/order_r100.log log/order_denoise.log

判读规则:如果四个变体的乱序行号集合基本一致,说明顺序由版面结构决定,继续调图像意义不大;如果乱序行号随缩放变小而增加、去噪后减少,说明图像质量参与其中,优先从输入侧解决。需要提醒的是,插值放大不会把丢失的笔画补回来,低分辨率样张若要验证,建议重新扫描一版,而不是把现有小图拉大。每次记录变化前后的差异时,至少写清“哪个变体、乱序行号、未匹配行数”三项,方便回看。

按版面类型归纳错乱触发条件

把每页的 .meta.txt 和对应 order 日志并排看,按版面类型记录“从什么页宽、什么栏数开始出现乱序”。下面是按机理整理的观察方向,具体页宽与栏数阈值需要你用自己样张的结果填入最后一列,不要照抄别人的数字。

版面类型典型错乱表现开始出现错乱的条件你的样张编号
单栏纯文本顺序基本正确,偶发页眉页脚被插入正文首段通常需要页宽过窄导致行被拆段时才出现
等宽双栏 + 跨栏标题右栏段落被接在左栏中部,或跨栏标题下文顺序颠倒栏间空白接近字间距、或标题块与正文块纵坐标重叠时
侧栏注解 / 边注边注被插进正文段落中间,正文被切碎边注宽度明显小于正文行宽、且与正文纵向有交叠时
图文环绕环绕块上下两段正文顺序互换图片与正文块纵坐标区间重叠时,按坐标排序会被图片截断
表格 / 表单混排表格单元格文字被当段落展开,挤占前后段位置存在无框线表格、或单元格文字成行出现时

归纳完再回看脚本的乱序行号:同一类版面反复出现在相同行号位置,就说明这是该类版面的固定触发点,可以在流程里提前规避(例如先按栏切图再分别解析,再按栏序拼接)。这种切栏处理属于有损策略,需要再跑一次对照脚本,确认乱序行号减少且没有引入新的未匹配行,才考虑固化。

给出降低错乱的输入预处理清单

三项预处理不要一次全上,逐项加、逐项验,否则又分不清是哪一步起了作用。

  • 扫描分辨率:正文建议不低于 300dpi,判断标准是单字高度在图上不低于约 20 像素;低于这个量级优先重新扫描或联系来源方出原图,不要用插值放大冒充。验证方式:同一页对比原图与重扫图跑出的 order 日志,乱序行号应减少或持平。
  • 去噪:先转灰度,再做轻度中值去斑或去斑滤镜,避免强二值化——细笔画和侧栏小字最先被吃掉。验证方式:跑 denoise 版本与 r100 版本的 order 日志做 diff,如果未匹配行数上升,说明去噪过头,回调参数。
  • 切边:去掉扫描黑边与装订阴影,但保留页眉、页脚和页码区域不裁——切掉它们反而可能让正文首段的纵坐标判断更乱;侧栏注解区域也不能裁。验证方式:切边后再跑一次对照脚本,确认乱序行号集合没有新增,再进入正式解析流程。

这三项做完,把最终版本的解析输出与固定样张的标注再对一次;如果乱序仍然集中在双栏和侧栏页,就不要再往图像质量上找了,按上表的触发条件处理版面。