基于GAN的手写文字擦除:OpenCV预处理与飞桨实现 简介面向计算机视觉方向学习与毕业设计需求的手写文字擦除方案以Python和OpenCV为基础结合深度学习模型实现图像中手写笔迹的自动去除与背景修复。资源包含完整工程代码、已训练模型参数、训练与预测脚本、数据加载与掩码生成模块以及详细项目说明文档方便二次开发或直接复现。压缩包共32个文件以Python脚本22个为主体辅以Shell运行脚本、模型权重文件.pdparams、说明文档md/txt/readme等整体约150.71MB结构清晰能覆盖从模型训练到推理评估的完整流程。工程中涉及SA-GAN、BiSeNetV2、非局部注意力等典型网络结构模块并配有PSNR损失等评价指标计算代码适合作为毕业设计、课程设计或科研入门参考。目前已有91人学习浏览遇到配置或运行问题还可联系作者远程协助调试上手门槛相对可控。1. 手写文字擦除先分清是去噪还是生成从扫描作业、合同单据或老照片上去掉手写批注直觉上应该先试 OpenCV 阈值加连通域分析。实际工程里手写笔迹的压感导致颜色深浅不均与印刷字体在灰度直方图上经常重叠传统图像处理做一次擦除需要同时调四五组参数换个扫描仪就得重新调一遍。这套方案把“擦除”定义为图像恢复任务输入带手写的页面输出尽可能接近原始干净排版的结果。项目代码同时包含 Python OpenCV 做前后处理、GAN 做核心重建、数据集与 dataloader 做监督训练还附带 compute_mask.py、PSNRLoss.py、EMA 和两个 checkpoints 权重。用到 STE_idr_best.pdparams 和 STE_str_best.pdparams 两种权重时你会发现损失策略对最终笔迹残留的影响比调网络深度更明显。适合课程设计和毕业设计直接参考也适合做文档 OCR 预处理的人借鉴——把“擦除”做成一个模块挂在识别链路前面。2. 项目结构与生成器判别器的分工2.1 文件结构的关键判定动 train.py 之前先把根目录的文件职责摸清。整个项目能跑通的核心文件如下表所示文件作用改动频率data/dataloader.py读入“含手写/干净”图对执行裁剪与归一化低compute_mask.py生成手写区域二值掩码用于区域加权损失中Loss.py、PSNRLoss.py、losses.py实现 L1、PSNR、感知等多项损失组合中sa_gan.py、idr.py、sa_aidr.py、networks.py生成器主网络及参考结构定义中BiSeNetV2.py、non_local.py分割骨干与全局上下文模块低discriminator.py判别器对生成图与干净图做真假判别低ema.py指数移动平均平滑训练中的权重波动低train.py、test.py、predict.py训练、批量评估、单图推理中convert_onnx.py将训练好的模型转为 ONNX 格式低checkpoints 目录里放置的是 .pdparams 后缀的飞桨格式权重。这套代码需要跑在 PaddlePaddle 环境里不能用 PyTorch 的 torch.load 直接加载。如果你从别处拿到的预训练权重是 .pth 后缀要用 ckpt_convert.py 先做张量名和维度顺序的转换否则第一个 batch 就会出现 NaN而且很难从日志里定位是数据问题还是权重加载问题。2.2 dataloader 在线合成训练对data 目录下 dataloader.py 的作用是把原始扫描页切成固定尺寸 patch并提供监督信号。数据集中没有配对标注时常见做法是离线合成先准备一张无手写的干净页面作为底图再随机位置叠加上手写笔画形成“脏图 干净图”的成对样本。dataloader 中典型的一段逻辑如下class PairLoader(Dataset): def __init__(self, root, crop256): self.pairs [] for d, c in zip(os.listdir(root /dirty), os.listdir(root /clean)): self.pairs.append((os.path.join(root, dirty, d), os.path.join(root, clean, c))) self.crop crop def __getitem__(self, idx): dirty cv2.imread(self.pairs[idx][0], cv2.IMREAD_GRAYSCALE) clean cv2.imread(self.pairs[idx][1], cv2.IMREAD_GRAYSCALE) h, w dirty.shape y random.randint(0, h - self.crop) x random.randint(0, w - self.crop) dirty dirty[y:y self.crop, x:x self.crop] clean clean[y:y self.crop, x:x self.crop] return (dirty / 255.0 - 0.5, clean / 255.0 - 0.5)这段代码做三件事。第一以灰度模式读图不让生成器去学习纸张本身的 RGB 色彩分布除非你确实想连纸张颜色一起还原第二随机裁剪到 256×256避免整页输入超出显存并保持 batch 内尺寸一致第三把像素归一化到 [-0.5, 0.5]这个线性映射让后续损失函数的梯度在取值范围内保持连续不会像 [0,1] 那样在两端饱和。实测中 crop256 对侧页的长手写笔画有截断风险如果生成器频繁出现半截文字消失把 crop 调到 512 并同步把 batch 从 8 降到 4 是更稳的组合。2.3 生成器与判别器的装配生成器主体由 sa_gan.py 和 BiSeNetV2.py 共同支撑理解上不要把它们看成并列模型而是一条 pipeline 的两段。BiSeNetV2 原本是实时语义分割网络低分辨率分支负责提取大范围文本行分布高分辨率分支保留笔画边缘在擦除任务里这两路特征融合后正好给出“哪里是手写、哪里是背景结构”。sa_gan.py 在其上叠加自注意力层让修复区域能从画面远处借纹理。non_local.py 给出了非局部注意力的一种实现常见写法如下def non_local_block(x, inter_channels32): b, c, h, w x.shape theta conv1x1(x, inter_channels).view(b, inter_channels, -1) theta theta.permute(0, 2, 1) phi conv1x1(x, inter_channels).view(b, inter_channels, -1) attn torch.softmax(torch.bmm(theta, phi), dim-1) g conv1x1(x, inter_channels).view(b, inter_channels, -1) out torch.bmm(g, attn.permute(0, 2, 1)) out out.view(b, c, h, w) return out x注意力图 attn 的空间维度是 (hw)×(hw)。如果直接对 512×512 的特征图做全局注意力矩阵会变成 262144×262144显存立刻爆掉。所以实际使用中都会先把空间分辨率压缩到 32×32 或 64×64再做全局上下文聚合。判别器由 discriminator.py 定义输入生成图与干净参考图训练时判别器与生成器交替更新这类对抗机制正是产生清晰边缘的关键。3. 掩码计算与损失函数的设计3.1 compute_mask.py 提供区域监督compute_mask.py 的目标是把手写区域标出来供损失函数按区域加权。常见做法是从像素差异出发将带手写图和干净图逐像素做差灰度差超过阈值的点记为 1再用膨胀操作把笔迹边缘扩一圈避免网络只在笔锋内部做修正def compute_mask(dirty, clean, thr30, dilate2): diff cv2.absdiff(dirty, clean) mask (diff thr).astype(np.uint8) mask cv2.dilate(mask, np.ones((3, 3), np.uint8), iterationsdilate) return mask膨胀参数 dilate 很重要。手写笔迹边缘存在半透明过渡灰度差常常达不到阈值膨胀的作用就是把这个过渡地带也纳入监督范围让生成器不只是处理高置信笔画还要处理颜色残留。另一种常见的掩码形态是背景掩码把印刷体文字区域也置为需要重建的对象此时网络要对整块内容做生成损失权重必须和前景掩码模式区分开否则会出现印刷体被过度平滑的问题。3.2 PSNR 损失与 L1 损失各管一段Loss.py 和 losses.py 负责把多种损失汇总到一起其中 PSNRLoss.py 看起来是在算图像质量实际是把这个指标改造成了可训练的回归损失。PSNR 由 MSE 推导而来mse mean((pred - gt)^2)psnr 10 * log10(max^2 / mse)。把 PSNR 直接作为损失项时网络会优先降低 MSE 大的区域对手写笔迹这种局部高误差像素非常敏感。相较单纯 L1PSNR 损失强调逐像素平均正确L1 则对异常值更宽容。两者配合的典型写法是def hybrid_loss(pred, target, weight_l11.0, weight_psnr0.1): l1 torch.mean(torch.abs(pred - target)) mse torch.mean((pred - target) ** 2) psnr 10 * torch.log10(1.0 / (mse 1e-8)) return weight_l1 * l1 - weight_psnr * psnr注意 psnr 项前面是负号。训练目标是让 PSNR 更大损失项需要取相反数才能和 L1 一起朝减小的方向优化。weight_psnr0.1 是一个起步值若擦除结果过度平滑说明 PSNR 权重压制了结构项调到 0.03 左右再对比一次。3.3 多损失与 EMA 的配合项目中存在的损失组合可归纳为下表损失项监督目标常见权重L1 损失逐像素绝对误差1.0PSNR 损失回归平均误差0.03~0.1对抗损失整体观感真实性0.1感知损失高层语义特征一致性0.05如果只用 L1恢复出的背景文字边缘会发糊如果让对抗损失比例过高可能会连印刷体一起“擦”掉。感知损失隔着预训练网络中间层对比特征让输出不再逐像素对齐而是整体结构对齐对保持文字骨架特别有效。EMA 则维护一份历史权重的滑动平均在评估时用小方差版本代替临时权重能明显压低训练后期损失曲线的抖动幅度。4. 模型训练与预测的具体跑法4.1 环境搭建与 OpenCV 版本项目依赖飞桨和 OpenCV。搭环境时Python 版本建议固定在 3.8 到 3.10 之间太新的解释器可能碰到 PaddlePaddle 预编译包尚未适配的情况。OpenCV 使用 4.x 即可不需要特殊版本主要承担图片读取、缩放和形态学操作conda create -n erase python3.8 -y conda activate erase pip install paddlepaddle-gpu2.5.2 pip install opencv-python4.8.0.74 numpy1.24.3如果只需 CPU 调试把 paddlepaddle-gpu 换成 paddlepaddle 即可但训练建议还是用 GPU。OpenCV 安装完可以用cv2.getBuildInformation()验证编译信息确认读图模块正常避免后面 predict.py 在 imread 阶段静默返回 None。4.2 train.sh 与 test.sh 的启动方式train.sh 内部最终调用的是 train.pytest.sh 对应 test.py。启动一个完整训练回合的常见参数组合如下python train.py \ --train_root ./data/train \ --val_root ./data/val \ --batch_size 8 \ --crop_size 256 \ --lr 2e-4 \ --epochs 200 \ --save_dir ./checkpoints参数含义train_root 指向包含 dirty 和 clean 两个子目录的根路径batch_size 根据显存调整crop_size 决定输入 patch 大小lr 是生成器初始学习率。判别器的学习率一般设置为生成器的十分之一代码中通常存在 discriminator 独立的学习率字段没找到就在 train.py 搜索lr_d或d_lr。跑 test.sh 时实际执行的是python test.py --resume ./checkpoints/STE_idr_best.pdparams --data_dir ./data/test注意权重路径指向的是 STE_idr_best 还是 STE_str_best两者效果差异主要在边缘残留分布上。前者对全局版面更敏感后者对单笔画更敏锐。拿到新的扫描件做推理时优先用 STE_str_best 看主观效果。4.3 ONNX 导出与二次集成convert_onnx.py 用于把训练好的飞桨模型导出为标准 ONNX方便脱离飞桨环境部署。导出时常见问题是动态尺寸导致 ONNX Runtime 报错建议固定输入分辨率import paddle model build_generator() model.set_state_dict(paddle.load(STE_str_best.pdparams)) model.eval() dummy paddle.zeros([1, 1, 256, 256]) paddle.onnx.export( model, erase.onnx, input_spec[paddle.static.InputSpec( shape[-1, 1, 256, 256], dtypefloat32)] )导出的 erase.onnx 可以交给 ONNX Runtime 或 OpenCV 的 DNN 模块加载。OpenCV 的 cv2.dnn.readNetFromONNX 在 4.5 版本后对常见算子支持已经比较完整但自注意力里的 reshape 和 transpose 组合偶发不支持这时需要回退到原版飞桨推理。5. 用 OpenCV 做边缘修复与结果验证预测输出往往不是完美的干净页面生成器在手写笔迹边缘会留下浅色残影。用 OpenCV 做一层轻量后处理能显著提升视觉效果。我常用的做法是先膨胀再腐蚀把离散的残影点闭合成小块再通过轮廓面积过滤掉孤立噪声import cv2 import numpy as np out cv2.imread(output.png, cv2.IMREAD_GRAYSCALE) kernel np.ones((3, 3), np.uint8) closed cv2.morphologyEx(out, cv2.MORPH_CLOSE, kernel, iterations1) med cv2.medianBlur(closed, 3) cv2.imwrite(output_clean.png, med)闭运算能合并非连续的残影中值滤波在保留笔画结构的同时抑制孤立的椒盐噪声。但不能对整张图做多次闭运算否则印刷字体笔画会被加粗反而降低后续 OCR 的置信度。验证清除效果时不要只看整图 PSNR。用手写掩码加权计算局部 PSNR 和 SSIM 更有意义掩码区域残影越少局部 PSNR 提升越明显。把生成的掩码与预测图对应区域拿出来单独统计比较 STE_idr_best 和 STE_str_best 在该区域上的数值差异就是两个权重文件最直观的区分方式。训练集里类似 DEHW 这类场景文本擦除数据的分布也会直接影响这个局部指标的表现。本文还有配套的精品资源点击获取