AI文生图模型在画质和构图上已经相当成熟,可画面一旦需要出现具体文字,比如海报标语、封面标题、产品包装上的品牌名,生成结果经常让人哭笑不得:字母多一笔少一横,汉字拧成抽象符号,整行文字歪斜断裂。这类问题的本质,是扩散模型对细粒度字形结构的控制能力不足,叠加文字区域被背景元素侵蚀、提示词对字形缺乏直接约束等多重因素。单靠反复抽卡碰运气效率太低,本文从文字位置约束和OCR后处理两条路线入手,给出一套可以落地组合的完整优化方案。

文字可读性差不是玄学,根因可以拆解
扩散模型生成图像的过程是在潜空间里逐步去噪,画面主体这类大尺度结构会率先稳定下来,而文字笔画属于高频细节,往往在去噪的最后几步才成形。这个阶段一旦引导信号不够强,笔画走向就容易跑偏,表现为字母粘连、笔画断裂或者凭空多出一截。换句话说,模型并不是看不懂提示词里的文字内容,而是缺少足够的约束把字形钉死在正确形态上。
第二个原因藏在文本编码器里。以Stable Diffusion为代表的模型用CLIP文本编码器理解提示词,CLIP对文字的编码是语义级别的,它知道Summer Sale是促销语境,但并不关心每个字母的像素级排布。77个token的输入上限进一步压缩了描述空间,你很难通过堆砌形容词让模型精确画出第几个字母长什么样。
第三个原因与训练数据分布有关。大规模图文数据集里,文字通常只占画面很小的区域,且大量存在模糊、透视变形、被遮挡的情况,模型从这些样本里学到的文字先验天然偏弱。此外生成过程中背景元素与文字区域的冲突也很常见,比如飘带、光斑、人物发丝穿过文字区域,直接把笔画结构搅乱。理解了这三层根因,后面两类优化手段的思路就顺理成章:位置约束解决区域冲突并强化字形引导,OCR后处理则在生成之后兜底校验与修复。
文字位置约束:把文字钉在规划好的区域里
最直接的一档方案是区域蒙版加局部重绘。思路是先用图像库把目标文字按真实字体排版渲染出来,得到一张只包含文字形状的二值蒙版,再把画面中对应区域交给inpainting模型重画。由于蒙版形状与目标字形完全一致,模型的生成空间被大幅压缩,笔画走样的概率明显下降。渲染时还要把蒙版向外膨胀几个像素,给笔画边缘留出与背景过渡的空间,否则重绘边界会显得生硬。
from PIL import Image, ImageDraw, ImageFont
import numpy as np
from scipy.ndimage import binary_dilation
def build_text_mask(canvas_size, text, font_path, position, font_size=72):
# 先按真实字体把文字渲染到独立图层
mask = Image.new("L", canvas_size, 0)
draw = ImageDraw.Draw(mask)
font = ImageFont.truetype(font_path, font_size)
draw.text(position, text, fill=255, font=font)
# 膨胀边缘,给笔画与背景的过渡留出空间
arr = np.array(mask) > 0
arr = binary_dilation(arr, iterations=6)
return Image.fromarray((arr * 255).astype("uint8"))
蒙版准备好后,配合Stable Diffusion的inpainting管线执行局部重绘。提示词写法上有两个要点:一是把目标文字原样写进提示词并用引号包裹,模型对这种写法的响应更稳定;二是负面提示词里明确排除模糊、变形、多余字母等坏特征。引导系数建议设在7到9之间,过低则字形约束不足,过高又容易让文字区域与整体画风脱节。
from diffusers import StableDiffusionInpaintPipeline
import torch
from PIL import Image
pipe = StableDiffusionInpaintPipeline.from_pretrained(
"runwayml/stable-diffusion-inpainting",
torch_dtype=torch.float16
).to("cuda")
base = Image.open("poster_base.png").convert("RGB")
mask = build_text_mask(base.size, "SUMMER SALE", "fonts/BebasNeue.ttf", (240, 80))
result = pipe(
prompt="the words 'SUMMER SALE' in bold sans-serif font, white letters",
negative_prompt="blurry text, distorted letters, extra characters, watermark",
image=base,
mask_image=mask,
num_inference_steps=30,
guidance_scale=7.5
).images[0]
result.save("poster_text.png")
这一档方案的短板在于,蒙版只约束了文字出现的区域和笔画大致轮廓,模型在重绘时仍可能填入错误的字形细节,尤其对汉字这类结构复杂的字符,成功率会明显低于英文单词。想要更强的字形控制,需要上第二档方案ControlNet。ControlNet的Canny控制模型可以接收一张边缘图作为附加条件,把渲染好的文字图层转成边缘图喂进去,生成过程中笔画的走向就被边缘条件牢牢锁定,controlnet_conditioning_scale参数建议从1.2起步向上微调,数值越高字形越贴近渲染模板,但过高会牺牲画面其他部分的自由度。
import cv2
import numpy as np
from PIL import Image
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel
controlnet = ControlNetModel.from_pretrained("lllyasviel/control_v11p_sd15_canny")
pipe = StableDiffusionControlNetPipeline.from_pretrained(
"runway-diffusion/stable-diffusion-v1-5",
controlnet=controlnet,
torch_dtype=torch.float16
).to("cuda")
# 复用build_text_mask的渲染逻辑,得到黑白文字图层
text_render = build_text_mask((1024, 1024), "SUMMER SALE", "fonts/BebasNeue.ttf", (240, 80))
gray = np.array(text_render.convert("L"))
edges = cv2.Canny(gray, 100, 200)
control_image = Image.fromarray(cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR))
image = pipe(
prompt="retail poster, large title 'SUMMER SALE', clean typography",
image=control_image,
controlnet_conditioning_scale=1.4,
num_inference_steps=40
).images[0]
image.save("poster_canny.png")
OCR后处理:让机器自己检查并修复生成结果
位置约束提升了成功率,但没有任何单一手段能保证百分之百正确,工程上更可靠的做法是生成之后加一道OCR校验,用识别结果反向判断文字是否达标,不达标就触发修复流程。整个流水线分三步:检测、比对、修复。检测阶段用PaddleOCR或EasyOCR定位图中所有文字区域并输出识别文本与置信度;比对阶段把识别文本与期望文本做相似度计算;修复阶段根据相似度高低选择重试、局部重绘或字体叠加等不同策略。
from paddleocr import PaddleOCR
import difflib
ocr = PaddleOCR(use_angle_cls=True, lang="en")
def check_text(image_path, expected, threshold=0.85):
result = ocr.ocr(image_path, cls=True)
best_ratio, detected = 0.0, ""
for line in result[0]:
box, (text, conf) = line
# 识别结果与期望文本的相似度,忽略大小写差异
ratio = difflib.SequenceMatcher(
None, text.lower().strip(), expected.lower()
).ratio()
if ratio > best_ratio:
best_ratio, detected = ratio, text
passed = best_ratio >= threshold
return {"passed": passed, "detected": detected, "score": round(best_ratio, 2)}
相似度阈值是这条流水线里最关键的参数。设成1.0意味着要求逐字符完全一致,对生成图像来说过于苛刻,大量本可接受的输出会被误杀;设得太低又会让错字漏网。实践里英文标题建议0.85到0.9,中文因为单字符信息密度高,建议0.9以上。另外OCR自身的置信度也要参考,置信度低于0.7的识别结果可能是把背景纹理误判成了文字,这类情况应先复核检测框位置是否落在规划的文字区域内,再决定是否触发修复。
修复策略推荐按成本从低到高分级执行。第一级换种子重试,成本最低,适合相似度只是略低于阈值的边缘案例;第二级针对识别出的具体错误字符做局部inpainting,只重绘出错字符所在的检测框,避免牵连已经正确的部分;第三级是兜底手段,直接用真实字体把文字叠加到画面上,再跑一次低强度的图到图重绘让文字与整体画风融合,强度参数控制在0.2到0.3之间,既能统一光影色调,又不至于把笔画再次画歪。
from PIL import Image, ImageDraw, ImageFont
def overlay_real_font(base, text, font_path, position, font_size=72):
layer = base.copy()
draw = ImageDraw.Draw(layer)
font = ImageFont.truetype(font_path, font_size)
# 加描边保证文字在复杂背景上依然清晰
draw.text(position, text, fill=(255, 255, 255), font=font,
stroke_width=2, stroke_fill=(20, 20, 20))
return layer
def repair_pipeline(base, expected, font_path, position, max_retry=3):
for attempt in range(max_retry):
# inpaint_text_region封装上一节的蒙版局部重绘逻辑
img = inpaint_text_region(base, expected, font_path, position)
report = check_text(img, expected)
if report["passed"]:
return img, attempt + 1
base = img # 上一轮结果作为下一轮底图继续修
# 多轮仍不达标,切换真实字体叠加兜底
img = overlay_real_font(base, expected, font_path, position)
# 低强度图到图重绘融合画风,strength控制在0.2到0.3
return blend_with_img2img(img, strength=0.25), max_retry
组合流水线的整体设计与效果数据
把前面的模块串起来,一条完整的生图文字保障流水线应该是这样的顺序:先规划文字内容、字体、位置和字号,生成文字蒙版或边缘控制图;然后执行带位置约束的生成;接着跑OCR校验,达标即输出,不达标进入分级修复;最后可选执行低强度重绘做风格融合。这个顺序的逻辑在于,约束生成解决的是源头质量,OCR校验提供客观的量化判断,分级修复控制修复成本,三者缺一不可。只做约束不做校验,你无法知道哪张图需要返工;只做校验不做约束,修复循环会频繁触发,整体耗时反而上升。
| 方案组合 | 文字正确率 | 画风融合度 | 平均耗时 |
|---|---|---|---|
| 纯提示词生成 | 约30% | 高 | 最短 |
| 区域蒙版加重绘 | 约55% | 较高 | 短 |
| ControlNet边缘约束 | 约70% | 中高 | 中 |
| 约束生成加OCR分级修复 | 95%以上 | 高 | 中长 |
表格里的数据来自电商海报场景的批量测试,场景不同数字会有浮动,但相对关系是稳定的:约束手段每加一档,正确率抬升一截,而OCR修复带来的提升幅度最大,因为它把不达标的结果也拉回了可用区间。值得注意的是耗时一列,分级修复虽然增加了OCR调用,但避免了无差别全图重生成,整体吞吐反而更优。
落地时还有几个参数细节值得记录。字体文件尽量选择笔画粗、结构简单的无衬线字体,细笔画字体在重绘时更容易糊掉;文字区域面积别低于画面的百分之五,过小的区域高频细节在潜空间压缩后损失严重;中文字符建议逐字渲染蒙版并适当放大字号,生成后再缩回目标尺寸,等价于一次超采样;OCR校验时把检测框与规划区域做IoU比对,可以过滤掉背景误检,减少不必要的修复触发。
最后提一个容易被忽视的点:修复循环要有硬性次数上限。理论上可以无限重试直到达标,但工程上必须设置熔断,比如三轮修复仍不达标就强制走字体叠加兜底,保证接口响应时间可控。生图服务的可用性往往就毁在这种没有边界的循环上。