HyOCR-1.5 读图报错时 / 先确认预处理链路有没有改坏图

文章导读
同一批图片里有的能识别、有的直接报错,先不要急着把问题归到 HyOCR-1.5 的模型能力上。读图报错通常落在三段里:输入图片本身的格式与通道、预处理链路(解码、缩放、旋转、格式转换)、以及调用时传的参数。最快的排查顺序是抽一张报错图做最小复现,再把预处理每一步的中间图落盘,和同批能跑通的样本逐项比对。多数“有的行有的不行”的情况,能在预处理环节看到图片被改了通道数、被二次有损压缩、被自动旋转,或
📋 目录
  1. Ⅰ 把报错样本单独抽出来,做成最小复现目录
  2. Ⅱ 在预处理每一步后落盘中间图,确认哪一步改了数据
  3. Ⅲ 校验输入格式与色彩通道,和能跑通的样本对比
  4. Ⅳ 在调用处加输入校验与异常兜底,避免整批中断
  5. Ⅴ 重跑原批次,确认报错样本被隔离而不是被吞掉
A A

同一批图片里有的能识别、有的直接报错,先不要急着把问题归到 HyOCR-1.5 的模型能力上。读图报错通常落在三段里:输入图片本身的格式与通道、预处理链路(解码、缩放、旋转、格式转换)、以及调用时传的参数。最快的排查顺序是抽一张报错图做最小复现,再把预处理每一步的中间图落盘,和同批能跑通的样本逐项比对。多数“有的行有的不行”的情况,能在预处理环节看到图片被改了通道数、被二次有损压缩、被自动旋转,或者被转成了模型不接受的位深与色彩模式。

判断方向:先确认预处理链路有没有把图改坏,再讨论模型或参数。操作上抽单张报错图建最小复现目录,预处理每步落盘中间图,用格式、通道、位深、EXIF 方向与同批正常样本对比。验证方式是比较中间图与原始图的元信息和肉眼观感。边界:如果原始图不经任何预处理直接送模型仍报错,问题在输入形态或调用参数,不在预处理;异常兜底只能隔离坏图,不能代替修复。

把报错样本单独抽出来,做成最小复现目录

批量脚本里同时存在遍历、预处理、并发、重试等因素,直接跑整批很难判断报错来自哪一步。先把报错的那张图原样复制出来,不要用图片查看器另存,另存会改变编码和元信息。目录建议按下面组织,原始样本与中间产物分开,日志单独存放:

repro/
  input/
    bad_001.png      # 报错样本,原样拷贝
    good_001.jpg     # 同批能跑通的对照样本
  steps/             # 预处理各步中间图
  fails/             # 兜底逻辑捕获的失败样本
  logs/
    run.log
    traceback.txt
  run_repro.py

复现命令固定为单图、单进程、关闭重试,把标准输出和标准错误一起留存。下面的写法可以直接替换成你项目里的入口脚本:

python run_repro.py `--image` input/bad_001.png `--steps` steps/bad_001 2>&1 | tee logs/run.log
sha256sum input/bad_001.png input/good_001.jpg >> logs/run.log
python -c "from PIL import Image; im=Image.open('input/bad_001.png'); print(im.format, im.mode, im.size, im.info)" >> logs/run.log

报错文本要完整记录,不要只留最后一行。需要保留的内容包括:完整 traceback、实际执行的命令行、输入文件的 sha256、图片的 format/mode/size 与 info 字段(EXIF、ICC、transparency 等)、以及 HyOCR-1.5 运行环境的版本信息。同一张图重复跑两次,如果报错位置一致,基本可以排除偶发的 IO 或并发问题。

HyOCR-1.5 读图报错时 / 先确认预处理链路有没有改坏图

在预处理每一步后落盘中间图,确认哪一步改了数据

预处理通常是解码、缩放、旋转、通道转换、格式保存几段的串联,报错往往发生在最后一步才知道,但根因在前面。做法是在每段之后把当前图像落盘为 PNG,PNG 无损,能避免因保存 JPEG 再次引入压缩差异。下面是通用骨架,函数名按你项目的实际接口替换:

import os
from PIL import Image

def dump(tag, img, outdir):
    os.makedirs(outdir, exist_ok=True)
    name = f"{tag}_{img.width}x{img.height}_{img.mode}.png"
    img.save(os.path.join(outdir, name))
    print(tag, img.mode, img.size, img.info.get("exif") is not None)

raw = Image.open(src)
dump("01_decode", raw, outdir)

img = raw.convert("RGB") if raw.mode not in ("RGB", "L") else raw.copy()
dump("02_channel", img, outdir)

img = img.resize((target_w, target_h), Image.BICUBIC)
dump("03_resize", img, outdir)

img = img.rotate(angle, expand=True)
dump("04_rotate", img, outdir)

命名规则建议固定为“序号_步骤_宽x高_模式.扩展名”,例如 02_channel_1240x1754_RGB.png。这样在目录里按名称排序就是执行顺序,肉眼和脚本都能快速定位。检查分两类:肉眼检查方向是否正确、是否被截断、透明区域是否变成黑底、文字边缘是否被过度锐化或糊化;元信息检查尺寸、模式、通道数、位深、EXIF Orientation 是否被消费掉、ICC 配置是否丢失、是否从 P 模式或 CMYK 转成了别的形态。两类检查都要做,只靠肉眼容易漏掉通道与方向问题。

校验输入格式与色彩通道,和能跑通的样本对比

当中间图看起来正常、报错仍然出现时,需要确认这张图的形态是不是模型不支持的输入。写一个小脚本把整批图的格式、模式、通道数、尺寸和异常标记列出来,正常样本与报错样本放在同一张表里对照:

HyOCR-1.5 读图报错时 / 先确认预处理链路有没有改坏图
from PIL import Image
import numpy as np, sys

for p in sys.argv[1:]:
    with Image.open(p) as im:
        arr = np.asarray(im)
        print(p, im.format, im.mode, im.size,
              arr.shape, arr.dtype,
              im.info.get("exif") is not None,
              "transparency" in im.info)

对比记录表按行记录样本,按列记录形态特征,便于看出差异集中在哪一列:

样本formatmode通道/位深EXIF 方向结果
bad_001PNGRGBA4ch/8bit有报错
good_001JPEGRGB3ch/8bit无正常

常见需要进入转换清单的样本包括:带 alpha 通道的 PNG、P 模式或含 transparency 的图、CMYK 的 JPEG、16 位灰度、带 EXIF 方向的手机照片、扩展名与实际编码不一致的文件。整理出清单后统一走同一条转换规则,而不是在调用处零散修补。

在调用处加输入校验与异常兜底,避免整批中断

确认坏图形态之后,调用处需要做两件事:进入 HyOCR-1.5 之前先校验,失败时捕获并隔离。下面的骨架是通用写法,OCR 函数替换成实际调用即可:

HyOCR-1.5 读图报错时 / 先确认预处理链路有没有改坏图
import os, shutil, traceback

ALLOWED = {"RGB", "L"}

def check_image(path):
    from PIL import Image
    with Image.open(path) as im:
        im.verify()
    with Image.open(path) as im:
        if im.mode not in ALLOWED:
            im = im.convert("RGB")
        return im.copy()

fails = []
for path in batch:
    try:
        img = check_image(path)
        result = ocr(img)          # 替换为 HyOCR-1.5 调用
    except Exception as e:
        dst = os.path.join("fails", os.path.basename(path))
        os.makedirs("fails", exist_ok=True)
        shutil.copy(path, dst)
        fails.append((path, type(e).__name__, str(e)[:200]))
        continue

失败样本统一落到 fails/,并单独写一份失败清单,记录路径、异常类型和异常首行。跳过还是重试要看原因:解码失败、格式或通道不支持,属于确定性问题,直接跳过并记录,重试通常没有意义;文件读取超时、临时占用这类问题可以有限次重试,重试次数要封顶并记录每次尝试。不要在异常分支里静默 pass,那会让坏图在结果里无声消失。

重跑原批次,确认报错样本被隔离而不是被吞掉

兜底加上之后必须重跑原批次验证。命令与正式任务保持一致,只把输出目录指向验证用的位置,避免覆盖既有结果:

python run_batch.py `--input` batch/ `--out` out_check/ 2>&1 | tee logs/rerun.log
wc -l logs/fails.tsv

核对方式做数量对账:输入文件总数应等于成功数、失败数、跳过数之和,差额为 0 才算没有静默丢失。再逐条打开 fails.tsv,确认之前的 bad_001 出现在失败清单里,而不是出现在成功结果里,也不是两边都没有。日志里需要保留的字段至少包括:时间、输入绝对路径、sha256、预处理步骤名、异常类型、traceback 首行、处理耗时、最终状态。保留这些字段,下一次再遇到同类报错时,可以直接从日志判断是同一类形态问题还是新问题。