老照片修复项目:深度学习退化建模与Python实战
发布时间:2026/10/8 7:32:19 作者:尧图编辑部 阅读量:1,286

简介针对严重退化老照片的修复需求这套实战项目基于深度学习提供了完整的 Python 实现适合具备一定 Python 基础的图像处理学习者和研究者。项目覆盖从数据预处理、模型训练到推理部署的主要环节既涉及 CNN 特征提取也引入 GAN 生成对抗思路可用于划痕、破损、模糊等旧照片的修复与还原。压缩包共 81 个文件其中 54 个 py 脚本构成核心代码包括训练、测试、人脸检测与对齐等模块17 张 png 图片展示修复流程和效果4 份 pdf 文档提供方案说明、提案与演示材料另有 txt 依赖清单、md 说明、jpg 示例图、gif 演示动图及 mp4 录屏整体约 50.71MB。目前已有 350 人学习下载。通过该包可快速了解老照片修复的工程化实现获得完整代码、测试图片和配套文档目录将训练、测试、人脸增强、数据配置等模块分开便于按需取用也能对比效果并扩展至自己的修复任务。1. 老照片修复项目为什么难在「严重退化」先建退化模型再谈深度学习严重退化的老照片难点从来不是「不够清晰」而是退化原因太多、太杂胶片颗粒、扫描噪声、划痕、对焦模糊、压缩伪影叠在一起。如果一上来就做超分辨率反而先把噪声放大了一遍。老照片修复项目在深度学习里的正确打开方式是把「退化」本身建模成可学的问题——用 Python 工程把数据合成、模型训练和推理脚本串成一条线再针对噪声、划痕、模糊逐项还原。这个方向特别适合手里有整批老照片要数字化、或者想在图像复原方向落地的工程师入门者拿到 zip 包下载解压、跑通推理、微调权重三步就能见到可交付的结果。2. 先搞清楚退化的是什么把老照片的损伤拆成深度学习能学的四个因子在动手跑模型之前我习惯先把「严重退化」四个字拆开。深度学习修复网络本质是在学一个映射输入退化图输出干净图。如果连退化长什么样都说不清训练数据的合成就是瞎猜模型泛化自然也谈不起。老照片的退化可以归纳成四类每一类都能用数学表达式写出来这也是后面合成训练数据的基础。2.1 噪声、模糊、划痕与低分辨率四类退化的来源与建模方式退化因子典型来源深度学习里常用的建模方式噪声胶片颗粒、高 ISO、保存不当高斯噪声、脉冲噪声叠加模糊对焦不准、长曝光抖动高斯模糊核、运动模糊核卷积划痕物理磨损、扫描尘土随机亮线/暗线叠加在像素上低分辨率早期扫描分辨率不足双三次/区域插值下采样把这四个因子分开建模型不只是为了描述清楚更重要的是后面训练时能控制组合强度。噪声在数学上是像素值叠加一个随机分布模糊是图像与卷积核的卷积划痕是局部像素被置成极端值低分辨率是采样点变少。真实老照片很少是均匀退化的大部分区域只是偏暗偏糊少数区域才有重度划痕和破损。如果合成数据做成全局一致的退化模型会学成一个「固定强度过滤器」拿到真实照片就翻车。2.2 用 Python 写一个合成退化器给训练准备「退化-干净」对import numpy as np import cv2 def degrade_old_photo(img, cfg): 把一张干净图合成为严重退化的老照片。 img: RGB uint8, H x W x C cfg: dict, 控制退化强度, 训练时每组随机采样 h, w img.shape[:2] # 1) 模糊模拟对焦不准和镜头像差, 核大小取奇数 blur_k int(cfg[blur_kernel]) | 1 img cv2.GaussianBlur(img, (blur_k, blur_k), cfg[blur_sigma]) # 2) 下采样模拟早期扫描分辨率不足 ratio cfg[down_ratio] img cv2.resize(img, (max(1, int(w / ratio)), max(1, int(h / ratio))), interpolationcv2.INTER_AREA) # 3) 噪声胶片颗粒和高 ISO 噪点统一用高斯噪声近似 noise np.random.normal(0, cfg[noise_level], img.shape) img np.clip(img.astype(np.float32) noise, 0, 255).astype(np.uint8) # 4) 划痕随机短线, 亮色暗色交替, 模拟扫描尘土和物理磨损 for i in range(cfg[scratch_num]): x1, y1 np.random.randint(0, img.shape[1]), np.random.randint(0, img.shape[0]) x2, y2 np.random.randint(0, img.shape[1]), np.random.randint(0, img.shape[0]) color (255, 255, 255) if i % 2 0 else (0, 0, 0) cv2.line(img, (x1, y1), (x2, y2), color, np.random.randint(1, 3)) # 5) 上采样回原尺寸让模型专注去退化, 而不是顺带做超分 img cv2.resize(img, (w, h), interpolationcv2.INTER_CUBIC) return img这个函数是整条修复管线的地基它把一张干净图退化成模拟老照片输入输出尺寸保持一致。很多人第一次写退化合成时会直接把下采样后的小图拿去当训练输入让网络同时学超分和去退化。这不是不行但任务变重在严重退化场景下非常不稳定。常见做法是先下采样再上采样回原尺寸把超分拆到后处理阶段单独做模型只负责「把退化去掉」。参数范围上blur_kernel 取 515 的奇数sigma 取 13太小等于没糊太大会把纹理全抹平模型永远学不回细节。noise_level 是高斯噪声标准差8bit 图建议在 040 之间采样。down_ratio 建议 24超过 4 再上采样回来边缘会带明显锯齿模型要花大量容量去修锯齿反而影响主要目标。2.3 合成退化的参数怎么定才不会被模型一眼识破一个很容易被忽略的问题如果训练时退化参数范围太小模型学到的是「某种噪声强度下的去噪」而不是「去退化」。我一般会把退化强度做成带空间分布的随机张量先对整图生成一张低分辨率随机图再上采样成每个像素的退化强度让退化的轻重在画面里自然过渡。真实老照片往往是中间清楚、边缘糊或者局部有一条重划痕其他区域干净全局均匀退化反而最不真实。还有一个便宜好用的增强在退化管线最后加一次 JPEG 压缩模拟用cv2.imencode( .jpg, img, [cv2.IMWRITE_JPEG_QUALITY, random.randint(20, 80)])再解码回来。早期扫描件和网络传播版本基本都有 JPEG 伪影这一小步对真实老照片泛化的提升比调半天网络结构都明显。3. 从 zip 到跑通第一张图Python 环境、依赖与最小推理脚本拿到 zip 之后第一件事不是急着双击解压而是先确认里面装的是什么。这一章讲完整落地顺序环境、推理脚本、以及影响修复效果的三个关键参数。3.1 先解压、再检查目录装深度学习依赖时别图省事unzip old_photo_repair.zip -d ./old_photo_repair cd ./old_photo_repair python -m venv .venv source .venv/bin/activate pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python pillow numpy tqdm scipy解压后先看目录结构确认有没有 weights、config、scripts 这三类东西。README 如果有先读一遍很多坑就写在里面比如「把图片放到 input/ 下」「只支持 RGB 输入」。建立虚拟环境很重要深度学习项目对 numpy、torch、opencv 版本极其敏感装进项目自己的 venv 里之后换项目互不污染。如果 zip 里自带 requirements.txt优先用pip install -r requirements.txt而不是手动装一遍报缺什么再单独补。GPU 机器的 PyTorch 版本要跟驱动匹配CPU 机器直接pip install torch torchvision装 CPU 版即可推理慢一些但流程一样跑得通。不要看到报错就装最新版先把报错信息里要求的版本范围读完再做决定。3.2 最小推理脚本加载预训练权重修一张老照片import torch import torchvision.transforms as T from PIL import Image def load_repair_model(weight_path, model_cls, devicecuda): state torch.load(weight_path, map_locationcpu) model model_cls() if state_dict in state: model.load_state_dict(state[state_dict]) else: model.load_state_dict(state) return model.eval().to(device) def fix_single(image_path, model, device, patch_size256, overlap32): img Image.open(image_path).convert(RGB) transform T.Compose([ T.ToTensor(), T.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), # 转到 [-1, 1] ]) x transform(img).unsqueeze(0).to(device) with torch.no_grad(): y model(x) y y.squeeze(0).cpu() y y * 0.5 0.5 # 逆归一化回 [0, 1] y torch.clamp(y, 0, 1) out T.ToPILImage()(y) # 自动把 float 转回 8bit return out这个脚本能跑通的前提是模型定义已经导入、权重文件名没写错。加载时的三个常见差异归一化范围是 [-1, 1] 还是 [0, 1]、通道顺序是 RGB 还是 BGR、输入是不是还需要一张噪声图作为辅助输入。这些都应该在 config 里确定好不要直接改模型权重适配。如果输入的是大尺寸老照片直接整图 forward 很可能显存不够。常见做法是滑窗推理把大图切成 patch_size 的块块与块之间留 overlap逐块 forward 再拼回去。拼接时对重叠区取平均而不是直接覆盖可以避免明显接缝。CPU 机器上只是慢不至于跑不出结果。3.3 三个必调参数patch_size、overlap、denoise_strength参数推荐范围作用patch_size128256决定网络感受野越大显存占用越高overlap1632滑窗拼接时消除接缝越大重叠计算越多denoise_strength0.51.0控制去噪强度与细节保留的平衡patch_size 不能太小否则网络看不到足够的上下文。老照片里的划痕往往横跨几十像素patch 太小会把划痕当成正常纹理。overlap 是滑窗推理体验差异最大的参数overlap 为 0 时块与块之间经常出现一条条亮度突变线肉眼非常明显设为 32 后几乎看不出来。denoise_strength 在很多项目里是对推理时噪声图的缩放系数本质是在告诉模型「这张图可信程度多高」。如果老照片扫得很粗糙、颗粒很重我会把 denoise_strength 调高一点如果只是颜色发黄、没有太多划痕调低反而能保住头发、衣纹这类小细节。这三个参数互相独立先固定 patch_size 和 overlap再单独试 denoise_strength不要一次动两个变量。4. 预训练模型不够用时用合成退化数据微调预训练权重不是万能的修上几十张图总会碰到特别顽固的几张。这时候就需要微调。但微调前必须做边界测试否则方向错了loss 降得再快也没用。4.1 边界测试先拿 10 张图把模型的能力边界摸清步骤很简单挑 10 张有代表性的图2 张人脸特写、2 张合影、2 张风景、2 张带文字或招牌、2 张重度划痕用默认参数逐个推理。然后把结果分成三类能直接交付、需要轻微后处理、彻底翻车。这一步能直接告诉你要不要微调。如果 5 张以上都能直接交付说明 zip 里的预训练权重和数据分布已经匹配微调价值不大调参数就够了。如果翻车集中在某一类场景比如人脸说明这类场景在预训练权重里没见过微调目标就很明确我只需要准备人脸老照片的数据而不是普遍撒网。翻车组里再做一次降强度测试把 denoise_strength 调低看是参数不合适还是模型能力不够——这一步能避免你白做一次微调。4.2 做一个在线退化的 Dataset训练时实时生成退化对from torch.utils.data import Dataset import glob import numpy as np import cv2 import torch class OldPhotoDataset(Dataset): def __init__(self, image_dir, patch_size192, degrade_cfgNone): self.files glob.glob(f{image_dir}/**/*.jpg, recursiveTrue) \ glob.glob(f{image_dir}/**/*.png, recursiveTrue) self.patch_size patch_size self.degrade_cfg degrade_cfg or { blur_kernel: 9, blur_sigma: 2.0, down_ratio: 3, noise_level: 20, scratch_num: 5, } def __len__(self): return len(self.files) def __getitem__(self, idx): img cv2.imread(self.files[idx]) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 随机裁剪一块, 训练时每轮的 patch 位置都在变 h, w img.shape[:2] x np.random.randint(0, max(1, w - self.patch_size)) y np.random.randint(0, max(1, h - self.patch_size)) patch img[y:y self.patch_size, x:x self.patch_size] # 在线合成退化, 每个 epoch 看到不同的退化组合 degraded degrade_old_photo(patch, self.degrade_cfg) degraded degraded.astype(np.float32) / 255.0 clean patch.astype(np.float32) / 255.0 # HWC - CHW, 转成 PyTorch Tensor dtype torch.float32 return torch.from_numpy(degraded).permute(2, 0, 1).to(dtype), \ torch.from_numpy(clean).permute(2, 0, 1).to(dtype)在线合成的价值有三个每一轮见到的退化都不一样模型学会的是「去退化的一般能力」而不是记住某一种噪声强度不需要预先存放大几十 GB 的训练图省磁盘模型测试时输入分布更稳定。注意把退化参数的随机范围先固定下来不要训到一半还在漂。patch_size 设在 128192 比较稳妥再大显存压力会陡增。4.3 损失函数与学习率L1、感知损失、判别器的搭配import torch.nn.functional as F def loss_fn(pred, clean, perceptual_model): l1 F.l1_loss(pred, clean) # 感知损失: 用 VGG 中间层特征做 L1, 把纹理拉回来 percep F.l1_loss(perceptual_model(pred), perceptual_model(clean)) # SSIM 损失抑制局部失真, 具体的 SSIM 实现可以用 pytorch-msssim ssim 1.0 - ssim_loss(pred, clean) total 0.6 * l1 0.35 * percep 0.4 * ssim return total严重退化修复里 L1 是主力它让输出在像素上贴近干净图感知损失补偿的是人眼关注的高级纹理没有它网络会为了降低 L1 而把细节全部抹平SSIM 损失对局部亮度偏差和边缘失真很敏感能减少块状伪影。不建议一上来就上对抗生成网络。常见做法是先用 L1 感知 SSIM 训练到 loss 平台期再以 0.05 的权重把 GAN 分支打开继续训一小段。GAN 权重过大的表现非常典型人脸会「变好看」但完全不像照片里的人。学习率从 1e-4 开始每 20k 步乘以 0.5batch_size 在显存允许的范围内尽量大patch 越小 batch 可以越大训练反而更稳。这些数字是常见起手式具体项目要根据数据量微调没有统一的「玄学最优值」。5. 避坑老照片修复项目的 5 个翻车现场与排查清单这一章写实操里最常遇到的五个问题每一条按现象、原因、解决来写遇到时可以直接照做。5.1 torch.load 加载权重直接崩现象跑推理脚本时torch.load报错要么ModuleNotFoundError要么Cant get attribute xxx on module y。原因权重是旧版 PyTorch 保存的里面的自定义类在加载时找不到或者当前机器没有 GPU权重里的 tensor 是 CUDA tensor直接加载会撞上「CUDA not available」。解决先统一torch.load(weight_path, map_locationcpu)把 tensor 搬到 CPU 再说加载前确保项目里的模型定义模块已经 import。新版 PyTorch 默认weights_onlyTrue遇到反序列化失败的权重可以显式传weights_onlyFalse但只对可信来源的 zip 用这是个安全提醒。5.2 显存不够修一张 4K 老照片被 OOM 卡死现象小图跑得飞快换大图后直接 CUDA out of memory。原因把整张高分辨率图直接喂给了网络特征图尺寸撑爆显存。解决改成滑窗推理。把大图切成 patch_size 的块块与块之间留 overlap逐块 forward 再拼回去重叠区取平均而不是直接覆盖。还可以用torch.autocast(cuda)把推理包起来半精度推理能省约 30% 显存。CPU 机器上也不会死只是慢可以先跑一张小图验证流程。5.3 人脸被修得「变好看」了但不像照片里的人现象修复完五官立体、皮肤光滑但用户说「这不是我爷爷」。原因生成式先验过强模型在脑补一张符合审美的人脸而不是还原像素分布。解决微调时把 GAN 分支权重从 0.1 降到 0.02或者干脆前期只用 L1 感知损失。推理时也有补救办法把输入 patch 放大到包含更多周围环境让网络看到画面上下文而不是只盯着脸局部。人脸修复的生成先验是把双刃剑用好了能修复皱褶用不好就变成换脸——这是血泪经验。5.4 loss 掉得很低验证集上却越修越糊现象训练 loss 一路降到很低验证集跑出来却像隔着一层雾细节全没了。原因合成退化的参数范围太窄模型实际上学成了一个低通滤波器真实照片的噪声强度稍有不同直接失效。解决把退化参数一次打散blur_sigma、noise_level、scratch_num 全部按区块随机再把 JPEG 压缩模拟加进退化管线。退化越随机模型越没有机会走捷径。另有一个习惯训练中每隔 5k 步拿一张真实老照片看一眼验证输出不要只盯 loss 曲线loss 漂亮不代表修得清楚。5.5 zip 解压后缺文件或权重文件是 0 字节现象README 里说有 weights.pth实际解压出来没有或者文件存在但大小是 0。原因压缩包传输不完整、杀毒软件把权重当成可疑文件隔离、Windows 中文路径导致解压异常。解决不要直接改代码绕过去先用python -m zipfile -e old_photo_repair.zip ./extract_dir重新解压观察有没有报错对比解压出的文件大小和压缩包内记录是否一致。再看杀毒隔离区把工程目录加白名单。最后把项目放到纯英文路径下再跑。压缩包里真的缺文件时优先尝试重新下载多数情况是源文件没传完自己硬补反而不值。6. 验证修复结果只看 PSNR 不够还要做三项检查6.1 用 cycle check 检查「修复→再退化」能不能回到原图def cycle_mse(old_img, repaired, cfg): degraded_again degrade_old_photo(np.array(repaired), cfg) return float(cv2.absdiff(old_img, degraded_again).mean())这个 check 不需要 ground truth。思路很简单一个诚实的修复结果应该落在退化合成器的「前像」附近把它再退化一次应该能大致回到原图的状态。如果 cycle loss 明显偏高说明模型在原始退化路径之外自由发挥了。真实场景下把同一个权重、三个不同参数组合的修复结果跑一遍 cycle选 loss 最小的那个通常比凭肉眼猜更可靠。6.2 无参考指标 放大抽查真实老照片没有干净原图PSNR 和 SSIM 根本没法算这时候只能用无参考指标辅助判断。OpenCV 里现成的 BRISQUE 可以直接用分数越低越好。我一般不会只依赖它而是固定一个放大倍数把修复结果放大两到三倍重点看三个位置物体边缘有没有振铃天空和墙面这类平滑区有没有色斑划痕是不是被修成了「合法物体」。最后这一类最阴险白划痕经常被脑补成一束光、一条溪流、一根树枝缩略图看完全没问题放大就露馅。我做过不少老照片修复项目最深的一条教训是只看指标的时候模型往往已经开始撒谎了。有次我交付了一张修好的合影PSNR 比上一版高了 0.8dB用户放大一看右下角多了一条闪光的水沟——那是原图的一道划痕。从那以后每个项目交付前我都会跑一遍 cycle check再让眼睛把放大图过一遍。希望帮到你。本文还有配套的精品资源点击获取