☰
老照片修复项目:深度学习退化建模与Python实战
2026/10/8 7:32:17 网站建设 项目流程

简介:针对严重退化老照片的修复需求,这套实战项目基于深度学习,提供了完整的 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))), interpolation=cv2.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), interpolation=cv2.INTER_CUBIC) return img

这个函数是整条修复管线的地基,它把一张干净图退化成模拟老照片,输入输出尺寸保持一致。很多人第一次写退化合成时,会直接把下采样后的小图拿去当训练输入,让网络同时学超分和去退化。这不是不行,但任务变重,在严重退化场景下非常不稳定。常见做法是先下采样再上采样回原尺寸,把超分拆到后处理阶段单独做,模型只负责「把退化去掉」。

参数范围上,blur_kernel 取 5~15 的奇数,sigma 取 1~3;太小等于没糊,太大会把纹理全抹平,模型永远学不回细节。noise_level 是高斯噪声标准差,8bit 图建议在 0~40 之间采样。down_ratio 建议 2~4,超过 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, device="cuda"): state = torch.load(weight_path, map_location="cpu") 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_size=256, overlap=32): 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_size128~256决定网络感受野;越大显存占用越高
overlap16~32滑窗拼接时消除接缝;越大重叠计算越多
denoise_strength0.5~1.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_size=192, degrade_cfg=None): self.files = glob.glob(f"{image_dir}/**/*.jpg", recursive=True) + \ glob.glob(f"{image_dir}/**/*.png", recursive=True) 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 设在 128~192 比较稳妥,再大显存压力会陡增。

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.5,batch_size 在显存允许的范围内尽量大,patch 越小 batch 可以越大,训练反而更稳。这些数字是常见起手式,具体项目要根据数据量微调,没有统一的「玄学最优值」。

5. 避坑:老照片修复项目的 5 个翻车现场与排查清单

这一章写实操里最常遇到的五个问题,每一条按现象、原因、解决来写,遇到时可以直接照做。

5.1 torch.load 加载权重直接崩

现象:跑推理脚本时torch.load报错,要么ModuleNotFoundError,要么Can't get attribute 'xxx' on module 'y'。

原因:权重是旧版 PyTorch 保存的,里面的自定义类在加载时找不到;或者当前机器没有 GPU,权重里的 tensor 是 CUDA tensor,直接加载会撞上「CUDA not available」。

解决:先统一torch.load(weight_path, map_location="cpu")把 tensor 搬到 CPU 再说;加载前确保项目里的模型定义模块已经 import。新版 PyTorch 默认weights_only=True,遇到反序列化失败的权重,可以显式传weights_only=False,但只对可信来源的 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,再让眼睛把放大图过一遍。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询