同一批图片里有的能识别、有的直接报错,先不要急着把问题归到 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 或并发问题。
在预处理每一步后落盘中间图,确认哪一步改了数据
预处理通常是解码、缩放、旋转、通道转换、格式保存几段的串联,报错往往发生在最后一步才知道,但根因在前面。做法是在每段之后把当前图像落盘为 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 转成了别的形态。两类检查都要做,只靠肉眼容易漏掉通道与方向问题。
校验输入格式与色彩通道,和能跑通的样本对比
当中间图看起来正常、报错仍然出现时,需要确认这张图的形态是不是模型不支持的输入。写一个小脚本把整批图的格式、模式、通道数、尺寸和异常标记列出来,正常样本与报错样本放在同一张表里对照:
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)
对比记录表按行记录样本,按列记录形态特征,便于看出差异集中在哪一列:
| 样本 | format | mode | 通道/位深 | EXIF 方向 | 结果 |
|---|---|---|---|---|---|
| bad_001 | PNG | RGBA | 4ch/8bit | 有 | 报错 |
| good_001 | JPEG | RGB | 3ch/8bit | 无 | 正常 |
常见需要进入转换清单的样本包括:带 alpha 通道的 PNG、P 模式或含 transparency 的图、CMYK 的 JPEG、16 位灰度、带 EXIF 方向的手机照片、扩展名与实际编码不一致的文件。整理出清单后统一走同一条转换规则,而不是在调用处零散修补。
在调用处加输入校验与异常兜底,避免整批中断
确认坏图形态之后,调用处需要做两件事:进入 HyOCR-1.5 之前先校验,失败时捕获并隔离。下面的骨架是通用写法,OCR 函数替换成实际调用即可:
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 首行、处理耗时、最终状态。保留这些字段,下一次再遇到同类报错时,可以直接从日志判断是同一类形态问题还是新问题。