ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于深度学习CNN实现图像着色:从Lab色彩空间到完整训练实战

基于深度学习CNN实现图像着色:从Lab色彩空间到完整训练实战 简介这份资源面向计算机相关专业的在校学生、教师及企业员工提供一套基于深度学习CNN网络实现图像着色的完整Python源码可用于课程设计、毕业设计、大作业或初期项目立项演示。压缩包共40个文件约17.52MB包含14个py源码文件、12张png效果图、10个pyc编译文件以及txt说明与pdf参考文献源码按colorizers模块组织涵盖eccv16、siggraph17、base_color、util等核心实现并附有caffe_v1、torch_v1等不同版本脚本便于对比学习。目前已有50人学习下载。项目代码完整且经过运行验证读者可借此理解CNN图像着色原理、模型加载与推理流程参考效果图与文献快速上手并在此基础上进行二次开发或功能扩展具有较高的学习借鉴价值。1. 从一张泛黄老照片说起CNN 图像着色到底在做什么翻出家里那张泛黄的全家福人脸是灰的衣服是灰的连背景那棵树都像蒙了一层雾。你可能会想能不能让程序自动给它上色这正是「基于深度学习 CNN 网络实现图像着色」要解决的问题。它的输入是一张灰度图单通道 L 通道输出是预测出的 a、b 两个颜色通道拼回 Lab 空间再转成 RGB就得到一张彩色图。和传统手工上色比CNN 方案不需要逐帧抠图训练完成后一张 256×256 的图推理只要几十毫秒。适合谁有 Python 基础、想跑通第一个深度学习视觉项目的同学以及需要批量处理老照片、漫画线稿、监控灰度帧的工程师。热搜里「深度学习入门」「cnn 基础」「python 深度学习教程」这几个词基本就是冲着这类项目来的——它足够小小到一张消费级显卡就能训又足够完整完整到能让你把数据加载、卷积、损失、推理整条链路走一遍。需要先泼一盆冷水图像着色不是「还原真实颜色」而是「预测一个看起来合理的颜色」。同一件衣服模型可能给你上成蓝色也可能上成棕色两者都不算错。理解这一点后面调参和评估时心态会稳很多。这个项目真正值钱的地方不是那几行卷积代码而是你会亲手处理 Lab 色彩空间、损失函数权重、以及灰度图与彩色图配对这套数据管线——这些经验换到去雾、超分、风格迁移上都能复用。2. 先把 Lab 色彩空间和 CNN 结构讲透再谈写代码2.1 为什么图像着色几乎都用 Lab 而不是 RGBRGB 三个通道高度相关你把 R 改了G、B 的合理范围也跟着变网络要同时学三个通道的联合分布难度大。Lab 的设计初衷就是让亮度和颜色分离L 管明暗a 管绿到红b 管蓝到黄。灰度图天然就是 L 通道网络只需要预测 a、b 两个通道输出维度直接砍掉三分之一任务也从「生成整张图」变成「给已知亮度配颜色」收敛快很多。具体数值范围要记牢这是后面归一化的依据通道含义原始范围常用归一化L亮度0 ~ 100除以 100 映射到 0~1a绿(-) 到 红()-128 ~ 127除以 128 映射到 -1~1b蓝(-) 到 黄()-128 ~ 127除以 128 映射到 -1~1常见做法是只对 a、b 做归一化L 保持 0~1。推理时把预测的 a、b 乘回 128和 L×100 一起塞进cv2.cvtColor转 RGB。这里有个高频翻车点OpenCV 读进来默认是 BGR转 Lab 前必须先转 RGB否则颜色整体偏。我一般会在数据加载函数第一行就写死img cv2.cvtColor(img, cv2.COLOR_BGR2RGB)省得后面查半天。2.2 一个能跑通的编码器-解码器 CNN 长什么样图像着色的经典结构是「编码器下采样 解码器上采样」中间可以加跳跃连接。编码器负责从灰度图里提取边缘、纹理、语义这是天空、那是草地解码器把这些特征还原成和原图同尺寸的 a、b 通道。下面是一个最小可用版本输入 256×256 灰度图输出 256×256×2。import torch import torch.nn as nn class ColorNet(nn.Module): def __init__(self): super().__init__() # 编码器逐步下采样通道数翻倍 self.encoder nn.Sequential( nn.Conv2d(1, 64, 3, stride2, padding1), # 256 - 128 nn.ReLU(inplaceTrue), nn.Conv2d(64, 128, 3, stride2, padding1), # 128 - 64 nn.ReLU(inplaceTrue), nn.Conv2d(128, 256, 3, stride2, padding1),# 64 - 32 nn.ReLU(inplaceTrue), ) # 解码器逐步上采样通道数减半 self.decoder nn.Sequential( nn.ConvTranspose2d(256, 128, 4, stride2, padding1), # 32 - 64 nn.ReLU(inplaceTrue), nn.ConvTranspose2d(128, 64, 4, stride2, padding1), # 64 - 128 nn.ReLU(inplaceTrue), nn.ConvTranspose2d(64, 2, 4, stride2, padding1), # 128 - 256 nn.Tanh() # 输出限制在 -1~1正好对应归一化后的 a、b ) def forward(self, x): x self.encoder(x) x self.decoder(x) return x逻辑说明编码器三次 stride2 卷积把 256 压到 32感受野足够覆盖语义区域解码器用转置卷积逐级还原最后Tanh把输出卡在 -1~1和 a、b 的归一化范围对齐。参数说明卷积核统一 3×3转置卷积用 4×4 保证尺寸对齐通道数 64→128→256 是显存和表达力的折中8G 显存跑 batch16 没问题。想加深可以再叠一层但要注意下采样太狠会丢细节人脸五官容易糊。提示最后一层千万别用 ReLU它会把负值全砍掉a 通道的绿色和 b 通道的蓝色直接消失输出永远偏红偏黄。2.3 损失函数怎么选L1 打底分类损失补语义只用 L1 或 L2 回归 a、b模型会倾向于输出「平均色」——天空一律灰蓝草地一律暗绿整张图发灰。原因是颜色分布是多峰的回归到均值损失最小。常见做法是引入分类思想把 a、b 各自量化成 313 个色块这是业内的经典做法预测每个像素落在哪个色块上用交叉熵。实操里我一般用「L1 主损失 分类辅助损失」的组合权重 1:0.5 起步。def loss_fn(pred_ab, true_ab, pred_logitsNone, true_binsNone): l1 nn.functional.l1_loss(pred_ab, true_ab) if pred_logits is not None: ce nn.functional.cross_entropy(pred_logits, true_bins) return l1 0.5 * ce return l1参数说明pred_ab是网络回归输出pred_logits是额外分类头输出可选。如果嫌分类头麻烦先纯 L1 跑通观察验证集上色是否发灰再决定要不要加。这个 0.5 的权重不是玄学调大到 1.0 颜色更鲜艳但容易溢出调小到 0.2 又退回发灰建议在验证集上扫一遍。3. 数据管线与训练脚本从灰度图到彩色图的完整复现3.1 数据集准备与配对样本生成着色任务的数据集不需要人工标注任何彩色图都能自动生成训练对把彩色图转 LabL 当输入a、b 当标签。所以理论上 ImageNet、COCO 甚至你自己拍的照片都能用。我一般选 1 万到 5 万张图起步太少容易过拟合太多单卡训不动。import cv2 import numpy as np import os def make_pair(img_path, size256): img cv2.imread(img_path) if img is None: return None img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 先转 RGB img cv2.resize(img, (size, size)) lab cv2.cvtColor(img, cv2.COLOR_RGB2LAB) # 再转 Lab L lab[:, :, 0] / 100.0 # 归一化到 0~1 ab lab[:, :, 1:] / 128.0 # 归一化到 -1~1 L L[None, :, :].astype(np.float32) # 加通道维 ab ab.transpose(2, 0, 1).astype(np.float32) return L, ab逻辑说明make_pair把任意彩色图变成 (L, ab) 训练对。参数说明size256是显存和细节的平衡点想上 512 得把 batch 降到 4 左右/100.0和/128.0必须和网络输出的 Tanh 范围对应改一个就得改另一个。注意cv2.cvtColor转 Lab 时OpenCV 的 L 范围是 0~255 不是 0~100所以严格来说应该除以 255但除以 100 后配合 Tanh 也能训只是数值略偏——这是很多人第一次跑发现颜色发暗的原因。稳妥做法是统一用L/255.0然后推理时乘回 255。3.2 训练循环与关键超参from torch.utils.data import Dataset, DataLoader class ColorDataset(Dataset): def __init__(self, paths): self.paths paths def __len__(self): return len(self.paths) def __getitem__(self, i): pair make_pair(self.paths[i]) if pair is None: return self.__getitem__((i 1) % len(self.paths)) return pair loader DataLoader(ColorDataset(img_paths), batch_size16, shuffleTrue, num_workers4) model ColorNet().cuda() opt torch.optim.Adam(model.parameters(), lr1e-4) for epoch in range(50): for L, ab in loader: L, ab L.cuda(), ab.cuda() pred model(L) loss loss_fn(pred, ab) opt.zero_grad() loss.backward() opt.step() print(fepoch {epoch} done)参数说明lr1e-4是 Adam 的稳妥起点loss 震荡就降到 5e-5batch_size16对应 8G 显存12G 可以上 32num_workers4取决于 CPU 核数设太大反而拖慢。训练 50 轮在 1 万张图上大约几小时验证集 loss 连续 5 轮不降就可以停。这里没有写验证逻辑实际项目一定要切 5% 做验证否则你不知道是收敛还是过拟合。3.3 推理与上色结果保存def colorize(model, gray_path, out_path): img cv2.imread(gray_path, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (256, 256)) L (img / 255.0)[None, None, :, :].astype(np.float32) with torch.no_grad(): ab model(torch.from_numpy(L).cuda()).cpu().numpy()[0] ab ab.transpose(1, 2, 0) * 128.0 # 还原 a、b L_full img.astype(np.float32) # 用原灰度当 L lab np.concatenate([L_full[:, :, None], ab], axis2) rgb cv2.cvtColor(lab.astype(np.uint8), cv2.COLOR_LAB2RGB) cv2.imwrite(out_path, cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR))逻辑说明推理时 L 用原始灰度值0~255a、b 用网络预测乘 128 还原拼成 Lab 后转 RGB 保存。参数说明IMREAD_GRAYSCALE保证输入是单通道保存前转回 BGR 是因为cv2.imwrite按 BGR 写。如果输出整体偏色先检查这里 L 的尺度是否和训练时一致——训练用 L/255推理也必须用 L/255 再乘回尺度不匹配是发灰发暗的头号原因。4. 避坑与排查着色项目里最容易翻车的 5 个地方4.1 输出一片灰颜色像蒙了层雾现象推理结果有颜色但饱和度极低天空是灰蓝人脸是灰黄。原因纯 L1 回归导致模型输出均值色或者 Tanh 输出被后续错误缩放。解决加入分类辅助损失或在推理时对 a、b 做适度饱和度增强乘 1.2~1.5 后 clip 到 -128~127。先确认不是尺度问题再动损失。4.2 颜色整体偏红或偏蓝现象所有图都蒙一层暖色或冷色。原因训练时 BGR/RGB 转换顺序错了或者 L 归一化用了 /100 而推理用了 /255。解决统一在数据入口写死BGR2RGBL 统一用 /255训练推理共用同一个预处理函数别在两处各写一遍。4.3 人脸、文字区域糊成一团现象大色块还行五官和细小文字完全丢失。原因下采样太深或者解码器没有跳跃连接细节特征在瓶颈层被压没了。解决在编码器和解码器对应层之间加 U-Net 式跳跃连接把浅层的高频特征直接传过去或者把输入尺寸从 256 提到 384。4.4 训练 loss 降不下去一直在高位震荡现象loss 从第一轮就卡在 0.1 左右不降。原因学习率太大或者数据里有大量损坏图片导致梯度异常。解决lr 降到 1e-4 以下在 Dataset 里过滤掉imread返回 None 的样本并加torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)做梯度裁剪。4.5 显存爆了batch 只能设到 2现象CUDA out of memory。原因256×256 的转置卷积中间特征图很大或者 num_workers 开的进程各自缓存了数据。解决把 batch 降到 8 并开启混合精度torch.cuda.amp或者把编码器通道从 256 降到 128。混合精度通常能省 40% 显存几乎不掉精度是性价比最高的一招。5. 让着色结果更耐看的三个进阶技巧跑通基础版之后你会发现结果「能用但不够惊艳」。下面三个技巧是我实际项目里反复验证有效的按投入产出比排序。第一个是在 Lab 之外引入感知损失。L1 只关心像素差不关心「看起来像不像」。把预训练 VGG 的某几层特征拿出来比较生成图和原图的特征距离能让纹理和边缘更自然。代价是要多加载一个 VGG显存和速度都会涨建议只在最后 10 轮微调时开启。vgg torchvision.models.vgg16(pretrainedTrue).features[:16].cuda().eval() def perceptual_loss(fake_rgb, real_rgb): f1 vgg(fake_rgb) f2 vgg(real_rgb) return nn.functional.l1_loss(f1, f2)参数说明取features[:16]是前三个卷积块兼顾浅层纹理和深层语义权重一般设 0.1太大反而会让颜色变淡。注意 VGG 输入要归一化到 ImageNet 的均值和方差别直接塞 0~1 的图。第二个是用类别先验约束颜色。天空大概率是蓝的草地大概率是绿的这些先验能显著减少离谱上色。做法是在解码器后接一个小的分类头预测每个像素的语义类别天空/植被/建筑/人再查一张类别到颜色分布的查找表做加权。这个思路在「基于深度学习的口腔疾病图像识别系统」这类医学影像项目里也常见——先分类再细化比端到端硬回归稳。第三个是推理时做多尺度融合。同一张图缩放到 256、384、512 各跑一次把 a、b 结果按双三次插值对齐后取平均。大尺度抓语义小尺度抓细节融合后边缘更干净。代价是推理时间翻三倍适合离线批处理不适合实时。技巧显存增量训练时间增量效果提升感知损失30%20%纹理明显更自然类别先验10%15%减少离谱颜色多尺度融合仅推理无边缘更干净最后说个我自己的习惯每次改完损失或结构先拿同一张老照片跑一遍肉眼对比别只看 loss 数字。着色这任务指标好看不代表好看人眼才是最终裁判。我踩过最深的坑就是盯着验证 loss 调了两天结果出图还不如第一版——从那以后我固定留 10 张「基准图」任何改动都先过这 10 张的眼。希望帮到你。本文还有配套的精品资源点击获取
返回列表