尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

图像翻译实战:基于U-Net与条件GAN的试卷手写擦除

图像翻译实战:基于U-Net与条件GAN的试卷手写擦除 简介基于深度学习的试卷手写文字擦除系统是一套完整可运行的毕业设计源码能够自动擦除试卷扫描件中的手写笔迹、还原空白试卷主要面向计算机科学、人工智能、数据科学等相关专业学生可用于毕业设计、课程设计或期末大作业等场景。压缩包共62个文件以44个Python脚本为核心覆盖数据加载、模型搭建、训练测试全流程同时提供shell脚本、说明文档与readme整体仅190KB结构清晰便于快速部署和二次开发。项目融合多种图像分割与生成对抗网络设计并针对文字擦除任务实现专门的损失函数与训练细节适合希望深入掌握深度学习图像处理技术、提升工程实践能力的学习者。目前已有166人学习下载源码经本地验证可正常运行配套运行说明完整遇到问题可与作者沟通具备较高的参考与扩展价值。1. 试卷手写文字擦除一个比单纯目标检测更考验模型的图像翻译任务一张写满答案的试卷如果要重新用于其他班级或下一届最常见的办法是人工拿橡皮擦但擦不干净还费时。把“擦手写”这件事交给代码本质上是把一张混合印刷体和手写体的图像转换回只保留印刷体和背景的干净图。这个任务不是去做手写文字识别而是把图像从一个域映射到另一个域属于典型的图像到图像翻译。传统方法用阈值分割加形态学修复一旦笔迹压在中文印刷字上就会把印刷字也吃掉而基于深度学习的方案例如 U-Net 和条件 GAN能学到笔迹纹理、颜色和边缘的上下文在保留印刷笔画的同时抹掉手写。这篇文章从数据准备、模型训练、运行参数到排错技巧讲完一套可复现的落地路径适合正在做同类毕业设计或者用 Python 折腾图像处理项目的开发者。2. 从问题定义到数据准备为什么要训练一个擦除模型而不是写一堆图像处理规则2.1 任务定义与理想输出的边界首先明确“擦除”要什么输出保留印刷体、横线、边框去除手写笔触。这个输出既不是原图也不是纯背景而是介于两者之间的“干净版本”。判断标准是人眼基本看不出残留同时印刷笔画不能断裂。正因为有这两个目标传统方法才难办。例如用阈值分离手写笔迹再对分离出来的区域做插值填充这类方式在笔迹没有碰到印刷体时有效一旦手写笔迹与印刷体同色或在空间上重叠分离结果就会把印刷体边缘也当作待修复区。而深度学习学的是一个映射函数 f: X→Y其中 X 是混合图Y 是干净图。由于 X 和 Y 之间的差异在像素级上非常稀疏手写区域通常占整页不到 10%模型需要有足够的感受野去感知这一小片区域的上下文这也是为什么纯图像处理规则很难泛化。2.2 数据集的构建人工合成比人工标注更可控目前没有标准公共数据集专门用于试卷手写擦除常见做法是自建。人工标注成对数据意味着要拍摄同一张试卷“有笔迹”和“无笔迹”两个版本成本太高。我一般推荐用合成方式生成训练对先收集一批不带手写痕迹的试卷扫描图作为干净图再选择不同颜色、粗细、倾斜度的笔迹字体合成到干净图上。合成时要注意三点笔迹字体用常见手写体字体渲染文字随机调整位置和旋转。笔迹颜色铅笔灰、蓝色圆珠笔、黑色中性笔三类的比例要贴近实际场景。笔触宽度通过形态学膨胀模拟笔尖粗细范围控制在 2 到 6 像素。下面是一个使用 OpenCV 和 Pillow 生成训练对的 Python 示例import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont def synth_train_pair(clean_img_path, font_path, out_txt_path): # 读取干净图并转为 PIL RGB clean cv2.imread(clean_img_path) h, w clean.shape[:2] clean_rgb cv2.cvtColor(clean, cv2.COLOR_BGR2RGB) # 在副本上绘制手写笔迹 mixed clean_rgb.copy() pil_img Image.fromarray(mixed) draw ImageDraw.Draw(pil_img) font_size np.random.randint(24, 42) font ImageFont.truetype(font_path, font_size) import random sentences [答案x1, 证明如下, 不会做, 选C] sentence random.choice(sentences) x random.randint(0, w - len(sentence) * font_size) y random.randint(0, h - font_size) # 随机颜色蓝色、黑色、灰色 color_choice random.random() if color_choice 0.5: color (60, 80, 200) # 蓝色 elif color_choice 0.8: color (50, 50, 50) # 黑色 else: color (150, 150, 150) # 灰度铅笔 draw.text((x, y), sentence, fillcolor, fontfont) mixed np.array(pil_img) mixed_bgr cv2.cvtColor(mixed, cv2.COLOR_RGB2BGR) # 保存配对图像 cv2.imwrite(out_txt_path .mixed.jpg, mixed_bgr) cv2.imwrite(out_txt_path .clean.png, clean)代码做了什么读入一张干净的试卷图复制后在随机坐标用随机颜色和字体的手写句子覆盖最后保存两张图作为一对训练样本。这里没有使用深度学习库只用了 OpenCV 和 Pillow因此合成数据可以在任何环境里快速执行。参数说明font_size在 24 到 42 之间随机对应真实手写约 12 到 21 磅覆盖常见笔迹范围。color_choice控制三类笔迹比例蓝色一半、黑色三成、灰色两成这是为了模拟考试中最常见的笔迹。如果实际场景偏铅笔可以把灰色权重调高到 0.4。合成后的成对数据按train/、val/、test/划分每个文件夹下再分mixed/和clean/子目录。注意 clean 图不要用 JPEG 保存因为 JPEG 压缩会在印刷字边缘引入伪影让模型误以为这些伪影也需要擦除mixed 图用 JPEG 则可以让模型适应扫描噪声。2.3 数据增强与样本平衡模型容易在背景纹理上偷懒所以要做数据增强。但增强不是越多越好太强的几何变换会让印刷字也变形模型会把竖线、横线都当作笔迹。常用的增强手段如下表增强操作范围/参数适用原因亮度扰动0.8~1.2模拟扫描仪或手机拍照的纸张明暗变化高斯噪声σ5~10增加对扫描噪声的鲁棒性透视变换轻微 ±3°模拟试卷摆放倾斜旋转±2°避免模型依赖水平的文字方向膨胀/腐蚀kernel 2×2让笔迹粗细更多变增强时要注意配对的两个图像必须做完全相同的变换尤其是平移和裁剪不能错位。建议使用 albumentations 库的Compose设置additional_targets{clean: image}这样一条管道就能同时处理 mixed 和 clean。提示如果训练集里手写文字颜色分布极不平衡比如全是黑色模型会对蓝色笔迹完全失明。建议在合成阶段按 0.4/0.4/0.2 的比例混合三种颜色再在增强阶段用HueSaturationValue轻微扰动颜色避免模型过拟合到单一笔迹色。这一章的核心结论是合成数据质量比模型结构更影响最终效果。数据里手写与印刷重叠得越真实训练出来的模型在真实扫描图上越不会误擦印刷字。3. 模型架构与训练用 U-Net 还是 conditional GAN 做擦除3.1 模型选型U-Net 的强项与 GAN 的补强手写文字擦除本质上是逐像素的回归任务。U-Net 通过编码器提取语义解码器恢复细节跳跃连接保留边缘信息所以非常适合这类图像到图像问题。但只用 L1 损失训练的 U-Net输出经常偏模糊尤其在笔迹密集区域。为了让人眼看着更干净常见做法是引入条件 GAN加一个 PatchGAN 判别器逼真输出。我的建议是分阶段先用 U-Net 加 L1Loss 训练几十个 epoch得到一个干净基线再叠加 PatchGAN 做对抗训练。范围再小一点如果只是毕业设计验证核心流程U-Net 加 L1 已经能应付没交叠的笔迹交叠区域效果不佳时再上 GAN 收尾。3.2 PyTorch 实现的最小 U-Net 生成器这里给出一个适合单卡训练的轻量 U-Net输入输出均为 3 通道图像特征通道从 64 起步。核心代码如下import torch import torch.nn as nn class UNet(nn.Module): def __init__(self, in_ch3, out_ch3, base64): super().__init__() # 编码器每层做2次卷积随后池化 self.enc1 self._block(in_ch, base) self.pool1 nn.MaxPool2d(2) self.enc2 self._block(base, base*2) self.pool2 nn.MaxPool2d(2) self.enc3 self._block(base*2, base*4) self.pool3 nn.MaxPool2d(2) # 中间层 self.bridge self._block(base*4, base*8) # 解码器上采样后拼接跳跃连接 self.up1 nn.ConvTranspose2d(base*8, base*4, kernel_size2, stride2) self.dec1 self._block(base*8, base*4) self.up2 nn.ConvTranspose2d(base*4, base*2, kernel_size2, stride2) self.dec2 self._block(base*4, base*2) self.up3 nn.ConvTranspose2d(base*2, base, kernel_size2, stride2) self.dec3 self._block(base*2, base) # 输出层1x1卷积映射到3通道 self.outc nn.Conv2d(base, out_ch, kernel_size1) def _block(self, in_ch, out_ch): return nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool1(e1)) e3 self.enc3(self.pool2(e2)) bridge self.bridge(self.pool3(e3)) d1 torch.cat([self.up1(bridge), e3], dim1) d1 self.dec1(d1) d2 torch.cat([self.up2(d1), e2], dim1) d2 self.dec2(d2) d3 torch.cat([self.up3(d2), e1], dim1) d3 self.dec3(d3) return self.outc(d3)这段结构的关键点是跳跃连接把e1,e2,e3拼到解码器对应层让模型同时看到高分辨率边缘特征和抽象语义特征。padding1保持特征图尺寸不变后续拼接受影响kernel_size3是经典配置胜过 5×5 的感受野收益参数量也更少。3.3 训练循环与超参数设置训练图像建议切成 256×256 或 512×512 的块。扫描原图往往几千像素直接整图跑显存扛不住。核心训练代码criterion nn.L1Loss() optimizer torch.optim.Adam(model.parameters(), lr2e-4, betas(0.5, 0.999)) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.5) for epoch in range(total_epochs): model.train() for mixed_img, clean_img in train_loader: mixed_img, clean_img mixed_img.cuda(), clean_img.cuda() out model(mixed_img) loss criterion(out, clean_img) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()这里用 L1Loss 而不是 MSE因为 L1 对离群像素不敏感输出边缘更锐利。betas(0.5, 0.999)是 GAN 训练常用的 Adam 参数能让训练更稳定如果只做纯 U-Net 回归学习率可以调到 1e-3 以加快收敛但要注意预设 epoch 数也要相应减少。超参数经验值如下超参数推荐值说明image_size256小显存用 224过小会丢失手写纹理batch_size8~16取决于显存太小会导致 BN 统计不稳定lr2e-4条件 GAN 的常见初始学习率total_epochs60~100合成数据少epoch 过多会过拟合lossL1 SSIM混合损失在擦净程度与结构保留上更好optimizerAdam(betas(0.5, 0.999))比用 SGD 稳定收敛更快训练时不要只盯一个 loss 曲线。每 5 个 epoch 保存一次权重并随机抽 4 张验证图做可视化对比。如果验证图上印刷字边缘也被擦掉说明训练集里手写与印刷重叠的样本太少需要回 2.2 节把合成时的手写位置向印刷字方向偏移制造更多冲突样本。这一步往往比去调模型结构更有效。4. 运行说明与部署如何加载模型并擦除一整张试卷4.1 环境准备这类项目最常见的坑是 Python 环境混乱把深度学习组件和图像处理库混装在一起。推荐单独建一个虚拟环境再安装核心依赖torch、torchvision、opencv-python、pillow、numpy。如果没有 NVIDIA GPU装 CPU 版 torch 也能跑推理只是训练会很慢。python -m venv .venv source .venv/bin/activate pip install torch torchvision opencv-python pillow numpy安装后先执行python -c import torch; print(torch.__version__)验证加载是否正常。检查无误后再进入下一步。4.2 编写推理脚本推理脚本需要完成三件事读取模型权重、切块预测、拼回原图尺寸。下面是一个可直接运行的命令行版本import argparse import cv2 import numpy as np import torch from model import UNet # 假设model.py里定义了UNet def erase_handwriting(args): device torch.device(cuda if args.cuda else cpu) model UNet().to(device) # 加载本地训练好的模型权重 state torch.load(args.weight, map_locationdevice) model.load_state_dict(state[model] if model in state else state) model.eval() img cv2.imread(args.input) # BGR格式 h, w img.shape[:2] # padding到tile_size的整数倍避免拼接处错位 tile args.tile_size pad_h (tile - h % tile) % tile pad_w (tile - w % tile) % tile img_padded cv2.copyMakeBorder(img, 0, pad_h, 0, pad_w, cv2.BORDER_REPLICATE) out np.zeros_like(img_padded) with torch.no_grad(): for y in range(0, img_padded.shape[0], tile): for x in range(0, img_padded.shape[1], tile): crop img_padded[y:ytile, x:xtile] tensor torch.from_numpy(crop.transpose(2,0,1)).float().div(255.0).unsqueeze(0).to(device) pred model(tensor).squeeze().cpu().numpy().transpose(1,2,0) out[y:ytile, x:xtile] pred * 255.0 # 去掉padding恢复原图尺寸 out out[:h, :w].clip(0, 255).astype(np.uint8) cv2.imwrite(args.output, out) print(output saved to, args.output) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--input, requiredTrue) parser.add_argument(--output, requiredTrue) parser.add_argument(--weight, requiredTrue) parser.add_argument(--tile_size, typeint, default256) parser.add_argument(--cuda, actionstore_true) args parser.parse_args() erase_handwriting(args)这段代码用滑窗切块、推理、拼接是“加载本地模型”部署的标准套路。关键点是copyMakeBorder用BORDER_REPLICATE复制边缘像素而不是用全零补边否则图像边界会生成黑色伪影tile_size必须与训练时的image_size一致。如果扫描图本身有大块黑边建议在调用脚本前先自动裁剪否则黑边会被当成背景内容。4.3 参数调优与后处理命令行里最值得调的是tile_size。切块过小上下文不足手写擦不干净切块过大显存压力大。对 300 DPI 的试卷扫描图我一般设 256 或 384。如果发现预测图在拼缝位置出现横向或纵向条纹可以把步长从tile_size改成tile_size * 0.8重叠区域取平均值类似一个简化的平滑融合stride int(tile * 0.8) for y in range(0, img_padded.shape[0] - tile 1, stride): ...重叠预测的代价是推理耗时增加约 50%但能明显消除接缝。此外推理结果出来后还可以做一个小后处理对输出图与原图做像素差值差值大的区域是模型重点修改过的区域对这个掩码做轻微高斯模糊能削弱模型生成的假纹理。注意模糊半径不能超过 3 像素否则手写笔迹周围的印刷字也会被抹糊。5. 进阶技巧用颜色先验保护印刷字体减少重叠区域误擦5.1 颜色差分掩码的原理当手写笔迹与印刷体颜色接近时模型倾向于把两者一起擦掉。一个简单有效的防护方案是在推理后加一个颜色先验掩码笔迹颜色通常集中在小范围灰度或蓝色而印刷体一般是纯黑。在 HSV 颜色空间里手写笔迹的饱和度低、亮度中等印刷体则集中在暗部。利用这一点可以生成一个“需要擦除”的区域掩码只让模型输出覆盖掩码区域其他位置保留原图。def color_mask_alpha(original, output, lower(0, 0, 80), upper(179, 90, 180)): # original和output都是BGR, 转HSV hsv cv2.cvtColor(original, cv2.COLOR_BGR2HSV) # lower/upper: H, S, V阈值 mask cv2.inRange(hsv, lower, upper) # 膨胀, 覆盖手写笔迹边缘 kernel np.ones((3, 3), np.uint8) mask cv2.dilate(mask, kernel, iterations1) # 融合: 掩码区域用模型输出, 其余保留原图 mask_f mask.astype(np.float32)[..., None] / 255.0 blended (1 - mask_f) * original mask_f * output return blended.astype(np.uint8)这个函数把 HSV 中饱和度低于 90 且亮度在 80 到 180 之间的像素视为“可能的手写”并在这些区域采用模型输出。如果手写是蓝色饱和度下限可以放开到 120上限到 255增加对蓝色圆珠笔的覆盖。5.2 验证方法如何量化擦除效果只靠肉眼判断很容易被自己的主观感受带偏。常用的量化指标是 PSNR 和 SSIM但如果手头没有真值干净图可以计算“擦除前后印刷边缘的保留率”。先对原图做边缘提取再对输出做同样的边缘提取统计两幅边缘图的重叠比例作为“印刷结构保留分”。重叠比低于 0.95 说明模型把印刷笔画的边缘也误擦了需要调高颜色掩码的保留权重或增加训练数据中的重叠样本。提示颜色掩码的阈值需要根据实际扫描纸的颜色重新标定。白纸上写蓝色笔迹的下限一般设为(100, 120, 150)如果你要处理的是黄旧试卷饱和度下限要降到 60 左右。这个颜色先验方法并不增加模型复杂度却能显著减少重叠区域误擦是这类系统在上线前最值得做的一次加固。本文还有配套的精品资源点击获取
返回列表