Channel mix with Pillow - python

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

Related

Subtract vignetting template from image in OpenCV Python

I have 750+ images, like this 'test.png', that I need to subtract the vignetting in 'vig-raw.png' from. I just started using opencv-python, so "I don't even know what I don't know".
Using GIMP, I desaturated 'vig-raw.png' to create 'vig-desat.png', which I then converted with Color to Alpha to create 'vig-alpha.png'.
This is my attempt to subtract 'vig-alpha.png' from 'test.png'.
import cv2 as cv
import numpy as np
img1 = cv.imread('test.png',0)
img1 = cv.cvtColor(img1, cv.COLOR_BGR2BGRA) # add alpha channel to RGB image
print(img1[0][0]) # show alpha
img2 = cv.imread('vig-alpha.png',flags=cv.IMREAD_UNCHANGED) # read RGBA image
print(img2[0][0]) #show alpha
img3 = cv.subtract(img1, img2)
img3 = cv.resize(img3, (500,250))
print(img3[0][0]) # show alpha
cv.imshow('result',img3)
cv.waitKey()
cv.destroyAllWindows()
However, this is the 'result'. I need to produce a uniform shading throughout the image while leaving the original colors intact. I don't know the correct terminology for this sort of thing, and it's hard to search for a solution with what I do know. Thanks in advance.
EDIT: As per Rotem's answer, image file format matters. StackOverflow converted the PNG files I posted to JPEG, which did effect results while checking their answer. See the comment I left on Rotem's answer below for more information.
Vignette template is not supposed to be subtracted, it supposed to be scaled.
The vignette correction process is known as Flat-field correction applies:
G = m / (F - D)
C = (R - D) * G
When D is dark field or dark frame.
We don't have dark frame sample - we may assume that the dark frame is all zeros.
Assuming D=zeros, the correction formula is:
G = m / F
C = R * G
m = mean(F), and F applies vig-alpha.
R is test.png.
For computing G (name it inv_vig_norm, we may use the following stages):
Read vig-alpha.png as grayscale, and convert it to float in range [0, 1] (vig_norm applies F):
vig = cv2.imread('vig-alpha.png', cv2.IMREAD_GRAYSCALE)
vig_norm = vig.astype(np.float32) / 255
Divide m by F:
vig_mean_val = cv2.mean(vig_norm)[0]
inv_vig_norm = vig_mean_val / vig_norm # Compute G = m/F
Compute C = R * G - scale img1 by inv_vig_norm:
inv_vig_norm = cv2.cvtColor(inv_vig_norm, cv2.COLOR_GRAY2BGR)
img2 = cv2.multiply(img1, inv_vig_norm, dtype=cv2.CV_8U) # Compute: C = R * G
For removing noise and artifacts, we may apply Median Blur and Gaussian Blur over vig (it may be required because the site converted vig-alpha.png to JPEG format).
Code sample:
import cv2
import numpy as np
img1 = cv2.imread('test.png')
vig = cv2.imread('vig-alpha.png', cv2.IMREAD_GRAYSCALE) # Read vignette template as grayscale
vig = cv2.medianBlur(vig, 15) # Apply median filter for removing artifacts and extreem pixels.
vig_norm = vig.astype(np.float32) / 255 # Convert vig to float32 in range [0, 1]
vig_norm = cv2.GaussianBlur(vig_norm, (51, 51), 30) # Blur the vignette template (because there are still artifacts, maybe because SO convered the image to JPEG).
#vig_max_val = vig_norm.max() # For avoiding "false colors" we may use the maximum instead of the mean.
vig_mean_val = cv2.mean(vig_norm)[0]
# vig_max_val / vig_norm
inv_vig_norm = vig_mean_val / vig_norm # Compute G = m/F
inv_vig_norm = cv2.cvtColor(inv_vig_norm, cv2.COLOR_GRAY2BGR) # Convert inv_vig_norm to 3 channels before using cv2.multiply. https://stackoverflow.com/a/48338932/4926757
img2 = cv2.multiply(img1, inv_vig_norm, dtype=cv2.CV_8U) # Compute: C = R * G
cv2.imshow('inv_vig_norm', cv2.resize(inv_vig_norm / inv_vig_norm.max(), (500, 250))) # Show inv_vig_norm for testing
cv2.imshow('img1', cv2.resize(img1, (500, 250)))
cv2.imshow('result', cv2.resize(img2, (500, 250)))
cv2.waitKey()
cv2.destroyAllWindows()
Results:
img1:
inv_vig_norm:
img2:

How Do I Develop a negative film image using python

I have tried inverting a negative film images color with the bitwise_not() function in python but it has this blue tint. I would like to know how I could develop a negative film image that looks somewhat good. Here's the outcome of what I did. (I just cropped the negative image for a new test I was doing so don't mind that)
If you don't use exact maximum and minimum, but 1st and 99th percentile, or something nearby (0.1%?), you'll get some nicer contrast. It'll cut away outliers due to noise, compression, etc.
Additionally, you should want to mess with gamma, or scale the values linearly, to achieve white balance.
I'll apply a "gray world assumption" and scale each plane so the mean is gray. I'll also mess with gamma, but that's just messing around.
And... all of that completely ignores gamma mapping, both of the "negative" and of the outputs.
import numpy as np
import cv2 as cv
import skimage
im = cv.imread("negative.png")
(bneg,gneg,rneg) = cv.split(im)
def stretch(plane):
# take 1st and 99th percentile
imin = np.percentile(plane, 1)
imax = np.percentile(plane, 99)
# stretch the image
plane = (plane - imin) / (imax - imin)
return plane
b = 1 - stretch(bneg)
g = 1 - stretch(gneg)
r = 1 - stretch(rneg)
bgr = cv.merge([b,g,r])
cv.imwrite("positive.png", bgr * 255)
b = 1 - stretch(bneg)
g = 1 - stretch(gneg)
r = 1 - stretch(rneg)
# gray world
b *= 0.5 / b.mean()
g *= 0.5 / g.mean()
r *= 0.5 / r.mean()
bgr = cv.merge([b,g,r])
cv.imwrite("positive_grayworld.png", bgr * 255)
b = 1 - np.clip(stretch(bneg), 0, 1)
g = 1 - np.clip(stretch(gneg), 0, 1)
r = 1 - np.clip(stretch(rneg), 0, 1)
# goes in the right direction
b = skimage.exposure.adjust_gamma(b, gamma=b.mean()/0.5)
g = skimage.exposure.adjust_gamma(g, gamma=g.mean()/0.5)
r = skimage.exposure.adjust_gamma(r, gamma=r.mean()/0.5)
bgr = cv.merge([b,g,r])
cv.imwrite("positive_gamma.png", bgr * 255)
Here's what happens when gamma is applied to the inverted picture... a reasonably tolerable transfer function results from applying the same factor twice, instead of applying its inverse.
Trying to "undo" the gamma while ignoring that the values were inverted... causes serious distortions:
And the min/max values for contrast stretching also affect the whole thing.
A simple photo of a negative simply won't do. It'll include stray light that offsets the black point, at the very least. You need a proper scan of the negative.
Here is one simple way to do that in Python/OpenCV. Basically one stretches each channel of the image to full dynamic range separately. Then recombines. Then inverts.
Input:
import cv2
import numpy as np
import skimage.exposure
# read image
img = cv2.imread('boys_negative.png')
# separate channels
r,g,b = cv2.split(img)
# stretch each channel
r_stretch = skimage.exposure.rescale_intensity(r, in_range='image', out_range=(0,255)).astype(np.uint8)
g_stretch = skimage.exposure.rescale_intensity(g, in_range='image', out_range=(0,255)).astype(np.uint8)
b_stretch = skimage.exposure.rescale_intensity(b, in_range='image', out_range=(0,255)).astype(np.uint8)
# combine channels
img_stretch = cv2.merge([r_stretch, g_stretch, b_stretch])
# invert
result = 255 - img_stretch
cv2.imshow('input', img)
cv2.imshow('result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()
# save results
cv2.imwrite('boys_negative_inverted.jpg', result)
Result:
Caveat: This works for this image, but may not be a universal solution for all images.
ADDITION
In the above, I did not clip when stretching as I wanted to preserver all information. But if one wants to clip and use skimage.exposure.rescale_intensity for stretching, then it is easy enough by the following:
import cv2
import numpy as np
import skimage.exposure
# read image
img = cv2.imread('boys_negative.png')
# separate channels
r,g,b = cv2.split(img)
# compute clip points -- clip 1% only on high side
clip_rmax = np.percentile(r, 99)
clip_gmax = np.percentile(g, 99)
clip_bmax = np.percentile(b, 99)
clip_rmin = np.percentile(r, 0)
clip_gmin = np.percentile(g, 0)
clip_bmin = np.percentile(b, 0)
# stretch each channel
r_stretch = skimage.exposure.rescale_intensity(r, in_range=(clip_rmin,clip_rmax), out_range=(0,255)).astype(np.uint8)
g_stretch = skimage.exposure.rescale_intensity(g, in_range=(clip_gmin,clip_gmax), out_range=(0,255)).astype(np.uint8)
b_stretch = skimage.exposure.rescale_intensity(b, in_range=(clip_bmin,clip_bmax), out_range=(0,255)).astype(np.uint8)
# combine channels
img_stretch = cv2.merge([r_stretch, g_stretch, b_stretch])
# invert
result = 255 - img_stretch
cv2.imshow('input', img)
cv2.imshow('result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()
# save results
cv2.imwrite('boys_negative_inverted2.jpg', result)
Result:

image looks overexposed (nearly all white) when using np.zeros with OpenCV imshow

I am writing a code in openCV python for copying an image for practice purpose only, though np.copy() command is already available. Code is as below:
import numpy as np
import cv2 as cv
img = cv.imread('Photos/image_1.jpg')
r, c, d = img.shape
img_copy = np.zeros((r, c, d))
for i in range(r):
for j in range(c):
for k in range(d):
img_copy[i, j, k] = img[i, j, k]
cv.imshow('original image', img)
cv.imshow('copied image', img_copy)
cv.waitKey(0)
The img_copy is not shown instead black image is shown. If I use img_copy = np.ones(rows, cols, 3), and apply same for loop, still then a white image is shown, original image is not shown. Can any one explain why this occures? Original and copied images are shown below.
This issue was caused by incompatible data types.
To determine the data type of the image, use
img = cv.imread('Photos/image_1.jpg')
print(img.dtype)
In my testing case, the output data type was uint8
Changing
img_copy = np.zeros((r, c, d))
to
img_copy = np.zeros((r, c, d), dtype=np.uint8)
will fix this issue
OpenCV's imshow() is sensitive to the element type of the array you pass it.
When the element type is np.uint8, the values must be in the range of 0 to 255.
When the element type is np.float32 or np.float64, the values must be in the range of 0.0 to 1.0.
You have two options:
Either scale your values: img_copy / 255
Or clip and convert your values: np.clip(img_copy, 0, 255).astype(np.uint8)

Optimization Basic Image Processing Python

I'm trying to accomplish a basic image processing. Here is my algorithm :
Find n., n+1., n+2. pixel's RGB values in a row and create a new image from these values.
I'm taking first pixel's red value,second pixel's green value and third pixel's blue value and create pixel. This operation continue for every row in image.
Here is my example code in python :
import glob
import ntpath
import numpy
from PIL import Image
images = glob.glob('balls/*.png')
data_compressed = numpy.zeros((540, 2560, 3), dtype=numpy.uint8)
for image_file in images:
print(f'Processing [{image_file}]')
image = Image.open(image_file)
data = numpy.loadasarray(image)
for i in range(0, 2559):
for j in range(0, 539):
pix_x = j * 3 + 1
red = data[pix_x - 1, i][0]
green = data[pix_x, i][1]
blue = data[pix_x + 1, i][2]
data_compressed[j, i] = [red, green, blue]
im = Image.fromarray(data_compressed)
image_name = ntpath.basename(image_file)
im.save(f'export/{image_name}')
My input and output images are in RGB format. My code is taking 5 second for every image. I'm open for any idea to optimization this task. I can use c++ or any other languages if necessary.
data_compressed = np.concatenate((
np.expand_dims(data[0:-2][:,:,0], axis=2),
np.expand_dims(data[1:-1][:,:,1], axis=2),
np.expand_dims(data[2:][:,:,2], axis=2)), axis=2)
Image1 : Original image
Image2: Original image shifted by one pixel
Image3: Original image shifted by two pixel
Take channel 0 of Image1, channel 1 of Image2 and channel 3 of Image3 concatenate.
Sample
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
image = Image.open("Lenna.png")
data = numpy.asarray(image)
data_compressed = np.concatenate((
np.expand_dims(data[0:-2][:,:,0], axis=2),
np.expand_dims(data[1:-1][:,:,1], axis=2),
np.expand_dims(data[2:][:,:,2], axis=2)), axis=2)
new_image = Image.fromarray(data_compressed)
If you want a stride over 3 pixels for calculating the next pixel again then you can use numpy slicing
new_image = Image.fromarray(data_compressed[:, ::3])
Original Image:
Transformed Image with 3 stride:
Well if are only looking for a speed up you should take a look at the module Cython. It lets you specify the type of different variables and then compile the script to functioning c code. This can often lead to great improvements when it comes to time complexity.
With plain python there's only so much you can do. Here is a small optimization which can help a bit since it will allocate less memory. Otherwise I would look at Cython/Numba as said previously or using other languages.
data_compressed[j, i, 0] = data[pix_x - 1, i][0]
data_compressed[j, i, 1] = data[pix_x, i][1]
data_compressed[j, i, 2] = data[pix_x + 1, i][2]

How could I implement image channel drop with the python lib of pillow

I need to randomly zero out 0, 1 or 2 channel of a pillow image. That means I need to randomly set 0, 1 or 2 channels of an image to 0.
How could I do that with pil?
Here's an easy, native PIL way of doing it by multiplying by a transform. I set the default transform so the maths looks like this:
newRed = 1*oldRed + 0*oldGreen + 0*oldBlue + constant
newGreen = 0*oldRed + 1*OldGreen + 0*OldBlue + constant
newBlue = 0*oldRed + 0*OldGreen + 1*OldBlue + constant
Then I just change the 1 to 0 where I want a channel zeroed out.
#!/usr/bin/env python3
from PIL import Image
# Open image
im = Image.open('input.png').convert("RGB")
# Pre-set R, G and B multipliers to 1
Rmult, Gmult, Bmult = 1, 1, 1
# Select one (or more) channels to zero out, I choose B channel here
Bmult=0
# Make transform matrix
Matrix = ( Rmult, 0, 0, 0,
0, Gmult, 0, 0,
0, 0, Bmult, 0)
# Apply transform and save
im = im.convert("RGB", Matrix)
im.save('result.png')
So, if you start with this:
and you set the Blue multiplier (Bmult) to zero, you'll get:
If you zero Red and Blue with:
Rmult = Bmult = 0
you'll get:
Have you tried using Numpy?
It's quite simple.
import numpy as np
import PIL.Image as Image
img = np.array(Image.open("image1.jpg")) # My Image
c = np.random.randint(3, size=1)[0] # Selecting a random channel c
img[:,:,c] = img[:,:,c] * 0 # channel c times 0.
TRY:-
from PIL import Image
import random
# Provide path to your image
img = Image.open(r"Image_Path")
# Converting the Image's mode to RGB, coz you wanted a random channel out of 3 channels
img.convert("RGB")
# Getting individual channels off the image
r, g, b = img.split()
# choice will store a random number b/w 0-2 we will use this value to extract a random channel
choice = random.randrange(0, 3)
null_channel = (r, g, b)[choice]
# printing the index of our randomly selected channel, ( 0 = Red; 1 = Green; 2 = Blue)
print(choice)
# changing each individual pixel value to 0, of our randomly selected channel
null_channel = null_channel.point(lambda p: 0)
# These conditions will provide the null channel to it's original channel's variable
if choice is 1:
g = null_channel
elif choice is 2:
b = null_channel
else:
r = null_channel
# creating a new image with the original two channels' and our null'd channel
new_img = Image.merge("RGB", (r, g, b))
new_img.save("new_img.jpg")
The above code will first convert an image's color mode to RGB, so that we can deal with 3 channels. Then it will extract the individual image channels. Then it will select a random channel of the 3 channel, and convert each pixel value of that channel to (0, 0, 0). Then it will figure out which channel was originally used (R or G or B) and then overwrite the modified values to the channel. In the end, it will create a Image Object by merging all the new channels and saves it.
Sample Image:-
Image after modification:-
After analysing the modified Image, we can clearly deduce that the red channel of the image was converted into null channel.

Categories