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?
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
output image
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. :)
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))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With