设计实践

Ideogram:用 Tesseract OCR 检查文字海报错字

文字海报最怕出现错字。这篇用本地的 Tesseract(chisim)把每张生成图里的文字识别出来,再和期望的文字比对:完全一致记为 PASS,相似但有错字或漏字记为 FIX,读不出来记为 FAIL,结果写进 ocrreport.csv。 识别前会先做灰度、放大 2 倍和 Otsu 二值化,再交给 Tesseract。

BINBHO · 图文教程

文字海报最怕出现错字。这篇用本地的 Tesseract(chi_sim)把每张生成图里的文字识别出来,再和期望的文字比对:完全一致记为 PASS,相似但有错字或漏字记为 FIX,读不出来记为 FAIL,结果写进 ocr_report.csv。 识别前会先做灰度、放大 2 倍和 Otsu 二值化,再交给 Tesseract。

先看清成品(box 上真实运行了 OCR,海报由模拟服务器生成)

  • 4 张图里 3 张 PASS。
  • seed 42 那张被模拟器故意画错了一个字(“伙日夜市”),被正确标成 FIX。
  • 测试 3 项通过。

合规提醒:IDEOGRAM_API_KEY 只放在服务器端。海报里的文字、品牌和人物要有使用权:不要仿冒他人商标或冒充真人;需要时可以开启 enable_copyright_detection,被标记的图会返回 is_image_safe: false。在中国境内公开发布生成内容,要遵守《生成式人工智能服务管理暂行办法》,并按《人工智能生成合成内容标识办法》加上“AI 生成”标识。Ideogram API 按次计费,本文不列价格;需要价格时用 dry_run=true 实时询价,示例中的金额一律写作 x.xx。

运行说明:box 上没有 Ideogram API Key,所以没有真正生成过图片。生成流程跑在我自己写的模拟服务器 fake_ideogram.py 上,图片是程序画的文字海报。用错误的 Key 请求真实接口确实运行过,返回 401 {"message": "Access denied. Please verify your API Token is valid."}。

做完之后你会得到什么?

成果 说明
ocr_check.py 预处理 + OCR + 判定,写出报告
test_ocr.py 判定规则、真实 OCR、彩色背景必须预处理
fixture_seed41.png 测试样图(模拟器生成)
ideo_poster.py、fake_ideogram.py 从 A 篇复制过来

需要准备哪些环境?

项目 要求
Tesseract box 上装的是 5.5.0:sudo apt install tesseract-ocr tesseract-ocr-chi-sim
Python pip install pillow pytest(不需要 numpy)

文件结构

50-ideogram-B-src/
├── ocr_check.py
├── test_ocr.py
├── fixture_seed41.png
├── ideo_poster.py
└── fake_ideogram.py

第 1 步:代码

"""海报文字校对:用本地 Tesseract(chi_sim)识别每张图里的文字,和期望文字比对。
完全一致记为 PASS,相似但不一致(错字、漏字)记为 FIX,读不出来记为 FAIL,结果写进 ocr_report.csv。
OCR 本身也会出错,所以它只用来初筛:PASS 的图仍要人工看一眼,FIX 的图优先人工复核。"""
import csv, difflib, json, pathlib, re, subprocess, sys, tempfile
from PIL import Image, ImageOps


def otsu(gray):
    """不依赖 numpy 的 Otsu 阈值:按灰度直方图找类间方差最大的分界。"""
    hist = gray.histogram(); total = sum(hist); s_all = sum(i * h for i, h in enumerate(hist))
    best, th, w0, s0 = 0, 128, 0, 0
    for t in range(256):
        w0 += hist[t]; s0 += t * hist[t]
        if w0 == 0 or w0 == total: continue
        m0, m1 = s0 / w0, (s_all - s0) / (total - w0)
        v = w0 * (total - w0) * (m0 - m1) ** 2
        if v > best: best, th = v, t
    return th


def preprocess(path):
    """转灰度 → 放大 2 倍 → Otsu 二值化,统一成白底黑字。彩色背景直接喂给 Tesseract 容易多认出笔画。"""
    g = ImageOps.grayscale(Image.open(path))
    g = g.resize((g.width * 2, g.height * 2), Image.LANCZOS)
    th = otsu(g)
    bw = g.point(lambda v: 255 if v > th else 0)
    if sum(bw.histogram()[:128]) > bw.width * bw.height / 2:   # 黑色占多数说明是深底浅字,反相
        bw = ImageOps.invert(bw)
    f = tempfile.NamedTemporaryFile(suffix=".png", delete=False); bw.save(f.name)
    return f.name


def ocr(path, lang="chi_sim", raw=False):
    src = str(path) if raw else preprocess(path)
    out = subprocess.run(["tesseract", src, "-", "-l", lang, "--psm", "6"],
                         capture_output=True, text=True, check=True).stdout
    return re.sub(r"\s+", "", out)


def judge(expected, got):
    if expected in got:
        return "PASS", 1.0
    ratio = difflib.SequenceMatcher(None, expected, got).ratio()
    return ("FIX" if ratio >= 0.5 else "FAIL"), round(ratio, 2)


def main(meta_path, expected):
    meta = json.loads(pathlib.Path(meta_path).read_text())
    rows = []
    for im in meta["images"]:
        if "file" not in im:
            rows.append([im["seed"], "", "", "SKIP", 0]); continue
        got = ocr(im["file"])
        verdict, score = judge(expected, got)
        rows.append([im["seed"], im["file"], got, verdict, score])
        print(f"{verdict:4} seed={im['seed']:<4} 相似度={score:<4} 识别结果:{got}")
    out = pathlib.Path(meta_path).with_name("ocr_report.csv")
    with open(out, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f); w.writerow(["seed", "file", "ocr", "verdict", "score"]); w.writerows(rows)
    best = [r for r in rows if r[3] == "PASS"]
    print(f"通过 {len(best)}/{len(rows)},报告:{out}")
    return rows


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

第 2 步:测试

import shutil, pytest
from PIL import Image, ImageDraw, ImageFont
from ocr_check import judge, ocr

FONT = "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc"


def test_judge():
    assert judge("秋日夜市", "秋日夜市") == ("PASS", 1.0)
    assert judge("秋日夜市", "伙日夜市")[0] == "FIX"
    assert judge("秋日夜市", "")[0] == "FAIL"


@pytest.mark.skipif(not shutil.which("tesseract"), reason="未安装 tesseract")
def test_ocr_real(tmp_path):
    im = Image.new("RGB", (600, 300), "white")
    ImageDraw.Draw(im).text((60, 90), "秋日夜市", fill="black", font=ImageFont.truetype(FONT, 110))
    im.save(tmp_path / "a.png")
    assert "秋日夜市" in ocr(tmp_path / "a.png")


@pytest.mark.skipif(not shutil.which("tesseract"), reason="未安装 tesseract")
def test_colored_background_needs_preprocess():
    # fixture_seed41.png 是模拟服务器画的彩色底海报;不预处理时本文实测识别成“秋上日夜市”
    assert ocr("fixture_seed41.png") == "秋日夜市"
    assert ocr("fixture_seed41.png", raw=True) != "秋日夜市"

第 3 步:运行

$ python ideo_poster.py '复古风格海报,正中央醒目的大字标题“秋日夜市”,暖橙色调' --resolution 1024x1024 -n 4 --seed 41 --yes
…
$ python ocr_check.py outputs/meta.json 秋日夜市
PASS seed=41   相似度=1.0  识别结果:秋日夜市
FIX  seed=42   相似度=0.75 识别结果:伙日夜市
PASS seed=43   相似度=1.0  识别结果:秋日夜市
PASS seed=44   相似度=1.0  识别结果:秋日夜市
通过 3/4,报告:outputs/ocr_report.csv

$ python -m pytest -q
3 passed in 0.39s

检查点:FIX 的那张图要人工看一眼,确认确实是错字,而不是 OCR 认错了。

调试示例

一开始 4 张图全部是 FIX。 第一版直接把原图交给 Tesseract,4 张图的识别结果都是“秋上日夜市”,凭空多出一个“上”字。问题出在彩色背景上:笔画边缘的抗锯齿像素被当成了额外的笔画。加上灰度、放大 2 倍和 Otsu 二值化(按灰度直方图自动找阈值)之后,3 张图正确识别为 PASS,真正的错字图仍然是 FIX。test_colored_background_needs_preprocess 把这两个结果都固定了下来:不预处理时识别不对,预处理后识别正确。

常见问题

现象 原因 处理
报错 Failed loading language 'chi_sim' 没装中文语言包 安装 tesseract-ocr-chi-sim
艺术字识别不出来 字体太花哨,或文字有透视变形 放宽阈值、人工复核;正式交付前必须有人看过
繁体字识别不对 用的是简体语言包 加 -l chi_tra
深色背景识别全错 底色和文字颜色反了 代码里已经自动反相

模板规则

  • OCR 只用来初筛,最终由人工确认。
  • 识别前先预处理,规则写进测试里。
  • 报告中保留原始识别结果和相似度,便于回查。

项目资源

用途 地址
Ideogram 4.0 生成接口 https://developer.ideogram.ai/v2/api-reference/images/generate/ideogram-4
Ideogram API 文档索引(llms.txt) https://developer.ideogram.ai/v2/llms.txt
Tesseract 项目 https://github.com/tesseract-ocr/tesseract
Tesseract 使用文档 https://tesseract-ocr.github.io/tessdoc/

把错字筛掉之后,剩下的图再交给设计师做最后挑选。