与 Pillow 的频道组合

Channel mix with Pillow

我想做一些颜色变换,例如给定 RGB 通道

R =  G + B / 2

或根据同一像素的其他通道的值计算通道值的其他一些转换。

好像.point()函数只能对一个通道进行操作。有没有办法做我想做的事?

对于这个特定的操作,颜色变换可以写成矩阵乘法,因此您可以使用自定义矩阵的 convert() 方法(假设没有 alpha 通道):

# img must be in RGB mode (not RGBA):
transformed_img = img.convert('RGB', (
    0, 1, .5, 0,
    0, 1, 0, 0,
    0, 0, 1, 0,
))

否则,您可以 split() the image into 3 or 4 images of each color band, apply whatever operation you like, and finally merge() 将这些波段恢复为单个图像。同样,原图应该是RGB或者RGBA模式。

(red, green, blue, *rest) = img.split()
half_blue = PIL.ImageChops.multiply(blue, PIL.ImageChops.constant(blue, 128))
new_red = PIL.ImageChops.add(green, half_blue)
transformed_img = PIL.Image.merge(img.mode, (new_red, green, blue, *rest))

使用 PIL.ImageChops 的替代方法是将图像数据转换为 Numpy 数组。 Numpy 使用本机机器数据类型,与在 Python 数字对象上执行 Python 循环相比,其编译例程可以非常快速地处理数组数据。所以 Numpy 代码的速度与使用 ImageChops 的速度相当。你可以在 Numpy 中进行各种数学运算,或者使用相关的库,比如 SciPy.

Numpy 提供了一个函数 np.asarray 可以从 PIL 数据创建一个 Numpy 数组。 PIL.Image 有一个 .fromarray 方法从 Numpy 数组加载图像数据。

这是一个脚本,展示了两种不同的 Numpy 方法,以及一种基于 kennytm 的 ImageChops 代码的方法。

#!/usr/bin/env python3

''' PIL Image channel manipulation demo

    Replace each RGB channel by the mean of the other 2 channels, i.e.,

    R_new = (G_old + B_old) / 2
    G_new = (R_old + B_old) / 2
    B_new = (R_old + G_old) / 2

    This can be done using PIL's own ImageChops functions
    or by converting the pixel data to a Numpy array and
    using standard Numpy aray arithmetic

    Written by kennytm & PM 2Ring 2017.03.18
'''

from PIL import Image, ImageChops
import numpy as np

def comp_mean_pil(iname, oname):
    print('Loading', iname)
    img = Image.open(iname)
    #img.show()

    rgb = img.split()
    half = ImageChops.constant(rgb[0], 128)
    rh, gh, bh = [ImageChops.multiply(x, half) for x in rgb] 
    rgb = [
        ImageChops.add(gh, bh), 
        ImageChops.add(rh, bh), 
        ImageChops.add(rh, gh),
    ]
    out_img = Image.merge(img.mode, rgb)
    out_img.show()
    out_img.save(oname)
    print('Saved to', oname)

# Do the arithmetic using 'uint8' arrays, so we must be 
# careful that the data doesn't overflow
def comp_mean_npA(iname, oname):
    print('Loading', iname)
    img = Image.open(iname)
    in_data = np.asarray(img)

    # Halve all RGB values
    in_data = in_data // 2

    # Split image data into R, G, B channels
    r, g, b = np.split(in_data, 3, axis=2)

    # Create new channel data
    rgb = (g + b), (r + b), (r + g)

    # Merge channels
    out_data = np.concatenate(rgb, axis=2)

    out_img = Image.fromarray(out_data)
    out_img.show()
    out_img.save(oname)
    print('Saved to', oname)

# Do the arithmetic using 'uint16' arrays, so we don't need
# to worry about data overflow. We can use dtype='float'
# if we want to do more sophisticated operations
def comp_mean_npB(iname, oname):
    print('Loading', iname)
    img = Image.open(iname)
    in_data = np.asarray(img, dtype='uint16')

    # Split image data into R, G, B channels
    r, g, b = in_data.T

    # Transform channel data
    r, g, b = (g + b) // 2, (r + b) // 2, (r + g) // 2

    # Merge channels
    out_data = np.stack((r.T, g.T, b.T), axis=2).astype('uint8')

    out_img = Image.fromarray(out_data)
    out_img.show()
    out_img.save(oname)
    print('Saved to', oname)

# Test

iname = 'Glasses0.png'
oname = 'Glasses0_out.png'

comp_mean = comp_mean_npB

comp_mean(iname, oname)

输入图片

输出图像

FWIW,该输出图像是使用 comp_mean_npB 创建的。

这 3 个函数计算出的通道值可能相差 1,这是由于它们执行计算的方式不同,但当然这种差异并不明显。 :)