
简介面向图像处理初学者的Python灰度图像彩色化实练资源围绕“给灰度图添加真实感色彩”这一典型实验任务提供可直接运行的源码与三页实验报告适合课程设计、实验复习或期末大作业借鉴。压缩包内共36个文件以py源码与pyc编译文件为核心搭配docx实验报告、jpg/png样例图片、xml配置及必要的依赖清单整体仅14.38MB目录将源码、样例图片与输出结果分开存放便于对照学习和按需查找。目前已有1373人学习下载。资源基于经典彩色化开源工程整理演示脚本覆盖读取灰度图、调用彩色化算法、输出彩色结果的完整流程实验报告不仅写明任务要求与实现思路还给出了参数调整方法和效果对比便于零基础读者理解图像特征计算与表示在彩色化任务中的实际作用进而扩展成自己的实验课题。1. 灰度图像彩色化一个没有标准答案的特征计算与表示问题把一张黑白照片变成彩色看上去是“无中生有”但仔细观察会发现真正决定天空该是蓝色、草地该是绿色的是图像里的边缘、纹理、物体形状这些局部特征。这正是“图像特征计算与表示”的核心作用灰度图像彩色化本质上是一个从亮度特征推断颜色特征的学习问题。它的做法是让网络把灰度图各像素点的特征算出来再映射到 Lab 色彩空间里缺失的 a、b 通道上。这类项目非常适合有 Python 基础、想通过一个小而完整的案例把数据集处理、模型训练和报告撰写串起来的同学源码结构不复杂换一张自己的照片就能跑。2. 为什么灰度图能被“算”出颜色Lab 空间与特征表示的选型2.1 为什么选 Lab 空间而不是 RGB/HSV很多人第一次做彩色化第一反应是在 RGB 空间里训练一个“灰度图 → 彩色图”的映射。这个思路表面可行实践时却很难收敛。RGB 三个通道的相关性极高灰度图丢失的是三个通道整体编码的色彩信息不是某一个通道。如果你把灰度图复制三次作为 RGB 输入等于让网络同时学习亮度、色调、饱和度三个纠缠在一起的东西输出容易偏色、发灰而且训练时 Loss 波动很大。HSV 空间也不行。H色调通道是一个环形值0 度和 360 度在数值上差 360但颜色上完全一致。直接用回归 Loss 去预测这种周期性数值模型会在色相边界上被“惩罚”错产生紫绿相间的奇怪条纹。工程上最稳的选择是 Lab 空间。Lab 把亮度 L 放在一个通道把颜色信息拆成 a、b 两个通道a 的正方向偏红、负方向偏绿b 的正方向偏黄、负方向偏蓝。这样做最大的好处是灰度图可以被直接当作 L 通道使用彩色化任务就从“预测三个通道”变成了“预测两个通道”。这也是学术界和工业界做灰度图彩色化的通用做法。实际使用中要注意 OpenCV 的读取习惯。cv2.imread 读出来是 BGR 顺序很多人写代码时用了 cv2.COLOR_BGR2LAB转回时却用了 cv2.COLOR_LAB2RGB通道顺序一错出来的图就是蓝绿互换的。下面这段是数据准备阶段最标准的转换流程import cv2 import numpy as np bgr cv2.imread(example.jpg) # 注意OpenCV 读进来是 BGR lab cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB) # 转 Lab 也要用 BGR2LAB L lab[:, :, 0] # 亮度通道值域 [0, 255]这就是灰度图 a lab[:, :, 1] # 绿-红通道值域 [-128, 127] b lab[:, :, 2] # 蓝-黄通道值域 [-128, 127] # 把 a/b 归一化到 [-1, 1]方便网络输出层用 tanh 激活 ab lab[:, :, 1:].astype(np.float32) / 128.0这里的归一化参数要根据激活函数来定。输出层用 tanh 时a/b 除以 128 正好落在 [-1, 1]L 通道除以 255 是常规亮度归一化。如果换了激活函数这两个除数也要跟着调。2.2 从特征计算到颜色表示传统方法与深度 CNN 的边界在深度学习流行之前灰度图彩色化主要有两条路基于参考图的颜色迁移和基于手工特征的最近邻匹配。颜色迁移的做法是找一张色调满意的参考图把目标灰度图的亮度分布和参考图对齐再把参考图的 a/b 通道直接搬过来。这个方法速度快、效果稳定但它要求参考图与目标图的场景内容相似否则天空会被染成树叶的绿色。手工特征最近邻的做法是把灰度图切块提取每个块的方向梯度直方图、局部二值模式等特征在带颜色的训练图库里检索最相似的块把对应颜色贴上去。它能处理一些内容不匹配的情况但块与块之间缺乏全局语义约束彩色化结果常常在物体边缘出现颜色渗漏。深度 CNN 能胜出靠的是特征表示的分层特性。浅层卷积核学到的是梯度方向、边缘极性这些小尺度特征中层开始组合出纹理周期深层则可以激活出“眼睛”“轮子”“窗户”这类高级语义。彩色化正好同时需要这两种尺度天空的蓝色来自大面积平滑区域的亮度统计而人的肤色需要局部边缘和形状一起判断。网络把两种特征融合起来就能在“无中生有”时给出合理猜测。方法是否需要训练是否依赖参考图边缘颜色一致性典型应用场景颜色迁移否是依赖人工选图单张快速出效果、老照片修复手工特征匹配是否一般块边缘易渗漏小规模实验、特征工程练习深度 CNN 回归是否好端到端学习批量彩色化、自然图像恢复这张对比表可以作为报告里“相关工作”部分的素材。3. 用 PyTorch 把灰度图变彩色最小可复现的源码流程3.1 源码骨架怎么组织文件划分与依赖这类彩色化项目源码通常不需要复杂工程结构三四个文件就够。我一般这样组织colorization/ ├── dataset.py # 数据读取、Lab 转换、归一化 ├── model.py # 网络结构定义 ├── train.py # 训练循环、Loss、保存权重 ├── infer.py # 加载权重、彩色化单张图片 └── color_net.pth # 训练产物Git 里一般不提交依赖方面只需要 torch、torchvision、numpy、opencv-python。Python 版本建议 3.8 或更高PyTorch 用 1.13 以上即可。这个组合的好处是每个文件都能单独运行验证写报告时也方便截图展示中间结果。小白容易犯的错是在 notebook 里把所有代码堆在一个 cell 里改网络结构时要连数据处理一起重跑。建议从一开始就按上面这个结构拆文件debug 的时候会省很多时间。3.2 从图像到训练张量读取、缩放、Lab 转换与归一化数据准备阶段要做三件事把图片统一缩放到固定尺寸转 Lab 空间把张量整理成“L 通道为输入、ab 通道为标签”的配对格式。下面是一份可以直接用的自定义 Datasetimport cv2 import numpy as np import torch from torch.utils.data import Dataset class GrayColorDataset(Dataset): def __init__(self, img_paths, size256): self.img_paths img_paths self.size size def __len__(self): return len(self.img_paths) def __getitem__(self, idx): bgr cv2.imread(self.img_paths[idx]) # BGR bgr cv2.resize(bgr, (self.size, self.size)) # 统一尺寸 lab cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB) L lab[:, :, 0].astype(np.float32) / 255.0 # 输入值域 [0, 1] ab lab[:, :, 1:].astype(np.float32) / 128.0 # 标签值域约 [-1, 1] L_t torch.from_numpy(L).unsqueeze(0) # (1, H, W) ab_t torch.from_numpy(ab).permute(2, 0, 1) # (2, H, W) return L_t, ab_tsize 参数直接决定显存占用和感受野。256×256 是训练效果和资源消耗的平衡点小于 128×128 时网络看不到足够的上下文彩色化结果会很花大于 384×384 时VGG 级别的 encoder 在单卡上很难跑大 batch。如果你的显卡只有 6GB 显存可以先用 128 跑通流程再换 256 调优。permute(2, 0, 1) 的作用是把 a/b 两个通道从最后一维移到最前面符合 PyTorch 的 NCHW 约定。新手经常忘了这一步训练时就会报维度不匹配的错。3.3 网络与训练循环关键参数和 Loss 选择彩色化任务不需要特别复杂的网络一个带跳连的小型 Encoder-Decoder 就够入门。我常用的结构是三层编码、两层解码在解码时把编码器同尺寸特征拼进来这样能缓解颜色边缘定位不准的问题import torch.nn as nn class ColorNet(nn.Module): def __init__(self): super().__init__() # 编码器下采样时通道数翻倍 self.enc1 nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(32, 32, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2)) self.enc2 nn.Sequential( nn.Conv2d(32, 64, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2)) self.enc3 nn.Sequential( nn.Conv2d(64, 128, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2)) # 解码器先上采样再与编码器同尺寸特征拼接 self.dec1 nn.Sequential( nn.Upsample(scale_factor2, modebilinear, align_cornersFalse), nn.Conv2d(128 64, 64, 3, padding1), nn.ReLU(inplaceTrue)) self.dec2 nn.Sequential( nn.Upsample(scale_factor2, modebilinear, align_cornersFalse), nn.Conv2d(64 32, 32, 3, padding1), nn.ReLU(inplaceTrue)) self.out nn.Sequential( nn.Conv2d(32, 2, 3, padding1), nn.Tanh()) def forward(self, x): e1 self.enc1(x) e2 self.enc2(e1) e3 self.enc3(e2) d1 self.dec1(e3) d1 torch.cat([d1, e2], dim1) d1 self.dec1[-1](d1) # 这里只取最后卷积ReLU避免重复上采样 d2 self.dec2(d1) return self.out(d2)这里有一个很容易绕晕的细节dec1 里已经包含了卷积但我在 forward 里先做 torch.cat 再用 dec1 的卷积层处理拼接后的张量所以注释里写了“只取最后卷积ReLU”。更清晰的写法是把上采样和卷积拆成两个层拼接后再过卷积。上面代码的关键是跳连通道数对应enc2 输出 64 通道dec1 上采样后拼接变成 128 64 192 通道下一层卷积输入的 in_channels 必须写成 192否则会报维度错误。训练循环用 L1 Loss 更合适。ab 通道的标签分布极不均匀大量像素的 a/b 值接近 0也就是灰色如果改用 MSELoss模型会倾向于把所有像素都预测成灰色来降低大误差项的惩罚结果就是彩色化效果“发灰、偏淡”。下面是完整的训练循环from torch.utils.data import DataLoader from torch.optim import Adam import torch dataset GrayColorDataset(img_paths, size256) loader DataLoader(dataset, batch_size16, shuffleTrue, num_workers2) model ColorNet() optimizer Adam(model.parameters(), lr1e-3) criterion nn.L1Loss() for epoch in range(30): model.train() total_loss 0.0 for L, ab in loader: pred model(L) loss criterion(pred, ab) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch}, loss: {total_loss / len(loader):.4f}) torch.save(model.state_dict(), color_net.pth)batch_size16 在 256×256 输入下已经比较吃显存了。如果你的显卡只有 6GB建议 batch_size 减到 8或者把 size 调成 128 跑通流程再逐步加大。Learning rate 从 1e-3 起步epoch 数可以按训练集大小调整几千张图 30 个 epoch 基本够。3.4 推理还原Lab 转回 RGB 的完整过程训练完成后推理阶段最大的坑是图像格式处理顺序。网络输入必须是归一化后的 L 通道输出是 tanh 压缩到 [-1, 1] 的 ab 通道。把 ab 乘回 128 后要先把输入灰度图还原成 0~255 的 L 通道再和 ab 拼成完整 Lab最后转 BGR 保存import cv2 import numpy as np import torch model.load_state_dict(torch.load(color_net.pth, map_locationcpu)) model.eval() gray cv2.imread(test.jpg, cv2.IMREAD_GRAYSCALE) gray cv2.resize(gray, (256, 256)) L_input gray.astype(np.float32) / 255.0 L_t torch.from_numpy(L_input).unsqueeze(0).unsqueeze(0) with torch.no_grad(): ab model(L_t)[0] # (2, H, W) ab_np ab.permute(1, 2, 0).numpy() * 128.0 # 还原到 [-128, 127] 区间 # 用原灰度图做 L 通道网络只负责生成 a/b lab np.zeros((256, 256, 3), dtypenp.float32) lab[:, :, 0] gray.astype(np.float32) lab[:, :, 1:] ab_np lab[:, :, 0] np.clip(lab[:, :, 0], 0, 255) # L 通道安全截断 lab[:, :, 1:] np.clip(lab[:, :, 1:], -128, 127) # a/b 范围截断 bgr cv2.cvtColor(lab.astype(np.uint8), cv2.COLOR_LAB2BGR) cv2.imwrite(colorized.jpg, bgr)这里强调一下L 通道必须用输入灰度图不能用网络预测的 L。网络只训练了 ab 通道的输出它的能力边界就在颜色预测上强行让它输出 L 只会引入额外偏差。最终效果好坏一部分取决于你训练数据里有没有和 test.jpg 相似的内容。数据里全是人脸模型的风景彩色化效果就会很随机。4. 灰度图彩色化的 5 个常见坑现象、原因与排查方法4.1 训练 Loss 降了输出却灰蒙蒙一片很多同学训练完第一个 epoch 就迫不及待去推理发现输出的图色彩饱和度极低整张图像蒙了一层灰。这不是训练没收敛而是 Loss 函数与标签分布不匹配。原因在于 ab 通道的标签分布极其不均匀。自然图像中大面积区域都是接近灰色的低饱和像素纯红、纯蓝这类高饱和像素占比很小。L1 Loss 会使模型通过“预测灰色”来最小化大多数像素的误差。解决方法是给 Loss 加权重让高饱和像素贡献更大的梯度。常见做法是按像素 ab 值距离中心的欧氏距离加权距离越远权重越大。实现时可以预先算一张权重图训练时对每个像素的 Loss 乘上对应权重。如果嫌麻烦至少要把 MSELoss 换成 L1Loss这能让输出饱和度提升不少。4.2 颜色边缘溢出像水彩画彩色化结果整体颜色是对的但物体的轮廓周围总有一圈颜色溢出来尤其是人物头发和背景的交界处。这个问题出在空间定位精度上。原因是网络连续两次 MaxPooling 下采样后特征图大小从 256×256 降到 64×64再进行上采样恢复尺寸时细节空间位置已经被压缩模糊了。ab 通道是逐像素预测任务对位置精度要求很高简单的 bilinear 上采样做不到像素级对齐。解决思路是加跳连这样深层语义特征和浅层边缘特征能在解码阶段融合。如果加了跳连还是有色偏可以把 nn.Upsample 换成 PixelShuffle 上采样方式让网络自己学习如何恢复空间细节。实际项目中我两者都用过跳连解决 80% 的溢出问题PixelShuffle 则是锦上添花。4.3 输出的图像色调完全离谱训练正常、Loss 正常但彩色化结果整体偏紫或偏绿像加了奇怪的滤镜。这种问题几乎总是出在数据预处理或后处理阶段而不是模型本身。最常见的翻车点是 OpenCV 的通道顺序。用 cv2.imread 读取彩色图是 BGR如果你训练时用了 cv2.COLOR_BGR2LAB推理转回时必须用 cv2.COLOR_LAB2BGR混用 RGB 转换函数会让 a/b 通道的符号互换红色变绿色、蓝色变黄色。排查方法很简单不要直接跳到模型推理先写一段代码读一张彩色图转 Lab 再转回 BGR保存后和原图对比如果颜色有偏差问题在转换逻辑如果一致才继续检查网络输出。4.4 显存不够训练直接中断在 256×256 输入下跑上面的 ColorNetbatch_size 开到 32 很容易触发 CUDA out of memory。这种问题不一定是显卡太差更多是参数配置不合理。解决有几个方向第一把输入尺寸降到 128×128 跑通全流程彩色化质量不会下降太多第二batch_size 减半配合梯度累积模拟大 batch第三检查 DataLoader 的 num_workers这个参数不会减少显存占用但会明显拖慢数据加载让人误以为程序卡死。如果你的显卡只有 4GB 显存还可以把编码器的初始通道从 32 减到 16。这样模型参数量减少四倍训练速度更快彩色化效果在缩略图上差距不大。4.5 复现开源源码效果和报告里不一致很多彩色化项目的主页展示效果惊艳但自己复现时怎么调都达不到那个水平。这不一定是你代码写错了而是报告里的效果图往往是精心挑选的。彩色化结果对训练集内容极其敏感。如果用 ImageNet 训练模型对场景理解很强换成自定义数据集效果会立刻回落。另外一个隐藏变量是随机种子数据 shuffle 顺序不同最终模型权重差异明显。建议固定 torch.manual_seed 和 numpy.random.seed 再训练。复现不重要更重要是理解实验逻辑。我一般会把注意力放在“模型 A 比模型 B 的 PSNR 高多少”这类相对比较上而不是纠结单张图的视觉质量。5. 想让结果更像原图色调推理阶段做一次颜色迁移微调纯靠网络预测的彩色化结果在观感上经常差一口气尤其训练数据不充足时输出颜色会偏灰偏淡。有个不增加训练成本的技巧能在推理阶段直接把结果拉回来拿一张同风格的参考图对预测的 ab 通道做一次统计颜色匹配。这个思路来自经典的颜色迁移方法核心是让预测图的 ab 通道均值和标准差向参考图靠拢。实现很简短30 行以内搞定def color_transfer(ab_pred, ab_ref): # ab_pred: (H, W, 2) 预测的 ab 通道 # ab_ref: (H, W, 2) 参考图的 ab 通道 mean_p ab_pred.reshape(-1, 2).mean(axis0) std_p ab_pred.reshape(-1, 2).std(axis0) mean_r ab_ref.reshape(-1, 2).mean(axis0) std_r ab_ref.reshape(-1, 2).std(axis0) ab_new (ab_pred - mean_p) / (np.maximum(std_p, 1e-5)) * std_r mean_r return np.clip(ab_new, -128, 127)用法是在保存最终结果前先用参考图计算 mean_r 和 std_r再对网络输出的 ab 做上述变换。参考图的选择很关键风景图配风景图、人像配人像风格越接近结果越自然。这个后处理不会改变图像的空间结构只调整色调分布所以不用担心产生伪影。我实际做这类项目时习惯先看网络裸输出再决定要不要做颜色迁移。如果裸输出已经接近预期就不用画蛇添足如果颜色发灰先用这招救急再回头检查训练集的色彩分布是否有问题。这个技巧也适合写进报告的实验部分作为“后处理对彩色化效果的影响”一节的数据支撑。希望这个从原理到避坑的流程能帮到你少走我之前走过的弯路。本文还有配套的精品资源点击获取