手写批注识别成乱码——Unlimited OCR 2 该换输入还是换切分方式?

文章导读
遇到手写批注被识别成乱码、还混进正文的情况,先换模型或调高分辨率并不是最可靠的入手点,更值得先做的是:找出乱码在输出里的位置。Unlimited OCR 2 这类工具通常会给每个文本块或每行返回坐标,把乱码块坐标画回原图,再和手写笔迹、印章位置对照,就能判断问题出在哪一侧。乱码只压在手写区上,说明手写区本身被当成正文送了进去,优先试按区块切分或跳过;乱码框落到印刷正文行上、或者整页输出顺序明显错乱
📋 目录
  1. 一 把手写区域和印刷区域在输出里分别标出来
  2. 二 同一页换二值化与去噪处理后对比手写区输出
  3. 三 按区块切分后再识别,看乱码是否还影响正文
  4. 四 写出分区处理清单的脚本骨架
  5. 五 合并结果时保留原始坐标做人工抽查
A A

遇到手写批注被识别成乱码、还混进正文的情况,先换模型或调高分辨率并不是最可靠的入手点,更值得先做的是:找出乱码在输出里的位置。Unlimited OCR 2 这类工具通常会给每个文本块或每行返回坐标,把乱码块坐标画回原图,再和手写笔迹、印章位置对照,就能判断问题出在哪一侧。乱码只压在手写区上,说明手写区本身被当成正文送了进去,优先试按区块切分或跳过;乱码框落到印刷正文行上、或者整页输出顺序明显错乱,则更像是二值化和去噪把笔迹、印章边缘连成了文字笔画,应该先把输入处理链固定下来。两条路都要在同一页样张上单独跑,一次只改一个变量。

乱码只出现在手写、印章位置时,换输入分辨率通常只是让乱码更清晰,先试按区块切分更划算;乱码混进相邻印刷正文时,应先查二值化与去噪是否把笔迹连到了正文行,再谈切分粒度。两种判断都要求输出带坐标、能回溯原图,并且同页单变量对照;不能只拿整批识别率这类无法定位的指标下结论。

把手写区域和印刷区域在输出里分别标出来

这一步只做定位,不改任何参数。目标是把「哪几个乱码块落在手写区」变成一张可核对的表,而不是靠肉眼看截图。

  • 从识别结果里取出每个文本块或文本行的框(常见是 x、y、宽、高,或左上右下两个点),连同该块识别出的文本、页码、块序号一起记下来。
  • 用同一个脚本把框画回原图,输出一张带框的对照图,手写批注和印章位置一眼可辨。
  • 坐标要区分是相对原图,还是相对缩放、裁剪后的图像;如果识别前做过缩放或去边,需要换算回原图坐标,这一步要结合当前环境确认,不能默认两者一致。
  • 记录字段建议固定为:页码、块序号、框坐标、识别文本、是否含乱码字符、框是否与手写区重叠、框是否压在印刷正文行上。
# 字段名以实际仓库或接口返回为准,这里只是记录结构示例
for page in pages:
    for blk in page.blocks:
        print(page.id, blk.id, blk.bbox, repr(blk.text),
              has_garbage(blk.text), overlap(blk.bbox, page.handwriting_boxes))

判断标准保持简单:乱码块基本都落在手写或印章框内,且印刷正文行的文本干净,就走切分方向;乱码块跨到手写区外、或者正文行内部出现乱码字符,就先回到预处理方向。两种情况都存在时,先处理跨区那一类,它更容易污染整页输出。

同一页换二值化与去噪处理后对比手写区输出

这一轮验证的是「输入图像本身是否被处理坏了」。固定项要写清楚:同一页、同一模型或同一接口版本、同一语言与字典设置、同一切分粒度(整页送入)、同一输出格式。只改预处理项,每次只动一个。

手写批注识别成乱码——Unlimited OCR 2 该换输入还是换切分方式?
  • 可对比的项通常包括:直接灰度送入、不同二值化方式与阈值、轻度中值或形态学去噪、按颜色通道压制印章、去底色与去阴影。
  • 观察两处,而不是只观察整体:手写区自身的文本块数量与内容有没有变化,相邻印刷正文区有没有新增块、丢块或字符被替换。
  • 特别留意乱码位置是否发生迁移。原本在手写区的乱码跑到正文行,说明预处理改变了版面结构,这类改动风险更高。

不要在这轮里直接抄别人给的阈值或核大小,不同扫描件的底色、笔迹浓度差别很大,参数需要结合样张自己看。对照结果可以按下面的表记:

  • 预处理项 | 手写区块数与文本 | 相邻正文区是否新增乱码 | 是否出现丢行
  • 结论只写「手写区乱码减少但正文丢行」这类可复核的描述,不写提升幅度。

按区块切分后再识别,看乱码是否还影响正文

如果第一步显示乱码集中在手写区,这一轮就验证切分粒度是否比调预处理更有效。做法是把同一页跑两遍:一遍整页送入,一遍先做版面区块检测,把印刷正文区单独切出后送入,手写区与印章区按需要跳过或单独识别。

手写批注识别成乱码——Unlimited OCR 2 该换输入还是换切分方式?
  • 核心观察点只有一个:正文输出里是否仍有乱码插入,以及原有正文行的顺序、行数是否保持。
  • 切分本身会带来新问题,例如一行正文被切成两半、上下标丢失、跨栏内容错序;所以切块之间建议留少量重叠并在合并时去重,重叠量需要按页面版式试。
  • 手写区单独识别时,输出应挂在独立字段上,不要和正文结果混在同一个文本流里,否则乱码仍会串进正文。

两遍结果都留档,对比同一段正文的文本差异,比只看「有没有乱码」更好判断切分是否带来副作用。

写出分区处理清单的脚本骨架

与其在调用处临时判断,不如给每页生成一份处理清单:先判定页面类型,再列区块与各自的路由,最后按清单执行。下面的函数名和字段都是占位,具体接口以实际仓库为准。

# 通用骨架:页面判定 -> 区块清单 -> 分支处理
def build_page_plan(page):
    plan = {'page_id': page.id, 'offset': (0, 0), 'route': 'whole_page', 'blocks': []}
    blocks = detect_blocks(page.image)  # 版面/区块检测,接口以实际仓库为准
    for b in blocks:
        kind = classify(b)  # printed / handwriting / stamp / table 等,按自己仓库的标签
        plan['blocks'].append({'id': b.id, 'bbox': b.bbox, 'kind': kind, 'route': 'ocr'})
    if any(b['kind'] in ('handwriting', 'stamp') for b in plan['blocks']):
        plan['route'] = 'split'
        for b in plan['blocks']:
            if b['kind'] in ('handwriting', 'stamp'):
                b['route'] = 'skip_or_separate'
    return plan

def run_page(page, plan):
    outs = []
    if plan['route'] == 'whole_page':
        outs += ocr(page.image, page_id=page.id, offset=(0, 0))
        return outs
    for b in plan['blocks']:
        if b['route'] != 'ocr':
            continue
        x0, y0, x1, y1 = b['bbox']
        crop = page.image.crop((x0, y0, x1, y1))
        outs += ocr(crop, page_id=page.id, offset=(x0, y0))
    return outs

清单本身建议落成 JSON 存盘,出问题时能直接看出这页走的是整页还是分区、哪些块被跳过。分支里不要写死阈值,把「是否含手写」的判定逻辑单独放一个函数,便于按批次复核。

手写批注识别成乱码——Unlimited OCR 2 该换输入还是换切分方式?

合并结果时保留原始坐标做人工抽查

分区处理最容易出的事故是错位和遗漏,而不是乱码。合并时每个输出块都要带三样东西:页码、在原图中的框坐标、裁剪时的偏移量。裁剪块识别出的坐标必须加上偏移量换算回整页坐标,再按纵向位置、横向位置排序,行间允许少量容差。

  • 排序后检查行数是否少于切块前的正文行数,少行基本可以判定为切分切断或合并去重过度。
  • 抽查位置选手写区与正文的交界带,例如批注正上方和正下方各一行、印章压到的正文行、页面边缘被裁到的行。
  • 核对方法:把带框对照图和合并后的文本按块序号逐条比对,看交界处的字有没有被截断、有没有重复出现两次、顺序是否颠倒。
  • 把抽查结论记在案,注明页码与块序号,便于后续同批样张复用同一套检查点。

如果交界处反复出现截断,通常说明切块边界压到了文字笔画,应先扩大切块边距或换用行级切分,再回头判断是否需要继续调整预处理。整条链路保持「先定位、再单变量对照、最后按清单执行」的顺序,换输入还是换切分方式就不再是猜的。