简介本资源面向计算机、人工智能、通信工程等专业的在校学生与教师以及希望进阶学习图像处理的小白开发者提供一套基于Welsh算法的灰度图像彩色化处理及优化完整方案。项目先利用Welsh算法将灰度图像自动彩色化再引入导向滤波进行去噪优化有效缓解传统滤波器在去噪时丢失细节、增强细节时放大噪声的问题使结果更自然真实。压缩包共16个文件包含4个py源码文件、8张jpg示例图片、3个pyc缓存文件及1份md项目说明整体约533KB源码附带详细注释便于理解算法流程与调参思路。目前已有431人学习下载。读者可获得可直接运行的彩色化与导向滤波实现代码、参考图与灰度图对照素材以及项目说明文档适合作为毕业设计、课程设计或图像处理入门项目的参考也可在此基础上修改扩展实现其他功能。1. Welsh 算法做灰度图彩色化为什么它至今仍是性价比最高的入门方案手里有一批老照片、医学影像或者监控截图全是灰度的想上色又不想训练模型——这是很多人搜「Welsh算法 灰度图像彩色化 python」时的真实处境。Welsh 算法是 Rehinhard 色彩传递体系里最实用的一支给一张彩色参考图把它的色彩「搬」到灰度图上靠的是亮度重映射加邻域统计匹配不需要神经网络不需要 GPU一张 512×512 的图在普通笔记本上跑完只要几秒。它的核心假设很朴素——灰度图里亮度相近、纹理相似的区域在参考图里也应该对应相近的颜色。这个假设在风景、人像、建筑这类自然图像上成立得相当好所以它成了课程设计、图像处理入门、老照片修复预处理里的常客。这篇笔记不讲空理论从算法拆解到 Python 源码落地再到参数调优和翻车现场一步步带你跑通一套能用的彩色化流程。适合有 Python 基础、懂一点 NumPy 和 OpenCV、想真正把灰度图彩色化做出来的人。2. Welsh 算法的匹配逻辑与 Python 工程拆解2.1 亮度重映射把参考图的色调对齐到灰度图Welsh 算法的第一步不是直接匹配颜色而是先把参考图的亮度分布「拉」到和灰度图一致。原因很直接如果参考图整体偏暗、灰度图整体偏亮直接按亮度找对应像素会全错位。做法是对参考图做亮度统计重映射让它的均值和标准差逼近灰度图。import numpy as np import cv2 def luminance_remap(ref_bgr, gray): 将参考图的亮度分布重映射到目标灰度图的分布上。 ref_bgr: 参考图BGR 三通道 gray: 目标灰度图单通道 返回: 亮度重映射后的参考图保持色度不变 ref_lab cv2.cvtColor(ref_bgr, cv2.COLOR_BGR2LAB).astype(np.float32) L_ref ref_lab[:, :, 0] L_gray gray.astype(np.float32) # 统计量对齐均值 标准差 mean_ref, std_ref L_ref.mean(), L_ref.std() mean_gray, std_gray L_gray.mean(), L_gray.std() # 防止除零std 太小时给一个下限 std_ref max(std_ref, 1e-6) L_new (L_ref - mean_ref) * (std_gray / std_ref) mean_gray L_new np.clip(L_new, 0, 255) ref_lab[:, :, 0] L_new return cv2.cvtColor(ref_lab.astype(np.uint8), cv2.COLOR_LAB2BGR)这段代码的关键在std_gray / std_ref这个缩放因子。如果参考图对比度很高std 大灰度图很平std 小缩放后参考图的亮度会被压扁颜色过渡更柔和。反过来如果灰度图对比度大颜色会被拉开。np.clip那行不能省LAB 空间里 L 通道超出 0-255 会导致后面转回 BGR 时出现色偏。实际调参时如果你发现上色结果整体偏灰、饱和度不够先检查这一步的 std 是不是被压得太狠了。2.2 邻域统计匹配Welsh 的核心搜索过程亮度对齐之后进入 Welsh 算法真正的核心对灰度图里每一个像素在参考图里找亮度最接近、且邻域统计特征也最接近的像素把它的颜色赋过来。Welsh 原文用的是「亮度 邻域均值和标准差」作为匹配特征比单纯比亮度鲁棒得多。def welsh_colorize(gray, ref_bgr, patch_size7, search_ratio1.0): Welsh 算法主流程。 gray: 目标灰度图 ref_bgr: 参考图已做亮度重映射 patch_size: 邻域窗口大小必须是奇数 search_ratio: 搜索范围比例1.0 表示全图搜索 h, w gray.shape ref_lab cv2.cvtColor(ref_bgr, cv2.COLOR_BGR2LAB).astype(np.float32) ref_L ref_lab[:, :, 0] # 预计算参考图每个像素的邻域均值和标准差 k patch_size ref_mean cv2.blur(ref_L, (k, k)) ref_sq_mean cv2.blur(ref_L ** 2, (k, k)) ref_std np.sqrt(np.maximum(ref_sq_mean - ref_mean ** 2, 0)) # 目标图同样计算邻域统计 gray_f gray.astype(np.float32) tgt_mean cv2.blur(gray_f, (k, k)) tgt_sq_mean cv2.blur(gray_f ** 2, (k, k)) tgt_std np.sqrt(np.maximum(tgt_sq_mean - tgt_mean ** 2, 0)) # 把参考图特征拉平成向量方便批量计算距离 ref_feat np.stack([ref_L.ravel(), ref_mean.ravel(), ref_std.ravel()], axis1) result_lab np.zeros((h, w, 3), dtypenp.float32) result_lab[:, :, 0] gray_f # 权重亮度最重要均值和标准差辅助 weights np.array([1.0, 0.5, 0.5]) for y in range(h): for x in range(w): feat np.array([gray_f[y, x], tgt_mean[y, x], tgt_std[y, x]]) diff (ref_feat - feat) * weights dist np.sum(diff ** 2, axis1) best np.argmin(dist) ry, rx divmod(best, ref_L.shape[1]) result_lab[y, x, 1] ref_lab[ry, rx, 1] result_lab[y, x, 2] ref_lab[ry, rx, 2] return cv2.cvtColor(result_lab.astype(np.uint8), cv2.COLOR_LAB2BGR)weights这三个数是 Welsh 算法里最值得调的参数。亮度权重给 1.0 是基准均值和标准差各 0.5 是经验值。如果你处理的是纹理丰富的图比如草地、砖墙把 std 权重提到 0.8 会让匹配更准如果是平滑的人像皮肤把 mean 权重提到 0.7 效果更好。patch_size一般取 5 到 9太小了统计不稳太大了边缘会糊。2.3 用 KD-Tree 把 O(N²) 搜索压到可接受范围上面那段双重循环是能跑但 512×512 的图就是 26 万次全图搜索每次还要算几万维的距离纯 Python 跑要几分钟。实际工程里必须上 KD-Tree 或者 FLANN 加速。from scipy.spatial import cKDTree def welsh_colorize_kdtree(gray, ref_bgr, patch_size7): 用 KD-Tree 加速的 Welsh 算法速度提升 10 倍以上。 h, w gray.shape ref_lab cv2.cvtColor(ref_bgr, cv2.COLOR_BGR2LAB).astype(np.float32) ref_L ref_lab[:, :, 0] k patch_size ref_mean cv2.blur(ref_L, (k, k)) ref_sq cv2.blur(ref_L ** 2, (k, k)) ref_std np.sqrt(np.maximum(ref_sq - ref_mean ** 2, 0)) gray_f gray.astype(np.float32) tgt_mean cv2.blur(gray_f, (k, k)) tgt_sq cv2.blur(gray_f ** 2, (k, k)) tgt_std np.sqrt(np.maximum(tgt_sq - tgt_mean ** 2, 0)) weights np.array([1.0, 0.5, 0.5]) ref_feat np.stack([ref_L.ravel(), ref_mean.ravel(), ref_std.ravel()], axis1) * weights tree cKDTree(ref_feat) tgt_feat np.stack([gray_f.ravel(), tgt_mean.ravel(), tgt_std.ravel()], axis1) * weights _, idx tree.query(tgt_feat, k1) ref_flat ref_lab.reshape(-1, 3) matched ref_flat[idx].reshape(h, w, 3) matched[:, :, 0] gray_f # 保留原图亮度 return cv2.cvtColor(matched.astype(np.uint8), cv2.COLOR_LAB2BGR)cKDTree的query一次性对所有目标像素做批量查询底层是 C 实现比 Python 循环快一个数量级。注意ref_feat和tgt_feat都要乘同样的weights否则距离度量就不一致了。k1表示只取最近的一个邻居如果你想做多邻居加权平均后面进阶部分会讲把k设成 5 或 10。3. 从零跑通环境配置、参考图选择与完整调用3.1 环境依赖与最小可运行脚本先把环境搭起来。Python 3.8 以上都行核心依赖就三个NumPy 做矩阵运算OpenCV 做色彩空间转换和滤波SciPy 提供 KD-Tree。pip install numpy opencv-python scipy如果你用的是 VSCode记得在设置里把 Python 解释器选对不然cv2导入报错能让你查半天。装完之后跑一个最小验证import cv2 import numpy as np from scipy.spatial import cKDTree # 读图灰度图用 IMREAD_GRAYSCALE参考图用 IMREAD_COLOR gray cv2.imread(target_gray.jpg, cv2.IMREAD_GRAYSCALE) ref cv2.imread(reference.jpg, cv2.IMREAD_COLOR) if gray is None or ref is None: raise FileNotFoundError(检查图片路径中文路径建议先用 cv2.imdecode 处理) # 参考图缩放到和灰度图接近的尺寸太大没必要 ref cv2.resize(ref, (gray.shape[1], gray.shape[0])) # 亮度重映射 KD-Tree 彩色化 ref_remapped luminance_remap(ref, gray) result welsh_colorize_kdtree(gray, ref_remapped, patch_size7) cv2.imwrite(colorized_output.jpg, result) print(完成输出尺寸:, result.shape)这段脚本就是完整的调用链读图 → 尺寸对齐 → 亮度重映射 → KD-Tree 匹配 → 输出。cv2.resize那步很多人会忘参考图和目标图尺寸差太多时邻域统计的尺度对不上颜色会错乱。中文路径的问题在 Windows 上特别常见cv2.imread遇到中文路径直接返回 None用cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)可以绕过。3.2 参考图怎么选决定成败的一步Welsh 算法是色彩传递参考图选错了后面参数怎么调都救不回来。选参考图有三条硬标准标准具体要求反例内容相似参考图和灰度图的主体类别一致用风景图给人物肖像上色光照一致两张图的明暗分布接近用夜景参考图给白天灰度图上色色彩干净参考图本身没有严重偏色或噪点用加了滤镜的网图做参考我一般会准备 3 到 5 张候选参考图分别跑一遍肉眼挑最好的。如果灰度图是人物参考图最好也是同角度、同光照的人物照如果是建筑参考图的天空和墙面颜色要典型。有个取巧的办法如果找不到合适的参考图可以从网上找一张同场景的彩色照片手动裁剪出和灰度图构图接近的区域。3.3 批量处理与结果保存实际项目里往往不是处理一张图而是一批。批量处理要注意内存管理和命名规范。import os import glob def batch_colorize(gray_dir, ref_path, out_dir, patch_size7): 批量彩色化一个目录下的所有灰度图。 os.makedirs(out_dir, exist_okTrue) ref cv2.imread(ref_path, cv2.IMREAD_COLOR) if ref is None: raise FileNotFoundError(f参考图读取失败: {ref_path}) exts (*.jpg, *.jpeg, *.png, *.bmp) files [] for e in exts: files.extend(glob.glob(os.path.join(gray_dir, e))) for f in files: gray cv2.imread(f, cv2.IMREAD_GRAYSCALE) if gray is None: print(f跳过无法读取: {f}) continue ref_resized cv2.resize(ref, (gray.shape[1], gray.shape[0])) ref_remap luminance_remap(ref_resized, gray) out welsh_colorize_kdtree(gray, ref_remap, patch_size) name os.path.splitext(os.path.basename(f))[0] _color.jpg cv2.imwrite(os.path.join(out_dir, name), out) print(f完成: {name}) batch_colorize(./gray_images, ./reference.jpg, ./output, patch_size7)glob那几行是为了兼容不同扩展名实际用的时候按你的图片格式增减。os.makedirs(exist_okTrue)保证输出目录不存在时自动创建。每张图处理完立刻写盘不要全部堆在内存里批量处理几百张图的时候内存会爆。4. 避坑与排查Welsh 彩色化最常见的 5 个翻车现场4.1 输出全灰或颜色极淡现象跑完程序输出图看起来还是灰的只有一点点若有若无的颜色。原因九成是亮度重映射那步把参考图的色度信息也破坏了。如果你在luminance_remap里直接对 BGR 三通道做统计重映射色度通道会被一起缩放颜色就没了。另一个可能是 LAB 转 BGR 时 L 通道超范围被截断。解决确认重映射只作用于 LAB 空间的 L 通道A、B 通道原样保留。检查np.clip(L_new, 0, 255)这行有没有漏。如果还不行把参考图的饱和度手动提一下ref_lab[:, :, 1] * 1.2; ref_lab[:, :, 2] * 1.2再 clip 回 0-255。4.2 颜色块状错乱天空变成绿色现象输出图里出现大块不合理的颜色比如天空被染成绿色人脸变成蓝色。原因匹配特征权重失衡。如果 std 权重给得太高算法会优先匹配纹理而不是亮度导致亮度差很远但纹理相似的像素被错误匹配。另一个常见原因是参考图和灰度图的内容类别差太远。解决先把weights调回[1.0, 0.5, 0.5]这个基准确认结果正常后再微调。如果内容类别差异大换参考图比调参有效得多。还可以加一个亮度差阈值距离计算时如果亮度差超过 30直接把这个候选排除掉。4.3 处理速度慢到无法接受现象一张 1024×1024 的图跑了十几分钟还没出结果。原因用了纯 Python 双重循环版本没有上 KD-Tree。或者图片分辨率太高像素数太多。解决换成welsh_colorize_kdtree。如果还慢先把图缩到 512 宽处理输出后再用cv2.resize放大回去颜色信息在放大时用双三次插值基本能保持。KD-Tree 的构建本身也有开销如果参考图特别大比如 4000×3000先把参考图缩到 800 宽再建树。4.4 边缘出现彩色噪点或光晕现象物体边缘一圈出现不自然的彩色噪点或者颜色溢出到相邻区域。原因patch_size太大邻域统计把边缘两侧的像素混在一起了。或者参考图本身有压缩噪点被匹配过来了。解决把patch_size从 7 降到 5 或 3边缘会干净很多代价是平滑区域可能出现色块。参考图先用cv2.bilateralFilter做一次保边去噪。如果噪点集中在特定区域可以在输出后做一次中值滤波但核不要超过 3否则颜色会糊。4.5 中文路径导致读图失败现象cv2.imread返回 None程序报FileNotFoundError但路径明明是对的。原因OpenCV 的imread在 Windows 上对中文路径支持有问题底层用的编码和系统不一致。解决用np.fromfilecv2.imdecode组合读取写入时用cv2.imencodetofile。def imread_unicode(path, flagscv2.IMREAD_COLOR): 支持中文路径的图片读取。 data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, flags) def imwrite_unicode(path, img): 支持中文路径的图片写入。 ext os.path.splitext(path)[1] ok, buf cv2.imencode(ext, img) if ok: buf.tofile(path) return ok这两个函数替换掉所有cv2.imread和cv2.imwrite调用中文路径问题就彻底解决了。imdecode的flags参数和imread一致灰度图传cv2.IMREAD_GRAYSCALE。5. 进阶技巧多参考图融合与局部色彩校正单张参考图的 Welsh 算法有个天花板参考图里没有的颜色输出图里也不会有。比如参考图是一片秋天的树林那灰度图里的水面永远上不出蓝色。解决办法是准备多张参考图分别匹配后做加权融合。def multi_ref_colorize(gray, ref_list, patch_size7): 多参考图融合彩色化每张参考图贡献一部分颜色。 results [] for ref in ref_list: ref_r cv2.resize(ref, (gray.shape[1], gray.shape[0])) ref_remap luminance_remap(ref_r, gray) out welsh_colorize_kdtree(gray, ref_remap, patch_size) results.append(out.astype(np.float32)) # 简单平均融合也可以按参考图质量加权 fused np.mean(results, axis0) return np.clip(fused, 0, 255).astype(np.uint8)融合的时候有个细节每张参考图单独做亮度重映射不要先把参考图平均再重映射那样会丢失各自的色彩特征。如果某张参考图明显更靠谱给它更高的权重比如weights[0.5, 0.3, 0.2]用np.average(results, axis0, weightsweights)。另一个实用技巧是局部色彩校正。Welsh 算法在全局上表现稳定但局部区域比如人脸经常偏色。可以手动框选几个区域指定期望颜色用简单的色彩偏移修正。def local_color_correct(img, regions): 局部色彩校正。 regions: [(x1, y1, x2, y2, target_bgr), ...] out img.copy().astype(np.float32) for x1, y1, x2, y2, target in regions: patch out[y1:y2, x1:x2] current_mean patch.reshape(-1, 3).mean(axis0) target np.array(target, dtypenp.float32) # 只做偏移不做缩放避免破坏原有色彩层次 offset target - current_mean out[y1:y2, x1:x2] np.clip(patch offset, 0, 255) return out.astype(np.uint8)regions里每个元组指定一个矩形区域和目标颜色。偏移量控制在 ±30 以内太大会出现明显的色块边界。校正之后可以用cv2.bilateralFilter在边界处做一次平滑过渡。验证彩色化效果好不好我一般用两个土办法一是把输出图转成灰度和原灰度图做 SSIM 对比结构相似度应该在 0.95 以上低于这个值说明颜色把结构破坏了二是把输出图缩小到 64×64 再看如果小图里颜色分布合理、没有突兀的色块大图基本就没问题。这两个检查花不了几秒钟但能挡掉大部分低级错误。我自己踩过最深的坑是早期版本忘了做亮度重映射直接拿原参考图匹配结果一批老照片全被染成了参考图的色调偏黄偏绿返工了一整天才定位到问题。后来养成的习惯是任何色彩传递类算法先把亮度对齐这步单独可视化出来看一眼确认没问题再往下走。希望帮到你。本文还有配套的精品资源点击获取