Is there way to invert only specific pixels with Pillow? - python

I am making a program where you can chose an image and a color and it will invert only the pixels that match that color. I've been surfing stackoverflow and reddit for solutions but so far no luck.
I tried to do something like this first:
img = Image.open('past.png')
pixels = img.load()
for i in goodpixels:
ImageOps.invert(pixels[i])
AttributeError: 'tuple' object has no attribute 'mode'
No luck with that because ImageOps.invert only inverts full images. Next I tried to use ImageOps.polarize but realized that I couldn't use it because it takes greyscale thresholds not rgb values.
img = Image.open('past.png')
pixels = img.load()
for i in goodpixels:
ImageOps.solarize(img, threshold=pixels[i])
TypeError: '<' not supported between instances of 'int' and 'tuple'
This is my issue, I don't know if this is even possible. If it takes too much work I will probably abandon the project anyways because I'm just keeping myself occupied, and this isn't for marks/job.
Some more code:
def checkpixels():
img = Image.open('past.png')
height, width = img.size
img = img.convert('RGB')
targetcolor = input('What color do you want to search for, you can use RGB format or common names like \'red\', \'black\', e.t.c. Leave this blank to invert all pixels. ')
print('Processing image. This could take several minutes.')
isrgb = re.match(r'\d+, \d+, \d+|\(\d+, \d+, \d+\)', targetcolor)
if type(isrgb) == re.Match:
targetcolor = targetcolor.strip('()')
targetcolor = targetcolor.split(', ')
targetcolor = tuple(map(int, targetcolor))
print(str(targetcolor))
for x in range(width):
for y in range(height):
color = img.getpixel((y-1, x-1))
if color == targetcolor:
goodpixels.append((y-1, x-1))
else:
try:
targetcolor = ImageColor.getcolor(targetcolor.lower(), 'RGB')
print(targetcolor)
for x in range(width):
for y in range(height):
color = img.getpixel((y-1, x-1))
if color == targetcolor:
goodpixels.append((y-1, x-1))
except:
print('Not a valid color smh.')
return goodpixels
goodpixels = []
goodpixels = checkpixels()
Edit: I figured it out! Thank you to Mark Setchell for his incredible brain! I ended up using numpy to convert the image and target color to arrays, making an inverted copy of the image, and using numpy.where() to decide whether or not to switch out the pixels. I also plan on making the target color a range so the chosen color doesn't have to be so specific. All in all my code looks like this:
goodpixels = []
targetcolor = inputcolor()
img = Image.open('past.png')
invertimage = img.copy().convert('RGB')
invertimage = ImageOps.invert(invertimage)
invertimage.save('invert.png')
pastarray = np.array(img)
targetcolorarray = np.array(targetcolor)
pixels = img.load()
inverse = np.array(invertimage)
result = np.where((pastarray==targetcolorarray).all(axis=-1)[...,None], inverse, pastarray)
Image.fromarray(result.astype(np.uint8)).save('result.png')
Of course, inputcolor() is a function offscreen which just decides if the input is a color name or rgb value. Also I used import numpy as np in this example.
A problem that I had was that original my .where method looked like this:
result = np.where((pastarray==[0, 0, 0]).all(axis=-1)[...,None], inverse, pastarray)
This brought up the error: AttributeError: 'bool' object has no attribute 'all'
It turns out all I had to do was convert my color into an array!

Many libraries allow you to import an image into Python as a numpy array. PIL or opencv2 are well documented libraries for working with images:
pip install opencv2
Example numpy.where() selection, meeting a set criteria, in this case inverting all pixel values below THRESHOLD:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# cut off thereshold
THRESHOLD = 230
pixel_data = cv2.imread('filename.png')
pixel_data = np.where(pixel_data < THRESHOLD, 1/pixel_data, pixel_data)
# display the edited image using matplotlib
plt.imshow(pixel_data)
The numpy.where() function applies a condition to your numpy array. More details available on the: numpy official documentation

Related

Python If Value of pixel in an image is then print

I have a small image that I would like to find all RGB values that are the same and then compare that against my variable and if the match print match or similar.
Below is a little snippet that I have put together from other sources.
I am able to print that I found the colour but it will print a line for every time that value is found in the image.
Is there a better way to search an image and match that to a single RGB value? Then if found do a thing.
import cv2
path = '3.png'
blue = int(224)
green = int(96)
red = int(32)
img = cv2.imread(path)
x,y,z = img.shape
for i in range(x):
for j in range(y):
if img[i,j,0]==blue & img[i,j,1]==green & img[i,j,1]==red:
print("Found colour at ",i,j)
General Python advice: do not say blue = int(224). Just say blue = 224
Your program expects to find those exact values. In any kind of photo, nothing is exact. You need to find ranges of values.
cv.imread returns data in BGR order. Be aware of that if you access individual values in the numpy array.
Use this:
import numpy as np
import cv2 as cv
img = cv.imread("3.png")
lower_bound = (224-20, 96-20, 32-20) # BGR
upper_bound = (224+20, 96+20, 32+20) # BGR
mask = cv.inRange(img, lower_bound, upper_bound)
count = cv.countNonZero(mask)
print(f"picture contains {count} pixels of that color")
And if you need to know where pixels of that color are, please explain what you need that for. A list of those points is generally useless. There are more useful ways to get these locations but they depend on why you need this information, what for.
I think this may help you with small touching :)
import cv2
import numpy as np
r = int(255)
g = int(255)
b = int(255)
img = 255*np.ones((5,5,3), np.uint8)
[rows, cols] = img.shape[:2]
print(img.shape[:2])
print(img.shape)
for i in range(rows):
for j in range(cols):
if img[i, j][0] == r and img[i, j][1] == g and img[i, j][2] == b:
print(img[i, j])
print(i, j)

Replace all pixels of multiple RGB values in OpenCV

I'm pretty new to image processing and python so bear with me
I'm trying to take a big image (5632x2048) which is basically a map of the world with provinces (ripped from Hearts of Iron 4), and each province is colored a different RGB value, and color it with a set of colors, each corresponding to a certain country. I'm currently using this code
import numpy as np
import cv2
import sqlite3
dbPath = 'PATH TO DB'
dirPath = 'PATH TO IMAGE'
con = sqlite3.connect(dbPath)
cur = con.cursor()
im = cv2.imread(dirPath)
cur.execute('SELECT * FROM Provinces ORDER BY id')
provinceTable = cur.fetchall()
for line in provinceTable:
input_rgb = [line[1], line[2], line[3]]
if line[7] == None:
output_rgb = [255,255,255]
else:
output_rgb = line[7].replace('[', '').replace(']','').split(',')
im[np.all(im == (int(input_rgb[0]), int(input_rgb[1]), int(input_rgb[2])), axis=-1)] = (int(output_rgb[0]), int(output_rgb[1]), int(output_rgb[2]))
cv2.imwrite('result.png',im)
The problem I'm running into is that it's painfully slow (50 minutes in and it hasn't finished), due to the fact I'm definitely using numpy wrong by looping through it instead of vectorizing (a concept I'm still new to and have no idea how to do). Google hasn't been very helpful either.
What's the best way to do this?
Edit: forgot to mention that the amount of values I'm replacing is pretty big (~15000)
As I mentioned in the comments, I think you'll want to use np.take(yourImage, LUT) where LUT is a Lookup Table.
So, if you make a dummy image the same shape as yours:
import numpy as np
# Make a dummy image of 5632x2048 RGB values
im = np.random.randint(0,256,(5632,2048,3), np.uint8)
that will be 34MB. Now reshape it to a tall vector of RGB values:
# Make image into a tall vector, as tall as necessary and 3 RGB values wide
v = im.reshape((-1,3))
which will be of shape (11534336, 3) and then flatten that to 24-bit values rather than three 8-bit values with np.dot()
# Make into tall vector of shape 11534336x1 rather than 11534336x3
v24 = np.dot(v.astype(np.uint32),[1,256,65536])
You will now have a 1-D vector of 24-bit pixel values with shape (11534336,)
Now create your RGB lookup table (I am making all 2^24 RGB entries here, you may need less):
RGBLUT = np.zeros((2**24,3),np.uint8)
And set up the LUT. So, supposing you want to map all colours in the original image to mid-grey (128) in the output image:
RGBLUT[:] = 128
Now do the np.dot() thing just the same as we did with the image so we get a LUT with shape (224,1) rather than shape (224,3):
LUT24 = np.dot(RGBLUT.astype(np.uint32), [1,256,65536])
Then do the actual lookup in the table:
result = np.take(LUT24, v24)
On my Mac, that take 334ms for your 5632x2048 image.
Then reshape and convert back to three 8-bit values by shifting and ANDing to undo effect of np.dot().
I am not currently in a position to test the re-assembly, but it will look pretty much like this:
BlueChannel = result & 0xff # Blue channel is bottom 8 bits
GreenChannel = (result>>8) &0 xff # Green channel is middle 8 bits
RedChannel = (result>>16) &0 xff # Red channel is top 8 bits
Now combine those three single channels into a 3-channel image:
RGB = np.dstack(RedChannel, GreenChannel, BlueChannel))
And reshape back from tall vector to dimensions of original image:
RGB = RGB.reshape(im.shape)
As regards setting up the LUT, to something more interesting than mid-grey, if you want to map say orange, i.e. rgb(255,128,0) to magenta, i.e. rgb(255,0,255) you would do something along the lines of:
LUT[np.dot([255,128,0],[1,256,65536])] = [255,0,255] # map orange to magenta
LUT[np.dot([255,255,255],[1,256,65536])] = [0,0,0] # map white to black
LUT[np.dot([0,0,0],[1,256,65536])] = [255,255,255] # map black to white
Keywords: Python, image processing, LUT, RGB LUT 24-bit LUT, lookup table.
Here is one way to do that using Numpy and Python/OpenCV. Here I change red to green.
Input:
import cv2
import numpy as np
# load image
img = cv2.imread('test_red.png')
# change color
result = img.copy()
result[np.where((result==[0,0,255]).all(axis=2))] = [0,255,0]
# save output
cv2.imwrite('test_green.png', result)
# Display various images to see the steps
cv2.imshow('result',result)
cv2.waitKey(0)
cv2.destroyAllWindows()
Result:
You can create a mask of the image first and use that to replace the colors. There's likely a pure numpy way of doing this that is faster, but I don't know it.
This code takes ~0.5 seconds to run. You should expect it to take about half a second for each color replacement.
import cv2
import numpy as np
import time
# make image
res = (5632, 2048, 3);
img = np.zeros(res, np.uint8);
# change black to white
black = (0,0,0);
white = (255,255,255);
# make a mask
start_time = time.time();
mask = cv2.inRange(img, black, black);
print("Mask Time: " + str(time.time() - start_time));
# replace color
start_time = time.time();
img[mask == 255] = white;
print("Replace Time: " + str(time.time() - start_time));
In terms of your code it'll look like this
for line in provinceTable:
input_rgb = [line[1], line[2], line[3]]
input_rgb = (int(input_rgb[0]), int(input_rgb[1]), int(input_rgb[2]))
if line[7] == None:
output_rgb = (255,255,255)
else:
output_rgb = line[7].replace('[', '').replace(']','').split(',')
output_rgb = (int(output_rgb[0]), int(output_rgb[1]), int(output_rgb[2]))
mask = cv2.inRange(im, input_rgb, input_rgb)
im[mask == 255] = output_rgb

Array to Image conversion

I've been struggling for hours now with this tiny problem.
I've been trying to do some image modification. Here is the code snipet :
import numpy as np
from PIL import Image
#Conversion image array
img = Image.open('lena.jpg')
array = np.array(img)
def niv_de_gris(img):
height = len(img)
width = len(img[0])
#Creation tableau vide
new_img = ([[[0 for i in range(3)] for j in range(width)] for k in range(height)])
for i in range(height):
for j in range(width):
m = np.mean(img[i][j])
for k in range(3):
new_img[i][j][k] = int(m)
return np.array(new_img)
array_gris = niv_de_gris(array)
img_gris = Image.fromarray(array_gris) #problem is here !!
the first conversion works perfectly fine : it takes an image an converts it into an array. The program runs flowlessly, the image modification works, it sends me back an array of the image converted in gray levels.
Yet when I want to convert this array into an image to .show() it, i get this error :
Error screenshot
Can anybody help me figure this out pls?
Have a nice day!
Array to PIL may need to ensure pixel values are not 0-1 but 0-255, type is unit8 and then define the mode as 'RGB'.
Try using -
array_gris = niv_de_gris(array) * 255 #Skip this step if pixels are 0-255 already
array_gris = array_gris.astype(np.uint8)
img_gris = Image.fromarray(array_gris, mode='RGB')
That worked for me on a random image that I choose. Depending on the function and the image, I believe the only real important step is to ensure they are legit pixel values and that type is uint8.

Why am I getting a NoneType?

Can anyone see where I'm going wrong? I've been through it line by line and it all produces expected results up until new_face, when new_face starts producing NoneType's.
import numpy, cv2
from PIL import Image
face_cascade = cv2.CascadeClassifier("..data/haarcascade_frontalface_default.xml") #absolute path cut down for privacy
def find_faces(image_for_faces):
image = image_for_faces.resize((1800,3150))
image_np = numpy.asarray(image)
image_gray = cv2.cvtColor(image_np, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(image_gray) #correct numpy array, prints correct coordinates per face
face_list = []
image.show()
for face in faces:
print(face)
box = (face[0], face[1], face[0]+face[2], face[1]+face[3]) #correctly produces numpy coordinates
copy_image = image.copy()
cropped = copy_image.crop(box = (box))
new_face = cropped.thumbnail((128,128))
face_list.append(new_face)
return face_list
y = Image.open('famphoto.jpg')
z = y.convert('RGB')
x = find_faces(z)
The Image.thumbnail() modifies the Image object in place, read more about it in the docs. It mentions that it returns a None type.
Image.thumbnail:
Make this image into a thumbnail. This method modifies the image to
contain a thumbnail version of itself, no larger than the given size.
This method calculates an appropriate thumbnail size to preserve the
aspect of the image, calls the draft() method to configure the file
reader (where applicable), and finally resizes the image.

coordinate location based off pixel color

I am trying to get specific coordinates in an image. I have marked a red dot in the image at several locations to specify the coordinates I want to get. In GIMP I used the purist red I could find (HTML notation ff000). The idea was that I would iterate through the image until I found a pure shade of red and then print out the coordinates. I am using python and opencv to do so but I can't find any good tutorials (best I could find is this but it's not very clear...at least for me). Here is an example of the image I am dealing with.
I just want to know how to find the coordinates of the pixels with the red dots.
EDIT (added code):
import cv2
import numpy as np
img = cv2.imread('image.jpg')
width, height = img.shape[:2]
for i in range(0,width):
for j in range(0,height):
px = img[i,j]
I don't know what to do from here. I have tried code such as if px == [x,y,z] looking for color detection but that doesn't work.
You can do it with cv2 this way:
image = cv2.imread('image.jpg')
lower_red = np.array([0,0,220]) # BGR-code of your lowest red
upper_red = np.array([10,10,255]) # BGR-code of your highest red
mask = cv2.inRange(image, lower_red, upper_red)
#get all non zero values
coord=cv2.findNonZero(mask)
You can do this with PIL and numpy. I'm sure there is a similar implementation with cv2.
from PIL import Image
import numpy as np
img = Image.open('image.png')
width, height = img.size[:2]
px = np.array(img)
for i in range(height):
for j in range(width):
if(px[i,j,0] == 255 & px[i,j,1] == 0 & px[i,j,2] == 0):
print(i,j,px[i,j])
This doesn't work with the image you provided, since there aren't any pixels that are exactly (255,0,0). Something may have changed when it got compressed to a .jpg, or you didn't make them as red as you thought you did. Perhaps you could try turning off anti-aliasing in GIMP.

Categories

Resources