简介这份资源面向具备一定C与OpenCV基础的图像处理学习者聚焦深度学习DNN模块在灰度图像上色中的实际应用。包内提供可直接运行的Visual Studio 2019工程基于OpenCV的DNN模块加载Caffe模型实现灰度图自动着色并支持通过参数调节控制上色强度便于观察不同阈值下的色彩渐变效果。资源共40个文件包含cpp源码、vcxproj工程文件、sln解决方案、caffemodel与prototxt模型文件、exe可执行程序及若干dll依赖库同时附带原始图像、灰度图像与多组不同参数下的结果图压缩包约190.34MB。已有570人学习下载。读者可借此掌握DNN模块调用预训练模型完成图像上色的完整流程理解参数调节对输出色彩的影响并参考工程结构快速搭建自己的实验环境。1. 从一张泛黄老照片说起DNN 给灰度图像上色到底在做什么家里翻出一张上世纪的黑白照片人脸轮廓清晰但毫无血色你想把它变成彩色又不想一张张手动涂。这个需求背后就是灰度图像上色输入一张单通道亮度图输出一张三通道彩色图。传统做法靠人工调色或查表映射遇到复杂场景直接翻车。深度学习尤其是 DNN 把这件事变成了可学习的映射问题——让网络从海量彩色图里学会“什么样的灰度纹理该配什么颜色”。它适合两类人一类是想拿它做毕设或实战项目的新手另一类是手里有大量历史影像、需要批量上色的从业者。核心难点不在网络多深而在颜色本身是多解的一件衣服在灰度下无法判断是红是蓝网络只能给出统计上最可能的颜色。理解这一点后面所有选型和调参才有依据。2. 上色任务的输入输出与颜色空间选型为什么多数方案在 Lab 里干活2.1 RGB 直接回归的坑与 Lab 的解法最直觉的做法是让网络直接输出 RGB 三个通道。我最早也这么干过结果训练 loss 降得挺好看推理出来整张图发灰发脏。原因在于 RGB 三通道高度相关网络要同时学亮度和色度而亮度信息在输入里已经给死了等于让它在已知条件下重复学一遍还容易把色度预测带偏。常见做法是转到 Lab 颜色空间。L 通道就是亮度恰好等于灰度输入a、b 两个通道承载色度网络只需要预测这两个通道。这样输入输出解耦任务从“重建整张图”变成“补两个色度通道”收敛快很多。OpenCV 里转换就一行import cv2 import numpy as np # 读入彩色图转成 Lab img_bgr cv2.imread(color.jpg) img_lab cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB) # L 通道作为网络输入a/b 作为监督目标 L img_lab[:, :, 0] # 取值 0-255 ab img_lab[:, :, 1:3] # 取值 0-255需归一化到 [-1, 1]逻辑说明COLOR_BGR2LAB在 OpenCV 里 L 范围是 0-255a/b 也是 0-255 且以 128 为零点。训练前要把 a/b 减 128 再除以 128映射到 [-1,1]否则网络要花额外容量去拟合这个偏移。推理时反过来预测的 ab 乘 128 加 128和 L 拼回 Lab再转 BGR。参数说明如果你用 PyTorch 的torchvision注意它没有直接的 Lab 转换得自己写或用kornia。别在 dataset 里对 L 做 ImageNet 均值方差归一化后又忘了对 ab 做同样处理这是新手最常见的翻车点。2.2 分辨率与下采样为什么上色网络常带 U 型结构灰度上色有个特点颜色在空间上是低频的。一片天空大概率是蓝的一块草地大概率是绿的局部像素的精确色度没那么重要。所以很多方案先把输入缩到 256x256 甚至 128x128 让网络预测 ab再上采样回原尺寸最后用原图 L 通道做引导滤波修正边缘。这就解释了为什么 U-Net 类结构在上色里特别常见编码器逐层下采样扩大感受野让网络“看到”足够大的区域再决定颜色解码器逐层恢复分辨率。跳跃连接把浅层的高频细节带回来避免上色后边缘糊成一团。如果你自己搭 DNN别一上来堆全连接卷积加下采样才是正路。3. 用 PyTorch 搭一个能跑通的 DNN 上色网络从数据到推理的最小闭环3.1 数据集准备与灰度-彩色配对生成上色是自监督任务任意一张彩色图都能造出训练对。把彩色图转灰度当输入原彩色图当标签。我一般用 COCO 或 ImageNet 的子集几千张就够跑出能看的效果。import torch from torch.utils.data import Dataset import cv2 import numpy as np class ColorizationDataset(Dataset): def __init__(self, paths, size256): self.paths paths self.size size def __len__(self): return len(self.paths) def __getitem__(self, idx): bgr cv2.imread(self.paths[idx]) bgr cv2.resize(bgr, (self.size, self.size)) lab cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB).astype(np.float32) L lab[:, :, 0] / 255.0 # 归一化到 [0,1] ab (lab[:, :, 1:3] - 128.0) / 128.0 # 归一化到 [-1,1] L torch.from_numpy(L).unsqueeze(0) # (1,H,W) ab torch.from_numpy(ab).permute(2, 0, 1) # (2,H,W) return L, ab逻辑说明unsqueeze(0)给 L 加通道维因为卷积要求 (C,H,W)。ab 从 (H,W,2) 转成 (2,H,W)。注意 OpenCV 读进来是 BGR转 Lab 前不用先转 RGBCOLOR_BGR2LAB直接吃 BGR。参数说明size设 256 是速度和质量的折中。想更清晰可以 512但显存翻四倍。batch size 在 8GB 显存上 256 分辨率大概能到 16。3.2 网络结构一个带跳跃连接的轻量 U-Netimport torch.nn as nn class ColorNet(nn.Module): def __init__(self): super().__init__() def block(i, o): return nn.Sequential( nn.Conv2d(i, o, 3, stride2, padding1), nn.BatchNorm2d(o), nn.ReLU(inplaceTrue)) self.enc1 block(1, 64) self.enc2 block(64, 128) self.enc3 block(128, 256) self.enc4 block(256, 512) self.up3 nn.ConvTranspose2d(512, 256, 2, stride2) self.up2 nn.ConvTranspose2d(512, 128, 2, stride2) self.up1 nn.ConvTranspose2d(256, 64, 2, stride2) self.out nn.Conv2d(128, 2, 1) # 输出 ab 两通道 def forward(self, x): e1 self.enc1(x) e2 self.enc2(e1) e3 self.enc3(e2) e4 self.enc4(e3) d3 self.up3(e4) d3 torch.cat([d3, e3], dim1) d2 self.up2(d3) d2 torch.cat([d2, e2], dim1) d1 self.up1(d2) d1 torch.cat([d1, e1], dim1) return torch.tanh(self.out(d1)) # 输出压到 [-1,1]逻辑说明编码器四次下采样解码器三次上采样并拼接对应层特征。最后tanh把输出限制在 [-1,1]和 ab 的归一化范围一致。输出通道是 2不是 3。参数说明ConvTranspose2d的stride2负责放大。拼接时通道数要对上up3输出 256 通道拼e3的 256 变成 512所以up2输入写 512。这个通道对齐是手搭 U-Net 最容易写错的地方报错就看 cat 前后的维度。3.3 损失函数与训练循环上色损失常用 L1因为它对颜色偏差的惩罚比 L2 更稳定L2 容易让网络输出“平均色”导致发灰。device cuda if torch.cuda.is_available() else cpu net ColorNet().to(device) opt torch.optim.Adam(net.parameters(), lr1e-3) criterion nn.L1Loss() for epoch in range(50): for L, ab in loader: L, ab L.to(device), ab.to(device) pred net(L) loss criterion(pred, ab) opt.zero_grad() loss.backward() opt.step() print(fepoch {epoch}, loss {loss.item():.4f})逻辑说明每个 batch 前向、算损失、反向、更新。打印最后一个 batch 的 loss 只为观察趋势。参数说明lr1e-3配 Adam 是稳妥起点loss 震荡就降到 3e-4。50 epoch 在几千张图上大概几小时取决于显卡。想加速可以先用 128 分辨率预训练再切 256 微调。3.4 推理与结果还原net.eval() with torch.no_grad(): L, _ dataset[0] pred_ab net(L.unsqueeze(0).to(device)).squeeze(0).cpu() pred_ab (pred_ab.permute(1, 2, 0).numpy() * 128.0 128.0) L_np (L.squeeze(0).numpy() * 255.0) lab np.concatenate([L_np[:, :, None], pred_ab], axis2) lab np.clip(lab, 0, 255).astype(np.uint8) bgr cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) cv2.imwrite(out.jpg, bgr)逻辑说明推理时把预测 ab 反归一化回 0-255和 L 拼成 Lab再转 BGR 保存。clip防止越界。参数说明net.eval()关掉 BatchNorm 的训练行为别忘了。如果结果整体偏色检查反归一化是不是漏了乘 128。4. 上色效果调优与常见翻车排查4.1 颜色发灰、饱和度不足怎么救现象推理结果整体像蒙了层灰颜色淡。原因通常是 L1 损失下网络倾向预测接近零的 ab因为零对应灰色风险最小。解决办法有三条一是损失里给 ab 加权比如对 a/b 通道乘 1.5二是改用带对抗损失的方案让判别器逼网络出鲜艳色三是后处理里对 ab 做饱和度拉伸简单粗暴但有效。# 后处理饱和度增强 pred_ab pred_ab * 1.3 # 在反归一化前乘系数参数说明系数 1.3 到 1.5 之间试太高会溢出产生色斑。4.2 大面积色块串色与边缘溢出现象天空被染成绿色人脸边缘出现彩色描边。原因是低分辨率预测的 ab 上采样后和原图边缘不对齐。解决用原图 L 通道做引导滤波把 ab 的边缘对齐到亮度边缘。# 引导滤波修正需 opencv-contrib guided cv2.ximgproc.guidedFilter(guideL_np.astype(np.uint8), srcpred_ab.astype(np.float32), radius8, eps1e-2)参数说明radius控制平滑范围8 左右eps越小越贴边缘1e-2 是常用值。4.3 训练 loss 不降或直接 NaN现象几个 batch 后 loss 变 NaN。原因多半是学习率太大或数据里有损坏图。解决先把 lr 降到 1e-4加梯度裁剪torch.nn.utils.clip_grad_norm_(net.parameters(), 1.0)并在 dataset 里对imread返回 None 的图做跳过。4.4 灰度输入本身带噪导致上色斑驳现象老照片扫描件有噪点上色后颜色一块块跳。原因网络把噪点当纹理去配色。解决输入前做非局部均值去噪或双边滤波别用高斯高斯会把边缘也糊掉。4.5 显存不够、batch 上不去现象CUDA out of memory。原因分辨率或模型通道数太大。解决降分辨率到 128或把编码器通道从 64 起改成 32 起或开混合精度torch.cuda.amp。我一般先降分辨率因为上色对分辨率没那么敏感。5. 让上色从“能跑”到“能用”验证方法与一个提色技巧训练完怎么判断模型好不好不能只看 loss。我习惯做三件事。第一固定几张验证图每个 epoch 存一次结果肉眼对比颜色是否越来越合理。第二算 PSNR 和 SSIM但要知道这两个指标对上色参考价值有限因为颜色多解指标高不代表好看。第三做用户偏好测试找几个人盲选这才是最终标准。一个具体提色技巧推理时对 ab 做温度采样。网络输出的是确定性值但你可以把它当分布均值加一点高斯噪声再解码多次采样取平均颜色会更自然。代码上就是在tanh输出后加torch.randn_like(pred)*0.05跑五次平均。噪声标准差 0.05 左右太大就花。preds [] for _ in range(5): p net(L.unsqueeze(0).to(device)) p p torch.randn_like(p) * 0.05 preds.append(p) pred_ab torch.stack(preds).mean(0).squeeze(0).cpu()参数说明采样次数 5 是速度和效果的平衡噪声系数 0.05 起步按结果微调。另外如果你的目标是历史影像批量上色别指望一个模型通吃。人像、风景、文档扫描件分布差异大我一般按类别各训一个小模型或者在一个模型里加类别条件。这个习惯帮我省了很多返工。希望帮到你。本文还有配套的精品资源点击获取
