简介:这份源码面向深度学习与图像处理方向的开发者、学生及研究者,提供一套可直接运行的DeOldify黑白照片上色Web应用实现,帮助理解生成式模型在图像着色任务中的工程落地方式。压缩包共139个文件、约4.28MB,以103个Python脚本为核心,承担模型加载、推理与Gradio界面逻辑;另有21个pyc字节码、2个C++与2个CUDA源文件用于算子加速,配合3个tpl模板、4张PNG示例图及yml、json、license等配置说明文件,结构完整。项目已吸引207人学习,适合作为课程设计、毕业设计或二次开发的参考底本。读者可从中掌握fastai训练流程、Gradio交互界面搭建、CUDA自定义算子调用与前后端协同思路,并借助示例图与配置快速复现上色效果,为老照片修复、档案数字化等场景提供可扩展的技术起点。
1. 从一张泛黄老照片说起:DeOldify 图像上色器到底在做什么
翻出家里八十年代的老相册,黑白照片里的人物轮廓清晰,但衣服颜色、背景天空、皮肤质感全是灰阶。想给这些照片上色,传统做法是在 Photoshop 里手动铺色、调曲线、做蒙版,一张图半小时起步,批量处理基本不现实。DeOldify 这类基于深度学习的图像上色器解决的正是这个问题:输入一张灰度图或褪色老照片,模型自动预测每个像素的合理色彩,输出一张看起来自然的彩色图。它属于图像着色(Image Colorization)任务,核心是把灰度通道映射到 a、b 两个色度通道,本质是一个像素级回归问题。适合谁用?做老照片修复的从业者、想拿深度学习练手的学生、需要批量处理历史影像档案的工程团队。源码层面,DeOldify 把训练好的生成器权重、推理脚本、Flask 服务封装在一起,拿到源码后你能直接跑推理,也能基于它做微调。这一章先把「它是什么、能解决什么、适合谁」讲清楚,后面几章拆开讲怎么跑通、参数怎么调、坑在哪。
2. DeOldify 上色器的网络结构与选型逻辑
2.1 为什么是 GAN 而不是普通 CNN 回归
图像上色有一个天然歧义:一张灰度图对应的彩色版本可能有很多种合理答案。天空可以是湛蓝也可以是灰白,衣服可以是红色也可以是蓝色。如果只用普通 CNN 做 L1/L2 回归,模型会倾向于输出所有可能颜色的平均值,结果就是整张图发灰、发褐,俗称「土黄色滤镜」。DeOldify 选择 GAN(生成对抗网络)路线,生成器负责上色,判别器负责判断「这张彩色图看起来像不像真实照片」。对抗训练逼着生成器输出更鲜艳、更符合真实分布的颜色,而不是安全但无聊的平均色。
具体到 DeOldify 的生成器,它用的是 U-Net 风格的编码器-解码器结构,编码器通常以 ResNet 为主干做特征提取,解码器逐级上采样恢复分辨率。判别器则是一个 PatchGAN,不看整张图,而是看局部 patch 是否真实。这种设计让模型在局部纹理和颜色过渡上更自然。
2.2 源码目录里各文件的分工
拿到 DeOldify 源码后,先别急着跑,花十分钟把目录结构看清楚,能省掉后面大量试错时间。常见结构如下:
| 路径 | 作用 |
|---|---|
deoldify/ | 核心包,含模型定义、数据集加载、训练循环 |
deoldify/visualize.py | 推理入口,负责加载权重、处理输入、保存输出 |
deoldify/generators.py | 生成器网络定义 |
deoldify/critics.py | 判别器网络定义 |
deoldify/learner.py | 训练调度、损失函数组合 |
fastai/ | 依赖的 fastai 库(部分版本内置) |
models/ | 预训练权重存放目录 |
test_images/ | 示例输入图 |
result_images/ | 推理输出目录 |
核心推理逻辑集中在visualize.py,它调用generators.py里定义的生成器,加载models/下的.pth权重,对输入图做 resize、归一化、前向传播、反归一化,最后保存。训练相关逻辑在learner.py,损失函数通常是感知损失(Perceptual Loss)+ 判别器对抗损失的组合。
2.3 预训练权重的三种版本差异
DeOldify 官方通常提供三种权重:Artistic、Stable、Video。它们不是简单的精度高低区别,而是训练策略不同导致风格差异明显。
- Artistic:颜色最鲜艳,对老照片的「焕新」效果最强,但容易过度上色,比如把灰色墙面染成蓝色。适合艺术化修复。
- Stable:颜色保守,更接近真实,适合档案类、纪实类照片。
- Video:针对视频帧序列做了时序一致性优化,单张图也能用,但颜色偏淡。
选哪个取决于你的场景。做家庭老照片修复,我一般先用 Artistic 看效果,如果颜色溢出严重再换 Stable。批量处理历史档案,直接上 Stable,避免颜色失真引发争议。
2.4 用 Python 跑通单张图推理的最小命令
下面这段代码是加载 DeOldify 生成器并对单张图做推理的最小可运行示例。注意路径和权重文件名需要根据你实际下载的版本调整。
import torch from deoldify.visualize import get_image_colorizer from pathlib import Path # 指定权重文件路径,这里以 Artistic 版本为例 # 实际文件名可能是 ColorizeArtistic_gen.pth,放在 models/ 目录下 colorizer = get_image_colorizer(artistic=True) # 输入图路径和输出路径 source_path = Path("test_images/old_photo.jpg") result_path = Path("result_images/old_photo_colorized.jpg") # 执行推理 # render_factor 控制上色强度,默认 35,范围一般 7~45 colorizer.plot_transformed_image( path=source_path, render_factor=35, results_dir=result_path.parent, compare=False ) print(f"上色完成,输出到 {result_path}")逻辑说明:get_image_colorizer(artistic=True)会去models/目录找对应的 Artistic 权重并初始化生成器。plot_transformed_image内部做了三件事——把输入图缩放到模型需要的尺寸、做归一化、前向传播后把 a、b 通道拼回 RGB。render_factor是关键参数,它控制推理时输入图的分辨率缩放比例,值越大模型看到的细节越多,但显存占用和耗时也越高。
参数说明:render_factor=35是官方推荐的默认值,适合大多数照片。如果显存不足(比如 4GB 以下),降到 20 左右;如果追求极致细节且显存充足(8GB 以上),可以试 45。compare=False表示不生成对比图,只输出上色结果;设为True会额外保存一张左右对比图,方便肉眼评估。
2.5 批量处理时的目录遍历与命名策略
单张跑通后,批量处理是下一步。直接写个循环遍历目录即可,但要注意输出文件命名不要覆盖原图,建议加后缀。
from pathlib import Path from deoldify.visualize import get_image_colorizer colorizer = get_image_colorizer(artistic=False) # Stable 版本适合批量 input_dir = Path("dataset/grayscale") output_dir = Path("dataset/colorized") output_dir.mkdir(parents=True, exist_ok=True) # 支持的图片格式 exts = {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff"} for img_path in input_dir.iterdir(): if img_path.suffix.lower() not in exts: continue out_name = img_path.stem + "_colorized" + img_path.suffix try: colorizer.plot_transformed_image( path=img_path, render_factor=30, results_dir=output_dir, compare=False ) print(f"OK: {img_path.name}") except Exception as e: print(f"FAIL: {img_path.name} -> {e}")逻辑说明:遍历输入目录,过滤非图片文件,对每张图调用推理函数。try/except保证单张失败不影响整体批次。输出文件名加_colorized后缀,避免和原图混淆。
参数说明:批量场景下render_factor建议统一设为 30,兼顾速度和质量。如果输入图尺寸差异很大(比如有的 500px 有的 4000px),可以在推理前统一 resize 到长边 1024px,减少显存波动。artistic=False切换到 Stable 权重,颜色更稳,适合档案类批量任务。
3. 训练自己的上色模型:数据、损失与微调策略
3.1 训练数据从哪来:COCO 与 ImageNet 的取舍
DeOldify 原版训练用的是 ImageNet 和 COCO 这类大规模彩色图数据集。思路很简单:把彩色图转成灰度图作为输入,原彩色图作为监督目标,让模型学会「从灰度恢复彩色」。如果你要微调自己的模型,数据来源有几个选择。
COCO 2017 约 12 万张图,覆盖 80 类常见物体,场景偏日常,适合通用上色。ImageNet 约 128 万张训练图,类别更细,但部分类别偏门,可能引入噪声。如果做特定领域(比如历史档案、医学影像),最好自己收集领域内彩色图,转灰度后做微调。数据量建议至少 5000 张起步,否则微调容易过拟合。
数据预处理的核心操作:把 RGB 图转成 Lab 空间,取 L 通道作为输入,a、b 通道作为目标。这样模型只需要预测两个通道,比直接预测 RGB 三个通道更容易收敛。
3.2 损失函数组合:感知损失 + 对抗损失 + L1
DeOldify 的训练损失不是单一 L1 或 L2,而是多损失加权组合。常见配置如下:
| 损失项 | 作用 | 典型权重 |
|---|---|---|
| L1 Loss | 保证像素级接近目标 | 1.0 |
| Perceptual Loss | 用 VGG 特征比对,保证语义一致 | 0.1 |
| Adversarial Loss | 判别器提供,逼生成器输出真实感 | 0.01 |
| Feature Matching | 判别器中间层特征对齐 | 0.1 |
L1 负责「大致对」,感知损失负责「看起来像」,对抗损失负责「鲜艳自然」。权重需要调,对抗损失太大会导致颜色溢出和伪影,太小则颜色发灰。我一般从官方默认权重起步,微调时只动对抗损失权重,观察验证集颜色饱和度变化。
3.3 微调脚本的关键参数与显存控制
微调时最现实的问题是显存。DeOldify 生成器基于 ResNet,输入分辨率 256 或 512 时,batch size 通常只能开到 4~8(取决于显卡)。如果显存不够,有三个手段:降低输入分辨率、冻结编码器只训解码器、用梯度累积模拟大 batch。
import torch from deoldify.learner import DeOldifyLearner from deoldify.generators import gen_inference_deep from deoldify.critics import critic_inference_deep # 初始化生成器和判别器 generator = gen_inference_deep() critic = critic_inference_deep() # 冻结编码器前几层,只微调解码器 for name, param in generator.named_parameters(): if "encoder" in name and "layer1" in name: param.requires_grad = False # 优化器只更新需要梯度的参数 optimizer = torch.optim.Adam( filter(lambda p: p.requires_grad, generator.parameters()), lr=1e-4, betas=(0.5, 0.999) ) # 梯度累积,模拟 batch_size=16 accum_steps = 4 for step, batch in enumerate(dataloader): loss = compute_loss(generator, critic, batch) loss = loss / accum_steps loss.backward() if (step + 1) % accum_steps == 0: optimizer.step() optimizer.zero_grad()逻辑说明:冻结编码器浅层是因为这些层学的是通用边缘、纹理特征,微调时不需要大改。只训解码器能大幅减少可训练参数量,显存占用和训练时间都降下来。梯度累积是把多个小 batch 的梯度加起来再更新,效果接近大 batch。
参数说明:lr=1e-4是微调常用学习率,比从头训练小一个量级。betas=(0.5, 0.999)是 GAN 训练的经验值,第一个 beta 调低有助于稳定对抗训练。accum_steps=4表示每 4 个 batch 更新一次,实际 batch size 等于4 × 单次batch。
3.4 验证集怎么选:别只看 PSNR
上色任务的评估指标和普通超分、去噪不一样。PSNR、SSIM 这些指标衡量的是像素误差,但上色本身有多解性——模型输出一个和原图不同但同样合理的颜色,PSNR 会很低,但人眼看着没问题。所以验证集评估要人眼 + 指标结合。
我一般这样做:从验证集里抽 50 张,分别用模型上色,然后和原彩色图并排看。重点看三类问题:肤色是否自然、天空是否合理、大面积纯色区域有没有色斑。指标只作为辅助,PSNR 突然掉很多通常意味着训练崩了,但 PSNR 高不代表颜色好看。
提示:验证时固定
render_factor,不要每张图换参数,否则无法横向对比不同 checkpoint 的效果。
4. 避坑与排查:上色器落地时最容易翻车的五个地方
4.1 输出图整体发灰或发褐
现象:推理结果颜色暗淡,像蒙了一层土黄色滤镜,尤其是天空和皮肤区域。
原因:最常见的原因是用了回归损失主导的权重,或者render_factor设得太低。另一个可能是输入图本身对比度太低,模型难以判断颜色边界。
解决:换 Artistic 权重试一次,如果颜色明显改善说明是权重问题。把render_factor从 20 提到 35 以上。如果输入图偏暗,先用 PIL 或 OpenCV 做一次直方图均衡化再送入模型。
4.2 颜色溢出:灰色墙面被染成蓝色
现象:原本应该是中性灰或白色的区域,被模型强行上了颜色,比如墙面变蓝、衣服变绿。
原因:Artistic 权重对抗损失权重较高,模型倾向于「大胆上色」。另外如果训练数据里类似纹理总是伴随某种颜色,模型会学到虚假关联。
解决:切换到 Stable 权重。如果必须用 Artistic,降低render_factor到 25 左右,减少模型看到的细节,从而减少过度推断。也可以在输出后做一次颜色校正,把低饱和度区域拉回灰色。
4.3 显存不足导致推理中断
现象:跑大图时程序崩溃,报 CUDA out of memory。
原因:render_factor越高,模型内部处理的特征图越大,显存占用呈平方级增长。输入图原始尺寸过大也会加剧问题。
解决:推理前把输入图长边限制在 1024px 以内。render_factor降到 20~25。如果还是不够,用torch.cuda.empty_cache()在每张图之间清理缓存。批量处理时不要一次性把所有图加载到内存,用生成器逐张读。
4.4 输出图出现网格状伪影
现象:上色结果上有规律的方块或条纹,尤其在平坦区域明显。
原因:这是 GAN 训练中常见的 checkerboard artifact,通常和转置卷积(deconvolution)的步长、卷积核不匹配有关。DeOldify 某些版本的解码器用了转置卷积,容易出这个问题。
解决:推理时对输出做一次轻微高斯模糊(sigma=0.5)能缓解。如果自己在训练,把转置卷积换成最近邻上采样 + 普通卷积,能从根源减少伪影。另外检查输入图是否有 JPEG 压缩块,压缩噪声会被模型放大。
4.5 批量处理时文件名冲突或覆盖
现象:跑完批量任务发现输出目录里文件数比输入少,部分图被覆盖。
原因:不同子目录下有同名文件,输出时都写到同一个目录,后写的覆盖先写的。
解决:输出文件名里保留相对路径信息,比如把subdir1/img.jpg输出为subdir1_img_colorized.jpg。或者按输入目录结构在输出目录建对应子目录。代码里加一个存在性检查,如果目标文件已存在就加序号后缀。
5. 把上色器接进实际工作流:从单张到服务的进阶技巧
单张推理和批量脚本跑通后,真正要落地还得解决「怎么让别人也能用」的问题。最常见的做法是包一层 Flask 或 FastAPI 服务,对外提供 HTTP 接口。下面是一个最小 Flask 服务示例,接收上传图片,返回上色后的图片。
from flask import Flask, request, send_file from deoldify.visualize import get_image_colorizer from pathlib import Path import uuid app = Flask(__name__) colorizer = get_image_colorizer(artistic=True) TMP = Path("/tmp/deoldify") TMP.mkdir(exist_ok=True) @app.route("/colorize", methods=["POST"]) def colorize(): file = request.files["image"] uid = uuid.uuid4().hex in_path = TMP / f"{uid}_in.jpg" file.save(in_path) colorizer.plot_transformed_image( path=in_path, render_factor=30, results_dir=TMP, compare=False ) out_path = TMP / f"{uid}_in_colorized.jpg" return send_file(out_path, mimetype="image/jpeg") if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)逻辑说明:服务启动时加载一次模型,常驻内存,避免每次请求都重新加载权重。每个请求生成唯一 ID,输入输出文件用 ID 命名,避免并发冲突。send_file直接把结果图返回给客户端。
参数说明:render_factor=30是服务端的折中值,兼顾响应时间和质量。如果并发量高,需要加请求队列或限制同时处理的请求数,因为 GPU 推理是串行的。生产环境建议用 gunicorn + 多 worker,但注意每个 worker 会加载一份模型,显存要算够。
进阶技巧方面,有两个方向值得投入。一是做分块推理:对于超大图(比如 8000px 扫描件),直接缩放到 1024px 会丢失细节,可以切成重叠的小块分别上色再拼接,重叠区域做羽化融合。二是做颜色一致性后处理:对视频或多张同场景照片,上色后做一次直方图匹配,让相邻帧或同组图的色调统一,避免闪烁感。
我自己的习惯是,任何上色任务先跑一张测试图,确认权重和render_factor合适后再批量。批量前一定先小样本跑 10 张,人眼过一遍,没问题再全量。这个习惯帮我省过很多次「跑了一晚上发现颜色全不对」的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取