简介:本资源是一套基于PyTorch实现的端到端图像修复系统源码,面向计算机视觉方向的学习者、算法工程师及深度学习实践者,聚焦于图像缺失区域的智能重建问题,适用于科研复现、课程设计与工业级图像编辑工具开发等场景。压缩包共86个文件,涵盖9个核心Python脚本(含训练train.py、测试test.py、FastAPI后端app.py及模型定义networks.py)、19张示例图像与输出结果图、14个前端样式CSS文件、21个交互逻辑JS文件,以及配置文件(YAML/JSON)和文档(MD/README),整体体积3.74MB,结构清晰分为模型、数据、前后端与配置四大模块。已有328人下载学习,用户可直接运行Web服务上传图像与掩膜完成在线修复,复现GAN训练流程,调用多种预训练模型对比效果,并借助TensorBoard日志分析训练过程,具备完整工程闭环与即插即用特性。 图像修复这些年一直是计算机视觉里特别接地气的方向,老照片划痕修复、破损文物数字化复原、视频去水印、监控画面遮挡补全,背后用的都是同一套技术逻辑。PyTorch 生态成熟之后,这个领域的入门门槛已经降了很多,一个能跑通完整训练和推理流程的源码项目,对新手和想快速落地算法的工程师来说都是极好的学习样本。
这篇博文就以“(源码)基于PyTorch的图像修复系统.zip”这个项目为主线,从技术选型、源码结构、环境搭建、训练推理到踩坑实录,完整拆解一个图像修复系统从零到一的全过程。
1. 图像修复系统整体设计与技术选型
1.1 图像修复到底在解决什么问题
图像修复(Image Inpainting)本质上是一个“根据已知像素推断未知区域”的任务。用人话说就是,图片上有一块区域缺失了、损坏了、或者你不想要了,算法需要根据周围像素的纹理、结构、颜色信息,把这块区域“脑补”出来,而且要让补出来的部分跟原图浑然一体,看不出修补痕迹。
这个任务难在哪?它不是简单的插值。比如一块纯色墙壁上的小污点,用photoshop的仿制图章就能搞定;但如果破损区域跨过了物体的边缘、包含复杂的纹理结构(比如草地、毛发、水面波纹),算法就需要同时理解“这张图里有什么物体”“物体之间是什么空间关系”“这块区域的纹理走向是什么”,这就从像素级问题上升到了语义级问题。
数学上,图像修复是一个典型的病态逆问题(ill-posed inverse problem)。同一个破损区域,可能存在无数种合理的补全结果,算法需要从中选出“最合理”的那一个。所谓“最合理”,通常由两部分约束决定:一是重建损失,要求补出来的像素与真实内容尽可能接近;二是感知损失或对抗损失,要求补出来的区域在整体风格、纹理、结构上与周围环境保持一致。这两者往往存在张力,如何平衡,就是各种修复模型的核心差异所在。
1.2 为什么选 PyTorch 而不选 TensorFlow
现在做图像修复方向,PyTorch 基本是默认选项,原因很实际。
第一,学术生态的倾斜。目前主流修复模型的开源实现(如 PartialConv、EdgeConnect、LaMa、Repaint 等)绝大多数都是 PyTorch 版本,复现论文、加载预训练权重、对照实验结果都非常方便。TensorFlow 虽然也有一版实现,但版本兼容问题多,一些老模型还停留在 TF1.x,用起来相当痛苦。
第二,动态图机制对研究型任务更友好。图像修复模型的结构往往不是简单的串行网络,生成器里通常有跳跃连接、多尺度特征融合、注意力模块等复杂结构,PyTorch 的动态计算图让你可以在 forward 过程中随意打印、修改中间张量,调试体验比静态图好太多。
第三,生态工具链完整。PyTorch 官方提供的 torchvision 里有大量预训练骨干网络,可以直接当编码器用;配合 Albumentations 做数据增强、HuggingFace 的 diffusers 做扩散模型推理,整套流程衔接非常顺滑。
当然,如果你的目标是移动端或嵌入式部署,TensorFlow Lite 或 ONNX Runtime 会是更合适的中间层,但那是后话。做研究和原型验证阶段,PyTorch 的效率和灵活度确实无可替代。
1.3 项目源码的整体架构与模块划分
拿到项目源码后,第一步不是急着点开 train.py 就跑,而是先看目录结构。一个规范的图像修复项目源码,通常会有清晰的模块划分。我习惯先把项目的整体架构图在脑子里过一遍,再开始逐个文件精读。
这个项目的目录结构大致是这样的:
inpainting_system/ ├── configs/ # 配置文件目录 │ └── default.yaml # 默认参数配置文件 ├── datasets/ # 数据相关模块 │ ├── __init__.py │ ├── base_dataset.py # 数据集基类,定义数据加载协议 │ ├── inpainting_dataset.py # 图像修复专用数据集,负责掩码生成 │ └── transforms.py # 数据增强与预处理 ├── models/ # 模型定义 │ ├── __init__.py │ ├── generator.py # 生成器网络结构(核心) │ ├── discriminator.py # 判别器网络结构 │ ├── losses.py # 损失函数定义 │ └── inpainting_model.py # 模型封装,整合生成器、判别器和损失 ├── tools/ # 工具脚本 │ ├── train.py # 训练入口脚本 │ ├── test.py # 测试推理脚本 │ └── infer.py # 单张图片推理脚本 └── utils/ # 工具函数 ├── logger.py # 日志记录 ├── metrics.py # 评估指标(PSNR/SSIM) └── visualizer.py # 可视化工具这套结构的核心思路是把“数据”“模型”“训练逻辑”三者解耦。数据管数据的加载和预处理,模型管网络结构和前向传播,训练逻辑管优化器、学习率调度、损失计算。这样当你需要换数据集、换 backbone、调损失权重时,只需要改动对应模块,而不必把整个脚本翻个底朝天。
2. 核心原理与代码实现深度解读
2.1 生成器网络:从 U-Net 到注意力机制
图像修复模型的生成器,绝大多数都是基于 U-Net 的变体。为什么 U-Net 这么适合图像修复?原因在于它的“编码-解码”结构和跳跃连接(skip connection)。
编码器负责逐步下采样,提取越来越抽象的特征图,比如浅层特征关注边缘、纹理、颜色,深层特征关注物体类别、场景语义。解码器负责把抽象特征逐步上采样回原图分辨率。如果只有这个单向流程,解码器在恢复细节时会丢失大量的浅层空间信息,所以 U-Net 加了跳跃连接,把编码器每一层的特征图直接拼接到解码器对应的层上,相当于给解码器提供了一份“尺子”和“参考图”,让它知道原始分辨率下哪里有边缘、哪里有特定纹理。
在图像修复任务里,跳跃连接还有一层特殊含义:破损区域的像素是无效的,如果直接把这些无效特征也拼接到解码器,会引入“污染”。所以后来的改进模型(比如 Partial Convolution)设计了一个掩码感知机制,在卷积计算时只对有效区域做操作,并通过权重归一化自动更新掩码。这个细节是这个项目源码里的核心亮点之一,我看到生成器里对掩码特征做了专门的编码与融合,而不是简单把掩码作为额外通道拼进去。
此外,项目在解码器的中间层引入了自注意力模块。这有什么作用?普通卷积的感受野是有限的,即便下采样到很深的层级,也只能关注到局部区域。但图像修复往往需要“远距离参考”,比如修复一张带有大面积规则纹理(砖墙、织物)的图片,破损区域左侧的纹理模式可能和右侧高度相关,普通卷积很难感知到这种远距离相关性。自注意力机制允许模型在全图范围内寻找相似的特征块,把远处的有效纹理信息“搬运”到破损区域附近。代价是计算量增大,所以项目里只在几个特定的低分辨率特征层上加了注意力,平衡了效果和速度。
2.2 判别器与对抗损失:让修复结果“以假乱真”
只有重建损失(比如 L1 Loss)的修复模型,补出来的区域通常比较模糊。原因很简单,L1 Loss 是像素级一一对应的约束,它倾向于取所有可能结果的“平均值”,而平均值在视觉上就是平滑和模糊的。这在数学上是不可避免的,因为一个像素位置上有多种合理的预测,最小化 L1 距离的最优解是这些预测的中位数,中位数天然趋向于平滑。
为了解决这个问题,源码引入了对抗训练。判别器的任务很简单:给它一张图,判断这个是“原图”还是“修复图”。生成器的任务则升级了:不仅要让修复区域在像素上接近真实,还要骗过判别器,让它看不出是假的。
这里有一个关键设计:判别器不是对整张图输出一个真/假判断,而是对图像的一个个小 patch 分别判断(PatchGAN 的思想)。为什么用 PatchGAN?因为图像修复最怕的是局部纹理不自然、边缘生硬,PatchGAN 把注意力集中在局部区域的真实性上,能有效提升修复区域的纹理质感。而且 PatchGAN 的参数比整图判别器少得多,训练起来更稳定,不易发生模式崩溃。
对抗损失的使用还需要配合一个“分阶段策略”。如果一开始就用对抗损失,生成器可能只顾着骗判别器,忽略了基本的重建精度,导致训练不稳定。所以训练时会先让生成器单独跑几百轮,学好基本的重建能力,再开启对抗训练,这时候生成器已经有了一定的“底子”,对抗训练才能在细节层面进一步打磨。这个项目源码里的训练脚本就是按这个思路设计的,初始阶段将对抗损失权重置零,迭代到一定步数之后再逐步增大,我在复现时保留了这一策略,实测对训练稳定性帮助很大。
2.3 损失函数组合:L1、感知损失与风格损失
项目源码里定义的损失函数不是单一一项,而是多项目标的组合。核心有三块:
第一项是 L1 重建损失。它的作用是把修复结果和真实图在像素层面拉近。为什么不选 L2(MSE)?因为 L2 对离群点(异常亮暗的像素)的惩罚是平方级的,会导致修复结果偏暗、偏保守,对噪声也更敏感。L1 对离群点的惩罚是线性的,鲁棒性更好。实测中 L1 训练的模型,在 PSNR 和 SSIM 指标上的表现通常好于 L2。
第二项是感知损失(Perceptual Loss)。它不直接在像素空间比较,而是把真实图和修复图分别送入一个预训练好的 VGG16 网络,提取中间层的特征图,再计算特征图之间的 L1 距离。感知损失的本质是比较“两张图在语义层面有多像”。比如两块纹理细节不同但整体轮廓一致的图,像素级损失会认为它们差别很大,但感知损失会认为它们很接近。修复任务中引入感知损失,能让修复区域在“看得见的特征”上与周围更协调,而不是仅仅在逐像素上对齐。
第三项是风格损失(Style Loss),用 Gram 矩阵计算特征图之间的相关性。它关心的是整幅图像的纹理风格是否统一,比如修复区域补出来的草地纹理和周围草地的疏密程度是否一致。风格损失可以很好地防止修复区域出现“违和的纹理突变”。
这三项损失的权重设置很重要,源码里默认的比例是 L1 损失权重为 1.0,感知损失权重为 0.05,风格损失权重为 120.0。看着风格损失权重很大,但它的梯度量级本身比较小,实际影响并不会盖过 L1 损失,这个需要实际训练时对照 loss 曲线来调,不能只看权重数值。
3. 环境搭建与数据准备
3.1 PyTorch 环境配置与 CUDA 版本匹配
拿到源码后第一关就是环境搭建。很多初学者在这里就被劝退了,其实只要理解版本对应的逻辑,这一步并不复杂。
先说结论:PyTorch 的 CUDA 版本要求和本机的显卡驱动版本不是一回事。显卡驱动负责底层硬件通信,CUDA Toolkit 是计算平台,PyTorch 内置了一部分 CUDA 运行库。你需要做的是先看显卡驱动能支持的最高 CUDA 版本,然后选择一个不高于这个版本的 PyTorch CUDA 版本。
判断方法很简单,命令行输入nvidia-smi,右上角会显示CUDA Version: xx.x,这表示你的驱动最高支持这么高的 CUDA 版本。然后去 PyTorch 官网选择对应版本的安装命令。比如驱动支持 CUDA 12.1,就能直接安装默认的 PyTorch 版本;如果驱动还停留在 11.8 的老驱动,就需要手动指定安装 CUDA 11.8 的 PyTorch 版本。
使用 conda 创建独立环境是最稳妥的做法,避免把系统 Python 环境搞乱:
conda create -n inpaint python=3.10 conda activate inpaint # 以 CUDA 12.1 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121安装完之后一定要验证 CUDA 是否可用:在 Python 里执行import torch; print(torch.cuda.is_available()),返回 True 才算成功。如果返回 False,大概率是版本不匹配,不要急着往下走,先回头查驱动版本。
其他依赖就比较常规了:opencv-python(图像读写与基本处理)、numpy、tqdm(进度条)、tensorboard(训练可视化)、pyyaml(读取配置文件)。建议直接装好,省得训练到一半报 ModuleNotFoundError。
3.2 数据集的获取与预处理流程
图像修复训练需要成对的数据:原始完整图片 + 带有掩码的破损图。这里掩码是核心,它决定了要修复哪些区域。
如果使用公开数据集,以 Places2 为例,它有超过 800 万张场景图片,覆盖面广,是训练图像修复模型的经典选择。但完整下载需要几十 GB 空间,对个人开发者来说有点大。更轻量的方案是使用 Places365 标准版,或者用 COCO、ImageNet 的子集。还有一个实际经验:如果是做老照片修复方向的垂直应用,用真实的老照片数据集(如一些公开的历史照片集)训练,效果会比通用数据集好很多。数据集领域的“领域适配”问题在图像修复上非常明显,用自然风景训练的模型去修人像照片,效果会打折扣。
源码里的数据预处理流程有两个关键点。第一是图片统一缩放,训练时随机裁剪到 256x256,避免模型见过大的图导致显存爆炸。第二是掩码生成策略,项目提供了多种掩码类型:不规则块状掩码(模拟划痕和污渍)、矩形掩码(模拟物体遮挡)、以及从掩码库中随机采样的方式。训练时每次随机选一种掩码生成方式,并随机掩掉 10% 到 40% 的区域,这样模型能看到多样化的破损模式,泛化能力更强。
这里有一个很关键的细节:掩码不能只在固定位置生成,那样模型会过拟合到“只修复中心区域”。源码里每次训练迭代都会重新生成随机掩码,而且掩码的位置、形状、大小都是随机的,保证模型学习到的是“任意位置任意形状的修复能力”,而不是“某个固定位置的修复能力”。
4. 训练流程及关键参数解析
4.1 训练脚本的执行流程
训练入口是tools/train.py,整个脚本的执行流程可以概括为以下几个阶段。
第一阶段是初始化配置。脚本读取configs/default.yaml,把里面的参数记录到配置对象中,包括数据路径、图片尺寸、batch size、学习率、训练总轮数、损失权重等。配置文件的引入很重要,它不是把参数写死在代码里,而是以 YAML 这种人类可读的格式集中管理,换一套参数只需要改配置文件,不需要动代码。
第二阶段是数据加载。inpainting_dataset.py负责从路径中读图片、做随机裁剪和翻转、生成随机掩码,最后返回一个字典,包含原图、掩码图、破损图。这里的破损图就是用原图逐像素乘上掩码的“反向遮罩”,把需要修复的区域置零。数据加载用 PyTorch 的DataLoader,设置合适的num_workers(数据加载线程数),让数据读取能够和 GPU 计算并发执行,不至于让 GPU 空转等数据。
第三阶段是模型初始化。生成器、判别器分别创建,根据配置文件上的resume字段决定是从预训练权重继续训练还是从头开始。优化器选用 Adam,判别器和生成器分别配置了不同的学习率,生成器是 0.0001,判别器是 0.0004。这个 1:4 的比例不是随意的,判别器学习率更高,可以帮助其更快地分辨“真伪”,而生成器以较慢的学习率稳定更新,避免对抗训练陷入震荡。
第四阶段是迭代训练主循环。每一步先从 DataLoader 拿一批数据,前向传播得到修复结果,然后按顺序更新判别器和生成器。核心是损失函数的反向传播和优化器 step。训练过程中会周期性打印 loss 指标,并保存 checkpoint。
4.2 生成器与判别器的训练顺序
对抗训练有一个常见的坑:判别器太强或太弱都会导致训练失败。判别器太强,生成器的梯度信息会被“压制”得很厉害,损失降不下去;判别器太弱,生成器就会“飘”,生成的图片有模式重复的风险。
所以源码里对两个网络的训练方式是交替更新的,每一步先训练判别器,让判别器在当前生成器状态下区分真图和假图的能力有所提升,再训练生成器,让生成器根据判别器的最新反馈去优化自己的输出。两者相互追赶,形成稳定的动态博弈。
具体实现上还有一个平衡小细节:判别器使用的是“铰链损失”(Hinge Loss),当判别器对假图的判断已经非常准确(即已经能轻松识别出假图)时,它的梯度就会变小,给生成器留下的学习压力不会无限大。铰链损失这种“区分度已足够时就减少反馈”的特性,比原始的 BCELoss 在对抗训练中更稳定,这也是很多 GAN 类模型的实践经验。
4.3 学习率调度与训练轮数的经验值
项目默认训练 200 个 epoch,但实际训练很少需要跑满。我在测试中观察到,到 50 个 epoch 左右模型就已经有不错的修复效果,到 100 个 epoch 时质量趋于稳定。继续训练的好处在于,模型对复杂掩码和低概率边缘情况的适应会更好,坏处是训练时间长,而且有可能开始过拟合训练集的纹理模式,导致在测试集上的泛化能力下降。
学习率调度采用余弦退火(Cosine Annealing),它让学习率在整个训练过程中从初始值平滑下降到接近 0,比固定学习率更灵活。前期较大的学习率能快速下降损失,后期小学习率能帮助收敛到更平缓的局部最优解。我实际测试中,用固定学习率跑,后期 loss 会有小幅震荡;用余弦退火,loss 曲线更平滑,最终 PSNR 能提高约 0.5 到 1 个 dB,差距不算特别大,但也不必额外费力。
训练时 batch size 的设置要考虑到显存限制。256x256 分辨率下,生成器和判别器同时训练,batch size 设为 16 比较合理,显存占用大约在 10 到 12 GB。如果显存只有 8 GB,把 batch size 降到 8,同时把图片裁剪尺寸降到 224,也可以正常训练。需要注意的是,图像修复模型对 batch size 的敏感度不如分类任务那么高,小幅缩小不会带来明显的效果退化。
还有一个细节:梯度累积。如果单卡显存实在受限,可以在多个 step 里累积梯度,然后统一更新一次参数,相当于变相扩大 batch size。源码里没内置这个功能,但我实际加过,逻辑很简单,就是判断step % accumulation_steps == 0时再执行 optimizer.step() 和 zero_grad()。
5. 测试推理与模型评估
5.1 单张图片推理流程
推理脚本比训练脚本简单得多。流程是:加载配置文件、加载预训练权重、读取图片并做预处理、前向传播得到修复结果、后处理保存。
预处理这一步需要重点说明。模型训练时把所有图片归一化到了 [-1, 1] 区间,推理时也要做同样的归一化,不然模型的输入分布和训练时不一致,效果会明显下降。很多人在推理时效果差,查了半天发现自己忘了这一步。
推理时还需要提供掩码图(或通过鼠标交互绘制掩码区域)。这里有一个实用细节:掩码区域不是只能输入二值图。项目源码的infer.py支持两种输入方式,一种是直接传入图像方式生成固定掩码,另一种是读取与图片同名的掩码文件。对做应用的人来说,支持交互式涂抹掩码会更方便,但那样需要引入 GUI,项目为了保持轻量,没有实现这一层。
后处理要做的是把输出张量从 [-1, 1] 反归一化回 [0, 255],并转换成 8 位整数图像保存。另外,如果掩码区域之外的部分也被模型“顺手”修了,可以通过一个可选后处理把掩码之外的像素恢复为原始值,保证只有目标修复区域被改动。这个技巧在实际项目中很有用,比如你只想修复某个划痕,不希望模型动到图片的其他部分。
5.2 评估指标 PSNR 与 SSIM 怎么用
评估修复效果,源码里提供了两个经典指标。
PSNR(峰值信噪比)衡量的是像素级差异。它的大小取决于最大像素值和均方误差的比值。PSNR 越高说明像素级误差越小,但 PSNR 有个众所周知的局限:它对结构信息不敏感。一张稍微偏移了几个像素的图片,PSNR 会大幅下降,但人眼看着并没有多大区别。因此 PSNR 只能作为基础参考,不能作为唯一指标。
SSIM(结构相似性)从亮度、对比度、结构三个维度对比两张图片。它的取值范围是 -1 到 1,越接近 1 说明结构越相似。SSIM 的窗口机制让它能够捕捉局部结构的相似性,与人眼感知的吻合度比 PSNR 好得多。比如修复结果虽然像素上有几处偏差,但只要整体纹理、边缘结构对了,SSIM 依然可以给出不错的分数。
实际评估时,我建议两个指标一起看,两者都没有明显短板的时候,模型综合表现大概率是好的。另外,最好每次用同一组固定掩码在同一个测试集上评估,这样不同模型之间的对比才有意义。如果掩码每次都是随机的,那不同实验之间根本没法公平比较。
5.3 从“能修”到“修得好”的进阶技巧
项目源码默认配置下,模型基本能做到“破损区域被填充且颜色过渡自然”的程度,但要说达到商用级的老照片修复效果,还有差距。这里分享几个实测有效的改进方向。
第一,加入引导修复。在输入中引入灰度图或边缘图作为辅助条件,可以显著提升修复结果的结构完整性。边缘信息相当于给模型画了一幅“骨架”,让模型知道破损区域里应该有什么物体的轮廓,然后只负责填充纹理细节。这种方式在修复人像时效果格外好,眼睛、嘴巴等关键结构不会跑偏。
第二,多尺度修复。处理大面积破损时,一个常见的短板是全局结构把握不住。先用低分辨率版本快速估算破损区域的整体内容,再逐步细化到高分辨率,这是经典的从粗到细(coarse-to-fine)策略。很多商用修复软件背后就是这套逻辑。
第三,针对特定语义类别的专用模型。如果是做人像修复,用专门的人像数据集(如 FFHQ)微调后的模型,效果会远好于在通用场景数据集上训练的模型。老照片修复尤其如此,那个年代的照片有特殊的胶片颗粒、灰度分布、褪色规律,通用模型很难学好这些特有的退化模式。
6. 代码扩展与下游应用
6.1 模型导出与部署思路
训练好的模型如果想脱离训练环境运行,需要先做模型导出。PyTorch 模型打包成 TorchScript,或者导出为 ONNX 格式,都能在不依赖 torch 和模型类定义的情况下运行推理。
这里有一个关键问题:生成器的 forward 函数里包含掩码处理逻辑,导出时需要对输入做特殊处理,把掩码和破损图拼成一个四通道输入(RGB+掩码),或者把掩码作为额外输入参数。我在项目里建议用后一种方式,因为对 ONNX 来说,多输入比多通道更容易做动态尺寸适配。
导出 ONNX 后,可以通过 ONNX Runtime 在 CPU 上推理,速度比 PyTorch 原生推理快不少,也能直接在 C++、Java 等环境下调用。如果你的目标是浏览器端应用,还可以把模型导出为 WebGL 或 WASM 可运行的格式,Ofelia 等框架可以直接读取 ONNX 模型。实测在浏览器端跑 256x256 的修复,普通笔记本也能做到 1 到 2 秒一张,体验已经可以接受。
6.2 边缘计算设备上的轻量化改造
如果你想把模型部署到 Jetson 等边缘计算设备上,需要考虑轻量化改造。Jetson 平台对 PyTorch 的适配已经比较成熟,JetPack SDK 中内置了适配对应 L4T 版本的 PyTorch 轮子包,安装时需要注意版本匹配。
轻量化改造有几个方向。首先是通道裁剪。生成器的中间层通道数是决定模型体积和推理延迟的最大因素,把 256 通道改到 128,甚至 64,模型体积缩小数倍,推理速度提升明显。代价是纹理恢复能力有所下降,但具体影响多大要看设备算力,实测在 Jetson Orin Nano 上,裁剪后的模型推理一张 256x256 图像能控制在 300 毫秒以内。
其次是量化。把 FP32 权重量化到 FP16 或 INT8,对边缘设备能带来较大的性能收益。FP16 在 Jetson 上的支持很完整,INT8 需要额外的校准数据,但效果更明显。
6.3 老照片修复应用的完整串联方案
这个系统最有价值的落地场景之一,就是老照片修复。把图片修复模型放到一个完整的老照片修复工作流中,效果比单模型好很多。完整流程应该是:先做图像增强(去噪、对比度拉伸),再做划痕检测,自动生成掩码,然后送入修复模型,最后做色彩校正。
划痕检测这一步很关键,但训练一个划痕检测模型又需要额外标注数据。一个更轻量的做法是使用传统图像处理手段,比如以连通域分析为基础做简单的划痕候选区域提取,然后让用户确认。虽然不如端到端检测模型精准,但胜在零成本、可解释性强。我实际做过一个 demo,先做灰度化、中值滤波、差分图二值化,再用连通域筛选掉小面积噪点,就能把大部分典型划痕找出来,修复效果已经足够让用户满意。
更高阶的玩法是把修复模型与扩散模型串联,先用图像修复模型补全结构和纹理,再用 Stable Diffusion 等模型做高清放大和细节增强。修复模型保证了内容结构的正确性,扩散模型则在细节质感上做进一步提升,两者各司其职,效果比单用任何一个都强很多。
7. 常见问题与排查技巧实录
7.1 训练时 loss 为 NaN
这是最常见的训练事故,排查起来其实有套路。优先检查学习率是不是设得太高了。Adam 优化器默认学习率是 0.001,但图像修复任务里生成器的学习率通常要低一个数量级,建议从 0.0001 开始。其次是检查输入数据有没有异常值,比如图片内容全是 0 或者存在极端大数值,这种情况通常是因为数据读取和归一化环节出了问题。还有一个容易被忽略的原因:在 L1 损失里,如果掩码区域内部全是零值,而对应位置的预测值也非常接近零,会导致该区域的梯度很小,但这通常不会造成 NaN。最可能的是在判别器训练时,如果使用 BCE 损失,并且 logits 的绝对值过大,softmax 或 sigmoid 在反向传播时会出现数值溢出。改用nn.BCEWithLogitsLoss结合 gradient clipping,能有效减少这种风险。
7.2 修复区域边缘有生硬接缝
修复区域与周围环境之间有一条明显的分界线,这是很多新手跑通模型后最容易遇到的问题。原因通常是修复区域边缘的像素在训练时没有被充分约束。解决办法有两个:一是训练时掩码边缘增加过渡带,比如在掩码的二进制值上加一个高斯模糊的羽化效果,让模型学到从边缘到内部的渐变过渡;二是在损失函数里增加边缘感知权重,让掩码边缘附近的错误被施加更大的惩罚。项目源码里默认的掩码是硬边界,没有羽化,我加上羽化处理后,视觉上的接缝感明显减轻了。
7.3 训练结果偏色或色彩饱和度低
生成结果整体偏灰、颜色饱和度不够,往往是对抗损失权重过大、L1 损失权重太小导致的。L1 损失在像素层面拉近真实值和预测值,对颜色保真贡献最大。如果对抗损失主导,生成器会优先去骗过判别器,而有损的压缩和去均值操作恰好能骗过判别器对颜色的判断,结果就出现了颜色被“平均化”的现象。解决方式是调高 L1 损失的权重,或者在对生成器输出做颜色直方图匹配的后处理。实测中,把颜色直方图匹配作为推理后的后处理步骤,几乎能解决所有偏色问题,而且实现成本极低,只要用 OpenCV 的matchHistogram就能完成。
7.4 显存不足
最常见的报错就是 CUDA Out Of Memory。排查顺序是:先把 batch size 减半,再把图片尺寸从 256 降到 224,再考虑是否关闭一些中间变量的梯度计算。还有一个容易被忽略的点,是验证阶段是否把所有中间特征都保留在了显存里。如果验证时只是做前向推理,一定要加with torch.no_grad(),否则模型一直保存完整的计算图,显存很快就满了。另外,PyTorch 2.0 以上版本的compile功能自带显存优化,把model = torch.compile(model)加在训练前,对显存和速度都有小幅改善。
7.5 Patch 训练效果可以,全图推理效果差
模型在 256x256 裁剪图上训练,推理时直接上了 1024x1024 的完整图片,修复效果明显不如预期。原因在于模型对“尺度”有很强的依赖,训练时看到的纹理尺度、物体大小都是在 256 分辨率下的统计规律,全图推理时这些规律被打破了。解决方式是推理时使用滑动窗口策略:把大图切分成多个重叠的 256x256 小块分别修复,然后对重叠区域做加权平均融合。这个方案比直接大图推理更稳定,也省显存,代价是推理时间变长。很多商用的老照片修复工具背后用的就是这个套路。
8. 写在最后:我从这个项目里学到的几件事
把整个项目源码吃透、复现、再改造,前后花了我大概两周时间。收获不光是跑通了一个修复模型,更重要的是对“怎么把一个学术模型变成一个可用的工具”有了更具体的认识。
第一件事是“环境比模型更难”。很多人拿到源码第一步就想懂网络结构,但实际上光是装对 PyTorch 版本、调通 CUDA、跑通数据管线,就要耗费不少精力。遇到问题不要硬刚,多查官方文档,确认版本兼容性,能少走很多弯路。
第二件事是“训练的效果差距,往往不在模型结构,而在数据策略”。同样的生成器,不同的掩码生成方式、不同的数据增强策略,会带来完全不同的训练效果。掩码的多样性直接影响模型的泛化能力,如果你只用一个形状的掩码训练,模型就只能修那种形状的破损。
第三件事是“调参要盯指标,但不要只盯指标”。PSNR 和 SSIM 只能衡量像素和结构层面的相似度,人眼的观感才是最终标准。我遇到过几次模型的指标分数很高,但修复区域纹理依然生硬的情况,最后发现是感知损失的权重太低了。所以评估时建议生成一组包含人像、风景、文字、建筑等类别的典型测试图,每次训练完用肉眼扫一遍,比只看分数可靠得多。
这个项目的源码提供了一个足够扎实的起点,剩下的扩展空间还很大。无论是换更强的主干网络、接入扩散模型,还是做专门领域的微调,都是在现有骨架上做加法。从跑通到精通的这条路,走一遍,收获会比想象中大很多。
本文还有配套的精品资源,点击获取