ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于优化的灰度图像自动着色:YUV空间与稀疏矩阵求解实战

基于优化的灰度图像自动着色:YUV空间与稀疏矩阵求解实战 简介这是一套面向图像处理与计算机视觉学习者的MATLAB开源着色项目聚焦灰度图像自动上色与颜色恢复。资源包约891KB页面上未列出文件总数与具体类型明细从项目性质看内容以MATLAB脚本、示例图像和使用说明为主。目前已有138人学习浏览。项目覆盖灰度图像转换、预处理与多种着色流程既可用rgb2gray理解灰度化公式用imsegkmeans做区域聚类也能借助高斯滤波、Canny边缘检测提取图像结构并尝试最近邻法、色彩扩散或深度学习CNN完成颜色推断。除经典算法外还可对比基于纹理、深度和邻域信息的着色策略同时项目采用开源许可便于查看、修改和二次分发。开发者既能学习图像分割、特征提取等经典操作也能实践从灰度到彩色的算法设计与调参适合作为MATLAB图像处理课程设计或计算机视觉入门项目的参考。 开头先聊点实在的。灰度图着色这个方向我在 MATLAB 里折腾过很久最早是被图像处理大作业逼的后来自己接了好几个相关的小项目越做越觉得这个“Colorization-project”有点意思。它本质上不是让你用 PS 一笔一笔涂色而是通过程序把黑白照片自动上色或者说让用户给几个颜色提示剩下的颜色由算法自己“脑补”出来。如果你手里正好有一批老照片、灰度图或者你在做图像处理、计算机视觉相关的课程项目这个项目完全可以当作一个高质量练手材料既能深入理解图像邻域结构又能把稀疏矩阵求解、颜色空间转换这些基本功练扎实。这篇东西会把整个思路、算法细节、实操步骤、踩坑记录全部分享出来跟着做一遍你能真正跑出一个能用的着色工具而不是仅仅看了个概念。1. 这个着色项目到底在解决什么问题1.1 为什么灰度图像着色不是“填色”那么简单先把问题定义清楚。这里的“着色”不是指把某个区域统一涂成纯色而是给定一张已经丢失颜色信息的灰度图算法要根据亮度和纹理结构推测出每个像素原本可能的颜色。你可以尝试用 PS 的笔刷手动上色一张人脸可能要花几个小时而且颜色过渡很难自然。而 Colorization-project 这类算法的思路是用户只需要在图片上随意画几笔颜色称为 color scribbles颜色涂鸦或者颜色提示算法就自动把这些颜色传播到整张图片。为什么传播是可行的这里有一个很朴素但极度关键的假设在图像里如果两个像素的亮度灰度值相近那么它们的颜色也应该相近。换句话说颜色不是凭空产生的它跟亮度信息有很强的空间相关性。这个假设在大多数自然图像上是成立的比如皮肤的灰度值通常在一个连续范围对应的肤色也应该连续。基于这个假设着色问题就变成了一个约束优化问题已知少量像素的颜色用户涂鸦处未知大量像素的颜色利用“亮度相似则颜色相似”的约束把所有未知颜色求出来。当时我第一次看到这个思路时觉得很妙它把“上色”这个听起来很艺术的事情转化成了一个数学上完全可解的问题。这也解释了为什么很多经典论文比如 Levin 等人在 2004 年发表的 Colorization using Optimization能成为这个领域的基石。1.2 为什么坚持用优化方案而不是深度学习现在的趋势是一提到图像上色就用深度学习模型比如基于卷积神经网络的自动着色。但 Colorization-project 用的是经典的优化方法很多人会问为什么不直接上深度学习我个人的看法是这两种方案解决的是不同的问题。深度学习方案擅长“无中生有”它见过几万张自然图像能直接预测一个多彩的合理结果缺点是可控性较差用户很难精确告诉模型“这个帽子要红色那个衣服要蓝色”。而优化着色的核心优势恰恰在于交互可控用户画的每一笔颜色都是硬约束算法必须严格满足其余部分顺势传播。这种感觉就像给黑白电影手工修复一样制作者掌握最终决定权。另外从学习角度来说优化着色把图像处理里最核心的几个知识点串起来了颜色空间转换、邻域结构分析、稀疏线性方程组的构造与求解。这些是深度学习方法给不了的底子。所以这个项目不光是做出来一个玩具它是一块很好的“跳板”——你把优化着色的原理吃透了后面再看自动着色模型理解深度是完全不同的。这也是我喜欢把它分享出来的原因。2. 核心细节解析算法里的关键假设与工程实现2.1 YUV颜色空间为什么不用RGB很多第一次做这个项目的同学会直接在 RGB 空间里算颜色传播结果总是不对劲。原因在于 RGB 三个通道是高度相关的直接把三个通道当成独立变量去求解会出现颜色溢出、边界模糊等奇怪现象。这个项目里采用的颜色空间是 YUV或者 YCbCr。简单解释一下Y 通道是亮度信息就是你从灰度图里看到的东西U 和 V 两个通道是色度信息负责颜色。之所以用 YUV目标非常明确——我们已知的就是 Y 通道灰度图需要求解的是 U、V 通道。这样就把问题分离开来Y 是已知的固定量U 和 V 是两个未知量各自独立求解互不干扰。这比在 RGB 空间里同时约束三个通道、还得考虑通道间关系要干净得多。具体在 MATLAB 里的实现可以用 rgb2ycbcr 函数直接把彩色参考图转成 YUV也可以手动用矩阵变换。代码如下% RGB转YUV (YCbCr) img imread(input.png); img_yuv rgb2ycbcr(img); Y img_yuv(:, :, 1); % 亮度 U img_yuv(:, :, 2); % 蓝色差 V img_yuv(:, :, 3); % 红色差这里要注意一点如果输入本身就是灰度图那就直接把它当作 Y 通道初始化一个全零的 U、V 通道然后填充用户涂鸦的颜色信息。整个算法只重建 U、V最后再通过 ycbcr2rgb 转回 RGB 显示。2.2 邻接关系与权重计算颜色传播的“通道”优化着色的核心结构是像素之间的邻接图。把每个像素看成图上的一个节点两个相邻像素之间有一条边边的权重表示“它们颜色应该有多接近”。权重不是随便定的它是根据亮度差计算的。标准的做法是对于像素 r满足某个条件时需要在求解矩阵里加上一个元素当两个像素的亮度差值越大权重越小亮度越接近权重越大。典型权重公式是w(r,s) exp( -(Y(r) - Y(s))^2 / (2 * sigma^2) )其中 sigma 是控制衰减速度的参数。这个公式本质上很像高斯函数因为自然图像中亮度连续变化的区域占大多数亮度差小的像素权重高颜色传播就更顺畅。所以说白了颜色传播就相当于在图上做一个“加权扩散”你画一笔红色这个红色会沿着亮度相似的像素“渗开”直到被亮度差异大的边界挡住。这就解释了为什么边缘保持效果会好——亮度突变处权重接近 0颜色过不去。实现的时候一个更稳健的做法是不仅考虑当前像素跟正上方、正右方的像素关系而是把每个像素的 4 邻域上下左右甚至 8 邻域都纳入计算。我实测下来4 邻域已经是基准配置8 邻域在某些纹理丰富的图上更好但矩阵规模更大计算时间长。核心权衡点在于邻域越广颜色互相影响的范围越大就越可能出现“跨界污染”。还有一个关键细节权重计算时用的不是原始灰度值而是加了一点线性拉伸的亮度值。原因是某些图片过暗或过亮直接算高斯会亮瞎差不多的像素。可以在预处理阶段做一次 imadjust 拉伸效果会稳定很多。2.3 稀疏矩阵求解从公式到代码当把每个像素都列出来并让每个像素跟邻域之间的颜色关系满足“亮度相似则颜色相似”的约束后整个问题就变成了解一个大型线性方程组。具体来说对每个未知颜色的像素 i我们希望它的颜色值以 U 通道为例和邻域颜色加权平均尽可能接近U(i) sum( w(i,j) * U(j) ) / sum( w(i,j) )对于用户画过颜色的像素直接把颜色值固定为输入值。把所有这样的式子整理成矩阵形式就得到了 A * U b其中 A 是一个稀疏矩阵b 是包含用户涂鸦信息的向量。然后只需要在 MATLAB 里用 A\b 求解即可。这里我必须强调一个非常重要的工程经验必须用稀疏矩阵绝对不能用全矩阵。假设图片是 500x500总共 25 万个像素如果构造一个 25 万行乘 25 万列的满矩阵内存直接爆炸MATLAB 直接卡死。好在 MATLAB 的 sparse 函数就是为这种情况设计的它只存储非零元素内存占用大幅下降。代码结构上要注意先用行索引、列索引、值三个数组收集非零元素最后再用 sparse 一次性构造这比循环里赋值要快很多。3. 实操过程与核心环节实现3.1 从灰度图到着色结果的完整流程一个完整可用的 Colorization-project 流程我整理成了六步读入灰度图并转换成 double 类型。构造用户涂鸦交互式画笔画颜色或者在代码里直接指定某些像素的颜色值。计算每个像素的邻域权重构建稀疏矩阵。分别求解 U、V 通道的线性方程组。将 Y、U、V 合并转回 RGB。显示结果图和原灰度图对比。整个流程最关键、也最容易出错的是第三步。如果权重矩阵算错了后面所有颜色都白搭。我的建议是先在一个很小的图上测试比如缩小到 100x100手动给几个点的颜色直接用代码单步调试权重矩阵确认数值合理再跑到全尺寸图上。3.2 关键代码逐段拆解这一部分把核心代码完整展示出来并逐段解释方便直接照抄修改。第一步读取图像并初始化img_gray imread(gray_image.png); if size(img_gray, 3) 3 img_gray rgb2gray(img_gray); % 防止输入是RGB图 end [rows, cols] size(img_gray); Y double(img_gray); % 亮度范围0~255 U zeros(rows, cols); % 色度U初始为0 V zeros(rows, cols); % 色度V初始为0 N rows * cols; % 像素总数第二步设置用户涂鸦。这里我提供一个简单的区域涂鸦方法比如把图片中心区域设为肤彩色把天空区域设为蓝色% 假设通过鼠标交互标记颜色点这里示例用矩形区域 mask false(rows, cols); mask(50:150, 100:200) true; % 手动标记区域 U_known zeros(rows, cols); V_known zeros(rows, cols); % 这两个数组存放已知点颜色未标记点为0 % 然后在mask区域内填入你期望的U、V值例如 U_known(mask) 120; % 这个值来自对应参考色的YCbCr分量 V_known(mask) 130;第三步构建邻接矩阵与权重。这部分是算法核心我建议分成两个函数来写一个负责返回像素的标识编号另一个负责构建稀疏矩阵。如下% 像素线性编号 id (i,j) (i-1)*cols j; rows_idx zeros(1, N*4); % 预留空间 cols_idx zeros(1, N*4); vals zeros(1, N*4); cnt 0; sigma 10; % 权重衰减参数可调 for i 1:rows for j 1:cols % 只处理4邻域中右边的和下边的像素避免重复 if j cols w exp( -(Y(i,j) - Y(i,j1))^2 / (2*sigma^2) ); if w 0.001 % 太小的权重直接丢弃保证稀疏性 cnt cnt 1; rows_idx(cnt) id(i,j); cols_idx(cnt) id(i,j1); vals(cnt) -w; % 注意矩阵A中非对角元素是负权重 cnt cnt 1; rows_idx(cnt) id(i,j1); cols_idx(cnt) id(i,j); vals(cnt) -w; end end if i rows w exp( -(Y(i,j) - Y(i1,j))^2 / (2*sigma^2) ); if w 0.001 cnt cnt 1; rows_idx(cnt) id(i,j); cols_idx(cnt) id(i1,j); vals(cnt) -w; cnt cnt 1; rows_idx(cnt) id(i1,j); cols_idx(cnt) id(i,j); vals(cnt) -w; end end end end这里只是记录了非对角线上的权重。对角线上的元素需要在最后加一个矩阵让每行的非对角元素之和的相反数落在对角线上。完整构造语句如下A_sp sparse(rows_idx(1:cnt), cols_idx(1:cnt), vals(1:cnt), N, N); % 修正为每行对角元素 该行所有非对角元素负值之和 A_sp A_sp - diag(sum(A_sp, 2)); % 这一步让每行和为零为什么每行和必须为零因为你要求解的本质是拉普拉斯方程未知点的颜色等于周围颜色的加权平均。反映在矩阵上就是该行的行和必须等于 0这是很多代码不一定提的细节。如果忘了这步解出来会整体偏色。然后添加奇异方程把已知颜色的像素固定住。方法是在对角线对应位置加上一个很大的 C比如 1e8并在 b 向量里写成已知颜色乘 Cb_U zeros(N, 1); b_V zeros(N, 1); for i 1:rows for j 1:cols if mask(i,j) idx id(i,j); A_sp(idx, idx) A_sp(idx, idx) 1e8; b_U(idx) U_known(i,j) * 1e8; b_V(idx) V_known(i,j) * 1e8; end end end最后一步求解和重建U_vec A_sp \ b_U; V_vec A_sp \ b_V; U reshape(U_vec, rows, cols); V reshape(V_vec, rows, cols); YUV_final cat(3, Y, U, V); rgb_final ycbcr2rgb(uint8(YUV_final)); figure, imshow(rgb_final);3.3 参数调节与效果优化这个项目里影响最明显的参数有三个sigma、网格大小和用户涂鸦的密度。sigma 控制权重衰减速度。太小比如 1只有亮度几乎一样的像素才会互相传递颜色结果会出现大量孤立色块太大比如 100亮度差很远的像素也互相影响颜色会“糊成一团”且跨边界溢出。我自己的经验是先取 5 到 15 之间根据图像噪音程度调节。图像噪音大亮度差值本来就不稳定sigma 要适当增大来提高抗噪性。网格大小直接影响计算量。如果原图很大超过 1000x1000建议先降到 300x400 左右做实验参数确定后再跑原图。甚至可以做一个由粗到细的多分辨率策略先在低分辨率图求出 U、V 结果再放大当作高分辨率图的初值这样能大幅减少迭代次数。还有一个比较容易被忽略的点灰度图如果是 uint8 类型的直接参与计算会溢出。一定要先转成 double 再处理最后结果再转回 uint8 显示。我见过不止一个同学因为忘记转换出来的图全是黑白条纹。4. 常见问题与排查技巧实录4.1 运行慢、内存爆掉怎么办这个项目的核心计算瓶颈在稀疏矩阵构造和线性方程组求解。如果你发现 MATLAB 卡死大概率不是算法问题而是代码写法问题。首先确认矩阵用的是 sparse 类型可以在命令行输入 whos 查看变量内存占用。其次尽量不要在 for 循环里一行一行添加 sparse 矩阵正确做法是先收集三个数组行索引、列索引、值一次调用 sparse 构造。这一点我可以说是踩过大坑的第一次用 1000x1000 的图逐像素循环赋值直接跑了一个小时还没跑完改成收集数组一次性构造后几秒钟就完成了。另外一个实用的替代方案是换求解器。默认的 A\b 对对称正定矩阵会走 Cholesky 分解对稀疏矩阵效果不错。如果你的 MATLAB 版本支持可以尝试 pcg预条件共轭梯度法配合不完全 Cholesky 预条件子对大图内存友好很多。4.2 着色结果颜色溢出或者边界发糊颜色溢出的根源在于权重计算时只考虑了亮度差异没有考虑空间距离。如果你发现平滑区域的边界也被“染色”了可以加上空间距离因素比如把权重改成w exp( -(亮度差^2) / (2*sigma^2) ) * exp( -(空间距离^2) / (2*sigma_d^2) )不过对于 4 邻域来说空间距离只有 1 个像素加不加其实区别很小。真正导致溢出的是涂鸦太靠近边界或者涂鸦覆盖了不同物体的边缘。解决方式是把涂鸦画在目标区域内部偏中央的位置不要贪心画太大。边界发糊还有一个容易被忽略的原因YUV 转换时uint8 和 double 混用。ycbcr2rgb 内部会做一些偏移如果输入输出类型不匹配可能导致色度通道像素值被截断表现出来就是边缘模糊。我的习惯是统一使用 double 参与计算只在最后一步转 uint8。4.3 求解结果出现 NaN 或全黑图这个情况我遇到过一次排查了很久最后发现是稀疏矩阵里有孤立的行——某个像素和它的所有邻域权重都被阈值裁掉了w 0.001导致矩阵该行只有对角元素求解出的结果就是 NaN。解决办法包括降低权重阈值比如 0.0001或者保证每个像素至少在两个方向上有连接。更稳妥的做法是在构造矩阵后检查一下每一行非零元素的数量如果发现有行为空就强制给它连上最近的邻域。另外全黑图大概率是 ycbcr2rgb 之前的 Y、U、V 取值范围不对。YCbCr 空间中 Y 范围是 16~235Cb、Cr 范围是 16~240。如果你直接把 double 型的 Y 从 0~255 送入 ycbcr2rgb转换结果可能被截断。解决办法是先把 Y 做 clamp 到 0~255再转到 uint8。4.4 用户交互涂鸦的体验优化如果项目不是纯代码而是要求做成交互式界面比如用到的 App Designer涂鸦的方式很影响使用体验。我建议用以下策略用户按住鼠标左键在图上拖动把鼠标经过路径上的像素记录为涂鸦点同时打开一个调色板让用户选择颜色。在代码层面可以把涂鸦点分成两类一类是“硬约束”严格固定颜色权重加大到 1e8另一类是“软约束”只是引导色权重设为 1 到 100。软约束的好处是允许算法在局部细节上偏离用户选择让结果更自然。还有一个交互技巧涂鸦的颜色值要用 YUV 空间的值。用户从调色板选的是 RGB 值必须先用 rgb2ycbcr 转换后再赋值给 U_known 和 V_known。这一步千万别用 RGB 直接赋值否则最后颜色会跑偏。我第一次做界面时就是直接给了 RGB结果涂鸦区域的颜色跟预期差了十万八千里。个人实操里的一点延伸这个项目做完后可以非常自然地向三个方向扩展。第一是自动生成初始涂鸦用显著性检测找到画面中的主体区域自动赋予一个颜色剩下的区域靠算法传播这就变成了半自动着色。第二是加入深度信息辅助比如用双目匹配得到的深度图来约束权重同一深度的区域互相传播颜色更可靠。第三是结合深度学习做后处理先自动着色出一个结果再让用户在上面修正涂鸦继续优化传播也算是一种“人机协同”。我个人在复现这个经典算法的过程里最大的收获不是写出了一堆能跑的代码而是真正理解了图像中颜色与亮度的内在关系这种底层的体会是调包深度学习模型给不了的。如果你也把整个项目从头到尾跑通一遍你一定会跟我有同样的感觉。本文还有配套的精品资源点击获取
返回列表