简介这份资源面向深度学习图像处理方向的学习者与开发者聚焦试卷场景下的手写文字擦除任务提供从数据构造到模型训练、推理测试的完整工程实现。包内共30个文件以22个Python脚本为核心覆盖网络结构、损失函数、数据加载与预测流程另含3个Shell脚本用于训练与测试调度以及readme、说明文档等辅助材料压缩包约94KB结构紧凑便于快速上手。数据方面使用1081对训练样本与81张验证集针对手写红黑蓝多色、手画线段图案、污渍脏点及与印刷字重叠等难点通过RGB通道平均差值生成mask并采用横向翻转、小角度旋转与512×512随机裁剪增强。训练分两阶段先以dice与l1联合损失收敛再单独用l1精调测试采用分块与交错分块策略配合镜像padding和边缘重叠保留中心预测提升大分辨率图像效果。目前已有256人学习适合希望复现赛题方案、理解擦除模型细节的读者参考。1. 试卷手写文字擦除从一张答题卡到干净底图这套源码能省下多少标注时间改过卷子的老师、做过教育硬件的工程师大概都遇到过同一个尴尬想把学生的答题卡扫描件重新排版、做电子归档或者把题目抠出来复用结果手写笔迹像牛皮癣一样糊在印刷体上OCR 一跑全是噪声。基于深度学习的试卷手写文字擦除干的就是这件事——输入一张带手写答案的试卷图输出一张只剩印刷题干、手写痕迹被抹掉的干净底图。它属于图像到图像的翻译任务和照片修复模型、文档去噪是同一类思路但难点在于「擦除」比「修复」更苛刻擦完不能留灰影不能把印刷体一起吃掉还不能在空白处凭空生成笔画。这套「源码模型文档说明」的组合价值不在于算法多新而在于它把数据构造、训练、推理、后处理这条链路打包好了新手能跑通熟手能改。适合谁做智慧作业、试卷电子化、题库录入的团队以及想拿一个真实文档场景练手深度学习的人。2. 手写擦除到底在擦什么任务定义、数据形态与模型选型2.1 擦除任务和普通去噪的本质区别很多人第一反应是「这不就是图像修复吗」其实差得远。图像修复inpainting是已知一块 mask 区域让模型把这块补全补出来的内容只要视觉合理就行。手写擦除不一样你没有精确的手写 mask得让模型自己判断哪些像素是手写、哪些是印刷体然后把手写区域替换成「本该在那里的印刷背景」。也就是说它同时要做手写/印刷体分离和背景重建两件事。这就决定了数据必须是成对的一张带手写的图一张对应的干净图。干净图从哪来常见做法是拿纯印刷体的空白试卷当底图再用程序把手写文字合成上去合成时记录下手写的位置和透明度这样既得到训练对又能顺手生成监督 mask。真实场景里也有用「同一份卷子学生写之前拍一张、写之后拍一张」来配对的但配准和对齐的坑极多一般作为验证集补充。从任务形态看它和 OCR 是互补关系OCR 关心「手写写了什么」擦除关心「手写在哪、怎么抹掉」。有些方案会把两者串起来先检测手写区域再擦但端到端模型更省事。2.2 为什么主流选生成式结构而不是纯分割纯分割思路是预测一个手写 mask然后把 mask 区域用周围像素填掉。问题在于填的方式太粗暴印刷体笔画穿过手写区域时会被切断出现断线。生成式结构编码器-解码器 跳跃连接或带注意力/Transformer 模块的变体能根据上下文重建被遮挡的印刷笔画效果明显更稳。选型上如果显存有限比如 8G 卡我一般会从轻量 U-Net 类结构起步输入裁到 512×512 或 768×768 的 patch 训练显存宽裕再上带 Transformer 的混合结构。文档说明里通常会给出推荐输入尺寸和 batch size照着调比盲目堆大模型划算。低显存运行模型这块梯度累积和混合精度是标配别硬扛。2.3 数据合成手写字体、颜色、透明度和背景噪声合成数据的质量直接决定模型上限。核心参数有四个字体、颜色、透明度、形变。字体准备 510 种手写风格字体覆盖工整和潦草两端别只用一种否则模型只认这一种笔迹。颜色黑、蓝、蓝黑为主偶尔加红笔批注RGB 值在合理范围内随机抖动。透明度alpha 在 0.61.0 之间随机模拟铅笔淡、钢笔浓。形变轻微旋转±3°、缩放、基线抖动模拟真实书写不齐。下面是一段合成脚本的核心逻辑用 PIL 把文字贴到空白卷面上from PIL import Image, ImageDraw, ImageFont import random def synthesize_handwriting(bg_path, text, font_paths, out_path): bg Image.open(bg_path).convert(RGB) draw ImageDraw.Draw(bg, RGBA) # 随机选字体和字号字号跟卷面高度挂钩 font_path random.choice(font_paths) font_size random.randint(28, 48) font ImageFont.truetype(font_path, font_size) # 随机颜色和透明度模拟不同笔 color random.choice([(20, 20, 20), (10, 30, 120), (30, 30, 90)]) alpha random.randint(150, 255) # 随机落点避开边缘 x random.randint(50, bg.width - 300) y random.randint(50, bg.height - 100) draw.text((x, y), text, fontfont, fillcolor (alpha,)) bg.save(out_path) # 参数说明 # bg_path 是纯印刷体空白卷面font_paths 是手写字体列表 # alpha 越低笔迹越淡训练时要覆盖淡笔场景否则推理遇到铅笔字会漏擦 # 落点范围要留边避免文字被裁掉导致监督信号不完整逻辑上这段脚本只负责「贴字」真正训练时还要同步生成 mask把 draw.text 的落点区域记录成二值图作为辅助监督。参数里最容易翻车的是 alpha 范围——如果合成时全是浓墨模型上线遇到淡铅笔字就擦不干净这是血泪经验。2.4 训练目标与损失组合单用 L1 损失会让结果发糊单用对抗损失又容易生成假笔画。常见组合是L1或 Charbonnier保内容 感知损失保结构 对抗损失保真实感权重按 1 : 0.1 : 0.01 量级起步。文档说明里如果给了权重表先照抄跑通再按自己数据微调。训练时建议保留验证集的可视化输出每几个 epoch 存一张对比图比盯 loss 曲线直观得多。3. 把源码跑起来环境、目录、训练与推理的最小闭环3.1 环境依赖与目录结构怎么读拿到一个「源码模型文档说明」的压缩包先别急着 pip install。第一步是看目录典型结构长这样目录/文件作用先看哪个train.py/infer.py训练与推理入口先看 infer确认能出图models/网络结构定义看输入输出通道数data/数据集与合成脚本确认配对格式weights/预训练模型确认是否可直接加载configs/超参配置改路径和 batch sizeREADME/ 文档说明对照命令逐条试环境上PyTorch torchvision 是主流CUDA 版本要和驱动匹配。我一般先建独立虚拟环境再按文档给的版本装避免和本机已有环境打架。conda create -n erase python3.9 -y conda activate erase # 按文档给的版本装别直接装最新版本错位是翻车重灾区 pip install torch1.13.1 torchvision0.14.1 pip install opencv-python pillow numpy tqdm装完先跑推理不要一上来就训练。推理能出图说明模型加载和前后处理链路是通的再去碰训练。3.2 用预训练模型跑通第一张擦除图推理脚本一般接受单图或文件夹输入。核心是三步读图、归一化、过模型、反归一化、保存。import torch, cv2, numpy as np from models.unet import EraseNet # 按实际模块名替换 device cuda if torch.cuda.is_available() else cpu model EraseNet().to(device) ckpt torch.load(weights/erase_best.pth, map_locationdevice) model.load_state_dict(ckpt[state_dict]) model.eval() img cv2.imread(test_paper.jpg)[:, :, ::-1] / 255.0 # BGR 转 RGB 并归一化 inp torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0).float().to(device) with torch.no_grad(): out model(inp) out out.squeeze(0).permute(1, 2, 0).cpu().numpy() out np.clip(out * 255, 0, 255).astype(np.uint8) cv2.imwrite(clean.png, out[:, :, ::-1])参数说明归一化方式必须和训练时一致训练用 [0,1] 推理就不能用 [-1,1]否则输出全灰。输入尺寸如果和训练不一致要么 resize 要么切 patch直接喂大图容易爆显存或边缘失真。跑完第一张重点看三个地方手写有没有残留、印刷体有没有被吃掉、空白处有没有伪影。3.3 训练自己的数据配置、启动与断点续训训练前把数据整理成配对格式常见是images/放带手写图labels/放干净图文件名一一对应。配置文件里改四个地方数据路径、batch size、学习率、epoch 数。python train.py \ --config configs/base.yaml \ --data_root ./dataset \ --batch_size 8 \ --lr 1e-4 \ --epochs 100 \ --resume weights/erase_best.pthbatch size 受显存限制8G 卡从 4 或 8 起步不够就开梯度累积。学习率 1e-4 是常见起点loss 震荡就降到 5e-5。断点续训靠--resume训练中断别从头再来。训练过程中每 5 个 epoch 存一次权重同时存一张验证图方便回看哪个阶段效果最好。3.4 推理后处理二值化、去灰影与批量脚本模型输出往往带一点灰影直接交付不够干净。后处理两步一是按阈值把接近白的像素拉纯白二是对残留的浅灰做一次轻度对比度拉伸。import cv2, numpy as np, glob, os for p in glob.glob(raw_out/*.png): img cv2.imread(p) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高于 235 的拉纯白压掉灰影 gray[gray 235] 255 # 轻度对比度拉伸让印刷体更实 gray cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX) cv2.imwrite(os.path.join(final, os.path.basename(p)), gray)阈值 235 不是固定的卷面偏黄或扫描偏暗时要往下调调到印刷体不断线为止。批量脚本记得加异常捕获单张图损坏别让整个批次挂掉。4. 擦除效果总是不干净这几处排查比调参更管用4.1 手写残留淡笔和连笔是重灾区现象浓墨手写擦掉了铅笔字和连笔还留一层浅影。原因通常是合成数据里淡笔样本太少模型没见过低对比度笔迹。解决在合成脚本里把 alpha 下限压到 100 左右专门补一批淡笔样本重训推理端配合 4.4 的阈值后处理兜底。4.2 印刷体被误擦笔画断裂和字消失现象题干里的细笔画被当成手写抹掉出现断字。原因是模型对手写和印刷体的边界学得不够或者训练时干净图本身有噪声。解决检查配对数据里干净图是否真的干净混入脏标签会让模型学歪损失里适当提高内容损失权重压制过度生成。4.3 空白处出现伪影模型在「脑补」现象原本空白的地方冒出莫名其妙的笔画或纹理。这是对抗损失权重过高、生成器过度自由的典型表现。解决把对抗损失权重降一个量级或加一个「非手写区域应保持不变」的约束损失让模型在没手写的地方老实输出原图。4.4 输出整体发灰归一化和后处理没对齐现象擦除结果像蒙了一层雾。原因多半是推理归一化方式和训练不一致或者后处理阈值设太高把该保留的灰也压了。解决先确认训练时的归一化区间再检查后处理阈值从 235 往下试到 210找到印刷体清晰、灰影消失的平衡点。4.5 显存爆掉输入尺寸和 batch 没算清现象训练跑几步就 OOM。原因是输入分辨率乘 batch size 超了显存。解决优先降 batch 开梯度累积其次把训练 patch 从 768 降到 512别一上来就砍模型层数那会伤效果。5. 让擦除结果真正能用评估指标、可视化对比和一个提效技巧跑通不等于能用得有一套判断「擦干净了没有」的方法。最直接的是人工看但批量交付不能全靠眼睛。我一般用三个指标交叉验证PSNR 和 SSIM 衡量和干净图的整体接近度手写区域内的残差均值衡量擦除彻底程度。注意 PSNR 高不代表视觉好发糊的图 PSNR 也可能不低所以必须配可视化。可视化对比建议做成三联图原图、模型输出、干净参考图横向拼一起每 20 张抽一张存盘。看的时候重点盯三处——淡笔、印刷体细笔画、空白区。这三处过关基本就能交付。一个提效技巧把擦除和 OCR 串成流水线。先擦除得到干净底图再送 OCR 识别题干识别准确率会比直接识别带手写的图高不少。如果目标是题库录入这条链路能省掉大量人工校对。具体做法是擦除输出统一转灰度、二值化后再进 OCR减少颜色干扰。评估脚本可以这样写import cv2, numpy as np from skimage.metrics import peak_signal_noise_ratio as psnr from skimage.metrics import structural_similarity as ssim def evaluate(pred_path, gt_path): pred cv2.imread(pred_path, 0) gt cv2.imread(gt_path, 0) # 尺寸不一致先对齐否则指标无意义 if pred.shape ! gt.shape: pred cv2.resize(pred, (gt.shape[1], gt.shape[0])) p psnr(gt, pred) s ssim(gt, pred) # 残差均值越小说明擦得越干净 diff np.abs(gt.astype(np.float32) - pred.astype(np.float32)).mean() return p, s, diff # 参数说明PSNR 低于 30 基本要回炉SSIM 低于 0.9 说明结构丢得多 # diff 是辅助参考单看它会被整体亮度偏移误导要和可视化一起看指标只是筛子最终还得靠眼睛。我自己踩过最深的坑是早期只看 loss 下降就以为成了结果上线发现淡铅笔字全漏回头补了两千张淡笔合成样本才救回来。所以现在的习惯是每训一版先抽 50 张覆盖浓淡、工整潦草、有无批注的样本做可视化指标达标且肉眼过关才进下一轮。这套流程慢一点但省下的返工时间远超那点等待。希望帮到你。本文还有配套的精品资源点击获取