news 2026/10/2 14:32:10

基于深度学习的图像修复:从选型到训练避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于深度学习的图像修复:从选型到训练避坑指南

简介:一套基于深度学习的图像修复算法完整实现方案,源自本科毕业设计并获得导师与评审认可的高分项目(96.5分),主要面向计算机相关专业正在做毕设、课程设计或需要实战练习的在校学生与开发者。资源包共84个文件,压缩后约3.57MB,包含20个Python源码脚本及对应pyc文件、PyTorch自定义算子(.cu/.cpp/.h)、Gradio可视化演示脚本、模型结构图与示例图片,并整合了Places和CelebA-HQ等测试集及掩码生成模块。累计已有253人学习浏览。项目封装了从数据集制作、掩码生成、模型训练到图像修复推理的完整链路,自带使用说明和预训练模型下载提示,支持CPU/GPU环境运行;借助Gradio可视化界面可直观对比破损图、原始图与修复结果,既可作为毕设课题的参考实现,也方便在此基础上扩展新模型或算法改进。

1. 基于深度学习的图像修复:从“填像素”到“生成语义”

拿到一份“基于深度学习的图像修复算法 Python 源码+数据集+项目说明”的项目包,第一件事不是点开 train.py 就跑,而是先想清楚这套代码在解决什么问题。图像修复(Image Inpainting)说白了就是:给一张残缺图和一张掩码,让模型把洞里缺失的内容补出来,补得既要在像素上连续,又要在语义上合理。传统扩散式 Inpainting 算法只会把周围像素往洞里平移,遇到细划痕和重复纹理还能应付,洞稍微大一点就开始拖影、糊成一片。深度学习把这个问题重新定义成条件生成:模型通过学习大量完整图像,学会“这里原本最可能有的是什么”。这篇笔记按一个修复项目从零到可用的顺序来拆:网络选型、数据构造、训练损失、推理细节和验证方法,新手能跟到跑通,熟手也能直接拿去比对参数和边界。

2. 修复算法选型:GAN、自编码器与扩散模型该选谁

2.1 修复任务拆解:结构、纹理与语义的三层需求

开始选模型前,先把修复区域按内容性质拆成三类,这样后面选网络、调损失都能对号入座,不会出现训练很久却效果很怪的情况。

第一类是纹理重复区域,比如墙面砖缝、草地颗粒、水面波纹。这类区域本身缺乏强语义,只要纹理统计上能和周围接上,人眼就很难挑毛病,传统方法在这一类里并不差。第二类是结构明确区域,比如人脸五官、车轮边缘、窗户棱线。这些位置有必然的几何走向,模型不能只生成“像素”,还得把线条位置、明暗关系和上下文对齐。第三类是语义生成区域,多见于大面积遮挡:物体被整块盖住,或者损伤区域跨过了多个物体,模型得像人一样根据上下文推断出“这里本来最可能是什么东西”。

需求层级掩码面积占比内容确定性典型场景合适方案
纹理重复小于10%低老照片划痕、旧文件字痕传统算法或轻量网络
结构引导10%~30%中人脸遮挡、行人移除感知损失 + U-Net 类结构
语义生成30%以上高大面积损毁、多重遮挡两阶段网络或扩散模型

不拆这个层级,最容易出现的结果是 L1 损失一直降、看着已经收敛,但输出的修复区域全是“平均脸”式的模糊——因为网络发现,把所有颜色取平均是让 L1 最小化的捷径。这个现象后面第 5 章还会单独讲。选模型之前先画一下自己任务的掩码比例区间和内容确定性,至少能省掉一半试错时间。

2.2 主流方案的适用范围与参数取舍

修复领域的成熟方案可以分成五类,每一类的网络结构、训练成本和效果边界都不一样。下面按我从简单到复杂的顺序排了张对比表,可以直接对着自己的任务选。

方法族代表实现核心思路优点缺点适用任务
扩散式传统Telea、Criminisi边界像素外推无需训练、秒级出结果只补重复纹理细划痕、小点状缺损
自编码器+判别器Context Encoder潜在空间重建加对抗判定能学到高层语义细节过平滑中小规模规则洞
部分卷积 U-NetPConv-UNet卷积只在有效像素上归一化大洞纹理自然对掩码分布敏感不规则中洞场景
双阶段粗到细EdgeConnect、StructureFlow先重建边缘结构,再填充纹理结构感明显训练链路长大洞、结构化场景
扩散采样模型RePaint、扩散修复变体迭代去噪采样生成质量最高推理慢、显存占用高离线高质量修复

实际项目里,大家起步最常用的还是自编码器加对抗训练这条线,原因是它训练相对稳定、迭代快,而且在中洞场景下效果足够好。扩散模型虽然生成质量高,但一次采样要迭代几十上百步,显存和时间成本不是所有场景都愿意付的。双阶段方案适合处理“结构很明显”的图,比如人脸和建筑,代价是网络从一套变成两套,训练数据也要多备一份边缘图。

2.3 为什么我首选 Partial Convolution 的 U-Net 作为起步基座

如果掩码形状是不规则的自由形状,普通卷积有个天然缺陷:卷积核扫到掩码边界时,会把“未知区域”的像素也当成有效特征一起算,结果就是边界信息被污染,生成的纹理出现模糊和重影。Partial Convolution(部分卷积)的思路是:每个卷积位置只根据该 kernel 覆盖到的有效像素做归一化,无效像素不参与统计。实现上很简单,训练稳定性却提升很多。

class PartialConv2d(nn.Module): def __init__(self, in_ch, out_ch, kernel_size=3, stride=1, padding=1, bias=True): super().__init__() self.conv = nn.Conv2d(in_ch, out_ch, kernel_size, stride, padding, bias=bias) # mask_conv 用来统计每个位置上有多少有效像素 self.mask_conv = nn.Conv2d(1, 1, kernel_size, stride, padding, bias=False) self.mask_conv.weight.data.fill_(1.0) self.mask_conv.requires_grad_(False) def forward(self, x, mask): # mask 为 0/1,1 表示已知有效像素 out = self.conv(x * mask) with torch.no_grad(): msum = self.mask_conv(mask) # 用有效像素数归一化,避免掩码边界处亮度跳变 scale = 1.0 / torch.clamp(msum, min=1e-8) if self.conv.bias is not None: out = (out - self.conv.bias) * scale + self.conv.bias else: out = out * scale mask_out = (msum > 0).float() return out * mask_out, mask_out

这段代码里有两个关键细节。第一个,mask_conv的权重全部初始化成 1 且不更新,它只是用来做“求和统计”,等价于统计每个卷积窗口里有效像素的个数。第二个,out除以有效像素个数之后,要去掉偏置再乘回去,因为Conv2d自带的 bias 是在所有像素上加的,如果不处理,掩码全为 0 的位置会把 bias 泄漏进输出。

基于这个算子,把 U-Net 的每一层卷积替换成PartialConv2d,就得到了最常用的 PConv-UNet 基座。它的输入是 4 通道:RGB 三通道加一个 mask 通道,输出 3 通道修复结果。这种结构的参数规模适中,编码器和解码器通道数一般取 [64, 128, 256, 512],训练起来对显存比较友好,也是后面第 4 章训练脚本里我采用的默认结构。

3. 构建修复训练集:掩码生成是决定泛化能力的第一步

3.1 用公开数据还是自采数据:先定“域”再定“集”

修复模型的效果上限,基本由训练数据决定。一个只在自然风景图上训练过的模型,拿去修工业图纸,结果通常不会太好:结构线条、字体边缘、材质反光都跟训练分布对不上。所以选数据集之前,先明确你最终要修的图属于哪个“域”。

如果目标是通用修复,可以选 Places365 这类以场景为主的公开数据集,室内、街道、建筑样本多,对大部分“抠掉多余物体”的任务够用。如果目标是修人脸,CelebA-HQ 是常见选择。如果目标是文档去划痕或者特定设备图像修复,那就别指望公开数据了,自己收集几百张同类型图像,反而更值得投入时间。公开数据集用来预训练、自采数据用来微调,是修复项目比较常见的组合路径。

注意:不要跳过掩码工程直接拿现成图片训练。修复任务里掩码的形状、面积分布和位置,直接决定模型泛化到真实场景的能力,这一点往往比网络结构更影响最终效果。

3.2 掩码生成代码与参数说明

真实世界里的缺陷形态五花八门,但用训练数据模拟时,常见做法是生成“自由形状”掩码——用随机多边形填充模拟刮痕、墨迹和遮挡。用固定矩形方块的掩码训练出的模型,到了一线场景很容易翻车,因为真实缺损很少是规则矩形。

import cv2 import numpy as np def random_free_form_mask(h, w, min_ratio=0.05, max_ratio=0.35, max_parts=8): """ 生成自由形状掩码。 返回值 mask 中 255 表示待修复区域,0 表示保留区域。 """ best_mask, best_ratio = None, 0 for _ in range(20): # 最多重采样 20 次,找到面积比例合适的掩码 mask = np.zeros((h, w), dtype=np.uint8) # 每张图叠加 4~8 个随机多边形,模拟多处破损 for _ in range(np.random.randint(4, max_parts)): n_pts = np.random.randint(3, 8) # 多边形顶点数,顶点越少形状越尖锐 pts = np.random.rand(n_pts, 2) pts[:, 0] *= w pts[:, 1] *= h cv2.fillPoly(mask, [pts.astype(np.int32)], 255) ratio = mask.mean() / 255.0 # 记录最接近 0.15 比例的掩码,作为重采样的兜底 if abs(ratio - 0.15) < abs(best_ratio - 0.15): best_ratio, best_mask = ratio, mask if min_ratio <= ratio <= max_ratio: return mask, ratio return best_mask, best_ratio

参数上,min_ratio和max_ratio控制掩码占全图面积的比例。建议训练时把区间放宽到 0.05~0.35,不要全部集中在 0.1 附近。掩码面积分布太窄,模型只见过一种“残缺程度”,测试时一旦遇到更大的洞就会崩,这个坑在第 5 章会展开。顶点数n_pts取 3~8,是为了让掩码边缘更随机——顶点太多会接近圆形,顶点太少又都是尖角,实际破损多半是弯弯曲曲的,混合几种形状更接近真实。

3.3 数据集的加载与训练集划分细节

掩码可以离线预生成存成文件,也可以在线随机构造。离线生成的优点是调试方便、掩码可复用,缺点是占磁盘空间,而且掩码一旦生成就固定了,模型反复看同一批掩码容易过拟合。在线生成则每次训练都拿到新掩码,泛化更好,代价是数据加载要快一点。我一般会在线生成掩码,把图像和掩码的读取逻辑封装在同一个 Dataset 里。

from torch.utils.data import Dataset import torch, glob, os import cv2 class InpaintDataset(Dataset): def __init__(self, root, image_size=256, mask_dir=None): self.paths = sorted(glob.glob(os.path.join(root, "*.jpg")) + glob.glob(os.path.join(root, "*.png"))) self.image_size = image_size self.mask_dir = mask_dir # 传 None 则在线生成掩码 def __len__(self): return len(self.paths) def __getitem__(self, idx): img = cv2.imread(self.paths[idx]) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (self.image_size, self.image_size)) / 255.0 img = torch.from_numpy(img).permute(2, 0, 1).float() if self.mask_dir: mask_path = os.path.join(self.mask_dir, os.path.basename(self.paths[idx])) mask = cv2.imread(mask_path, 0) # 掩码缩放必须用最近邻插值,否则会引入灰色过渡边界 mask = cv2.resize(mask, (self.image_size, self.image_size), interpolation=cv2.INTER_NEAREST) else: mask, _ = random_free_form_mask(self.image_size, self.image_size) mask = (mask > 127).astype(np.float32) return img, torch.from_numpy(mask).unsqueeze(0).float()

这里有两个细节值得写进注释。第一,图像缩放到目标尺寸时要保持 RGB 和掩码的空间一致,掩码变尺寸必须用INTER_NEAREST,用双线性插值会给掩码边界引入 0~1 之间的过渡值,训练时网络会在那些位置收到混乱的监督信号。第二,训练集和验证集划分不要偷懒用random_split不固定 seed 的方式,建议先对图片路径按 hash 或直接按文件名顺序间隔划分,保证每次实验的验证集一致,否则两个实验的指标差异到底是模型造成的还是数据划分造成的,根本说不清楚。

4. 训练与推理实现:从源码结构到损失函数的调参

4.1 项目目录与最小可跑训练脚本

一套修复项目的源码组织,我会按下面这个结构来放,路径短、模块边界清楚,关键配置全部收敛到config.py,这样排错时不用到处找参数。

inpaint_project/ ├── config.py # 所有超参数集中在这里 ├── dataset.py # 数据集读取与掩码生成 ├── model.py # PConvUNet 网络定义 ├── losses.py # 组合损失函数 ├── train.py # 训练主脚本 ├── inpaint.py # 推理脚本 └── checkpoints/ # 模型权重保存目录

训练主循环的核心部分可以写成下面这样,重点是把 mask 和图像的组合方式写对。

import torch import torch.optim as optim from torch.utils.data import DataLoader from dataset import InpaintDataset device = "cuda" if torch.cuda.is_available() else "cpu" train_loader = DataLoader( InpaintDataset("data/train", image_size=cfg.image_size), batch_size=cfg.batch_size, shuffle=True, num_workers=4, drop_last=True, ) model = PConvUNet(in_ch=4, out_ch=3, base_ch=64).to(device) opt = optim.Adam(model.parameters(), lr=cfg.lr, betas=(0.5, 0.999)) criterion = TotalLoss(gamma_l1=1.0, gamma_per=0.05, gamma_adv=0.1) for epoch in range(cfg.epochs): model.train() for imgs, masks in train_loader: imgs, masks = imgs.to(device), masks.to(device) # 先盖住待修复区域,再拼上 mask 作为条件输入 masked_img = imgs * (1 - masks) model_in = torch.cat([masked_img, masks], dim=1) pred = model(model_in) # pred 是模型输出的完整结果,与原始保留区域合成 pred_comp = imgs * (1 - masks) + pred * masks loss, loss_detail = criterion(pred, pred_comp, imgs, masks) opt.zero_grad() loss.backward() opt.step()

训练逻辑里容易被忽略的是pred_comp这一行。模型直接输出的 pred 是“在掩码区域生成的像素”,但如果只拿它和真实图像在掩码区域算损失,会让网络只关注洞里,忽略洞边缘的连续性。把pred和原图保留区域拼回pred_comp再算感知损失,能让生成内容和周围边界一起被监督,边缘过渡会自然很多。我第一次跑修复训练时没拼这一层,结果所有输出在掩码附近都有一圈暗边,后来加回pred_comp才消掉。

4.2 损失函数三件套:L1、感知与对抗的权重

修复任务很少只用一个损失函数,常见组合是 L1 重建损失、感知损失和对抗损失三件套。它们各自盯的东西不一样,权重配比直接决定输出是“锐利但有瑕疵”还是“平滑但模糊”。

损失项实现方式常用权重监督对象权重带来的影响
L1 重建掩码区域像素差绝对值1.0像素级颜色与结构权重过高会让纹理模糊
感知损失VGG 中间特征图的 L10.05高层语义与结构过低会造成结构错位
对抗损失PatchGAN 判别器0.1纹理真实度与分布过高会出现色斑和不稳定块

写损失模块时,感知损失要特别注意输入分布。VGG16 预训练模型是在特定归一化方式下训练的,如果你的图像是 0~1 范围的 RGB,直接喂给 VGG 提取特征,特征分布会偏移,感知损失约等于一个“没对准的尺子”,实际起不到监督作用。正常做法是在损失函数里加一个固定的归一化层,把输入先归一化到 VGG 的期望范围,再提取特征。

import torch.nn.functional as F import torch import torch.nn as nn class TotalLoss(nn.Module): def __init__(self, gamma_l1=1.0, gamma_per=0.05, gamma_adv=0.1): super().__init__() self.gamma_l1 = gamma_l1 self.gamma_per = gamma_per self.gamma_adv = gamma_adv def forward(self, pred, pred_comp, target, mask): # 只在掩码区域计算 L1,背景不参与 l1_loss = F.l1_loss(pred * mask, target * mask) # 感知损失和对抗损失用合成后的完整图计算 per_loss = F.l1_loss(vgg_features(pred_comp), vgg_features(target)) adv_loss = -torch.mean(discriminator_logit(pred_comp)) total = self.gamma_l1 * l1_loss + self.gamma_per * per_loss + self.gamma_adv * adv_loss return total, {"l1": l1_loss.item(), "per": per_loss.item(), "adv": adv_loss.item()}

这里的vgg_features一般取 VGG16 的 relu1_2、relu2_2、relu3_3、relu4_3 四层特征,而不是只取最后一层。多层特征分别对应低层纹理和高层结构,四层一起算 L1 比单层稳定得多。对抗损失里discriminator_logit是判别器对合成图的打分,训练时判别器和生成器交替更新,具体在代码里体现为“生成器 loss 反向后先更新生成器,再单独更新一次判别器”,这个节奏不要省。

4.3 推理阶段的两个关键细节:掩码平滑与归一化

训练完成后写推理脚本,要注意两件事:第一,输入尺寸需要对齐到模型下采样的整数倍,否则 U-Net 的跳层连接拼接时尺寸对不上,输出边缘会出现条纹伪影;第二,直接输出硬拼接的修复图,掩码边界往往会露出一条接缝,推理时把掩码做一次高斯平滑再融图,可以省掉后面大量修边工作。

import torch import cv2 import numpy as np def inpaint_one(model, img_bgr, mask_bgr): # 输入约定:img 取 0~255,mask 取 0 或 255 img = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) / 255.0 h, w = img.shape[:2] # 把尺寸裁到 16 的整数倍,避免 U-Net 下采样后尺寸不匹配 h2, w2 = h // 16 * 16, w // 16 * 16 img = cv2.resize(img, (w2, h2)) mask = cv2.resize(mask_bgr, (w2, h2), interpolation=cv2.INTER_NEAREST) / 255.0 img_t = torch.from_numpy(img.transpose(2, 0, 1)).unsqueeze(0).float() mask_t = torch.from_numpy(mask).unsqueeze(0).unsqueeze(0).float() with torch.no_grad(): x = torch.cat([img_t * (1 - mask_t), mask_t], dim=1).to(device) pred = model(x) pred_np = pred.squeeze(0).cpu().numpy().transpose(1, 2, 0) # 把掩码边界做高斯模糊,与周围像素渐变混合,减少接缝 soft_mask = cv2.GaussianBlur(mask, (5, 5), 0)[..., None] out = img * (1 - soft_mask) + pred_np * soft_mask out = np.clip(out, 0, 1) return cv2.cvtColor((out * 255).astype(np.uint8), cv2.COLOR_RGB2BGR)

cv2.GaussianBlur的核大小取 5 比较常规,取的太大洞边缘会被背景颜色“浸”进来,修复区域边缘发灰;取的太小又起不到过渡作用。如果训练时已经对掩码做了多种形态处理,推理这里的卷积核大小可以和训练阶段的mask_dilate参数配合调。

5. 图像修复避坑记录:五个常见的翻车场景与对应解法

5.1 修复区域像被水彩晕开

现象:训练损失下降正常,但输出里洞区一片平滑,像把周围颜色调淡之后直接填上去,完全没有纹理细节,人脸修复尤其明显——皮肤成了塑料质感。

原因:L1 重建损失权重过大,对抗损失权重过小。L1 对每个像素求绝对差值,模型要找最小化全体误差的解,最优策略就是把所有候补颜色取平均。取平均之后细节自然丢失。另一个常见诱因是判别器先崩:判别器损失一路为零或者原地震荡,生成器拿不到有效梯度,对抗部分形同虚设。

解决:先给训练脚本加一条判别器 loss 的日志。如果发现判别器 loss 长期为零,把判别器学习率降到生成器的十分之一,或者把每轮判别器更新次数从 1 次提到 2 次。然后按第 4 章的权重表调对抗权重,从 0.1 提到 0.2,感知权重从 0.05 提到 0.08。如果还不理想,把全局判别器换成分辨率更细的 PatchGAN,判别器只看局部 70×70 的区域,给生成器更密集的纹理反馈。

5.2 掩码边缘一圈灰色接缝

现象:洞里补出来的内容本身没问题,但沿着掩码边界有一圈肉眼可见的暗边或亮线,像两张图没对齐直接叠在一起。

原因:掩码在 0/1 边界硬切换,Partial Convolution 在边界位置上归一化分母突然跳变,导致输出在边界出现亮度变化。另一个常见来源是训练阶段掩码 resize 时用了双线性插值,掩码边界混入了灰色中间值,网络被一个“模糊的监督信号”带偏了。

解决:推理阶段按第 4.3 节的做法,把掩码做高斯平滑后与原始背景融合。训练阶段给掩码 resize 时严格使用INTER_NEAREST,用一次就少踩一个坑。还能在训练数据里加一个预处理:把掩码用 3×3 的卷积核做一次腐蚀,让掩码边界和真实遮挡物的边界错开几个像素,模型就不会总去学“边界必须落在固定位置”。

5.3 生成的内容和上下文语义冲突

现象:一个人的半张脸修复后,眼窝朝向反了,或者额头高光位置跟另一边相差太多;一张马路的修复结果里,空缺处被补出一片完全不像路面的花丛纹理。

原因:模型只学会了纹理统计,没学会高层结构先验。L1 和感知损失能把低频颜色、整体布局压对,但像“眼睛看向哪里”“门的边缘线该往哪拐”这类强结构信息,单靠一个生成网络很难学进去。训练数据里同类缺损样本太少,也会加重这个问题。

解决:最直接的办法是换成双阶段结构,先让一个网络重建残缺区域的边缘图,再把边缘图作为条件送入填充网络生成纹理。做法是训练前先用 Canny 对原始图像提边缘,把掩码区域的边缘置为零,第一阶段网络补边缘,第二阶段再填纹理。这个方案比单纯调损失权重有效得多,因为“边缘结构”这种强约束被显式建模了。

5.4 训练稳定但大掩码推理崩坏

现象:训练全程损失正常,小掩码验证集上的指标也很好,但测试时掩码面积超过训练常见范围,输出直接变成大块色斑、锐度过曝或者胡乱拼接的结构。

原因:训练时掩码面积分布扎堆在小比例区间。模型在训练中见过的掩码大部分占图面积 5% 到 15%,一旦测试掩码上到 30% 以上,周围已知像素提供的信息太少,模型进入从未见过的“低信息输入”区间,输出自然崩坏。这是典型的训练分布和测试分布不一致。

解决:把第 3 章的掩码生成参数放宽,min_ratio=0.05、max_ratio=0.35,并且每个 batch 内保证掩码面积大小差异明显。最简单的方式是每个 batch 采样时给掩码池里分桶,一部分小掩码、一部分中掩码、一部分大掩码,按固定比例混合。这样模型每一轮迭代都看到不同残缺度,泛化边界能明显外推。

5.5 显存溢出与恢复训练的状态丢失

现象:训练跑到第 300 轮直接 CUDA out of memory,重启后把模型权重载回来继续训练,发现效果反而比不中断跑完的版本差,损失曲线也回不到中断前的位置。

原因:OOM 通常是单次 batch 峰值显存超限,常见于输入分辨率过大或者 batch size 过大;而重启后效果变差,大概率是 checkpoint 里只存了模型权重,没保存优化器状态和学习率调度器状态。Adam 这类优化器带有动量信息,丢掉之后学习率重新从零开始,相当于训练节奏被强制打断。

解决:checkpoint 至少保存四样:模型权重、优化器状态、调度器状态和当前 epoch。示意见下。

torch.save({ "epoch": epoch, "model": model.state_dict(), "optimizer": opt.state_dict(), "scheduler": scheduler.state_dict(), }, "checkpoints/latest.pt")

如果单卡显存确实不够,先用梯度累积把等效 batch size 撑住,再考虑换大显存卡。梯度累积实现方式很直接,每 4 个 batch 调用一次opt.step()和opt.zero_grad(),等效 batch size 翻 4 倍,显存峰值不变。这样比盲目调小 batch size 保留更多训练稳定性。

6. 效果验证与二次迭代:不只看 PSNR 和 SSIM

验证修复效果时,只报一个全局 PSNR 很容易骗到自己。PSNR 和 SSIM 都是全局统计指标,修复区域只占全图一小部分时,全局指标会被大面积未修改的背景拉高,模型烂没烂根本看不出来。建议把指标限定在掩码区域单独算,同时引入 LPIPS 感知距离,它和人眼的主观判断更接近。

import lpips import torch from skimage.metrics import structural_similarity, peak_signal_noise_ratio lp = lpips.LPIPS(net="alex") # 感知距离,值越低表示越接近参考图 def evaluate_one(pred, target, mask): # pred/target 都是 0~1 的 RGB ndarray,mask 为 0~1 的单通道 psnr_global = peak_signal_noise_ratio(target, pred, data_range=1.0) ssim_global = structural_similarity(target, pred, channel_axis=2, data_range=1.0) p_dist = lp(torch.from_numpy(pred).permute(2, 0, 1).unsqueeze(0), torch.from_numpy(target).permute(2, 0, 1).unsqueeze(0)).item() # 只在掩码区域算 PSNR,反映真实的修复质量 m = mask > 0 psnr_mask = peak_signal_noise_ratio(target[m], pred[m], data_range=1.0) return {"psnr_global": psnr_global, "ssim_global": ssim_global, "lpips": p_dist, "psnr_mask": psnr_mask}

掩码区域单独算指标之后,你会发现很多全局指标好看的模型在psnr_mask上露馅。我自己的习惯做法是:每次训练完,把验证集里psnr_mask最低的那一批图捞出来,不看指标光看人眼。观察这批失败样本到底是纹理糊、结构错还是颜色偏,再回头决定下一步动数据还是动损失。举个例子,如果失败样本集中在特定掩码形状上,基本可以断定是训练掩码分布不够多样;如果集中在特定内容类型上,那就是对应域的训练样本稀缺,继续调模型结构意义不大。修复这个方向,数据分布和掩码工程的影响通常大于网络结构微调,把自动指标和人工抽检放在一起用,才不会在“指标好看但实际不可用”上面空转太久。希望这个验证思路能帮你在自己的修复项目里少走弯路。

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

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

OpenHarmony Flutter 布局:Expanded 原理与实战解析

1. 为什么要在 OpenHarmony 上重新理解 Expanded1.1 一个组件背后的布局哲学先说个有意思的事。很多从 Android 或者前端转过来的朋友&#xff0c;第一次看到 Flutter 的布局方式都会有点懵——为什么没有 wrap_content 和 match_parent&#xff1f;为什么一个 Row 里塞几个 Te…

作者头像 李华
网站建设 2026/10/2 14:31:24

OneDrive位置改到移动硬盘:官方迁移与目录挂载完整指南

你是不是也被C盘空间不足逼到过墙角&#xff1f;笔记本出厂就一块256G的固态&#xff0c;OneDrive挂了几年&#xff0c;同步文件夹硬生生吃掉几十个G&#xff0c;明明云端买了1TB&#xff0c;系统盘却先报警了。之前同事问我“OneDrive能不能放到移动硬盘里”&#xff0c;我想都…

作者头像 李华
网站建设 2026/10/2 14:31:24

公路落石检测数据集:小目标+强干扰场景实战指南

简介&#xff1a;本资源是一份面向计算机视觉初学者与目标检测实践者的公路落石检测专用数据集&#xff0c;聚焦小样本场景下的单类别&#xff08;stone&#xff09;边界框标注任务&#xff0c;适用于YOLO系列模型训练、VOC格式转换练习及数据预处理全流程学习。压缩包共1019个…

作者头像 李华
网站建设 2026/10/2 14:30:56

工业软件全景入门:从鼠标三键到CAD/CAE/CAM与PLM链路

先讲一个我在车间里见过无数次的场景&#xff1a;老师傅扔过来一个铁疙瘩说“把这个翻个角度看看”&#xff0c;新来的大学生打开电脑&#xff0c;鼠标在三维模型上划了半天&#xff0c;愣是不知道怎么转视角——因为在这类软件里&#xff0c;中键才是旋转视角的按键&#xff0…

作者头像 李华
网站建设 2026/10/2 14:29:10

GIS批量赋值实战指南:从字段规划到空间关联与AI辅助

批量赋值这四个字&#xff0c;干过GIS数据整理的同行应该都不陌生。项目急着交&#xff0c;几千个地块要按行政区划填编码&#xff0c;几百个采样点要根据高程区间打等级&#xff0c;图斑属性要按面积批量归类——手动一个个改字段&#xff0c;改到眼睛发花是常态&#xff0c;更…

作者头像 李华
网站建设 2026/10/2 14:28:51

游戏美术和数字雕刻怎么选?先分清职业体系与技能工具

“老师&#xff0c;游戏美术和数字雕刻哪个更适合我&#xff1f;”这句话&#xff0c;我基本每周都会在私信和社群里看到一次。问的人里有刚毕业的美术生&#xff0c;有工作几年想转行的从业者&#xff0c;也有纯粹想学门技能搞副业的上班族。但说实话&#xff0c;我第一次看到…

作者头像 李华