简介:基于GAN的复杂背景文字图像修复Python源码项目,面向计算机视觉学习者与深度学习实践者,清晰演示从数据准备、模型训练到测试验证的完整流程。压缩包共12429个文件,整体约176.4MB,其中包含12375个jpg训练样本、34个ttf字体、7个py脚本(含trainwork.py与testwork.py)、4个xml配置及2个pth预训练权重,各部分分别服务于图像数据、字体渲染、训练测试与模型保存。已有445人学习下载,适合作为入门参考或二次开发基础。通过trainwork.py可了解生成器与判别器的网络设计、损失计算和对抗训练细节,testwork.py支持加载权重直接修复新图像;大量jpg与字体文件还能帮助理解中文文字图像数据集的构造方式。压缩包以computer-vision-main目录组织,覆盖模型定义、训练日志、数据预处理脚本与结果展示等模块,便于按需检索和复用。
1. 复杂背景文字图像修复:为什么GAN是绕不开的解法
一张票据扫描件上,金额位数被印章压住了;一本旧书的书脊上,标题笔画被污渍吃掉了一半;一张产品照片里,包装上的生产批号拍糊了——这些场景都有个共同点:背景不是单纯的白底,而是纹理、阴影、反光交织的复杂背景,而文字恰恰又是图像里信息密度最高、对结构最敏感的区域。这类需求在业内常被归为GAN图像修复(Generative Adversarial Network based Text Image Restoration),核心是用GAN网络对破损文字区域做语义级重建,而不是简单模糊去噪。复杂背景里重建文字,难在两点:既要补出让人能认、让OCR系统能读的笔画,又要让补出来的区域和周围背景在纹理、明暗、边缘上融为一体。本文我按自己的落地经验讲清楚:任务怎么定义、数据怎么造、模型怎么选、损失怎么配、训练有哪些坑、以及最后怎么把修复效果量化出来。适合正在做文档去污、票据增强、古籍数字化、OCR前处理的算法工程师——不是泛讲GAN原理,而是照着标题把这个任务真正跑通。
2. 把“文字残缺”定义成可训练的任务:数据集与预处理
2.1 文字遮挡与模糊的多种退化方式
做基于GAN的文字修复,第一步不是调模型,而是先把“复杂背景”和“文字受损”这两个概念变成能监督学习的东西。真实场景里,文字受损常见形式有几种:大色块覆盖,比如印章、污渍、贴纸;边缘磨损,笔画断开但不缺整块区域;运动模糊或失焦,笔画的对比度下降;反光与阴影,文字被高光或暗影局部吞掉。不同退化形式对模型的要求差异很大——大色块覆盖考验生成器的语义想象能力,边缘磨损考验笔画连续性建模,模糊则是低频信息保留问题。如果只用单一退化方式训练,模型一上真实数据就会露馅。
常见做法是走合成数据路线:找一批不带文字的复杂背景图,把文字用随机字体、随机颜色、随机角度渲染上去,得到“干净图”,再叠加退化得到“破损图”,同时保存一个掩码标明哪些像素需要修复。这套三元组(clean, damaged, mask)既便宜又能精确控制退化类型。背景图源不必追求高质量,纹理丰富更重要——墙面上拍的照片、布料纹理、带噪点的扫描纸都行。我一般把背景库分成训练集和验证集,验证集里刻意留出和训练集不同来源的背景,避免模型背下背景纹理分布。
2.2 数据标注与掩码生成:一条脚本拉通
合成脚本的核心是“文字渲染 + 退化叠加 + 掩码生成”三个环节。下面这段是我常用套路的一个精简版,逻辑清晰、能直接改来用:
import numpy as np import cv2 from PIL import Image, ImageDraw, ImageFont def render_text_on_background(bg, text, font_path, max_angle=15): """ 在背景图上渲染文字,返回干净文字图和文字所在的矩形掩码 """ bg_h, bg_w = bg.shape[:2] # 随机生成字体大小与颜色,确保和复杂背景有真实拍摄的对比度 font_size = np.random.randint(bg_h // 12, bg_h // 6) font = ImageFont.truetype(font_path, font_size) # 先量文字尺寸,再做旋转,避免画布裁剪 canvas = Image.new("RGB", (bg_w, bg_h), (0, 0, 0)) draw = ImageDraw.Draw(canvas) text_w, text_h = draw.textbbox((0, 0), text, font=font)[2:] text_img = Image.new("RGBA", (text_w + 40, text_h + 40), (0, 0, 0, 0)) text_draw = ImageDraw.Draw(text_img) fill = (np.random.randint(30, 220), np.random.randint(30, 220), np.random.randint(30, 220), 255) text_draw.text((20, 20), text, font=font, fill=fill) # 随机位置、旋转、透视,贴到背景上 angle = np.random.uniform(-max_angle, max_angle) text_img = text_img.rotate(angle, expand=True) paste_x, paste_y = np.random.randint(0, bg_w - text_img.width), np.random.randint(0, bg_h - text_img.height) bg_pil = Image.fromarray(bg) bg_pil.paste(text_img, (paste_x, paste_y), text_img) # 掩码:文字区域为1,背景为0,之后退化会叠加在掩码上 mask = np.zeros((bg_h, bg_w), dtype=np.uint8) mask[paste_y:paste_y + text_img.height, paste_x:paste_x + text_img.width] = 255 return np.array(bg_pil), mask这段代码的要点:先用RGBA画布独立渲染文字,再旋转、贴回背景,是为了保证文字边缘不会因为直接画在背景上而带上看不见的黑边;字体颜色取30到220的随机范围,避开纯黑纯白,更接近真实拍摄中的反光环境。掩码按文字外接矩形生成,是一个粗粒度掩码——这背后有意图:真实场景里的遮挡往往比单条笔画大,用矩形掩码比逐像素笔画掩码更能逼模型做语义重建,而不是做膨胀卷积。
退化叠加的函数我通常配合掩码一起写,思路是:对掩码区域分别施加模糊、污渍纹理、噪声、以及随机形状的色块,生成最终的破损图。一个容易被忽略的参数是退化面积占整张图的比例。我一般让掩码区域占图的5%到25%之间浮动。太低,模型不需要学习背景建模就能中;太高,生成器会倾向于把整图重画,出现背景“重绘失真”的副作用。
def degrade_region(clean, mask, blur_ks=(9, 9), stain_intensity=0.4): """对掩码区域执行多种退化,返回受损图和保真掩码""" damaged = clean.copy() # 笔画级模糊:模拟失焦与运动模糊 damaged = cv2.GaussianBlur(damaged, blur_ks, 0) # 污渍:用噪声叠加做半透明色块 noise = np.random.randint(0, 60, clean.shape, dtype=np.uint8) stain_mask = (mask > 0).astype(np.uint8) damaged = cv2.addWeighted(damaged, 1 - stain_intensity, noise, stain_intensity, 0) # 只保留掩码区域的信息破坏,背景保持不变 out = clean.copy() out[stain_mask > 0] = damaged[stain_mask > 0] return out这里关键的参数是stain_intensity,它控制退化强度。0.4表示原始干净文字的信息保留60%、噪声叠加40%,适合第一轮预训练;如果一上来就设0.8,生成器要同时学重建和猜测,训练很容易震荡。数据规模上,我的经验是:单卡训练用2万到5万张合成图足够,不要盲目追求百万级——合成数据的多样性瓶颈在字体和背景组合,不在数量。字体至少要准备10种以上,常见做法是把系统自带的中文字体目录扫一遍,再加上开源的宋体、黑体、手写体。艺术字、繁体、异体字这类在后期微调阶段再引入,前期搅进去会拖慢收敛。
3. 复杂背景为什么会骗过模型:GAN在文字修复上的选型逻辑
3.1 为什么L1/L2损失在复杂背景上会翻车
如果用纯卷积网络加L1损失去重建文字区域,模型会在“模糊平均”和“背景纹理复制”之间摇摆。原因是L1损失是逐像素损失的期望最小化,它认为所有候选修复结果里,取像素均值是最安全的。对白底黑字的规则票据,这个均值结果尚可接受;对复杂背景,像素均值意味着颜色被周围背景拉平,文字边界形成一圈“糊边”,而且背景里本该有的高频纹理会被抹掉。我见过不少团队一上来就套用超分辨率或去噪的L1模型,结果修复出来的文字方向错乱、笔画扭曲,背景变成塑料表面——这就是任务定义错了。
GAN的介入价值在于:判别器不是逐像素对比,而是对整块patch的真伪做判断,它强制生成器输出落在真实图像的流形上。具体到文字修复,判别器会学到“文字既不能模糊,也不能是背景纹理的简单延伸”,这让生成器被迫做出语义选择。注意我说的是“选择”——生成器在看到被印章盖住的数字“3”和“8”的中间状态时,必须根据上下文猜一个最可能的数字。这种猜的能力来自对抗训练,不来自重建损失。
3.2 判别器要看什么:感受野、局部判别与多尺度
文字修复的判别器设计和图像生成不完全一样。生成人脸时,判别器看全局整体就行;文字修复里,一个字的笔画可能只有十几个像素宽,全局判别器会对整幅图的统计特征做判断,根本看不清局部笔画是否断了。常见的做法是把判别器换成分块判别——PatchGAN,输出每个patch的真伪概率图,强迫生成器在每个局部块上都和真实图像分布对齐。Patch大小我一般取70x70,对256x256的输入图来说,差不多能覆盖两三个汉字,既能约束细节又不至于把生成器锁死在单个笔画上。
多尺度判别器是另一个有效增强:同时对原图、1/2分辨率、1/4分辨率做判别。低分辨率分支约束整体色调和排版布局,高分辨率分支约束笔画锐度和背景纹理。我在训练时会明显感到多尺度让生成图更少出现“颜色断层”——单判别器对色彩漂移不敏感,多尺度里的低分分支会强行把全局色温拉回来。
3.3 生成器怎么选:U-Net、编解码还是带注意力
生成器主体用U-Net是最稳妥的起点。U-Net的跳跃连接把编码器各层的浅层特征直接接到解码器对应层,这对文字修复至关重要——文字笔画是高频边缘信息,如果经过瓶颈层再上采样,高频细节已经丢了,跳跃连接让解码器可以直接引用原图的高清特征。在U-Net卷积模块上,我建议用门控卷积替代普通卷积:门控卷积对每个像素学习一个动态开关,遇到掩码区域会把门打开去做重建,遇到背景区域会把门关上保持原样,这就避免模型把背景纹理误当成破损区进行重绘。
如果预算允许,可以在U-Net的瓶颈层加一个Transformer block。文字修复有一个特点:同一个字符的上下文往往跨越很长的空间距离,比如“某银行”三个字被横向污渍连成一条,纯卷积的感受野不够,需要注意力机制建立跨区域依赖。不过实际工程中,Transformer的收益和训练成本不成正比,我的建议是先上U-Net加门控卷积,效果不够再提级。
生成器里我还会挂一个辅助分支:结构预测头,输入中间特征,输出和原图同尺寸的单通道掩码,监督信号是文字笔画骨架的GT掩码。这个分支在推理时可以直接剪掉,但在训练时相当于给生成器一个“先找文字再修文字”的显式引导,比在损失函数里加权重更有效。下面是一个可运行的生成器骨架代码,突出了我上面说的点:
import torch import torch.nn as nn class GatedConvBlock(nn.Module): """门控卷积:学习掩码区域的开合状态,保留背景纹理不被重绘""" def __init__(self, in_ch, out_ch): super().__init__() self.conv_feat = nn.Conv2d(in_ch, out_ch, 3, padding=1) self.conv_gate = nn.Conv2d(in_ch, out_ch, 3, padding=1) self.bn = nn.BatchNorm2d(out_ch) def forward(self, x): feat = self.conv_feat(x) gate = torch.sigmoid(self.conv_gate(x)) return self.bn(feat * gate) class TextRestorationGenerator(nn.Module): def __init__(self, in_ch=4, out_ch=3): """ in_ch=4:RGB图像 + 二进制掩码(掩码区域=1) out_ch=3:修复后的RGB图像 """ super().__init__() # 简化的U-Net编码-解码结构 self.enc1 = GatedConvBlock(in_ch, 64) self.enc2 = GatedConvBlock(64, 128) self.enc3 = GatedConvBlock(128, 256) self.dec3 = GatedConvBlock(256 + 128, 128) self.dec2 = GatedConvBlock(128 + 64, 64) self.dec1 = nn.Conv2d(64 + in_ch, 3, 3, padding=1) def forward(self, x, mask): inp = torch.cat([x, mask], dim=1) e1 = self.enc1(inp) e2 = self.enc2(e1) e3 = self.enc3(e2) d3 = self.dec3(torch.cat([e3, e2], dim=1)) d2 = self.dec2(torch.cat([d3, e1], dim=1)) # 跳跃连接保留原始输入,让生成器直接看到无伤背景 out = self.dec1(torch.cat([d2, inp], dim=1)) return torch.tanh(out)代码里的关键是门控卷积中gate的sigmoid输出:它在训练中会自动学习“打在背景上的浅层特征门值为0、打在掩码区域的门值为1”,这就是背景不被重绘的机制。输入把掩码作为第四通道连进去,比单靠内部学习掩码更直接,相当于显式告知模型哪里要修、哪里不能动。U-Net的跳跃连接体现在e3与e2拼接、d3与e1拼接、最后又与inp拼接,层数比较浅,但正好够256x256输入的文字修复任务。如果想要更强的重建能力,把每层卷积替换成ResBlock,并在瓶颈层加自注意力,参数会增加到原来的2到3倍,训练时间也随之上升——先跑通这个版本,再决定要不要加重。
4. 基于GAN的文字修复训练流程:从预训练到微调
4.1 第一阶段:结构感知预训练
GAN训练最怕的是生成器和判别器在对抗中双双摆烂,尤其是文字修复这种任务,生成器如果一开始输出就是乱的,判别器很容易学到“凡是模糊就是假的”这种粗暴规则,后续再怎么调都难救。我几乎总是先单独训生成器,用L1加感知损失加结构引导,不接判别器。这个阶段让生成器先学会“大方向正确”:笔画位置对、字体粗细大致对、背景颜色不被破坏。
结构感知预训练的损失函数核心是三部分加权:L1重建损失保证像素级接近;感知损失用VGG16中间层的特征做比对,让生成图像和干净图像在高级语义特征上接近;结构引导损失是把辅助分支预测的笔画骨架和GT骨架做二值交叉熵。权重我一般配成L1:感知:结构 = 1:0.1:0.05。感知损失权重不要贪大,0.1已经能让背景纹理自然很多,权重超过0.3就会出现“纹理太丰富、文字失真”的另一面翻车。
def structure_aware_loss(generated, gt_text, pred_structure, gt_structure, l1_w=1.0, vgg_w=0.1, struct_w=0.05): import torch.nn.functional as F # 感知损失需要预训练的VGG特征,这里示意计算流程 l1_loss = F.l1_loss(generated, gt_text) vgg_loss = F.mse_loss(vgg_features(generated), vgg_features(gt_text)) struct_loss = F.binary_cross_entropy_with_logits(pred_structure, gt_structure) return l1_w * l1_loss + vgg_w * vgg_loss + struct_w * struct_loss这个阶段的优化器我用Adam,初始学习率1e-4,batch size看显存取4到8。分辨率建议先在256x256上训练,不要一上来就512,因为文字修复的判别器最终要在高分辨率上精修,而生成器结构在低分辨率先稳定更容易。训练步数大约5万步,期间把学习率按cosine退火降到1e-5。判断这个阶段是否收敛,我看的不单是loss曲线,而是直接抽几批验证集图看:文字区域是否从“糊成一团”变成“可辨认字体形状”,背景是否保持原样。如果背景变了,多半是感知损失权重太高,生成器在“篡改背景”的边缘试探。
4.2 第二阶段:GAN对抗学习
预训练稳定后,加入判别器进入对抗阶段。常见错误是直接沿用上一阶段的低学习率。此时生成器的参数已经有合理的初始化,判别器是白纸,两者天然不平衡。我的做法是冻结生成器主干的前两层,只解冻高层和辅助分支,学习率降到1e-5。判别器单独用5e-5的学习率,让它快速追上而不过冲。对抗损失用hinge形式,比原始GAN的log-loss更稳定,梯度在高置信区间被截断,不会出现判别器瞬间把损失降到0的情况。
训练节奏参考这个经验:每轮的判别器和生成器更新频次固定1:1,不搞交替加练判别器——文字修复的判别器特征相对简单,加练会让它过拟合到背景纹理上,生成器随即开始大量生成假纹理来对抗,最终输出“五彩斑斓的黑”。我在训练中会盯两个数值:D对真实patch的均值输出,和D对生成patch的均值输出。理想状态是真实值略高于生成值,两者差值不超过0.3,且都在0.5附近振荡。如果真实值从训练开始就一直是1.0,说明判别器太强,生成器梯度会快速消失。
对抗阶段的完整训练步可以用下面伪代码概括:
for step in range(total_steps): real_imgs, damaged_imgs, masks, gt_struct = next(loader) # 生成 pred_imgs, pred_struct = generator(damaged_imgs, masks) # 判别器更新:真实patch判定为真,生成patch判定为假 d_real = discriminator(real_imgs) d_fake = discriminator(pred_imgs.detach()) d_loss = F.relu(1 - d_real).mean() + F.relu(1 + d_fake).mean() d_optimizer.zero_grad() d_loss.backward() d_optimizer.step() # 生成器更新:对抗损失 + 重建损失 + 结构损失 d_fake_for_g = discriminator(pred_imgs) g_adv = -d_fake_for_g.mean() g_recon = structure_aware_loss(pred_imgs, real_imgs, pred_struct, gt_struct) g_loss = g_adv + g_recon g_optimizer.zero_grad() g_loss.backward() g_optimizer.step()阶段二我一般跑到2万步左右。此时批量大小可以降到2或4,因为判别器输入的是原尺寸图像加随机裁剪的patch,显存占用比阶段一高很多。一个实用技巧是阶段性余弦退火:每5000步把学习率降到当前值的0.2再恢复,这样能让生成器再尝试几轮比较激进的结构变化,不至于在一个局部解上停留太久。
4.3 关键参数设置与消融验证
参数表说一百遍不如跑一轮消融。建议在验证集上做三组对照:不加GAN只保留结构感知预训练、加GAN但不加结构损失、完整版。对比指标除了OCR字准率,还要肉眼对比背景纹理的一致性。我遇到过的情况是OCR准确率两者差不多,但人工一眼能看出加GAN的图背景更自然——这说明可读性提升和视觉质量提升并不完全同步,最终目标决定了你要不要付出对抗训练的时间成本。
| 参数/组件 | 预训练阶段 | 对抗阶段 | 说明 |
|---|---|---|---|
| 学习率(生成器) | 1e-4 | 1e-5 | 第二阶段不能沿用大学习率,容易撕裂前期结构 |
| 学习率(判别器) | 无 | 5e-5 | 判别器要从头学,速率快于生成器 |
| 批量大小 | 8 | 2-4 | 显存输入与patch数量共同决定 |
| 损失权重(L1/感知/结构) | 1 / 0.1 / 0.05 | 1 / 0.1 / 0.05 减半对抗 | 对抗损失加入后,重建损失权重可以略微释放 |
| 分辨率 | 256 | 256-512 | 先在低分辨率定型结构,再上调做细节精修 |
消融实验本身也要设定统一的客观指标,推荐用修复后图像的OCR字符准确率(OCR Accuracy)作为核心指标,配合SSIM和LPIPS。OCR准确率反映的是“能不能读出来”,LPIPS反映的是“人眼看像不像”,两者结合才能判断一次改动是变好还是变糟。
5. 复杂背景文字修复的常见问题排查:5个典型坑
5.1 现象:文字修出来了,背景糊成一片
这是综合症,我在不止一个项目里见过。生成器确实把文字区域补上了,笔画也连贯,但背景的纹理没了,原来墙面的水泥颗粒、纸面的纤维感,全变成光滑的渐变。原因在于重建损失太大,生成器选择了一种保守策略:输出频率低的均值图像,背景高频纹理全部牺牲。另外,生成器接收的输入是带掩码的原图,如果门控卷积的门没有真正学会区分掩码内外,背景区域也会被重新渲染一遍。
解决的办法是三个方向同时上:第一,把重建损失从L1换成L1加拉普拉斯金字塔损失,加强对高频细节的约束;第二,检查门控卷积的gate输出分布,如果gate在背景区域也不为零,说明预训练阶段背景重绘没被抑制,可以加大背景区域的掩码权重,让mask通道更强地压制生成活动;第三,把判别器的感受野从70x70缩到更小,让局部纹理分析被加强。
5.2 现象:训练loss在下降,但生成图越修越“脏”
这是GAN训练里最隐蔽的陷阱。生成器的重建损失持续下降,但抽出的样本里文字边缘出现大量颗粒状伪影、背景出现不自然的色斑。原因在于判别器被“骗过”了:它发现生成器的输出中带着某种高频特征就能稳定骗过自己,于是生成器就强化这种特征,形成对抗中的“特征欺骗”循环。这种伪影不是噪声,它和真实文字笔画交织在一起,肉眼看着像墨迹晕染。
解决思路非常直接:验证集上的人工观察比任何loss都权威。一旦发现这种迹象,立刻把对抗损失的权重下调一半,或者切回纯预训练模式再训一段时间,让重建损失把伪影磨掉。另一个有效手段是给生成patch加随机噪声后才送进判别器,破坏判别器对单一特征的依赖,伪影会明显减少。
5.3 现象:显存OOM,或训练中途不再收敛
训练到一半显存爆掉是家常便饭。文字修复比普通图像生成更吃显存,因为输入要额外带一个掩码通道,判别器又要处理原图和生成图两个分支。可能出现的情况是:batch=4在256分辨率下勉强能跑,一旦把生成器换成更大的ResBlock版本,显存直接溢出。
解决方法是分块训练。把原图按patch切出若干区域,每块做一个样本,掩码也跟着切。常见做法是取128x128的patch,步长64,这样每张图能出多个训练样本,batch可以调大。注意推理时要切成同样尺寸还要带overlap,否则拼接处会有一条可见的接缝。训练收敛不了,还要检查是否用了BatchNorm——GAN训练中BatchNorm在小batch下统计量抖动大,建议换成InstanceNorm或干脆去掉,注意这一改动会改变生成器输出的确定性,适合在预训练阶段就定下来。
5.4 现象:修复后的文字OCR能读,但字体、笔画形态失真
这个现象很尴尬:OCR准确率到95%了,但人眼一看就知道字是“画”出来的,宋体的衬线丢了,手写体的粗细变化没了。原因在于模型学习的只是字符的语义类别,并没有学习字体级的结构细节。这在线下部署到票据识别场景时尤其致命——下游系统往往需要同时读文字内容和做印章比对,字体失真会干扰比对算法。
解决方向,一是数据侧:合成数据时引入字体多样性,同一个字符用不同字体渲染多份,让模型看到同一个字在不同骨架下的表现形式。二是模型侧:在结构预测分支的监督信号里,不仅要预测笔画骨架,还要让辅助分支输出一个“字重”二值图,区分笔画的粗细分叉。三是损失侧:在感知损失之外加一个针对笔画边缘方向的损失,用Sobel算子提取梯度方向直方图,让生成图在笔画方向的统计上和GT一致。这个方法能显著减少“笔画走向诡异”的问题。
5.5 现象:真实场景掩码与训练掩码分布不一致,效果崩盘
模型在合成mask下表现很好,一到真实数据就各种怪异。这是分布偏移的典型表现。真实场景的掩码有多种来源:印章叠加是非刚性的半透明掩码,笔画缺损是不规则细长掩码,而标注人员框选的可能是整块矩形。合成数据里掩码是规则的矩形或圆形,真实掩码分布完全不同,无监督GAN很容易把分布外的输入归为“可以乱来”的区域。
最实用的解决方式是做一个“掩码增强”的预处理模块:对标注的真实掩码做膨胀、腐蚀、随机旋转、加噪声,生成“伪真实掩码”加入训练。同时保留一份纯规则掩码的数据,比例约3:7,规则7、增强3,避免模型彻底适应伪真实掩码的噪声特征。我在真实项目里还会专门收集10到20张带真实遮挡的原图和标注掩码,作为验证集合的补充——这比再多合成1万张数据都管用,因为它是分布偏移方向的校准锚点。
6. 评估指标与部署微调:修复效果怎么量化、模型怎么上线
6.1 三类评估指标:OCR准确率、像素指标、人工打分
修复模型不能只看“好看”,也不能只看“准确率”,要分三维度量化。第一是任务指标:用OCR系统分别识别修复前和修复后的图像,统计字符错误率(CER)和词错误率(WER)。修复算法有效的最低标准是CER显著下降,如果OCR系统原来错得离谱、修复后也只是改错成另一种错,那算法再好看也不能投入生产。第二是画质指标:PSNR和SSIM是入门参考,但PSNR在复杂背景上不可全信,它在纹理区域容易被大误差主导,LPIPS才是观察人类感知相似度更可靠的指标。计算LPIPS需要预训练的AlexNet特征,跑一次全量验证集大概十几秒,完全值得加进每次实验的评测代码里。第三是人工视觉打分:每张验证图由3个人按5分制评价四个维度——文字可读性、字体保真度、背景一致性、整图自然度。我所在的团队是每次模型更新必过一次人工打分,数量不用多,40张图足够发现指标掩盖的细节问题。
6.2 把模型接入OCR前处理管线
实际部署时,模型要作为OCR前处理的一环,而不是独立产品。常见的接入方式:输入图像先做文档矫正,然后文字检测器定位文字区域,检测出的文字bbox内的图像如果质量太差就送入修复模型,修复结果再送回识别模型。修复模型不要对整张图跑——文字区域在整张图里占的比例不大,整图推理会浪费大量算力,还可能在无文字背景区域引入幻觉。用一个二次检测结果作为掩码生成的前置条件,按patch推理,只在需要修复的区域上花算力。
模型导出和加速上,PyTorch训练好的模型转ONNX是常见做法。注意转ONNX时的坑:U-Net里有形状推断分支的话,要用onnx动态轴或者固定输入尺寸,否则batch size变化时导出失败;门控卷积没有特殊算子,转ONNX顺畅。如果目标是CPU推理,INT8量化能把推理时间减一半以上,代价是文字边缘的锐利度略降——这个取舍要在真实测试集上用OCR准确率重新验证,不要凭感觉接受或拒绝。
6.3 内存、速度与边缘部署的取舍
落到实际生产,复杂背景文字修复的算力需求往往比想象中高。生成器本身不算大,参数量通常在20M到40M,但推理时的有效计算量不低。256x256输入、单张图80ms在GPU上是可以接受的,但换到OCR一体机那种小盒子CPU,一张图可能要2到3秒。这就面临取舍:是优先OCR一次识别准确率,还是优先整条识别链路的时延预算。
我的建议是分层策略:第一轮先用轻量级传统算法做快速修复,比如形态学重建搭配局部自适应阈值,能解决一部分遮挡不严重的场景;只有第一轮修复后OCR置信度仍低于阈值时,才调用GAN修复模型。这种“快通道加慢通道”的结构在实际项目中能覆盖80%的简单场景,算力消耗控制在GAN常开方案的十分之一以内。系统部署后还要做回归测试池:固定一批票据、古籍、产品包装图,每次模型更新都跑同一套OCR评测脚本,回头翻看指标波动。
我现在养成的习惯是,任何模型改动都要拿一张最复杂、最苦手的真实样例做“体感测试”:不只看人工打分,还看OCR在修复前后输出的置信度分布——如果置信度上升但不稳定,说明修复方向对了但还在猜测边缘,需要回去调结构损失。这条路跑下来,复杂背景文字修复并没有玄学,每一步都有迹可循:数据栈、模型栈、损失栈、评测栈,把每一层拆开见真章。希望帮到你。
本文还有配套的精品资源,点击获取