简介:这套基于Welsh算法的灰度图像彩色化处理及优化Python实现,包含完整源码、项目说明与详细注释,面向计算机相关专业学生、教师或开发者,适用于毕业设计、课程设计、课题初期演示及算法学习进阶。压缩包共16个文件,含4个Python源码、3个pyc编译文件、8张彩色/灰度/处理效果图像和1份项目说明文档,整体仅533KB,结构清晰便于查阅;其中Python源码均带详细注释,项目说明文档对算法原理和实验流程作了梳理。从颜色统计匹配到边缘保持滤波,代码覆盖Welsh颜色转移彩色化、导向滤波去噪优化以及图像自然感处理等核心环节,所有代码均测试运行成功,可在此基础上修改扩展,帮助深入理解灰度图像彩色化中的颜色匹配、边缘保持与噪声抑制思路。目前已有432人浏览学习,适合作为计算机视觉方向入门实践或实际项目借鉴参考。
1. 灰度图上色不是加滤镜:Welsh算法解决的是“猜颜色分布”
上周帮朋友处理一张黑白全家福,扫描件脸部噪点很重。我第一反应是丢给深度学习上色模型跑一版,结果同一个输入跑了三次出了三种肤色,其中一次把窗帘染成荧光绿。换回Welsh算法后,一张参考彩色图加一套近邻匹配,十分钟出稳定结果,同一个输入永远同一个输出。很多人觉得“黑白上色不重要”,但对老照片修复、老电影着色、界面原型快速出彩图这类场景来说,稳定、可复现、不依赖训练权重的方案反而比“抽卡式”的深度学习上色更实用。Welsh算法的核心思路很朴素:灰度图里每个像素只有亮度可用,那就把目标像素邻域的亮度统计特征,拿到参考彩色图里做最近邻匹配,再把参考像素的色度通道借过来。它不需要GPU、不需要数据集,适合想快速落地一个可靠彩色化工具的开发者。
2. 先看懂Welsh再写代码:亮度重映射与邻域匹配是根
2.1 颜色迁移的两条路线:为什么Welsh选择“局域匹配”
灰度图彩色化在图像处理里属于颜色迁移(color transfer)问题。这个方向有两条经典路线,一条是全局统计迁移,另一条就是Welsh代表的局域匹配迁移。
全局统计迁移的思路是:把目标图和参考图都转到某个颜色空间,分别计算每个通道的均值和标准差,然后把目标图的均值和标准差强行掰成和参考图一致。这条路线实现极其简单,几十行就能跑通,处理偏色校正(比如把一张发黄的照片调回正常色调)非常有效。但放到灰度图上色场景,它会立刻暴露问题:一张灰度图里可能同时有人脸、天空、草地、衣服,全局统计迁移会把参考图的整体色调强加给所有区域,结果就是天空带上了草地的绿,人脸带上了天空的蓝,整张图颜色发闷、粘成一团。原因在于全局统计只描述“整张图”的颜色分布,丢失了空间位置和局部结构信息。
Welsh走的则是另一条路:局域匹配。它假设“亮度结构相似的地方,颜色也应该相似”。算法逐个像素(或采样点)分析邻域内的亮度均值和标准差,在参考图里找到结构最相似的像素,把那个像素的α、β色度通道搬过来。正因为匹配发生在局部,天空的平滑区域会去参考图里找平滑区域,草地的纹理区域会去参考图里找纹理区域,空间对应关系被保留下来。这就是Welsh在处理多区域复杂灰度图时明显优于全局统计迁移的原因。
两条路线的对比如下表:
| 对比项 | 全局统计迁移 | Welsh局域匹配 |
|---|---|---|
| 匹配粒度 | 整张图一个统计量 | 每个像素/采样点独立匹配 |
| 适合场景 | 偏色校正、单色调图 | 灰度图上色、多区域复杂场景 |
| 空间一致性 | 无,整体色调统一 | 局部保持,边缘相对清晰 |
| 计算成本 | 低,O(HW) | 高,O(HW×N),需要优化 |
| 输出稳定性 | 稳定 | 稳定(采样固定时) |
2.2 Welsh主流程五步与三个关键参数
Welsh算法的落地流程可以分为五步,理解了这五步,后面的代码就是按顺序填空。
第一步,把参考彩色图和灰度图都从RGB转到lαβ颜色空间。为什么要用lαβ而不是RGB?因为lαβ的l通道代表亮度,α和β代表色度,三个通道的相关性被大幅降低。在灰度图里我们只有l,匹配时只需要比较亮度相关特征,最后再把参考像素的α、β注入目标像素。这个颜色空间是经典论文里反复验证过适合做颜色迁移的。
第二步,亮度重映射。把灰度图的亮度l的均值和标准差,线性映射到和参考图一致。这一步不是必须的,但强烈建议保留。原因很实在:最近邻匹配对特征尺度非常敏感,如果两幅图的亮度分布差异过大,匹配出来的参考像素结构上根本不相似,颜色自然离谱。
第三步,计算邻域统计特征。对每个像素,在窗口内计算邻域亮度的均值和标准差。窗口内的均值描述了局部亮度水平,标准差描述了局部纹理强度。平滑的天空区域标准差小,纹理丰富的草地标准差大,这两个特征足以区分绝大多数结构。
第四步,最近邻匹配。把目标像素的邻域特征拿到参考图的采样点集合里找距离最近的参考像素,记录它的α、β值。这一步是整算法的核心,也是性能瓶颈。
第五步,写回色度并转回RGB。用匹配到的α、β替换目标像素的α、β,l通道仍然用原灰度图的值,再逆变换回RGB输出。
这里有三个关键参数,直接决定最终效果:
第一个是邻域窗口大小win。win=3时噪声大,win=15时颜色溢出严重,常见取值是5到9。窗口太小,标准差特征不稳定,平滑区域容易匹配到随机位置,产生椒盐噪点;窗口太大,边缘两侧的结构会被混在一个窗口里,导致颜色越过边界。我一般先用5x5起步,观察平坦区域是否有噪点再逐步调大。
第二个是采样点数量sample_num。原论文的加速思路是:不必让每个目标像素在全图所有参考像素里搜索,而是先从参考图中随机采样一批像素,只在这批像素里做最近邻。采样点过少(比如50个),候选颜色不够,输出会出现明显色块感;采样点过多,计算量线性上升。400到1000是一个比较稳的区间。
第三个是特征权重w。标准Welsh用邻域均值和邻域标准差两个特征,默认权重可以给(1.0, 0.5)。亮度均值权重偏高,是因为亮度水平是判断“天空vs草地”这类大类别的最强信号;标准差权重用来修正纹理差异,不需要给太高,给高了会造成平坦区域和纹理区域之间匹配过于敏感。
3. 从零实现Welsh算法:核心匹配代码与参数设置
3.1 颜色空间转换与亮度重映射:先搭好lαβ通道
我把灰度图约定为三通道灰度RGB(每个通道数值相同),参考图为普通彩色RGB。第一步先把两者统一转到lαβ空间。这里直接给出论文里的变换矩阵,RGB取值归一化到0~1。
import numpy as np import cv2 _RGB2LMS = np.array([ [0.3811, 0.5783, 0.0402], [0.1967, 0.7244, 0.0782], [0.0241, 0.1288, 0.8444], ]) _LMS2LAB = np.array([ [1/np.sqrt(3), 1/np.sqrt(3), 1/np.sqrt(3)], [1/np.sqrt(6), 1/np.sqrt(6), -2/np.sqrt(6)], [1/np.sqrt(2), -1/np.sqrt(2), 0], ]) def rgb_to_lab(rgb): # rgb: float32, 取值 [0,1], 形状 HxWx3 lms = rgb @ _RGB2LMS.T lms = np.log10(np.maximum(lms, 1e-8)) lab = lms @ _LMS2LAB.T return lab def lab_to_rgb(lab): # 逆变换:lαβ -> LMS -> RGB,结果裁剪到 [0,1] lms = lab @ np.linalg.inv(_LMS2LAB).T lms = np.power(10.0, lms) rgb = lms @ np.linalg.inv(_RGB2LMS).T return np.clip(rgb, 0, 1)这段代码里有两个细节要留意。首先是np.maximum(lms, 1e-8),LMS通道经过矩阵乘法后可能产生接近零甚至负的数值,直接取对数会得到-inf或nan,所以需要先做下限保护。其次是逆变换用了np.linalg.inv去求两个变换矩阵的逆,避免我手写逆变换公式出错,代价是每次调用多一点点矩阵运算,对单张图像完全可接受。
亮度重映射的代码更短,但它是容易被跳过的一步。
def remap_luminance(src_l, ref_l): # 把src的亮度均值/方差映射到和ref一致 sm, ss = src_l.mean(), src_l.std() rm, rs = ref_l.mean(), ref_l.std() return (src_l - sm) / (ss + 1e-8) * rs + rm这个线性映射的直觉是:参考图的亮度分布决定了它有哪些亮度层次,目标图的亮度如果整体偏暗,匹配时所有像素都会去找参考图里暗部的颜色,亮部颜色全部浪费。映射之后,两幅图的亮度均值相同、方差相同,匹配的候选分布更均匀。1e-8是为了防除零。这里注意,重映射改变的是目标图的l通道,在最后输出时,这个被修改过的l仍然会被保留——Welsh算法允许对亮度做调整,因为亮度本来就是原图的信息,只是做了统计对齐。
3.2 邻域特征提取与最近邻匹配:核心代码与参数含义
特征提取我用OpenCV的盒式滤波来实现,比用双层for循环快两个数量级,而且代码更短。
def compute_stat_features(l_channel, win=5): # l_channel: HxW float32,这里是lαβ空间的亮度通道 k = np.ones((win, win), dtype=np.float32) / (win * win) mean = cv2.filter2D(l_channel, -1, k, borderType=cv2.BORDER_REFLECT) sq = cv2.filter2D(l_channel * l_channel, -1, k, borderType=cv2.BORDER_REFLECT) std = np.sqrt(np.maximum(sq - mean * mean, 0)) return np.stack([mean, std], axis=-1)盒式滤波计算邻域均值的原理是卷积,每个像素的输出是周围win×win区域的加权平均。标准差用“窗口内平方的均值减去均值的平方”来算,这是E[X²]-E[X]²的恒等变形,能一次滤波搞定,不用先求均值再逐像素减。BORDER_REFLECT表示边缘用镜像填充,比默认的补零更稳——补零会让图像边缘窗口内混入大量零值,导致边缘像素的均值和标准差被低估,上色后四周一圈颜色明显发暗。
最近邻匹配是全文最核心的代码块。第一步先做参考图采样:
def sample_ref(ref_feat, ref_ab, sample_num, seed=42): # 随机采样参考图中的像素,返回采样后的特征和色度 h, w = ref_feat.shape[:2] rng = np.random.default_rng(seed) idx = rng.choice(h * w, size=sample_num, replace=False) return ref_feat.reshape(-1, 2)[idx], ref_ab.reshape(-1, 2)[idx]seed固定很重要。Welsh算法本身是确定性算法,但随机采样这一步引入了随机性。如果seed不固定,同一张图每次跑出来的颜色会有细微差异。固定seed之后,输出完全可复现,排障时才能对比不同参数的影响。
然后是朴素的最近邻匹配:
def nearest_match_naive(target_feat, ref_feat_s, ref_ab_s, w=(1.0, 0.5)): # target_feat: HxWx2, ref_feat_s: Nx2, ref_ab_s: Nx2 h, w = target_feat.shape[:2] out = np.zeros((h, w, 2), dtype=np.float32) block = 64 for y in range(0, h, block): for x in range(0, w, block): tf = target_feat[y:y+block, x:x+block] d = (w[0] * (tf[..., None, 0] - ref_feat_s[None, None, :, 0]) ** 2 + w[1] * (tf[..., None, 1] - ref_feat_s[None, None, :, 1]) ** 2) best = np.argmin(d, axis=-1) out[y:y+block, x:x+block] = ref_ab_s[best] return out这里距离计算用的是特征差平方加权和,等价于加权欧氏距离。块大小block=64,是为了控制中间张量的规模:每个块产生的距离矩阵形状是64×64×N,N取500时约2000万个浮点数,内存占用几十MB,可以接受。如果直接对整个H×W图像做广播距离计算,一张800×600的图会产生800×600×500的中间矩阵,约2.4亿个浮点数,内存直接爆掉。这是初写Welsh最容易翻车的地方,后面优化章节会彻底解决。
3.3 把流程串成主函数:从灰度图到彩色图的完整闭合
有了前面的转换、重映射、特征提取、匹配四个函数,主函数就是把它们按顺序串起来。
def colorize(gray_rgb, ref_rgb, win=5, sample_num=400, w=(1.0, 0.5), remap=True): # gray_rgb: 三通道灰度图,uint8;ref_rgb: 参考彩色图,uint8 target_lab = rgb_to_lab(gray_rgb.astype(np.float32) / 255.0) ref_lab = rgb_to_lab(ref_rgb.astype(np.float32) / 255.0) if remap: target_lab[..., 0] = remap_luminance(target_lab[..., 0], ref_lab[..., 0]) t_feat = compute_stat_features(target_lab[..., 0], win) r_feat = compute_stat_features(ref_lab[..., 0], win) ref_ab = ref_lab[..., 1:] # 取α、β通道 ref_f_s, ref_ab_s = sample_ref(r_feat, ref_ab, sample_num) ab = nearest_match_naive(t_feat, ref_f_s, ref_ab_s, w) target_lab[..., 1:] = ab out = lab_to_rgb(target_lab) return (out * 255).astype(np.uint8)调用示例:
gray = cv2.imread('gray_photo.jpg', cv2.IMREAD_GRAYSCALE) gray_rgb = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB) ref = cv2.imread('reference.jpg') out = colorize(gray_rgb, ref, win=5, sample_num=400) cv2.imwrite('result.jpg', out)主函数里有几个易错点。第一,rgb_to_lab内部要求输入是0到1的浮点数,uint8图像必须除以255,否则矩阵乘法结果量级不对,输出整个偏色。第二,ref_ab取的是参考图色度,目标图的α、β被赋值覆盖,但l通道保持重映射后的值不变,不能把参考图的l通道也搬过来。第三,lab_to_rgb返回的已经是0到1的浮点RGB,最后要乘255再转uint8。到这一步,一个可运行的Welsh彩色化流程就完整了。接下来要解决性能问题:这张800×600的图在sample_num=400时,朴素的最近邻匹配大约需要几秒到十几秒,图像越大越慢,优化的空间非常明显。
4. Welsh彩色化的三条优化路线:随机采样、多尺度与KDTree加速
4.1 优化1:亮度分层采样,把随机采样从“碰运气”变成“按需取色”
朴素的随机采样在参考图像素分布极不均匀时有个隐患:如果参考图里暗部占80%、亮部占20%,随机采400个点,亮部可能只分到80个候选点,结果目标图里原本该匹配亮色的区域,因为候选不够,最后都匹配到了附近的暗部颜色,整体发闷。
解决方法是亮度分层采样。先把参考图所有像素按亮度分成K个桶,每个桶内固定采N_K个点,保证每个亮度层次都有足够候选。
def stratified_sample(l_channel, ref_feat, ref_ab, sample_num, bins=8, seed=0): # 按亮度直方图分层采样,每个亮度桶采样 sample_num//bins 个点 h, w = l_channel.shape[:2] flat_l = l_channel.reshape(-1) flat_feat = ref_feat.reshape(-1, 2) flat_ab = ref_ab.reshape(-1, 2) rng = np.random.default_rng(seed) selected = [] edges = np.percentile(flat_l, np.linspace(0, 100, bins + 1)) for i in range(bins): mask = (flat_l >= edges[i]) & (flat_l <= edges[i + 1]) idx = np.where(mask)[0] if len(idx) == 0: continue n = max(1, sample_num // bins) pick = rng.choice(idx, size=min(n, len(idx)), replace=False) selected.append(pick) sel = np.concatenate(selected) return flat_feat[sel], flat_ab[sel]分桶用np.percentile算亮度边界,保证每个桶的像素数量大致相等,而不是在亮度值上等距切分。这样即便参考图整体偏暗,亮部桶也能分到足够的候选。参数上,bins取8到12比较合适,太小不起作用,太大每个桶候选过少。这个采样的计算量比全图扫描小得多,但比纯随机采样多了一次分桶和掩码选择,对总耗时影响可以忽略。
4.2 优化2:用KDTree把匹配从线性扫描变成对数查询
朴素匹配的时间复杂度是O(HW×N),N是采样点数量。图像越大,N越大,耗时线性增长。KDTree可以把每次查询的复杂度降到O(log N),整张图降到O(HW log N)。这正是Welsh算法在工程落地里最值得做的一步优化。
from scipy.spatial import cKDTree def nearest_match_kdtree(target_feat, ref_feat_s, ref_ab_s, w=(1.0, 0.5)): # 把特征先乘权重,这样KDTree里的欧氏距离就等于加权距离 w_arr = np.array(w, dtype=np.float32) ref_scaled = ref_feat_s * w_arr # 建立KDTree,query时返回最近邻的索引 tree = cKDTree(ref_scaled) h, w_img = target_feat.shape[:2] t_scaled = target_feat.reshape(-1, 2) * w_arr _, best_idx = tree.query(t_scaled, k=1) return ref_ab_s[best_idx].reshape(h, w_img, 2)这里有个容易忽略的细节:加权距离不能直接丢给KDTree,因为cKDTree内部算的是标准欧氏距离。正确的做法是先把特征乘上权重,让加权距离等价于权重空间里的欧氏距离。注意,如果特征各维度的尺度差异很大,KDTree的构建和查询效率会下降,但这里只有两维,完全不用担心。替换掉nearest_match_naive后,同样参数下耗时通常能从十几秒降到一两秒,图像越大收益越明显。
4.3 优化3:多尺度由粗到细匹配,消除平坦区域的匹配跳变
朴素匹配和KDTree匹配都是逐像素独立寻优,存在一个视觉上的隐患:在天空、墙面这类大范围平坦区域,很多像素的特征非常接近,最近邻结果可能在不同参考位置之间跳变。反映到输出上就是同一片天空出现几块深浅不一的颜色过渡。多尺度匹配是解决这类空间不一致问题的常用手法。
思路是:先在缩小后的图像上做匹配,得到一个“参考坐标”的粗估计,然后把坐标图上采样回原分辨率,只在参考坐标附近的小邻域里重新搜索。
def colorize_multiscale(gray_rgb, ref_rgb, win=5, sample_num=600): # 由粗到细的匹配,核心是记录参考坐标而不是直接记录颜色 def match_with_guide(scale): g = cv2.resize(gray_rgb, None, fx=scale, fy=scale, interpolation=cv2.INTER_AREA) r = cv2.resize(ref_rgb, None, fx=scale, fy=scale, interpolation=cv2.INTER_AREA) # 正常计算 lαβ、特征、采样、KDTree匹配 # 得到匹配结果后,把每个目标像素命中的参考像素坐标存成coord_map return coord_map, g coord, _ = match_with_guide(0.25) for scale in [0.5, 1.0]: coord = cv2.resize(coord, None, fx=2, fy=2, interpolation=cv2.INTER_LINEAR) # 在coord附近半径r=5的窗口内搜索最优匹配,更新coord # 最后一层根据最终coord从参考图取色 return out_rgb这个方案比直接对原图跑一次匹配多耗一半时间,但平坦区域的色块跳变明显减少。实践中的参数经验是:初始尺度取0.25,每往上一层半径从8像素减到4像素再减到2像素。注意,多尺度匹配里记录的是“匹配到的参考坐标”而不是参考像素的α、β值,因为只有坐标才能在尺度间传递和插值。如果你只想让流程简单,可以先跳过这一节,先跑通4.1和4.2,等观察到明显的颜色跳变再回来加这一层。
5. 避坑指南:Welsh彩色化的5个翻车现场
5.1 参考图亮度结构与目标图差距太大:整体偏色、发灰
现象:输出图所有颜色都灰蒙蒙的,饱和度很低,像是给灰度图蒙了一层彩色的纱。
原因:亮度重映射只对齐了均值和标准差,但两幅图的亮度直方图形状可能完全不同。比如目标图是逆光人像,暗部像素极多,参考图是明亮风景,亮部像素极多。匹配时目标图的暗部像素找不到结构相似的参考候选,只能勉强匹配到最近的暗部颜色,颜色自然发闷。
解决:先做亮度直方图匹配再进Welsh,或者直接换一张与目标图亮度分布接近的参考图。直方图匹配可以用cv2.calcHist配合cv2.LUT实现,把目标图l通道的直方图映射到参考图l通道的累积分布上,效果比重映射更精细。如果直方图匹配后仍不理想,还有一个笨办法:把参考图也转成灰度,观察它的灰度图和目标图的灰度图在视觉上是否接近,如果明显一暗一亮,先对参考图做亮度调整。
5.2 邻域窗口设错:要么椒盐噪点,要么颜色糊成一片
现象:win=3时,平滑区域出现密密麻麻的小色斑,像是彩色噪点;win=15时,人物衣服和背景墙的颜色互相渗透,边缘模糊。
原因:窗口太小,标准差特征在平坦区域极度不稳定,几个像素的微小灰度波动就会显著改变标准差,导致匹配坐标随机跳动;窗口太大,窗口内混入了边缘两侧的不同结构,特征失去了区分能力。
解决:从win=5开始跑,如果输出有椒盐感,逐步加到7或9。经验上5x5适合人像和中等纹理图像,7x7适合大块天空、水面这类平滑区域较多的图像,9x9以上一般只在超大画幅降噪时用。注意,win增大时计算量也会增大,因为滤波核变大,但开销远小于匹配阶段,不用太担心。
5.3 αβ通道写回后越界:颜色断层与暗部发黑
现象:输出图的暗部区域出现明显的色带断裂,颜色从深蓝直接跳到黑色,过渡极不自然。
原因:lαβ空间逆变换回RGB后,部分像素的RGB值超出0到1范围,简单的np.clip会把超出部分截断。大面积截断会让暗部细节同时丢亮度又丢色度。
解决:在lab_to_rgb的clip之前,先做百分位截断。取RGB三通道各自1%和99%分位点,把低于1%的分位点的值压到1%分位点,高于99%的压到99%分位点,再clip到0~1。这样既避免了极端离群点带来的整体偏色,又不破坏正常灰度梯度。如果用了百分位截断后暗部仍然发黑,检查一下输入灰度图的暗部是否有大量纯黑像素,这类像素在lαβ空间里log(0)会被保护成很小但非零的数,逆变换回来时会偏离原位置,这是颜色空间的固有问题,建议对输入灰度图先做一次轻微的对比度拉伸。
5.4 采样点数量被调太低:色块感严重
现象:整张图像被分割成一块块颜色不同的区域,同一片区域的内部颜色不连续,像是低精度量化效果。
原因:sample_num太小,参考候选点太少,相邻目标像素经常会匹配到同一个或零散的几个参考点,颜色在空间上无法连续过渡。
解决:把sample_num提到600以上再看效果。如果你的参考图内容本身比较单一(比如只有天空和人脸),400可能够用;参考图内容越丰富,需要的采样点越多。我之前踩过一次,为了追求速度把采样点压到100,整张脸成了三块色斑,后来分层采样加800个点,问题立刻消失。这里不用盲目追求1000以上,采样点到一定数量后收益递减,还会拖慢KDTree构建。
5.5 只拿亮度做匹配,不看邻域统计:纹理错配
现象:水面波纹区域被匹配上了草地的颜色,砖墙区域被匹配上了树叶的颜色,输出图局部颜色和原物体完全对不上。
原因:如果特征只包含当前像素的亮度l,匹配对“亮度相同但纹理完全不同”的区域毫无分辨力。灰度图里水波纹和草地叶片可能亮度接近,但纹理差异巨大;邻域标准差正是用来区分这两类结构的。
解决:确保特征向量里包含邻域标准差,并把标准差权重w[1]从0.5适当提高到0.8左右。代价是匹配会更挑剔,如果参考图和目标图的结构相似度不够高,匹配失败率反而上升。另一个补充手段是在特征里加入灰度梯度方向直方图,但特征维度从2维变成更高维后,KDTree的查询效率会下降,除非对纹理匹配质量有硬性要求,否则我一般不动。
6. 把Welsh结果变好用:回环验证与参数微调技巧
灰度图上色一个天然的难点是:它没有唯一正确的真值。一张黑白人脸,肤色可以偏白也可以偏黄,谈不上谁对谁错。所以调参时不能直接用“像不像原图”来评判,我习惯用回环验证法:先取一张彩色参考图,把它转成灰度,再拿这张灰度图和原彩色图分别作为目标和参考跑Welsh,最后与原图算PSNR、SSIM和色差。因为已知原图的颜色分布,这一步能客观评估算法本身是否把结构带歪了。
color_img = cv2.imread('ground_truth.jpg') gray = cv2.cvtColor(color_img, cv2.COLOR_RGB2GRAY) gray_rgb = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB) out = colorize(gray_rgb, color_img, win=5, sample_num=600) psnr = cv2.PSNR(color_img, out) ssim_val = ssim(color_img, out, channel_axis=2) print(f"PSNR={psnr:.2f} dB, SSIM={ssim_val:.4f}")注意指标的含义边界:PSNR和SSIM只衡量“结构还原度”,不能代表“语义正确性”。真实黑白照片上色没有标准答案,这两个指标只用来在参数迭代时判断改动是变好还是变坏,而不是追求一个绝对高分。
参数微调顺序上,我习惯先固定sample_num=600、w=(1.0, 0.5),只调win。用win=5、7、9三档各跑一次回环验证,选PSNR最高或视觉上色块最少的那档。win定了之后再调sample_num,从400到1000每次翻倍,观察平坦区域是否还有跳变。最后才动特征权重,通常只在纹理错配严重时把w[1]调到0.8。
一个小技巧:如果参考图里包含多个亮度层次差异巨大的区域,可以在strata_sample里把bins从8提高到12。这样暗部、中间调、亮部各分到更多候选。但bins过高会让每个桶的候选过少,反而失去意义。我自己最早调参时把采样压到100还怪算法不行,后来发现问题在候选数量不够,用800点加分层采样后效果天差地别。遇到结果不理想,先检查候选量和窗口,再怀疑算法本身。希望帮到你。
本文还有配套的精品资源,点击获取