ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

老照片修复实战:PyTorch+LaMa+GFPGAN搭建深度学习修复流水线

老照片修复实战:PyTorch+LaMa+GFPGAN搭建深度学习修复流水线 简介一份面向老旧照片修复与修补的深度学习完整源码项目适合具备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 融合。LaMaLarge Mask Inpainting则不是生成对抗网络路线它靠的是感受野特别大的空洞卷积。它不需要像 Partial Conv 那类模型那样维护掩码更新状态直接输一张图加一张掩码图就出结果。它对纹理补全的真实感很强且代码改动很小适合嵌入现有工程。2.3 一条最小修复流水线预处理 → 超分 → 删补 → 校色把模型串起来我常用这条流水线预处理降噪滤波可选转 RGB缩放到模型输入尺寸。划痕定位先用低阈值边缘检测拿到候选划痕再人工或半自动标注掩码如果懒得手动标也可用带监督的划痕检测模型。修补把原图与掩码输入 LaMa只重建掩码区域内的像素。超分增强将修补后的图输入 GFPGAN 或 Real-ESRGAN 做 2x 或 4x 放大。校色与后处理做一次白平衡调整饱和度与对比度输出成图。这里要强调掩码质量是修补效果的天花板。掩码做小了划痕残留在边缘肉眼可见掩码做大了会把人脸正常纹理框进补全区生成一张光滑的假脸。所以掩码膨胀与腐蚀操作在流水线里不是可选项是必选项。具体数值后面在避坑里细说。3. 用 PyTorch 跑通老照片修复最小实现依赖安装与三段串联代码工具链选型Python 3.10PyTorch 2.xUbuntu 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_pathweights/lama_big_512.pth, devicecuda 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 # 初始化模型 # upscale2 表示输出尺寸为输入的两倍 # archclean 表示使用干净的 ResNet 骨架显存占用更低 # channel_multiplier2 是 GFPGANv1.4 的默认宽度配置 restorer GFPGANer( model_pathweights/GFPGANv1.4.pth, upscale2, archclean, channel_multiplier2, bg_upsamplerNone ) # 推理输入是上一步修补完成的图 input_img cv2.imread(old_photo_inpainted.png, cv2.IMREAD_COLOR) output_img, _ restorer.enhance( input_img, has_alignedFalse, only_center_faceFalse, paste_backTrue ) cv2.imwrite(old_photo_enhanced.png, output_img)逻辑说明enhance()内部会自动检测人脸关键点裁剪出人脸区域做增强增强后再贴回原图。has_alignedFalse表示输入图没有做过人脸对齐需要模型自动检测。paste_backTrue把增强后的人脸贴回背景避免背景被 GAN 过度生成。这里bg_upsampler不传值因为 GFPGAN 自带的背景增强较弱背景细节保留不如 Real-ESRGAN后续若要更精细的背景纹理可单独用 Real-ESRGAN 再做一次。参数说明upscale2时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_pathlama_path, devicedevice) self.gan GFPGANer( model_pathgfpgan_path, upscale2, archclean, channel_multiplier2, bg_upsamplerNone ) 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_alignedFalse) # 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_okTrue) 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)参数说明contrast1.1是轻微的 S 曲线效果太强会丢掉暗部细节saturation1.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 的电脑跑 LaMa512×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_faceFalse参数时所有检测到的人脸都会做增强如果照片里有多张人脸建议逐张处理。同时降低超分倍数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_axis2) print(fPSNR: {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我在实际交付时每张照片还会保存一份修复参数记录方便出问题时复盘。老照片修复没有绝对正确的参数影响最大的还是掩码质量手工标注虽然慢却是效果提升最明显的一步。这份方案做下来最值钱的经验就是一句话不要把深度学习模型当成黑匣子每一层退化的处理都要有归属模型最后用对比图来验收每一步。希望帮到你。本文还有配套的精品资源点击获取
返回列表