简介:一份面向老旧照片修复与修补的深度学习完整源码项目,适合具备Python与深度学习基础的研究者、开发者和爱好者,可用于破损照片的自动修复、去除划痕、降噪与超分辨率重建等图像处理场景。整套资源共九十五个文件,其中六十三个Python脚本构成核心逻辑,覆盖图像预处理、特征提取、模型训练与评估、界面交互等环节;另包含十九张示例图像、若干配置文件、使用说明文档、Dockerfile容器化部署支持、预训练权重下载脚本,压缩包整体约四十兆。项目内置全局修复、人脸增强、人脸检测等子模块,基于卷积神经网络实现从全局到局部的完整修复流程,并提供训练、预测与可视化接口。已有四百五十六人学习;对研究图像修复算法或希望直接复现的开发者而言,这份源码具备清晰的模块划分、自动化脚本和模型接口,便于快速上手、二次开发与技术验证,是一套难得的工程化参考资料。
1. 老旧照片修复的“设计源码”,核心是把补全和增强做成一条可落地的流水线
很多人拿到一张裂痕密布、局部脱色、边缘磨没了的父母结婚照,第一反应是找美图工具拉一拉对比度、调一调色,结果只会让裂痕更显眼,人脸更糊。老旧照片修复不是“调滤镜”,而是把退化过程建模出来,然后反向估计原图:划痕、脏点、霉斑属于“区域信息丢失”,需要用图像修补(inpainting)来补;磨皮一般模糊、噪点颗粒属于“全局退化”,需要用超分辨率模型增强;颜色褪化则是“非线性色偏”,要在后期交出一个校正分支。这个技术方向在深度学习领域已经有一批成熟权重和能直接改的项目骨架,真正做成一键修复工具,难点反而不在模型选型,而在如何把预训练模型、掩码生成、前后处理串成一个稳定可跑的 pipeline。
这套东西适合三类人:一是做老照片数字化保存的开发者,二是想给个人相册做批量修复的摄影师,三是刚入门深度学习、想用真实数据集做一个完整项目的同学。下面这份实战笔记,按我实际跑通的做法写清楚:原理上该选哪类模型,操作上每一步输什么命令、调什么参数,翻车时看哪些日志,以及最后怎么做成一个批处理工具。
2. 老旧照片修复不是单模型任务:四层退化分解与模型选型逻辑
没有哪个单一深度模型能直接吞掉老照片输出一张完美新图。老照片的退化是叠加的:先有扫描器件带来的锯齿和噪点,再有长期存放产生的物理划痕和霉菌,还有化学反应导致的偏色与褪色,最后是当年相机本身分辨率不足带来的模糊。这些退化特征各不相同,把问题拆开,逐个让擅长对应任务的模型处理,比端到端训练一个统一模型稳定得多。
2.1 老旧照片为什么难修:划痕语义与图像退化的三层原因
第一层问题是“区域缺失”。划痕、霉斑、折痕覆盖了原始像素,这部分区域已经不存在有效信息,网络修不出细节,只能根据周围纹理做语义猜测。传统图像算法用中值滤波或邻域插值,小划痕还好,一旦划痕横穿眼睛、眉毛这类高频结构,结果就是一团糊。深度学习模型在这里的优势是能利用语义先验:知道这是眼睛,就会在补全时按眼睛的结构去生成。
第二层问题是“全局模糊与噪点”。银盐照片经过几十年存放,卤化银颗粒退化会让照片产生噪点;扫描仪的 CCD 采样又叠加了摩尔纹和模糊。这一层属于盲复原范畴,模型要估计模糊核并反向去卷积。实际工程中很少单独做去卷积,而是交给超分辨率模型当作低分辨率退化一并处理。
第三层问题是“非线性色偏”。深棕、发黄、局部退色,这些是化学褪色和存放环境共同导致的。线性白平衡算法会把高光拉爆,而深度学习模型可以直接学习“褪色照片 → 正常色调”的映射。
所以我一般把老旧照片修复拆成两条主路径:物体区域用 inpainting 模型补,全局用超分模型增强,最后校色。两条主路径之外,还要先做一次预处理:把照片从实体扫描成数字图像,扫描分辨率建议 400–600 DPI,低于 300 DPI 的话后续超分倍数要增加,质量不稳定。
2.2 模型选型组合:GFPGAN、Real-ESRGAN、LaMa 各自解决哪一类缺陷
选模型不要追求“一个模型全包”,要按任务分工。常用的开源权重和对应强项如下:
| 缺陷类型 | 推荐模型 | 原理要点 | 适用场景 |
|---|---|---|---|
| 大块划痕与缺失区 | LaMa / DeepFillv2 | 卷积带大感受野的 inpainting,直接生成缺失像素 | 划痕面积较大、破坏人脸结构的照片 |
| 全局模糊、低分辨率边角 | Real-ESRGAN / GFPGAN | 盲超分,附加 GAN 判别器优化感知质量 | 老照片整体发糊、边缘发虚 |
| 人脸五官细节恢复 | GFPGAN | 基于人脸先验的生成式修复,内置人脸解析分支 | 人像老照片,五官磨没的局部 |
| 全局色偏与褪色 | 简单 CNN 校色 / 传统白平衡 | 训练端到端映射,或直接用 retinex 类算法 | 发黄、发棕、发灰的旧照片 |
需要说明一下 GFPGAN 和 Real-ESRGAN 的关系。GFPGAN 的全称是 Generative Facial Prior GAN,它把人脸先验注入到超分网络里,所以对人脸特别有效;但背景区域,比如墙的纹理、树叶,GFPGAN 的表现不如 Real-ESRGAN 稳定,有时会生成奇怪的重复纹理,所以在生产流水线里我会把两者结合:人脸区域用 GFPGAN 的结果,背景区域用 Real-ESRGAN 的结果,做 alpha 融合。
LaMa(Large Mask Inpainting)则不是生成对抗网络路线,它靠的是感受野特别大的空洞卷积。它不需要像 Partial Conv 那类模型那样维护掩码更新状态,直接输一张图加一张掩码图就出结果。它对纹理补全的真实感很强,且代码改动很小,适合嵌入现有工程。
2.3 一条最小修复流水线:预处理 → 超分 → 删补 → 校色
把模型串起来,我常用这条流水线:
- 预处理:降噪滤波(可选),转 RGB,缩放到模型输入尺寸。
- 划痕定位:先用低阈值边缘检测拿到候选划痕,再人工或半自动标注掩码;如果懒得手动标,也可用带监督的划痕检测模型。
- 修补:把原图与掩码输入 LaMa,只重建掩码区域内的像素。
- 超分增强:将修补后的图输入 GFPGAN 或 Real-ESRGAN 做 2x 或 4x 放大。
- 校色与后处理:做一次白平衡,调整饱和度与对比度,输出成图。
这里要强调掩码质量是修补效果的天花板。掩码做小了,划痕残留在边缘,肉眼可见;掩码做大了,会把人脸正常纹理框进补全区,生成一张光滑的假脸。所以掩码膨胀与腐蚀操作在流水线里不是可选项,是必选项。具体数值后面在避坑里细说。
3. 用 PyTorch 跑通老照片修复最小实现:依赖安装与三段串联代码
工具链选型:Python 3.10,PyTorch 2.x,Ubuntu 20.04 或 Windows WSL2 均可。GPU 推荐显存 ≥ 6GB,能跑 FP16;没有 GPU 也可以跑 CPU,但图像尺寸建议控制在 512×512 以内,不然推理一次要等几分钟。下面按我实际使用的步骤来写。
3.1 环境准备:PyTorch 与 GPU 检查,显存低于 6G 的注意事项
先确认 CUDA 是否可用。运行下面脚本来检查环境,同时判断显存容量:
import torch print("PyTorch 版本:", torch.__version__) print("CUDA 是否可用:", torch.cuda.is_available()) if torch.cuda.is_available(): print("GPU 名称:", torch.cuda.get_device_name(0)) print("显存总量: {:.1f} GB".format(torch.cuda.get_device_properties(0).total_memory / 1024**3))逻辑说明:PyTorch 的cuda.is_available()返回 False 时,不用急着重装环境,先执行nvidia-smi看驱动是否正常。驱动正常而 PyTorch 检测不到 CUDA,通常是装了 CPU 版 PyTorch,用pip list | grep torch确认。最常用的安装命令是我用的这套:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118参数说明:cu118对应 CUDA 11.8,兼容性最广,Turing、Ampere、Ada 架构显卡都能用。显存低于 6G 的显卡(比如 GTX 1660 6G 或笔记本的 RTX 3050 Laptop 4G),要在加载模型时把torch_dtype设为torch.float16,并把输入图先缩到 512 以内再进模型,不然中间特征图会把显存顶爆。
3.2 划痕区定位与修补:LaMa 模型从加载到输出
LaMa 的权重是.pth格式,配合官方仓库的predict.py使用。但要把 LaMa 接进自己的代码,可以直接调用它的LamaImageInpainting接口。核心代码如下:
import torch from PIL import Image import numpy as np # 加载预训练的 LaMa 模型(以官方 lama-v1 结构为例) from models.lama import LamaImageInpainting model = LamaImageInpainting( model_path="weights/lama_big_512.pth", device="cuda" if torch.cuda.is_available() else "cpu" ) # 读取原始照片,并同步读取手工标注的划痕掩码(1 表示需要修补区域) image = Image.open("old_photo.jpg").convert("RGB") mask = Image.open("old_photo_mask.png").convert("L") # 统一输入尺寸:LaMa 在 512x512 上训练,直接缩放即可 image = image.resize((512, 512), Image.LANCZOS) mask = mask.resize((512, 512), Image.NEAREST) # 转为张量:BCHW 格式,RGB 值归一到 [0, 1] image_t = torch.from_numpy(np.array(image)).permute(2, 0, 1).unsqueeze(0).float() / 255.0 mask_t = torch.from_numpy(np.array(mask)).unsqueeze(0).unsqueeze(0).float() / 255.0 # 推理:只重建掩码区域内的内容 with torch.no_grad(): result = model(image_t, mask_t) # 保存结果 result_img = result.squeeze(0).permute(1, 2, 0).numpy() * 255.0 Image.fromarray(result_img.astype(np.uint8)).save("old_photo_inpainted.png")逻辑说明:LaMa 的输入是双分支结构,image_t是整张图,mask_t是布尔掩码;模型内部把掩码区域作为条件,用周围有效像素推理缺失内容。掩码必须用NEAREST插值缩放,不能用双线性,否则掩码边缘会变成灰色半透明区域,模型会把它当作纹理去补全,造成灰边伪影。
参数说明:lama_big_512.pth是官方在大型数据集上训练的权重,对不规则划痕泛化较好。如果照片里的划痕是小而密的裂纹,可以换用lama_finetune_places系列的权重,它在 Places 数据集上做过微调,对室内背景照片的纹理补全更真实。
3.3 融合放大与降噪:用 GFPGAN 做 2x 超分恢复
LaMa 补完划痕后,照片还是低分辨率状态。下一步用 GFPGAN 做人脸增强与超分。GFPGAN 的推理接口封装得比较友好,直接调用即可:
from gfpgan import GFPGANer # 初始化模型: # upscale=2 表示输出尺寸为输入的两倍; # arch='clean' 表示使用干净的 ResNet 骨架,显存占用更低; # channel_multiplier=2 是 GFPGANv1.4 的默认宽度配置 restorer = GFPGANer( model_path="weights/GFPGANv1.4.pth", upscale=2, arch="clean", channel_multiplier=2, bg_upsampler=None ) # 推理输入是上一步修补完成的图 input_img = cv2.imread("old_photo_inpainted.png", cv2.IMREAD_COLOR) output_img, _ = restorer.enhance( input_img, has_aligned=False, only_center_face=False, paste_back=True ) cv2.imwrite("old_photo_enhanced.png", output_img)逻辑说明:enhance()内部会自动检测人脸关键点,裁剪出人脸区域做增强,增强后再贴回原图。has_aligned=False表示输入图没有做过人脸对齐,需要模型自动检测。paste_back=True把增强后的人脸贴回背景,避免背景被 GAN 过度生成。这里bg_upsampler不传值,因为 GFPGAN 自带的背景增强较弱,背景细节保留不如 Real-ESRGAN,后续若要更精细的背景纹理,可单独用 Real-ESRGAN 再做一次。
参数说明:upscale=2时,512×512 的输入图输出 1024×1024。如果老照片扫描分辨率已经在 600 DPI 以上,不建议再放大,直接在原分辨率上做人脸增强就够了。放大倍数越高,GAN 的幻觉内容越重,人脸皮肤会变得像塑料。这个参数不是越大越好。
3.4 把三步串联成完整 pipeline:输入目录输出目录
单张跑通后,要把它变成可复用的脚本。我习惯把代码组织成以下结构:
import os import sys import cv2 from pathlib import Path class OldPhotoRestorer: def __init__(self, lama_path: str, gfpgan_path: str, device: str = "cuda"): from models.lama import LamaImageInpainting from gfpgan import GFPGANer self.lama = LamaImageInpainting(model_path=lama_path, device=device) self.gan = GFPGANer( model_path=gfpgan_path, upscale=2, arch="clean", channel_multiplier=2, bg_upsampler=None ) self.device = device def repair(self, img_path: str, mask_path: str | None, out_path: str): # 1. 修补 image, mask = self._load_pair(img_path, mask_path) with torch.no_grad(): inpainted = self.lama(image, mask) # 2. 增强 enhanced, _ = self.gan.enhance(inpainted, has_aligned=False) # 3. 保存对比图 self._save_grid(img_path, enhanced, out_path) if __name__ == "__main__": input_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("./photos") mask_dir = Path(sys.argv[2]) if len(sys.argv) > 2 else Path("./masks") output_dir = Path(sys.argv[3]) if len(sys.argv) > 3 else Path("./output") output_dir.mkdir(exist_ok=True) restorer = OldPhotoRestorer("weights/lama_big_512.pth", "weights/GFPGANv1.4.pth") for img_file in input_dir.glob("*.jpg"): mask_file = mask_dir / (img_file.stem + "_mask.png") if not mask_file.exists(): print(f"跳过 {img_file.name}: 缺少掩码文件") continue restorer.repair(str(img_file), str(mask_file), str(output_dir / img_file.name))逻辑说明:这个包装类的核心价值是把模型初始化放在__init__里,避免每张照片都重新加载权重。模型加载是 IO 密集型操作,每张照片重新载入会让批量处理变慢 10 倍以上。批量运行时,如果某张照片没有对应掩码,直接跳过并提示,而不是报错中断整个任务。
4. 修复老照片最常踩的五个坑:翻车现场与修改方案
这条路我走了很久才稳定,核心是五个坑:掩码膨胀、偏色、显存崩、GPU变慢、幻觉。下面按现象、原因、解决的顺序逐条拆。
4.1 扩到掩码太小:划痕边缘残留“亮线”,越修越明显
现象:修复结果里,划痕边缘还会有一条细细的白线或黑线;直接放大对比原图,划痕位置反而更显眼。
原因:手工标注掩码时,一般只覆盖划痕本身,但划痕周围的像素已经受物理损伤影响,颜色值发生偏移,只是肉眼暂时看不出来。LaMa 只重建掩码覆盖住的区域,边缘损伤像素保留下来,和周围重建后的纹理形成反差。
解决:对掩码做一次膨胀操作,注意不是腐蚀。用 OpenCV 的dilate函数,核大小根据图像分辨率决定。512×512 图上我一般用cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))膨胀两轮。膨胀后掩码会轻微侵入正常纹理区,LaMa 会连这些损伤边缘一并修复,虽然会牺牲一点点纹理细节,但视觉上干净很多。
4.2 模型输出发灰发闷:校色分支被忽略
现象:修复完的照片色彩暗淡,人脸发灰,天空发闷,和原图比信息量确实变多了,但观感很差。
原因:深度学习模型训练时偏向均方误差优化,输出倾向于回归到训练集均值,导致饱和度偏低、对比度保守;而老照片本身色彩偏黄偏棕,两相叠加,结果就会发灰。
解决:在 pipeline 末尾加一次自适应校色。最简单的做法是切换到 LAB 色彩空间,对 L 通道做对比度拉伸,对 a、b 通道做饱和度增强。代码如下:
import cv2 import numpy as np def tone_adjust(img: np.ndarray, contrast: float = 1.1, saturation: float = 1.15) -> np.ndarray: # 转为 LAB 空间,L 管亮度,a/b 管颜色 lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB).astype(np.float32) l, a, b = cv2.split(lab) # 亮度对比度:线性拉伸,clip 防止溢出 l = np.clip((l - 128) * contrast + 128, 0, 255) # 饱和度:把 a、b 分量向远离中心的方向拉伸 a = np.clip((a - 128) * saturation + 128, 0, 255) b = np.clip((b - 128) * saturation + 128, 0, 255) merged = cv2.merge([l, a, b]).astype(np.uint8) return cv2.cvtColor(merged, cv2.COLOR_LAB2BGR)参数说明:contrast=1.1是轻微的 S 曲线效果,太强会丢掉暗部细节;saturation=1.15是保守提饱和度。如果原图褪色很严重,可分段式处理,先提饱和度到 1.3,再降高光。做批量处理前,先挑 5 张有代表性的照片微调这三个值,不要照搬参数。
4.3 显存不够:LaMa 把显存顶爆,程序直接 OOM
现象:输入一张 1024×1024 的图,LaMa 推理时报CUDA out of memory,程序崩溃退出。
原因:LaMa 的特征图分辨率从输入分辨率开始逐级向下采样,但中间有个扩张路径会恢复到输入分辨率;显存占用和输入面积成正相关。1024×1024 的输入,再叠加批量维度,需求接近 8–10GB 显存,很多消费级显卡扛不住。
解决:三条路同时走。第一,输入图缩到 512×512,修复完成后再把修补区域贴回到原分辨率的图上;第二,开启torch.cuda.amp.autocast(),用 FP16 推理,显存减半;第三,使用torch.no_grad()正确释放中间梯度缓存。老照片修复不需要反传梯度,no_grad()能让中间特征图立刻释放,节省几 GB 显存。
with torch.no_grad(): with torch.cuda.amp.autocast(): result = model(image_t, mask_t)4.4 CPU 推理速度慢到无法接受:批量处理一小时只跑了十张
现象:没有 GPU 的电脑跑 LaMa,512×512 的图需要 30 秒到 1 分钟,GFPGAN 更慢,批量修复一整个相册几乎不可能。
原因:LaMa 里的空洞卷积在 CPU 上没有深度优化,GFPGAN 的 GAN 生成器在 CPU 上也无法利用并行加速。
解决:优先用 GPU,这个没什么商量空间。实在没有 GPU,就降低分辨率到 384×384,使用torch.set_num_threads(8)开启多线程,并把torch_dtype强制设为 FP32,避免 CPU 上 FP16 带来的额外转换开销。还有一种妥协方案:只对包含人脸的中心区域做 GFPGAN,背景只做 LaMa 修补,操作量能减半。
4.5 人脸生成过度平滑:GAN 幻觉糊掉了皮肤纹理
现象:老照片的人脸被修复后,皮肤像磨皮过度,毛孔和皱纹细节全没了,整体像 3D 渲染出来的人偶。
原因:GFPGAN 的训练目标是生成逼真人脸,所以在低质量输入上会倾向于生成一张“看上去很正常”的脸。输入越模糊,模型越依赖先验,先验占主导时,原始身份特征就被覆盖了。这就是所谓“黑匣子幻觉”问题。
解决:调整 GFPGANer 的only_center_face=False参数时,所有检测到的人脸都会做增强;如果照片里有多张人脸,建议逐张处理。同时,降低超分倍数upscale从 2 降到 1.2 或 1,让网络主要做修复而不是重新生成;还可以在贴回时降低增强图的不透明度,按 7:3 或 8:2 比例与原图混合,保留下原始纹理。下面是一个简单的融合写法:
# 原图为 base,增强图为 enhanced alpha = 0.8 # 增强图权重 blended = cv2.addWeighted(enhanced, alpha, base, 1 - alpha, 0)5. 把修复代码包装成一个能批量交付的小工具:验证指标与调参习惯
单张图跑通了,接下来要面对真实需求:批量修复一百张照片,并且要能向别人证明效果变好了。这里说两个实用指标,以及我长期沉淀下来的调参顺序。
5.1 批量处理与一个动作完成对比图生成
批量处理时,我很忌讳“只输出一张修复图”。没有对比图,你无法快速判断引入的偏色和细节变化,也无法向人解释修复效果。建议每次输出一个两图对比网格,代码写起来也很短:
import cv2 import numpy as np def write_comparison(before_path: str, after_path: str, output_path: str): before = cv2.imread(before_path) after = cv2.imread(after_path) # 保证两张图高度一致,方便横向拼接 h = min(before.shape[0], after.shape[0]) before = cv2.resize(before, (int(before.shape[1] * h / before.shape[0]), h)) after = cv2.resize(after, (int(after.shape[1] * h / after.shape[0]), h)) grid = np.hstack([before, after]) cv2.imwrite(output_path, grid)逻辑说明:横排对比是最直观的展示方式。批量模式下,每张照片处理完,立刻生成一张_comp.jpg存入输出目录。注意先统一高度再拼接,否则高分辨率扫描图与输出图尺寸不一致时拼接会报数组维度错误。
5.2 效果验证不能只看感受:PSNR 与 SSIM 的使用边界
修复类任务没有 ground truth,因为原始未退化照片根本不存在,所以 PSNR、SSIM 这类全参考指标只能用在“人为退化实验”里:拿一张高清新照片做高斯模糊、丢划痕,把它当作退化图,修复后与原始高清图对比。这个验证方法能反映模型在可控条件下的恢复能力,但真实老照片的表现会有出入。
具体做法是:
from skimage.metrics import structural_similarity as ssim, peak_signal_noise_ratio as psnr def evaluate(original_path: str, repaired_path: str): orig = cv2.imread(original_path) repaired = cv2.imread(repaired_path) if orig.shape != repaired.shape: repaired = cv2.resize(repaired, (orig.shape[1], orig.shape[0])) p = psnr(orig, repaired) s = ssim(orig, repaired, channel_axis=2) print(f"PSNR: {p:.2f} dB | SSIM: {s:.4f}")参数说明:PSNR 值低于 28dB 时,修复结果往往还有明显噪声或模糊;高于 32dB 时视觉上默认可接受。SSIM 在 0.9 以上说明结构保持良好,低于 0.8 说明细节丢失严重或出现过平滑。真实老照片修复没法算这两个指标,只能靠主观评估,但人为退化实验能帮你校准参数方向。
5.3 调参顺序与最终交付:哪些参数值得保留,哪些要针对每张照片单独调
我自己的调参顺序是固定的:先固定模型和权重,调整 LaMa 的输入分辨率(512 左右),确认掩码膨胀轮数,修复效果稳定后再动 GFPGAN 的upscale和融合 alpha。不要一上来就同时拉对比度和饱和度,否则出了问题根本不知道是哪一步引入的。
最后封装交付时,把参数整理成一份config.yaml,比在代码里写死好维护:
lama: model_path: weights/lama_big_512.pth input_size: 512 mask_dilate_iters: 2 mask_dilate_kernel: 7 gfpgan: model_path: weights/GFPGANv1.4.pth upscale: 2 alpha: 0.8 color: contrast: 1.1 saturation: 1.15我在实际交付时,每张照片还会保存一份修复参数记录,方便出问题时复盘。老照片修复没有绝对正确的参数,影响最大的还是掩码质量,手工标注虽然慢,却是效果提升最明显的一步。
这份方案做下来,最值钱的经验就是一句话:不要把深度学习模型当成黑匣子,每一层退化的处理都要有归属模型,最后用对比图来验收每一步。希望帮到你。
本文还有配套的精品资源,点击获取