Mapping values of pixels in an image with Python - python

I am working with .png files, having pixels values 1 and 255.
I have to map pixel values 255 to 0 and, in other cases, pixels with values 1 to 2.
I tried:
for img_name in good_imgs:
img = Image.open(img_name)
pixels = img.load()
for i in range(img.size[0]):
for j in range(img.size[1]):
if pixels[i,j] == 255:
pixels[i,j] = 0
img.save(img_name)
for img_name in bad_imgs:
img = Image.open(img_name)
pixels = img.load()
for i in range(img.size[0]):
for j in range(img.size[1]):
if pixels[i,j] == 255:
pixels[i,j] = 0
elif pixels[i,j] == 1:
pixels[i,j] == 2
img.save(img_name)
but the images saved in this way, have the same pixels values as the originals.
What is wrong with the above code? How can I change pixels values in this kind of images?
Updtate:
I have noticed that if I modified the pixel of a single image, the pixels are mapped correctly, i.e. now I suppose the issue is about saving images with img.save()

As I mentioned in the comments, for loops are slow, inefficient and error-prone for processing images in Python.
Here's a more efficient way of doing what you ask. Your colours of 0, 1, 2 and 255 are hard to see on StackOverflow's background and hard to differentiate from each other because 3 of them are essentially black, so I am transforming 64 and 150 into 192 and 32 but you can adapt to your own numbers:
from PIL import Image
# Load image and ensure greyscale, i.e. 'L'
im = Image.open('image.png').convert('L')
# Remap colours 150 -> 32, 64 -> 192
res = im.point((lambda p: 32 if p==150 else (192 if p==64 else 0)))
res.save('result.png')
If you want to use Numpy instead, which is also vectorised and optimised, it might look like this (other techniques are also possible):
from PIL import Image
import numpy as np
# Load image and ensure greyscale, i.e. 'L'
im = Image.open('image.png').convert('L')
# Make into Numpy array
na = np.array(im)
# Anywhere pixels are 150, make them 32
na[na==150] = 32
# Anywhere pixels are 64, make them 192
na[na==64] = 192
# Convert back to PIL Image and save
Image.fromarray(na).save('result.png')

Related

i want to change every single pixel of an img to black exluding transparent python

I want to edit all the pixels of a PNG image except for the transparent pixels. I have this code:
img_da_maskerare = Image.open("temp.png")
width = img_da_maskerare.size[0]
height = img_da_maskerare.size[1]
for i in range(0,width):
for j in range(0,height):
data = img_da_maskerare.getpixel((i,j))
#print(data) #(255, 255, 255)
if (data[0]!=255 and data[1]!=0 and data[2]!=0):
img_da_maskerare.putpixel((i,j),(00, 00, 00))
img_da_maskerare.show()
example: original becomes this
What is wrong with the code?
Iterating over images in Python with for loops should only be a very last resort - it is slow, error-prone and inefficient.
Try to favour built-in functions that are programmed in optimised C or vectorised Numpy functions.
Essentially, you seem to want an image that is black anywhere the original image is opaque. So, let's be guided by that:
from PIL import Image
# Load image
im = Image.open('Ow40z.png')
# Extract the alpha channel and threshold it at 200
alpha = im.getchannel('A')
alphaThresh = alpha.point(lambda p: 255 if p>200 else 0)
# Make a new completely black image same size as original
res = Image.new('RGB', im.size)
# Copy across the alpha channel from original
res.putalpha(alphaThresh)
res.save('result.png')
Just FYI, that takes 337 microseconds versus 166 milliseconds for your loop-based approach, i.e. it is around 500x faster, and there are no loops and indices to get wrong.
I mentioned you can use Numpy so here is much the same thing done using that:
import numpy as np
# Open image and make into Numpy array
na = np.array(Image.open('Ow40z.png'))
# Make every pixel, of every row of every column black in RGB channels, i.e. channel 0, 1, 2
na[:, :, :3] = 0
# Make Boolean True/False mask of all alphas > 200
mask = na[:,:,3] > 200
# Set alpha channel to 255 wherever mask is True, and 0 elsewhere
na[:, :, 3] = mask * 255
# Convert back from Numpy array to PIL Image
pilImage = Image.fromarray(na)
if data[3] == 255
The data has a length of 4 representing the RGBA. Where A alpha represents the opacity of the pixel. which has values from 0 to 255. 0 means completely transparent, and 255 means completely opaque.
Here, I used data[3] > 200 to include those pixels too that have slight transparency.
The code code changes
to
# import PIL module
from PIL import Image
img_da_maskerare = Image.open("temp.png")
width = img_da_maskerare.size[0]
height = img_da_maskerare.size[1]
for i in range(0,width):
for j in range(0,height):
data = img_da_maskerare.getpixel((i,j))
# RGBA = Red, Green, Blue, Alpha
# Alpha = 0 = transparent, 255 = opaque
# alpha > 200 means almost opaque
if data[3] >= 200:
img_da_maskerare.putpixel((i,j),(00, 00, 00))
img_da_maskerare.save('temp_maskerad.png')

How to generate a bespoke black and white bitmap

image example:
I want to create a black and white bitmap from scratch (not converting or manipulating an existing image) and have the ability to change individual pixels to either black or white using pixel coordinates, somehow, maybe via a dictionary?. Something like a chessboard but with one pixel per chessboard-square (if that makes sense?).I found something to generate a colour spectrum image but don't know how to adapt this.
from PIL import Image
img = Image.new( 'RGB', (300,50), "black") # Create a new black image
pixels = img.load() # Create the pixel map
for i in range(img.size[0]): # For every pixel:
for j in range(img.size[1]):
pixels[i,j] = (i, j, 100) # Set the colour accordingly
img.show()
Zoomed in on leftmost edge of bitmap
You can do individual odd pixels here and there like this:
from PIL import PIL
# Create new black image - L mode for b/w
img = Image.new( 'L', (10,6))
# Make pixels at locations (0,5) and (2,1) white (255)
img.putpixel((0,5), 255)
img.putpixel((2,1), 255)
# Save result
img.save('result.png')
However, if you want to do whole rows, or columns, or longer lines, I would recommend round-tripping to Numpy like this:
import numpy as np
# Create new black image - L mode for b/w
img = Image.new( 'L', (10,6))
# Convert to Numpy array for easy processing
na = np.array(img)
# Make row 1 white
na[1,:] = 255
# Make column 8 white
na[:,8] = 255
# Revert to PIL Image from Numpy array and save
Image.fromarray(na).save('result.png')
Or if you want to do a block:
... as above ...
na[1:3,5:9] = 255
... as above ...

How can I convert an array of pixel colors into an image efficiently with Python?

I am working on a project that implements Numba and CUDA with Python 3.8. Currently, I create an array with the dimensions of the final image. Next, I generate an image with a CUDA kernel (incredibly fast). Then, I copy the pixel color into a Pillow Image (incredibly slow). My code:
for x in range(width):
for y in range(height):
if pixels[x][y] = 0:
color = [0, 0, 0]
else:
# Get color from int as tuple
color = rgb_conv(pixels[x][y]
red = int(numpy.floor(color[0]))
if red > 255:
red = 255
elif red < 0:
red = 0
green = int(numpy.floor(color[1]))
if green > 255:
green = 255
elif green < 0:
green = 0
blue = int(numpy.floor(color[2]))
if blue > 255:
blue = 255
elif blue < 0:
blue = 0
image.putpixel((x, y), (red, green, blue))
Are there any more efficient Python image libraries for this implementation? Is there a way to convert the array to an image on the GPU? Any help with direction works. Thanks!
EDIT 1: A request was made for the function rgb_conv. This is a function I found to convert a single integer into a three-wide color.
def rgb_conv(i):
color = 255 * numpy.array(colorsys.hsv_to_rgb(i / 255.0, 1.0, 0.5))
return tuple(color.astype(int))
However, I didn't particularly like the colors this function produces, so I removed it and began working with the following:
pixelArr = image.load()
for x in range(width):
for y in range(height):
color = int(numpy.floor(pixels[x][y]))
pixelArr[x, y] = (color << 21) + (color << 10) + color * 8
This adjustment doesn't do much to the running time of the code. I am looking further into a suggestion load the image from an array rather than putting each pixel into the image.
It is not efficient to place each pixel into an image with pillow. Creating an image from a numpy array is significantly faster than before. By faster, I mean the 3840x2160 image took minutes, but now takes 0.0117 seconds.
To create an array that can be converted to an image:
import numpy
pixels = numpy.zeros((height, width, 3), dtype=numpy.uint8)
Here, I run calculations on the GPU to create an image with the pixels array. To efficiently convert an array of pixels into an image:
from PIL import Image
image = Image.fromarray(pixels)

How to generate a mask using Pillow's Image.load() function

I want to create a mask based on certain pixel values. For example: every pixel where B > 200
The Image.load() method seems to be exactly what I need for identifying the pixels with these values, but I can't seem to figure out how to take all these pixels and create a mask image out of them.
R, G, B = 0, 1, 2
pixels = self.input_image.get_value().load()
width, height = self.input_image.get_value().size
for y in range(0, height):
for x in range(0, width):
if pixels[x, y][B] > 200:
print("%s - %s's blue is more than 200" % (x, y))
``
I meant for you to avoid for loops and just use Numpy. So, starting with this image:
from PIL import Image
import numpy as np
# Open image
im = Image.open('colorwheel.png')
# Make Numpy array
ni = np.array(im)
# Mask pixels where Blue > 200
blues = ni[:,:,2]>200
# Save logical mask as PNG
Image.fromarray((blues*255).astype(np.uint8)).save('result.png')
If you want to make the masked pixels black, use:
ni[blues] = 0
Image.fromarray(ni).save('result.png')
You can make more complex, compound tests against ranges like this:
#!/usr/bin/env python3
from PIL import Image
import numpy as np
# Open image
im = Image.open('colorwheel.png')
# Make Numpy array
ni = np.array(im)
# Mask pixels where 100 < Blue < 200
blues = ( ni[:,:,2]>100 ) & (ni[:,:,2]<200)
# Save logical mask as PNG
Image.fromarray((blues*255).astype(np.uint8)).save('result.png')
You can also make a condition on Reds, Greens and Blues and then use Numpy's np.logical_and() and np.logical_or() to make compound conditions, e.g.:
bluesHi = ni[:,:,2] > 200
redsLo = ni[:,:,0] < 50
mask = np.logical_and(bluesHi,redsLo)
Thanks to the reply from Mark Setchell, I solved by making a numpy array the same size as my image filled with zeroes. Then for every pixel where B > 200, I set the corresponding value in the array to 255. Finally I converted the numpy array to a PIL image in the same mode as my input image was.
R, G, B = 0, 1, 2
pixels = self.input_image.get_value().load()
width, height = self.input_image.get_value().size
mode = self.input_image.get_value().mode
mask = np.zeros((height, width))
for y in range(0, height):
for x in range(0, width):
if pixels[x, y][2] > 200:
mask[y][x] = 255
mask_image = Image.fromarray(mask).convert(mode)

Axis out of bounds error in Python using PIL and OpenCV 2

I wanted to change all the pixels in an image to a grey color (r = g = b = 128) if they are in a certain threshold (if the value is between 50 and 150 change it). I imported the image and when i try to process the image it gives me the following error : IndexError: index 3474 is out of bounds for axis 0 with size 3474 (the image is 3474x4632).
Here's the code:
from PIL import Image
import numpy as np
image = Image.open("texture.jpg")
w, h = image.size
print ("%d %d" % (w, h)) #to be sure what the width and height are
im = np.array(image)
for x in range(0, w):
for y in range(0, h):
if (im[x][y][0] <= 150 and im[x][y][0] >= 50):
im[x][y][0] = 128
im[x][y][1] = 128
im[x][y][2] = 128
cv2.imwrite("image2.jpg", im)
And here's the image i'm trying to convert: https://ibb.co/hnjq4p (too large to upload here). Any ideas about why it doesn't work ?
I believe that numpy reverses the axis order from PIL. Actually the first index is rows. So you should loop through w,h = im.shape or h,w = image.size instead. Maybe you can verify that this is correct by comparing image.size and im.shape?
That said, it will be much better if you do not loop. You can use masking and broadcasting to achieve the for loop task like this:
im[(im[...,0]<=150)&(im[...,0]>=50)] = 128 # will modify im in place
This will be much faster especially on large images like this.
Note that this only checks the first channel of the image to be between 150 and 50. This is what your for loop says so I guess it's what you want.
Please check im.shape: you should index your pixels as im[y,x] after converting to a numpy.array.

Categories