1. 项目背景与整体设计思路
1.1 图像着色到底是什么,为什么值得做
图像着色(Image Colorization)这个方向,我最早接触是在计算机视觉课程的项目展示上。当时看到一张黑白的老照片经过模型处理后变成了色彩自然的彩色照片,第一反应是“这东西有点魔法”,后来自己动手做毕业设计时才发现,魔法背后全是对图像结构、色彩分布和模型设计的深度理解。
从任务定义上看,图像着色是典型的“病态问题”(ill-posed problem)。一张灰度图,每个像素只有亮度信息,没有色度信息,而现实中同一个物体可以有很多种合理的颜色。比如一件衣服,可能是红色也可能是蓝色,人和机器都没有办法从灰度信息中确定唯一的答案。所以这个任务本质上是让模型去学习“场景语义 + 颜色分布”之间的映射关系。
正因为这种不确定性,图像着色非常适合用来检验模型对图像语义的理解能力。模型如果能给天空上蓝色、给草地染上绿色、给皮肤赋予合理的肤色,说明它真的“看懂”了画面里的内容,而不只是在做简单的颜色填充。
作为毕业设计选题,图像着色有几个天然优势:
- 视觉效果好,演示时很容易让评委和同学get到项目价值
- 技术栈成熟,有大量开源论文和代码可以借鉴
- 问题定义清晰,评价标准相对明确(PSNR、SSIM、用户主观评分)
- 可以做成完整的应用系统,而不只是训练一个模型
1.2 为什么选深度卷积神经网络而不是传统方法
在深度学习普及之前,图像着色主要靠两类方法。一类是用户交互式着色,比如在Photoshop里手动给区域指定颜色,算法负责扩散;另一类是基于图像配准的方法,找参考图然后把颜色迁移过来。
传统方法的本质问题是“依赖外部信息”。交互式方法需要人提供色彩种子,参考图方法需要找到内容匹配的彩色图,这在真实场景里往往做不到。而深度CNN模型可以直接从大量数据中学习灰度图和彩色图之间的统计规律,输入一张灰度图直接输出三个通道的颜色结果,全自动、无需人工干预,这正是它能够成为主流方案的根本原因。
1.3 项目的功能定位与系统边界
我做的这个项目,定位是一个“研究 + 应用”双层结构的系统,而不是仅仅训练一个模型就结束。
研究层面要做的事情包括:
- 对比不同网络结构对着色效果的影响
- 比较不同颜色空间(RGB与Lab)下的训练差异
- 分析不同损失函数对结果饱和度、真实感的影响
应用层面要做的事情包括:
- 封装一个简单的图形界面,支持用户上传黑白图片
- 调用训练好的模型进行推理,展示着色前后对比图
- 支持批量处理,让用户一次导入多张图片
这套设计的核心思路是:先通过对比实验确定核心方案,再围绕这个方案搭建可用的应用系统。研究部分给系统提供了理论支撑,系统部分给研究结果一个可感知的落地载体,两者互相印证。
2. 核心技术方案与选型分析
2.1 为什么采用Lab颜色空间而不是RGB
先说结论:做图像着色,强烈建议用Lab颜色空间作为训练和推理的色彩表示。这一点几乎是所有主流着色模型(比如经典论文里的做法)的共识,原因可以从三个层面理解。
第一个层面是任务分解的合理性。RGB空间里三个通道R、G、B都是颜色通道,输入输出之间没有天然的解耦方式。而Lab空间把图像拆成了L通道(亮度)和a、b通道(色彩),灰度图本质上就是Lab空间里a、b通道为零的图。所以着色任务可以简洁地描述为:保留输入灰度图的L通道不变,预测缺失的a、b通道,最后把L、a、b三个通道合并还原成彩色图。这种“一个负责结构、两个负责色彩”的分解方式,让模型的优化目标变得非常清晰。
第二个层面是感知均匀性。RGB空间里两个颜色之间的数值距离,和人的视觉感知距离并不一致。而Lab空间的设计目标就是感知均匀,两个颜色点的欧氏距离大致反映了人眼感知的色彩差异。这意味着用L2损失在Lab空间优化,比较接近在感知空间优化。
第三个层面是实际训练效果。我做过对比实验,同一个U-Net结构,RGB空间下模型输出的颜色经常偏灰、偏暗淡,而Lab空间下训练大约相同的epoch,色彩饱和度明显更好。原因在于Lab空间把亮度信息和色彩信息分离后,模型不需要同时拟合两个耦合在一起的任务,学习难度降低了。
2.2 模型结构选择:U-Net为什么比普通CNN更合适
最初的实验我尝试了普通的卷积编码器-解码器结构:输入灰度图,经过几层卷积和池化逐步降低分辨率,到达瓶颈层后再通过上采样恢复到原始尺寸。
这个结构能跑,但效果不太好。问题在于池化和多层卷积会让浅层的细节信息逐渐丢失,上采样之后的解码器只能“凭记忆”重建细节,结果就是着色后的图像轮廓模糊,细小物体比如树枝、发丝、眼镜框等会出现彩色溢出或者颜色错误。
后来我把结构换成了U-Net,问题明显改善。U-Net的核心设计是编码器和解码器之间加了跳跃连接(skip connection),把编码器各层的特征图直接拼接到解码器的对应层上。
打个比方,普通编码器-解码器像是“口头传话”,信息经过一层层传递后细节已经丢失得差不多了;U-Net则是“每层都递笔记”,解码器在恢复分辨率的同时,能直接拿到编码器各阶段的原版特征图做参考。对于着色任务来说,这些浅层特征包含了边缘、纹理、小物体边界等空间细节,对生成锐利、准确的彩色边界至关重要。
在实现层面,U-Net的几个关键配置也经过了实际调参验证:
- 编码器部分:4层下采样,每层两个3x3卷积 + BatchNorm + ReLU
- 瓶颈部分:保持最低分辨率特征,增加通道数
- 解码器部分:4层上采样,使用转置卷积或双线性插值上采样
- 跳跃连接:每层用concat方式融合,而不是add
2.3 损失函数的选择与搭配策略
损失函数是训练着色模型的核心调节阀,它决定了模型会倾向生成什么样的颜色。
我实验的第一版用的是纯L2损失,即在Lab空间的a、b通道上计算预测值与真实值的均方误差。L2损失在训练过程中非常稳定,梯度平滑,不容易导致训练发散,但它的问题是会让模型选择“平均色”作为最优解。因为L2损失假设误差是高斯分布,模型为了最小化整体误差,会对每个像素输出所有可能颜色的均值,结果就是颜色发灰、发闷,缺少那种鲜艳的视觉冲击力。
针对这个问题,主流方案有两种解决思路。一种是改用分类损失,把a、b通道的取值量化为若干离散的bin,让模型输出每个bin的概率分布;另一种思路是混合使用多种损失函数,让不同损失优势互补。
我在项目中采用的是加权组合方案:L2损失作为基础约束,同时加入感知损失(perceptual loss)。感知损失不是直接在像素空间比较,而是把预测图和真实图分别送入一个预训练好的分类网络(我这里用VGG16),在网络的中间层提取特征,然后计算特征之间的L2距离。
直观理解就是:L2损失让颜色“大致对”,感知损失让颜色“看起来对”。感知损失约束的是图像的语义内容和整体结构,训练出来的结果在边缘保持、语义颜色准确性方面都有明显提升。
实验中我还尝试过对抗损失,用PatchGAN判别器判断输出是真实彩图还是模型生成的彩图。对抗损失的效果是色彩更加鲜明生动,但训练难度显著增加,需要反复调整G和D的训练节奏,否则很容易出现模式崩塌。考虑到毕业设计周期有限,最终正式版本没有把对抗损失放进主流程,这个取舍在答辩时也解释了,评委普遍认可。
2.4 数据集选择与预处理细节
数据集我用了两个:Places365的一个子集和ImageNet的部分类别。Places365的优势是场景类数据丰富,包含大量自然风景、城市建筑、室内空间等图像,这些图像的色彩分布比较有规律;ImageNet的优势是物体类别多样,可以帮助模型学习到更多种类的物体颜色。
预处理方面有几个细节值得记录:
- 统一缩放到256x256,这个分辨率在训练速度和效果之间比较平衡
- 随机水平翻转做数据增强,这对着色任务有效,因为左右翻转不会改变物体颜色的语义
- 转成Lab空间后,L通道作为输入,a、b通道作为监督信号
- 对a、b通道做标准化,避免数值范围过大影响训练
注意:数据预处理时,要保证转Lab空间的代码和颜色空间参数在不同库版本之间一致。我踩过一个坑,训练时用的是某版本库的转换函数,推理时用了另一个实现,结果同样是灰度图输入,输出的颜色偏了一截,排查了很久才发现是色彩空间转换不一致。
3. 应用系统设计与实现过程
3.1 系统整体架构与模块划分
研究模型训练完成之后,下一步就是搭建应用系统。整个系统我划分成了四个模块:前端界面模块、后端服务模块、模型推理模块、结果管理模块。
前端界面模块负责用户交互,主要功能包括上传图片、显示着色结果、对比查看、批量模式切换。后端服务模块负责接收前端的请求、调度模型推理、处理异常情况。模型推理模块是核心,负责任务加载模型、执行前向计算、把Lab输出合并转换成RGB图。结果管理模块负责保存处理前后的图片,生成对比缩略图,记录处理时间等日志信息。
因为推理任务对实时性要求不高,我采用了同步处理的方式,用户上传图片后,接口给前端返回一个任务ID,前端在收到结果之前显示处理中的状态。批量处理则采用了一个简单的任务队列,逐张处理并实时更新进度。
3.2 PyTorch模型导出与推理优化
模型训练框架用的PyTorch,训练完成之后保存的是状态字典,格式为.pt或.pth。但应用系统里直接加载PyTorch模型有一个问题:依赖PyTorch运行环境,部署比较重,而且首次加载速度偏慢。
我把模型做了两步优化。第一步是把训练好的模型冻结参数,转换为推理模式,然后导出为TorchScript格式。TorchScript的好处是脱离开了Python的动态图绑定,模型结构被固化成一个静态计算图,可以在C++环境下运行,推理速度也会有一定提升。
第二步是采用半精度推理(FP16)。在显卡支持的情况下,把模型权重从FP32转为FP16,并用torch.cuda.amp的自动混合精度方式做推理。实测下来,转为FP16后模型文件体积缩小约一半,推理速度提升约60%,着色质量肉眼几乎看不出差别。
3.3 图形界面的交互设计
图形界面我用了PySide6(Qt for Python)实现。选择它的原因很简单:Python生态配合好,控件丰富,打包发布也比较成熟。
界面分成三个区域:
- 左侧是操作区:上传按钮、批量模式开关、模型选择下拉框、处理按钮
- 中间是预览区:并排显示原图和着色结果,支持鼠标滚轮缩放
- 底部是状态栏:显示当前处理进度、单张耗时、总耗时
这里有一个交互细节值得说明:模型推理本身不需要太多时间,但读取大图、缩放、格式转换这些I/O操作容易让用户感觉“卡住了”。所以我在处理流程中加了一个独立的进度反馈线程,实时更新界面上的进度条,避免界面假死。这个小改进对用户体验的提升非常明显,答辩演示时整个流程显得很流畅。
3.4 批量处理与业务场景结合
批量处理是这个系统实用性最强的部分。比如用户拿了一组几十张的老照片,一张张上传太慢了,批量模式可以一次导入整个文件夹。
批量处理流程实现如下:
- 遍历文件夹,过滤出.jpg、.png、.bmp等格式的图片
- 按文件名排序,保证处理顺序稳定
- 对每张图片执行:读取 → 缩放 → 转Lab → 推理 → 合并 → 转RGB → 保存
- 处理失败的图片记录到日志中,不中断整个队列
考虑到内存占用问题,批量模式中我限制了一次最多处理200张图片,并且处理完一张就释放一张的中间张量,避免长时间运行导致内存持续增长。这些细节在平时开发时容易被忽略,但在实际使用中非常关键。
4. 训练过程与参数调优实录
4.1 训练环境配置与训练流程
训练环境的配置是决定项目进度的重要因素。我的配置是单张消费级显卡(8GB显存),CPU为16核,内存32GB。这个配置训练一个基础版本的U-Net需要大约6到8小时跑50个epoch。
训练流程中最重要的几个环节是:学习率调度、梯度裁剪、模型保存策略。
学习率调度方面,我采用了阶梯式衰减方式。初始学习率设为2e-4,每20个epoch下降为原来的十分之一。前20个epoch是快速收敛阶段,模型学习全局色彩分布;中间20个epoch是细化阶段,学习率降低让参数更新更精细;最后的epoch阶段学习率已经很小,主要做微调收敛。
梯度裁剪是一个非常容易被忽视但至关重要的细节。在这个项目中,由于输入图像差异较大,个别样本的梯度范数可能异常大,导致训练震荡。我在优化器更新前加了一步梯度裁剪,把全局梯度范数限制在10以内。加入之后训练的稳定性明显提升,val loss没有出现大起大落的情况。
模型保存策略采用的是“最优模型 + 最近模型”双保险机制。每个epoch结束后验证一次在验证集上的PSNR指标,如果比历史最优值高,就保存一份best模型;同时每隔5个epoch保存一份常规模型,防止训练意外中断导致前面的努力白费。
4.2 关键超参数的实验对比与结论
训练过程中,我系统性做了几组对照实验。以下是部分记录:
| 实验组 | 结构 | 损失函数 | 训练epoch | 验证PSNR | 主观效果 |
|---|---|---|---|---|---|
| A组 | 普通编解码 | L2 | 50 | 23.8 | 色彩暗淡,边缘模糊 |
| B组 | U-Net | L2 | 50 | 26.1 | 结构清晰,颜色偏灰 |
| C组 | U-Net | L2 + 感知损失 | 50 | 25.4 | 色彩较生动,细节锐利 |
| D组 | U-Net + 残差块 | L2 + 感知损失 | 50 | 25.9 | 综合效果最佳 |
一个有意思的现象是:加入感知损失后,PSNR反而比纯L2低了。这与理论是一致的,因为PSNR直接衡量像素级误差,L2损失优化方向就是最小化这个误差;而感知损失优化的是高维特征的距离,牺牲了一点像素精度换来了更好的感知质量。所以评价模型不能只看PSNR,必须结合用户主观评价。
模型深度方面我也做了一个小实验,把编码器的下采样层从4层增加到5层,结果在256x256输入下效果没有明显提升,但参数量和推理时间增加了接近一倍。考虑到系统实际运行效率,最终还是选择了4层的配置。
4.3 模型训练中关于数据增强的实战经验
数据增强这个环节,我一开始只做了水平翻转,效果已经不错,后来逐步加入了以下策略:
- 随机亮度调整:在L通道上做小幅gamma变换,增强模型对过曝、欠曝图片的适应性
- 随机裁剪:先缩放再裁剪一个较大的区域作为训练图
- 随机模糊:以小概率对输入图做轻微高斯模糊,模拟老照片的模糊质感
加入这些增强后的模型,在真实老照片上的泛化能力有明显提升。最开始没有做增强时,模型对清晰的现代图片着色效果很好,但一遇到老照片自带的噪点、划痕、模糊,输出结果就会出现颜色斑块。做了增强之后,这类问题少了很多。
有一点要提醒:对着色任务做数据增强时要非常谨慎,不能做改变色彩语义的增强,比如随机调整饱和度、色调反转,这些会破坏训练数据的真实颜色分布,导致模型学到错误的映射。
4.4 推理阶段的后处理技巧
训练完成后,后处理环节对最终视觉效果的影响比预想的大。
第一个技巧是色彩校正。模型输出合并后的RGB图,整体有时会偏暗或偏灰。我在推理阶段加了一个可选的后处理:对输出图像的饱和度和亮度做轻微调整,常用参数是饱和度乘以1.1、亮度略微提升。这个操作直接提升了输出结果的视觉舒适度。
第二个技巧是边缘保留滤波。对输出结果做一次引导滤波(guided filter),以输入的L通道作为引导图。这样做的效果是:在颜色过渡比较突兀的区域,颜色分布会被L通道的细节结构约束,减少彩色边缘溢出。
第三个技巧是缩放策略。推理时如果输入不是256x256,直接resize到256x256会丢失细节,而直接原图推理又会导致显存溢出。我采用的方案是:长边超过1024的图片先按比例缩放到长边1024,推理完成后再用双三次插值放大回原尺寸。实测下来这个方案在速度和效果之间取得了较好的平衡。
5. 常见问题与排查技巧实录
5.1 训练阶段的常见问题与解决方案
训练过程中我遇到了不少问题,这里整理成一张速查表,方便以后有需要的人对照排查:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Loss震荡不收敛 | 学习率过大 | 降低学习率,添加学习率warmup |
| 输出全为灰色 | L2损失平均化 | 换用分类损失或添加感知损失 |
| 颜色有明显网格状伪影 | 转置卷积重叠 | 改用最近邻/双线性上采样 + 普通卷积 |
| 训练后期过拟合 | 数据量不足 | 增加数据增强,加入Dropout |
| 显存溢出 | 批量太大或图分辨率高 | 降低batch size,使用梯度累积 |
其中网格状伪影这个问题需要展开说一下。最早解码器我用了转置卷积做上采样,训练到后期发现输出的彩色部分出现棋盘格一样的伪影,尤其在人脸和天空区域明显。研究后发现这是因为转置卷积的核在重叠区域产生了不均匀的贡献,形成了一种周期性的亮度或色彩模式。
解决方案是放弃转置卷积,改用双线性插值上采样然后接一个3x3卷积来调整通道数。双线性插值的感受野比较平滑,不会产生周期性的棋盘格模式。这个改动在视觉质量上的提升非常直接。
5.2 推理阶段起作用的三个关键技巧
推理阶段遇到的第一个坑是颜色空间不一致。有一次我在集成系统时发现,训练时模型看到的是某个库转换出的Lab值,但系统推理时用了另一个实现,两个实现的缩放系数不同导致输入分布不一致,结果输出颜色整体偏绿。排查方法很简单:分别打印同一张图在不同转换函数下得到的L、a、b通道的数值范围,一对比差异就出来了。
第二个坑是batch size的影响。系统推理时如果一次处理多张图片,要保证每张图的尺寸一致。我最初没有对尺寸做统一处理,直接让模型处理不同分辨率的图,结果模型内部因为reshape不匹配直接报错。后来统一做padding到固定尺寸,或者自适应缩放后再推理,问题解决。
第三个技巧是热力蒸馏,这是我个人比较喜欢的一个手段。推理结束后,把生成的彩色图与原始灰度图做一次亮度融合保持,即最终输出图的L通道直接替换为输入图的L通道。这样能保证输出图像完全保留原始照片的纹理和光影细节,只增加色彩信息。这个步骤在技术上简单,但对结果真实感的提升非常显著。
5.3 应用系统层面的实战避坑
系统层面遇到的问题多是与性能和环境相关。
一个比较典型的场景是,在批处理大量图片时出现内存持续上升,长时间运行后系统变得卡顿甚至崩溃。排查后发现是PyTorch在CPU模式下推理时,每次前向计算产生的中间张量没有被及时释放。解决办法是在每张图推理完成后显式调用torch.cuda.empty_cache()(GPU模式)或者主动删除中间张量并调用垃圾回收。
另一个问题是多线程环境下模型推理的线程安全。刚开始我用共享模型实例处理并发请求,结果出现偶尔推理结果异常。后来改为为每个线程创建独立的模型副本,或者在请求处理前加锁,问题不再出现。
还有一次比较隐蔽的问题是模型部署时对比色差。同一张图片在开发机上的输出和部署机上差异明显,排查后发现是两台机器的显示色彩配置文件不同,以及图片在Qt中加载时默认色彩空间是sRGB,而保存时用了另外的色彩空间标签。统一了图片加载和保存时的色彩空间配置之后,色彩差异消除了。
6. 项目心得与后续扩展方向
6.1 从做项目中获得的认知升级
这个项目做完后,有几个体会是从课本上学不到的。
第一个体会是实验记录的重要性。训练深度学习模型的过程充满不确定性,版本迭代又多,如果没有系统记录每个实验的配置参数、损失曲线、验证指标,很容易陷入“改了参数但忘了改了啥”的困境。我在项目中用电子表格维护了一份每周更新的实验日志,包括数据集版本、模型结构改动、损失函数配置、学习率策略、关键指标截图。这份日志在写论文和答辩时发挥了巨大作用,每一个结论都能追溯到具体的实验证据。
第二个体会是“好结果不是调出来的,是设计出来的”。最初我以为模型的最终效果主要靠调参,做了大量实验后才意识到,结构设计上的合理性才是决定效果上限的因素。比如U-Net的跳跃连接、Lab空间的通道分解、混合损失函数的搭配,这些是从任务本质出发的设计选择,调参只是在这些选择的基础上做局部优化。
第三个体会是应用系统哪怕再简单,都要以用户视角检验。我的第一个界面版本功能完整但没有中间状态提示,点完按钮要等十秒才有反馈,体验很糟糕。加了进度条和状态提示之后,整个系统给人感觉完全不同。技术指标是硬实力,但用户体感是关键时刻能拿得出手的软实力。
6.2 这个系统还可以怎样扩展
如果后续要在这个基础上继续深入,我结合自己的经验提供几个方向。
技术层面可以考虑引入Transformer结构或者扩散模型,这两类方法在色彩生成的真实感方面已经超过了传统CNN方案。也可以做视频着色,把时间维度加进来,利用光流信息保持相邻帧的颜色一致性。还可以加入参考图引导功能,让用户提供一张参考彩色图,模型从这里提取色彩风格和颜色分布,实现更可控的着色。
应用层面可以考虑把系统封装成Web服务,用轻量化的后端框架暴露HTTP接口,前端做成浏览器内交互,这样不依赖本机环境,用户体验更好。也可以针对老照片修复场景做整体方案,结合去噪、超分辨率、着色三个模块,形成一套完整的老照片增强管线。
6.3 给再做类似项目的同学一点实在建议
最后分享几个非常实际的经验,说不上是方法论,但确实是踩过坑之后才懂的。首先是数据集一定要提前准备好,并且做好充分的预处理和可视化检查,不要像我最初那样训练到一半发现数据里有很多图像带有水印和异常色彩,白废了好几天训练时间。其次是尽量早上GPU资源,城市算力或学校实验室的机器早点排队申请,千万别拖到最后一周才找机器。再次是答辩时能有一个已经调好参数的现场演示流程,比任何技术细节的PPT描述都有说服力,建议提前准备一套演示材料并至少完整演练三遍。
图像着色这个项目的迷人之处,在于它不是单纯的“填色游戏”。模型要学会感知场景内容,理解物体语义,还要掌握真实世界的色彩规律。我做完整个项目后深刻体会到,一次毕业设计更像是一次完整的工程化训练,从问题定义到方案设计,从模型训练到系统落地,再到最后的展示答辩,每一个环节都在逼着人把知识真正用起来。这种“把一件事真正做完”的成就感,往往比模型跑出来的那个数字更有价值。