news 2026/10/19 9:37:05

PmrNet:门控卷积与掩膜引导的手写字消除网络

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PmrNet:门控卷积与掩膜引导的手写字消除网络

做OCR的朋友可能都有那种体验:一张质量还不错的扫描件,被红笔黑笔批注得密密麻麻,扔给识别引擎,出来的文本先被笔迹干扰。手写笔迹和印刷体叠在一起,传统的阈值滤波根本没法分离。这篇是我的手写字消除系列第二篇,重点把PmrNet这个自研的图像修复网络完整展开:网络结构怎么设计、训练数据怎么造、训练有哪些关键开关,以及一堆踩出来的坑。如果你是做文档图像预处理、OCR前处理,或者单纯对图像修复感兴趣,这篇应该能直接照着跑。

PmrNet全称我写的是Partial Mask Refinement Network,核心思路很简单:不把整张图都当脏图处理,而是先告诉网络“哪些地方有手写、哪些地方没有”,让网络只修复被遮住的区域,同时尽量保持印刷体边缘和纸张纹理的完整。下面我把整个项目从设计到落地一段段拆开讲。

1. 任务拆解:为什么手写字消除不是“抠图”

1.1 先把问题定义清楚

手写字消除本质上是一个图像修复任务,但和我们常见的去除水印不太一样。去水印通常只需要把半透明logo区域抹掉,对底下内容的保留要求没那么高。但手写字往往是写在印刷文字上面的,笔迹覆盖住了印刷体的一部分。我们要做的不是把整块区域重新生成一张纹理,而是要把“被覆盖的印刷笔画”尽量恢复出来,同时把手写笔迹清理干净。

用输入输出的说法就是:输入一张带手写笔迹的文档扫描图,以及一张标记出手写位置的单通道mask,输出一张干净文档图。这里的关键是,目标图并不是不存在手写的原图,而是“没有手写批注、但保留了印刷内容”的图。也就是说,网络同时要做两件事:检测哪些像素是手写、哪些像素是印刷体纹理,然后在手写像素位置重建底层内容。

1.2 传统方法为什么干不了

很多人问过我,为什么不用OpenCV直接处理。不是没试过,是真的不行。手写笔迹的颜色、粗细、深浅变化非常大,有的红色圆珠笔,有的黑色签字笔,有的铅笔扫描后还有反光。传统的二值化、颜色阈值、形态学开闭运算,遇到颜色接近印刷体或者笔画叠加在文字上时,要么把印刷体一起擦掉,要么留下一堆边缘残渣。

纹理合成类的方法我也试过,比如把遮挡区域周围的纹理搬过来填充。问题在于手写笔迹不是均匀分布的水印,它经常和印刷文字叠在一起,周围根本找不到足够干净的“相似纹理”。而且文档图像的字体笔画宽度、行距、纸纹都是高度结构化的,直接填纹理会产生明显的断裂和错位。这块用深度学习方法做,本质上是从大量样本里学习出“印刷字怎么长、手写笔迹怎么盖上去”的规律,泛化能力比手工规则强一个量级。

1.3 方案选型:为什么是PmrNet而不是普通UNet或GAN

最早我先用普通UNet跑了一个baseline。效果很尴尬:把手写区域擦干净了,但印刷体的字形结构也被抹花了,尤其是笔画密集的汉字,周围会出现一圈灰蒙蒙的过度平滑区域。原因是UNet对整张图同样看待,模型不知道哪些区域权重该大,最终学出来的重建结果倾向于“保守模糊”。

也试过直接用GAN的判别器去拉高清晰度。GAN确实让输出看起来更锐利,但训练不稳定,loss曲线像心电图一样,而且保存下来的模型在验证集上经常会“幻觉”出一些不存在的笔画,这比模糊更麻烦。PmrNet做了一个折中:保留UNet这种编码器-解码器的基础骨架,但用门控卷积替代普通卷积,并加入手写笔迹感知模块,把“哪里需要修”这个先验显式地塞进网络。训练稳定,输出的边缘也更干净。

方案是否能去手写印刷体保留程度训练稳定性部署成本
阈值+形态学偶尔差不需要训练低
普通UNet能中稳定中
普通GAN能中上差高
PmrNet能好稳定中

2. PmrNet网络结构详解

2.1 整体骨架:编码器-解码器与门控卷积

PmrNet整体结构还是编码器-解码器,下采样四倍,中间通过跳连接保持细节。但每一层卷积我用的不是普通卷积,而是门控卷积。门控卷积的特别之处在于,卷积输出会拆成两部分:一部分是正常的特征,另一部分通过sigmoid得到一个0到1的“开关”,两者相乘后再继续向下传递。这样网络会根据输入内容动态决定哪些位置允许通过,哪些位置要抑制。对于手写笔迹和印刷体叠在一起的区域,这个“开关”会自适应地调节特征强度,比普通卷积单纯堆通道更灵活。

输入层面,模型接收的是三通道RGB图加一通道掩膜图,四个通道拼在一起。掩膜图里手写区域为1,背景区域为0。训练的时候掩膜是人工生成的,推理的时候可以用一个简单的笔迹检测模型或者人工框选来生成。把掩膜当作输入而不是隐式学习,最大的好处是避免了“检测和修复同时做”的耦合问题。设备端使用时如果发现某些手写没被清掉,只需要把掩膜扩大一点,就能立刻改善,不用重新训练模型。

2.2 手写笔迹感知模块的作用

我加了一个叫笔迹感知注意力的小模块,放在编码器最深层。这个模块的核心不是普通通道注意力,而是把掩膜信息经过两次卷积后变成一个空间权重图,再和编码器特征做逐通道相乘。之所以要这样做,是因为手写笔迹通常具有连续的笔画走向,掩膜能提供粗粒度定位,但掩膜的边缘往往比真实笔迹细一两圈。如果直接把掩膜当输入让网络自己学,边缘误差会被后续层放大。通过这个注意力模块,网络可以沿着掩膜区域的中心线往两侧扩展感受野,从周围印刷体结构推断出被覆盖的笔画走向。

实际实现上,这个模块不会显著增加显存占用。特征图在最低分辨率通常是64×64左右,在上面做几个3×3卷积开销很小,但带来的收益是肉眼可见的。我用一个简单比喻解释:普通UNet像是让修图师拿画笔在指定区域慢慢补,而PmrNet像是给了修图师一个“哪里坏了”的放大镜,同时告诉他要把周边两圈也纳入参考,修出来的笔画能接上原来的走势。

2.3 损失函数组合:L1、感知损失和SSIM如何搭配

PmrNet的训练损失用了三项加权组合,不是单纯一个L2。第一项是L1损失,负责约束像素级别的绝对误差。用L1而不是L2,是因为L2在训练后期容易把输出推向模糊均值,造成边缘过平滑,而L1对离群点的惩罚没那么激烈,保留细节的效果更好。

第二项是感知损失,我用了一个预训练的VGG16,把生成图和真实干净图分别送到VGG16里,取conv3_3和conv4_3的特征图,算它们之间的L1距离。感知损失的作用是让生成图在“人眼看内容一致”的层面更接近真实图,而不是每像素数值相同。在汉字修复里,这项损失对笔画结构的恢复尤其关键,因为印刷字体的笔画粗细和走向是高度稳定的,像素级L1可能允许笔画稍微歪一点,但感知特征会把这种结构差异拉回来。

第三项是SSIM损失,直接优化结构相似度。三者的权重我设为L1为1.0,感知损失为0.4,SSIM为0.2,这是反复测试出的一个相对稳定的组合。感知损失权重要控制得很小,否则网络会倾向于生成高频纹理来欺骗VGG,使得笔迹虽然被清理了,但纸张纹理看起来像磨砂玻璃。初始化时先用纯L1跑前5000步,让模型先学会基本修复,再逐步把感知损失加进去,能有效避免训练初期震荡。

3. 数据集是怎么造出来的

3.1 合成数据为什么优于手工标注

图像修复这种密集预测任务,手工标注一张带手写的干净底图几乎是不可能的——因为拿到带手写图的时候,覆盖住的印刷体根本没人知道原来长什么样。所以数据集必须走合成路线。合成数据最大的优势是:我们可以同时拿到“带手写图”和“干净底图”以及“精确的笔迹掩膜”三份结果,完全不用人工标注。

我的做法是准备一批没有手写批注的文档图作为底图,这些底图大部分是自己排版的印刷文本,再人工做一个覆盖层,把各种各样的手写笔迹叠上去。这里最需要注意的坑是:手写笔迹的透明度、颜色、大小要和真实场景对齐。我见过有人随意把字体渲染上去,结果训练出来的模型只会处理那种接近打印体的笔画,手写特有的连笔和抖动完全不行。

3.2 掩膜生成策略

掩膜的生成不需要复杂分割,因为合成阶段我们本来就知道手写笔迹画在哪里。如果手写是用画笔工具或者字体渲染出来的,那就顺带把对应的轮廓抠出来当作mask。但除了这种硬边缘mask,还需要做一些小trick让模型对误差更鲁棒:我把mask做随机腐蚀和膨胀,大概各执行1~2个像素,同时把边缘做高斯模糊,让mask不一定是精确的0/1边界。

实际操作中,我建议生成三种mask变体:精确mask、边缘膨胀1到2像素的mask、边缘腐蚀1到2像素的mask。训练时随机选一种。这样处理之后,模型对mask质量的要求不会过高。否则推理时一旦mask稍微错位,修复效果会断崖式下跌。这个点很关键,很多复现一开始跑得好,一上真实数据就崩,问题往往就出在mask上。

3.3 预处理与数据增强

输入图像不能直接丢给网络,要先统一尺寸。我固定为256×256分辨率,训练时随机裁剪。如果文档是A4扫描图,通常直接resize成1024×1024,再从里面随机切256×256的小块。这样每个批次能看到更多不同位置的文字区域,模型学得更充分。测试时用滑窗拼接,避免直接resize小图导致印刷字笔画断裂。

数据增强我做了四类:随机调整亮度、对比度、色相;随机旋转1到5度;随机添加高斯噪声和焦点模糊;对底图和手写层做同样的几何增强,但不要对颜色增强共享,因为手写和底图在颜色分布上应该保持相对独立。还有个细节:mask永远跟着底图做同样的几何变换,而不是跟手写层。因为手写层在合成时已经和底图对齐过一次,后续几何增强只动底图位置,mask就按照底图的变换走,这样保证掩膜覆盖位置始终正确。

3.4 训练集和验证集的划分

合成数据虽然便宜,但不能只在一个固定背景下生成。我生成训练集和验证集的时候,会刻意让底图的排版风格、字号、字体不完全一样。训练集如果用10种字体,验证集就要额外用5种没见过的字体;训练集手写风格用方正手写体这一类,验证集就要加入少量真实手写图片。这样设置是为了检测模型是否真的学到了“修复”而不是“背题”。

总量方面,我最终使用了大约5万张256×256样本,验证集选5000张。数据量看起来不多,但合成阶段做了大量随机化,因为文档修复任务不需要海量真实数据,关键是多场景覆盖。如果训练末期发现验证集loss和训练集loss差距大,优先增加手写风格的多样性,而不是盲目加数据量。

4. 训练代码与参数调优

4.1 环境配置:省心的版本组合

整个项目基于PyTorch实现。硬件上我用的是一张24GB显存的显卡,实际发现8GB显存也能跑,只是batch要调小。依赖版本大致是:Python 3.8、PyTorch 1.12、CUDA 11.6、OpenCV-Python 4.6。如果显卡驱动是新的,也可以直接上PyTorch 2.x,但实测1.12对这类任务并没有明显的性能差。

训练前先把数据组织好,目录结构建议如下:

data/ train/ input/ # 带手写笔迹的合成图 target/ # 原始干净底图 mask/ # 手写区域掩膜 val/ input/ target/ mask/

4.2 数据加载与掩膜处理代码

PyTorch的Dataset里,我习惯把图和mask路径一次性读进内存,因为样本尺寸小,跑起来比每次从磁盘加载快很多。下面的代码演示了怎么对输入的图片和mask做同步变换:

import torch import cv2 import numpy as np from torch.utils.data import Dataset class DocInpaintingDataset(Dataset): def __init__(self, input_dir, target_dir, mask_dir, size=256, crop=True): self.input_paths = sorted(glob(input_dir + '/*.png')) self.target_paths = sorted(glob(target_dir + '/*.png')) self.mask_paths = sorted(glob(mask_dir + '/*.png')) self.size = size self.crop = crop def __getitem__(self, idx): image = cv2.imread(self.input_paths[idx]) target = cv2.imread(self.target_paths[idx]) mask = cv2.imread(self.mask_paths[idx], cv2.IMREAD_GRAYSCALE) # 把mask二值化到0/1 mask = (mask > 127).astype(np.float32) # 随机裁剪到统一尺寸 if self.crop and image.shape[0] > self.size: h, w = image.shape[:2] y = np.random.randint(0, h - self.size) x = np.random.randint(0, w - self.size) image = image[y:y+self.size, x:x+self.size] target = target[y:y+self.size, x:x+self.size] mask = mask[y:y+self.size, x:x+self.size] # HWC转CHW,然后归一化到[-1,1]的范围 image = torch.from_numpy(image.transpose((2, 0, 1)).copy()).float() / 127.5 - 1.0 target = torch.from_numpy(target.transpose((2, 0, 1)).copy()).float() / 127.5 - 1.0 mask = torch.from_numpy(mask[np.newaxis, ...].copy()).float() return image, mask, target

这里我做了一个关键决定:mask归一化后还是0/1,不会跟着image一起缩放到[-1,1],因为后续网络里要用sigmoid门控,0/1的数值范围最合适。

4.3 训练脚本关键段:模型、优化器与损失

PmrNet本身可以用一个简单的UNet结构加门控卷积来搭。下面给出核心层的伪代码实现,注意力模块我用注释标出来:

import torch.nn as nn class GatedConv2d(nn.Module): def __init__(self, in_ch, out_ch, kernel_size=3, stride=1, padding=1): super(GatedConv2d, self).__init__() self.conv = nn.Conv2d(in_ch, out_ch, kernel_size, stride, padding) self.mask_conv = nn.Conv2d(in_ch, out_ch, kernel_size, stride, padding) self.sigmoid = nn.Sigmoid() def forward(self, x): feature = self.conv(x) gate = self.sigmoid(self.mask_conv(x)) return feature * gate class PmrNet(nn.Module): def __init__(self, in_ch=4, out_ch=3): super(PmrNet, self).__init__() # 这里用最简单的四层编码器和四层解码器示意 # 实际基础模型可替换成ResNet或MobileNet块 self.enc1 = GatedConv2d(in_ch, 64, kernel_size=3, stride=1, padding=1) self.enc2 = GatedConv2d(64, 128, kernel_size=3, stride=2, padding=1) self.enc3 = GatedConv2d(128, 256, kernel_size=3, stride=2, padding=1) self.enc4 = GatedConv2d(256, 512, kernel_size=3, stride=2, padding=1) # 笔迹感知注意力模块 self.attn = nn.Sequential( nn.Conv2d(512, 128, 1), nn.ReLU(inplace=True), nn.Conv2d(128, 512, 1), nn.Sigmoid() ) self.dec4 = GatedConv2d(512 + 256, 256, kernel_size=3, stride=1, padding=1) self.dec3 = GatedConv2d(256 + 128, 128, kernel_size=3, stride=1, padding=1) self.dec2 = GatedConv2d(128 + 64, 64, kernel_size=3, stride=1, padding=1) self.dec1 = nn.Conv2d(64, out_ch, kernel_size=3, stride=1, padding=1) def forward(self, image, mask): x = torch.cat([image, mask], dim=1) e1 = self.enc1(x) e2 = self.enc2(e1) e3 = self.enc3(e2) e4 = self.enc4(e3) attn = self.attn(e4) e4 = e4 * attn + e4 # 残差式注意力输出 d4 = self.dec4(torch.cat([e4, e3], dim=1)) d3 = self.dec3(torch.cat([d4, e2], dim=1)) d2 = self.dec2(torch.cat([d3, e1], dim=1)) out = self.dec1(d2) return out

这个结构只为演示核心思路,实际项目里我把每层换成了更深的残差块,并在解码器里加了上采样卷积。训练时的损失函数封装如下:

def perceptual_loss(img_pred, img_target, vgg): pred_feat = vgg((img_pred + 1) / 2) # 范围映射到0-1后进VGG target_feat = vgg((img_target + 1) / 2) loss = 0 for pf, tf in zip(pred_feat[3:5], target_feat[3:5]): loss += F.l1_loss(pf, tf) return loss for batch in train_dataloader: img, mask, target = batch img, mask, target = img.to(device), mask.to(device), target.to(device) pred = model(img, mask) l1_loss = F.l1_loss(pred, target) perc_loss = perceptual_loss(pred, target, vgg) ssim_loss = 1 - ssim(pred, target) total_loss = l1_loss * 1.0 + perc_loss * 0.4 + ssim_loss * 0.2 optimizer.zero_grad() total_loss.backward() optimizer.step()

要注意的是,感知损失里VGG的权重必须全程冻结,而且不需要梯度。如果忘记加requires_grad=False,训练时会多出一大块显存占用,且模型参数更新方向会被干扰。

4.4 训练策略:warmup、EMA与边界约束

训练策略上,我推荐几个比较实用的配置。优化器选择AdamW,初始学习率1e-4,权重衰减5e-4。第一个epoch做线性warmup,从1e-5慢慢拉到1e-4,能避免模型在初期被大梯度推开。之后使用余弦退火调度器,共训100个epoch。

EMA(指数移动平均)必须开。我是给所有模型参数维护一个滑动平均值,每步更新当前模型参数和EMA参数,验证集推理时用EMA参数。经验上EMA能让PSNR提升大约0.2到0.3dB,而且几乎不增加显存消耗。在训练脚本里写一个简单的EMA类即可。

红色或蓝色手写笔迹有一个隐性危险:通道数值域远高于印刷体。如果训练时不做约束,网络可能会输出偏色区域。我训练时额外加了边界约束,让没有mask覆盖的地方严格向原图回归——简单做法是在L1损失的权重上,背景区域的loss再乘以1.5。这样网络会优先保证底色不变,对遮罩外内容破坏更小。在推理时如果发现印刷体被“淡出”了一截,多半是这一项权重不够。

5. 推理流程与质量评估

5.1 推理代码:把模型输出转成干净图片

测试阶段别直接把模型输出的tensor转图,还要做指数移动平均处理。下面是推理端的标准流程:

def inference(model, image, mask, size=256): model.eval() image = preprocess_image(image) # 调整尺寸、归一化 mask = preprocess_mask(mask) # resize + 二值化 resized = F.interpolate(image, size=(size, size), mode='bilinear', align_corners=False) resized_mask = F.interpolate(mask, size=(size, size), mode='nearest') with torch.no_grad(): pred = model(resized, resized_mask) # 把输出映射回原图尺寸,回贴到原图位置 pred = F.interpolate(pred, size=(image.shape[2], image.shape[3]), mode='bilinear', align_corners=False) result = pred.squeeze(0).cpu().numpy().transpose((1, 2, 0)) result = (result + 1) / 2 * 255 result = np.clip(result, 0, 255).astype(np.uint8) return result

这里有个小地方要注意:mask做resize时必须用最近邻插值,不能双线性。因为mask是离散的0/1标签,双线性插值会在边缘产生灰色值,这些值喂进模型后会让门控卷积产生不可预测的响应。测试时如果发现修复区域有一圈淡淡的边框,先检查是不是mask插值方式出了问题。

5.2 评估指标:PSNR、SSIM与LPIPS,还要看OCR效果

很多人只关注PSNR和SSIM,这在文档修复上是会骗人的。一个输出图可能PSNR很高,但印刷字笔画位置差了一两个像素,肉眼根本看不出,OCR引擎却已经误读了。所以我在验证时额外加了一个LPIPS指标,专门衡量感知相似度,LPIPS越低表示看起来越接近干净图。

但最实用的指标其实是OCR正确率。把修复后的图送进OCR引擎,和原干净底图的OCR结果对比,看错字数量少了多少。我做过一批实验:100张带手写测试图上,原图OCR准确率大约只有61%,PmrNet修复后可以提升到92%,而只做阈值预处理能到70%左右。这个指标更接近业务价值,建议你也纳入评估体系。

方法PSNR(dB)SSIMLPIPSOCR准确率
原图(无修复)---61%
阈值+形态学24.30.810.3270%
普通UNet27.80.890.1985%
PmrNet29.50.930.1292%

这个数据来自我的模拟测试集,里面的合成比例和字体分布刻意做了分离,算是比较能反映泛化情况的。

5.3 实测效果与典型失败场景分析

PmrNet最理想的使用场景是:手写笔迹叠加在印刷体上,但底稿文字清晰,纸张扫描亮度均匀。在这种场景下修复结果基本可以做到以假乱真。我测试了一张白纸黑字、蓝色圆珠笔批注的文档,修复后蓝色痕迹消失干净,印刷体的横折撇捺没有明显断裂,放大后边缘有一点点轻微柔化,但不影响阅读。

失败的场景也有两个典型方向。第一种是手写笔迹覆盖区域特别大,比如一整行都被画了粗线,模型因为没有足够上下文线索,会把该区域重构得模模糊糊,像是“猜”出来的。第二种是底图本身质量很差,比如扫描分辨率不足、纸张灰度不均匀,模型输出会呈现网格状伪影。我的解决办法是训完主模型之后,再单独训练一个轻量精修模型,把主模型输出和原图做一次late融合,专治这种极端区域。

6. 训练与使用中踩过的坑

6.1 掩膜边缘发黑或发亮

这个坑在图像修复里特别常见。原因一般是两个:mask边缘不准确,或者训练时没有对mask做膨胀腐蚀。我之前图省事,直接用合成脚本的精确掩膜,结果验证时一条深色边缘像描边一样留在笔画周围。

处理办法很简单:训练时对mask做随机腐蚀膨胀,同时把mask的边缘模糊1到2像素。添加后边缘伪影立刻减轻。如果推理时还出现,可以尝试降低mask输入通道的权重,或者把注意力模块里的sigmoid增益减小一点。

6.2 训练loss到一定程度不降

训练到第20个epoch左右,L1 loss降到大概0.038附近就停滞不前了。先不要急着调网络结构,大概率是学习率太小或者batch size太小。我建议先看图像重构输出:如果整体修复质量可以但细节模糊,就需要提高loss中感知损失权重;如果是某些特定字体完全修不对,那就去看训练集里这类字体占比是不是太少。

还有一种情况是EMA没有同步更新BN层的统计值。EMA只更新模型参数,但BatchNorm的running_mean和running_var不会跟着EMA版本走。PmrNet的编码器里用了不少BN,这会导致验证集效果比训练集差一截。解决办法是训练最后5个epoch关闭BN的running统计更新,或者直接切换到InstanceNorm。

6.3 手写笔迹颜色深浅差异大

红色笔迹通常容易去掉,因为红色通道和印刷体的黑白颜色差异大,网络很快能识别。但铅笔灰度笔迹更坑,亮度、对比度和灰度印刷体非常接近,模型经常把两者一起擦掉。我的应对措施是:在合成数据里专门增加一批低对比度手写样本,把手写层的透明度压低到40%到70%之间,同时叠加纸张扫描噪声,让模型学会“即使看起来像印刷体,只要在mask范围内就要处理”。

推理时如果遇到特别淡的铅笔字,可以先做一次对比度增强,再喂给PmrNet,输出后再把增强撤销,效果比直接硬跑好很多。

6.4 显存不足时怎么续训

PmrNet在256×256输入下,batch size设为8时大概占9GB显存,还算可控。如果显存更小,优先降低batch size到4或2,同时开启混合精度训练。混合精度在PyTorch里就是加一个torch.cuda.amp.autocast(),能省接近四成显存,而且对修复精度影响很小。

训练中断是家常便饭。我建议每500步保存一次checkpoint,同时把optimizer的state_dict和EMA参数一起保存。恢复时用last.pt继续训练,而不是从final.pt开始。我之前用final.pt续训过,结果学习率调度已经走完,导致loss直接飞了,这个教训很深刻。

6.5 快速排查表

现象常见原因处理方案
修复区域有明显黑边mask边缘不精确膨胀腐蚀mask并做边缘模糊
印刷字被削薄感知损失权重过低提高感知损失到0.6再试
手写笔迹没清干净掩膜定位覆盖率不足推理时手动膨胀mask 2像素
输出整片模糊纸张纹理过强且L1权重过大降低L1权重、提高SSIM权重
GPU显存不足batch过大开AMP或减小batch
验证loss远高于训练loss手写风格覆盖不足增加低对比度手写合成数据

PmrNet这套方案我前后打磨了大概三周,最花时间的部分不是网络设计,反而是合成数据的真实度和mask的鲁棒性。个人经验是,做这类文档修复项目,数据合成代码的工程含量决定了模型上限,网络结构反而是在搭好的数据舞台上锦上添花。后续要扩展的话,可以把PmrNet直接接到OCR前处理管线里,用纯CPU做量化推理,跑一张A4图也能控制在50毫秒左右。如果你手上正好有被手写笔迹污染的扫描件,建议照着这个思路先造一套小数据,把首轮效果跑出来,再决定要不要调大模型。

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

Windows开启CPU虚拟化与关闭Hyper-V完整指南

在虚拟化这条路上,我见过太多用户在 BIOS 里把“启用虚拟化”的开关来回拨,重启进系统后虚拟机软件还是不给面子。要么弹“此主机支持 Intel VT-x,但该功能处于禁用状态”,要么提示“VT-x is not available”,明明设置…

作者头像 李华
网站建设 2026/10/19 9:37:02

手写字图片修复:PmrNet掩膜细化网络的设计与训练

手写字图片修复这事,我断断续续折腾了大半年。起因很朴素:手里有一堆扫描的老笔记、试卷截图,上面的手写批注比正文还抢眼。用PS仿制图章一点点抠,效率低到让人怀疑人生,而且修完总能看到明显的涂抹痕迹。后来转念一想…

作者头像 李华
网站建设 2026/10/19 9:36:21

开源CLI代码评审工具:Git原生集成+本地LLM驱动

1. 项目概述:这不是又一个“AI写代码”玩具,而是一套可嵌入日常开发流的开源代码评审工作流“open-code-review”这个名字乍看平平无奇,但拆开来看——open(开源)、code(代码)、review&#xff…

作者头像 李华
网站建设 2026/10/19 9:35:52

AI论文降AI率实战:从AIGC检测原理到文本改造方法

做这期实测的起因很直接:我自己那段时间正在集中写两篇带实验数据的年度报告,第一稿全部用AI辅助搭框架、出初稿,结果拿某主流AIGC检测平台一跑,段落里面有一大半被标成了“高风险AI生成”。当时急着交材料,整个人相当…

作者头像 李华
网站建设 2026/10/19 9:35:34

FastAPI+LangGraph实战:多Agent服务四层架构与工程化落地

最近我一直在搞一套多Agent服务,技术栈就是标题里这套组合:FastAPI 提供 HTTP 网关能力,LangGraph 负责 Agent 编排,底层按照 Gateway / Agent / Tool / Memory 四层彻底切开。说句实在话,Demo 阶段怎么玩都行&#xf…

作者头像 李华
网站建设 2026/10/19 9:35:06

OpenMPI集群部署实战:从源码编译到性能调优的完整指南

1. 从一次集群部署翻车说起:为什么OpenMPI安装远不止一条命令很多人第一次接触OpenMPI,都是在高性能计算课程或者实验室集群上。老师或者师兄丢过来一句“装个OpenMPI就行”,然后你就去搜,搜到的答案清一色是sudo apt install ope…

作者头像 李华