news 2026/9/28 16:50:40

基于U-Net的手写试卷擦除系统:端到端图像重建实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于U-Net的手写试卷擦除系统:端到端图像重建实践

简介:本资源是一个基于深度学习的试卷手写文字智能擦除系统,面向计算机、人工智能、大数据等专业的本科生及毕设/课程设计学习者,解决考试卷面手写内容自动化清除与图像复原的实际问题。项目含62个文件,主体为44个Python源码(涵盖模型定义、训练/测试/预测流程、损失函数、数据加载等核心模块)、6个Shell脚本(用于环境配置、模型转换与批量处理)、4份README/说明文档及2个辅助ZIP包,整体仅190KB,轻量易部署。已有166人下载学习,项目源自高分毕业设计(评审98.5分),提供完整可运行代码、预训练模型、详细运行说明及二次开发支持。读者可直接复现端到端擦除效果,深入理解BiSeNetV2、SA-GAN等轻量级图像修复架构,掌握PSNR损失设计、掩码生成、ONNX模型转换等关键技术点,并基于现有结构快速拓展至其他文档清洁场景。

1. 为什么一张扫描试卷上的手写答案,用传统图像处理总擦不干净?——这个毕设用深度学习把“橡皮擦”变成了可训练的神经网络

你有没有试过:把学生手写的数学试卷扫描成图,想自动擦掉答题区的手写内容,只留下印刷题干用于归档或二次出题?OpenCV 膨胀腐蚀试了八遍,阈值调到眼花,结果不是擦不净(墨迹残留像鬼影),就是连印刷体一起吃掉(题干被啃掉半边)。这不是你技术不行,是问题本质错了——手写和印刷在像素级上不是“颜色深浅不同”,而是纹理结构、笔锋走向、纸张形变、墨水渗透的复合差异。这个毕设标题里的“基于深度学习开发的试卷手写文字擦除系统”,核心突破点就在这里:它不把擦除当成二值分割,而是建模为条件图像生成任务——给定带手写的试卷图,生成“该试卷若无人书写”的理想底图。模型不是判别“这是不是字”,而是重建“字没写上去时该长什么样”。它适合两类人:一是高校计算机/教育技术方向做毕设的学生,需要可复现、有完整 pipeline 的参考方案;二是教务或考试中心的技术支持人员,想快速验证这类需求是否真能落地——它不依赖昂贵硬件,单卡 4GB 显存就能跑通推理,且模型权重已固化进.pth文件,无需从头训。下面所有步骤,都基于你解压后看到的model.pth、erase.py和test_images/这三个真实存在的东西展开。

2. 擦除不是删除,是重建:为什么选 U-Net 而不是 GAN 或 CNN 分类器?

2.1 任务本质决定网络骨架:U-Net 的编码-解码+跳跃连接,专治“局部结构强依赖”

手写擦除的本质,是像素级重建:每个位置的输出,高度依赖其周围几十像素内的印刷体结构(比如横线、方框、题号数字)、纸张纹理走向,甚至邻近字迹造成的阴影畸变。CNN 分类器只能输出“此处有字/无字”的标签,无法生成连续灰度值;GAN 虽能生成图像,但训练不稳定、模式坍塌风险高,且生成结果不可控(可能把“选择题A选项”错生成成“B选项”)。而 U-Net 的设计天然匹配此任务:

  • 编码器(下采样):用 ResNet-18 作为骨干,逐层提取多尺度特征(边缘→字符轮廓→版式结构);
  • 解码器(上采样):通过转置卷积逐步恢复分辨率;
  • 跳跃连接:把编码器中对应层级的特征图(如 128×128 的边缘图)直接拼接到解码器同尺度层,确保细小印刷体线条(如分数线上的横杠)不会在下采样中丢失。
    这比单纯堆叠卷积层效果提升显著——在作者提供的val_dataset/上,U-Net 的 PSNR 达到 28.3 dB,比同等参数量的纯 CNN 高 4.7 dB,关键在于它保留了结构信息。

2.2 输入输出设计:为什么输入是单通道灰度图,输出却是三通道 RGB?

你解压后会发现test_images/里全是 JPG,但代码里cv2.imread(path, cv2.IMREAD_GRAYSCALE)强制读成单通道。这是刻意为之:

  • 输入单通道:试卷扫描图本质是灰度信息,彩色通道冗余且增加计算负担;
  • 输出三通道:并非为了彩色,而是兼容 OpenCV 保存逻辑。cv2.imwrite()对单通道数组默认保存为灰度图,但若后续需叠加透明度或与彩色 UI 集成,三通道更鲁棒。实际模型输出层是Conv2d(64, 3, 3),但训练时用torch.nn.MSELoss计算三通道与目标图的均方误差,因目标图也是三通道(cv2.cvtColor(gray_img, cv2.COLOR_GRAY2RGB)生成),这样避免通道数不匹配报错。

提示:若你只想节省显存,可在erase.py中将model = EraseNet().cuda()后加一行model = torch.nn.DataParallel(model),但需确保 batch_size ≥ 2,否则 DataParallel 会报错。

2.3 损失函数选择:L1 + SSIM 组合,比纯 MSE 更抗“糊脸”

纯 MSE 损失会让模型倾向生成模糊平均值(所有像素趋近中间灰度),导致擦除后题干边缘发虚。作者采用L1 Loss + SSIM Loss 加权组合:

  • L1Loss:强制像素级精确重建,抑制大偏差;
  • SSIMLoss(结构相似性):衡量局部窗口内亮度、对比度、结构三者相似度,让模型关注“横线是否平直”“数字是否清晰”,而非单个像素值。
    权重设置为0.8 * L1 + 0.2 * SSIM,在验证集上比纯 MSE 提升 1.2 dB PSNR。SSIM 实现直接调用kornia.losses.SSIMLoss,需pip install kornia,版本必须为0.6.11(高版本 API 变更会导致window_size参数报错)。

3. 从解压到第一张图擦除成功:5 分钟跑通最小可运行流程

3.1 环境准备:Python 3.8 + PyTorch 1.12.1 + CUDA 11.3(非必须,CPU 也能跑)

不要试图用最新版 PyTorch——作者训练时用的是torch==1.12.1+cu113,CUDA 版本必须严格匹配。若你机器无 GPU,可降级为 CPU 模式,速度慢 8 倍但结果一致。执行以下命令(Windows/Linux 通用):

# 创建独立环境,避免污染主 Python python -m venv erase_env erase_env\Scripts\activate # Windows # source erase_env/bin/activate # Linux/Mac # 安装指定版本(关键!) pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 torchaudio==0.12.1 --extra-index-url https://download.pytorch.org/whl/cu113 pip install opencv-python==4.8.0 numpy==1.23.5 kornia==0.6.11 tqdm==4.64.1

注意:kornia==0.6.11是硬性要求,0.7.0+版本中SSIMLoss的window_size参数名改为kernel_size,不改会报TypeError: __init__() got an unexpected keyword argument 'window_size'。

3.2 数据预处理:为什么测试图必须是 512×512?——尺寸对齐是推理前提

模型输入层固定为torch.Size([1, 1, 512, 512]),因此任何输入图都必须 resize 到此尺寸。作者在erase.py中用双线性插值实现:

def preprocess_image(img_path): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (512, 512), interpolation=cv2.INTER_LINEAR) # 必须双线性! img = img.astype(np.float32) / 255.0 # 归一化到 [0,1] img = torch.from_numpy(img).unsqueeze(0).unsqueeze(0) # 添加 batch 和 channel 维度 return img.cuda() if torch.cuda.is_available() else img
  • cv2.INTER_LINEAR是关键:用最近邻插值(INTER_NEAREST)会导致锯齿,影响模型对印刷体边缘的判断;
  • unsqueeze(0).unsqueeze(0)生成[1,1,512,512]张量,符合模型forward(x)的输入要求;
  • 若你的试卷图长宽比非 1:1(如 A4 扫描图 2480×3508),先按长边缩放再 pad 黑边,否则拉伸变形会破坏印刷体比例。

3.3 模型加载与推理:三行代码完成擦除,但每行都有玄机

解压后找到model.pth,这是训练好的权重文件。erase.py中加载逻辑如下:

model = EraseNet() # 初始化网络结构 model.load_state_dict(torch.load('model.pth', map_location='cpu')) # 强制 CPU 加载,兼容无 GPU 环境 model.eval() # 关闭 dropout/batchnorm,否则推理结果随机波动
  • map_location='cpu':即使你在 GPU 环境训练,保存时用了torch.save(model.state_dict(), 'model.pth'),加载时也建议指定'cpu',避免RuntimeError: Attempting to deserialize object on a CUDA device;
  • model.eval():这是血泪经验!若忘记设,模型中的 Dropout 层会随机丢弃神经元,导致同一张图多次推理结果不同(有时擦得干净,有时残留墨点);
  • 推理后需torch.clamp(output, 0, 1)截断输出到 [0,1],否则负值或超 1 值经uint8转换会溢出成纯黑/纯白块。

4. 擦除失败的 5 个高频现场:现象、原因、一行代码解决

4.1 现象:擦除后整张图变灰蒙蒙,像蒙了一层雾

原因:输入图未归一化,像素值范围是 [0,255],但模型训练时输入是 [0,1],直接喂入导致内部激活值爆炸,输出全趋近 0.5。
解决:检查preprocess_image()中是否有img = img.astype(np.float32) / 255.0,缺则补上。

4.2 现象:擦除区域出现明显网格状伪影(类似马赛克)

原因:模型输入尺寸非 512×512,OpenCV resize 时用了INTER_AREA(区域插值),该算法对缩小操作优化,但对非整数倍缩放会产生周期性误差。
解决:强制使用cv2.INTER_LINEAR,并在 resize 前添加校验:

if img.shape[0] != 512 or img.shape[1] != 512: img = cv2.resize(img, (512, 512), interpolation=cv2.INTER_LINEAR)

4.3 现象:擦除后印刷体文字变粗、粘连,如“1”变成“H”

原因:模型输出未做后处理,直接保存为 uint8。浮点输出 [0,1] 经*255转 uint8 时,小数部分四舍五入引入误差,相邻像素灰度跳变被放大。
解决:添加 Gamma 校正平滑过渡:

output = output.clamp(0, 1).cpu().numpy()[0, 0] output = np.power(output, 1.0/2.2) * 255 # 伽马校正,缓解灰度跳变 output = output.astype(np.uint8)

4.4 现象:程序报错KeyError: 'conv1.weight'

原因:model.pth是用torch.save(model, 'model.pth')保存的整个模型对象,而非state_dict。但erase.py中用load_state_dict()加载,二者不兼容。
解决:两种方法任选其一:

  • 方法一(推荐):重载模型时用torch.load()直接加载:
    model = torch.load('model.pth', map_location='cpu') model.eval()
  • 方法二:用原作者训练脚本重新导出state_dict(需有train.py)。

4.5 现象:GPU 显存爆满,CUDA out of memory

原因:默认 batch_size=1,但模型中某些层(如 BatchNorm)在 eval 模式下仍占显存,且kornia.ssim计算时临时变量较多。
解决:在erase.py开头添加:

import os os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:128' torch.backends.cudnn.benchmark = False # 关闭 cuDNN 自动优化,减少显存碎片

并确保推理时torch.no_grad()包裹前向传播:

with torch.no_grad(): output = model(input_tensor)

5. 让擦除结果“肉眼可信”的 3 个后处理技巧:不是越干净越好

5.1 印刷体锐化:用拉普拉斯算子找回被平滑掉的细节

U-Net 解码器的上采样过程天然带来轻微模糊,尤其对细线(如坐标轴、表格线)。直接套用 OpenCV 的cv2.Laplacian()会放大噪声,正确做法是:

  • 先用cv2.GaussianBlur(output, (3,3), 0)对擦除图做轻度高斯模糊(σ=0.8);
  • 再计算拉普拉斯:laplacian = cv2.Laplacian(blurred, cv2.CV_64F);
  • 最后融合:sharpened = output + 0.8 * laplacian。
    系数0.8是经验值,大于 1 会过冲产生白边,小于 0.5 效果不明显。此操作使 PSNR 提升 0.9 dB,主观评价“题干更锐利”。

5.2 纸张纹理补偿:用 CLAHE 增强低对比度区域的背景一致性

扫描试卷常有光照不均,导致擦除后左上角发白、右下角发灰。全局直方图均衡会过曝,应使用CLAHE(限制对比度自适应直方图均衡):

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) output_enhanced = clahe.apply(output) # 注意:CLAHE 输入必须是 uint8 单通道
  • clipLimit=2.0:限制局部直方图峰值,避免噪声放大;
  • tileGridSize=(8,8):将图分 64 块分别均衡,适配 A4 尺寸;
  • 此操作不改变文字结构,仅让背景灰度分布更均匀,消除“擦除痕迹感”。

5.3 手写残留检测:用形态学闭运算定位未擦净区域,供人工复核

完全自动化擦除存在风险,需标记可疑区域。作者在erase.py中预留了residual_map输出:

# 原图减擦除图,取绝对值 residual = np.abs(original_gray.astype(float) - output.astype(float)) # 二值化 + 闭运算填充空洞 _, mask = cv2.threshold(residual, 30, 255, cv2.THRESH_BINARY) kernel = np.ones((3,3), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 在原图上画红框 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: x,y,w,h = cv2.boundingRect(cnt) if w*h > 100: # 过滤噪点 cv2.rectangle(original_color, (x,y), (x+w,y+h), (0,0,255), 2)
  • 阈值30对应灰度差约 12%,能捕获墨迹残留但忽略正常纹理差异;
  • MORPH_CLOSE填充手写笔画间的缝隙,使检测框连续;
  • 最终生成residual_marked.jpg,红框圈出需人工确认的区域,这才是生产环境该有的严谨性。

6. 我的毕设答辩被追问最多的 3 个问题,以及我怎么用代码当场演示回答

6.1 “模型擦除后,如何保证不改变原始印刷体语义?”——用 OCR 置信度反向验证

评委最担心“擦着擦着把‘+’擦成‘-’”。我的应对不是讲原理,而是现场跑 OCR:

# 用 PaddleOCR 检测擦除前后文字 from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang="ch", use_gpu=False) def ocr_confidence(img_path): result = ocr.ocr(img_path, cls=True) confs = [line[1][1] for line in result[0]] if result[0] else [] return np.mean(confs) if confs else 0 before_conf = ocr_confidence('test_images/scan1.jpg') after_conf = ocr_confidence('output/scan1_erased.jpg') print(f"擦除前 OCR 置信度: {before_conf:.3f}, 擦除后: {after_conf:.3f}") # 输出:擦除前 OCR 置信度: 0.921, 擦除后: 0.918 → 差异 <0.005,证明语义未损
  • 关键点:用同一 OCR 引擎、同一参数,对比置信度均值;
  • 若after_conf < before_conf - 0.02,说明擦除过度,需调整模型输出截断阈值。

6.2 “不同笔迹(圆珠笔/铅笔/签字笔)效果一样吗?”——构建笔迹敏感度测试表

我把test_images/里 12 张图按笔迹分类,用skimage.metrics.structural_similarity计算 SSIM:

笔迹类型样本数平均 SSIM备注
黑色签字笔40.892墨水渗透深,擦除后偶有浅灰晕染
2B 铅笔30.871石墨反光强,模型易误判为高光区域
蓝色圆珠笔50.903色彩饱和度低,擦除最干净

结论:蓝色圆珠笔效果最优,因 RGB 通道中蓝色分量在灰度转换时权重最低(0.114*B),模型最难从灰度图中还原其存在,故擦除倾向更强。这解释了为何训练集需包含多笔迹样本。

6.3 “能擦除印刷体上的手写批注吗?比如老师打的‘√’和‘×’”——用 ROI 裁剪+局部擦除精准打击

原模型针对整图,但批注常集中在题号旁小区域。我在答辩时现场改代码:

# 定义批注区域(x,y,w,h) annotation_roi = (120, 85, 40, 40) # 题号旁 40×40 像素 x, y, w, h = annotation_roi roi_img = original[y:y+h, x:x+w] # 对 ROI 单独擦除 roi_tensor = preprocess_image_roi(roi_img) # 尺寸缩放至 256×256 降低显存 roi_output = model(roi_tensor) # 贴回原图 original[y:y+h, x:x+w] = roi_output.cpu().numpy()[0,0] * 255
  • ROI 尺寸缩放为 256×256(非 512),显存占用降为 1/4;
  • 局部擦除 SSIM 达 0.932,比整图擦除高 0.04,因模型更聚焦于小区域纹理。

这三年我带过 7 届毕设,凡是做图像擦除的,90% 卡在“擦不干净”和“擦过头”,最后靠调参玄学收场。而这个方案的价值,不是它有多先进,是它把“擦除”从一个模糊需求,拆解成可测量(SSIM)、可定位(ROI)、可验证(OCR 置信度)的工程动作。你不需要懂反向传播,只要理解resize用INTER_LINEAR、model.eval()必须加、residual_map是你的后悔药——这些才是让毕设不翻车的真正护栏。希望帮到你。

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

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

利用VN1630A/VN1640A的I/O接口在CANoe中搭建简易示波器

做汽车电子调试那几年&#xff0c;我经常碰到一种很尴尬的情况&#xff1a;手头没有示波器&#xff0c;却要临时查看一个PWM信号占空比、LIN唤醒电平的上升沿&#xff0c;或者传感器输出电压的变化趋势。总不至于为了看一个信号就跑去仪器间借一台示波器。后来我发现&#xff0…

作者头像 李华
网站建设 2026/9/28 16:48:39

QQ空间数据导出工具GetQzonehistory:3步完成本地备份

QQ空间数据导出工具GetQzonehistory&#xff1a;3步完成本地备份 【免费下载链接】GetQzonehistory 获取QQ空间发布的历史说说 项目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory GetQzonehistory 是一个 QQ 空间说说本地备份工具&#xff0c;解决历史…

作者头像 李华
网站建设 2026/9/28 16:47:22

Submersion AI推出Basin:基于CyberGym训练的安全专用模型解析

1. 从“Submersion AI Debuts Basin”说起&#xff1a;这个标题到底在讲什么第一次看到“Submersion AI Debuts Basin”这个标题&#xff0c;我脑子里蹦出来的第一个念头是&#xff1a;这又是一个把“潜水”和“水池”拼在一起的AI概念&#xff1f;但仔细拆开看&#xff0c;Sub…

作者头像 李华
网站建设 2026/9/28 16:46:20

Jev智能if语句:一次调用多判断与置信度路由实战

1. 从「if-else」到「智能路由」&#xff1a;为什么我们需要把AI判断封装成语句写过业务代码的人都有体会&#xff0c;最让人头疼的不是复杂算法&#xff0c;而是那些层层嵌套的条件判断。一个订单要不要走风控审核&#xff0c;一个客服工单要不要升级&#xff0c;一条内容要不…

作者头像 李华
网站建设 2026/9/28 16:46:01

CANOe+CAPL实现UDS诊断上位机开发实战

1. 项目概述&#xff1a;这不是“5分钟速成”&#xff0c;而是老司机带你绕过UDS上位机开发的90%坑CANOe实战&#xff1a;5分钟搞定UDS诊断上位机开发&#xff08;附CAPL脚本&#xff09;——这个标题乍看像短视频封面&#xff0c;但实际在汽车电子测试圈里&#xff0c;它戳中的…

作者头像 李华