简介面向图像处理学习者和开发者的GFPGAN老照片修复Python源码工程以泛用性人脸先验引导修复网络为核心结合GAN生成对抗机制增强人脸细节可用于老照片人像修复、画面清晰度恢复等场景。压缩包共51个文件、约6.09MB包含21个Python脚本、7个YAML配置文件、6个PNG与2个JPG样例图、2个Markdown文档以及2个MDB数据库文件等。Python脚本覆盖推理、训练与网络结构实现涉及gfpganv1_clean_arch、stylegan2_clean_arch、arcface_arch等核心模块YAML用于参数配置与数据集说明图片提供修复前后对比效果工程内还附带pth模型权重、VERSION版本文件与license许可说明。项目另含CI工作流、代码规范及PaperModel论文笔记目录划分清晰便于二次开发与算法复现。已有491人学习浏览适合具备Python和深度学习基础、想快速上手人脸修复算法的开发者。1. 老照片修复为什么绕不开GFPGAN从现实需求到源码设计的起点如果你手头有一批褪色、有折痕的人脸老照片又不满足于美图秀秀那种全局调色那基于GFPGAN算法的Python老照片修复设计源码这条路值得认真走一遍。GFPGANGenerative Facial Prior GAN是目前把“人脸先验”融入GAN修复的代表方案它专门解决人脸区域反复重绘出奇怪五官的痛点比起直接拿通用超分模型去修修复结果更接近“原来的那个人”。这篇文章我来拆一个可直接运行的老照片修复项目源码从环境配置、模型加载到参数调优和排错都按我实际踩坑的经验写。适合正在做图像修复项目的Python开发者、AI应用工程师以及想自己动手修复全家福老照片的玩家。2. 从零搭一个GFPGAN项目目录设计与环境准备2.1 项目目录设计与文件职责老照片修复项目看着就一个推理脚本真做起来远不是“装个库、跑一段代码”那么简单。模型权重放在哪、输入输出怎么组织、日志写到哪里、后续要不要接批量处理这些都得在动手前定好。我一般会搭一个这样的目录old_photo_fix/ ├── main.py # 命令行入口支持文件或目录输入 ├── requirements.txt # 依赖清单 ├── weights/ │ └── GFPGANv1.4.pth # 预训练人脸修复模型 ├── inputs/ # 待修复的老照片 ├── outputs/ # 修复结果输出目录 ├── utils/ │ ├── __init__.py │ ├── io_utils.py # 图片读写、路径校验 │ └── face_align.py # 人脸检测辅助可选 └── configs/ └── fix_config.yaml # 参数配置方便调参不动代码为什么要单独建weights/和configs/因为 GFPGAN 的模型文件在 GitHub Release 和 Hugging Face 上都能找到但不同渠道下载的文件名可能带后缀大小也可能不一致。固定放在weights/目录并统一命名为GFPGANv1.4.pth代码里就只需要维护一个路径变量不会出现“测试时能跑、换台机器就跑不了”的尴尬。configs/则把 upscale、strength 这类参数从代码里剥离出去后面做参数扫描时不用反复改 main.py。2.2 Python 环境安装与依赖要求GFPGAN 依赖 PyTorch 和它配套的人脸工具库官方推理用的是gfpgan包 realesrgan做背景增强。我的环境踩过几轮坑最省事的一组命令是conda create -n gfpgan python3.8 -y conda activate gfpgan pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install gfpgan realesrgan opencv-python第一行创建 Python 3.8 环境不是越新越好GFPGAN 的有些依赖特别是 facexlib在 Python 3.11 上偶尔会因为 setuptools 兼容问题报错3.8 最稳。第二行装 PyTorchcu118是 CUDA 11.8 版本如果你的显卡驱动只支持 CUDA 10.2这里要改成cu102否则导入 torch 时会报libcusparse.so找不到。第三行装人脸修复主力库opencv-python建议装 4.5.4 之后的新版本老版本配合gfpgan偶尔会出现在cv2.rectangle里画不出框的诡异问题。装完后先别急着跑把facexlib也要确认一下它是人脸检测的后端依赖。gfpgan库在 pip install 时通常会拉进来但如果你用的是精简版 Python 镜像可能缺少可以手动补一条pip install facexlib。检查环境用python -c import gfpgan; print(gfpgan.__version__)能正常打印就不是环境问题。2.3 获取预训练模型权重放置与校验GFPGAN 的模型文件是一个很大的.pth文件很多老照片修复教程都会让你去下载但不告诉你下载完怎么确认文件没坏。我建议加一道校验逻辑避免修复到一半发现模型损坏白等几分钟。import os model_path weights/GFPGANv1.4.pth if not os.path.exists(model_path): raise FileNotFoundError( 模型缺失请把 GFPGANv1.4.pth 放到 weights/ 目录 ) file_size os.path.getsize(model_path) if file_size 100 * 1024 * 1024: # 100MB 只是第一道门槛 raise ValueError( f模型文件异常大小只有 {file_size / 1024 / 1024:.1f}MB 很可能下载断点完成请重新下载完整文件 ) print(f模型文件通过基础校验大小为 {file_size / 1024 / 1024:.1f}MB)这段逻辑放在 main.py 的最前面比直接跑模型更实在。GFPGANv1.4.pth的实际体积在 300MB 以上如果只有几 MB 或几十 MB基本就是下载不完整。用文件大小做第一道校验好处是简单明了不用维护繁琐的哈希表但对追求严谨的工程化项目建议拿到文件后用官方提供的 SHA256 再算一遍这里就不展开哈希脚本了。3. 核心推理代码逐段拆解修复一张老照片的最小可运行实现3.1 先写 main.py解析参数与输入校验老照片修复的推理链路很清晰读图 → 人脸检测 → GFPGAN 增强人脸 → 贴回原图 → 保存。但每一环都有参数干扰命令行设计做不好调参就会变成反复改代码。我的做法是把变动最多的参数全部提到argparse里。import argparse def parse_args(): parser argparse.ArgumentParser(descriptionGFPGAN老照片修复) parser.add_argument(--input, typestr, requiredTrue, help输入图片路径或包含多张图片的目录) parser.add_argument(--output, typestr, defaultoutputs, help修复结果存放目录) parser.add_argument(--model_path, typestr, defaultweights/GFPGANv1.4.pth, helpGFPGAN权重路径) parser.add_argument(--upscale, typefloat, default2, help放大倍率建议2老照片不宜过大) parser.add_argument(--strength, typefloat, default0.6, help修复强度0到1越高人脸重绘越激进) parser.add_argument(--face_size, typeint, default512, help送入GFPGAN的人脸对齐尺寸) return parser.parse_args()这段代码的用意是把“修复成什么样”的控制权交给使用者。--input是必填参数支持单张图片或整个目录这一点直接决定了脚本能否批量处理一卷老照片。--upscale默认 2 而不是 4是因为老照片本身分辨率低、噪声多强行放大 4 倍会让背景区域的 JPEG 块状感更明显。--face_size是很多人容易忽略的参数它控制检测到的人脸区域会被放大到多大再送入网络512 是 GFPGAN v1.4 的推荐值太小会丢失细节太大容易出现“看图猜脸”的幻觉。有了参数解析main.py 的核心入口只需要四行解析参数 → 校验模型 → 初始化修复器 → 批量处理。我习惯把初始化单独拆一个函数方便后面做 Web 接口时复用。3.2 初始化 GFPGAN 修复器关键参数的含义GFPGAN 的GFPGANer类是所有神经网络黑盒子的封装点初始化参数决定了用哪一版模型、放大几倍、要不要额外做背景增强。import torch from gfpgan import GFPGANer def init_restorer(args): if not torch.cuda.is_available(): print(警告当前没有可用GPUCPU推理速度会非常慢) restorer GFPGANer( model_pathargs.model_path, upscaleargs.upscale, archclean, # v1.4模型对应 clean 架构 channel_multiplier2, # 通道数系数v1.4固定为2 bg_upsamplerNone, # 背景增强器None表示不做 ) return restorer这里的archclean是最容易翻车的地方。GFPGAN 早期版本的模型用的是original架构v1.4 之后官方把生成器改成clean架构两者的网络层配置不一样。你如果拿 v1.4 的权重去配archoriginal会直接报state_dict加载失败。channel_multiplier也是同理v1.4 模型的通道系数是 2如果误写成 1虽然能加载但输出图片会有一层灰蒙蒙的伪影。bg_upsamplerNone在本阶段是最可控的选择。它不增强背景部分只处理和贴回人脸区域避免把背景里的颗粒噪声也放大。如果你想连背景一起修复后面会讲到怎么接realesrgan但代价是单张图片的耗时至少翻倍。3.3 修复单张图片预处理与后处理如果把插件的所有花哨功能抛掉核心的单图修复代码就下面这些但它解决了一个非常隐蔽的问题输入输出图像的通道顺序。import cv2 def fix_one(restorer, img_path, strength0.6): 修复单张老照片返回修复后的BGR图像 img cv2.imread(img_path, cv2.IMREAD_COLOR) if img is None: raise ValueError(f无法读取图片: {img_path}) # 记录原图尺寸后面检查修复结果尺寸是否有变化 h, w img.shape[:2] # enhance返回三个对象裁剪脸、恢复脸、最终贴回图 _, _, restored restorer.enhance( img, has_alignedFalse, only_center_faceFalse, paste_backTrue, weightstrength, ) if restored is None: print(f注意{img_path} 未检测到人脸返回原图) restored img return restoredenhance是 GFPGAN 的推理入口内部的流程是先用 RetinaFace 检测人脸把检测到的人脸裁剪、对齐、缩放到模型输入尺寸然后跑一次生成器得到修复后的人脸最后根据paste_back参数决定要不要把修复脸贴回原图的对应位置。这里的关键是weight参数它控制生成器的输出占比相当于一个人工“复原程度”旋钮。has_alignedFalse表示输入的是普通照片需要内部重新检测对齐如果你自己已经裁剪过只有人脸区域的图可以传True跳过检测速度更快但必须保证人脸已经对齐且居中。paste_backTrue是必须开的特别是在老照片中背景的折痕和噪点如果也被生成器处理会丢失大量原始质感开这个参数才能只换脸、留背景。注意返回值有三个我们只取第三个restored。前两个分别是裁剪出的人脸和独立修复后的人脸它们用于调试和人脸对比普通修复流程用不上。restored是完整的输出图像通道顺序是 BGR直接用cv2.imwrite保存不会偏色。3.4 批量循环与文件保存实际拿回家整理老照片不可能一张一张敲命令。我一般会让脚本自动扫描目录下所有图片并给输出文件加上参数后缀避免同一张照片用不同参数修了两遍后互相覆盖。import os def batch_fix(restorer, input_path, output_dir, strength): os.makedirs(output_dir, exist_okTrue) if os.path.isfile(input_path): files [input_path] else: files [ os.path.join(input_path, f) for f in os.listdir(input_path) if f.lower().endswith((.jpg, .jpeg, .png, .bmp)) ] if not files: raise ValueError(f输入目录中没有找到图片: {input_path}) for idx, file_path in enumerate(files): try: restored fix_one(restorer, file_path, strength) base os.path.splitext(os.path.basename(file_path))[0] out_name f{base}_gfpgan_s{strength}.png out_path os.path.join(output_dir, out_name) cv2.imwrite(out_path, restored) print(f[{idx 1}/{len(files)}] 完成: {out_path}) except Exception as e: print(f处理 {file_path} 出错: {e})批量循环里最值得注意的有两个点。第一输出固定用 PNG 格式因为老照片修复后的图片包含更多过渡色阶JPG 压缩会在边缘引入新的振铃噪声PNG 是无损压缩适合做中间结果。第二每个文件的处理都包了一层try/except这不是为了掩盖错误而是防止批量任务运行到一半因为单张损坏图片中断导致后面前功尽弃。批处理最忌“单张失败、全盘停摆”。4. 修复效果调优的五个必调参数让老照片从能用到可用4.1 upscale放大倍率与老照片分辨率的匹配--upscale控制最终输出相对原图放大的倍数默认 2很多人一上来就拉到 4。如果你修的是几百像素的证件照4 倍有意义但大部分老照片是扫描件分辨率少说有两三千像素再放大 4 倍不仅慢还会把背景里的纸张纹理放大成“磨砂滤镜”。我的建议对 1500 像素以上的图先用 2 倍看效果对 500 像素以下的缩略图可以上 4 倍。调这个参数不需要重新训练模型直接改命令行参数即可。4.2 strength修复强度如何避免“网红AI脸”strength是 enhance 函数里的weight值的范围是 0 到 1。0 表示完全不用生成器只保留原图1 表示完全用生成器重建人脸。很多老照片修复教程会默认 0.5 到 0.8但现实中修自家扫描的老照片0.6 已经会让原来有特征的国字脸变成窄下巴的“AI 网红脸”。# 参数扫描同一张图让strength在0.3~0.8之间出多张结果 for strength in [0.3, 0.5, 0.6, 0.8]: _, _, restored restorer.enhance( img, has_alignedFalse, only_center_faceFalse, paste_backTrue, weightstrength, ) cv2.imwrite(fretry_s{strength}.png, restored)这段扫描代码是调参的“后悔药”。把同一张照片用 4 个强度各跑一遍用照片查看器标清序号肉眼对比哪张最像原图里的亲人。我自己的经验是折痕和污点严重的图0.8 能把污点彻底清掉但会丢掉肤质纹理只想去噪、保真0.4 足够。strength没有最优值它取决于你对“像不像本人”的容忍程度。4.3 face_size人脸对齐尺寸与细节量GFPGANer内部会把检测到的人脸区域裁剪后缩放到face_size × face_size再送入生成器。face_size越大生成器看到的细节越多但前提是裁剪出来的人脸区域本身清晰。老照片人脸通常只有两三百像素硬放大到 1024 反而会让生成器“脑补”出并不存在的皮肤毛孔出现磨皮过度的塑料感。用 512 就够了这是 GFPGAN 官方推理脚本的默认语义。如果你发现修复后眼睛经常变成“熊猫眼”把face_size从 512 降到 384 往往能改善。4.4 bg_upsampler要不要开启背景增强bg_upsampler参数在初始化时传入最常见的是传入realesrgan的 RealESRGANer 实例。开了之后背景区域也会被超分增强整张照片看起来更清晰。但这个参数有代价单张图的推理时间从 2 秒变成 8 秒以上GPU 上而且对满是噪声的老照片背景超分算法会把噪点当成细节来强化结果产生密密麻麻的伪细节。我一般只在人脸占画面比例超过三分之一、且背景本身相对干净的照片上开bg_upsampler。如果只是想快速给全家人看效果保持None是更稳的起点。4.5 only_center_face多人合影时的选择与风险GFPGAN 默认会检测并修复照片里的所有人脸。但在多人合影里边缘的人脸经常被鼻子、手部遮挡检测器会漏检或误检反而留下一张没处理的脸在旁边对比感很差。把enhance的only_center_faceTrue打开后只处理画面中心最大的人脸适合单人照偏多的场景。python main.py --input inputs/1978_family.png --output outputs --strength 0.5 --upscale 2如果遇到多人合影建议先跑一次默认参数然后看输出图里漏了哪张脸。漏脸时就只能用更精细的折叠方法先裁剪出每个单人脸区域分别修复再拼回去。这个方案确实麻烦但对大合照来说比修正单个整图参数更可靠。但注意only_center_faceTrue对只有一个人的老照片没有任何副作用可以放心用。5. GFPGAN常见问题排查四种最容易翻车的场景及对策5.1 现象修复后的人脸五官变形严重原因strength设得过高生成器把原图人脸特征当噪声丢掉直接按照“平均脸”重建了一个新脸或者face_size设置过大导致人脸区域被过度放大后局部特征失真。解决先把strength降到 0.4 重新跑同一张图观察眼睛距离、颧骨轮廓是否接近原图。如果还变形把face_size从默认 512 改为 384。我在修 1970 年代黑白照片时发现这种照片的扫描图人脸通常只有 200 到 300 像素512 的输入其实是在强行补一个不存在的清晰度所以调低face_size反而更安全。5.2 现象输出图的颜色与原图差异很大偏红或偏青原因GFPGAN 内部处理的是 BGR 图像但在调试时很多人习惯用 matplotlib 或 PIL 显示。如果你用 PIL 的Image.fromarray(restored)直接展示PIL 会默认把 BGR 数组当成 RGB于是人脸就偏蓝。另外老照片本身有严重的泛黄调性修复后的皮肤色更粉嫩视觉上会觉得颜色“不对”。解决显示时先做一次通道转换cv2.cvtColor(restored, cv2.COLOR_BGR2RGB)。保存时不要转直接用cv2.imwrite写 BGR 原数组这样得到的 PNG 用看图软件打开颜色是正常的。如果确认通道没写错还是偏色那就是白平衡问题可先用cv2.xphoto.createSimpleWB()对原图做一次白平衡再送入修复但这属于高级调参先用通道问题排查。5.3 现象批量修复时内存溢出或速度越来越慢原因输入图片分辨率过大。扫描仪导出的老照片动辄 6000×9000 像素GFPGANer会先把整张图读进内存做人脸检测批量任务中一张大图就能占满 8GB 显存。如果开了upscale4显存占用更是成倍增长。解决在送入enhance之前先把图片最长边限制到 2000 像素修复完成后再用传统插值放大到所需尺寸这样模型只需处理一个合适的分辨率。MAX_EDGE 2000 img cv2.imread(path) h, w img.shape[:2] scale min(1.0, MAX_EDGE / max(h, w)) if scale 1.0: img cv2.resize(img, (int(w * scale), int(h * scale)), interpolationcv2.INTER_AREA)这段代码里的INTER_AREA是下采样时最常用的插值方式能减少摩尔纹。批量任务前先统计所有图的最长边把超大图统一压到 2000 内速度问题基本就能解决。5.4 现象人脸没有被检测到代码直接返回原图原因一张老照片里人脸区域太小、太糊或者人脸是侧脸、低头姿势。RetinaFace 检测器对“正脸且高度大于 32 像素”的目标比较敏感而为扫描压缩后的老照片人脸宽度常常不足 50 像素检测器直接漏过。解决不要急着调 GFPGAN 参数先把原图放大 2 倍再跑修复同时对侧脸照片做一次 90° 旋转再试。如果放大后仍检测不到可以用facexlib底层的检测器画出人脸框确认框的位置再决定是不是要把图片局部裁剪后单独修。6. 把修复从脚本变成工具Web调用与接口封装的小技巧6.1 用 Flask 封装一个修复接口如果只是自己修几张照片命令行脚本已经够用。一旦要让不懂 Python 的家人使用或者接入一个小程序后台就得把它封装成 HTTP 接口。我一般用 Flask 写一个最小可用的服务import io import numpy as np from flask import Flask, request, send_file app Flask(__name__) restorer None def init_global_restorer(model_path): global restorer if restorer is None: restorer init_restorer(model_path) return restorer app.route(/fix, methods[POST]) def fix_image(): photo request.files.get(photo) if photo is None: return {error: 缺少 photo 文件}, 400 img_bytes photo.read() img_array np.frombuffer(img_bytes, np.uint8) img cv2.imdecode(img_array, cv2.IMREAD_COLOR) _, _, restored restorer.enhance( img, has_alignedFalse, only_center_faceFalse, paste_backTrue, weight0.6, ) ok, buffer cv2.imencode(.png, restored) if not ok: return {error: 图像编码失败}, 500 return send_file(io.BytesIO(buffer.tobytes()), mimetypeimage/png, download_namerestored.png)接口里的restorer必须用全局变量持有不要在每次请求时初始化。GFPGAN 的模型加载需要读入 300MB 权重单次初始化要花 5 到 10 秒如果每个请求都重新加载一次服务根本没法用。cv2.imdecode能把上传的字节流直接解码成图像数组比先保存临时文件再读取要干净得多。实际部署时建议在服务启动前先调用一次init_global_restorer做预热再对外提供服务。6.2 两个验证技巧先看局部贴脸再算定量指标封装完成后不能只靠用户说“好像变年轻了”来验收。老照片修复没有真实的高清参考图所以我的习惯是先做局部验证用facexlib检测出人脸框把修复前后的两张图的同一区域放大到 200%对比眼角纹理和牙齿轮廓。如果人脸轮廓与原图一对比差太多说明strength还是太高。定量方面可以算 PSNR但因为处理和原图不是同一分辨率直接算 PSNR 没有参考意义。更实用的是把修复结果重新压缩成 JPG再和原图做结构相似度 SSIM虽然不能用来证明“修得好”至少能衡量本次修复是否保留了原始构图结构。我自己在项目里加了一个--compare参数输出修复图和原图的 SSIM 值低于 0.6 就提示人工复核。6.3 保留 EXIF 与时间戳给修复留一份后悔药最后一个小技巧老照片扫描件可能带了拍摄时间和扫描参数等 EXIF 信息而 GFPGAN 输出图像不会自动保留。我建议在保存时从原图读取 EXIF 写入输出文件或者至少把原始文件名、修复参数写进一个manifest.csv。这样三个月后想换一套参数重新修复还能知道上次用的是什么strength不用靠记忆猜。我在一次给客户做批量老照片修复时就是因为忘了记录参数发现同一张图被两个批次用不同强度处理交给客户时新老照片分不清最后只好重跑全部批次。从那以后每张输出图的名字里都带上了s和up参数这也成了我的固定习惯。这个方案并不复杂但它能让你以后翻看结果时少走很多弯路。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?