news 2026/10/1 5:42:11

基于深度学习的试卷手写擦除:U-Net与GAN实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于深度学习的试卷手写擦除:U-Net与GAN实战指南

简介:本资源为基于深度学习的试卷手写文字擦除毕业设计完整项目包,面向计算机视觉方向的高年级本科生与研究生,以及需要复现图像文字擦除任务的开发者。项目围绕从试卷、文献扫描件中去除手写笔迹并保留背景信息这一核心问题,提供源码、模型与文档说明三部分内容。压缩包共29个文件,以22个Python脚本为主体,涵盖数据加载、损失函数、网络结构与训练测试流程,另含3个Shell脚本用于训练与转换,以及readme和md文档辅助理解,整体约94KB。源码中可见U-Net、BiSeNetV2、SA-GAN、非局部模块等网络实现,并配有判别器、EMA与ONNX转换脚本,便于读者研究生成对抗与分割思路在手写擦除中的结合。文档部分说明项目背景、算法原理与实验结果,帮助快速上手与二次开发。目前已有124人学习,适合作为毕业设计参考或图像修复方向的入门实践。

1. 试卷手写擦除:从「拍完照还得手动涂」到模型一键还原空白卷

带过毕业设计的人大概都经历过这种场景:把批改过的试卷拍成照片,想重新打印一份空白卷给学生再做一遍,结果发现上面的红笔批注、学生手写的答案、甚至铅笔草稿全都印在纸上,用修图软件一点点擦,一张卷子能耗掉半小时。基于深度学习的试卷手写文字擦除,要解决的就是这件事——输入一张有手写痕迹的试卷照片,模型自动把手写笔画抹掉,同时把印刷体的题干、表格线、公式符号原样保留下来。它属于图像到图像的转换任务,但和普通去水印、去噪点不一样:手写文字和印刷文字在颜色、笔画粗细、位置上高度混杂,模型必须学会区分「哪些像素该删、哪些像素必须留」。这套「源码+模型+文档说明」的组合,适合做毕业设计的学生、想入门图像修复方向的开发者,以及需要批量处理试卷扫描件的教务场景。下面按「原理选型 → 数据准备 → 训练调参 → 推理部署 → 避坑」的顺序,把这条路走通。

2. 擦除任务的技术选型:为什么多数方案落在 GAN 和 U-Net 之间

2.1 手写擦除本质是「带条件约束的图像修复」

先把问题定义清楚。给定输入图像 $I$,希望输出图像 $O$,满足:手写区域被替换成背景(通常是白纸或表格底色),非手写区域像素尽量不变。这跟图像修复(inpainting)的区别在于,修复的掩码是已知的、人为给定的,而手写擦除的掩码需要模型自己预测——没人会提前告诉你哪一笔是手写的。所以主流做法分两条路:一条是两阶段,先做手写检测分割出掩码,再用修复网络填补;另一条是端到端,直接让生成网络输出擦除后的图像。毕业设计里常见的是端到端,因为流程短、代码量可控,但两阶段在可解释性和可控性上更好。

从网络结构看,U-Net 系列是绕不开的基线。它的编码器-解码器加跳跃连接,天然适合「输入输出尺寸一致、且需要保留高频细节」的任务。试卷上的表格线、小字号印刷体都属于高频信息,纯编码器-解码器结构容易在降采样过程中把这些细节丢掉,跳跃连接能把浅层特征直接送到对应解码层,缓解这个问题。我一般会建议先跑通一个 U-Net 基线,再考虑加对抗损失。

2.2 GAN 判别器带来的「真实感」与训练不稳定

只靠 L1/L2 重建损失训练出来的结果,往往偏模糊——模型倾向于输出所有可能结果的「平均值」,手写笔画边缘会留下灰蒙蒙的残影。生成对抗网络(GAN)的思路是加一个判别器,让它判断输出图像「像不像真实空白卷」,生成器则努力骗过判别器。这样输出会更锐利、更接近真实纸张质感。代价是训练不稳定,判别器太强会导致生成器梯度消失,太弱又起不到约束作用。常见做法是给对抗损失加一个权重系数,比如 0.01 到 0.1 之间,并且用谱归一化(Spectral Normalization)稳定判别器。毕业设计里如果时间紧,可以先不上 GAN,用 U-Net + L1 出第一版结果,再迭代。

2.3 损失函数组合:像素损失、感知损失、对抗损失怎么配

单一损失很难同时管住「擦干净」和「不误伤」。像素级 L1 损失保证整体结构不跑偏,但对模糊不敏感;感知损失(Perceptual Loss)用预训练 VGG 提取特征做比较,能让输出在语义层面更接近真实空白卷;对抗损失负责提升纹理真实感。一个可用的配比是 L1 权重 1.0、感知损失权重 0.1、对抗损失权重 0.01。这个配比不是金科玉律,但作为起点能跑出可看的结果。调参时优先动对抗损失权重,它对手写残留和印刷体误擦的影响最直接。

2.4 数据从哪来:合成配对样本是毕业设计最现实的路径

真实场景下很难拿到「同一张试卷,擦除前和擦除后」的配对数据。常见做法是合成:找一批干净的空白试卷扫描件或电子版渲染图,作为目标图像 $O$;然后往上面随机叠加手写文字,作为输入图像 $I$。手写文字可以从手写数据集(如 CASIA-HWDB 等公开手写库)里裁剪单字或整行,随机调整颜色(红、蓝、黑)、透明度、旋转角度、缩放比例,再贴到试卷的答题区域。这样就有了配对样本。合成数据的质量直接决定模型上限——如果合成时手写总是贴在空白处,模型就学不会处理「手写压在印刷体上」的情况。所以合成策略要尽量覆盖:手写覆盖印刷体、手写覆盖表格线、手写超出答题框、不同笔迹粗细和颜色。

3. 从零跑通训练:数据合成、网络搭建与参数配置

3.1 合成配对数据的脚本实现

下面这段代码演示如何把一行手写文字贴到试卷图像上,生成训练对。实际项目中会批量循环处理,这里只保留核心逻辑。

import cv2 import numpy as np import random def synthesize_pair(clean_paper, handwriting, position=None): """ clean_paper: 空白试卷图像, BGR, shape (H, W, 3) handwriting: 手写文字图像, BGRA, 带透明通道 返回: (input_img, target_img) """ H, W = clean_paper.shape[:2] h, w = handwriting.shape[:2] # 随机缩放,模拟不同书写大小 scale = random.uniform(0.6, 1.2) new_w, new_h = int(w * scale), int(h * scale) handwriting = cv2.resize(handwriting, (new_w, new_h)) # 随机选择粘贴位置,若未指定则限制在图像内 if position is None: x = random.randint(0, max(0, W - new_w)) y = random.randint(0, max(0, H - new_h)) else: x, y = position # 提取 alpha 通道做融合 alpha = handwriting[:, :, 3:4] / 255.0 bgr = handwriting[:, :, :3] roi = clean_paper[y:y+new_h, x:x+new_w].astype(np.float32) blended = bgr * alpha + roi * (1 - alpha) input_img = clean_paper.copy() input_img[y:y+new_h, x:x+new_w] = blended.astype(np.uint8) return input_img, clean_paper.copy()

这段逻辑的关键在 alpha 融合:手写笔画的透明通道决定了它在纸面上的覆盖程度,alpha 越接近 1 越不透明。参数上,scale控制手写大小,建议覆盖 0.6 到 1.2 倍;粘贴位置要允许手写压到印刷体上,不能只往空白区域贴。合成时还要注意手写颜色,红色批改笔和黑色答题笔在颜色空间上差异大,训练集里两种都要有。

3.2 U-Net 生成器的搭建与通道数设置

生成器用经典 U-Net 结构,输入 3 通道 RGB 图像,输出 3 通道擦除后图像。编码器每层通道数可以设为 64、128、256、512,解码器对称。下面给出 PyTorch 版本的核心模块。

import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.net = nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True) ) def forward(self, x): return self.net(x) class UNetGenerator(nn.Module): def __init__(self, in_ch=3, out_ch=3, base=64): super().__init__() self.enc1 = DoubleConv(in_ch, base) self.enc2 = DoubleConv(base, base*2) self.enc3 = DoubleConv(base*2, base*4) self.enc4 = DoubleConv(base*4, base*8) self.pool = nn.MaxPool2d(2) self.bottleneck = DoubleConv(base*8, base*16) self.up4 = nn.ConvTranspose2d(base*16, base*8, 2, stride=2) self.dec4 = DoubleConv(base*16, base*8) self.up3 = nn.ConvTranspose2d(base*8, base*4, 2, stride=2) self.dec3 = DoubleConv(base*8, base*4) self.up2 = nn.ConvTranspose2d(base*4, base*2, 2, stride=2) self.dec2 = DoubleConv(base*4, base*2) self.up1 = nn.ConvTranspose2d(base*2, base, 2, stride=2) self.dec1 = DoubleConv(base*2, base) self.out = nn.Conv2d(base, out_ch, 1) def forward(self, x): e1 = self.enc1(x) e2 = self.enc2(self.pool(e1)) e3 = self.enc3(self.pool(e2)) e4 = self.enc4(self.pool(e3)) b = self.bottleneck(self.pool(e4)) d4 = self.dec4(torch.cat([self.up4(b), e4], dim=1)) d3 = self.dec3(torch.cat([self.up3(d4), e3], dim=1)) d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1)) d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1)) return torch.sigmoid(self.out(d1))

base控制首层通道数,显存不够就降到 32。跳跃连接用torch.cat拼接,这是 U-Net 保留细节的关键。输出层用 Sigmoid 把像素值压到 0 到 1 之间,训练时目标图像也要归一化到同一范围。如果显存紧张,可以把base设为 32,同时把输入裁剪成 256×256 的块训练,推理时再滑窗拼接。

3.3 训练循环与关键超参数

训练循环里要同时算 L1 损失和对抗损失。判别器用 PatchGAN,输出一个特征图而不是单个标量,能更好捕捉局部纹理。

import torch.optim as optim device = torch.device("cuda" if torch.cuda.is_available() else "cpu") G = UNetGenerator(base=64).to(device) D = PatchDiscriminator().to(device) opt_G = optim.Adam(G.parameters(), lr=2e-4, betas=(0.5, 0.999)) opt_D = optim.Adam(D.parameters(), lr=2e-4, betas=(0.5, 0.999)) l1_loss = nn.L1Loss() bce_loss = nn.BCEWithLogitsLoss() lambda_l1 = 1.0 lambda_adv = 0.01 for epoch in range(num_epochs): for inp, tgt in dataloader: inp, tgt = inp.to(device), tgt.to(device) # 训练生成器 fake = G(inp) pred_fake = D(fake) loss_g_adv = bce_loss(pred_fake, torch.ones_like(pred_fake)) loss_g_l1 = l1_loss(fake, tgt) loss_g = lambda_l1 * loss_g_l1 + lambda_adv * loss_g_adv opt_G.zero_grad() loss_g.backward() opt_G.step() # 训练判别器 pred_real = D(tgt) pred_fake = D(fake.detach()) loss_d = 0.5 * (bce_loss(pred_real, torch.ones_like(pred_real)) + bce_loss(pred_fake, torch.zeros_like(pred_fake))) opt_D.zero_grad() loss_d.backward() opt_D.step()

学习率 2e-4 是 GAN 训练的常用起点,Adam 的 betas 设成 (0.5, 0.999) 比默认的 (0.9, 0.999) 更稳。lambda_adv从 0.01 开始,如果发现手写残留明显就往上调到 0.05,如果印刷体被误擦就往下调。判别器每步都更新,但生成器可以每两步更新一次,防止判别器过强。训练时每 5 个 epoch 存一次权重,方便回滚。

4. 推理与部署:把模型跑成能用的擦除工具

4.1 单张试卷的推理流程

推理时不需要判别器,只加载生成器权重。输入图像先归一化到 0 到 1,送进网络,输出再乘 255 转回 uint8。

def erase_handwriting(model, img_path, out_path, img_size=512): model.eval() img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w = img.shape[:2] # 缩放到网络输入尺寸,记录原始尺寸用于还原 inp = cv2.resize(img, (img_size, img_size)) inp = inp.astype(np.float32) / 255.0 inp = torch.from_numpy(inp).permute(2, 0, 1).unsqueeze(0).to(device) with torch.no_grad(): out = model(inp) out = out.squeeze(0).permute(1, 2, 0).cpu().numpy() out = (out * 255).astype(np.uint8) out = cv2.resize(out, (w, h)) out = cv2.cvtColor(out, cv2.COLOR_RGB2BGR) cv2.imwrite(out_path, out)

缩放会损失细节,如果试卷文字小,建议用 512 或 768 的输入尺寸。显存不够就分块推理再拼接,块与块之间留 32 像素重叠,避免拼接缝。推理速度上,512×512 输入在单张消费级显卡上大约几十毫秒,批量处理几百张试卷完全可行。

4.2 后处理:二值化和对比度增强

模型输出偶尔会在手写位置留下浅灰色残影,肉眼能看出来。加一步后处理能明显改善:先转灰度,做自适应阈值二值化,再把结果与原图做加权融合。

def postprocess(erased_img, original_img, alpha=0.7): gray = cv2.cvtColor(erased_img, cv2.COLOR_BGR2GRAY) binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 10) binary = cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR) # 用二值化结果压制残影,同时保留原图色彩 result = cv2.addWeighted(erased_img, alpha, binary, 1 - alpha, 0) return result

alpha控制融合比例,0.7 表示更信任模型输出,0.5 表示更信任二值化结果。如果试卷本身有底色或表格线颜色浅,二值化阈值要调小,否则会把表格线也吃掉。这一步不是必须的,但加上之后视觉干净度提升明显。

4.3 批量处理与文件组织

毕业设计交付时通常要能批量跑。建议目录结构按input/、output/、weights/分开,推理脚本遍历input/下所有图片,输出到output/,文件名保持一致。权重文件单独放,方便替换。如果要做成可交互工具,可以用 Gradio 或 Streamlit 包一层界面,上传图片、点按钮、下载结果,几十行代码就能搞定。文档说明里要写清楚环境依赖、权重放置路径、运行命令,以及输入图像的推荐分辨率。

5. 避坑与排查:手写擦不干净、印刷体被误擦怎么办

5.1 手写残留明显,尤其是红色批改笔

现象:模型输出里手写笔画变淡了但没完全消失,红色批改痕迹尤其明显。原因通常是训练集里红色手写样本太少,或者 L1 损失权重过高导致模型倾向于「保守」输出。解决:在合成数据阶段提高红色手写的比例,同时把对抗损失权重从 0.01 提到 0.05,让生成器更激进地抹除。如果还不行,检查输入图像是否做过颜色归一化,不同扫描设备的色偏会影响模型判断。

5.2 印刷体被误擦,表格线断裂

现象:题干文字变淡、表格线中间断开。原因一般是跳跃连接不够强,或者训练时目标图像本身就有噪声。解决:确认目标图像是干净的空白卷,如果目标图像里表格线就不清晰,模型学到的就是「擦掉模糊线条」。另外可以把 L1 损失权重从 1.0 提到 2.0,让模型更严格地保持非手写区域。网络层面可以增加编码器浅层通道数,保留更多高频细节。

5.3 训练到一半判别器 loss 变成 0,生成器输出全白

现象:判别器 loss 骤降,生成器输出一片白或一片灰。原因是判别器太强,生成器梯度消失。解决:降低判别器学习率到 1e-4,或者把判别器更新频率降到每两步一次。也可以在判别器里加谱归一化,限制它的 Lipschitz 常数。如果已经崩了,回滚到崩之前的权重,调低lambda_adv重新训练。

5.4 推理时显存溢出

现象:训练能跑,推理大图时 OOM。原因是推理输入尺寸比训练时大。解决:分块推理,每块 256×256,重叠 32 像素,输出拼接时取重叠区域的平均值。或者直接把推理尺寸降到 384,牺牲一点细节换稳定。如果显卡显存小于 6GB,训练时base设 32,推理时也保持一致。

5.5 合成数据与真实试卷分布差距大

现象:合成数据上指标很好,真实试卷上效果差。原因是合成时手写字体、颜色、纸张纹理和真实场景不匹配。解决:收集少量真实试卷,人工标注手写区域做掩码,用这些真实样本对模型做微调。哪怕只有几十张,也能明显缩小域差距。另外合成时可以加入纸张纹理、光照不均、扫描噪声等增强,让训练数据更接近真实分布。

6. 进阶技巧:用注意力机制和低显存策略把效果再推一截

如果基线跑通后还想提升,有两个方向值得试。第一个是在 U-Net 的跳跃连接处加注意力门控(Attention Gate),让解码器在融合浅层特征时自动聚焦到手写区域,抑制无关背景。实现上就是在拼接前用一个 1×1 卷积加 Sigmoid 生成空间注意力图,乘到浅层特征上。这个改动代码量不大,但在手写与印刷体混杂的场景下,能减少印刷体被误擦的概率。

第二个是低显存训练策略。毕业设计常用笔记本显卡,显存可能只有 4GB 到 6GB。除了减小base和输入尺寸,还可以用混合精度训练(AMP),把部分计算转成 float16,显存占用能降三成左右。PyTorch 里用torch.cuda.amp几行就能开启。另外梯度累积也能在有限显存下模拟大 batch:每算完一个小 batch 不立即更新,累积几次梯度再更新一次,效果接近大 batch 训练。

验证模型是否真的学到东西,不能只看 loss 曲线。我习惯准备一组固定测试样本,每个 epoch 结束后跑一遍推理,把输入、输出、目标三张图拼在一起看。重点看三个地方:手写是否完全消失、印刷体是否完整保留、表格线是否连续。如果 loss 在降但视觉没改善,说明损失函数和实际目标有偏差,得回去调权重。这套流程走下来,从数据合成到推理部署,一个毕业设计的完整闭环就成型了。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 5:41:01

iOS 上跑 Windows 应用:Wine + FEX-Emu + DXMT 跨架构翻译实战

1. 项目缘起:为什么要在 iOS 上折腾 Wine“Madeira”这个项目标题,乍一看像是个地名,但在我们这行里,它指向的是一套非常具体的工程实践:在 iOS 设备上通过 Wine 及其衍生方案运行 x86-64 架构的 Windows 应用。热搜词…

作者头像 李华
网站建设 2026/10/1 5:41:01

openrig开放式工作站:选型、装机与排障实战

1. 项目概述与设计思路拆解1.1 openrig 到底是什么如果你在网上搜 openrig,大概率会看到一堆风格迥异的结果——有人拿它当开源机械键盘项目的代号,有人用来称呼自组赛车模拟器支架,但在玩机圈和内容创作圈里,它更多被当作Open Ri…

作者头像 李华
网站建设 2026/10/1 5:40:17

YT Config Tools引脚配置Excel导出:工程级结构化数据生成

1. 这不是普通导出——YT Config Tools引脚配置清单Excel化的真实价值你手头有一块新拿到的嵌入式开发板,芯片手册厚达800页,引脚定义散落在“Pin Multiplexing”“I/O Configuration”“Electrical Characteristics”三个独立章节里;你刚接手…

作者头像 李华
网站建设 2026/10/1 5:40:17

Vivado实战排错指南:从驱动签名到时序违例的五级故障诊断

1. 这不是“教程”,而是我三年Vivado实战踩坑后整理的生存手册Vivado不是软件,是FPGA工程师的日常战场。你打开它,不是为了点几下鼠标生成bit流,而是要和时序收敛、仿真发散、板卡识别失败、License过期、ILA抓不到信号这些具体问…

作者头像 李华
网站建设 2026/10/1 5:40:16

Java读写锁ReentrantReadWriteLock源码解析与本地缓存压测实战

做并发编程的都知道,synchronized和ReentrantLock虽然可靠,但有个很要命的点:不管你是读数据还是写数据,所有线程都得抢同一把互斥锁。你想想,实际业务里读多写少才是常态,配置中心、路由表、本地缓存&…

作者头像 李华
网站建设 2026/10/1 5:40:15

OpenCV+Qt+YOLO构建人形检测系统:从脚本到桌面应用的完整实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华