
简介这份资源是一套基于Python深度学习的老照片修复实战项目面向具备一定编程基础、希望入门图像恢复与计算机视觉的开发者与学习者用于解决破损、划痕或严重退化老照片的智能修复问题。压缩包共81个文件约50.71MB以54个py源码文件为核心辅以17张png效果图、4份pdf说明文档、2个txt依赖文件以及gif演示、jpg示例、md说明和mp4讲解视频覆盖从数据预处理、模型训练到推理部署的完整流程。项目围绕卷积神经网络展开涉及损失函数设计、优化器选择、权重保存与加载并进一步引入生成对抗网络提升修复真实感同时包含人脸检测与对齐等辅助模块。目前已有350人学习下载适合作为深度学习图像修复方向的练手案例帮助读者理解CNN与GAN在像素级预测任务中的落地方式并掌握TensorFlow、Keras或PyTorch框架下的工程组织思路。1. 老照片修复项目到底在修什么从一张划痕脸说起家里翻出一张 1987 年的黑白合影人脸只有指甲盖大小布满霉斑和折痕扫描成 300dpi 之后噪点比五官还清楚。这就是老照片修复项目要处理的对象不是简单调个色、磨个皮而是让模型从严重退化的像素里把结构重新猜回来。深度学习做这件事的核心思路是用大量退化图—清晰图配对样本训练一个映射网络让它学会去划痕、去噪、超分、补全人脸区域。Python 在这个方向几乎是默认语言因为 PyTorch、OpenCV、BasicSR 这套工具链最顺手社区里能直接跑的老照片修复项目也大多以 Python 脚本形式分发。这篇笔记面向两类人一类是刚配好深度学习环境、想找一个能跑通全流程的实战项目练手另一类是想把修复能力接进自己业务影楼、档案数字化、家族相册整理的工程师。下面按数据怎么造、模型怎么选、训练怎么调、坑在哪的顺序讲透你照着能复现也能判断值不值得投入。2. 数据与退化建模老照片修复项目的成败八成在这里2.1 为什么真实老照片不能直接拿来训练很多人第一反应是去网上搜一批老照片当训练集结果训练 loss 死活降不下去。原因很直接监督学习需要成对的输入—目标而真实老照片只有退化后的那一张没有对应的未退化原图。你没法告诉模型这张脸本来长什么样。所以老照片修复项目的主流做法是合成退化拿一批高清干净的人脸或风景图当 ground truth用程序人为加上划痕、噪点、模糊、压缩块、褪色生成配对的退化图。这样模型学的是从退化分布反推干净分布。常见做法是参考 ESRGAN、GFPGAN、Bringing Old Photos Back to Life 这几条技术路线的退化管线。我一般会按下面几个维度组合退化参数而不是只用单一高斯噪声退化类型典型参数范围模拟的真实问题高斯噪声sigma 5~25胶片颗粒、扫描噪点高斯模糊kernel 3~15失焦、镜头老化运动模糊长度 5~20角度随机手抖、快门慢JPEG 压缩quality 30~70多次转存、低质扫描划痕/折痕随机线条宽度 1~5px物理损伤褪色/泛黄通道偏移 对比度下降化学老化参数不是越狠越好。sigma 拉到 50、模糊核拉到 31模型确实能学到强去噪但输出会过度平滑人脸变成塑料脸这是老照片修复里最典型的翻车。我的经验是退化强度分档训练时随机采样让模型见过从轻到重的连续分布而不是只见过极端情况。2.2 用 Python 写一个可复现的退化脚本下面这段是退化管线的核心基于 OpenCV 和 NumPy可以直接嵌进 Dataset 的__getitem__里做在线退化也可以离线批量生成。import cv2 import numpy as np import random def add_gaussian_noise(img, sigma): noise np.random.normal(0, sigma, img.shape).astype(np.float32) return np.clip(img.astype(np.float32) noise, 0, 255).astype(np.uint8) def add_motion_blur(img, length, angle): # 构造运动模糊核长度控制拖影距离angle 控制方向 kernel np.zeros((length, length), dtypenp.float32) kernel[length // 2, :] 1.0 M cv2.getRotationMatrix2D((length / 2, length / 2), angle, 1) kernel cv2.warpAffine(kernel, M, (length, length)) kernel / kernel.sum() return cv2.filter2D(img, -1, kernel) def add_scratches(img, num3): h, w img.shape[:2] out img.copy() for _ in range(num): x1, y1 random.randint(0, w), random.randint(0, h) x2, y2 random.randint(0, w), random.randint(0, h) color random.choice([255, 0, 200]) # 白痕、黑痕、泛黄痕 cv2.line(out, (x1, y1), (x2, y2), (color, color, color), random.randint(1, 3)) return out def degrade(img): # 顺序有讲究先几何/划痕再模糊最后噪声和压缩 img add_scratches(img, numrandom.randint(1, 4)) if random.random() 0.7: img add_motion_blur(img, random.choice([5, 9, 13]), random.uniform(0, 180)) if random.random() 0.5: img cv2.GaussianBlur(img, (random.choice([3, 5, 7]),) * 2, 0) img add_gaussian_noise(img, random.uniform(5, 20)) # JPEG 压缩模拟多次转存 quality random.randint(35, 75) _, enc cv2.imencode(.jpg, img, [cv2.IMWRITE_JPEG_QUALITY, quality]) return cv2.imdecode(enc, cv2.IMREAD_COLOR)逻辑说明degrade里的顺序不是随便排的。真实老照片的损伤是叠加的——先有物理划痕再经历失焦最后是扫描噪声和压缩。如果先加噪声再模糊噪声会被抹平模型学不到去噪这一项。add_motion_blur用旋转单位核的方式生成任意方向的拖影比固定水平核更接近真实手抖。add_scratches里颜色随机是为了让模型不把白线当成唯一划痕特征。参数说明sigma建议 5~20超过 25 输出会糊length取奇数5~15 之间太大整张图会拖没JPEG quality 不要低于 30否则块效应强到模型只能学到去块而忽略结构。在线退化时每个 epoch 重新随机等于无限数据增强离线生成则建议至少 5 万对起步人脸类任务少于这个量级容易过拟合。提示如果你手上只有少量真实老照片可以先用它们做测试集训练集全部用合成退化。测试集必须保留真实退化否则指标好看但上线就崩。3. 模型选型GAN、Transformer 还是扩散模型3.1 三条技术路线的取舍老照片修复项目在模型层面大致分三代。第一代是 CNN 自编码器如 SRCNN、UNet结构简单、训练快但输出偏平滑细节靠猜。第二代是 GAN 路线代表是 ESRGAN、GFPGAN用对抗损失逼出纹理人脸细节明显更锐代价是训练不稳定、容易出现伪影。第三代是扩散模型和 Transformer如 SwinIR、Restormer质量上限最高但推理慢、显存吃紧一张 512×512 的图在消费级显卡上可能要几秒到几十秒。我的选型习惯是看场景如果是批量处理家族相册、追求速度和稳定UNet 感知损失就够如果要做人脸特写、要求五官清晰上 GFPGAN 这类带人脸先验的 GAN如果是做产品级修复、能接受推理成本扩散模型值得试。别一上来就堆最重的模型老照片修复的瓶颈往往在数据和退化建模不在网络深度。3.2 一个最小可训练的 UNet 修复网络下面是一个能直接跑起来的 UNet 骨架输入退化图输出修复图用 L1 感知损失训练。它不追求 SOTA但能让你在几小时内看到划痕变淡的效果适合验证数据管线是否正确。import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.net nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.net(x) class UNet(nn.Module): def __init__(self, base64): super().__init__() # 编码器逐层下采样扩大感受野抓全局结构 self.d1 DoubleConv(3, base) self.d2 DoubleConv(base, base * 2) self.d3 DoubleConv(base * 2, base * 4) self.pool nn.MaxPool2d(2) # 解码器上采样 跳跃连接恢复空间细节 self.u2 nn.ConvTranspose2d(base * 4, base * 2, 2, stride2) self.c2 DoubleConv(base * 4, base * 2) self.u1 nn.ConvTranspose2d(base * 2, base, 2, stride2) self.c1 DoubleConv(base * 2, base) self.out nn.Conv2d(base, 3, 1) def forward(self, x): d1 self.d1(x) d2 self.d2(self.pool(d1)) d3 self.d3(self.pool(d2)) u2 self.c2(torch.cat([self.u2(d3), d2], dim1)) u1 self.c1(torch.cat([self.u1(u2), d1], dim1)) return torch.sigmoid(self.out(u1))逻辑说明编码器每下采样一次感受野翻倍到第三层时一个像素能看到原图较大范围这对补全划痕覆盖的区域很关键——划痕下面的结构得靠周围上下文推断。跳跃连接把浅层的高频细节直接送到解码器避免上采样时细节全丢。最后一层用sigmoid把输出压到 0~1配合归一化后的输入。参数说明base64是显存和容量的平衡点24G 显存可以开到 96输入尺寸建议裁到 256×256 训练推理时用全图或分块。损失函数我一般用L1 * 1.0 VGG感知损失 * 0.1纯 L1 会糊纯感知会引入纹理噪声。优化器 Adam学习率 1e-4训练 20~50 个 epoch 就能看到明显效果。注意UNet 对划痕这类局部遮挡有效但对大面积缺失比如半张脸没了无能为力那需要带生成先验的模型。别指望一个 UNet 解决所有退化。4. 训练与推理参数怎么设、显存怎么省4.1 训练循环里必须盯住的几个量训练老照片修复模型光看 loss 会骗你。L1 loss 降到很低时输出可能已经糊成一片因为模型学会了平均化来降低像素误差。我一般同时盯三个量L1像素保真、感知损失纹理真实度、以及每隔几个 epoch 存一张验证图肉眼对比。验证图比任何指标都诚实。import torch from torch.utils.data import DataLoader def train_one_epoch(model, loader, optimizer, criterion_l1, criterion_perc, device): model.train() total 0 for deg, clean in loader: deg, clean deg.to(device), clean.to(device) pred model(deg) loss_l1 criterion_l1(pred, clean) loss_perc criterion_perc(pred, clean) loss loss_l1 0.1 * loss_perc # 感知损失权重不能大 optimizer.zero_grad() loss.backward() # 梯度裁剪防止 GAN 或深网络梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total loss.item() return total / len(loader)逻辑说明感知损失权重设 0.1 是经验值设到 1.0 会让输出出现高频噪点因为 VGG 特征对纹理敏感模型会为了骗过VGG 而生成假纹理。梯度裁剪max_norm1.0在训练初期尤其重要老照片退化强、输入分布乱梯度容易炸。参数说明batch size 在 256×256 输入下8G 显存用 824G 用 32。学习率用余弦退火从 1e-4 降到 1e-6。如果 loss 在前几个 epoch 就震荡先把学习率砍到 1e-5 试。4.2 推理阶段的分块与显存控制全尺寸老照片比如 4000×3000 扫描件直接喂进网络会爆显存。常见做法是分块推理再拼接但块与块之间会有接缝。解决办法是重叠分块 加权融合。def tile_inference(model, img, tile512, overlap64, devicecuda): h, w img.shape[:2] output np.zeros_like(img, dtypenp.float32) weight np.zeros((h, w, 1), dtypenp.float32) step tile - overlap for y in range(0, h, step): for x in range(0, w, step): y2, x2 min(y tile, h), min(x tile, w) patch img[y:y2, x:x2] # 边缘块补齐到 tile 尺寸避免尺寸不一致 ph, pw patch.shape[:2] pad np.zeros((tile, tile, 3), dtypenp.uint8) pad[:ph, :pw] patch t torch.from_numpy(pad).permute(2, 0, 1).float().unsqueeze(0).to(device) / 255.0 with torch.no_grad(): out model(t).squeeze(0).permute(1, 2, 0).cpu().numpy() # 高斯权重让接缝处平滑过渡 wy np.hanning(ph * 2)[:ph][:, None] wx np.hanning(pw * 2)[:pw][None, :] wmap wy * wx output[y:y2, x:x2] out[:ph, :pw] * wmap[..., None] weight[y:y2, x:x2] wmap[..., None] return (output / np.maximum(weight, 1e-6) * 255).astype(np.uint8)逻辑说明overlap64保证相邻块有足够重叠区域做融合。汉宁窗权重让块中心权重高、边缘权重低拼接时不会出现硬边。pad是为了处理图像右下角不足一整块的情况补齐后再裁掉。参数说明tile取 512 是显存和速度的平衡显存小可以降到 256但块越小接缝越多overlap 要相应加大到 64~96。推理时记得model.eval()和torch.no_grad()否则显存翻倍。5. 避坑与排查老照片修复项目里最容易翻车的五件事5.1 输出全是塑料脸细节被抹平现象修复后的人脸光滑得像磨皮过度毛孔、皱纹全没了看着假。原因L1 损失主导 退化强度过大模型学会了输出平均脸来最小化像素误差。解决降低退化强度上限加入感知损失和对抗损失或者换用带人脸先验的模型如 GFPGAN。我一般会把 L1 权重降到 0.5感知损失提到 0.2肉眼立刻不一样。5.2 训练 loss 正常但验证图全是噪点现象训练集 loss 一路降验证集输出雪花点。原因训练集和验证集的退化分布不一致或者验证集用了真实老照片而训练集全是合成退化域 gap 太大。解决验证集也混入一部分合成退化图做对照同时用真实老照片做定性评估。如果真实图效果差说明退化建模不够真实回去补划痕和压缩的多样性。5.3 显存溢出OOM在训练中途才出现现象前几个 epoch 正常突然 OOM。原因DataLoader 的num_workers开太大或者某些 batch 里混进了超大尺寸图片没裁齐。解决Dataset 里强制resize或random_crop到固定尺寸num_workers设成 CPU 核数的一半加pin_memoryTrue。另外检查是否有图片是 4 通道 PNG转成 3 通道再进网络。5.4 划痕去掉了但留下模糊的鬼影现象划痕位置变成一片模糊像被橡皮擦擦过。原因模型只学会了填平没学会重建结构通常是感受野不够或跳跃连接太弱。解决加深编码器、增大卷积核或者在划痕区域加一个 mask 引导损失让模型重点学被遮挡区域的重建。Bringing Old Photos Back to Life 里就是用 mask 做局部监督的。5.5 推理速度慢到无法批量处理现象一张 2000×2000 的图要跑十几秒。原因分块太小导致块数爆炸或者模型没转半精度。解决tile提到 512~768开启torch.cuda.amp.autocast()半精度推理速度能快 1.5~2 倍。如果还慢考虑把模型导出成 ONNX 或 TensorRT但注意 GAN 类模型导出后可能有算子不支持。6. 进阶技巧用 mask 引导和参考脸提升修复上限走到这一步基础管线已经能跑通但你会发现严重退化的区域大面积划痕、缺失五官还是修不好。这时候有两个进阶方向值得投入。第一个是mask 引导训练在生成退化图时同步记录划痕和缺失区域的二值 mask训练时把 mask 作为额外输入通道损失函数在 mask 区域加权。这样模型知道哪里需要重点重建而不是全图一视同仁。实现上只需在 Dataset 里多返回一个 mask网络第一层改成 4 通道输入损失写成loss (l1 * (1 mask * 3)).mean()mask 区域权重翻三倍。第二个是参考脸先验也就是人脸修复里常说的 face restoration with reference。思路是先用一个人脸检测和对齐模型如 RetinaFace把退化人脸对齐到标准姿态再从一张高质量人脸库或同一人的其他照片里取参考特征通过注意力机制注入修复网络。GFPGAN 和 CodeFormer 都用了类似的人脸先验区别在于先验是隐式的从训练集学到的平均脸还是显式的外部参考图。显式参考效果更好但需要额外的人脸检索和对齐步骤工程复杂度高不少。验证修复质量时别只信 PSNR 和 SSIM。这两个指标在生成式修复里经常和肉眼感受相反——PSNR 高的图往往更糊。我习惯用三个层次评估像素级看 PSNR/SSIM感知级看 LPIPS最终以人眼盲测为准。具体做法是随机抽 20 张测试图把原图、退化图、修复图并排让不参与项目的同事选哪张最像真实照片这个主观分比任何自动指标都可靠。最后说个我踩过的坑早期我为了追求指标把退化强度调得特别大模型在合成测试集上 PSNR 很高结果拿真实老照片一跑输出全是伪影。后来才明白合成退化和真实退化之间永远有 gap与其在合成数据上刷分不如花时间收集几十张真实老照片做定性验证哪怕它们没有 ground truth。修复这件事肉眼说好才是真的好。希望帮到你。本文还有配套的精品资源点击获取