ARTICLE DETAIL

资讯详情

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

Welsh灰度图像彩色化:Python实现与向量化加速优化

Welsh灰度图像彩色化:Python实现与向量化加速优化 简介本资源面向计算机、人工智能、通信工程等专业的在校学生与教师以及希望进阶学习图像处理的小白开发者提供基于Welsh算法的灰度图像彩色化处理及优化完整Python实现。项目先利用Welsh算法将灰度图像自动彩色化再引入导向滤波进行去噪优化有效缓解双边滤波常见的梯度反转伪影使结果更自然真实可作为毕业设计、课程设计或项目立项演示的参考方案。压缩包共16个文件包含4个py源码文件、8张jpg示例图像、3个pyc缓存文件及1份md项目说明整体约533KB源码附带详细注释便于理解算法流程与调参思路。目前已有431人学习下载。读者可从中获取颜色转移彩色化的核心实现、导向滤波去噪的完整代码、参考图与灰度图的配对示例以及项目结构组织方式适合在此基础上修改扩展以实现其他图像处理功能。1. Welsh灰度图像彩色化从一张黑白老照片说起手里有一张几十年前的黑白照片人脸轮廓清晰但毫无色彩想给它上色又不想在 Photoshop 里一层层手涂。这种需求在修复老照片、给漫画线稿上色、医学影像增强等场景里反复出现。Welsh 算法就是解决这类问题的经典方案它不依赖深度学习模型不需要 GPU只用一张彩色参考图和一张灰度目标图通过亮度匹配和邻域纹理统计把参考图的颜色迁移过去。核心思路是——灰度图里每个像素的亮度值在参考图中找到亮度最接近的若干候选像素再在这些候选里用邻域标准差和均值做二次筛选最终加权得到颜色。Python 实现这套流程代码量不大但参数调不好就会出现颜色溢出、区域串色、边缘模糊等问题。这篇文章面向想动手复现 Welsh 算法并做优化的开发者从原理拆到代码从参数讲到踩坑最后给出可运行的完整实现路径。2. Welsh算法原理拆解与Python实现骨架2.1 亮度匹配与邻域统计Welsh到底在算什么Welsh 算法的核心假设很朴素灰度图中亮度相近的像素在彩色参考图中也应该有相近的颜色。但只靠单像素亮度匹配会出大问题——不同物体可能有相同亮度比如白墙和白云直接匹配就会把墙涂成云的颜色。所以 Welsh 在亮度匹配之后加了一层邻域统计过滤。具体流程分三步。第一步把参考图和目标灰度图都转到 YUV 或 Lab 空间取出亮度通道 Y 或 L。第二步对目标图中每个像素在参考图中找亮度差最小的 N 个候选像素N 一般取 20 到 50。第三步在这 N 个候选里计算每个候选像素邻域比如 5×5 窗口的均值和标准差和目标像素邻域的均值和标准差做比较用加权距离重新排序取最优的 M 个候选M 一般取 5 到 10。最后把这 M 个候选的颜色值加权平均赋给目标像素。这里的关键参数有三个候选数 N、最终保留数 M、邻域窗口大小。N 太小会导致候选不足颜色选择范围窄N 太大则引入噪声计算量也上去。M 决定最终颜色的平滑程度M 越大越平滑但可能混入不相关颜色。邻域窗口大小影响纹理匹配的敏感度窗口太小对噪声敏感窗口太大则丢失局部细节。用 Python 实现时numpy 的广播机制可以大幅简化距离计算。下面是一个最小可运行的骨架代码import numpy as np from PIL import Image def rgb2yuv(img): RGB转YUV返回Y亮度通道和UV色度通道 R, G, B img[:,:,0], img[:,:,1], img[:,:,2] Y 0.299 * R 0.587 * G 0.114 * B U -0.147 * R - 0.289 * G 0.436 * B V 0.615 * R - 0.515 * G - 0.100 * B return Y, U, V def welsh_transfer(gray_img, ref_img, N30, M8, win5): gray_img: 目标灰度图 (H, W) ref_img: 参考彩色图 (Hr, Wr, 3) N: 亮度候选数 M: 最终保留数 win: 邻域窗口大小 ref_y, ref_u, ref_v rgb2yuv(ref_img.astype(np.float32) / 255.0) ref_y_flat ref_y.flatten() ref_u_flat ref_u.flatten() ref_v_flat ref_v.flatten() h, w gray_img.shape result np.zeros((h, w, 3), dtypenp.float32) pad win // 2 gray_pad np.pad(gray_img, pad, modereflect) for i in range(h): for j in range(w): # 目标像素亮度 y_target gray_img[i, j] / 255.0 # 亮度距离 y_dist np.abs(ref_y_flat - y_target) # 取前N个候选 idx_n np.argpartition(y_dist, N)[:N] # 邻域统计 target_patch gray_pad[i:iwin, j:jwin] / 255.0 target_mean target_patch.mean() target_std target_patch.std() # 计算候选的邻域统计简化用参考图对应位置的局部窗口 # 实际实现需预计算参考图每个像素的邻域统计 scores [] for idx in idx_n: ri, rj idx // ref_img.shape[1], idx % ref_img.shape[1] r_pad np.pad(ref_y, pad, modereflect) ref_patch r_pad[ri:riwin, rj:rjwin] ref_mean ref_patch.mean() ref_std ref_patch.std() # 加权距离 score y_dist[idx] 0.5 * abs(target_mean - ref_mean) 0.5 * abs(target_std - ref_std) scores.append((score, idx)) scores.sort(keylambda x: x[0]) top_m scores[:M] # 加权平均颜色 weights np.array([1.0 / (s[0] 1e-6) for s in top_m]) weights / weights.sum() u_val sum(w * ref_u_flat[s[1]] for w, s in zip(weights, top_m)) v_val sum(w * ref_v_flat[s[1]] for w, s in zip(weights, top_m)) # YUV转RGB y_val y_target r y_val 1.140 * v_val g y_val - 0.395 * u_val - 0.581 * v_val b y_val 2.032 * u_val result[i, j] np.clip([r, g, b], 0, 1) return (result * 255).astype(np.uint8)这段代码展示了 Welsh 算法的核心逻辑但直接跑会非常慢——双重循环加内层循环一张 512×512 的图可能要跑几分钟。实际工程中必须做向量化优化后面章节会讲。参数方面N30、M8、win5 是一组比较稳妥的默认值适合大多数自然场景。如果参考图和目标图内容差异大可以适当增大 N 到 50让候选池更丰富。2.2 从YUV到RGB颜色空间转换的工程细节Welsh 算法在 YUV 空间做匹配但最终输出要转回 RGB。YUV 转 RGB 的公式看起来简单但有几个工程细节容易翻车。第一YUV 的取值范围。如果 Y 在 [0,1]U 和 V 通常在 [-0.5, 0.5] 附近但不同标准BT.601、BT.709系数不同。上面代码用的是 BT.601 系数适合标清内容。如果处理高清图建议换成 BT.709# BT.709 系数 Y 0.2126 * R 0.7152 * G 0.0722 * B U -0.09991 * R - 0.33609 * G 0.436 * B V 0.615 * R - 0.55861 * G - 0.05639 * B第二转回 RGB 后要 clip 到 [0,1]否则会出现颜色溢出表现为某些区域过亮或过暗。第三如果参考图和目标图的亮度分布差异大建议先做直方图匹配把目标图的亮度分布对齐到参考图再做 Welsh 迁移。这一步能显著减少颜色偏差。def histogram_match(source, reference): 将source的直方图匹配到reference src_hist, _ np.histogram(source.flatten(), 256, [0, 256]) ref_hist, _ np.histogram(reference.flatten(), 256, [0, 256]) src_cdf np.cumsum(src_hist).astype(np.float64) ref_cdf np.cumsum(ref_hist).astype(np.float64) src_cdf / src_cdf[-1] ref_cdf / ref_cdf[-1] # 建立映射 mapping np.zeros(256, dtypenp.uint8) for i in range(256): diff np.abs(src_cdf[i] - ref_cdf) mapping[i] np.argmin(diff) return mapping[source]直方图匹配这步在参考图和目标图曝光差异大时几乎是必做的否则 Welsh 匹配会偏向参考图中亮度集中的区域导致颜色单一。我一般会先跑一遍直方图匹配肉眼确认目标图亮度分布和参考图接近了再进 Welsh 主流程。3. 向量化加速把分钟级降到秒级3.1 用numpy广播替代双重循环上一章的骨架代码用了 Python 双重循环512×512 的图要跑 26 万次内层循环每次还要做 argpartition 和排序实际耗时可能超过 5 分钟。这在工程上不可接受。优化方向很明确把逐像素操作改成矩阵操作。核心思路是预计算参考图所有像素的邻域统计量然后用 numpy 的广播机制一次性计算目标图所有像素和参考图所有像素的亮度距离。内存够的话可以构建一个 (HW, HrWr) 的距离矩阵但这样内存爆炸。更实际的做法是分块处理每次处理目标图的一行或一块。def precompute_ref_stats(ref_y, win5): 预计算参考图每个像素的邻域均值和标准差 pad win // 2 ref_pad np.pad(ref_y, pad, modereflect) h, w ref_y.shape means np.zeros((h, w), dtypenp.float32) stds np.zeros((h, w), dtypenp.float32) for i in range(h): for j in range(w): patch ref_pad[i:iwin, j:jwin] means[i, j] patch.mean() stds[i, j] patch.std() return means, stds def welsh_vectorized(gray_img, ref_img, N30, M8, win5, block_size64): 向量化版本分块处理 ref_y, ref_u, ref_v rgb2yuv(ref_img.astype(np.float32) / 255.0) ref_means, ref_stds precompute_ref_stats(ref_y, win) h, w gray_img.shape gray_norm gray_img.astype(np.float32) / 255.0 pad win // 2 gray_pad np.pad(gray_norm, pad, modereflect) # 预计算目标图邻域统计 target_means np.zeros((h, w), dtypenp.float32) target_stds np.zeros((h, w), dtypenp.float32) for i in range(h): for j in range(w): patch gray_pad[i:iwin, j:jwin] target_means[i, j] patch.mean() target_stds[i, j] patch.std() ref_y_flat ref_y.flatten() ref_u_flat ref_u.flatten() ref_v_flat ref_v.flatten() ref_means_flat ref_means.flatten() ref_stds_flat ref_stds.flatten() result np.zeros((h, w, 3), dtypenp.float32) for bi in range(0, h, block_size): for bj in range(0, w, block_size): ei min(bi block_size, h) ej min(bj block_size, w) block_y gray_norm[bi:ei, bj:ej].flatten() block_tm target_means[bi:ei, bj:ej].flatten() block_ts target_stds[bi:ei, bj:ej].flatten() # 亮度距离矩阵 (block_pixels, ref_pixels) y_dist np.abs(block_y[:, None] - ref_y_flat[None, :]) # 邻域统计距离 mean_dist np.abs(block_tm[:, None] - ref_means_flat[None, :]) std_dist np.abs(block_ts[:, None] - ref_stds_flat[None, :]) total_dist y_dist 0.5 * mean_dist 0.5 * std_dist # 每行取前N个 idx_n np.argpartition(total_dist, N, axis1)[:, :N] # 在这N个里取前M个 rows np.arange(idx_n.shape[0])[:, None] dist_n total_dist[rows, idx_n] idx_m np.argpartition(dist_n, M, axis1)[:, :M] final_idx idx_n[rows, idx_m] final_dist dist_n[rows, idx_m] # 加权平均 weights 1.0 / (final_dist 1e-6) weights / weights.sum(axis1, keepdimsTrue) u_vals ref_u_flat[final_idx] v_vals ref_v_flat[final_idx] u_out (weights * u_vals).sum(axis1) v_out (weights * v_vals).sum(axis1) # YUV转RGB y_out block_y r y_out 1.140 * v_out g y_out - 0.395 * u_out - 0.581 * v_out b y_out 2.032 * u_out rgb_block np.stack([r, g, b], axis1) rgb_block np.clip(rgb_block, 0, 1) result[bi:ei, bj:ej] rgb_block.reshape(ei-bi, ej-bj, 3) return (result * 255).astype(np.uint8)这个向量化版本把内层循环全部消掉用矩阵运算替代。block_size 控制每次处理的目标像素数64 是一个比较平衡的值——太小则循环次数多太大则距离矩阵内存占用高。以 512×512 目标图和 256×256 参考图为例block_size64 时每次距离矩阵大小是 4096×65536约 2.7 亿个 float32内存约 1GB。如果内存紧张把 block_size 降到 32。实测下来向量化版本比原始双重循环快 20 到 50 倍512×512 的图能在 10 秒内跑完。如果还想更快可以把距离计算放到 GPU 上用 cupy但那就超出纯 Python 的范围了。3.2 参数调优N、M、win怎么选Welsh 算法的输出质量高度依赖三个参数。我整理了一张对照表方便快速定位参数作用推荐范围偏小的影响偏大的影响N亮度候选数20-50颜色选择少区域偏色计算量增大噪声引入M最终保留数5-10颜色不平滑有噪点颜色混浊边缘模糊win邻域窗口3-9对噪声敏感纹理匹配差丢失局部细节边缘过渡宽调参顺序建议先固定 win5调 N 让颜色丰富度合适再调 M 让平滑度满意最后微调 win 改善边缘。如果目标图噪声多win 可以取 7 或 9如果目标图细节丰富比如人脸win 取 3 或 5 更好。还有一个隐藏参数是邻域统计的权重。上面代码里 mean_dist 和 std_dist 的系数都是 0.5实际可以根据场景调整。如果参考图和目标图纹理差异大把 std_dist 的权重调高到 0.7 或 0.8让纹理匹配占主导。如果参考图和目标图整体亮度分布接近把 mean_dist 权重降到 0.3。# 纹理主导场景 total_dist y_dist 0.3 * mean_dist 0.7 * std_dist # 亮度主导场景 total_dist y_dist 0.7 * mean_dist 0.3 * std_dist这两个权重没有理论最优值靠实验试出来。我一般会准备 3 到 5 组参考图每组跑一遍看效果选综合表现最好的那组权重。4. 避坑与排查Welsh彩色化常见的5个翻车现场4.1 颜色溢出到不该有的区域现象目标图中某些区域被涂上了参考图中完全不相干的颜色比如天空被涂成草地绿。原因亮度匹配只看了单像素亮度没有考虑空间位置。如果参考图中某个亮度值对应的颜色恰好是绿色而目标图天空区域也有相同亮度就会被误匹配。解决增大邻域统计的权重让纹理信息参与筛选。另外可以在匹配前对目标图做超像素分割限制每个超像素内的颜色来自参考图的同一区域。简单做法是加一个空间距离惩罚项# 在total_dist中加入空间距离 spatial_dist np.abs(block_i - ref_i) np.abs(block_j - ref_j) # 归一化后 total_dist y_dist 0.5 * mean_dist 0.5 * std_dist 0.1 * spatial_dist4.2 输出图像整体偏灰或偏暗现象彩色化后的图看起来像蒙了一层灰颜色不鲜艳。原因YUV 转 RGB 时 Y 通道直接用了目标图的亮度但目标图的亮度分布可能和参考图不一致导致颜色饱和度被压缩。解决先做直方图匹配把目标图亮度对齐到参考图。或者在 YUV 转 RGB 后做一次饱和度增强# 饱和度增强 def enhance_saturation(rgb, factor1.3): hsv rgb_to_hsv(rgb) hsv[:,:,1] np.clip(hsv[:,:,1] * factor, 0, 1) return hsv_to_rgb(hsv)4.3 边缘出现彩色噪点现象物体边缘有红绿蓝的杂色像素像彩噪。原因邻域窗口跨越了边缘导致统计量混入了两侧的信息匹配时选到了不合适的颜色。解决用双边滤波或者导向滤波对邻域统计量做平滑或者在边缘处减小窗口。更简单的做法是对最终结果做一次中值滤波from scipy.ndimage import median_filter result_filtered median_filter(result, size3)4.4 大区域颜色单一缺乏层次现象大片区域比如人脸、天空只有一种颜色没有明暗变化。原因M 取值太大加权平均把候选颜色混得太均匀丢失了局部变化。解决减小 M 到 3 或 5让颜色选择更集中。另外可以引入亮度重映射把目标图的亮度细节叠加到颜色上# 保留目标图亮度细节 y_detail gray_norm - target_means # 高频细节 result_rgb result_rgb 0.3 * y_detail[:,:,None]4.5 处理大图时内存溢出现象跑 1024×1024 以上的图时程序崩溃报 MemoryError。原因距离矩阵太大block_size 设置不合理。解决减小 block_size 到 16 或 32或者用 float16 代替 float32 存储距离矩阵。如果还不行改用逐行处理每次只算一行目标像素和所有参考像素的距离。# 逐行处理内存占用最小 for i in range(h): row_y gray_norm[i, :] y_dist np.abs(row_y[:, None] - ref_y_flat[None, :]) # ... 后续处理5. 进阶技巧用导向滤波提升边缘质量Welsh 算法最被诟病的地方是边缘处理——邻域窗口在边缘处会混入两侧信息导致颜色渗色。一个有效的改进是在 Welsh 迁移之后用导向滤波Guided Filter对结果做一次边缘保持平滑。导向滤波用目标灰度图作为引导图让颜色在亮度边缘处不扩散。def guided_filter(guide, src, radius8, eps1e-3): guide: 引导图 (H, W)用目标灰度图 src: 待滤波图 (H, W, 3)用Welsh输出 radius: 滤波半径 eps: 正则化参数 from scipy.ndimage import uniform_filter guide guide.astype(np.float32) src src.astype(np.float32) mean_g uniform_filter(guide, size2*radius1) mean_s np.stack([uniform_filter(src[:,:,c], size2*radius1) for c in range(3)], axis2) cov_gs np.stack([uniform_filter(guide * src[:,:,c], size2*radius1) - mean_g * mean_s[:,:,c] for c in range(3)], axis2) var_g uniform_filter(guide * guide, size2*radius1) - mean_g * mean_g a cov_gs / (var_g[:,:,None] eps) b mean_s - a * mean_g[:,:,None] mean_a np.stack([uniform_filter(a[:,:,c], size2*radius1) for c in range(3)], axis2) mean_b np.stack([uniform_filter(b[:,:,c], size2*radius1) for c in range(3)], axis2) return mean_a * guide[:,:,None] mean_b导向滤波的关键参数是 radius 和 eps。radius 控制平滑范围一般取 4 到 16边缘密集的图取小值。eps 控制边缘保持强度取 1e-3 到 1e-1越小边缘保持越好但可能引入伪影。我一般先用 radius8、eps1e-3 跑一遍如果边缘还是有渗色把 radius 降到 4如果颜色过渡太生硬把 eps 升到 1e-2。另一个进阶方向是引入超像素分割做区域约束。用 SLIC 算法把目标图分成若干超像素每个超像素内只从参考图的对应区域选颜色这样能从根本上避免跨区域串色。SLIC 可以用 skimage 的 segmentation.slic 直接调from skimage.segmentation import slic segments slic(gray_img, n_segments200, compactness10) # 对每个超像素统计其亮度均值和标准差在参考图中找匹配区域这套组合拳——Welsh 迁移 导向滤波 超像素约束——能把彩色化质量提升一个档次代价是计算量增加约 30%。如果追求实时性可以只保留 Welsh 导向滤波超像素那步跳过。最后说一个我踩过的坑参考图的选择比算法参数重要得多。选一张色调统一、内容相似的参考图比调半天参数效果好得多。我一般会准备 5 到 10 张候选参考图快速跑一遍看哪张最顺眼再针对那张图微调参数。这个习惯帮我省了大量调参时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表