Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Channel mix with Pillow

Tags:

python

pillow

I would like to do some color transformations, for example given RGB channels

R =  G + B / 2

or some other transformation where a channel value is calculated based on the values of other channels of the same pixel.

It seems that .point() function can only operate on one channel. Is there a way to do what I want?

like image 381
Paolo Avatar asked Feb 05 '23 20:02

Paolo


2 Answers

An alternative to using PIL.ImageChops is to convert the image data to a Numpy array. Numpy uses native machine data types and its compiled routines can processes array data very quickly compared to doing Python loops on Python numeric objects. So the speed of Numpy code is comparable to the speed of using ImageChops. And you can do all sorts of mathematical operations in Numpy, or using related libraries, like SciPy.

Numpy provides a function np.asarray which can create a Numpy array from PIL data. And PIL.Image has a .fromarray method to load image data from a Numpy array.

Here's a script that shows two different Numpy approaches, as well as an approach based on kennytm's ImageChops code.

#!/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)

input image

Glasses, by Gilles Tran

output image

Transformed Glasses

FWIW, that output image was created using comp_mean_npB.

The calculated channel values produced by the 3 functions can differ from one another by 1, due to the differences in the way they perform the calculations, but of course such differences aren't readily visible. :)

like image 159
PM 2Ring Avatar answered Feb 07 '23 11:02

PM 2Ring


For this particular operation, the color transformation can be written as a matrix multiplication, so you could use the convert() method with a custom matrix (assuming no alpha channel):

# 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,
))

Otherwise, you can split() the image into 3 or 4 images of each color band, apply whatever operation you like, and finally merge() those bands back to a single image. Again, the original image should be in RGB or RGBA mode.

(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))
like image 32
kennytm Avatar answered Feb 07 '23 10:02

kennytm