用Python与深度学习为黑白照片自动上色:DDColor实战全解析 简介一套基于深度学习为黑白图片自动着色的项目源码面向Python开发者和初中级算法工程师解决灰度图像生成合理色彩这一典型图像处理问题可应用于老照片修复、影视素材复原、创意绘画等领域。压缩包共9个文件以6个Python脚本为主干分别负责图像数据格式转换、样本裁剪、网络模型结构定义、训练循环、效果评估和最终推理另有环境配置脚本与说明文档包体仅8KB轻量且便于逐模块阅读。已有2724人学习。项目中完整包含了从彩色图转灰度、黑白彩色配对到卷积神经网络训练再到新图片色彩化输出的实现链路可帮助读者理解图像特征提取、颜色空间转换及损失函数设计的实际写法。同时脚本间耦合度较低配合说明文档可快速上手既适合作为深度学习入门者的代码参考也可直接调整后用于个人实验或二次开发。 周末整理移动硬盘时翻出一堆翻拍的黑白老照片看着家人年轻时的轮廓却完全没有色彩确实遗憾。关于如何用 Python 给黑白图片自动上色我最早接触到的是深度学习相关项目后来自己动手做了一个叫 ImageColor 的上色流程这里把完整经验整理出来。简单说这个项目不是传统图像处理而是让模型从海量彩色照片里学习“一个灰度区域大概率对应什么颜色”再对新的黑白图做推断。它适合想批量修复老照片、又不想被商业化修图工具绑定的读者也适合准备入门深度学习视觉方向的朋友作为一个完整实战案例。下面我尽量把原理、选型、代码、踩坑一次讲透。1. 为什么黑白照片上色是“看起来简单、做起来难”的技术活1.1 一张灰度图背后的信息缺失灰度图的每个像素只有一个亮度值比如 0 到 255 之间的某个数。这个数值只能告诉我们这一点的明暗完全没有颜色信息。换句话说红苹果和灰砖墙在某个曝光条件下完全可能对应同一个灰度值。给定一个缺失信息的问题计算机无法从单点恢复出唯一正确答案这在数学上叫“不适定问题”。传统做法是怎么解决的美术修复师手动涂抹靠着对历史照片年代、衣物材质、场景语义的经验来猜测。Photoshop 里也有自动上色功能但参数不透明、也无法针对老照片批量处理。真正能自动化的路径只有一条喂给模型成千上万张彩色照片让它自己总结出“天空通常是蓝的、草地通常是绿的、肤色通常在一个范围内”这些规律。1.2 深度学习如何“猜”出合理颜色用来做上色的深度模型核心任务其实是一个条件生成问题输入灰度图输出彩色图。常用策略是先对灰度图做特征提取然后根据语义信息生成 ab 两个颜色通道。这里要提一下图像色彩空间的差异。RGB 空间里亮度和颜色耦合在一起不太适合作为上色任务的中间表示。更常见的是把图像转到 Lab 色彩空间L 通道只表示亮度a 通道表示从绿到红b 通道表示从蓝到黄。灰度图天然只有 L 通道模型只需要预测 a、b 两个通道再把三个通道合并转回 RGB 即可。这也是很多经典上色模型如 Colorful Image Colorization、DeOldify 采用的思路。现代模型如 DDColor 直接输出 RGB但底层仍然遵循同样的逻辑从灰度输入推断颜色分布。模型本质是在学习一个从“亮度 语义”到“颜色”的映射这个映射是靠大规模彩色图片训练出来的。所以我一直认为上色项目是最适合新手理解深度学习“特征提取”的项目之一它不像目标检测那样要画框也不像分割那样要做像素级标注只需用现成的 ImageNet 预训练权重做迁移训练和推理链路都足够直观。2. 模型选型对比DeOldify、DDColor 和我最终的选择2.1 三条技术路线的对比项目刚开始时我也在几个主流开源模型之间纠结过。下面这个表是我实测下来的直观感受不是官方指标但对选型很有参考价值模型原理要点优势劣势适用场景Colorful Image Colorization将 ab 通道量化为 313 类用分类任务处理颜色不确定性理论清晰、经典易读色彩偏保守有时发灰学习原理、跑 baselineDeOldify基于 GAN 生成对抗训练生成器输出整张彩色图色彩鲜艳细节纹理好偶尔产生伪色、人工感略重艺术化老照片修复DDColor双解码器结构语义特征与颜色特征融合后回归 RGB色彩自然、语义准确、代码清晰权重文件偏大显存占用相对高批量上色、视频上色从模型效果上看DeOldify 的色彩冲击力最强但如果你批量处理几十张年代风格不同的老照片会发现它偶尔会把不该鲜艳的区域染得很浓。DDColor 更注重语义一致性它对建筑、天空、植被、人物的区分明显更可靠。2.2 我最终选择 DDColor 的理由我的主要使用场景是修复家里几十张翻拍老照片色彩要求是“自然、协调”而不是“鲜艳、惊艳”。DDColor 在这方面更贴合需求。其次它的开源仓库提供了 512x512 和 1024x1024 两种预训练权重输入输出都是 RGB代码结构比 DeOldify 来说相对干净部署和二次开发成本低。还有一点很实际DeOldify 的默认实现依赖较重的 GAN 训练设施DDColor 推理时只需要加载生成器显存和 CPU 内存占用都更友善。我用一张 8GB 显存的显卡跑 1024x1024 的图也没有压力。如果你手头只有 CPUDDColor 也能跑只是要适当调小输入尺寸。2.3 环境准备清单与安装命令我推荐使用 Python 3.10 及以上版本深度学习框架选择 PyTorch。安装命令如下# 创建虚拟环境 conda create -n imagecolor python3.10 conda activate imagecolor # 安装 PyTorch带 CUDA 的版本自行到官网选择 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装依赖 pip install pillow numpy opencv-python scikit-image tqdm pip install githttps://github.com/piddnad/DDColor.git依赖里 scikit-image 是为了方便做 Lab/RGB 转换实验如果只用 DDColor 官方代码也可以不装。opencv-python 则是给老照片做降噪和预处理用的后面踩坑部分会提到。3. 核心代码拆解从灰度图到彩色图的完整流程3.1 一次典型推理的全流程整体流程可以拆成五步读图、预处理、模型推理、后处理、保存。下面这段代码是完整可跑的示例参考了 DDColor 官方仓库的推理写法我把它精简成适合批量处理的版本import os import glob import torch from PIL import Image, ImageEnhance from torchvision import transforms from tqdm import tqdm from ddcolor import DDColor device cuda if torch.cuda.is_available() else cpu # 加载预训练模型 checkpoint torch.load(dct_512x512.pth, map_locationdevice) model DDColor(...) # 具体构造参数参考官方仓库 model.load_state_dict(checkpoint) model.to(device).eval() # 预处理统一缩放到模型输入尺寸并转为张量 transform transforms.Compose([ transforms.Resize((512, 512)), transforms.ToTensor(), ]) def process_image(path, save_path): # 读图并转 RGB避免灰度图或 PNG 带 alpha 通道导致的通道数不匹配 img Image.open(path).convert(RGB) input_tensor transform(img).unsqueeze(0).to(device) # 推理 with torch.no_grad(): output model(input_tensor) # 输出是 [B, C, H, W] 的 RGB 张量先裁剪到合理范围再转回 PIL output output.squeeze(0).cpu().clamp(0, 1) result transforms.ToPILImage()(output) # 后处理饱和度微调再缩放回原始尺寸 result ImageEnhance.Color(result).enhance(1.2) result result.resize(img.size) result.save(save_path) os.makedirs(output, exist_okTrue) for path in tqdm(glob.glob(input/*.jpg)): name os.path.basename(path) process_image(path, os.path.join(output, name))这段代码看起来简单但有几个细节值得展开说。3.2 预处理灰度图转成模型能“看懂”的格式很多人第一次跑失败问题出在输入格式上。灰度图如果用Image.open(path)直接读得到的是单通道如果 PNG 带透明通道得到的是 RGBA 四通道。而大部分模型要求三通道 RGB 输入所以读图后必须主动执行.convert(RGB)。这是最不起眼、也最容易翻车的坑。另外要注意尺寸和归一化。上面代码里Resize((512, 512))做的事情是把任意尺寸的老照片统一缩放到模型输入尺寸。ToTensor()会自动把像素值从 0-255 归一化到 0-1。如果你自己写预处理忘了归一化模型输出基本就是一片乱色这点新手要格外留意。如果你用的是经典 Lab 方案比如 DeOldify 或 Zhang 2016预处理会多一步将灰度图转换成 Lab 空间的 L 通道作为输入推理得到 a、b 通道后再合成。示意代码如下import numpy as np from skimage.color import rgb2lab, lab2rgb gray np.array(img.convert(L)) # 单通道亮度 lab rgb2lab(np.stack([gray]*3, axis-1)) L_channel lab[:, :, 0] # 保留亮度 # 模型预测 a_channel, b_channel ... result lab2rgb(np.stack([L_channel, a_channel, b_channel], axis-1))这个逻辑并不复杂但理解它对迁移到不同模型很有帮助。3.3 模型推理与 FP32/FP16 选择推理代码里最重要的一行是with torch.no_grad():。很多新手跑深度学习代码时不理解为什么要加这个其实是为了在推理阶段关闭梯度计算省下大量显存和计算量。训练时需要反向传播所以要保留梯度推理时只需要前向结果不开梯度可以明显加快速度。推理阶段还有一个和部署强相关的细节浮点数格式选择。默认情况下 PyTorch 使用 FP32 精度也就是每个数值占 4 字节。如果你把模型和输入都转换成 FP16显存占用直接减半部分新显卡推理速度也能提升一截。实测同一张 1024x1024 老照片FP16 推理在 RTX 3060 上大约能比 FP32 快 20% 到 30%颜色输出差异肉眼几乎不可见。FP16 并非只有好处。老显卡对 FP16 支持不完整强行使用会变慢甚至报错。如果你的需求是追求极致的精度稳定性比如做修复存档FP32 更稳妥。还有一个中间选项是 TF32相比 FP16 更接近 FP32 的数值稳定性在 Ampere 架构显卡上可以用torch.backends.cuda.matmul.allow_tf32 True开启。BF16 则主要用于大模型训练场景推理阶段通常用不上。总之上色模型部署选 FP32 起步确认没问题后再试 FP16 提速。3.4 后处理与批量脚本中的实用技巧后处理阶段我加了两个步骤饱和度增强和尺寸还原。饱和度增强是因为模型推理结果通常偏保守尤其是经过归一化再转回 PIL 后颜色容易发灰。用ImageEnhance.Color(result).enhance(1.2)可以把饱和度提高 20%对老照片来说效果立竿见影。注意这个系数不宜过大超过 1.5 后肤色会明显假。尺寸还原同样关键。模型输出是 512x512直接保存会丢失原始分辨率。先保存小图再放大显然不划算所以代码里在保存前把结果resize回img.size。如果原图非常大也可以考虑用超分辨率模型先放大老照片再做上色这个经验后面另说。批量脚本我习惯用glob.glob配合tqdm做进度显示。数据集几百张图时没有任何提示的等待很容易让人误以为程序卡死。另外建议每次写入时加try-except因为翻拍的图片偶尔会有损坏跳过坏图比中断整个任务更实用。4. 实测最容易踩的五个坑与完整排查思路4.1 模型权重加载失败的定位链路我最初跑 DDColor 时卡在加载权重这一步。报错信息大致是state_dict中的 key 对不上或者直接提示文件不存在。我的排查思路是这样的先确认文件是否真的下载完整。很多权重文件是从外部存储服务下载的网络波动可能导致下载中断文件大小不足但不会报下载失败。一个简单直接的验证办法是用torch.load加载后打印 checkpoint 的键名ckpt torch.load(dct_512x512.pth, map_locationcpu) print(ckpt.keys())如果加载时报UnicodeDecodeError或键名异常基本可以确定文件损坏重新下载即可。如果文件正常但 key 数量对不上一般是因为官方仓库更新过模型结构你用的模型构造参数和预训练权重不匹配。这时需要检查 DDColor 官方仓库 README 里的权重说明确认和模型代码版本一致。这个排查思路可以复用到几乎所有开源深度学习项目先确认文件完整再确认代码版本最后才考虑环境问题。4.2 输出偏灰或偏黄的修复颜色偏灰是最常见的现象。根因通常是输出张量没有做范围裁剪或者模型原本输出就偏保守。我的处理是在后处理时强制clamp(0, 1)把任何超出合理范围的像素值拉回来。这个操作看似简单却能防止十几个像素的“过冲”扩散到整张图片的观感。还有一种情况是偏黄多见于年代久远的照片本身有泛黄底色。这不是模型的问题而是原始图片经过了非中性灰的处理。解决方法是先把输入做一次白平衡预处理用 OpenCV 的cv2.xphoto.createSimpleWB()或手动对三个通道做增益校正。上色前先把底色拉回中性输出的颜色就会干净很多。4.3 人脸肤色发绿的问题批量处理人像老照片时最容易发现的问题是人脸区域颜色不准有时发绿、有时发灰。这主要是因为老照片里人脸区域的对比度低、噪点多模型从低质量输入里提取不到足够的面部语义信息只能“蒙”一个肤色而肤色本身就是颜色分布很窄的区域。我的解决思路是先做一次降噪再上色。用 OpenCV 的非局部均值降噪函数能让皮肤纹理更平滑模型对脸部区域的判断也会更稳定import cv2 img_cv cv2.imread(path) img_cv cv2.fastNlMeansDenoisingColored(img_cv, None, 5, 5, 7, 15) cv2.imwrite(denoised.jpg, img_cv)如果人脸在画面中占比很小更有效的方法是先把人脸区域裁剪出来单独上色再拼回原图。这个思路依赖于人脸检测模型虽然代码复杂一些但对以人物为主的老照片效果提升非常明显。4.4 CPU 推理性能优化只靠 CPU 推理时512x512 输入一张图可能要十几秒。如果只是处理两三张图还能接受批量一多就会很煎熬。我实测过几种优化策略效果如下优化方式512x512 单张耗时i5 16G 内存效果直接 FP32 CPU约 18s基准输入降到 384x384约 10s色彩略淡可接受输入降到 256x256 超分放大约 5s细节丢失需再处理ONNX Runtime 4 线程约 11s兼容性好部署方便如果你只有 CPU我的建议是保持 384x384 输入做上色然后用超分辨率模型把结果放大回原始尺寸。虽然会多一步但整体耗时往往比直接跑 1024x1024 更低观感也更好。4.5 老照片扫描件的白边和噪点干扰翻拍照片通常带有白色边框或灰度背景这些区域会干扰模型的语义判断比如把背景误识别为天空或墙面上色后会变成意想不到的颜色。我的习惯是预处理阶段先做简单的背景裁剪或边缘检测将照片主体尽量占满画面。这一步不需要太精确只要能把模型注意力集中在主体区域就行。噪点的影响也不可忽视。老照片的银盐颗粒在放大后会变成明显的纹理噪声模型可能把这些纹理误当成颜色边界产生“色块渗色”的现象。所以我的批处理流程里降噪总是排在上色之前这一步几乎从不省略。5. 效果评估与扩展玩法让项目不止玩一次5.1 如何判断上色结果是否靠谱没有真实彩色原图做对比时怎么评估结果我总结了三个维度一是语义合理性。天空不该是紫色草地不该是红色水面不该是荧光色。如果模型输出明显违背常识说明该区域的语义特征提取失败直接返回错误结果。二是局部一致性。同一件衣服的不同区域、同一块墙壁的不同亮度面颜色应该保持连续不能出现一块红一块蓝的割裂感。三是色彩分布自然度。老照片受限于当时胶片特性整体色调偏温和如果输出饱和度过高就要手动降下来一点。如果你想定量验证可以在网上找一些同场景的彩色老照片做参考或者拿几张现代彩色照片转成灰度再上色和原图计算 PSNR 或 SSIM。但说实话这类指标数值高不代表“感觉对”上色任务的主观因素占比非常大。5.2 视频帧上色、超分修复、Web Demo 的扩展方向这个项目的可扩展性很强我后续计划做三件事。第一是视频上色。老电影视频本质上就是连续的黑白帧把每帧抽取出来做上色再合成视频即可。但直接逐帧处理会出现明显的帧间闪烁因为相邻帧的语义判断可能略有不同所以需要考虑光流约束或时序一致性。DDColor 官方仓库已经给了视频上色的参考代码直接在其基础上调整就行。第二是结合超分辨率。很多老照片本身分辨率很低直接上色只能得到“低分辨率彩色图”。我目前的方案是先用超分模型将图像放大到 2 到 4 倍再送入上色模型。这样做的好处是模型能看到更多细节颜色判断的语义信息更丰富。第三是部署成 Web Demo。Gradio 三五十行代码就能把上色函数包装成网页应用部署到带 GPU 的云服务器上随时随地处理照片。如果要把程序分享给不会装 Python 的朋友可以用 PyInstaller 封装成 Windows 可执行文件模型权重随包带上双击就能用。最后再分享一个经验做这种图像处理项目别一上来就追求模型的完美表现。先跑通流程处理 20 张风格差异大的老照片打印出来看效果你会很快发现模型在不同题材上的强项和弱项。然后再根据弱项针对性地加预处理、换权重、调参数这比闷头调模型结构有效得多。希望这套 ImageColor 流程能给你的老照片修复项目省下一点弯路。本文还有配套的精品资源点击获取