Python图像处理实战:Reinhard与Welsh颜色迁移算法对比及性能优化

何川
何川
发布时间:2026-10-12 | 2人浏览过
原创

本文深入解析Python中两种主流颜色迁移算法:Reinhard与Welsh。通过对比两者在LAB色彩空间下的处理流程,分析Reinhard的高效性与Welsh的涂抹感差异。重点介绍通过去除循环、使用均值滤波近似标准差及Numpy优化查找逻辑,将Welsh算法执行速度提升18倍的实战技巧,并提供完整代码实现与效果对比。

前言

Reinhard算法源自论文《Color Transfer between Images》,由Erik Reinhard提出。

Welsh算法源自论文《Transferring Color to Greyscale Images》,由Tomihisa Welsh提出。

应用场景

主要应用于人像肤色调整以及风景图片的颜色风格迁移。

出发点

  1. RGB三个通道之间存在强关联性,直接同时调整这三个通道以实现自然颜色改变较为困难。
  2. 因此需要寻找一种三个通道互不相关(正交)的颜色空间。作者参考了Ruderman等人提出的Lαβ颜色空间,该空间的三个轴向正交,意味着调整任一通道不会影响其他通道,从而能较好地保持原图的自然观感。这三个通道分别代表:亮度(L)、黄蓝差异(α)和红绿差异(β)。

Reinhard算法流程

  1. 输入变换图像和颜色参考图像,将两者从BGR色彩空间转换为LAB色彩空间。
  2. 分别计算变换图像和参考图像在LAB空间中的均值与方差。
  3. 计算公式为:(变换图像LAB值 - 变换图像均值) / 变换图像方差 * 参考图像方差 + 参考图像均值。
  4. 将处理后的变换图像从LAB空间转换回BGR空间,得到最终输出结果。

Welsh算法流程

  1. 输入变换图像和颜色参考图像,将两者从BGR色彩空间转换为LAB色彩空间。
  2. 定义随机参考点数量segment、邻域大小window_size以及加权系数ratio。从参考图像中随机选取segment个样本点,保存这些样本点的像素亮度值L及其在L空间window_size邻域内的方差σ,计算两者的加权值W,公式为W = L * ratio + σ * (1 - ratio)。由此得到segment个W值,以及与之对应的a通道和b通道数值。
  3. 对变换图像的L通道基于参考图像的L通道进行亮度重映射,以确保后续像素匹配的正确性。
  4. 对变换图像进行逐像素扫描,计算每个像素的权值W(计算方式同上)。在第二步得到的样本点中,找到与当前像素权值最接近的参考点,将该参考点的a通道和b通道值赋给变换图像对应的a通道和b通道。
  5. 将变换后的图像从LAB空间转换回BGR空间。

Reinhard与Welsh算法对比

  1. Reinhard算法操作简单、效率高,处理速度显著快于Welsh算法。
  2. Welsh算法涉及参考图像W值的计算,若场景固定且参考图已知,此步骤可置于初始化阶段;否则,该计算过程较为耗时。
  3. Welsh算法整体速度较慢,主要瓶颈在于方差的计算。
  4. Welsh算法的输出效果受随机参考点数量及位置影响,每次运行结果可能存在差异。
  5. Welsh算法的效果可能呈现涂抹不均匀的感觉,而Reinhard算法则无此问题。

代码实现

Reinhard算法实现

def color_trans_reinhard(in_img, ref_img, in_mask_lists=[None], ref_mask_lists=[None]):
    ref_img_lab = cv2.cvtColor(ref_img, cv2.COLOR_BGR2LAB)
    in_img_lab = cv2.cvtColor(in_img, cv2.COLOR_BGR2LAB)
 
    in_avg = np.ones(in_img.shape, np.float32)
    in_std = np.ones(in_img.shape, np.float32)
    ref_avg = np.ones(in_img.shape, np.float32)
    ref_std = np.ones(in_img.shape, np.float32)
 
    mask_all = np.zeros(in_img.shape, np.float32)
    for in_mask, ref_mask in zip(in_mask_lists, ref_mask_lists):
        #mask,取值为 0, 255, shape[height,width]
        in_avg_tmp, in_std_tmp = cv2.meanStdDev(in_img_lab, mask=in_mask)
        np.copyto(in_avg, in_avg_tmp.reshape(1,1,-1), where=np.expand_dims(in_mask,2)!=0) #numpy.copyto(destination, source)
        np.copyto(in_std, in_std_tmp.reshape(1,1,-1), where=np.expand_dims(in_mask,2)!=0) 
 
        ref_avg_tmp, ref_std_tmp = cv2.meanStdDev(ref_img_lab, mask=ref_mask)
        np.copyto(ref_avg, ref_avg_tmp.reshape(1,1,-1), where=np.expand_dims(in_mask,2)!=0) #numpy.copyto(destination, source)
        np.copyto(ref_std, ref_std_tmp.reshape(1,1,-1), where=np.expand_dims(in_mask,2)!=0) 
 
        #mask
        mask_all[in_mask!=0] = 1
 
    in_std[in_std==0] =1 #避免除数为0的情况
    transfered_lab = (in_img_lab - in_avg)/(in_std) *ref_std + ref_avg 
    transfered_lab[transfered_lab<0] = 0
    transfered_lab[transfered_lab>255] = 255
 
    out_img = cv2.cvtColor(transfered_lab.astype(np.uint8), cv2.COLOR_LAB2BGR)
 
    if in_mask_lists[0] is not None and ref_mask_lists[0] is not None:
        np.copyto(out_img, in_img, where=mask_all==0) 
        
    return out_img
 
 
"""
#img1 = cv2.imread("imgs/1.png")
#img2 = cv2.imread("imgs/2.png")
#img1 = cv2.imread("welsh22/1.png", 1)
#img2 = cv2.imread("welsh22/2.png", 1)
img1 = cv2.imread("welsh22/gray.jpg", 1)
img2 = cv2.imread("welsh22/consult.jpg", 1)
cv2.imwrite("out.jpg", color_trans_reinhard(img1, img2, [np.ones(img1.shape[:-1],np.uint8)*255], [np.ones(img2.shape[:-1],np.uint8)*255]))
"""
img1 = cv2.imread("ab.jpeg")
img2 = cv2.imread("hsy.jpeg")
mask1 = cv2.imread("ab_parsing.jpg", 0)
mask1[mask1<128]=0
mask1[mask1>=128]=255
mask2 = cv2.imread("hsy_parsing.jpg", 0)
mask2[mask2<128]=0
mask2[mask2>=128]=255
cv2.imwrite("out.jpg", color_trans_reinhard(img1, img2, [mask1], [mask2]))

Welsh算法实现

改进点

  1. 核心改进在于去除Python层面的for循环操作。
  2. 将计算邻域标准差(std)的操作,使用均值滤波结合Numpy实现近似替换,目测效果无明显差异。
  3. 修改参考图的权重(weight)处理,全部转换为整数类型,仅保留不重复的权重值,实际测试表明约150个权重即可满足需求。
  4. 修改最近权重查找逻辑,使用Numpy减法操作结合argmin函数,替代二分查找。
  5. 经过上述优化,整体运行速度比原始代码提升约18倍。
def get_domain_std(img_l, pixel, height, width, window_size):
    window_left = max(pixel[1] - window_size, 0)
    window_right = min(pixel[1] + window_size + 1, width)
    window_top = max(pixel[0] - window_size, 0)
    window_bottom = min(pixel[0] + window_size + 1, height)
 
    window_slice = img_l[window_top: window_bottom, window_left: window_right]
 
    return np.std(window_slice)
 
 
def get_weight_pixel(ref_img_l, ref_img_a, ref_img_b, ref_img_height, ref_img_width, segment, window_size, ratio, ref_mask_lists=[None]):
    weight_list = []
    pixel_a_list = []
    pixel_b_list = []
 
    ref_img_mask  = np.ones((ref_img_height, ref_img_width), np.uint8)
    if ref_mask_lists[0] is not None:
        for x in ref_mask_lists:
            ref_img_mask = np.bitwise_or(x, ref_img_mask)
 
    ref_img_l_mean = cv2.blur(ref_img_l, (window_size, window_size))
    ref_img_l_std = np.sqrt(cv2.blur(np.power((ref_img_l - ref_img_l_mean), 2),  (window_size, window_size)))
    for _ in range(segment):
        height_index = np.random.randint(ref_img_height)
        width_index = np.random.randint(ref_img_width)
 
            
        pixel = [height_index, width_index]  #[x,y]
 
        if ref_img_mask[pixel[0], pixel[1]] == 0:
            continue
 
        pixel_light = ref_img_l[pixel[0], pixel[1]]
        pixel_a = ref_img_a[pixel[0], pixel[1]]
        pixel_b = ref_img_b[pixel[0], pixel[1]]
 
        #pixel_std = get_domain_std(ref_img_l, pixel, ref_img_height, ref_img_width, window_size)
        pixel_std = ref_img_l_std[height_index, width_index]
 
        weight_value = int(pixel_light * ratio + pixel_std * (1 - ratio))
        if weight_value not in weight_list:
            weight_list.append(weight_value)
            pixel_a_list.append(pixel_a)
            pixel_b_list.append(pixel_b)                          
 
    return np.array(weight_list), np.array(pixel_a_list), np.array(pixel_b_list)
 
 
 
def color_trans_welsh(in_img, ref_img, in_mask_lists=[None], ref_mask_lists=[None]):
    start = time.time()
    #参考图
    ref_img_height, ref_img_width, ref_img_channel = ref_img.shape
    window_size=5 #窗口大小
    segment= 10000#随机点个数
    ratio=0.5     #求weight的比例系数
 
    ref_img_lab = cv2.cvtColor(ref_img, cv2.COLOR_BGR2Lab)
    ref_img_l, ref_img_a, ref_img_b = cv2.split(ref_img_lab)
 
    #计算参考图weight
    ref_img_weight_array, ref_img_pixel_a_array, ref_img_pixel_b_array =  get_weight_pixel(ref_img_l, ref_img_a, ref_img_b, ref_img_height, ref_img_width, segment, window_size, ratio, ref_mask_lists)
 
    ref_img_max_pixel, ref_img_min_pixel = np.max(ref_img_l), np.min(ref_img_l)
 
 
    #输入图
    in_img_height, in_img_width, in_img_channel = in_img.shape
    in_img_lab = cv2.cvtColor(in_img, cv2.COLOR_BGR2LAB)
 
    # 获取灰度图像的亮度信息;
    in_img_l, in_img_a, in_img_b = cv2.split(in_img_lab)
 
    in_img_max_pixel, in_img_min_pixel = np.max(in_img_l), np.min(in_img_l)
    pixel_ratio = (ref_img_max_pixel - ref_img_min_pixel) / (in_img_max_pixel - in_img_min_pixel)
 
    # 把输入图像的亮度值映射到参考图像范围内;
    in_img_l = ref_img_min_pixel + (in_img_l - in_img_min_pixel) * pixel_ratio
    in_img_l = in_img_l.astype(np.uint8)
 
 
    in_img_l_mean = cv2.blur(in_img_l, (window_size, window_size))
    in_img_l_std = np.sqrt(cv2.blur(np.power((in_img_l - in_img_l_mean), 2),  (window_size, window_size)))
 
 
    in_img_weight_pixel = ratio * in_img_l + (1 - ratio) * in_img_l_std
 
    nearest_pixel_index = np.argmin(np.abs(ref_img_weight_array.reshape(1,1,-1) - np.expand_dims(in_img_weight_pixel, 2)), axis=2).astype(np.float32)
 
    in_img_a = cv2.remap(ref_img_pixel_a_array.reshape(1, -1), nearest_pixel_index, np.zeros_like(nearest_pixel_index, np.float32), interpolation=cv2.INTER_LINEAR)
    in_img_b = cv2.remap(ref_img_pixel_b_array.reshape(1, -1), nearest_pixel_index, np.zeros_like(nearest_pixel_index, np.float32), interpolation=cv2.INTER_LINEAR)
 
 
    merge_img = cv2.merge([in_img_l, in_img_a, in_img_b])
    bgr_img = cv2.cvtColor(merge_img, cv2.COLOR_LAB2BGR)
 
    
    mask_all = np.zeros(in_img.shape[:-1], np.int32)
    if in_mask_lists[0] is not None and ref_mask_lists[0] is not None:
        for x in in_mask_lists:
            mask_all = np.bitwise_or(x, mask_all)
        mask_all = cv2.merge([mask_all, mask_all, mask_all])
        np.copyto(bgr_img, in_img, where=mask_all==0) 
    
    end = time.time()
    print("time", end-start)
    return bgr_img
 
 
 
if __name__ == '__main__':
 
    # 创建参考图像的分析类;
    #ref_img = cv2.imread("consult.jpg")
    #ref_img = cv2.imread("2.png")
    ref_img = cv2.imread("../imgs/2.png")
 
    # 读取灰度图像;opencv默认读取的是3通道的,不需要我们扩展通道;
    #in_img = cv2.imread("gray.jpg")
    #in_img = cv2.imread("1.png")
    in_img = cv2.imread("../imgs/1.png")
 
    bgr_img = color_trans_welsh(in_img, ref_img)
    cv2.imwrite("out_ren.jpg", bgr_img)
    """
    ref_img = cv2.imread("../hsy.jpeg")
    ref_mask = cv2.imread("../hsy_parsing.jpg", 0)
    ref_mask[ref_mask<128] = 0
    ref_mask[ref_mask>=128] = 255
    in_img = cv2.imread("../ab.jpeg")
    in_mask = cv2.imread("../ab_parsing.jpg", 0)
    in_mask[in_mask<128] = 0
    in_mask[in_mask>=128] = 255
    bgr_img = color_trans_welsh(in_img, ref_img, in_mask_lists=[in_mask], ref_mask_lists=[ref_mask])
    cv2.imwrite("bgr.jpg", bgr_img)
    """

效果对比

从左至右依次为:原图、参考图、Reinhard算法处理效果、Welsh算法处理效果。

从左至右依次为:原图、原图皮肤掩膜、参考图、参考图皮肤掩膜、Reinhard算法处理效果、Welsh算法处理效果。

以上为Python图像处理中颜色迁移(Reinhard与Welsh对比)的详细内容,更多关于Python颜色迁移的资料请关注本站其他相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表