I have images of the stanrd 52 game cards. Some of them are black and some are red. A neural network has been trained on it to recognize them correctly. Now it turns out that sometimes green is used instead of red. That's why I want to convert all images that are green(ish) to red(ish). If they are blackish or redish they should not be changed much or at all if possible.
What's the best way to achieve this?
The easiest way to do this, IMHO, is to split the image into its constituent R, G and B channels and then recombine them in the "wrong" order:
#!/usr/bin/env python3
from PIL import Image
# Open image
im = Image.open('cards.jpg')
# Split into component channels
R, G, B = im.split()
# Recombine, but swapping order of red and green
result = Image.merge('RGB',[G,R,B])
# Save result
result.save('result.jpg')
Alternatively, you can do the same thing via a colour matrix multiplication:
#!/usr/bin/env python3
from PIL import Image
# Open image
im = Image.open('cards.jpg')
# Define color matrix to swap the green and red channels
# This says:
# New red = 0*old red + 1*old green + 0*old blue + 0offset
# New green = 1*old red + 0*old green + 0*old blue + 0offset
# New blue = 1*old red + 0*old green + 1*old blue + 0offset
Matrix = ( 0, 1, 0, 0,
1, 0, 0, 0,
0, 0, 1, 0)
# Apply matrix
result = im.convert("RGB", Matrix)
result.save('result.jpg')
Alternatively, you could use ImageMagick in Terminal to separate the image into its constituent R,G and B channels, swap the Red and Green channels and recombine like this:
magick cards.jpg -separate -swap 0,1 -combine result.png
Alternatively, you could use ImageMagick to perform a "hue rotation". Basically, you convert the image to HSL colourspace and rotate the hue leaving the saturation and lightness unaffected. That gives you the flexibility to make almost any colour you desire. You can do that in the Terminal with:
magick cards.jpg -modulate 100,100,200 result.jpg
where the 200 above is the interesting parameter - see documentation here. Here is an animation of the various possibilities:
If still using v6 ImageMagick, replace magick with convert in all commands.
Keywords: Python, image processing, hue rotation, channel swap, cards, prime, swap channels, PIL, Pillow, ImageMagick.
Here's one approach using a tolerance value to decide on the -ish factor to set a mathematical notation to it -
def set_image(a, tol=100): #tol - tolerance to decides on the "-ish" factor
# define colors to be worked upon
colors = np.array([[255,0,0],[0,255,0],[0,0,0],[255,255,255]])
# Mask of all elements that are closest to one of the colors
mask0 = np.isclose(a, colors[:,None,None,:], atol=tol).all(-1)
# Select the valid elements for edit. Sets all nearish colors to exact ones
out = np.where(mask0.any(0)[...,None], colors[mask0.argmax(0)], a)
# Finally set all green to red
out[(out == colors[1]).all(-1)] = colors[0]
return out.astype(np.uint8)
A more memory-efficient approach would be to loop through those selective colors, like so -
def set_image_v2(a, tol=100): #tol - tolerance to decides on the "-ish" factor
# define colors to be worked upon
colors = np.array([[255,0,0],[0,255,0],[0,0,0],[255,255,255]])
out = a.copy()
for c in colors:
out[np.isclose(out, c, atol=tol).all(-1)] = c
# Finally set all green to red
out[(out == colors[1]).all(-1)] = colors[0]
return out
Sample run -
Input image :
from PIL import Image
img = Image.open('green.png').convert('RGB')
x = np.array(img)
y = set_image(x)
z = Image.fromarray(y, 'RGB')
z.save("tmp.png")
Output -
Related
for y in range(image.getHeight()):
for x in range(image.getWidth()):
def GetDistance(tuple1,tuple2):
tuple1=(r,g,b)
tuple2=(r2,g2,b2)
import math
math.sqrt(((r-r2)**2)+((g-g2)**2)+((b-b2)**2))
return
I am trying to compare to pixel colors in an image. For the colors that are the closest to red I want to change to blue, the closest to blue I want to change to green, and the color in the image that are closest to green I want to change to red. How can I get the color pixels I am looking for in the image?
Personally, I wouldn't approach this using for loops as they are slow, inefficient and error-prone in Python. However, I have implemented it with for loops so it matches your way of thinking and is easier to understand. If you look through some of my other answers you will find better ways of doing it.
I note a couple of things:
you don't need to take the square root of the distances, you can simply compare the squared distances. If a**2 > b**2, it follows a>b, as long as there are no negatives involved which will always be the case here because both a and b are themselves the sums of squares
as your output image will consist of just 3 colours, it can best be stored as a palette image - see here. So I did that.
#!/usr/bin/env python3
from PIL import Image
def closestColour(px):
"""Returns palette index for whichever colour (out of R, G and B) is nearest"""
# Unpack the tuple into its constituent parts
R, G, B = thispx
# Calculate distance to solid Red, solid Green and solid Blue corners of colour cube
# No need for sqrt(), if a**2 < b**2 we know a>b as long as no negative numbers are involved and there aren't because a amd b are sums of squares
RdistSquared = (255-R)**2 + G**2 + B**2
GdistSquared = R**2 + (255-G)**2 + B**2
BdistSquared = R**2 + G**2 + (255-B)**2
# Find index of minimum value in list
values = [RdistSquared, GdistSquared, BdistSquared]
mi = values.index(min(values))
# Map red to blue, blue to green and green to red
lkup = [2, 0, 1]
return lkup[mi]
# Open input image and load pixels
im = Image.open('billiards.jpg')
px = im.load()
# List of output pixels
outpx = []
for y in range(im.height):
for x in range(im.width):
thispx = px[x,y]
outpx.append(closestColour(thispx))
# Create palettised output image, same size as original
out = Image.new('P', im.size)
# Stuff in the list of pixels
out.putdata(outpx)
# Stuff in the palette, index 0 = Red, index 1 = Green, index 2 = Blue
out.putpalette([255,0,0, 0,255,0, 0,0,255])
out.save('result.png')
That transforms this:
Photo by Sandesh Sharma on Unsplash
into this:
By the way, there is no real need to write any Python to do this, you can just do it in the Terminal with ImageMagick, like this:
# Make a small palette of the colours we want to remap to
magick xc:red xc:lime xc:blue +append PNG8:rgb-palette.png
# Remap to the new palette, without dithering and then re-order colour channels
magick billiards.jpg +dither -remap rgb-palette.png -channel-fx ',1,2,0' result.png
I have two functions.
The first one changes the opacity of all black pixels in the overlay image to 0.
The second image overlays onto another one with changing opacity. But only for all pixels that are not black.
Overall I want to overlay images with different opacities and prevent that the black background does not show up.
It works just fine, but not really fast (both avg > 2sec). Is there a way to speed up the process a little? Numpy for example?
Here is what I have so far:
def remove_background(image):
im = image.convert('RGBA')
w, h = im.size
for i in range(w):
for j in range(h):
cordinate = i, j
rgb_map = im.getpixel(cordinate)
if rgb_map == (0, 0, 0, 255):
im.putpixel(cordinate, (0, 0, 0, 0))
return im
def overlay_image(background, overlay, opacity=0.5):
background = background.convert('RGBA')
overlay = overlay.convert('RGBA')
opacity = int(opacity * 255)
w, h = background.size
for i in range(w):
for j in range(h):
cordinate = i, j
rgb_map = overlay.getpixel(cordinate)
r = rgb_map[0]
g = rgb_map[1]
b = rgb_map[2]
if r == 64 and g == 128 and b == 19:
overlay.putpixel(cordinate, (64, 128, 19, opacity))
background.paste(overlay, (0,0), overlay)
return background
I know there is something from PIL called Image.blend(), but this is just for the whole image and not for certain pixels.
Here is an example I want to overlay:
animal
mask
result
Updated Answer
I think this is what your updated question is looking for:
#!/usr/bin/env python3
from PIL import Image
# Open the mask, discarding any alpha channel
mask = Image.open('mask.png').convert('RGB')
# Generate a new alpha channel that is transparent wherever the mask is black
alpha = mask.copy()
alpha = alpha.convert('L')
alpha.point(lambda p: 128 if p > 0 else 0)
# Open the animal, discarding its pointless alpha channel
animal = Image.open('animal.png').convert('RGB')
# Paste the mask over the animal, respecting transparency
animal.paste(mask,mask=alpha)
animal.save('result.png')
Alternative
As an alternative, you could simply add the mask to the animal. Where the mask is black (i.e. zero) it will add nothing:
#!/usr/bin/env python3
from PIL import Image, ImageChops
# Open the mask, discarding any alpha channel
mask = Image.open('mask.png').convert('RGB')
# Open the animal, discarding any alpha channel
animal = Image.open('animal.png').convert('RGB')
# Add the mask to the animal
res = ImageChops.add(animal, mask)
res.save('result.png')
You could scale the mask first, say by a half, using Image.point() with a lambda like below, if the green is too much.
Original Answer
Mmmm, if you want to mask an input image with another input image and show the results, I think you'd need three images, not two.
So, I am guessing this is what you want and it should be nice and fast:
#!/usr/bin/env python3
from PIL import Image
# Open the mask, discarding its pointless alpha channel and colour information
mask = Image.open('mask.png').convert('L')
# Generate a new alpha channel that is transparent wherever the mask is black
mask.point(lambda p: 255 if p > 0 else 0)
# Open the animal, discarding its pointless alpha channel
animal = Image.open('animal.png').convert('RGB')
# Put our lovely new alpha channel into animal image and save
animal.putalpha(mask)
animal.save('result.png')
mask.png
animal.png
Just for fun, here's another way of doing it leveraging Numpy:
#!/usr/bin/env python3
from PIL import Image
import numpy as np
# Open the overlay and animal, discarding any alpha channel
overlay = Image.open('mask.png').convert('RGB')
animal = Image.open('animal.png').convert('RGB')
mask = overlay.convert('L')
# Make into Numpy arrays
overlay= np.array(overlay)
animal = np.array(animal)
mask = np.array(mask)
# Blend animal and overlay
blended = animal//2 + overlay//2
# Choose blended or original at each location depending on mask
res = np.where(mask[...,np.newaxis], blended, animal)
# Revert to PIL Image and save
Image.fromarray(res).save('result.png')
This question already has answers here:
Finding red color in image using Python & OpenCV
(3 answers)
Closed 10 months ago.
I am trying to make a program where I detect red. However sometimes it is darker than usual so I can't just use one value.
What is a good range for detecting different shades of red?
I am currently using the range 128, 0, 0 - 255, 60, 60 but sometimes it doesn't even detect a red object I put in front of it.
RGBis not a good color space for specific color detection. HSV will be a good choice.
For RED, you can choose the HSV range (0,50,20) ~ (5,255,255) and (175,50,20)~(180,255,255)using the following colormap. Of course, the RED range is not that precise, but it is just ok.
The code taken from my another answer: Detect whether a pixel is red or not
#!/usr/bin/python3
# 2018.07.08 10:39:15 CST
# 2018.07.08 11:09:44 CST
import cv2
import numpy as np
## Read and merge
img = cv2.imread("ColorChecker.png")
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
## Gen lower mask (0-5) and upper mask (175-180) of RED
mask1 = cv2.inRange(img_hsv, (0,50,20), (5,255,255))
mask2 = cv2.inRange(img_hsv, (175,50,20), (180,255,255))
## Merge the mask and crop the red regions
mask = cv2.bitwise_or(mask1, mask2 )
croped = cv2.bitwise_and(img, img, mask=mask)
## Display
cv2.imshow("mask", mask)
cv2.imshow("croped", croped)
cv2.waitKey()
Related answers:
Choosing the correct upper and lower HSV boundaries for color detection with`cv::inRange` (OpenCV)
How to define a threshold value to detect only green colour objects in an image :Opencv
How to detect two different colors using `cv2.inRange` in Python-OpenCV?
Detect whether a pixel is red or not
Of course, for the specific question, maybe other color space is also OK.
How to read utility meter needle with opencv?
You could check that the red component is the maximum and others are both clearly lower:
def red(r, g, b):
threshold = max(r, g, b)
return (
threshold > 8 # stay away from black
and r == threshold # red is biggest component
and g < threshold*0.5 # green is much smaller
and b < threshold*0.5 # so is b
)
This can be implemented very efficiently using numpy.
The "right way" would be doing a full conversion to HSV and check there, but it's going to be slower and somewhat trickier (hue is an angle so you cannot just take the absolute value of the difference, moreover colors like (255, 254, 254) are going to be qualified as "red" even if they're considered white for a human).
Note also that human visual system tends to compensate for average, so something could be seen as "blue" even if indeed the biggest component is red, but everything in the image is red, so that "doesn't count" for our brain.
In the image below if you ask a human what color is the part in the circle area most would say "blue" while indeed the biggest component is red:
Please, use HSV or HSL (hue, saturation, luminance) instead of RGB, in HSV the red color can be easily detected using the value of hue within some threshold.
Red Color means Red value is higher than Blue and Green.
So you can check the differences between Red and Blue, Red and Green.
You can simply split RGB into individual channels and apply threshold like this.
b,g,r = cv2.split(img_rgb)
rg = r - g
rb = r - b
rg = np.clip(rg, 0, 255)
rb = np.clip(rb, 0, 255)
mask1 = cv2.inRange(rg, 50, 255)
mask2 = cv2.inRange(rb, 50, 255)
mask = cv2.bitwise_and(mask1, mask2)
Hope it can be a solution for your problem.
Thank you.
How to split image to RGB colors and why doesn't split() function work?
from PIL import Image
pil_image = Image.fromarray(some_image)
red, green, blue = pil_image.split()
red.show()
Why does red.show() shows image in greyscale instead of red scale?
PS. The same situation using green.show() and blue.show().
I've created a script that takes an RGB image, and creates the pixel data for each band by suppressing the bands we don't want.
RGB to R__ -> red.png
RGB to _G_ -> green.png
RGB to __B -> blue.png
from PIL import Image
img = Image.open('ra.jpg')
data = img.getdata()
# Suppress specific bands (e.g. (255, 120, 65) -> (0, 120, 0) for g)
r = [(d[0], 0, 0) for d in data]
g = [(0, d[1], 0) for d in data]
b = [(0, 0, d[2]) for d in data]
img.putdata(r)
img.save('r.png')
img.putdata(g)
img.save('g.png')
img.putdata(b)
img.save('b.png')
A single channel image will always show as grayscale. If you want it to show in native colours (ie a red "R" channel, blue "B" channel, green "G" channel) you need to concatenate 3 channels and zero the ones you are not interested in. Remember to maintain channel order so that you don’t get a red "G" channel.
Might be easier to simple take 3 copies of the image and zero the irrelevant channels rather than using split.
You can use either OpenCV or Pillow. It's simple in both. I've written a class (Uses Pillow, https://github.com/mujeebishaque/image-splitter) that you can utilize and get all the channels saved in the current directory just by calling a function.
In OpenCV, you'd use the method split() on the image to get RGB or RGBA channels.
I have already taken a look at this question: SO question and seem to have implemented a very similar technique for replacing a single color including the alpha values:
c = Image.open(f)
c = c.convert("RGBA")
w, h = c.size
cnt = 0
for px in c.getdata():
c.putpixel((int(cnt % w), int(cnt / w)), (255, 0, 0, px[3]))
cnt += 1
However, this is very slow. I found this recipe out on the interwebs, but have not had success using it thus far.
What I am trying to do is take various PNG images that consist of a single color, white. Each pixel is 100% white with various alpha values, including alpha = 0. What I want to do is basically colorize the image with a new set color, for instance #ff0000<00-ff>. SO my starting and resulting images would look like this where the left side is my starting image and the right is my ending image (NOTE: background has been changed to a light gray so you can see it since it is actually transparent and you wouldn't be able to see the dots on the left.)
Any better way to do this?
If you have numpy, it provides a much, much faster way to operate on PIL images.
E.g.:
import Image
import numpy as np
im = Image.open('test.png')
im = im.convert('RGBA')
data = np.array(im) # "data" is a height x width x 4 numpy array
red, green, blue, alpha = data.T # Temporarily unpack the bands for readability
# Replace white with red... (leaves alpha values alone...)
white_areas = (red == 255) & (blue == 255) & (green == 255)
data[..., :-1][white_areas.T] = (255, 0, 0) # Transpose back needed
im2 = Image.fromarray(data)
im2.show()
Edit: It's a slow Monday, so I figured I'd add a couple of examples:
Just to show that it's leaving the alpha values alone, here's the results for a version of your example image with a radial gradient applied to the alpha channel:
Original:
Result:
Try this , in this sample we set the color to black if color is not white .
#!/usr/bin/python
from PIL import Image
import sys
img = Image.open(sys.argv[1])
img = img.convert("RGBA")
pixdata = img.load()
# Clean the background noise, if color != white, then set to black.
for y in xrange(img.size[1]):
for x in xrange(img.size[0]):
if pixdata[x, y] == (255, 255, 255, 255):
pixdata[x, y] = (0, 0, 0, 255)
you can use color picker in gimp to absorb the color and see that's rgba color
The Pythonware PIL online book chapter for the Image module stipulates that putpixel() is slow and suggests that it can be sped up by inlining. Or depending on PIL version, using load() instead.