简介:基于Python的深度生成对抗网络(GAN)图像修复模型源码包,专为计算机专业毕业设计、期末大作业及项目实战学习者打造。项目以图像修复为核心任务,覆盖模型定义、训练流程与推理补全等关键环节,难度适中,适合快速上手并深入理解GAN原理与应用。压缩包共7个文件,包括6个Python脚本和1个Markdown文档,脚本分别承担网络结构搭建、数据预处理、判别器与生成器实现、DCGAN训练以及图像补全等功能,文档则提供原理说明与使用指引,整体结构清晰、便于阅读。资源体积仅12KB,轻量易用,代码均经过严格调试可稳定运行,评审得分高达98分。已有164人学习下载,是课程设计与实战练手的可靠参考。通过该资源,读者可掌握GAN图像修复从数据准备到模型训练、再到结果补全的完整流程,同时获得可扩展的项目框架,为后续深入研究生成对抗网络奠定基础。
1. 从“细纹补图”到“语义级修复”,这套GAN该怎么做才对
图像修复在大部分人的直觉里是个“补洞”问题:给一个mask,把缺失像素用周围颜色填上。但从工程落地看,传统的Diffusion-based、Patch-based方法一旦遇到大块缺失或人脸眼睛、汽车轮胎这类强语义结构,补出来的是“颜色正确、内容荒谬”的模糊团块。把修复任务交给GAN,本质上不是让网络“补像素”,而是让网络“在约束下做视觉上可信的内容生成”——这是判别器给生成器提供的“真实性”监督和L1/L2像素级监督之间做对抗与平衡。本文基于Python实现深度生成对抗网络GAN的图像修复模型,并从数据、模型结构、损失函数到训练策略拆开讲清楚,读者可以拿这套方案直接复现。适合刚接触GAN复现的算法工程师,也适合做图像编辑、老照片修复、OCR遮挡还原的工程团队。下面按“原理→建网络→训练坑点→评估部署”的顺序推进。
2. GAN图像修复的原理边界:为什么普通GAN补不出来细节
2.1 图像修复不是纯生成,而是在“已知域约束”里做采样
图像修复与文生图或风格迁移最本质的差异在于:修复结果必须强制满足已知区域的像素一致性。这要求模型同时具备两个能力:重建能力(Encoder-Decoder能把已知区域的信息压缩再解压)和生成能力(缺失区域的高频纹理与结构要由先验知识补全)。纯GAN结构,比如直接用DCGAN做Inpainting,生成器会陷入“自由发挥”,产生与周围完全无关的纹理。原因在于生成器的对抗性损失只惩罚“真实分布与生成分布之间的W距离”,并没有显式约束已知区域的逐像素一致。所以常用做法是在损失里加一个遮蔽区域的L1项,或在推理时做“Blending”操作——把生成器输出中已知区域的部分用原图直接替换。
2.2 门控卷积与注意力决定了修复质量的“语义上限”
传统卷积对有效像素和缺失像素一视同仁,网络很难区分“哪个位置是坏点该重新生成,哪个位置是好点该保留”。门控卷积(Gated Convolution)通过额外学一个动态掩码,让网络根据当前特征决定每个空间位置“放行多少信息”,这极大缓解了颜色偏差和模糊伪影。另一种提升语义上限的机制是注意力层,让缺失区域的特征可以“查询”到已知区域的相似纹理。实际复现时,我优先推荐Partial Convolution + Attention的组合,它比单纯堆U-Net层数在PSNR上见效更快。
2.3 数学视角:生成器在最小化一个带空间权重的混合损失
如果把修复模型写成一个优化问题,生成器G的目标函数长这样:
L_total = λ_adv * L_adv(G, D) + λ_l1 * ||M ⊙ (G(I_masked) - I_gt)||₁ + λ_per * ||φ(G(I_masked)) - φ(I_gt)||₂- M 是二值mask,损坏区域为1,有效区域为0
- ⊙ 是逐元素相乘,保证L1只在缺失区域起作用
- φ 是预训练VGG的特征提取层,用来做感知损失
- λ参数通常取 L_adv=1,L1=20,Perceptual=10,因为L1需要更高权重来防止颜色漂移
这里的关键洞察是:对抗损失站在全局真实性角度,L1站在像素重建角度,感知损失站在高维语义特征角度。三个损失在训练早期会互相拉扯,但收敛后生成器会逐渐找到“既符合周围语义、又保持局部清晰”的均衡点。
2.4 先验选择:Context Encoder还是DeepFillv2
从成型的网络架构看,业界量产较多的路线是DeepFillv2的变体——双编码器加粗粒度到细粒度的两阶段生成。但作为复现,我建议先用Context Encoder(CE)结构打个底:单编码器入、解码器出,中心mask固定,先保证L1能收敛,再往上加对抗和注意力。不要一上来就追SOTA,图像修复快速失败的关键在于损失项有没有分开验证。把CE练到能重建中心区域后,再替换成门控卷积和PatchGAN判别器,这样可以精确判断每个模块是否真的带来了收益。
3. 用Python搭出可运行的GAN修复源码:结构、损失与数据集
3.1 核心文件结构与职责划分
一个可维护的源码工程建议按下面这个文件树组织逻辑,而不是把所有训练逻辑堆在一个脚本里:
inpainting/ ├── data/ │ ├── dataset.py # 数据加载、mask生成、归一化 │ └── transforms.py # 随机裁剪、翻转、颜色抖动 ├── models/ │ ├── generator.py # 门控卷积U-Net │ ├── discriminator.py # PatchGAN判别器 │ └── losses.py # L1、感知损失、GAN损失的封装 ├── trainer.py # 训练循环、梯度惩罚、EMA ├── infer.py # 推理脚本,支持单张图片+自定义mask ├── config.yaml # 超参数统一管理 └── utils/ ├── metrics.py # PSNR / SSIM / FID计算 └── visualizer.py # 训练过程可视化在实际落地时,config.yaml把batch size、学习率、mask比例、损失权重全部收拢,避免频繁改代码。训练和推理分开,因为推理时需要把生成结果做一次“已知区域替换”的后处理,这一步不应该出现在训练流程里。
3.2 生成器核心:GatedConv + U-Net跳连
我在工业项目里验证过,GatedConv比普通卷积在mask边界处能减少约30%的色差伪影。下面给出核心的GatedConv模块与生成器组装逻辑,这段代码等同于DeepFillv2的轻量级平替:
import torch import torch.nn as nn import torch.nn.functional as F class GatedConv2d(nn.Module): def __init__(self, in_ch, out_ch, kernel_size=3, stride=1, padding=1): super().__init__() self.conv = nn.Conv2d(in_ch, out_ch, kernel_size, stride, padding) self.gate_conv = nn.Conv2d(in_ch, out_ch, kernel_size, stride, padding) def forward(self, x): # 通过sigmoid生成0~1的动态门控 feature = self.conv(x) gate = torch.sigmoid(self.gate_conv(x)) return feature * gate class InpaintGenerator(nn.Module): def __init__(self, in_ch=4): super().__init__() # 输入拼接4通道:3通道RGB图 + 1通道mask self.enc1 = GatedConv2d(in_ch, 64, kernel_size=5, padding=2) self.enc2 = GatedConv2d(64, 128, stride=2) self.enc3 = GatedConv2d(128, 256, stride=2) # 中间的扩张卷积层,扩大感受野而不降低分辨率 self.dilated = nn.Sequential( GatedConv2d(256, 256, kernel_size=3, padding=2, stride=1), GatedConv2d(256, 256, kernel_size=3, padding=4, stride=1), GatedConv2d(256, 256, kernel_size=3, padding=8, stride=1), ) # 解码器,逐步还原分辨率 self.dec1 = GatedConv2d(256 + 256, 128) # 与enc3跳连 self.dec2 = GatedConv2d(128 + 128, 64) # 与enc2跳连 self.dec3 = nn.Conv2d(64 + 64, 3, kernel_size=3, padding=1) self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False) def forward(self, x, mask): # mask与图像通道拼接到一起,让门控卷积感知损坏区域 x = torch.cat([x, mask], dim=1) e1 = self.enc1(x) e2 = self.enc2(e1) e3 = self.enc3(e2) d = self.dilated(e3) d = self.up(d) d = self.dec1(torch.cat([d, e3], dim=1)) d = self.up(d) d = self.dec2(torch.cat([d, e2], dim=1)) d = self.up(d) out = torch.tanh(self.dec3(torch.cat([d, e1], dim=1))) return out代码里的关键点有三处:第一,输入通道设计成RGB+Mask四通道,这意味着mask以稠密掩码矩阵形式直接参与卷积计算,而不是简单地用0填充缺失区域,避免了白色像素对卷积权重的污染;第二,扩张卷积层负责把感受野拉到缺失区域之外,让解码器能参考到更多的全局上下文;第三,tanh输出是为了匹配[-1,1]的像素空间,因为数据归一化时用的是(x/255)*2-1,如果换成别的归一化方式,输出层也必须同步调整。推理时已知区域不做像素硬替换,而是让模型自己决定边界处怎么过渡,这样阴影和光照会自然衔接。
3.3 PatchGAN判别器与谱归一化
判别器不需要像分类任务那样输出一个全局真/假标量,PatchGAN的做法是把特征图划分为 N×N 个小块,每个块独立判真假,再取平均。这个设计对修复任务很关键——它强制生成器在局部纹理层面做到逼真,而不只是整体色调接近。在实现时可以用nn.Conv2d直接构建一个全卷积网络,输出通道数降为1,空间分辨率降到输入尺寸的1/(2^k),其中k是下采样次数,我一般取k=3,那么一个 256×256 的输入分出 32×32 的Patch网格。
class PatchDiscriminator(nn.Module): def __init__(self, in_ch=3): super().__init__() self.layers = nn.Sequential( nn.Conv2d(in_ch, 64, kernel_size=4, stride=2, padding=1), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(128, 256, kernel_size=4, stride=2, padding=1), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(256, 1, kernel_size=4, stride=1, padding=1) ) def forward(self, x): return self.layers(x)谱归一化不是必须加在每一层,否则判别器收敛过慢。我的经验是只对Conv2d层做spectral_norm,并且去掉BatchNorm,换成InstanceNorm,这样在batch size小于8时不会出现统计量抖动。判别器的输入拼接方式也有讲究——把原图、mask、生成结果三者拼起来作为输入,能让判别器明确知道哪些区域是可信基准,哪些区域需要重点审查,这比只输入RGB图效果稳定很多。
3.4 数据管道与mask生成的坑
图像修复的mask生成策略直接决定模型的泛化能力,最忌训练时只用一个固定的矩形掩码,推理时却遇到不规则划痕或文字遮挡。我实际采用的是一套多类型mask混合策略:规则矩形块模拟物体遮挡,细长条形模拟划痕,随机噪声块模拟大块腐蚀。每种mask在数据加载时有固定的概率被选中,训练轮数到后期再逐步调大不规则mask的比例。
def generate_mask(batch_size, img_size=256, mask_type='mixed'): masks = [] for _ in range(batch_size): m = torch.zeros(img_size, img_size) p = np.random.rand() if p < 0.4 or mask_type == 'rectangle': h, w = np.random.randint(32, 128, size=2) x0, y0 = np.random.randint(0, img_size - w), np.random.randint(0, img_size - h) m[y0:y0+h, x0:x0+w] = 1 elif p < 0.7 or mask_type == 'stroke': # 用少量关键点插值构成细长条 points = [np.random.randint(0, img_size, size=2) for _ in range(np.random.randint(3, 6))] for i in range(len(points) - 1): cv2.line(np.asarray(m), tuple(points[i]), tuple(points[i+1]), 1, thickness=np.random.randint(2, 8)) else: # 随机块状mask,模拟腐蚀 m = torch.rand(img_size, img_size) > 0.8 masks.append(m) return torch.stack(masks).unsqueeze(1)这里有个性能陷阱:在GPU训练时,如果mask_generator写在Dataset.__getitem__里,每个step都会产生同步阻塞,CPU和GPU无法并行。更好的做法是两个独立的DataLoader worker线程预先异步生成mask,训练主循环只负责取tensor。mask的尺度也值得注意,送入网络的mask分辨率必须与输入图一致,如果先resize再生成mask,会造成边界处半透明像素,模型学不到锐利的修复边缘。
4. 训练过程中的参数配置与三个高频踩坑点
4.1 训练稳定性的核心超参表
GAN训练本来就敏感,图像修复模型因为多了L1和感知损失,超参取错直接表现为“颜色灰蒙蒙”或“训练震荡不收敛”。下面给出我在多次实验中沉淀出来的参考值,batch size、λ权重和优化器参数三者之间有联动关系,不建议单独改其中一项。
| 超参数 | 推荐值 | 调整依据 |
|---|---|---|
| 输入分辨率 | 256×256 | 低于128修复纹理细节会崩溃,高于384显存压力太大 |
| Batch Size | 8 | 受显存约束,低于4时建议关闭BatchNorm |
| G学习率 | 1e-4 | 与D保持1:1,注意不是D能赢就完事 |
| D学习率 | 1e-4 | 使用Adam的beta1=0.5, beta2=0.999 |
| λ_adv / λ_l1 / λ_per | 1 / 20 / 10 | 先固定L1=20,后续再看结果微调Perceptual |
| Mask比例 | 20%~40% | 超过40%模型会倾向忽略已知域 |
| 训练轮数 | 50~80 epoch | 感知损失在30 epoch后开始明显生效 |
| EMA衰减 | 0.999 | 推理时使用EMA参数能有效去噪 |
优化器上的两个细节:Adam的beta1必须从默认的0.9降到0.5,否则生成器方差大,颜色不稳定;weight_decay不需要,GAN里L2正则通常会压低生成纹理的高频分量,让结果变糊。学习率要不要做余弦退火?我建议前30个epoch保持不变,后20个线性衰减到1e-5,这个策略在实际复现时比StepLR稳定得多。
4.2 踩坑一:对抗损失与L1损失互相碾压怎么办
一个典型症状是训练到第10个epoch时,生成结果里缺失区域变成均匀的“补丁色块”,察觉不到任何纹理细节。这时去打印每个损失的数值,你会发现L1已经降到0.01以下,但D_loss还在1.2左右震荡。原因是L1的梯度占据了绝对主导,判别器根本带不动生成器。解决方案不是把λ_adv调大,而是换一个思路:把对抗损失的输入从“只看生成区域”改成“只看整幅图的生成结果”,同时把L1损失里的权重从固定值改成随epoch衰减的曲线,让模型前期学会重建结构,后期才让判别器逐步接管纹理细节。具体实现时可以这样写调度逻辑:
def adaptive_l1_weight(epoch, max_epoch=50): # 前10轮L1权重拉满,之后线性衰减到原本的1/4 if epoch < 10: return 20.0 return 20.0 * (1 - 0.75 * (epoch - 10) / (max_epoch - 10))这个写法的出发点是保底策略:前1/5的训练周期让生成器快速学会大结构,后面再慢慢顺应判别器的高频偏好。如果你观察到生成结果纹理清晰但颜色不对,则说明λ_per太小,与λ_l1方向矛盾,优先调大λ_per而不是λ_adv。
4.3 踩坑二:mask边界出现明显接缝伪影怎么办
接缝伪影的来源不单是模型,有时是你推理时“已知区域硬替换”的边界没做羽化处理。训练得再好,推理时在mask边界直接做result = mask * output + (1-mask) * original也会形成一条锐利分割线。我的处理方式是用OpenCV对mask做一个膨胀和模糊处理,把二值mask转换为soft mask,让边界区域允许一定程度的过渡。
import cv2 import numpy as np def soft_mask(mask, kernel_size=15): """将二值mask羽化为灰度mask,边界区域平滑过渡,减少接缝伪影""" kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) dilated = cv2.dilate(mask, kernel, iterations=3) blurred = cv2.GaussianBlur(dilated, (kernel_size, kernel_size), 0) return blurred.astype(np.float32) / 255.0 # 推理时拼接 out = (soft_mask_s * generated) + ((1 - soft_mask_s) * original)soft_mask的kernel_size理论上不能太大,过大会把已知区域的有效纹理也“羽化”掉,让颜色渗入缺失区域。一般kernel取mask宽度比例的1/10到1/15,如果是细长划痕就不需要做soft mask,直接用硬替换即可。更进阶的做法是在模型输出的已知区域部分也重新过一遍生成器,用cycle一致性约束来天然消除接缝,但这样训练复杂度会明显提高,初次复现不必上。
4.4 踩坑三:memory增长但loss不降,GAN训练特有的假收敛
Adam自带的学习率在GAN训练里容易让判别器进入局部最优,具体表现为D_loss下降到接近0,但生成器loss稳定在某个常量附近。这时不能继续等,需要立刻恢复训练——nn.utils.clip_grad_norm_只对生成器生效,判别器梯度不裁剪,并把判别器学习率临时降到生成器的1/2。另一个更隐蔽的原因是输入数据的分布没对齐:如果原始图片是0~255范围,而mask是0~1浮点,两个输入拼接到一起会让第一层卷积的权重初始统计产生偏差。检查以下三处:输入图像是否归一化到 [-1, 1]、mask是否与图像同一个LUA坐标系且不为布尔值、生成器最后是否用了与数据范围匹配的输出激活。这里任何一个错位都让你的梯度方向不是朝着真实分布走。
5. 从源码到可交付:推理脚本的工程化与模型效果验证
5.1 用infer.py对任意图像做单图修复的命令行操作
训练完的模型最终是要丢给别的工程调用或给测试人员跑demo的。如果还在Jupyter Notebook里传tensor,那一定交付不了。我一般把推理封装成命令行工具,支持--image、--mask、--checkpoint三个路径参数,输出修复图和mask叠加的可视化图。
python infer.py --image ./samples/old_photo.jpg --mask ./samples/mask.png --checkpoint ./checkpoints/best_model.pth --output ./results/这个命令背后做的事情按顺序对应五步:加载图片并缩放至模型输入尺寸(保持宽高比,不足部分用边缘填充)→ 同步缩放mask到与图像一致尺寸并二值化 → 模型前向传播得到生成结果 → 用上节提到的soft_mask做边界融合 → 把结果变换回0~255整数类型并保存。值得注意的一点是--checkpoint要选择EMA权重而不是最近一次保存的权重,通常EMA权重比原始权重在PSNR上高出0.5dB以上,且纹理更干净。前向推理时不要开torch.no_grad()以外的优化——图像修复模型不是大模型,没必要做半精度推理,因为精度损失在边界区域特别容易显型。
5.2 一套简单的客观指标评估脚本:PSNR + SSIM + L1
算法组的同事习惯用PSNR衡量重建精度,但图像修复的PSNR天然会偏低,因为缺失区域没有“标准答案”——感知上的正确不代表像素上的重合。因此评估脚本里PSNR只作为参考,具体看SSIM(结构相似度)以及人工目检纹理连续性。评估时按mask覆盖率分组统计,mask覆盖10%以下与40%以上要有不同的及格线。
import cv2 import numpy as np from skimage.metrics import structural_similarity as ssim def calc_metrics(gt, pred, mask): """按mask区域分别计算PSNR与SSIM,返回它们在损坏区域的值""" gt = (gt + 1) / 2 # 反归一化到0~1 pred = (pred + 1) / 2 mask = mask.squeeze() # 只取mask区域的像素计算PSNR masked_gt = gt * mask[..., None] masked_pred = pred * mask[..., None] mse = np.mean((masked_gt - masked_pred) ** 2) psnr_val = 10 * np.log10(1.0 / (mse + 1e-10)) # SSIM要在整图范围内计算,窗口大小取7 ssim_val = ssim(gt, pred, channel_axis=-1, data_range=1.0) return psnr_val, ssim_val目检时建议把原图、mask、生成结果、已知区域替换后的最终图拼成2×2网格保存到tensorboard里,每200步刷一次。这样你在训练第5个epoch时就能发现mask边界是否出现灰圈或黑边,不需要等整个训练跑完。另一个值得参考的无参考指标是FID,但FID需要至少几千张图的分布统计,单图修复场景下不稳定,不建议作为常规指标。
5.3 一个能直接落地的技巧:用边缘先验引导修复
图像修复模型在纯视觉大块缺失下,经常出现“结构漂移”——比如人脸的左眼和右眼不对称,或者建筑的窗户数量不对。源头上是纹理信息与结构信息在隐空间里耦合了,模型不知道几何约束。一个轻量级的补救办法是在输入侧多路一个边缘图通道,在训练时用Canny提取原图的边缘作为辅助监督信号,推理时对缺失区域填一个“平均边缘模板”。这个做法不改变模型结构,只增加一个输入通道,但对多数结构类场景相对有效。实现时可以用预训练的HED网络提取边缘特征,把边缘图与RGB图分别输入编码器,在隐空间相加。代价是训练数据需要额外生成一遍边缘图,前置耗时不超过总训练时间的5%,但能有效缓解大块mask下结构错乱的现象。如果你的核心业务是修复人脸或建筑照片,建议优先尝试这个扩展。
本文还有配套的精品资源,点击获取