Changing pixel color using PIL on Python - python

I'm very new to programming, and I am learning more about image processing using PIL.
I have a certain task that requires me to change every specific pixel's color with another color. Since there are more than few pixels I'm required to change, I've created a for loop to access to every pixel. The script "works" at least, however the result is just a black screen with (0, 0, 0) color in each pixel.
from PIL import Image
img = Image.open('/home/usr/convertimage.png')
pixels = img.load()
for i in range(img.size[0]):
for j in range(img.size[1]):
if pixels[i,j] == (225, 225, 225):
pixels[i,j] = (1)
elif pixels[i,j] == (76, 76, 76):
pixels [i,j] = (2)
else: pixels[i,j] = (0)
img.save('example.png')
The image I have is a grayscale image. There are specific colors, and there are gradient colors near the borders. I'm trying to replace each specific color with another color, and then replace the gradient colors with another color.
However for the life of me, I don't understand why my output comes out with a single (0, 0, 0) color at all.
I tried to look for an answer online and friends, but couldn't come up with a solution.
If anyone out there knows what I'm doing wrong, any feedback is highly appreciated. Thanks in advance.

The issue is that your image is, as you said, greyscale, so on this line:
if pixels[i,j] == (225, 225, 225):
no pixel will ever equal the RGB triplet (255,255,255) because the white pixels will be simply the greyscale vale 255 not an RGB triplet.
It works fine if you change your loop to:
if pixels[i,j] == 29:
pixels[i,j] = 1
elif pixels[i,j] == 179:
pixels [i,j] = 2
else:
pixels[i,j] = 0
Here is the contrast-stretched result:
You may like to consider doing the conversion using a "Look Up Table", or LUT, as large numbers of if statements can get unwieldy. Basically, each pixel in the image is replaced with a new one found by looking up its current index in the table. I am doing it with numpy for fun too:
#!/usr/local/bin/python3
import numpy as np
from PIL import Image
# Open the input image
PILimage=Image.open("classified.png")
# Use numpy to convert the PIL image into a numpy array
npImage=np.array(PILimage)
# Make a LUT (Look-Up Table) to translate image values. Default output value is zero.
LUT=np.zeros(256,dtype=np.uint8)
LUT[29]=1 # all pixels with value 29, will become 1
LUT[179]=2 # all pixels with value 179, will become 2
# Transform pixels according to LUT - this line does all the work
pixels=LUT[npImage];
# Save resulting image
result=Image.fromarray(pixels)
result.save('result.png')
Result - after stretching contrast:
I am maybe being a bit verbose above, so if you like more terse code:
import numpy as np
from PIL import Image
# Open the input image as numpy array
npImage=np.array(Image.open("classified.png"))
# Make a LUT (Look-Up Table) to translate image values
LUT=np.zeros(256,dtype=np.uint8)
LUT[29]=1 # all pixels with value 29, will become 1
LUT[179]=2 # all pixels with value 179, will become 2
# Apply LUT and save resulting image
Image.fromarray(LUT[npImage]).save('result.png')

Related

Python Pillow(PIL) Not completely recoloring gradients?

I am having an issue where I'm using Pillow to recolor an image that has a lot of soft gradients but it seems to not completely color the most translucent part of these gradients, with the recolored image having a gradient that is not as smooth. Is there a way to fix this issue? Example Images and current code below.
enter image description here
Original Gradient: 1: https://i.stack.imgur.com/VFi75.png
enter image description here
Recolored Gradient: 1: https://i.stack.imgur.com/e5iNa.png
Here is the Original transparent PNG of the image
import random
import Owl_Attributes
from PIL import Image, ImageColor
# I create the image here and convert the color code to RGBA
RGB_im = image_base_accent3.convert("RGBA")
datas = RGB_im.getdata()
newData = []
for item in datas:
if item[0] == 208 and item[1] == 231 and item[2] == 161:
newData.append((255, 0, 0, item[3]))
else:
newData.append(item)
RGB_im.putdata(newData)
RGB_im.save('Owl_project_pictures\_final_RGB.png')
First, a couple of things to consider:
Inspect your images before you start work. Yours has an alpha channel that is pretty much pointless and irrelevant so I would discard that to save space and processing time.
Using for loops over Python lists of pixels is slow, inefficient, and error-prone in Python. Try to use built-in functions based on C code, or to use vectorised functions like Numpy.
On to your image. There are a whole load of shades and gradations of tone in your image and dealing with one separately through if statements is going to be difficult. I would suggest you want to use HSV colourspace instead.
I think you want the basic result to be a very saturated red with the lightness dictated by the lightness of the original image.
So, I would make an image with:
Hue=0 (see lower part of this diagram), and
Saturation=255 (i.e. fully saturated), and
Value (i.e. brightness) of the original image.
In code that might look like this:
#!/usr/bin/env python3
# ImageMagick command-line "equivalent"
# magick -size 599x452 xc:black xc:white \( VFi75.png -colorspace gray +level 0,60% \) +combine HSL result.png
from PIL import Image
# Load image and create HSV version
im = Image.open('VFi75.png')
HSV = im.convert('HSV')
# Split into separate channels for processing, discarding Hue and Saturation
_, _, V = HSV.split()
# Synthesize Hue channel, same size as input image, filled with 0, to make Red
H = Image.new('L', (im.width, im.height), 0)
# Synthesize Saturation channel, same size as input image, filled with 255, to make fully saturated
S = Image.new('L', (im.width, im.height), 255)
# Recombine synthesized H, S and V (based on original image brightness) back into a recombined image
RGB = Image.merge('HSV', (H,S,V)).convert('RGB')
# Save processed result
RGB.save('result.png')
If you wanted to make it lime green, you would change the Hue angle like this:
# Synthesize Hue channel, same size as input image, filled with 120, to make Lime Green
H = Image.new('L', (im.width, im.height), 120)
If you wanted to make it less saturated, you would change the saturation like this:
# Synthesize Saturation channel, same size as input image, filled with 64, to make less saturated
S = Image.new('L', (im.width, im.height), 64)

How to change the colour of pixels in an image depending on their initial luminosity?

The aim is to take a coloured image, and change any pixels within a certain luminosity range to black. For example, if luminosity is the average of a pixel's RGB values, any pixel with a value under 50 is changed to black.
I’ve attempted to begin using PIL and converting to grayscale, but having trouble trying to find a solution that can identify luminosity value and use that info to manipulate a pixel map.
There are many ways to do this, but the simplest and probably fastest is with Numpy, which you should get accustomed to using with image processing in Python:
from PIL import Image
import numpy as np
# Load image and ensure RGB, not palette image
im = Image.open('start.png').convert('RGB')
# Make into Numpy array
na = np.array(im)
# Make all pixels of "na" where the mean of the R,G,B channels is less than 50 into black (0)
na[np.mean(na, axis=-1)<50] = 0
# Convert back to PIL Image to save or display
result = Image.fromarray(na)
result.show()
That turns this:
Into this:
Another slightly different way would be to convert the image to a more conventional greyscale, rather than averaging for the luminosity:
# Load image and ensure RGB
im = Image.open('start.png').convert('RGB')
# Calculate greyscale version
grey = im.convert('L')
# Point process over pixels to make mask of darker ones
mask = grey.point(lambda p: 255 if p<50 else 0)
# Paste black (i.e. 0) into image where mask indicates it is dark
im.paste(0, mask=mask)
Notice that the blue channel is given considerably less significance in the ITU-R 601-2 luma transform that PIL uses (see the lower 114 weighting for Blue versus 299 for Red and 587 for Green) in the formula:
L = R * 299/1000 + G * 587/1000 + B * 114/1000
so the blue shades are considered darker and become black.
Another way would be to make a greyscale and a mask as above. but then choose the darker pixel at each location when comparing the original and the mask:
from PIL import Image, ImageChops
im = Image.open('start.png').convert('RGB')
grey = im.convert('L')
mask = grey.point(lambda p: 0 if p<50 else 255)
res = ImageChops.darker(im, mask.convert('RGB'))
That gives the same result as above.
Another way, pure PIL and probably closest to what you actually asked, would be to derive a luminosity value by averaging the channels:
# Load image and ensure RGB
im = Image.open('start.png').convert('RGB')
# Calculate greyscale version by averaging R,G and B
grey = im.convert('L', matrix=(0.333, 0.333, 0.333, 0))
# Point process over pixels to make mask of darker ones
mask = grey.point(lambda p: 255 if p<50 else 0)
# Paste black (i.e. 0) into image where mask indicates it is dark
im.paste(0, mask=mask)
Another approach could be to split the image into its constituent RGB channels, evaluate a mathematical function over the channels and mask with the result:
from PIL import Image, ImageMath
# Load image and ensure RGB
im = Image.open('start.png').convert('RGB')
# Split into RGB channels
(R, G, B) = im.split()
# Evaluate mathematical function over channels
dark = ImageMath.eval('(((R+G+B)/3) <= 50) * 255', R=R, G=G, B=B)
# Paste black (i.e. 0) into image where mask indicates it is dark
im.paste(0, mask=dark)
I created a function that returns a list with True if the pixel has a luminosity of less than a parameter, and False if it doesn't. It includes an RGB or RGBA option (True or False)
def get_avg_lum(pic,avg=50,RGBA=False):
num=3
numd=4
if RGBA==False:
num=2
numd=3
li=[[[0]for y in range(0,pic.size[1])] for x in range(0,pic.size[0])]
for x in range(0,pic.size[0]):
for y in range(0,pic.size[1]):
if sum(pic.getpixel((x,y))[:num])/numd<avg:
li[x][y]=True
else:
li[x][y]=False
return(li)
a=get_avg_lum(im)
The pixels match in the list, so (0,10) on the image is [0][10] in the list.
Hopefully this helps. My module is for standard PIL objects.

How to get color of all pixels in an area in python

I'm trying to write a program on linux that does something if the pixels in an area aren't all the same color, for example:
if color not "255, 255, 255":
#do something
this is what i have for one pixel:
import time, pyautogui
time.clock()
image = pyautogui.screenshot()
color = image.getpixel((1006, 553))
print(time.clock())
print(color)
I know how to get the color of a pixel using .getpixel() but that only gets one pixel
Basically, how do i get the color of an area of pixels when i know all the pixels in that area are the same color.
Also, as quick as possible, like 0.5s or under.
I keep recommending it, but the scikit-image library is pretty great, and they have some really solid documentation and examples. I would recommend a combo of that and using numpy arrays directly. It is just a lot faster when working directly with pixels.
You will have to convert the PIL image to a numpy array...but this should work with that:
import pyautogui
import numpy as np
image = pyautogui.screenshot()
np_image = np.array(image)
You can slice the image:
red_slice = np_image[0:50, 0:50,0]
red_mask = red_slice == 200
This would give you the values for red in the upper right 50x50 pixel area. red_mask is an array of True/False values whether each red value in that area is equal to 200. This can be repeated for the other channels as you see fit.

Skimage : rotate image and fill the new formed background

When rotating an image using
import skimage
result = skimage.transform.rotate(img, angle=some_angle, resize=True)
# the result is the rotated image with black 'margins' that fill the blanks
The algorithm rotates the image but leaves the newly formed background black and there is no way - using the rotate function - to choose the color of the newly formed background.
Do you have any idea how to do choose the color of the background before rotating an image?
Thank you.
Just use cval parameter
img3 = transform.rotate(img20, 45, resize=True, cval=1, mode ='constant')
img4 = img3 * 255
It's a little annoying that the cval parameter to skimage.transform.warp() can only be a scalar float.
You can set it to any value outside of the range of your data (like -1), and use np.where() to fix it afterwards. Here's an example that sets the background of a 3-channel image to red:
output = tf.warp(image, tform, mode='constant', cval=-1,
preserve_range=True, output_shape=(256, 256), order=3)
w = np.where(output == -1)
output[w[0], w[1], :] = [255, 0, 0]
tbh I don't know why everyone set the cval value to a strange value.
I think it's intuitive when you look at the documentation of the function
cval : float, optional
Used in conjunction with mode ‘constant’, the value outside the image boundaries.
Therefore depending on what your input image is, in my case, my picture already used cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), therefore in order to set it to white, I can simply set cval=255.
If it's ndarray(img more than 1 channel), you can either rotate each channel separately, or set it to strange value then use np.where to replace afterward. (idk why but I just don't like this method lol)
if your pic is a gray picture,use cval ;
if pic is rgb ,no good way,blow code works:
path3=r'C:\\Users\forfa\Downloads\1111.jpg'
img20=data.imread(path3)
import numpy as np
img3=transform.rotate(img20,45,resize=True,cval=0.1020544) #0.1020544 can be any unique float,the pixel outside your pic is [0.1020544]*3
c,r,k=img3.shape
for i in range(c):
for j in range(r):
if np.allclose(img3[i][j],[0.1020544]*3):
img3[i][j]=[1,0,0] #[1,0,0] is red pixel,replace pixel [0.1020544]*3 with red pixel [1,0,0]
I don't know how do it, but maybe this help you
http://scikit-image.org/docs/dev/api/skimage.color.html
Good luck! ;)

Python Imaging Library- Merging one type of pixel into another image

Basically, I have two images. One is comprised of white and black pixels, the black pixels making up a word, and the other image that I'm trying to paste the black pixels on top of. I've pasted the code below, however I'm aware that there's an issue with the "if pixels [x,y] == (0, 0, 0):' being a tuple and not an indice, however I'm uncertain of how to get it to look for black pixels with other means.
So essentially I need to find, and remember the positions of, the black pixels so that I can paste them onto the first image. Any help is very much appreciated!
image_one = Image.open (image_one)
image_two = Image.open (image_two)
pixels = list(image_two.getdata())
for y in xrange(image_two.size[1]):
for x in xrange(image_two.size[0]):
if pixels[x,y] == (0, 0, 0):
pixels = black_pixels
black_pixels.append()
image = Image.open (image_one);
image_one.putdata(pixels)
image.save(image_one+ "_X.bmp")
del image_one, image_two;
You're almost there. I am not too familiar with the PIL class, but instead of calling the getdata method, let's use getpixel directly on the image object, and directly set the results into the output image. That eliminates the need to store the set of pixels to overwrite. However, there may be cases beyond what you've listed here where such an approach would be necessary. I created a random image and then set various pixels to black. For this test I used a different condition - if the R channel of the image is greater than 50. You can comment that out and use the other test, which tests for tuple (R,G,B) == (0,0,0) which will work fine.
imagea = PIL.Image.open('temp.png')
imageb = PIL.Image.open('temp.png')
for y in xrange(imagea.size[1]):
for x in xrange(imagea.size[0]):
currentPixel = imagea.getpixel((x,y))
if currentPixel[0] > 50:
#if currentPixel ==(0,0,0):
#this is a black pixel, you can directly modify image 2 now
imageb.putpixel((x,y),(0,0,0))
imageb.save('outputfile.png')
An alternative way to do this is just to multiply the two images together. Any pixel that's black in the binary image will be black in the result (multiply by zero) and any pixel that's white in the binary image will be unchanged from the other image in the result (multiply by one).
PIL can do this,
from PIL import Image, ImageChops
image_one = Image.open("image_one.bmp")
image_two = Image.open("image_two.bmp")
out = ImageChops.multiply(image_one, image_two)
out.save("output.bmp")

Categories

Resources