
简介这份资源面向希望上手图像自动着色与深度先验学习的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 空间做而不是 RGBRGB 三个通道互相耦合亮度一变三个通道全变模型很难学。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 还是 L2L2 会让模型倾向于输出“平均色”结果就是整张图偏灰偏褐俗称“褪色感”。L1 对异常值更鲁棒颜色更饱和。实际用的时候我一般 L1 为主加一点 L2 做平滑def color_loss(pred_ab, true_ab, lambda_l20.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, size256): 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, axis0) 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(), lr1e-4, betas(0.5, 0.999)) dataset ColorDataset(./images, size256) loader DataLoader(dataset, batch_size16, shuffleTrue, num_workers4) 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(fepoch {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, size256): 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], axis2) 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(pretrainedTrue).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 乘以一个系数再 clipdef boost_saturation(ab, factor1.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 以下再考虑加感知损失和分类头。一上来就堆复杂结构大概率卡在数据管道或归一化上浪费好几天。另外训练集里如果某种场景特别少比如夜景、水下模型在那类图上会翻车补数据比调模型管用。希望帮到你。本文还有配套的精品资源点击获取