☰
基于深度神经网络的灰度图像自动上色:从Lab空间到编码器-解码器实战
2026/10/1 12:13:47 网站建设 项目流程

简介:这份资源面向希望上手图像自动着色与深度先验学习的Python开发者与计算机视觉学习者,核心是两套预训练着色模型(eccv16与siggraph17)的推理代码,可用于为黑白照片或灰度图像实时生成彩色结果,适合具备一定PyTorch基础、想快速跑通着色demo或研究深度先验应用的人群。压缩包共23个文件,约4.47MB,以py脚本为主,包含模型定义、工具函数与演示入口,另有pyc缓存、若干jpg/jpeg/png示例图片、LICENSE、README与requirements等说明文件,结构清晰便于直接运行与二次修改。目前已有680人学习下载。资源完整保留了从Lab空间转换、256×256缩放、着色到与原分辨率拼接再转回RGB的预处理与后处理流程,读者可据此理解模型加载方式、推理管线与结果对比,并参考imgs_out中的输出图验证效果,快速搭建自己的着色实验环境。

1. 黑白照片上色这件事,为什么值得用深度神经网络重做一遍

手里有一批老照片,扫描完全是灰度的,想上色。传统做法是手工调色、分层蒙版,一张图半小时起步,批量处理基本不现实。自动着色要解决的就是这个:输入一张灰度图,输出一张看起来合理的彩色图。注意是“合理”不是“真实”,因为灰度图本身丢掉了色度信息,任何模型都只能猜。深度神经网络之所以能猜得比较像,是因为它在训练集里见过大量“这种纹理通常配这种颜色”的统计规律——天空大概率偏蓝,草地大概率偏绿,人脸肤色有个大致范围。

这个方向适合两类人:一类是想拿现成 Python 代码跑通流程、给自己的老照片或数据集批量上色的工程师;另一类是想搞清楚编码器-解码器结构、损失函数怎么设计、Lab 色彩空间怎么用的学习者。下面按“先跑通再调优”的顺序拆,代码可以直接抄,参数会逐个说明。

2. 自动着色的技术底座:从 Lab 色彩空间到编码器-解码器

2.1 为什么必须在 Lab 空间做,而不是 RGB

RGB 三个通道互相耦合,亮度一变三个通道全变,模型很难学。Lab 空间把亮度(L)和色度(a、b)解耦:L 通道就是灰度信息,a 和 b 是要预测的两个色度通道。这样任务就变成了——输入 L,预测 a 和 b,拼回去再转回 RGB。这是绝大多数自动着色方案的标准做法。

具体转换用 OpenCV 就行:

import cv2 import numpy as np def rgb_to_lab(img_rgb): # OpenCV 读进来是 BGR,先转 RGB img_rgb = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2RGB) # 归一化到 [0,1] 再转 Lab,避免 8bit 量化误差 img_rgb = img_rgb.astype(np.float32) / 255.0 img_lab = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2Lab) return img_lab def lab_to_rgb(img_lab): img_rgb = cv2.cvtColor(img_lab, cv2.COLOR_Lab2RGB) img_rgb = np.clip(img_rgb * 255.0, 0, 255).astype(np.uint8) return img_rgb

逻辑说明:先转 float 再转 Lab 是关键,uint8 直接转会在 a、b 通道产生明显台阶。参数上,L 通道范围是 [0,100],a、b 大约在 [-128,127]。训练时通常把 L 归一化到 [-1,1],a、b 也缩放到 [-1,1],这样和 tanh 输出匹配。

2.2 编码器-解码器结构怎么搭

主流做法是 U-Net 风格的编码器-解码器:编码器逐层下采样提取语义特征,解码器逐层上采样恢复分辨率,中间用 skip connection 把浅层的边缘、纹理信息传过去。没有 skip connection 的话,输出会糊成一片,边缘全丢。

一个能跑的最小结构:

import torch import torch.nn as nn class ColorNet(nn.Module): def __init__(self): super().__init__() # 编码器:输入 1 通道 L,输出 512 通道特征 self.enc = nn.Sequential( nn.Conv2d(1, 64, 4, 2, 1), nn.ReLU(), nn.Conv2d(64, 128, 4, 2, 1), nn.BatchNorm2d(128), nn.ReLU(), nn.Conv2d(128, 256, 4, 2, 1), nn.BatchNorm2d(256), nn.ReLU(), nn.Conv2d(256, 512, 4, 2, 1), nn.BatchNorm2d(512), nn.ReLU(), ) # 解码器:上采样回原分辨率,输出 2 通道 ab self.dec = nn.Sequential( nn.ConvTranspose2d(512, 256, 4, 2, 1), nn.BatchNorm2d(256), nn.ReLU(), nn.ConvTranspose2d(256, 128, 4, 2, 1), nn.BatchNorm2d(128), nn.ReLU(), nn.ConvTranspose2d(128, 64, 4, 2, 1), nn.BatchNorm2d(64), nn.ReLU(), nn.ConvTranspose2d(64, 2, 4, 2, 1), nn.Tanh(), ) def forward(self, x): return self.dec(self.enc(x))

参数说明:卷积核 4、步长 2、padding 1 是下采样和上采样的经典配置,每层分辨率减半或翻倍。BatchNorm 在 batch size 小于 8 时统计量不稳,可以换 InstanceNorm。最后一层 Tanh 把输出压到 [-1,1],和归一化后的 ab 对应。

2.3 损失函数选什么:L1 还是 L2

L2 会让模型倾向于输出“平均色”,结果就是整张图偏灰偏褐,俗称“褪色感”。L1 对异常值更鲁棒,颜色更饱和。实际用的时候我一般 L1 为主,加一点 L2 做平滑:

def color_loss(pred_ab, true_ab, lambda_l2=0.1): l1 = nn.functional.l1_loss(pred_ab, true_ab) l2 = nn.functional.mse_loss(pred_ab, true_ab) return l1 + lambda_l2 * l2

如果追求更鲜艳的结果,可以再加一项分类损失:把 ab 空间量化成 313 个色块(这是常见做法),让模型预测每个像素属于哪个色块,最后取期望。这个方案出自一篇经典论文,效果比纯回归好,但实现复杂度高不少,建议先把回归跑通再考虑。

3. 用 Python 把训练和推理跑起来:数据、训练循环、推理脚本

3.1 数据准备:灰度图从哪来,怎么配对

自动着色的训练数据不需要额外标注——任何彩色图都能用:把彩色图转成 Lab,取 L 当输入,ab 当标签。所以 ImageNet、COCO 甚至自己拍的照片都能用。

from torch.utils.data import Dataset import cv2, numpy as np, os class ColorDataset(Dataset): def __init__(self, img_dir, size=256): self.files = [os.path.join(img_dir, f) for f in os.listdir(img_dir) if f.lower().endswith(('.jpg', '.png', '.jpeg'))] self.size = size def __len__(self): return len(self.files) def __getitem__(self, idx): img = cv2.imread(self.files[idx]) img = cv2.resize(img, (self.size, self.size)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 lab = cv2.cvtColor(img, cv2.COLOR_RGB2Lab) L = lab[:, :, 0] / 50.0 - 1.0 # [0,100] -> [-1,1] ab = lab[:, :, 1:] / 128.0 # 约 [-1,1] L = np.expand_dims(L, axis=0) ab = ab.transpose(2, 0, 1) return torch.from_numpy(L).float(), torch.from_numpy(ab).float()

逻辑说明:L 除以 50 再减 1,把 [0,100] 映射到 [-1,1];ab 除以 128 近似归一化。resize 到 256 是权衡显存和细节,想保留更多纹理可以上 512,但 batch size 要相应减小。

3.2 训练循环与关键超参

from torch.utils.data import DataLoader device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = ColorNet().to(device) opt = torch.optim.Adam(model.parameters(), lr=1e-4, betas=(0.5, 0.999)) dataset = ColorDataset('./images', size=256) loader = DataLoader(dataset, batch_size=16, shuffle=True, num_workers=4) for epoch in range(50): model.train() total_loss = 0 for L, ab in loader: L, ab = L.to(device), ab.to(device) pred = model(L) loss = color_loss(pred, ab) opt.zero_grad() loss.backward() opt.step() total_loss += loss.item() print(f'epoch {epoch}, loss {total_loss/len(loader):.4f}')

参数说明:学习率 1e-4 配 Adam 是这套结构的稳妥起点,betas 用 (0.5, 0.999) 是 GAN 类训练的常见设置,纯回归用默认 (0.9, 0.999) 也行。batch size 16 在 8GB 显存上跑 256 分辨率没问题。50 个 epoch 在几万张图上大概能出可看的结果,想更好要上百 epoch。

3.3 推理脚本:把灰度图变成彩色图

def colorize(model, gray_path, out_path, size=256): model.eval() img = cv2.imread(gray_path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (size, size)) L = img.astype(np.float32) / 255.0 * 100.0 L_norm = L / 50.0 - 1.0 L_tensor = torch.from_numpy(L_norm).unsqueeze(0).unsqueeze(0).float().to(device) with torch.no_grad(): ab = model(L_tensor).cpu().numpy()[0].transpose(1, 2, 0) * 128.0 lab = np.concatenate([L[:, :, None], ab], axis=2) rgb = cv2.cvtColor(lab.astype(np.float32), cv2.COLOR_Lab2RGB) rgb = np.clip(rgb * 255, 0, 255).astype(np.uint8) cv2.imwrite(out_path, cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR))

逻辑说明:推理时把灰度图当 L 通道,模型预测 ab,拼成 Lab 再转 RGB。注意 OpenCV 的 Lab 范围是 L:[0,100]、ab:[-127,127],和训练时的归一化要对应上,否则颜色会偏。

4. 避坑与排查:自动着色翻车的五个典型场景

4.1 输出一片灰褐,几乎没有颜色

现象:推理结果整体偏灰,只有轻微色偏。原因:L2 损失占主导,模型学到的是条件均值。解决:换成 L1 为主,或者加分类损失;检查 ab 归一化是否把范围压得太小,导致梯度信号弱。

4.2 颜色溢出到不该有的区域

现象:天空的蓝色渗到建筑上,人脸肤色蔓延到背景。原因:编码器下采样太狠,空间信息丢失,skip connection 没接或接得太少。解决:加 skip connection,或者把下采样层数从 4 降到 3,保留更高分辨率的特征。

4.3 训练 loss 降不下去,一直在高位震荡

现象:loss 从第一轮就卡在 0.3 左右不降。原因:学习率太大,或者数据归一化不一致——比如训练时 L 归一化到 [-1,1],推理时忘了做同样处理。解决:先把学习率降到 1e-5 试,再检查数据管道里 L 和 ab 的范围是否和模型输出匹配。

4.4 显存爆了,batch size 降到 1 还是 OOM

现象:CUDA out of memory。原因:256 分辨率下 512 通道的特征图占用很大,如果还开了梯度累积或没释放中间变量,显存会持续涨。解决:用 torch.cuda.empty_cache(),把 num_workers 调小,或者把模型通道数减半(512 降到 256)。混合精度训练也能省一半显存。

4.5 推理速度慢,一张图要好几秒

现象:单张 256 图推理超过 2 秒。原因:模型在 CPU 上跑,或者没开 eval 模式导致 BatchNorm 还在更新统计量。解决:确认 model.eval() 和 torch.no_grad() 都加了;导出 ONNX 或 TorchScript 能再快一截;如果只是批量处理老照片,用 GPU 跑,一张图应该在 50ms 以内。

5. 让颜色更准的两个进阶技巧:感知损失与后处理

5.1 加感知损失,让颜色更符合语义

纯像素损失只关心数值接近,不关心“看起来像不像”。感知损失的做法是:把预测图和真实图都送进一个预训练的 VGG,取中间层特征算 L1 距离。这样模型会被迫学出语义上合理的颜色,而不是像素级平均。

import torchvision.models as models vgg = models.vgg16(pretrained=True).features[:16].to(device).eval() for p in vgg.parameters(): p.requires_grad = False def perceptual_loss(pred_rgb, true_rgb): # pred_rgb / true_rgb 都是 [0,1] 的 RGB feat_pred = vgg(pred_rgb) feat_true = vgg(true_rgb) return nn.functional.l1_loss(feat_pred, feat_true)

参数说明:取 VGG 前 16 层(到第三个 block 的 ReLU),太低层是边缘纹理,太高层是语义,中间层对颜色最敏感。权重一般设 0.01 到 0.1,太大会让输出过度平滑。

5.2 后处理:直方图匹配与饱和度微调

模型输出有时整体偏淡,可以在 Lab 空间对 ab 通道做直方图匹配,或者简单地把 ab 乘以一个系数再 clip:

def boost_saturation(ab, factor=1.2): # ab 形状 [H,W,2],范围约 [-128,127] ab = ab * factor return np.clip(ab, -127, 127)

factor 设 1.1 到 1.3 之间比较自然,超过 1.5 会出现明显的色块和伪影。这个技巧在批量处理老照片时特别有用,因为老照片本身色彩就淡,模型容易输出更淡的结果。

我自己的习惯是:先把回归模型跑通,确认 L1 损失能降到 0.02 以下,再考虑加感知损失和分类头。一上来就堆复杂结构,大概率卡在数据管道或归一化上,浪费好几天。另外,训练集里如果某种场景特别少(比如夜景、水下),模型在那类图上会翻车,补数据比调模型管用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询