☰
Welsh算法灰度图彩色化:原理、实现与参数优化
2026/10/11 2:23:46 网站建设 项目流程

简介:基于Welsh算法的灰度图像彩色化处理及优化Python实现项目,面向计算机视觉方向的在校生、毕业生及入门开发者,解决灰度图像自动上色与后续降噪优化问题。项目选用基于颜色转移的Welsh算法,通过颜色匹配将参考彩色图像色彩映射到目标灰度图;针对直接上色产生的噪声,引入导向滤波(Guided Filter)进行边缘保留去噪,避免高斯、Sobel等传统滤波器抹除细节或产生梯度反转伪影,使彩色化结果更自然。压缩包共16个文件,含4个Python源码、3个编译缓存、8张测试样图与1份Markdown项目说明;源码均带详细注释与测试图像,标注了实验运行环境,可直接运行验证效果,也便于对比分析不同参考图对彩色化结果的影响。整套资源仅533KB,轻量易读,适合毕业设计、课程设计或算法进阶参考,可基于注释修改实现其他功能。已有432人学习下载,适合想系统掌握主流彩色化算法及滤波优化技巧的读者。

1. 基于Welsh算法的灰度图像彩色化:为什么用“找相似”就能上色

灰度图着色是个不适定问题:同一个浅灰像素,可能是白墙、蓝天或银色车身。人脑能猜,靠的是语义先验;普通程序没有。Welsh算法的思路是绕开语义:在彩色参考图上采样,再用灰度图的局部亮度、邻域纹理去找“长得最像”的像素,把参考图的a、b颜色通道直接搬过来。没有深度学习的智能,但对老照片上色、黑白电影修复这类场景,它仍然是最容易跑通、最容易解释的基线。源码里常见的“优化”,基本集中在两点:怎么让匹配更快,怎么让颜色过渡更自然。下面按“原理→最小实现→参数优化→踩坑→批量封装”来拆,适合要从零复现并自己调参的人。

2. 核心前处理:先换成Lab颜色空间,再用均匀采样锁定参考色

如果你一上来就在RGB空间里做最近邻,大概率会翻车。RGB三个通道跟光照强耦合,两个看起来都是“灰色”的像素,在RGB距离上可能差得很远。Welsh算法是在Lab空间操作的。Lab把像素拆成L(亮度)加a、b(色度),这样匹配亮度时不被颜色干扰,传递颜色时直接复制a、b。OpenCV的cvtColor和skimage.color.rgb2lab都能完成转换,区别是OpenCV走BGR顺序,skimage走RGB且L范围是0~100。为了跟源码里常见写法保持一致,下面统一用OpenCV约定。

2.1 为什么RGB不行,Welsh非要用Lab?

RGB距离没有感知均匀性。同样是色差2,在暗部能感知,在亮部却看不出来;像素匹配时RGB某通道的噪声稍大,最近邻就会跳到颜色完全不对的像素。Lab接近感知均匀,L通道与a、b通道相关性低,适合把亮度和颜色分开处理。实际复现时我一般只取L通道做匹配特征,a、b只用来写回。如果你换成HSV,H通道在低饱和度时极为敏感,灰度图几乎没有饱和度,会导致参考像素乱跳。所以在基于Welsh算法的灰度图像彩色化项目里,第一步几乎永远是cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB),而不是在RGB里硬算距离。

2.2 灰度图转Lab的OpenCV写法:直接转会出现“假三通道”

灰度图只有一个通道,很多人直接写cv2.cvtColor(gray, cv2.COLOR_GRAY2LAB),OpenCV会报错,因为不存在这种枚举。正确做法是先cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)得到三通道BGR,每个通道值相同,再转LAB。这个“假三通道”不会引入色彩,Lab转换后a、b会在128附近,对应中性色。代码块如下:

import cv2 import numpy as np def gray_to_lab_single_channel(gray_path): gray = cv2.imread(gray_path, cv2.IMREAD_GRAYSCALE) bgr = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR) # 三个通道全等于gray lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB).astype(np.float32) # 如果只想取亮度L,直接取第0通道 L = lab[:, :, 0] # a、b用于后续写回,OpenCV的Lab里中性色是128 return lab, L

逻辑说明:COLOR_GRAY2BGR只是格式对位,把一个通道复制成三个通道,不引入彩色信息。astype(np.float32)很关键,后续要算局部标准差和KD树,uint8容易在滤波时溢出。若从skimage读入,注意L范围是0~100而不是0~255,匹配前要把目标图和参考图统一到同一量纲。我习惯全部统一到OpenCV的0~255范围,差值和滤波更直接,调试时看中间结果也直观。

2.3 参考图采样:均匀网格比纯随机更有效

Welsh用参考图的彩色信息建立颜色表,逐像素匹配不现实。纯随机采样在颜色分布极不均匀的图上会漏掉罕见色彩,而且随机种子会引发不可复现的结果。我一般用均匀网格采样:按步长在参考图上取行列索引,既保证空间覆盖,又天然可复现。

def sample_uniform_grid(lab_image, step=4): h, w, _ = lab_image.shape rows = np.arange(0, h, step) cols = np.arange(0, w, step) grid_y, grid_x = np.meshgrid(rows, cols, indexing='ij') grid_rows = grid_y.ravel() grid_cols = grid_x.ravel() sampled_lab = lab_image[grid_rows, grid_cols, :] # 同时把行列索引留着,排查时能定位颜色来自参考图的哪个区域 return sampled_lab, grid_rows, grid_cols

这个函数把参考图压成N×3矩阵,N大约等于(h/step)*(w/step)。step越小颜色表越完整,匹配越慢。对1920×1080参考图,step=4大约是十三万个样本,KD树还扛得住;step=2就超过五十万,内存和耗时快十倍。我一般把默认步长设在4到8之间,按目标图分辨率动态调。再补充一个原则:采样前最好先按亮度直方图对参考图做筛选,去掉曝光不足或过曝的区域,否则这些像素会占用大量样本却几乎没有可用颜色。

在Welsh的匹配里,亮度动态范围不一致会带来系统性偏色。比如目标图整体偏暗,参考图整体偏亮,匹配结果会把目标所有像素都拽到参考图的亮部颜色上。所以我会在采样之前做一步亮度重映射,把目标灰度图的亮度分布对齐到参考图:

def match_brightness_distribution(target_L, ref_L, clip_range=(5, 95)): p_t = np.percentile(target_L, clip_range) p_r = np.percentile(ref_L, clip_range) # 线性拉伸目标亮度到参考亮度的百分位区间 scale = (p_r[1] - p_r[0]) / (p_t[1] - p_t[0] + 1e-6) offset = p_r[0] - p_t[0] * scale mapped = np.clip(target_L * scale + offset, 0, 255) return mapped.astype(np.float32)

5和95百分位可以避免个别极端亮点把整体亮度带偏。给不同年代老照片处理时,这一步比后面调饱和更能决定成败;不做重映射,匹配结果会整体偏灰、偏粉,颜色浓淡也不对。

3. 匹配与颜色传递:Welsh算法最小可跑的Python实现

到了核心部分。前面所有准备都是为了在参考图里找到每个目标像素的“相似兄弟”,把它的颜色搬过来。最直接的实现是两层循环:外层遍历目标图,内层遍历参考样本,算亮度差找最小值。小图还能忍,一张640×480的图就有30万个目标像素,乘以十几万个参考样本,慢到没法用。所以我把scipy.spatial.cKDTree作为标准解法。

3.1 用“亮度+局部标准差”做匹配特征,纹理信息不能省

如果只用单点亮度L做匹配,会看到一种典型现象:天空和白色墙壁颜色被错配,因为它们在局部小块里都亮。Welsh强调邻域信息,就是为了让纹理相似度参与决策。常用特征有两种:当前像素为中心的窗口标准差,或窗口内亮度梯度的幅值。我一般用标准差,计算快参数少。窗口大小3~7像素,太小忽略纹理,太大会把边缘模糊。

def build_feature(L_channel, window=5): kernel = np.ones((window, window), np.float32) / (window * window) mean = cv2.filter2D(L_channel, -1, kernel, borderType=cv2.BORDER_REFLECT) mean_sq = cv2.filter2D(L_channel * L_channel, -1, kernel, borderType=cv2.BORDER_REFLECT) # 方差 = E[X^2] - (E[X])^2 variance = np.maximum(mean_sq - mean * mean, 0.0) stddev = np.sqrt(variance) # 特征向量是2维:亮度 + 局部标准差 feature = np.stack([L_channel, stddev], axis=2) # H x W x 2 return feature

cv2.filter2D做的是盒式滤波,BORDER_REFLECT反射边缘,避免图像边界出现黑边。np.maximum把浮点误差造成的负方差归零。堆叠后特征向量是2维,你还可以加水平梯度和垂直梯度扩展成4维,但计算量增加,对噪声更敏感。在精致老照片上,多一个维度带来的提升常不如手动把参考图选好来得明显。

3.2 最小实现:KD树最近邻匹配与ab通道写回

把参考特征建树,目标特征查询,一步到位:

from scipy.spatial import cKDTree def welsh_colorize(target_lab, ref_lab, window=5, sample_step=4, top_k=5): # 参考图取L、ab ref_L = ref_lab[:, :, 0] ref_feat_full = build_feature(ref_L, window) h, w = ref_feat_full.shape[:2] rows = np.arange(0, h, sample_step) cols = np.arange(0, w, sample_step) grid_y, grid_x = np.meshgrid(rows, cols, indexing='ij') sample_rows = grid_y.ravel() sample_cols = grid_x.ravel() ref_feat = ref_feat_full[sample_rows, sample_cols, :].reshape(-1, 2) ref_ab = ref_lab[sample_rows, sample_cols, 1:].reshape(-1, 2) kdt = cKDTree(ref_feat) target_L = target_lab[:, :, 0] target_feat = build_feature(target_L, window) # H x W x 2 target_flat = target_feat.reshape(-1, 2) # 查询最近邻,top_k=1时就是经典Welsh匹配 _, idx = kdt.query(target_flat, k=top_k, workers=-1) # idx形状是(N, top_k),取参考ab并做平均 matched_ab = ref_ab[idx].mean(axis=1).reshape(target_lab.shape[0], target_lab.shape[1], 2) out_lab = np.zeros_like(target_lab) out_lab[:, :, 0] = target_L out_lab[:, :, 1:3] = matched_ab out_bgr = cv2.cvtColor(out_lab.astype(np.uint8), cv2.COLOR_LAB2BGR) return out_bgr

逻辑说明:构建参考KD树,目标特征target_flat一次性查询,返回值idx里每一行是参考样本的索引。ref_ab[idx]形状为(N, top_k, 2),在axis=1取平均,就是k近邻颜色融合。workers=-1让所有CPU核心参与计算,一百万个目标像素的查询也就一两秒。最后把目标L和匹配ab合回,转BGR输出。astype(np.uint8)会截断浮点,但对显示结果影响很小;如果你要保存无损中间结果,可以保留float32先存npy。

这段代码好处是短。坏处是它没有做亮度重映射和后处理,所以直接用在普通图上效果可能偏噪。实际工程中我会在调用前先跑match_brightness_distribution,输出后再对ab做一些平滑。

3.3 参数表:window、sample_step、top_k应该怎么配

参数调优是Welsh项目最花时间的部分。我把常用范围和经验整理成表:

参数常用范围影响建议
window3~7纹理特征范围人物用5,风景用7,小图用3
sample_step4~8参考样本密度参考图大用6~8,小图用3~4
top_k1或5颜色平滑度5时ab取均值,色块感明显下降
亮度重映射百分位(5,95)动态范围匹配老照片用(10,90)更保守
ab中值滤波核3~5去除孤立色点先试0,再试3,强烈色斑时试5

window过小,纹理相似度退化成像素亮度;window过大,边缘完全被平滑,树叶和头发纹理没法区分。sample_step直接影响参考样本量,参考图有丰富色彩渐变时要取小一些,否则亮部或暗部颜色被抽稀。top_k不建议超过10,超过后颜色会被平均成灰色,饱和度反而下降。

4. 优化到底优化什么:亮度重映射、加速匹配和后处理三步

源码包里写“优化”两个字,指的不是换算法,而是围绕这三件事:匹配前让目标图和参考图更“门当户对”,匹配时用更少的时间,匹配后让颜色更干净。这三件事顺序不能反。先调亮度,再加速,最后平滑,每一步的效果都能单独量化。

4.1 先修参考图:亮度重映射和直方图匹配

第2章给的百分位拉伸是最轻量的做法。如果重映射后效果仍不对,可以升级到直方图匹配:把目标灰度图的累计分布函数映射到参考图的累计分布函数,这样每个亮度区间的像素占比一致。直方图匹配会让对比度变化更明显,但也会把灰雾放大。在老化胶片上我通常先用百分位拉伸,直方图匹配作为备选。这里要留意一个坑:参考图本身颜色偏色时,直方图匹配只会让偏色更重,所以参考图要先做简单白平衡,比如把a、b通道均值移到中性色。

4.2 加速:降采样到小分辨率匹配,再上采样ab通道

高分辨率目标图直接跑KD树,内存和耗时都不小。做法技巧是:先把目标灰度图缩到640或768像素长边,跑完匹配得到低分辨率ab通道,再用cv2.resize双线性插值放大回原分辨率,最后和目标原图L合并。ab通道本身是低频色度信息,双线性上采样不会像深度网络那样产生明显的伪影。代码略写成这样:

def colorize_at_lowres(gray_large, ref_lab, scale=0.5): h, w = gray_large.shape[:2] small_h, small_w = int(h * scale), int(w * scale) small_gray = cv2.resize(gray_large, (small_w, small_h), interpolation=cv2.INTER_AREA) small_bgr = cv2.cvtColor(small_gray, cv2.COLOR_GRAY2BGR) small_lab = cv2.cvtColor(small_bgr, cv2.COLOR_BGR2LAB).astype(np.float32) result_small_lab = welsh_colorize(small_lab, ref_lab, window=5, sample_step=5, top_k=5) # 取ab通道放大回原尺寸 ab_up = cv2.resize(result_small_lab[:, :, 1:3], (w, h), interpolation=cv2.INTER_LINEAR) gray_bgr = cv2.cvtColor(gray_large, cv2.COLOR_GRAY2BGR) gray_lab = cv2.cvtColor(gray_bgr, cv2.COLOR_BGR2LAB).astype(np.float32) gray_lab[:, :, 1:3] = ab_up out_bgr = cv2.cvtColor(gray_lab.astype(np.uint8), cv2.COLOR_LAB2BGR) return out_bgr

INTER_AREA降采样抗锯齿好,INTER_LINEAR上采样平滑。降到720p以下时,单张图的匹配耗时通常不到一秒。缺点是精细边缘会有点颜色渗边,此时可以把缩放系数提高到0.7,或在上采样后对ab做3×3中值滤波。

4.3 平滑:ab通道中值滤波与饱和度自适应

逐像素最近邻必然存在颜色不连续。一个像素匹配到参考图的蓝天上,旁边像素因为微小的亮度差异匹配到屋顶,于是出现孤立色点。修复方法是只对ab通道滤波,不能动L通道。cv2.medianBlur不能直接处理双通道浮点,我会拆通道分别处理或用cv2.filter2D做核加权。中值滤波天然保边缘,比高斯滤波更合适。滤波核大小时机很关键:先跑一次不加滤波的输出,看色点密集程度,再决定用3还是5;一上来就上5会把浅色蓝天染成青色斑块。

饱和度自适应是最后一步。完成匹配后,统计参考图ab通道的标准差,如果匹配输出ab标准差只有参考图的一半,说明颜色被平均过程稀释,可以用一个1.0~1.3的系数拉伸ab。但拉伸系数超过1.3时,肤色容易变成“荧光脸”,所以我只对自然风景批量处理时开1.1左右,人像基本不开。

4.4 参数组合建议:老照片、黑白电影、灰暗扫描件

三类常见输入,我总结了三套心得。老照片人像:参考图选光线柔和、肤色正常的中年女性或男性正面照,window=5,top_k=5,输出后对ab中值滤波3×3,不做饱和度拉伸。黑白电影:按镜头预剪片段,每段单独选参考帧,window=7,sample_step=6,ab饱和度1.05,防止整体偏绿。灰暗扫描件:先做百分位(10,90)的亮度重映射,目标缩到800px,输出后再把四周暗角裁掉;因为扫描件动态范围窄,不重映射的结果色块很重。

这些组合不是万能公式,但总比在源码里瞎试参数靠谱。原则是:一次只改一个参数,改一次出一次图,拿上次输出并排放着看。Welsh是统计匹配,跟“深度玄学”一样,效果受参考图影响极大,参数调优的过程必须保留版本记录。

5. 避坑指南:Welsh着色复现中的5个典型踩坑与排查

这一节内容来自我自己复现和排查的积累。Welsh这类算法看着简单,实际跑起来处处是意料之外的偏色,不把现象、原因、解决方法串起来看,很容易陷入换一张参考图试一次的循环。

5.1 颜色一大片偏蓝或偏黄,整体像蒙了一层滤镜

现象:输出图整体色调偏移到某个奇怪方向,比如蓝调或黄调。原因:参考图亮度分布与目标图不一致,大量目标像素集中匹配到参考图最亮或最暗的那一小批样本上;参考图整体偏蓝,结果当然偏蓝。解决:先做百分位亮度重映射,再做采样。条件允许时,参考图上色前先做白平衡,把a、b通道均值拉回中性色。很多源码包不会自动做这一步,只能靠调用方处理。

5.2 输出图有拼接感,亮暗区域过渡处出现彩色斑块

现象:物体边缘有彩边,平面区域颜色像补丁。原因:每个目标像素独立查最近邻,没有约束相邻像素颜色一致性,这是逐点匹配方法的通病。解决:把top_k从1改成5,取多个最近邻的ab均值;再对ab通道做3×3中值滤波。如果还有斑块,把window从3加到7,让纹理特征更稳定,匹配不会因为单个像素噪声跳变。

5.3 明明加了KD树还是慢,CPU单核跑满

现象:KD树查询很快,但整体耗时几十秒甚至几分钟。原因:耗时通常不在query,而在两层Python循环做邻域特征、逐像素取颜色、反复调用cv2.cvtColor。解决:先用time.perf_counter分阶段测耗时,把循环换成矩阵索引ref_ab[idx],把参考样本压进连续numpy数组。另一个常见坑是cKDTree默认多线程,但构建特征时受GIL限制,单核跑满很正常;优先优化特征构建阶段的向量化。

5.4 换一张参考图效果就翻天,黑匣子行为明显

现象:同样的参数和目标图,参考图从风景换成建筑,输出连天空都变成砖红色。原因:Welsh不做语义理解,它只匹配纹理和亮度。砖墙和天空在局部方差上可能非常接近,错误匹配被放大。解决:选择与目标图同域的参考图;更进一步的方案是把目标图按物体区域粗略分成上下两半,分别匹配不同参考区域,也就是“二次匹配”的思路。我在源码里看到的优化也常沿着这个方向做,只不过没有公开写进注释。

5.5 生成的彩色照片饱和度低得感人

现象:颜色很淡,几乎接近灰度。原因:可能是参考图本身不饱和,也可能是因为top_k取平均时,大量中性的相似像素把色度稀释了。解决:检查参考图ab通道标准差;若目标输出标准差明显偏低,在Lab空间做饱和度拉伸:new_a = (a - 128) * s + 128,s在1.0~1.3之间。注意人像不要开超过1.1,否则肤色会发红。饱和度低“感人”,但至少没有偏色,比蒙了一层滤镜更容易通过后处理救回来。

6. 进阶用法:把Welsh着色封装成可批量处理的模块,并顺手验证效果

当手上有几十张黑白照片要批量上色,代码得从“脚本”变成“可复用模块”。我会把参考图和目标图路径、window、sample_step、top_k、是否做亮度重映射、降采样尺寸、ab中值滤波核这些参数收进一个配置对象里,参考图首次加载后把KD树缓存下来,后续只查树不重建。

接口上可以设计成类似这样的结构:

class WelshColorizer: def __init__(self, ref_lab, window=5, sample_step=4): ref_L = ref_lab[:, :, 0] ref_feat = build_feature(ref_L, window) # 这里抽稀参考样本并建树,参考图不变时只建一次 self.kdt = cKDTree(...) self.ref_ab = ... def colorize(self, target_lab, top_k=5): # 查询、取ab、写回L return out_bgr

批处理时,我会先跑一张基准图,保存成base.png;之后所有参数改动都跟base.png对比,而不是凭记忆“感觉变好了”。这个习惯救过我很多次,因为Welsh的效果波动很大,眼睛很容易被单张好看结果带偏。

验证效果时不能只看主观感受。如果手里有原彩色图当标注,我会跑三个指标:PSNR看整体重建质量,SSIM看结构保持,ΔE看色差。若没有标注,就把每张黑白图分别结合两张不同参考图,比较两次输出在ab通道上的一致性,一致性差说明算法方差大,那点参数优化就不具统计意义。常用的实现是skimage.metrics.structural_similarity和colormath或OpenCV自带Lab距离。要注意SSIM对L通道变化很敏感,所以评估色差时应固定L为原灰度,只比较ab通道。

Welsh的边界也要认清。单张复杂场景、人脸特写或对语义敏感的老照片,我会优先用深度图着色模型当粗上色,再用Welsh做局部颜色传递微调。Welsh更像一个可解释、可定制的方案,用它做批量预览、给人工上色打草稿,比硬扛深度学习模型更省心。我自己的习惯是:任何项目都先跑通未优化Welsh拿到基线图,再上参数优化;连基线图都不看就调参,最后容易陷进没有对照的“玄学调参”里。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询