ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手写文字擦除实战:基于深度学习的检测与图像修复方案

手写文字擦除实战:基于深度学习的检测与图像修复方案 简介这是一份面向图像处理与OCR赛题选手的手写文字擦除冠军方案针对试卷上红黑蓝手写字与印刷字重叠、污渍脏点及手画线干扰等复杂场景提供完整的Python源码、数据划分与说明文档。官方提供1081对训练集作者将其拆分为1000张训练与81张验证并依据RGB平均差值计算mask掩码平均差值大于20置1否则取比例值。方案基于EraseNet网络改写成Paddle版本采用多分支多阶段结构包含mask生成分支与两阶段图像生成分支同时引入感知损失与GAN损失提升背景生成逼真度与PERT方法对比后表明EraseNet在本数据集上效果更优。资源共30个文件以22个Python脚本为主覆盖训练、测试、预测、mask计算、模型转换等环节辅以3个Shell脚本便于一键复现另含说明文档与README。压缩包仅98KB已有694人学习适合需参考完整竞赛方案、快速搭建并调试训练流程的进阶开发者。1. 手写文字擦除不是去水印它处理的是“人写的字”当一张试卷扫描件里红色批注压在印刷题干上你想要的不是变白而是“擦掉批注后印刷字还在”。手写文字擦除这个任务和普通去水印的最大差别在于水印位置通常固定、颜色统一而手写笔迹有笔锋、深浅、倾斜甚至与背景文字粘连。标题里这个zip包包含Python源码、训练好的数据模型和文档说明是一个竞赛里拿到第1名的成熟方案帮你把这条链路直接跑起来。如果你正在做试卷去批改痕迹、答题卡清理、笔记文档数字化这篇实战笔记正是为你写的。2. 先搞懂模型在做什么检测修复两段式架构为什么不能直接整图送进修复模型因为手写擦除的难点不是“把洞补上”而是“只补该补的地方”。整图重建很容易把印刷字当成噪声一起改掉尤其红色笔迹和印刷黑字重叠时模型根本分不清谁要留。所以竞赛第1名方案几乎都采用两段式先用分割网络找到手写像素的位置输出一张二值遮罩再在遮罩引导下对局部区域进行图像修复。这样做的好处是可控——你可以调整遮罩的膨胀程度来决定擦除范围而不是让模型自己猜。2.1 为什么要先“找到字”再“抹掉”检测分支的必要性先想一个场景一张手写笔记扫描件背景是干净的横线纸文字是蓝色圆珠笔写的。如果跳过检测直接修复模型会把横线当成背景的一部分擦除时连横线一起抹掉。检测分支输出的mask解决了这个“保底”问题它告诉修复器只有这些像素需要重建其他部分保持原样。在源码包里检测分支一般独立训练权重命名通常类似detect.pth或seg.pth。我解压常见结构是checkpoints/model_det.pth手写区域分割权重checkpoints/model_inp.pth修复权重config.yaml参数配置文件train.py / test.py训练和推理入口如果文档说明里写着“先用detect生成mask然后用mask作为inpaint的输入”这就对了。你甚至可以只把detect的结果存成PNG用Photoshop手动擦掉效果也不错——可见检测分支的重要性。2.2 修复分支的三种选择UNet、GAN、扩散模型的取舍修复分支是整条链路里最烧钱的部分。我按实际体验排序UNet系参数量小CPU也能跑适合打印机扫描的、对比度较高的图。缺点是笔划边缘容易发糊。GAN系LaMa、Edge-Connect风格恢复纹理能力强是竞赛方案的主流。训练需要一张不错的GPU推理一张512图在1080Ti上大概0.2秒。扩散系效果最好但一次推理要几十秒在“比赛提交”这种场景下基本不选。怎么判断手里这个方案用的是哪种看配置文件里的arch字段或者看文档说明里有没有提到“Generator/Discriminator”。如果是GAN源码里通常有两个模型文件——一个生成器、一个判别器但推理时只用生成器别把判别器也加载进去。我就踩过这个坑加载了D模型的权重结果load_state_dict报了大量Key不匹配最后发现是缺少strictFalse。2.3 数据模型怎么用权重文件、配置与类名映射很多人拿到model_det.pth直接torch.load后不知所措。原因是这些权重文件是“参数状态字典”不是独立可跑的模型它必须挂在你源码里的模型定义上。我建议按这个套路做import torch from src.models import build_model # 源码中通常有的工厂函数 config {arch: lama, or_size: 512} # 以config.yaml为准 detector build_model(config[arch]) ckpt torch.load(checkpoints/model_det.pth, map_locationcpu) state ckpt[state_dict] if state_dict in ckpt else ckpt # 用strictFalse允许少部分层不匹配比如BN统计层 miss detector.load_state_dict(state, strictFalse) print(miss)这段代码的逻辑是build_model根据arch创建出带随机初始化的网络torch.load读入权重字典然后load_state_dict把字典里的数值放进网络对应层。miss会打印缺少或多余的Key通常是一些缓存变量不影响推理但如果缺失的是卷积层那问题就大了得核对版本是否一致。参数说明arch一定要读你手头config.yaml里的值不要凭记忆填map_locationcpu是为了在没有CUDA环境的机器上也能先加载等推理时再.to(cuda)如果权重文件是PyTorch老格式存成*.tar而不是.pth则要手动抽取model字段。3. 用Python源码跑通最小推理环境、命令行与三行代码手写文字擦除方案再强不能跑都是零。这一章我会把从解压到出结果的最小路径走一遍包括环境安装、命令行推理和Python API封装三块。每一步都是我自己操作过的顺序照着来能少绕很多弯。3.1 环境依赖到底要装什么torch、opencv、numpy缺一不可打开源码包的requirements.txt通常会有torch1.8、opencv-python、numpy这几个基础依赖。但我不建议无脑pip install -r requirements.txt因为torch的版本选择直接决定后续会不会踩坑比如权重反序列化失败。我更推荐用conda创建干净环境分两步装conda create -n py38 python3.8 -y conda activate py38 pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cu117 pip install opencv-python numpy tqdm说明第一行创建Python 3.8环境很多老权重都是3.8范围训练的用3.10偶尔会遇到torch.load的pickle问题第二行激活第三行安装GPU版torch如果你没有NVIDIA显卡把--index-url去掉装CPU版即可第四行装图像处理和工具库。装完验证一下python -c import torch, cv2, numpy; print(torch.__version__, cv2.__version__)如果torch版本和源码里的权重序列化版本差太多比如源码用1.8保存你装2.3torch.load可能报_pickle.UnpicklingError。遇到这种翻车不要死磕直接把torch降到1.8-1.13之间再试往往就通了。这条血泪经验至少帮我节省过两个小时。3.2 一条命令跑通单张图片源码里本该有的cli接口绝大多数竞赛方案会留下一个test.py或者infer.py目的就是让你一条命令得到结果。假设文档说明里写了python test.py --input ./images/scan.jpg --output ./outputs --checkpoints ./checkpoints执行前确认outputs目录存在源码不一定自动创建。参数含义--input支持单张图或目录如果指向目录它会对目录内所有图片逐个处理。--output结果保存路径可以是文件也可以是目录。--checkpoints指向model_det.pth和model_inp.pth所在的目录。第一次跑如果报错ModuleNotFoundError: No module named src多半是工作目录不对。先cd到源码根目录再运行因为test.py里的from src.models import ...依赖当前路径。如果还有类似vgg19缺失问题那就是GAN修复分支去加载感知loss的预训练权重但推理时根本不需要你直接在test.py里找加载判别器或VGG的代码把它注释掉或者加一个--mode inference的参数跳过。3.3 从命令行到脚本用Python API调用模型并处理批量图片命令行适合验货真正工程化还得靠Python API。源码里通常会把两段式封装成Eraser类调用方式大致如下import cv2 from src.pipeline import Eraser demo Eraser( checkpoint_dir./checkpoints, devicecuda, # CPU 或 cuda:0 size512, # 推理尺寸和训练一致 ) img cv2.imread(test.jpg) # BGR img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) out_rgb demo.erase(img_rgb) # 返回 RGB out_bgr cv2.cvtColor(out_rgb, cv2.COLOR_RGB2BGR) cv2.imwrite(result.jpg, out_bgr)这段代码的核心在于Eraser内部会依次调检测和修复先resize到size跑分割得到mask再在mask引导下跑修复。size不能随意改必须和权重训练尺寸一致如果你传了原尺寸的高清图Eraser内部可能先缩放再放大导致细笔迹被忽略。所以我建议外部调用者也遵循同样的预处理先用cv2.resize把图像统一到合理尺寸再送进Eraser。如果Eraser类没有提供你也可以自己串两个模型的forward逻辑并不复杂但务必处理好resize的插值方式——检测分支用最近邻保留边缘修复分支用双线性。4. 数据模型背后的秘密你的输入决定了擦除上限源码、数据模型、文档说明三者之中数据模型往往被当作“黑盒”使用。但实际上它的上限是由训练数据定义的。用不行就怪模型菜是新人常犯的错真正有经验的工程师会先研究数据分布再决定要不要调参、要不要微调。4.1 训练数据长什么样手写区域mask是关键一个训练样本由三部分组成原始干净文档背景、叠加手写笔迹后的损坏图、手写区域的二值mask。模型在训练时学到的是“把mask内像素重建为背景”。所以如果mask标注得粗糙模型上限就低。在源码包的文档说明里通常有一节“数据集格式”或“数据准备”会写明训练用了多少张图、分辨率多少、手写字体用了哪些。我建议你找到示例数据看一眼如果训练集是白底黑字的扫描件你的场景是黄底铅笔字模型输出变灰是必然的。这时候不要急着改模型先做色彩归一化。下面是常见的预处理手段把彩色背景拉回接近训练分布import cv2 import numpy as np def to_gray_norm(img): # 转灰度再用自适应阈值去掉背景阴影 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray clahe.apply(gray) # 归一化到0-255范围保持笔迹对比度 norm cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX) return norm注意这里没有二值化只增强对比度因为模型需要看到灰阶不能直接变黑白。clipLimit越大对比度越强但笔迹和印刷字都会变粗可能导致检测误报。我一般先设2.0效果不行再往1.5或3.0调。4.2 数据模型的分辨率和通道为什么默认224或512配置文件里常见input_size: 512或224。这些数字不是随便定的它们和网络的降采样倍数有关。以UNet为例5次下采样是32倍512除以32得16特征层不会出现小数尺寸224是ImageNet的标准尺寸很多预训练encoder自带224的输入。如果你的模型是RGB训练而输入是灰度图模型也勉强能跑但颜色通道的统计信息变了检测准确率会下降“两三个点”。我通常的做法是把单通道灰度复制成三通道而不是直接当作单通道输入。另外所有图片在进模型前都必须除以255归一化源码里如果没做你要在预处理里补上。4.3 如果模型效果差先看是不是数据域偏移我在实际项目中遇到过用第1名方案擦官方样例图几乎完美但擦自己的手机拍摄笔记结果出现整片白色矩形框。原因就是数据域偏移——手机照片有透视变形和低光照模型没在类似图上见过。解决办法优先级如下归一化输入增强对比度、去阴影成本为零先做用你的10~20张小图做手动标注mask在方案权重基础上微调修复分支只调最后两层成本小重新采集数据训练检测分支成本高最后考虑。第1名方案的数据模型是个很好的起点但它不是银弹。每个“好用的模型”背后都有一段数据适配过程你拿着别人的权重直接上生产翻车才是常态。5. 避坑与排查手写擦除最常见的5个翻车现场这一章我把自己跑手写文字擦除时遇到过的、以及给朋友排过的几个高发问题列出来。每条都按“现象→原因→解决”写希望帮你躲开同样的坑。5.1 现象印刷字被一起擦掉输入是试卷扫描件手写答案压在印刷题干上跑完发现印刷字也缺了一块。原因检测分支输出的mask把粘连处的手写像素和印刷像素一起圈了进去修复阶段把整块都重建了。尤其是印刷字和手写笔迹颜色接近时分割网络会分不干净。解决先调检测器的置信度阈值——看config.yaml里有没有confidence_threshold把它从0.5提到0.7减少误检如果还在后处理对mask做连通域过滤只保留面积大于某个像素值的区域同时用形态学腐蚀把mask边缘往里收一圈。不要直接调修复模型它只是执行者。5.2 现象手写笔画残留半透明阴影擦除结果里隐约还能看到笔迹的轮廓像水渍一样。原因mask覆盖的范围不够精确修复网络只重建了mask中心边缘的过渡区留了残影或者是输入分辨率太低笔锋细节被压缩到亚像素级人脸都看不清。解决对mask做膨胀用cv2.dilate把mask向四周扩展2~5像素给修复器更大一点的重建区域如果还影绰把推理分辨率从512提高到768如果显存够但要重新归一化。我用强度为3的3x3核膨胀后阴影基本消失。5.3 现象彩色笔迹变成灰黑色斑块用红色圆珠笔写的那行字擦完之后留下一团灰黑。原因手写擦除模型多数在RGB空间训练但增强时只做了灰度扰动彩色笔迹对模型来说反而成了“异常颜色”。模型可能把红色通道的大响应当成了某种噪声直接生成反色。解决预处理时把输入从RGB转换为灰度再复制成三通道也就是去掉色彩干扰如果源码里已经有数据增强的随机颜色抖动可以给输入套一层ColorJitter让模型自适应。遇到蓝色笔迹同样适用。5.4 现象表格线和格子被破坏擦完手写内容后原来的横线纸变成断断续续的虚线。原因修复分支把表格线当作和手写无关的背景纹理但修复区域覆盖到了线上如果mask膨胀过大会把网格线一并吞掉。解决控制mask膨胀不要超过3像素修复分支不是局部patch而是全图生成的话查看输出是不是整图重建尽量改用只对mask区域做修复的架构。如果源码里修复分支带mask输入把mask换成经过骨架线剔除的版本先检测直线再把直线上的mask点置0保留表格线。5.5 现象训练loss很低但测试效果差自己微调后在训练集上擦得干净换新图就崩。原因训练数据规模太小、模型过拟合或者是微调时把全部权重都解冻了导致遗忘预训练知识。解决微调时只解冻检测分支的最后两层和修复分支的decoder末尾冻结encoderbatch_size至少8用训练集验证集分离并保存验证集mask的IoU最优权重。如果资料集本身只有十几张就不要动权重直接改用传统图像处理先做预处理。6. 把方案用好验证、调参与落地到自己的项目代码跑通只是开始真正要决定“这个方案能不能进生产”你得先回答“它在我这个数据上表现如何”。我一般不会直接看一两个样例就下结论而是用30张图跑一个快速验证。6.1 用30张图验证方案是否适合你的场景挑30张有代表性的图——覆盖不同笔颜色、不同纸张底色、不同手写密度——跑一遍推理然后人为统计三个指标完全干净的比例、有轻微残留的比例、伤及印刷体的比例。只要完全干净超过60%轻微残留不到30%这方案就能用如果伤及印刷体超过5%说明检测分支在你的数据上崩了先回头调mask。这比单张肉眼判据可靠得多也能帮你决定值不值得为这个业务微调。6.2 调参优先级mask阈值、膨胀核、推理尺寸按我的经验调参优先级是这样排的第一是检测置信度阈值它控制误检和漏检的平衡第二是mask膨胀像素太大会伤背景太小会留残影第三是推理分辨率提升分辨率能救回部分细笔迹但显存和耗时上涨。这三项都是改一个数字就能看效果的属于最低成本的杠杆。每一轮调整后都用这30张图重新跑一遍对比统计结果不要凭感觉判断。输入输出上也有一点经验下游是OCR的话输出白底黑字的PNG不要存JPEG避免压缩噪声干扰文字识别人工看图的话高分辨率JPEG就够。写代码时把中间mask和最终结果同时导出方便复盘哪里走调了。我每次拿到这种“第1名方案”习惯先花半小时跑通最小推理再可视化中间mask——这能帮你快速判断方案的边界和问题比调一百次参数都管用。希望你能在这个过程中把这份源码真正变成你自己的工具而不是一个跑完就丢的demo。希望帮到你。本文还有配套的精品资源点击获取
返回列表