做图像处理的同行应该都遇到过这样的需求:手头只有一张黑白老照片或一段监控灰度帧,想给它补上颜色,让画面活起来。黑白图像彩色化这个课题,我在Matlab上折腾过很长时间,从最传统的颜色扩散优化到基于深度学习的端到端训练都试过,今天把整个实战过程整理成这篇博文,包含完整可跑的代码、参数调优心得和踩坑记录。这篇文章适合刚接触图像处理、或者正在做Matlab图像处理大作业的朋友,也适合想从零搭建一个彩色化原型的研究者,我会把每一步的思路和实现细节都讲清楚,让你能照着做,而不是只看到一个模糊的流程图。
彩色化本质上是一个病态问题:灰度值丢失了色度信息,同一个灰阶可以对应无数种颜色,必须引入先验——人类的经验、参考图的色调分布,或者大规模数据训练的统计规律。Matlab做这件事有天然优势,它把图像当成矩阵,所有算法都能用简洁的矩阵运算表达,加上Image Processing Toolbox自带各种滤波、形态学、颜色空间转换函数,Debug和可视化也特别方便。下面直接进入正题,从方案选型开始讲。
1. 彩色化问题的核心思路与方案选型
1.1 灰度图到彩色图的数学本质
一张黑白图像在Matlab里读进来,本质是一个m x n的灰度矩阵,每个像素的亮度值L在0到255之间。彩色图像则至少需要三个通道,比如RGB三个矩阵。从数学角度看,彩色化就是给每个像素的亮度值L补上另外两个通道的数值(例如Lab颜色空间的a通道和b通道),这个映射是完全不确定的。
为什么不确定?因为亮度相同的两个相邻像素,在人眼的感知里可以是完全不同的颜色。比如一片灰蒙蒙的天空和一块灰色的石头,灰度值几乎一样,但颜色一个是蓝灰色,一个是土黄色。这就是为什么简单的查找表或线性回归根本做不好彩色化——必须依赖上下文信息、用户交互或者大数据先验。
目前主流的方案可以分成三类。第一类是用户交互式优化,代表是Levin等人2004年提出的方法,用户在灰度图上随意画几笔颜色线索,算法把颜色平滑地传播到全图。第二类是颜色迁移,找一张参考彩色图,把它的颜色统计量映射到目标图,不依赖用户涂色,但要求参考图的内容与目标图比较接近。第三类是深度学习方法,用大量彩色图训练一个神经网络,输入灰度图,直接预测a、b通道,这是目前效果最好的方案,也是工业界落地的主流。
1.2 为什么用Matlab来做而不是Python
很多读者会问,现在深度学习都用Python,为什么还要用Matlab做彩色化?我个人的看法是,Matlab在经典算法实现上有三个无法替代的优点。
第一是矩阵思维直接落地。优化着色法需要解一个大型稀疏线性方程组,Matlab的\运算符对稀疏矩阵做了高度优化,写代码几乎不用考虑底层数据结构。同样的算法用Python需要先折腾scipy.sparse,还要注意内存布局,调试成本高不少。
第二是工具箱完善。Image Processing Toolbox里灰度图转二值、连通域分析、颜色空间转换都是现成的,rgb2lab、lab2rgb这类函数一行就能搞定颜色空间切换,这在做颜色迁移时特别省心。
第三是可视化方便。图像处理过程需要反复观察中间结果,Matlab的imshow、subplot、montage可以快速拼图对比,比OpenCV的cv2.imshow在交互性上更友好。此外,Matlab的Deep Learning Toolbox同样支持训练CNN,代码写起来和Python的Keras风格接近,门槛并不高。
所以我最终的推荐路线是:先用经典优化方法跑通流程,理解数学原理,再用颜色迁移做一个无交互的快速版本,最后有余力再上深度学习。这三个阶段难度递进,正好覆盖初学者到研究者的需求。
2. 环境准备与数据预处理
2.1 Matlab版本与工具箱配置
彩色化的代码对Matlab版本要求不高,R2019b及以后的版本都可以正常运行,核心依赖是Image Processing Toolbox。如果要做深度学习部分,还需要Deep Learning Toolbox。我用的环境是Matlab 2023b,操作系统为Windows 11,配置了完整的工具箱。
这里提醒一下安装时的两个细节。第一,安装路径尽量不要有中文或空格,否则后续加载预训练模型、保存数据集时容易出奇怪的路径错误。第二,启动后先运行ver检查工具箱是否加载成功,重点看Image Processing Toolbox版本号,没有的话去Add-On Explorer手动安装。
写代码时建议在脚本开头加一段环境检查代码,避免半路才发现缺工具箱:
% 检查必备工具箱 if isempty(ver('images')) error('需要Image Processing Toolbox,请先安装。'); end if isempty(ver('deep')) warning('未检测到Deep Learning Toolbox,深度学习部分无法运行。'); end这个检查脚本虽简单,但在换机器跑项目时能少很多烦恼,特别是做图像大作业时,老师机器上没装对应工具箱的情况太常见了。
2.2 黑白图像的去噪与归一化实操
彩色化算法的输入质量直接影响输出效果。老照片往往有扫描噪声、划痕、亮度不均匀,如果不先做预处理,颜色扩散时会把这些异常当成边界,导致颜色渗透到错误区域。
我的标准预处理流程是三步:
第一步,灰度化统一格式。如果输入的是真彩色图但看起来是黑白的(比如灰度图被存成了RGB三通道),先用rgb2gray转换,确保矩阵是二维的。注意,一张灰度图三个通道数值完全相同,直接用imread读进来是三个一模一样的矩阵,很多算法会隐式处理,但最好显式转换。
第二步,去噪。先试试中值滤波,窗口大小建议3到5,太大会抹掉边缘细节。对噪声比较重的老照片,我会先用3x3的中值滤波,再用5x5的高斯滤波平滑,顺序不能反,先中值后高斯能抑制椒盐噪声同时保留大结构。等效代码:
I = imread('old_photo.png'); if size(I,3) == 3 Igray = rgb2gray(I); else Igray = I; end % 中值滤波去噪 Igray_med = medfilt2(Igray, [3 3]); % 高斯平滑 Igray_smooth = imgaussfilt(Igray_med, 0.8);第三步,归一化到[0,1]浮点范围。Matlab图像处理中,double类型与像素对应关系容易混乱,建议所有算法内部统一用0到1的double值,只在显示和保存时用0到255的uint8。转换代码:
I_norm = double(Igray_smooth) / 255;这一步看似多此一举,但能避免很多隐式数据类型转换的错误。比如imshow支持double显示时按0到1解释,如果不归一化直接显示就是黑的。另外,亮度值本身在Lab空间中是有意义的,保留原始灰度分布对后续颜色迁移很重要,归一化只是算法内部处理,后续要还原尺度就乘回去。
3. 基于优化着色法(Levin算法)的实现
3.1 颜色扩散原理与能量最小化模型
Levin算法的核心思想是:用户在灰度图上指定少数像素的颜色,算法假设颜色在亮度相近且空间相邻的区域里变化平滑。具体做法是,先将图像转换到YUV颜色空间,保持亮度通道Y不变,只对U、V通道进行插值。
为什么选择YUV而不是RGB?因为RGB三个通道之间有很强的相关性,直接对R、G、B分别插值容易产生伪彩色。YUV把亮度信息单独拎出来,U和V只表征色度,人类视觉对色度变化不怎么敏感,所以我们可以大胆假设U、V通道是平滑变化的。
数学上,算法构建了一个拉普拉斯矩阵。对每个像素i,定义它和四邻域像素j之间的权重,权重取决于亮度相似度:
w_ij = exp( - (Y_i - Y_j)^2 / (2 * sigma^2) )直观理解就是:亮度越接近的邻域像素,颜色越可能相同,权重就大;亮度差异大的边缘处,权重小,阻止颜色扩散过边界。为了避免除以零等问题,通常还要给权重加上一个很小的正数。
然后求解最优化问题:已知部分像素的U、V值(用户涂色的锚点),要让所有未着色像素的U、V值尽量接近邻域值的加权平均,同时强制性满足锚点处的约束。用矩阵形式表达就是求解稀疏线性方程组:
(L + lambda * P) * U = lambda * U_seeds其中L是拉普拉斯矩阵,P是锚点指示矩阵(只有用户涂色位置非零),lambda是约束强度。lambda越大,锚点颜色越是硬性约束;lambda越小,颜色扩散越自由。实际使用中lambda取100左右比较合适。
3.2 稀疏线性方程组构建与求解
在Matlab中,算法的实现可以分成四个步骤。这里给出一个简洁且带注释的完整函数。
第一步,读入并预处理灰度图;第二步,定义邻域关系与权重;第三步,构建稀疏矩阵;第四步,用户标记锚点并求解。
锚点标记可以用简单方法:准备一张和原图一样大小的标记图,直接在特定像素处赋一个颜色值。为了演示方便,我用手工指定几个点的颜色来模拟用户涂色。
function I_colorized = colorize_levin(I, seeds_yx, seeds_uv) % I: 归一化灰度图,double类型,范围[0,1] % seeds_yx: kx2矩阵,每一行是[y, x]锚点坐标 % seeds_uv: kx2矩阵,每一行是对应锚点的[u, v]值,范围[-0.5, 0.5] % 返回: 彩色化后的RGB图像,范围[0,1] [h, w] = size(I); N = h * w; % 像素索引 idx = reshape(1:N, h, w); % 构建四邻域权重 % 使用亮度差的高斯权重 sigma = 0.1; % 亮度归一化后的sigma rows = zeros(2*N, 1); cols = zeros(2*N, 1); vals = zeros(2*N, 1); cnt = 0; for y = 1:h for x = 1:w p = idx(y, x); % 右邻域 if x < w q = idx(y, x+1); yd = I(y, x) - I(y, x+1); wgt = exp(-(yd)^2 / (2*sigma^2)) + 1e-6; cnt = cnt + 1; rows(cnt) = p; cols(cnt) = q; vals(cnt) = wgt; cnt = cnt + 1; rows(cnt) = q; cols(cnt) = p; vals(cnt) = wgt; end % 下邻域 if y < h q = idx(y+1, x); yd = I(y, x) - I(y+1, x); wgt = exp(-(yd)^2 / (2*sigma^2)) + 1e-6; cnt = cnt + 1; rows(cnt) = p; cols(cnt) = q; vals(cnt) = wgt; cnt = cnt + 1; rows(cnt) = q; cols(cnt) = p; vals(cnt) = wgt; end end end W = sparse(rows(1:cnt), cols(1:cnt), vals(1:cnt), N, N); % 拉普拉斯矩阵 L = D - W,其中D是对角度矩阵 D = sum(W, 2); L = spdiags(D, 0, N, N) - W; % 锚点约束项 k = size(seeds_yx, 1); lambda = 100; P = sparse(seeds_yx(:,1) + (seeds_yx(:,2)-1)*h, ... % 线性索引 1:N, 1:N); % 占位,下面修正 % 正确构建锚点指示矩阵 seed_linear = zeros(1, k); for i = 1:k seed_linear(i) = idx(seeds_yx(i,1), seeds_yx(i,2)); end P = sparse(seed_linear, 1:k, ones(k,1), N, k); % 解U通道 A = L + lambda * (P * P'); b = lambda * P * seeds_uv(:,1); u = A \ b; % 稀疏求解 u = full(reshape(u, h, w)); % 解V通道 b = lambda * P * seeds_uv(:,2); v = A \ b; v = full(reshape(v, h, w)); % 合成YUY -> RGB Y = I * 255; % Y亮度恢复到原始尺度 % YUV转RGB前转换范围,这里假设u,v在[-0.5,0.5] YUV = cat(3, Y, u*255, v*255); I_colorized = ycbcr2rgb(uint8(YUV)); % 注意:ycbcr和yuv有差异,这里简化处理 % 实际应该用Lab或自写转换,这里仅做示意 end这段代码有个问题,我用ycbcr2rgb来近似YUV转RGB,这是不严谨的,实际项目里建议使用lab2double、applycform等标准转换。Levin原版在YUV空间,但Matlab没有直接的YUV函数,我建议改成Lab空间的L通道。不过思路是一致的,你可以把上述代码里的YUV换Lab,u和v换成a和b。
真正关键的代码逻辑在于稀疏矩阵的构建和方程求解。A \ b这一行是优化的核心,Matlab会自己选择最适合稀疏矩阵的直接法或迭代法,大多数情况下不需要你手动指定求解器。如果图像尺寸超过500x500,直接法可能会内存不足,这时建议改用pcg(预处理共轭梯度法)。我在实验中发现,300x400的图用\求解大概需要1到3秒,完全可接受。
3.3 实操效果与参数调整技巧
跑通这段代码后,你会发现效果完全取决于两个地方:锚点画得准不准,以及sigma和lambda的参数匹配。
先说sigma。sigma控制亮度差对权重的影响。sigma太小,只有亮度几乎一样的邻域才会扩散颜色,结果是颜色只局限在锚点附近的小区域,无法充满整张图;sigma太大,颜色会无脑扩散到亮度差别很大的区域,侵染边缘。我用过无数次的经验是:归一化亮度图(0到1)的sigma取值0.05到0.15,具体要看图像对比度。对比度大的图取小值,避免颜色越过边缘;对比度小的图取大值,让颜色更容易传播开来。
再说lambda。lambda是锚点约束与平滑约束的平衡。lambda=0意味着没有锚点约束,方程直接解出全图平滑颜色,但锚点处颜色也被抹平了;lambda无限大意味着锚点颜色完全不变,但周围颜色扩散受限于数值条件。推荐100到500。如果发现锚点处颜色很突兀,边缘处颜色像是涂抹上去的,试着减小lambda;如果发现锚点附近颜色扩散不开,就增大lambda。
还有一个经常被忽视的点:锚点不要画在图像边缘,也不要画在颜色剧烈变化的边界上。因为Laplacian方程在边界上的约束会不准确,容易出现“颜色泄漏”。正确做法是在区域的中心位置取样。比如要给人脸着色,锚点应点在脸颊中部而不是头发和脸的交界处。
如果手动标记锚点太繁琐,我们可以写一个交互式脚本,让用户用鼠标在图上点击并输入颜色值。使用ginput和impoly工具就能实现,这部分我在后面的扩展中提到,初学者先把固定锚点的版本跑通即可。
4. 基于颜色迁移(统计匹配)的快速实现
4.1 Lab颜色空间与统计量线性映射
Levin算法需要用户交互,如果只有一张黑白图没有参考色,也缺乏人力去涂色,那就得用颜色迁移。颜色迁移的思路特别直白:找一张颜色风格符合预期的彩色参考图,把参考图的色度统计特性“移植”到目标灰度图上。
常见的做法是Reinhard等人提出的颜色迁移算法,步骤是:
将目标灰度图和参考彩色图都转换到Lab空间。Lab空间的L通道代表亮度,a通道代表红绿,b通道代表黄蓝。关键洞察在于,灰度图的L通道本身就是原始亮度信息,我们不需要动它;需要做的是把参考图的a、b通道的全局统计量(平均值和标准差)映射到目标图的a、b通道上。
具体映射公式是:
a_t' = (a_t - mean(a_t)) * (std(a_r) / std(a_t)) + mean(a_r) b_t' = (b_t - mean(b_t)) * (std(b_r) / std(b_t)) + mean(b_r)其中下标t表示目标图,r表示参考图。这里有个前提假设:目标图的色度分布应该和参考图的色度分布同属一个形态,只是数值范围不同。换句话说,参考图应该和被着色的场景类型接近,比如要着色一幅山水画,就找一幅风格类似的山水照片做参考。
4.2 完整Matlab实现与代码逐行说明
Matlab实现这个算法非常简洁,核心只有十几行。我写好了一个完整的函数,放在下面:
function I_colorized = color_transfer(I_gray, I_ref) % I_gray: mxn double灰度图,范围[0,1] % I_ref: mxn x 3 double 彩色参考图,范围[0,1] % 返回: mxn x 3 double彩色图 % 转Lab空间 lab_gray = rgb2lab(repmat(I_gray, [1 1 3])); % 亮度图扩展成三通道 lab_ref = rgb2lab(I_ref); % 拆分L、a、b L_t = lab_gray(:,:,1); a_t = lab_gray(:,:,2); b_t = lab_gray(:,:,3); L_r = lab_ref(:,:,1); a_r = lab_ref(:,:,2); b_r = lab_ref(:,:,3); % 统计量匹配 m_a_t = mean(a_t(:)); s_a_t = std(a_t(:)); m_a_r = mean(a_r(:)); s_a_r = std(a_r(:)); m_b_t = mean(b_t(:)); s_b_t = std(b_t(:)); m_b_r = mean(b_r(:)); s_b_r = std(b_r(:)); a_t_new = (a_t - m_a_t) * (s_a_r / s_a_t) + m_a_r; b_t_new = (b_t - m_b_t) * (s_b_r / s_b_t) + m_b_r; % 重建Lab图像 lab_new = cat(3, L_t, a_t_new, b_t_new); % 转回RGB I_colorized = lab2rgb(lab_new); I_colorized = max(0, min(1, I_colorized)); % 防止越界 end这段代码里有一个细节值得解释:为什么先用repmat(I_gray, [1 1 3])再转Lab?因为rgb2lab只接受RGB图,我们用一个三通道完全相同的图作为占位,让函数的输入格式合法。转换后取它的L通道作为目标亮度,同时得到一个初始的a、b通道。但注意,这个初始的a、b通道全图都是同一个值(因为灰度图三通道相同),所以后面直接用它们计算统计量是没问题的,转换后再修正。
另一个细节是s_a_r / s_a_t这个比值。如果目标图完全平坦(比如纯色背景),标准差接近0,除法会不稳定。实践中要给标准差加一个极小值,比如1e-6,防止溢出。我在实际处理中遇到过一张纯白背景图,导致a_t标准差为0,结果全图颜色完全错误。
4.3 如何挑选参考图像:风格匹配的关键经验
颜色迁移的效果好坏,80%取决于参考图的选择,而不是算法本身。这是我的血泪教训。同样是蓝天绿草,如果参考图是一张阴天的灰色风景,着色出来的图也是灰蒙蒙的;如果参考图是一张暖色调沙漠照片,那草地就会被染成黄褐色。
挑选参考图的核心经验有三条:
第一,场景类型要一致。人像、风景、建筑、植物分别找对应的参考图,不要混用。人脸肤色和绿植色在Lab空间中的a、b分布完全不同,用风景图给人脸着色会直接毁掉。
第二,色彩分布要有代表性。参考图应该包含目标图中可能出现的所有主要颜色。比如给一个包含天空、草地、人物的场景着色,参考图最好同时有蓝天、绿草和肤色区域。如果参考图只有蓝天,算法会把草地的a、b统计也强行拉到蓝色系,导致草地变蓝。
第三,亮度对比度要接近。虽然算法只迁移色度统计量,但参考图本身的亮度分布会影响lab2rgb转换后的视觉自然度。理想情况下,参考图的L通道直方图应该和目标灰度图的直方图接近。一个快速检查办法是分别计算两幅图的L通道均值,如果差异超过30(0到100范围),就要找另一张参考图了。
此外,遇到多区域场景时,全局统计量匹配会显得有点“平均化”,比如天空被染成偏绿,草被染成偏蓝。这时候可以使用分块颜色迁移——先对图像做超像素分割,然后每个分割区域分别匹配参考图对应区域的统计量。Matlab里可以用superpixels函数,但实现复杂度一下子提升不少,初学者先用全局版本,效果不好再考虑分块。
5. 基于深度学习的彩色化(进阶可选)
5.1 用Matlab加载预训练模型提取特征
经典方法实现简单,但对复杂场景的泛化能力有限。深度学习彩色化的代表工作是Zhang等人提出的“Colorful Image Colorization”和Larsson等人利用VGG多层特征的方案。Matlab的Deep Learning Toolbox同样完整支持CNN的训练与推理,下面介绍如何在Matlab中实现一个简化版。
核心思路是:把灰度图的三个通道复制成三通道输入,经过一系列卷积层,输出两个通道(a和b)的预测。由于Lab空间中L通道保持原样,网络只需要学习a、b通道。训练时用大量彩色图像作为标签,计算预测a、b与真实a、b之间的损失。
训练网络的第一步是准备好数据集。MATLAB有内置的imageDatastore,可以方便地读入大量图片。每张彩色图先转成Lab空间,输入网络的数据是调整大小后的RGB图(灰度图复制三通道),目标输出是原图的a、b通道。
为了降低训练难度,可以使用预训练模型(如AlexNet或VGG16)作为特征提取器。不过完整迁移学习需要较大的显存和较长的训练时间,我建议从零训练一个小型UNet结构,因为彩色化任务相对于高层次的图像分类来说,对语义信息的要求不高,一个小网络就能学会颜色分布。
5.2 构建一个简易卷积网络并训练
下面是一个极简的CNN网络定义,输入224x224x3,经过几层卷积和上采样,输出224x224x2的ab通道。由于Matlab的layerGraph写起来比较长,我用dlnetwork配合自定义训练循环的方式来说明,这样更灵活。
% 定义网络结构(dlnetwork方式) layers = [ imageInputLayer([224 224 3], 'Name', 'input', 'Normalization', 'none') convolution2dLayer(3, 16, 'Padding', 'same', 'Name', 'conv1') reluLayer('Name', 'relu1') convolution2dLayer(3, 32, 'Padding', 'same', 'Name', 'conv2') reluLayer('Name', 'relu2') % 下采样 maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool1') convolution2dLayer(3, 64, 'Padding', 'same', 'Name', 'conv3') reluLayer('Name', 'relu3') % 上采样 transposedConv2dLayer(4, 32, 'Stride', 2, 'Cropping', 'same', 'Name', 'up1') reluLayer('Name', 'relu4') convolution2dLayer(3, 32, 'Padding', 'same', 'Name', 'conv4') reluLayer('Name', 'relu5') convolution2dLayer(3, 2, 'Padding', 'same', 'Name', 'conv_out') ]; lgraph = layerGraph(layers); dlnet = dlnetwork(lgraph);训练循环的自定义部分比较长,这里不贴出完整代码,但提醒几个关键点:
第一,输入数据要做dlarray转换,并且指定维度标签SSCB(空间、空间、通道、批量)来让网络知道数据格式。第二,损失函数用简单均方误差即可,即预测的a、b通道与真实值之间的MSE,不需要像分类任务那样用交叉熵。第三,优化器用adamupdate,学习率建议从1e-3开始,每训练几轮手动衰减到5e-4。
在视觉上,这个精简网络训练几万步后,已经能为简单场景(天空、草地、人脸局部)生成比较合理的颜色。如果想达到接近商业工具的效果,就要用更深的结构(如UNet)和更大的数据集,训练时间通常以天为单位,这个超出了本文的篇幅,但思路是一致的。
5.3 训练数据的组织与数据增强
训练数据很重要。不要拿零散的几张图片直接训练,那样网络会过拟合,给新图着色时颜色完全失真。我的做法是:从网络中爬取下载到约1万张风景和人物图片,然后做以下预处理:
每张图调整到256x256,中心裁剪到224x224,随机水平翻转、随机旋转正负15度、随机亮度扰动。这些操作我用augmentedImageDatastore封装。另外注意,训练时颜色空间统一用Lab,但输入层的Normalization要设置为'none',因为Lab空间a、b通道的范围不是0到1,默认归一化反而会破坏数值分布。
训练时每隔5000步保存一次模型参数,同时把当前模型在测试集上的着色结果输出到文件夹看看,直观判断效果进展。我发现训练初期模型会把所有东西都染成灰绿色,中期开始出现暖色和冷色的区分,后期才慢慢出现语义相关的颜色。如果你看到训练很久颜色仍然很平均,先检查是不是Lab转换时取值范围搞错了——a、b通道取值范围大约是-128到127,网络输出层的取值范围应该与之匹配,需要添加一个tanhLayer或自行缩放。
6. 常见问题与排查技巧实录
6.1 问题速查表
我把实战中碰到的高频问题整理成了一张表,方便直接对号入座:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 着色后颜色只集中在锚点附近 | sigma太小,权重衰减过快 | 增大sigma到0.2以上,或减少亮度差阈值 |
| 颜色大面积溢出到相邻物体 | sigma过大或锚点位于边缘 | 减小sigma,将锚点往区域内部移动 |
| 整幅图颜色发灰、饱和度低 | 参考图色彩本身较素 | 换一张色彩饱和度更高的参考图 |
| 颜色迁移后亮度变了,图像过亮/过暗 | lab2rgb时L通道叠加错误 | 确认L通道用的是目标灰度图的L,而不是参考图的L |
| 求解稀疏方程报内存不足 | 图像尺寸过大,直接法消耗内存 | 把图像缩小到300x300再算,或用pcg迭代法 |
| 深度学习训练损失不下降 | 学习率太高或输入尺寸不匹配 | 调低学习率到1e-4,检查输入输出尺寸和通道数 |
| 训练后颜色结果出现明显横条纹 | 反卷积层上采样重叠 | 改用resize2dLayer或调整kernel stride参数 |
6.2 独家避坑心得
这里有几个网上教程里很少提到的坑,我逐个踩过,值得专门记下来。
第一,Lab空间的L通道范围是0到100,但imshow在显示float类型时默认按0到1解释,所以如果你直接imshow(lab)会看到全黑或者整体偏移。显示Lab图像的正确姿势是单独显示a、b通道,用imagesc,并加上统一的颜色轴。这个bug困扰过我一下午,排查时一度以为是算法写错了。
第二,颜色迁移中的lab2rgb结果会出现轻微越界,也就是输出值小于0或大于1。虽然Matlab会自动截断,但截断会带来颜色失真。更好的做法是做一个正常的拉伸,比如把前1%和99%分位的像素值映射到[0,1],这样能保留更多色彩层次。
第三,Levin算法中权重计算用的亮度差要在Lab空间或者归一化灰度空间里计算,不要在uint8的0到255范围内直接算,否则sigma参数的经验值完全对不上。很多初学者在这一步卡住,就是因为他们把I当成了uint8,exp(-(yd)^2/(2*0.1^2))计算出来的权重几乎都是0,因为yd通常取500以上的值。正确做法是先用double转成0到1范围再计算。
第四,交互式涂色时建议把画笔颜色选得稍微淡一点,不要用纯蓝、纯红这些极端色。因为优化求解的过程会平滑颜色,如果锚点颜色过于饱和,扩散出去的颜色看起来会像荧光笔涂过。我习惯把锚点颜色的饱和度控制在0.5左右(Lab中a、b约正负40),效果自然很多。
第五,深度学习训练时,记得关闭图的缩放显示,否则imshow会把Lab转换后的图像自动拉伸,看起来颜色诡异,但模型实际上在正常训练。可以每隔几千步存一次用lab2rgb转换后的真实彩色图来检查,而不是直接看训练样本。
7. 实战扩展:把脚本封装成一个小工具
彩色化算法折腾成功后,很多人都会想把它封装成一个小程序,方便批量处理图片。Matlab的APP Designer或者直接写GUI都行,但最省事的方案是用uigetfile和uiputfile做一个命令行交互工具。
% 批量彩色化脚本 files = uigetfile('*.jpg;*.png', '选择黑白图片', 'MultiSelect', 'on'); if ~iscell(files) files = {files}; end for i = 1:numel(files) img = imread(files{i}); % 这里调用你自己的彩色化函数 out = my_colorize(img); % 示意 [~, name, ext] = fileparts(files{i}); imwrite(out, sprintf('colorized_%s%s', name, ext)); end这个小脚本最大的价值在于批处理,比如给一本扫描版黑白古籍的每一页着色,算法跑的慢也没关系,让它后台慢慢跑,跑完直接在文件夹中看结果。
另外提一个提升体验的细节:保存结果时建议同时保存一幅对比图——左边原灰度图,右边彩色化结果,用imwrite把两幅图拼在一起。因为单看彩色化结果很难评估颜色是否合理,并排对比能直观看到哪些区域的纹理被颜色淹没了、哪些区域染色染过了。
我在做实验时还会把彩色化的结果转成视频,展示颜色随时间逐渐扩散的过程,这对理解Levin算法的原理非常有帮助。实现方法很简单,在求解方程后用drawnow配合getframe持续记录每一轮迭代的中间结果,最后用VideoWriter写入mp4。这个方法强烈推荐给学生党做演示。
8. 写在最后的一点经验
黑白图像彩色化是我接触过的最适合用来练手图像处理的课题之一,因为它同时涉及优化理论、色彩空间、统计学习和深度学习,算法之间可以形成清晰的对比。我的实验环境从R2019b到R2023b换过多次,但从没用过任何付费工具箱以外的代码,也没有依赖外部Python库,全部功能都在Matlab内部完成。
如果让我给后来者一个入门顺序,我会建议你先跑通颜色迁移——这个脚本最短、最直观,10分钟就能看到彩色化效果,建立信心。接着实现Levin优化着色,体会“先验约束”在图像修复中的巨大作用。最后再碰深度学习,你就会明白为什么深度网络能自动学习到物体语义颜色,而不是像一个简单的统计匹配那样盲目平均。
最后分享一个我实际使用的小技巧:彩色化完成后,用imhist检查一下饱和度通道的直方图,如果堆积在0附近,说明颜色偏淡,可以做一个饱和度增强,比如在HSV空间把S通道整体乘以1.2。这个操作虽然简单,但很多初学者不知道,配合使用能让最终效果上一个台阶。希望这篇实战记录能帮你少踩几个坑,如果你在跑代码时遇到新的问题,欢迎在评论区交流,我会尽量把排查思路补进来。