简介:基于GFPGAN算法的老照片修复Python设计源码,面向图像修复、人脸增强方向的Python开发者和研究者,用于将模糊、破损的旧照片恢复得更清晰自然。压缩包共51个文件、约6.09MB,核心为21个Python脚本,承担训练与推理流程;7个YAML文件管理参数配置,png/jpg图片提供输入输出样例,md文档与txt说明辅助上手,另有mdb数据记录和pth模型权重文件,目录按gfpgan、models、archs、options等模块划分,结构清晰。已有491人学习下载。借助这套工程,读者可完整了解GFPGAN的代码组织方式、生成对抗网络的人脸先验修复思路,并在自有数据集上尝试跑通修复流程;项目附带配置、测试脚本、预训练权重及环境说明,适合作为二次开发或课程设计的直接起点。
1. 老照片修复先修脸:为什么我把修复方案定在 GFPGAN 上
拿一张上世纪八九十年代的合影,人脸边缘几乎融进背景里。直接上普通超分,结果是糊被放大成更糊;用锐化,人脸上又泛起一圈圈振铃。真正让人脸“重新变回人脸”的,是这套基于GFPGAN算法的Python老照片修复设计源码。GFPGAN 的思路很直接:不用普通卷积一味放大,而是借助生成模型先“回忆”出一张清晰人脸长什么样,再做退化去除。所以它对低分辨率、噪声、模糊混在一起的老照片人像特别有效。这篇笔记就讲它怎么选型、怎么在 Python 里跑通、批量修一批图时哪些参数必须调,以及哪些老照片它其实管不了。
2. GFPGAN 的修复逻辑与模型选型:先搞懂它在做什么再动手
2.1 人脸修复不是放大像素:退化去除与生成先验
老照片人脸的退化不是单纯的低分辨率。化学褪色、镜头失焦、扫描噪声、灰尘划痕叠加在一起,高频细节实际上已经丢了。普通超分模型学的是“低分辨率到高分辨率的映射”,它只能把模糊边缘锐化,但补不出鼻子轮廓、睫毛方向、皮肤纹理这些物理上不存在的信息。
GFPGAN 的做法是两路并行:一路从输入图里提取人脸的身份特征,另一路把这些特征送入一个预训练好的生成模型,在 latent 空间里重新生成一张清晰人脸,再与输入做退化对齐。换句话说,它用生成先验“补”了原本丢失的信息,这一步本质上是生成而不是放大。这也是为什么它修复出来的脸看起来惊艳,但稍不注意也会出现“不像本人”的问题——因为它确实有“想象”的成分。理解这一点,后面调 weight、调混合比例时才不会抓瞎。
工程上还有个容易忽略的好处:GFPGAN 不像传统修复管线那样依赖一堆手工设计的先验规则,比如边缘保护、纹理合成。你给它一张糊脸,它自己知道该往哪个方向修,这大大减少了调参工作量。代价是模型体积和显存占用都不算小,以及它对“人脸先验”的依赖决定了它只能修人脸区域,背景和纸张纹理它并不负责。
2.2 选哪个模型:v1.2、v1.3、v1.4 还是 RestoreFormer
官方仓库里你能拿到的模型大致分四个,初次接触的人很容易随便下一个就开跑,最后抱怨效果不对。其实四个模型定位差别挺明显:
| 模型 | 特点 | 适合场景 |
|---|---|---|
| GFPGAN v1.2 | 修复保守,身份保持好,但细节增强弱 | 轻微模糊、人脸结构还比较完好的老照片 |
| GFPGAN v1.3 | 细节增强猛,去模糊明显,容易出现“塑料脸” | 人脸很糊、想突出修复效果时 |
| GFPGAN v1.4 | 细节与身份平衡好,肤色自然,目前最推荐 | 大多数老照片修复,默认选它 |
| RestoreFormer | 对遮挡、眼镜、夸张姿态鲁棒 | 极端退化、侧脸、多人小脸场景 |
我的习惯是一律先跑 v1.4。如果某张照片修复后“太现代”,再退回 v1.2 看身份保持效果。RestoreFormer 更适合戴眼镜、帽子这种结构化遮挡多的照片,v1.4 遇到眼镜容易把镜框修变形。选型的另一个关键是 background upsampler:GFPGANer 里的bg_upsampler参数如果挂上超分模型,背景会被一起增强,旧照片的颗粒感和纸张纹理就被抹掉了。修老照片我一般把bg_upsampler=None,先把人脸修好,背景是否处理再单独决定。
2.3 推理管线拆开看:检测、对齐、修复、回贴
整条推理链路比单跑一个模型要长。GFPGAN 拿到输入图后,第一步用轻量人脸检测器找出所有人脸框,然后检测五个关键点(双眼、鼻尖、嘴角两端),再做仿射变换把人脸对齐到标准角度和尺寸。对齐之后才送进 GFPGAN 主模型做修复。
最后一步paste_back把修复后的人脸用掩膜贴回原图。这个细节决定了你会遇到的一大类问题:如果人脸检测失败,后续所有步骤都不会执行,输出图就是原样;如果对齐旋转没算好,贴回的人脸会歪。所以调试时别只盯着主模型,先确认检测环节有没有出问题。多人合影默认只修中心区域最大的一张脸,要修所有人脸,必须把only_center_face设为False。
3. 把 GFPGAN 跑起来:Python 环境与最小可用脚本
3.1 环境准备:一套不出错的安装顺序
GFPGAN 依赖基础超分库 basicsr、人脸工具库 facexlib,以及 PyTorch。最容易翻车的点是安装顺序:先装 PyTorch,再装 basicsr,最后装 GFPGAN 本身。反过来装,basicsr 会在安装过程中重新编译算子,跟已装的 PyTorch 版本对不上,就会报一堆 CUDA 扩展错误。
# 用 conda 建独立环境,避免污染其它项目 conda create -n gfpgan python=3.10 -y conda activate gfpgan # 按 PyTorch 官方命令装对应 CUDA 版本的 torch,装 CPU 版也能跑,只是慢 pip install torch torchvision # 先装两个关键依赖 pip install basicsr facexlib # 下载 GFPGAN 官方仓库源码并本地安装 git clone <gfpgan 官方仓库地址> cd gfpgan pip install -r requirements.txt python setup.py develop这里解释几个关键点。Python 版本我固定用 3.10,3.11 和 3.12 也能跑,但 facexlib 和 basicsr 的一些依赖对旧版 numpy 有要求,被系统升级过 numpy 之后容易在运行时才报错。setup.py develop是开发者模式安装,它不会把源码复制到 site-packages,而是直接引用当前目录,这样你后续改仓库里的代码立刻生效,调模型结构时不用反复重装。如果看到No module named 'basicsr.utils'这类错误,八成是 basicsr 没装或者装错了目录,重跑一次pip install basicsr即可。
还有一点注意:facexlib 首次运行会在用户目录下自动下载人脸检测模型权重,如果下载失败,代码会在检测那一步崩溃。解决办法是把同事或其它机器上已下载好的权重目录整个复制到对应路径下,这比反复试网络稳得多。
3.2 模型文件放对位置
模型权重体积不小,下载回来后先确认文件完整,别只看了个文件名就扔进目录。权重文件名要和代码里model_path指向完全一致,大小写、后缀都不能错。我一般单独建一个 weights 目录统一管理:
mkdir -p weights # 把从官方 release 页面下载的 GFPGANv1.4.pth 放到 weights 目录 ls -lh weights/GFPGANv1.4.pth如果执行ls显示的文件大小与官方标注差太多,说明下载不完整,加载时会报EOFError或者尺寸不匹配。另一个容易踩的点是用户图方便把模型名改成中文或者带空格,读取路径一旦出错报错信息只有FileNotFoundError,排查半天才发现是名字问题。
3.3 单张照片修复的最小脚本
下面这段脚本是能跑通的最小集,不涉及批量处理,方便你验证环境和模型是否正常:
import cv2 from gfpgan import GFPGANer # 初始化修复器 restorer = GFPGANer( model_path="weights/GFPGANv1.4.pth", # 指向刚才放好的权重 upscale=1, # 先不要放大,保持原尺寸 arch="clean", # 官方仓库默认架构,v1.4 用它 channel_multiplier=2, # 必须与权重配套,乱改会直接加载失败 bg_upsampler=None # 关掉背景超分,避免旧纸纹被抹掉 ) img = cv2.imread("old_photo.jpg") assert img is not None, "图片读取失败,检查路径与文件格式" # enhance 返回三个结果:裁剪出的人脸、修复后的人脸、整张修复图 cropped_faces, restored_faces, restored_img = restorer.enhance( img, has_aligned=False, # 输入是自然图像,需要内部做人脸检测 only_center_face=True, # 只修中心区域最大的人脸 paste_back=True, # 把修复后的人脸贴回原图 weight=0.5 # 0-1,越大修复感越强,越大越不像本人 ) cv2.imwrite("old_photo_restored.jpg", restored_img)代码本身很短,但每个参数都值得说清楚。upscale=1意思是让 GFPGAN 只做修复不做超分。老照片修复的核心诉求是还原清晰度而不是放大尺寸,图像放大放到最后一步单独做更好控制。only_center_face=True时,即使画面里有多张人脸,也只处理中心区域面积最大的那张,对于家人合影这种场景,这个值要改成False。weight控制最终贴回时“修复脸”和“原脸”的混合比例,0.5 是平衡点;想让结果更自然更像本人,往 0.3 调;想修得彻底干净,往 0.8 调。
第一次跑如果显存不够,优先把upscale改小,其次把输入图先压缩到长边 1600 像素再处理。CPU 也能跑,v1.4 模型约几百兆,纯 CPU 处理一张图可能要几十秒到几分钟,但不会报错。
4. 把单张脚本做成批量修复流程:从几张图到一批老照片
4.1 批量修复脚本:目录遍历与失败隔离
修一批老照片时,最怕的是某张图让人脸检测崩了,整个脚本停住。批量脚本必须做到“单张失败不影响整批”,还要把失败文件名打出来,方便事后单独处理。我一般这样组织:
import os import cv2 from gfpgan import GFPGANer restorer = GFPGANer( model_path="weights/GFPGANv1.4.pth", upscale=1, arch="clean", channel_multiplier=2, bg_upsampler=None ) def fix_one(path_in, path_out, weight=0.5): img = cv2.imread(path_in) if img is None: print(f"[skip] 读取失败: {path_in}") return False try: _, _, restored = restorer.enhance( img, has_aligned=False, only_center_face=False, # 合影也要修所有人脸 paste_back=True, weight=weight ) cv2.imwrite(path_out, restored) return True except Exception as exc: print(f"[fail] {path_in}: {exc}") return False os.makedirs("output", exist_ok=True) for name in os.listdir("input"): if not name.lower().endswith((".jpg", ".jpeg", ".png", ".bmp")): continue ok = fix_one( os.path.join("input", name), os.path.join("output", name) ) print(f"{name}: {'ok' if ok else 'failed'}")这里有个工程细节:输出目录与输入目录分开,绝不在原图上覆盖写。修复结果如果不满意,原图还在,这是你的后悔药。only_center_face=False对合影是必须的,否则一张合照只有中间的人被修复,边上的人脸保持原样,对比起来很突兀。批量跑的时候如果发现某些人脸修得过度,再单独针对那几张把weight调低,不要全局一刀切。
遇到子目录结构时,把os.listdir换成os.walk就能递归处理:拿到root后在output下重建同名路径。还有一个常见需求是保留修复前后对比图。我习惯在写完restored后,用cv2.hconcat把原图和结果横向拼接保存为一组,方便批量验收时快速翻阅。
4.2 修复前预处理:去掉白边、折痕和网纹
老照片大多是扫描件,直接喂进模型会被白边干扰检测,也会让模型把网纹当成画面细节。先做两步预处理能明显提升效果。第一步是自动裁掉白边:
import numpy as np img = cv2.imread("scan_old.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 白边像素接近255,阈值化为纯黑白后找最小外接矩形 _, th = cv2.threshold(gray, 250, 255, cv2.THRESH_BINARY) coords = cv2.findNonZero(th) x, y, w, h = cv2.boundingRect(coords) # 四周留2%的边,避免把真正的照片边缘裁掉 margin_x = int(w * 0.02) margin_y = int(h * 0.02) x = max(0, x - margin_x) y = max(0, y - margin_y) img = img[y:y + h + margin_y * 2, x:x + w + margin_x * 2] cv2.imwrite("scan_cropped.jpg", img)阈值取 250 是因为很多旧相纸底色不是纯白,带一点淡黄,阈值太高裁不掉,太低会把人物白衣服裁进去。折痕处理我一般用快速傅里叶域滤波,但那种方法参数敏感,处理不好会出现水波纹。更保险的做法是先用cv2.medianBlur对折痕区域做局部模糊,再用原图反向叠加修复。注意折痕不能交给 GFPGAN 修,它只认人脸,遇到跨脸部的划痕甚至可能把人脸修歪。
网纹和折痕不同,它是印刷品扫描后形成的规律纹理。对背景区域的网纹,我会做低频抑制,但人脸区域严重不建议直接滤波,因为网纹频率和皮肤纹理频率重叠,过滤网纹会顺带把皮肤细节一并抹掉。我的常规顺序是:先裁白边,再修折痕,最后把人脸修复交给模型。
4.3 修复后处理:让脸不假、背景不糊
GFPGAN 输出的人脸干净但偏平滑,和老照片的粗糙颗粒感放在一起反而显得“假”。这类问题通过后处理混合能救回来大半。最简单有效的方法是把修复结果和原图按权重叠加:
blended = cv2.addWeighted(restored, 0.8, img_cropped, 0.2, 0)0.8是修复图权重,0.2是原图权重,意思是保留大部分修复效果,但把原图的颗粒质感透出来一点。如果原图太糊,0.2原图部分会把模糊感带回来,那就把原图权重降到0.1。这里要找平衡:老照片修复追求的是“像老照片但又清晰”,不是“完全新的高清图”。
背景糊的问题另说。如果整张照片背景同样模糊,而只有人脸特别清晰,观感很割裂。我一般分两种情况处理:一种是接受背景现状,把修复后的人脸做边缘羽化再贴回;另一种是对全图做一次轻量超分,但这样会改变照片整体质感。我自己更常做的是前者,毕竟背景模糊在旧照片里是可以被容忍的,人脸糊才是最不能接受的。羽化贴回可以用cv2.seamlessClone做泊松融合,它对肤色边界有天然过渡,能解决“贴纸感”。人脸框和姿态检测得越准,融合越自然。
5. 避坑:GFPGAN 修老照片最容易翻车的 5 个现场
5.1 显存溢出:RuntimeError: CUDA out of memory
现象:批量跑图跑到一半,程序直接崩溃,报CUDA out of memory。原因:看起来单张图不大,但人脸检测和生成模型会同时占显存,且upscale=4时会放大特征图,显存占用翻数倍。解决:第一,upscale设为 1,修复阶段不要放大;第二,批量脚本里每处理一张图后把中间变量释放,用restored保存结果并del img, cropped_faces, restored_faces;第三,显存只有几 GB 的机器,先把输入图长边压到 1600 像素。这些做完仍溢出,就分块修复,把一张图切成几个区域各自处理再拼回。
5.2 输出和原图一模一样:人脸检测根本没触发
现象:跑完一张侧脸照片,输出文件名也生成了,但用图片对比工具一看,原图输出逐像素一致。原因:facexlib 自带的检测器在目标人脸过小、侧脸角度过大、或被帽子头发遮挡时检测失败,enhance没有检测到人脸,就直接返回了原图。这种“静默失败”特别坑,因为程序没报错,你以为修完了。解决:把only_center_face=False加上,让检测器遍历全图找所有可能的人脸;如果仍然没检测到,在预处理阶段用 OpenCV 的人脸检测器把脸的区域裁剪出来,单独放大 1.5 倍再送进去修复。别让整张图参与检测,很多老照片人脸区域面积不到全图的十分之一,放大后检测成功率会明显提高。
5.3 脸修得不像本人:生成先验收不住
现象:修复后五官精致、皮肤细腻,但怎么看都不像照片里的人,像是换了张“网红脸”。原因:weight设置过大,生成结果主导了混合结果,身份特征被覆盖了。生成模型本来就有想象力,参数越大它越敢“自由发挥”。解决:把weight从 0.5 降到 0.3,让原图人脸的结构信息多保留一些;同时把输入人脸区域裁得稍大一点,让模型看到更多脸型轮廓和发际线,这些特征能约束生成方向。如果降到 0.2 还像别人,那就要换 v1.2 模型试,v1.2 的身份保持能力明显更强。超过 0.8 的weight除了极端的模糊照片,其他场景不建议用。
5.4 脸部与周围肤色脱节:贴纸感严重
现象:人脸区域修复后,肤色明显比脖子和手部亮或红,边界清晰可见,像贴了张新皮。原因:模型对人脸区域做了独立照亮和肤色归一化,而原图周围区域保留原始色调,两者的光照信息不一致。解决:最简单的办法是修复后对人脸区域做一次直方图匹配,让脸部直方图向原图邻域对齐。用代码实现就是截取人脸框周边 20 像素区域作为参考直方图,对修复后的人脸做cv2.matchHistograms。如果项目愿意投入一点工作量,用seamlessClone替代直接贴回,边界过渡会自然很多。
5.5 模型加载报错:channel_multiplier 与权重不匹配
现象:脚本初始化就报错,提示张量尺寸对不上,比如size mismatch for style_conv1.weight。原因:channel_multiplier设错了。不同版本权重的通道倍率不同,v1.4 对应 2,某些旧权重对应 1,混用就会出现尺寸不匹配。解决:先确认你下载的权重文件名和代码里加载的是同一个;再对照官方示例里的channel_multiplier值。改这个参数之前先看权重文件体积,不同倍率的模型体积差很多,基本一眼能判断。另外,网上能搜到一些别人转换过的 ckpt 文件,文件名和官方对不上,加载逻辑也不一样,尽量避免用这种来源不明的版本。
6. 修完怎么验收:从“看起来不错”到“修对了”
6.1 用闪烁对比法做主观验收
我看修复结果从来不看单张图,而是用脚本把原图和修复图快速交替切换,像幻灯片闪烁一样。人眼对变化区域非常敏感,原图中脸上糊掉的轮廓在切换时会“跳动”,这个跳动幅度直接告诉你修改了哪里。如果脸型、发际线、眼神方向都没跳,只有皮肤纹理在变,说明修复是收敛的;如果整张脸都“跳”得厉害,说明这次修复的改动超出了结构范围,必须调低 weight。这个方法比盯着单张图更容易发现“修过了”的问题。
6.2 两个能快速算的量化指标
import cv2 # 拉普拉斯方差:衡量图像锐利度 lap_var = cv2.Laplacian( cv2.cvtColor(restored, cv2.COLOR_BGR2GRAY), cv2.CV_64F ).var() print(f"sharpness: {lap_var:.1f}")lap_var太小人脸依然糊,太大则可能过度锐化出现振铃。每张图的合理区间不同,不能跨图比较,但同一张图修前修后这个值必然上升。另一个指标是 facexlib 人脸检测时返回的置信度,修复后同一个人脸的检测分数会比原来高。如果修复后检测分数反而下降,说明脸被改得偏离了人脸分布,模型可能把脸“修歪”了。这两个指标只做辅助,最终用闪烁对比法拍板。
6.3 我的验收习惯:分类处理而不是一把梭
我现在处理一批老照片,第一步永远是分类:黑白照和彩色照分开,单人照和合影分开,糊脸照和轻微退化照分开。黑白照先修结构后上色,彩色照直接修;合影必须only_center_face=False;严重糊脸用 0.7 的高 weight,轻微退化用 0.3 保身份。分类之后批量脚本就变成了三个配置文件的拼装,每组照片跑一组参数。之前我图省事,一批老照片用同一套参数跑完,结果黑白照肤色怪异、合影漏人、轻微退化被修成假脸,三样问题凑齐,差点整个返工。
那次之后再做什么修复工程,我都会在输出目录里留一份记录修复参数的文本文件,至少写清楚模型名、weight、是否开背景超分、输入是否裁剪。这样一个人修完交付给另一个人,后者能准确知道当时的处理路径,而不是对着结果猜。老照片修复没有绝对标准答案,但过程参数必须可复现。希望这篇笔记能帮你少走点弯路,把我踩过的这些坑提前填平。
本文还有配套的精品资源,点击获取