【Agent Harness】AI 生成图片里的文字全是“鬼画符”?一文讲透错字根源与工业级解决方案
AI 生成图片里的文字全是“鬼画符”?一文讲透错字根源与工业级解决方案
摘要:AI 生成图片中的文字错乱(如“新年快乐”变“新年快东”)是文生图领域的常见痛点。本文深入剖析了错字的五大技术根源:文本编码器不关注字符级信息、图像生成损失不惩罚拼写错误、VAE 压缩丢失笔画细节、训练数据标注粗糙、布局与生成任务耦合过重。并提供了从“无字背景+排版引擎”到“字形条件局部编辑”再到“自研双编码器模型”的工业级解决方案,帮助开发者和设计师从架构上根治文字错乱问题。
关键词:AI 文生图;文字生成;错字;Stable Diffusion;Midjourney;DALL-E;文本编码器;VAE;OCR;Glyph-ByT5;AnyText;工业级方案
就像本文封面图一样,你让 AI 画一张“新年快乐”的海报,它给你输出“新年快东”;让它写 “SALE”,它给你写 “SLAE”。更离谱的是,同样的 Prompt 跑十次,可能错得各不相同。为什么 AI 画图就是写不对字?这事到底能不能根治?
一、现象:越想让 AI 写字,它越容易翻车
几乎所有用过 Midjourney、Stable Diffusion、DALL‑E 的文生图玩家都遇到过:
- 英文单词字母顺序颠倒(
COFFEE→COFFE) - 字母被吃掉(
WELCOME→WELCME) - 字母被替换(
SALE→SAL E或SALC) - 中文缺胳膊少腿(“福”变成“示”+“一口”)
- 多个文字区域互相串词
- 不存在的“幻觉文字”自动冒出来
- 同一 Prompt 跑多次,错误各不相同
最典型翻车现场:
生成一个店铺招牌,要求写 “FRESH COFFEE”,结果图里写的是 “FRES H COFFE” 或 “FRE5H COFFEE”。
很多人的第一反应是“换 Prompt”“加权重”“多抽几次卡”——但这只能碰运气。要根治,得先搞清楚底层原理。
二、核心结论:文生图模型不是打字机,是“视觉模仿者”
大多数文生图模型本质上是像素/视觉纹理生成器,不是文字排版引擎。
语言模型生成文字时,输出的是离散 token,错一个字母就会受到明确惩罚(交叉熵损失)。而图像模型生成文字时,输出的是连续的像素或图像 latent。对模型来说,“SALE”和“SLAE”在形状、颜色、位置和整体语义上可能已经足够相似,因此普通图像生成损失不会强烈惩罚这种拼写错误。
可以这样理解:
- 文字排版软件是在“打印一个确定的字符串”;
- 文生图模型是在“模仿一块看起来像有文字的招牌”。
因此,仅靠改 Prompt 很难从根本上解决。真正可靠的方案,是把文字内容、文字布局和视觉风格分开处理。
三、为什么错字屡禁不止?五大技术原因拆解
1. 文本编码器只看“语义”,不数“字母”
很多文生图模型使用 CLIP、T5 等文本编码器,把 Prompt 转换成向量。文本编码器主要表达的是“画面是什么意思”“有什么对象”“风格是什么”,而不是为每一个字母、汉字建立严格的一一对应关系。
例如:
A coffee shop sign saying "FRESH COFFEE"
模型可能很好地理解:
- 咖啡店
- 木质招牌
- “新鲜咖啡”这一语义
- 英文招牌的视觉样式
但它未必获得类似这样的硬约束:
字符1必须是 F
字符2必须是 R
字符3必须是 E
……
字符12必须是 E
证据:Glyph-ByT5 研究指出,常规 CLIP 更关注概念级视觉语义,而不是字符级信息及其与字形的对应关系,因此研究者专门引入了“字符感知、字形对齐”的编码器。
2. 图像生成损失 ≠ 拼写检查
典型扩散模型学习的是:
预测噪声 ≈ 真实噪声,或者让生成图像在视觉特征上接近训练图像。
但是,对文字而言:
- 少一横,可能从一个汉字变成另一个汉字;
E多一笔,可能变成类似B的形状;rn粘在一起,可能看起来像m;- 两个相邻字母交换,整体图案仍然很像。
这些错误在人类语言层面非常严重,但在像素距离、感知距离或图像语义距离上,差异可能很小。损失函数不会因此加大惩罚,模型自然懒得纠结。
3. Latent 空间压缩把笔画“磨没了”
许多文生图系统并不是直接在最终像素上生成,而是在经过 VAE 压缩的 latent 空间中生成。
例如,一张 1024×1024 的图片会先缩成 64×64 的特征图。普通物体能承受这种压缩:
- 一棵树少几片叶子,仍然是一棵树;
- 一张脸的纹理略有变化,仍然是一张脸。
文字却不能:
- 一个汉字可能只占几个 latent 单元;
- 短横、点、撇可能在压缩中消失;
- 相邻笔画可能合并;
- 解码时又可能被重新“猜”出来。
TextInVision 等分析明确指出,VAE 本身可能成为视觉文字生成的瓶颈。SceneVTG++ 则使用像素级去噪来保留小文字特征,避开 latent VAE 压缩造成的细节损失。
4. 训练数据中文字标注不够“细”
训练图像中的文字通常来自:
- Logo、路牌、海报、包装、水印
- 常见品牌、常见英文短词
但这些图片的 Caption 往往不会准确抄录图片上的全部文字。即使使用 OCR 自动标注,也会受到模糊、遮挡、艺术字体、倾斜和语言识别错误影响。
因此模型可能学会:
“咖啡店招牌应该长得像有英文单词”
而不是:
“无论给我什么字符串,我都能准确逐字符排版”
研究也印证了这点:模型生成常见单词(如 SALE)比生成随机字符串(如 X7KpQ)准确得多。这说明它更多依赖记忆而非真正的文字复制能力。
5. 布局与字符生成同时进行,任务太重
普通文生图模型要一次完成:
- 决定文字放在哪里
- 决定几行、字体、字号、透视
- 决定每个字符的准确形状
- 同时生成背景和其他对象
文字越多、区域越多,注意力越容易混乱,出现:
- 丢字、多字、字符顺序交换
- 两行文字互相串字
- 自动增加不存在的装饰文字
TextDiffuser 因此把任务拆成两个阶段:先规划文字布局和字符区域,再由扩散模型结合布局生成图像。
四、为什么中文、阿拉伯文更“难搞”?
中文的“地狱级”难度
- 字符规模大:英文 26 个字母,中文常用汉字几千个。
- 笔画密度高:小字号下,相同面积内要容纳更多结构。
- 形近字多:少一笔或多一笔就变另一个字(“日”与“曰”,“未”与“末”)。
- 字体差异大:宋体、黑体、楷体、书法体形态迥异。
- 训练数据不均衡:高质量中文 OCR 标注远少于英文。
FLUX-Text 等研究特别指出,中文、日文、韩文等非拉丁文字的复杂字形更容易发生笔画缺失和字形扭曲,细微错误也很容易被读者发现。
复杂文字体系(阿拉伯文、印度文、韩文)
- 阿拉伯文:字母根据位置改变形态,涉及连写和从右向左排版。
- 印度文:辅音+元音符号构成复杂组合字形。
- 韩文:需要正确组合初声、中声、终声。
- Emoji/组合字符:一个用户感知字符可能由多个 code point 组成。
Unicode 本身区分 code point、字素簇和用户感知字符。简单地把 Unicode 字符逐个作为普通 token 处理,远不能完成正确排版。
五、最可靠的解决原则:让专业的人做专业的事
不要让图像模型负责“写字”,让它负责“画背景和视觉效果”。
把文字内容、文字布局和视觉风格分开处理,可靠程度远高于“一次生成”。
| 使用场景 | 推荐方法 | 文字准确性 |
|---|---|---|
| 海报、Banner、封面、电商图 | 生成无字背景,再用 SVG/Canvas 排字 | 最高,确定性正确 |
| 图表、教学材料、长段落 | HTML/SVG/PDF 排版引擎 | 最高 |
| 照片中的路牌、包装、店招 | 字形约束的局部重绘 + OCR 校验 | 较高 |
| 艺术字、霓虹字、金属字 | 确定字形骨架 + AI 风格化 | 较高 |
| 直接一次性文生图并要求准确长文字 | 不推荐 | 不稳定 |
“先生成无字背景,再渲染文字”已成为视觉文字研究领域的明确共识:先创建 text-free background,再根据字形条件融合文字,控制更准确,实际可用性更强。
六、工业级落地的三套方案
方案 A:背景生成 + 确定性文字排版(最推荐)
适用于海报、广告、电商、社交媒体。
流程图:
关键数据设计:
文字内容必须作为独立结构化字段保存,不能混在 Prompt 里。
{
"scene_prompt": "夜晚的现代咖啡店门面,中央有一块空白木质招牌,不出现任何文字",
"text_items": [
{
"id": "sign_1",
"content": "城市咖啡",
"locale": "zh-Hans",
"position": [0.23, 0.16, 0.54, 0.14],
"alignment": "center",
"render_mode": "vector_then_blend"
}
]
}
规则:
content不允许被 LLM 改写或翻译;- 场景 Prompt 中只描述“空白招牌”,不让模型生成正式文字;
- 文字在最终输出尺寸上排版,不依赖生成模型超分;
- 保留 SVG/PSD 图层,便于后续修改。
Python 代码示例:方案 A 核心步骤实现
以下代码演示了使用 LLM 拆分需求、调用图像模型生成无字背景、再用 PIL 渲染精确文字的全流程。实际生产环境中需替换为真实的 LLM API 和图像模型 API。
import json
import requests
from PIL import Image, ImageDraw, ImageFont
from typing import Optional
# ============================================================
# 步骤 1:使用 LLM 拆分场景描述和文字内容
# ============================================================
def parse_user_request_with_llm(user_input: str) -> dict:
"""
模拟 LLM 将用户需求拆分为场景描述和文字项列表。
实际使用时请替换为真实的 LLM API 调用(如 OpenAI / 本地模型)。
"""
# 模拟 LLM 输出(实际应调用 LLM API)
mock_llm_output = {
"scene_prompt": "夜晚的现代咖啡店门面,中央有一块空白木质招牌,不出现任何文字",
"text_items": [
{
"id": "sign_1",
"content": "城市咖啡",
"locale": "zh-Hans",
"position": [0.23, 0.16, 0.54, 0.14], # [x, y, w, h] 归一化坐标
"alignment": "center",
"font_size_ratio": 0.08 # 字号相对于图片高度的比例
}
]
}
return mock_llm_output
# ============================================================
# 步骤 2:调用图像模型生成无字背景
# ============================================================
def generate_text_free_background(scene_prompt: str,
width: int = 1024,
height: int = 768) -> Optional[Image.Image]:
"""
调用图像模型生成不含文字的背景图。
实际使用时请替换为 Stable Diffusion / DALL-E / Midjourney API。
"""
# 模拟:生成一张纯色背景(实际应调用图像生成 API)
# 示例:requests.post("https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image", ...)
print(f"[步骤 2] 生成无字背景,Prompt: {scene_prompt}")
background = Image.new("RGB", (width, height), color=(30, 40, 60))
# 模拟一些渐变效果
draw = ImageDraw.Draw(background)
for y in range(height):
r = int(30 + 20 * (y / height))
g = int(40 + 30 * (y / height))
b = int(60 + 40 * (y / height))
draw.line([(0, y), (width, y)], fill=(r, g, b))
return background
# ============================================================
# 步骤 3:使用 PIL 将精确文字渲染到背景上
# ============================================================
def render_text_on_background(background: Image.Image,
text_items: list,
font_path: str = "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc") -> Image.Image:
"""
在背景图上渲染精确文字。文字内容由 LLM 解析得到,不经过图像模型。
注意:font_path 需替换为系统中实际的中文字体路径。
"""
img = background.copy()
draw = ImageDraw.Draw(img)
img_width, img_height = img.size
for item in text_items:
content = item["content"]
# 关键:content 不允许被 LLM 改写或翻译,直接使用原始字符串
x_norm, y_norm, w_norm, h_norm = item["position"]
alignment = item.get("alignment", "center")
font_size = int(img_height * item.get("font_size_ratio", 0.08))
# 计算文字区域的实际像素坐标
box_x = int(x_norm * img_width)
box_y = int(y_norm * img_height)
box_w = int(w_norm * img_width)
box_h = int(h_norm * img_height)
# 加载字体(生产环境应使用系统字体或自定义字体文件)
try:
font = ImageFont.truetype(font_path, font_size)
except IOError:
# 回退到默认字体(不推荐,中文可能显示为方块)
print(f"[警告] 未找到字体 {font_path},使用默认字体")
font = ImageFont.load_default()
# 计算文字尺寸,确定对齐位置
bbox = draw.textbbox((0, 0), content, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
if alignment == "center":
text_x = box_x + (box_w - text_width) // 2
elif alignment == "left":
text_x = box_x
elif alignment == "right":
text_x = box_x + box_w - text_width
else:
text_x = box_x
# 垂直居中
text_y = box_y + (box_h - text_height) // 2
# 绘制文字(先描边再填充,增强可读性)
# 描边
stroke_color = (0, 0, 0)
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
if dx != 0 or dy != 0:
draw.text((text_x + dx, text_y + dy), content,
font=font, fill=stroke_color)
# 填充
draw.text((text_x, text_y), content, font=font, fill=(255, 255, 200))
print(f"[步骤 3] 渲染文字 '{content}' 到位置 ({box_x}, {box_y}),字号 {font_size}")
return img
# ============================================================
# 主流程:串联三个步骤
# ============================================================
def main():
# 用户原始需求(不可被 LLM 改写)
user_request = "帮我生成一张咖啡店招牌海报,招牌上写'城市咖啡',夜晚风格"
# 步骤 1:LLM 拆分
print("=" * 60)
print("[步骤 1] LLM 拆分用户需求")
parsed = parse_user_request_with_llm(user_request)
scene_prompt = parsed["scene_prompt"]
text_items = parsed["text_items"]
print(f"场景 Prompt: {scene_prompt}")
print(f"文字项: {json.dumps(text_items, ensure_ascii=False, indent=2)}")
print()
# 步骤 2:生成无字背景
print("=" * 60)
background = generate_text_free_background(scene_prompt)
background.save("step2_background.png")
print("背景图已保存为 step2_background.png")
print()
# 步骤 3:渲染精确文字
print("=" * 60)
result = render_text_on_background(background, text_items)
result.save("step3_final_output.png")
print("最终输出已保存为 step3_final_output.png")
print()
print("=" * 60)
print("✅ 方案 A 核心流程完成!")
print("关键原则:文字内容由 LLM 解析后直接传给排版引擎,")
print(" 图像模型只负责生成无字背景,不参与文字生成。")
if __name__ == "__main__":
main()
代码要点说明:
- 文字内容不可变:
content字段由 LLM 从用户需求中解析,后续直接传给 PIL 渲染,不允许图像模型或 LLM 再次改写。 - 场景 Prompt 不含文字:
scene_prompt中明确要求“不出现任何文字”,避免图像模型生成错误文字。 - 确定性排版:使用 PIL 的
ImageFont.truetype加载系统字体,确保每个字符精确渲染,不受图像模型影响。 - 生产替换:实际使用时,
parse_user_request_with_llm应调用真实的 LLM API(如 GPT-4),generate_text_free_background应调用 Stable Diffusion / DALL-E 等图像生成 API。 - 字体路径:Linux 系统可安装
fonts-noto-cjk包获取中文字体;Windows/macOS 需替换为系统字体路径。
方案 B:确定字形 + AI 自然场景融合
适用于弯曲包装、皱褶衣服、倾斜路牌、金属蚀刻等。
流程:
专门视觉文字模型(如 AnyText、CharGen)正是向模型提供字形、位置、Mask、OCR 特征等条件,把任务从“猜字”降为“局部融合”。
方案 C:OCR 验收与闭环修复
任何仍然由生成模型参与文字生成的方案,都应该有机器验收。
校验流程:
常用指标:字符错误率(CER)
CER = (替换数 + 删除数 + 插入数) / 目标字符总数
生产要求:
- 品牌名、价格、日期、地址、型号:CER = 0;
- 所有目标文字精确匹配;
- 不得出现额外非预期文字;
- 中英混排用各自适合的 OCR;
- 关键内容使用两个 OCR 交叉验证;
- 高风险内容增加人工终审。
七、给 AI Agent 的设计建议:任务路由 + 不可变文字
一个可靠的图片生成 Agent 不应把所有需求直接拼成一个 Prompt 丢给图像模型,而应先路由,再执行。
关键约束
-
文字内容不可变
解析后立即保存原始字符串、Unicode code points、语言标签。后续 LLM 可改字号、颜色、位置,但绝不可改 content。 -
场景 Prompt 使用占位符
不要写“招牌上写‘新品上市’”,而写“有一块标记为<TEXT_REGION_1>的空白招牌”。
真正的文字通过独立字段传给排版器。 -
按任务类型路由
hard_text = true
→ 确定性排版,不走纯文生图
natural_scene_text = true
→ Glyph 条件局部编辑 + OCR
stylized_text = true
→ 确定字形骨架 + 生成式风格化
long_document = true
→ HTML/SVG/PDF 排版
八、如果需要训练或微调模型,技术路线怎么走?
1. 双编码器:语义通道 + 文字通道
- 语义通道:常规 T5/CLIP 编码画面内容、风格。
- 文字通道:使用字符级/字节级编码或字形图像编码,并输入字符位置、语言方向等信息。
Glyph-ByT5、EasyText 等近年工作都证明,字符级编码能大幅提升文字准确性。
2. 显式提供空间条件
模型不应自行猜测文字布局,应接收:
- 每行 bounding box、基线
- 字符位置 / 分割图 / 字形 mask
- 字间距、行间距、方向、透视变换
- 字体和颜色 embedding
这样模型的任务从“既想文字内容,又想文字位置”降为“在这个区域,按照给定字形做视觉融合”。
3. 高分辨率文字分支
使用双分支:背景分支用正常 latent / DiT,文字分支用高分辨率 RGB 或更低压缩率的 latent,避免小字被 VAE 压缩抹掉细节。
UM-Text 等近期方法已采用 latent 与 RGB 区域一致性监督来改善字形。
4. 改造训练数据:不要只喂“常见单词”
合成数据必须包含:
- 常用词 + 随机字符串 + 生僻字
- 多种字体、字号、字距、透视、弯曲、遮挡
- 多语言混排、形近字
真实数据必须包含精确 OCR 标注(字符级位置和转录)。尤其要大量加入从未见过的随机组合,强迫模型学习“复制字符”而非“记忆单词”。
5. 文字专用损失函数
总损失可以设计为:
L_total = L_image
+ λ1 * L_OCR
+ λ2 * L_glyph
+ λ3 * L_layout
+ λ4 * L_stroke
+ λ5 * L_extra_text
- L_OCR:OCR 识别序列损失
- L_glyph:目标字形与生成字形的感知距离
- L_layout:位置偏差
- L_stroke:笔画/边缘误差
- L_extra_text:惩罚额外文字
注意:不能只依赖一个 OCR 模型做奖励,否则生成器可能学会“机器能认、人类看着怪”的字形。
九、最终架构:矢量 + 生成式混合体
长远来看,最可靠的不是让一个纯图像模型“更努力地写字”,而是把创造性和确定性分离:
让神经网络负责创造性,让传统渲染器负责确定性。
十、落地优先级建议
| 阶段 | 行动 | 覆盖场景 |
|---|---|---|
| 阶段一(无需训练) | 无字背景 + SVG/Canvas 文字层 + OCR 校验 + 可编辑图层 | 海报、封面、Banner、电商图、教学图 |
| 阶段二(提升融合度) | 增加文字区域检测、透视/深度估计、Glyph mask、局部 Inpainting | 路牌、包装、衣服、墙面、招牌 |
| 阶段三(自研模型) | 字符/字形双编码器 + 空间布局条件 + 高分辨率文字分支 + 联合损失 | 所有场景,追求极致准确性 |
十一、总结
只要文字的准确性属于业务要求,就不能把“纯文生图一次生成成功”作为系统保证。
错字的根源在于:
- 文本编码器不关心字符级信息
- 图像生成损失不惩罚拼写错误
- VAE 压缩丢失笔画细节
- 训练数据标注粗糙
- 布局与生成任务耦合过重
根治的方法:
- 确定性文字(海报/广告)→ 排版引擎,不做文生图
- 自然场景文字(路牌/包装)→ 字形条件 + 局部编辑 + OCR 闭环
- 艺术风格文字(霓虹/金属)→ 确定字形骨架 + AI 风格化
- 训练/微调 → 双编码器 + 空间条件 + 高分辨率分支 + 字符级监督
下次再看到 AI 把“新年快乐”写成“新年快东”,别再盲目抽卡了——按这套方法论,从架构上解决问题,才能一劳永逸。
浙公网安备 33010602011771号