基于深度学习的图像修复与修补:U-Net与mask引导的老照片复原关键技术
2026/9/16 7:35:51 网站建设 项目流程

简介:这套基于深度学习的老旧照片修复与修补项目源码,面向具备Python与机器学习基础的开发者与研究人员,用于对破损、划痕、模糊的老照片进行自动检测与高质量重建,可广泛应用于老照片收藏、历史影像数字化及数字媒体修复等场景。源码共95个文件,压缩包大小约39.27MB,以63个Python脚本为核心,覆盖数据集加载、特征提取、模型训练、人脸检测对齐、图像映射及GUI交互等完整流程;同时附带19张PNG示例图、Dockerfile与权重自动下载脚本,便于在不同环境快速部署。已有455人学习浏览,适合作为图像修复方向的实战参考。项目不仅支持老旧照片的划痕修复与局部补全,还包含超分辨率、降噪等扩展能力,并配有清晰的目录组织与readme说明,方便读者从零运行、二次开发或迁移到其他图像处理任务。

1. 基于深度学习的老旧照片修复与修补源码:先认清损伤结构,再谈模型

基于深度学习的老旧照片修复与修补,第一步不是选模型,而是看明白照片上的损伤。扫描一张上世纪合影,300dpi 下问题全暴露:划痕横穿人脸,边缘缺角,胶片颗粒糊住暗部,黄褐色偏色带偏所有肤色。用去噪、锐化插件逐个试,每修好一项就在另一项上留下新痕迹——退化是叠加的,单功能算法天然不匹配。

这套设计的核心,是把「修复」和「修补」放进同一个网络:输入侧拼接二进制 mask 通道标注损伤与缺失,输出侧用按 mask 加权的损失把优化资源倾斜给破损区。两个任务共享权重,推理时不用在模型间切换。适合跑过基础分类、想往图像复原方向落的工程师,也适合手里有批量扫描件的人。单张 11G 显存卡就能从零训练到出图,下面按模型选型、数据合成、训练调参、大图推理展开。

2. 修复与修补的模型骨架:U-Net 为什么比复杂结构更可靠

2.1 老照片的四类退化与退化建模

老照片上的退化是复合的,不是单一噪声或单一模糊。工程上常用的退化模型把观测图写成:

I_obs = M ⊙ T(I_clean) + (1 - M) ⊙ ε

M 是标注破损位置的二进制 mask,T 是全局退化(偏色、模糊、压缩痕迹),ε 是加性噪声。这个式子精确划出了修复与修补的分界:当 M 全为 0,模型做的是恢复被全局变换和噪声破坏的像素,属于图像修复(restoration);当 M 非 0,被 mask 盖住的像素完全没有观测值,模型必须依靠周围上下文生成内容,这才是修补(inpainting)。标题里「修复与修补」并列,本质就是要求一个模型同时处理这两种情况。

落到真实扫描件上,退化可以拆成四类,各自的处理逻辑差异很大:

  1. 划痕是稀疏、线性、高对比的结构损伤。像素占比很小,但视觉权重极高。模型需要先定位再抑制,定位信息最好由 mask 通道显式提供,而不是指望卷积自己从数据里发现规律。
  2. 撕裂与虫蛀孔洞是大面积缺失。局部滤波完全无效,必须靠下采样带来的全局语义、以及跳连带回的边缘结构共同决定内容。
  3. 胶片颗粒接近高斯噪声,但空域分布不均匀。CNN 的滑窗统计天然匹配这类退化,不需要单独串联一个去噪模块。
  4. 偏色是全局通道缩放,等价于可学习的 1×1 颜色映射。它本身最简单,但和去噪、去划痕耦合在一起,端到端优化更合适。

四类退化里,前两类靠 mask 引导,后两类靠全局统计。要把它们放进同一个网络,关键设计就是让 mask 成为输入的一部分。

2.2 U-Net 的 3 个选择理由与参数规模对比

U-Net 是 2015 年提出的结构,在老照片修复这类数据量不大、退化类型杂、还要批量上生产的任务里,它仍然是性价比最高的骨架。

第一个理由是多尺度感受野与高频信息兼顾。四次下采样把 256×256 的输入压到 16×16,最深层每个位置都相当于看到了整个 patch 的上下文,这是补大面积缺失的前提;跳连又把下采样前的高频特征原样带回解码器,保证了划痕边缘这类细节不会被池化抹掉。去掉跳连的编解码结构,输出往往是「结构对了、细节糊了」,正是修复任务最忌讳的结果。

第二个理由是收敛行为可预期。只用 L1/L2 损失就能收敛,不依赖 GAN 的对抗平衡。对比 SwinIR 需要小心调节窗口注意力的归一化参数,MPRNet 要维护多阶段监督,U-Net 在单卡上从零开始训练极少出现 loss 发散的状况。

第三个理由是层数边界清晰,参数容量好控制。下表是三种骨架在实际修复场景里的对比。

骨架参数规模混合退化适应度收敛可靠性256×256 推理显存实现成本
U-Net(base=48)约 5M约 1.5 GB
SwinIR(large 变体)数十 M 量级约 3 GB
MPRNet约 20M(多阶段开销大)中低约 4 GB

一个容易忽略的点:修复任务里 SwinIR 的瓶颈不在参数量,而在窗口注意力的 padding 和 reshape 开销,批量处理几千张照片时速度差距会被放大。

2.3 把 mask 拼进输入通道:修复与修补共用一份权重的代码实现

常见错误做法是把缺损区域直接置黑。网络看到的输入是一个黑色窟窿,它分不清这个窟窿是「需要补的内容」还是「本来就暗的背景」,于是倾向于输出模糊的暗色斑块。正解是把 mask 作为额外通道和 RGB 拼接,输入变成四通道 (B, 4, H, W)。网络在每一层都知道哪些位置没有观测值,解码器在这些位置可以放开手做语义生成;在 mask 为 0 的位置,则按恢复任务处理。

import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv = 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.conv(x) class UNet(nn.Module): def __init__(self, in_ch=4, out_ch=3, base=48): super().__init__() self.pool = nn.MaxPool2d(2) self.e1 = DoubleConv(in_ch, base) self.e2 = DoubleConv(base, base * 2) self.e3 = DoubleConv(base * 2, base * 4) self.e4 = DoubleConv(base * 4, base * 8) self.up1 = nn.ConvTranspose2d(base * 8, base * 4, 2, stride=2) self.d1 = DoubleConv(base * 8, base * 4) self.up2 = nn.ConvTranspose2d(base * 4, base * 2, 2, stride=2) self.d2 = DoubleConv(base * 4, base * 2) self.up3 = nn.ConvTranspose2d(base * 2, base, 2, stride=2) self.d3 = DoubleConv(base * 2, base) self.out_conv = nn.Conv2d(base, out_ch, 1) def forward(self, x): # x: (B, 4, H, W) = 3 通道受损图 + 1 通道 mask h1 = self.e1(x) h2 = self.e2(self.pool(h1)) h3 = self.e3(self.pool(h2)) h4 = self.e4(self.pool(h3)) d = self.d1(torch.cat([self.up1(h4), h3], dim=1)) d = self.d2(torch.cat([self.up2(d), h2], dim=1)) d = self.d3(torch.cat([self.up3(d), h1], dim=1)) return torch.sigmoid(self.out_conv(d))

逻辑说明:编码器每次池化把空间尺寸减半、通道数翻倍,四次之后 256×256 变成 16×16,底层特征在感受野层面已经覆盖整个输入。解码器每次用转置卷积上采样,再用 concat 把对应层的跳连特征接回来,因此 DoubleConv 的输入通道数是「上采样结果 + 跳连特征」两者之和。最后的sigmoid把输出约束到 (0,1),与训练数据的归一化范围一致,保存 PNG 时再乘回 255。

参数说明:in_ch=4对应 RGB+mask,训练和推理必须保持一致;如果只做纯修复不做修补,改成 3 即可。base=48是容量旋钮,代表第一层卷积的输出通道数;out_ch=3固定为 RGB。使用 mask 通道时注意值域必须严格是 0/1 的 float,不能是 0/255 的 uint8,否则拼接后 mask 的数值范围比 RGB 大两个量级,第一层卷积的输出会被它主导。

2.4 网络层数与通道数:修复任务真正值得调的两个旋钮

深度学习里的「网络层数」在图像复原中和分类不同:分类可以堆几十层换准确率,修复模型堆深了只会让底层特征图变小、梯度信号变弱。对 256×256 输入,四层下采样是公认的平衡点。只有缺失块大到占据图像宽度一半时,才值得把 patch 提到 384 并加到五层下采样,但底层特征图只剩 12×12,需要显著增加 epoch 数才能充分收敛。

模型容量不够时,优先加 base 通道数而不是加层。base 从 48 提到 64,参数量大约从 5M 涨到 8M,依然可控;base 到 96 约为 18M,显存不是问题,问题是数据量喂不喂得饱。几百张干净图用 base=48 就够,数据量到几千张再上 base=64,否则过拟合的速度比 PSNR 涨得快。

3. 数据合成与 Dataset:照片修复源码里最影响效果的部分

3.1 用 OpenCV 在线合成划痕、缺失、噪声和偏色

老照片修复没有天然的成对数据,不可能拿到同一张照片损坏前后的两份扫描件。标准做法是拿干净的高质量图像(现代数码照片、DIV2K/BSD 这类公开数据集),在训练时随机合成退化,把合成前后的图像对当作监督信号。这一步对真实泛化能力的影响,超过后续任何模型结构改动。如果你按《动手学深度学习》的方式跑过图像分类,这里唯一的变化是目标从标量变成像素张量。

import cv2 import numpy as np def random_damage(img, max_scratch=8, prob_missing=0.2): """ img: float32,RGB,值域 [0,1] 返回 (受损图, mask),mask 中 1 表示需要修补的区域 """ h, w, _ = img.shape damaged = img.copy() mask = np.zeros((h, w), dtype=np.float32) # 1) 划痕:亮暗随机,厚度随机 for _ in range(np.random.randint(1, max_scratch + 1)): x0, y0 = np.random.randint(0, w), np.random.randint(0, h) angle = np.random.uniform(0, 2 * np.pi) length = np.random.randint(30, max(h, w) // 2) x1 = int(np.clip(x0 + length * np.cos(angle), 0, w - 1)) y1 = int(np.clip(y0 + length * np.sin(angle), 0, h - 1)) color = 1.0 if np.random.rand() > 0.5 else 0.0 thickness = np.random.randint(1, 3) cv2.line(damaged, (x0, y0), (x1, y1), (color, color, color), thickness) cv2.line(mask, (x0, y0), (x1, y1), 1.0, thickness) # 2) 缺失块:模拟撕裂/撕角,置黑并写入 mask if np.random.rand() < prob_missing: bx, by = np.random.randint(0, w // 2), np.random.randint(0, h // 2) bw = np.random.randint(w // 8, w // 3) bh = np.random.randint(h // 8, h // 3) damaged[by:by + bh, bx:bx + bw] = 0.0 mask[by:by + bh, bx:bx + bw] = 1.0 # 3) 胶片颗粒:加性高斯噪声 noise = np.random.normal(0, 0.04, (h, w, 1)) damaged = np.clip(damaged + noise, 0, 1) # 4) 褪色/偏色:随机通道增益与偏置 gain = np.random.uniform(0.85, 1.15, (1, 1, 3)) bias = np.random.uniform(-0.05, 0.05, (1, 1, 3)) damaged = np.clip(damaged * gain + bias, 0, 1) return damaged, mask

逻辑说明:函数按顺序叠加划痕、缺失、噪声、偏色。划痕的 mask 必须与画线的 thickness 完全一致,否则网络会学到「mask 边缘有一圈对应不上的残影」;缺失区域的 mask 宽度与图像置零区域严格对应,这是保证后续加权损失位置准确的前提。胶片颗粒和偏色故意不进 mask,因为它们影响整幅图,把它们也标进去会让 mask 变成全 1,失去区分意义。

参数说明:max_scratch=8控制每条样本的划痕数量上限,真实扫描件通常在 3~15 条之间,设太小模型学不到多划痕叠加的效果;prob_missing=0.2表示约五分之一样本带缺失块,太低会让修补分支欠训练,太高会让网络过度依赖 mask 而忽视全局恢复;噪声 sigma 0.04 对应中等颗粒感,偏色增益 0.85~1.15 模拟胶片褪色的不均衡。想让模型更鲁棒,可以把划痕从直线换成cv2.polylines画的折线,缺失块从矩形换成交错的随机多边形。

3.2 Dataset 类:把受损图、干净图、mask 打包成训练样本

from torch.utils.data import Dataset import torch class OldPhotoPairDataset(Dataset): def __init__(self, clean_paths, patch_size=256, train=True): self.clean_paths = clean_paths self.patch_size = patch_size self.train = train def __len__(self): return len(self.clean_paths) def __getitem__(self, idx): img = cv2.imread(self.clean_paths[idx]) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 h, w, _ = img.shape if self.train: x0 = np.random.randint(0, w - self.patch_size + 1) y0 = np.random.randint(0, h - self.patch_size + 1) img = img[y0:y0 + self.patch_size, x0:x0 + self.patch_size] if np.random.rand() < 0.5: img = img[:, ::-1] k = np.random.randint(0, 4) if k: img = np.rot90(img, k).copy() damaged, mask = random_damage(img) damaged_t = torch.from_numpy(damaged.transpose(2, 0, 1).copy()).float() mask_t = torch.from_numpy(mask).unsqueeze(0).float() clean_t = torch.from_numpy(img.transpose(2, 0, 1).copy()).float() x = torch.cat([damaged_t, mask_t], dim=0) return x, clean_t, mask_t

逻辑说明:__getitem__返回三个张量——四通道输入 x、三通道干净图 clean、单通道 mask。训练时做随机裁剪、水平翻转和 90° 旋转;验证时不裁剪、不做增强。裁剪发生在退化合成之前,确保退化内容与 mask 始终对齐;如果先在整图上合成再裁剪,patch 边缘的划痕会被截断,mask 标注宽度和实际损伤位置会出现偏差。注意rot90[:, ::-1]都会产生非连续内存视图,torch.from_numpy之前必须.copy(),否则后面.float()会隐式复制多一次,白花内存。

参数说明:patch_size=256是显存与感受野的折衷,缺失块大的数据集建议升到 384;train=False时仍要求原图不小于 patch_size,数据准备阶段把不足 256×256 的老照片先等比放大,否则randint会抛异常。翻转和旋转对老照片很必要,很多扫描件在扫描时就已经歪了 90° 或者左右反了。训练前先遍历所有路径调用一次cv2.imread确认非空,影像文件损坏是这类管线最常出现的静默故障。

3.3 按 mask 加权的恢复损失:把优化资源写给破损区

普通 L1 损失对所有像素一视同仁。划痕和缺失区域通常只占整张照片的 5%~10%,模型完全忽视它们也能把全局 loss 压得很低,结果就是训练结束时修补区域仍然模糊。加权损失的思路是单独计算 mask 区域的 L1,并按面积归一化,让修补损失和全局损失处在同一个量级。

def restore_loss(pred, clean, mask, mask_weight=5.0): # 全图 L1:负责整体色调和结构 l1 = torch.abs(pred - clean) base_loss = l1.mean() # mask 区域 L1:按面积归一化,避免小破损被全局平均淹没 masked_loss = (l1 * mask).sum() / (mask.sum() + 1e-6) return base_loss + mask_weight * masked_loss

逻辑说明:l1 * mask中 mask 会自动广播到三个通道,只保留破损位置的误差;除以mask.sum()得到的是破损区域的平均绝对误差,与破损面积无关,所以面积很小的划痕也能获得和整图同等的梯度量级。mask_weight是两者之间的天平,默认 5.0 表示破损区域的优化优先级是全局的 5 倍。

如果只靠加权 L1 输出还是偏平滑,可以再加上感知损失:用预训练 VGG16 的relu1_2relu2_2relu3_3三个特征层,分别提取 pred 与 clean 的特征后计算 L1。感知损失和对抗损失的取舍如下表:

损失组合输出观感训练成本适用场景
仅 L1平滑、保守管线验证
L1 + mask 加权破损区域更干净默认推荐
再加 VGG 感知纹理自然干净图 2000 张以上
再加 PatchGAN 判别器锐利、细节多追求观感且能接受调参

这个顺序不要跳:先让加权 L1 把结构修对,再加感知损失提纹理,最后才考虑对抗损失。跳过前两步直接上 GAN,训练过程会同时面临位置不准和纹理伪影两个问题,排错难度翻倍。

4. 训练循环与参数调优:epoch、patch、batch 怎么设才可靠

4.1 训练主循环与断点续训

from torch.optim import Adam from torch.optim.lr_scheduler import CosineAnnealingLR from torch.utils.data import DataLoader device = "cuda" if torch.cuda.is_available() else "cpu" torch.backends.cudnn.benchmark = True model = UNet(in_ch=4, out_ch=3, base=48).to(device) dataset = OldPhotoPairDataset(clean_paths, patch_size=256, train=True) loader = DataLoader(dataset, batch_size=8, shuffle=True, num_workers=4, drop_last=True) optimizer = Adam(model.parameters(), lr=1e-4, betas=(0.9, 0.99)) scheduler = CosineAnnealingLR(optimizer, T_max=150, eta_min=1e-6) for epoch in range(1, 151): model.train() epoch_loss = 0.0 for x, clean, mask in loader: x, clean, mask = x.to(device), clean.to(device), mask.to(device) pred = model(x) loss = restore_loss(pred, clean, mask, mask_weight=5.0) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() epoch_loss += loss.item() scheduler.step() print(f"epoch {epoch:3d}/150 loss {epoch_loss / len(loader):.4f}") if epoch % 25 == 0: torch.save(model.state_dict(), f"unet_epoch_{epoch:03d}.pth")

逻辑说明:每个 epoch 内是标准的 forward-backward 三步;梯度裁剪放在backward()之后、step()之前,防止早期个别样本产生超大梯度推毁参数。CosineAnnealingLR的学习率从 1e-4 按余弦曲线衰减到 1e-6,配合 150 个 epoch,让网络前 60 个 epoch 大步搜索、后 90 个 epoch 精细收敛。断点续训不需要额外框架:保存时把模型、optimizer、scheduler 三份state_dict放进同一个字典,恢复时按同样顺序加载。

参数说明:drop_last=True是因为最后一个 batch 不足 8 张时,BatchNorm 的统计量会偏;num_workers=4对应机械硬盘要降到 2、SSD 可以提到 8,否则数据读取会成为训练瓶颈。cudnn.benchmark = True在输入尺寸固定时能带来明显提速,但输入尺寸不固定时反而会因为反复探查算法增加开销,必须关掉。

4.2 关键参数表

参数推荐初值调优方向
patch_size256×256缺失块大就升 384;显存不足先降 batch 而非 patch
batch_size811G 卡的上限;报 OOM 降到 4
learning_rate1e-4loss 震荡降为 3e-5;50 epoch 不下降升到 3e-4
total_epochs150验证集 PSNR 连续 20 epoch 不涨就早停
mask_weight5.0破损区域效果差调到 8,超过 10 容易出现彩色噪点
gradient clip1.0前几轮 loss 爆炸降到 0.5
weight_decay1e-5数据量小于一千张时加,防止过拟合合成退化

环境配置按 pytorch 官网的 CUDA 匹配流程走即可,这份源码只用了基础算子,pytorch 1.13 之后到 2.x 都能跑;常见的 cuDNN 兼容问题会在第一个 epoch 直接抛错,属于最容易排查的一类。

4.3 三个典型症状与排查顺序

第一个症状是 loss 不降或前二十个 epoch 震荡。优先怀疑数据管线而不是模型:检查 mask 与损伤是否对齐,最常见的 bug 是划痕画在img.copy()上、mask 却画在别的数组上,两处 thickness 不一致。打印每个 batch 的mask.sum()并随机抽一组可视化 x、clean、mask 叠加图,十秒钟就能确认问题出在哪一侧。

第二个症状是输出像水彩画,边缘柔和、没有纹理。这是纯 L1 损失的典型结果。按 3.3 的顺序先加 mask 加权,再加 VGG 感知损失;不要跳过结构修复直接上对抗损失,否则训练会同时面对位置偏移和纹理伪影两个问题。

第三个症状是训练集 PSNR 高、真实老照片效果差。这是合成退化和真实分布之间的 gap。改进手段:划痕加弯曲度(用cv2.polylines画折线)、缺失块用随机多边形代替矩形、噪声里混合少量盐椒噪声。工程上还有一个偷懒但有效的技巧:在几张真实照片上手动标注 mask,把标注样本按 1:9 混进训练集,专门修正分布偏移。

5. 大图分块推理与羽化拼接:源码里最后一段工程代码

5.1 分块推理与羽化拼接代码

训练时模型看的是 256×256 的 patch,实际扫描件动辄 6000×4000。把整图直接喂进去显存先爆,就算塞进去,下采样四次后的特征图也失去了细小划痕的空间分辨率。标准做法是滑动窗口分块推理,窗口之间留 overlap,拼接时用羽化权重融合重叠区,消除接缝。

def infer_large(model, img, mask, tile_size=512, overlap=64, device="cuda"): """ img: (H, W, 3) float32 [0,1] mask: (H, W) float32,1 表示破损 返回与 img 同尺寸的修复结果 """ model.eval() h, w = img.shape[:2] stride = tile_size - overlap pad_h = (tile_size - h % tile_size) % tile_size pad_w = (tile_size - w % tile_size) % tile_size img = np.pad(img, ((0, pad_h), (0, pad_w), (0, 0)), mode="edge") mask = np.pad(mask, ((0, pad_h), (0, pad_w)), mode="constant") H, W = img.shape[:2] y_steps = list(range(0, H - tile_size, stride)) if not y_steps or y_steps[-1] != H - tile_size: y_steps.append(H - tile_size) x_steps = list(range(0, W - tile_size, stride)) if not x_steps or x_steps[-1] != W - tile_size: x_steps.append(W - tile_size) out = np.zeros_like(img) weight = np.zeros((H, W, 1), dtype=np.float32) half = max(overlap // 2, 1) ramp = np.ones(tile_size, dtype=np.float32) for i in range(half): ramp[i] = i / half ramp[tile_size - 1 - i] = i / half alpha = ramp[:, None, None] * ramp[None, :, None] with torch.no_grad(): for y0 in y_steps: for x0 in x_steps: t_img = img[y0:y0 + tile_size, x0:x0 + tile_size] t_mask = mask[y0:y0 + tile_size, x0:x0 + tile_size] x = torch.from_numpy(t_img.transpose(2, 0, 1)).unsqueeze(0).float().to(device) m = torch.from_numpy(t_mask).unsqueeze(0).unsqueeze(0).float().to(device) pred = model(torch.cat([x, m], dim=1)) pred = pred.squeeze(0).cpu().numpy().transpose(1, 2, 0) out[y0:y0 + tile_size, x0:x0 + tile_size] += pred * alpha weight[y0:y0 + tile_size, x0:x0 + tile_size] += alpha out /= np.clip(weight, 1e-6, None) return out[:h, :w]

逻辑说明:每个 tile 推理完按羽化权重累加到输出缓冲区,同时累加权重图,最后逐元素除法归一化。羽化权重由两个一维 ramp 做外积得到,tile 边缘贡献趋近于 0、中心为 1,重叠区里两侧权重之和恰好构成平滑过渡,接缝因此消失。步长列表末尾显式补一个H - tile_size,保证 stride 不整除时最右和最下的边缘也被遍历到,不会缺角。

参数说明:tile_size=512是精度与速度的折衷,显存紧张就降到 256;overlap=64是 tile 的八分之一,重叠越小越快、接缝越明显。mode="edge"的 padding 只影响图像边缘一圈像素,最终裁剪回原尺寸时会丢弃。

提示:遇到 mask 全为 0 的 tile 时,直接复制原始像素跳过一次前向推理,批量处理几百张图能省下三分之一以上的耗时。

5.2 交付前的三项验证

第一,验证 masked PSNR。在保留的合成测试集上只计算 mask 区域的均方误差,公式与 3.3 的加权损失一致。masked PSNR 与全局 PSNR 差距超过 8dB 时,说明模型把大部分能力花在全局恢复上,修补分支还需加训练量。第二,验证羽化拼接缝。选一张缺陷较多的图,分别用 overlap=32、64、128 推理,放大 200% 对比重叠区;正常情况三者看不出差异,如果 overlap=64 还看到交叉虚影,说明羽化 ramp 或归一化除法有 bug,不是 overlap 不够。第三,验证显存峰值。分块推理的显存峰值由单个 tile 决定,与整图尺寸无关,6000×4000 的图配 tile_size=512 通常落在 2~3GB。

最后留一个工程细节:真实老照片的 mask 很少来自全自动标注,通常是阈值分割加人工修正。mask 精度会直接传导到修补区域边缘,拿捏尺度比调参更影响最终观感。标注时把 mask 边缘比划痕实际宽度放宽 1~2 像素,修复痕迹反而更不明显。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询