Compare images Python PIL - python

How can I compare two images? I found Python's PIL library, but I do not really understand how it works.

To check does jpg files are exactly the same use pillow library:
from PIL import Image
from PIL import ImageChops
image_one = Image.open(path_one)
image_two = Image.open(path_two)
diff = ImageChops.difference(image_one, image_two)
if diff.getbbox():
print("images are different")
else:
print("images are the same")

based on Victor Ilyenko's answer, I needed to convert the images to RGB as PIL fails silently when the .png you're trying to compare contains a alpha channel.
image_one = Image.open(path_one).convert('RGB')
image_two = Image.open(path_two).convert('RGB')

It appears that Viktor's implementation can fail when the images are different sizes.
This version also compares alpha values. Visually identical pixels are (I think) treated as identical, such as (0, 0, 0, 0) and (0, 255, 0, 0).
from PIL import ImageChops
def are_images_equal(img1, img2):
equal_size = img1.height == img2.height and img1.width == img2.width
if img1.mode == img2.mode == "RGBA":
img1_alphas = [pixel[3] for pixel in img1.getdata()]
img2_alphas = [pixel[3] for pixel in img2.getdata()]
equal_alphas = img1_alphas == img2_alphas
else:
equal_alphas = True
equal_content = not ImageChops.difference(
img1.convert("RGB"), img2.convert("RGB")
).getbbox()
return equal_size and equal_alphas and equal_content

Another implementation of Viktor's answer using Pillow:
from PIL import Image
im1 = Image.open('image1.jpg')
im2 = Image.open('image2.jpg')
if list(im1.getdata()) == list(im2.getdata()):
print("Identical")
else:
print ("Different")
Note:
This might not work correctly if the images have an alpha channel, or are using different modes.

That did work for me, you just have to set the quantity of different pixels you accept, in my case 100, as the image diference was a complete black image but still had 25 diferent pixels. I tested other completelly diffferent images and they had diferrent pixels by thousands:
from PIL import ImageChops
if len(set(ImageChops.difference(img1, img2).getdata())) > 100:
print("Both images are diffent!")

You can use ImageChops.difference in combination with getbbox:
from PIL import Image
from PIL import ImageChops
def are_equal(image_a: Image, image_b: Image) -> bool:
diff = ImageChops.difference(image_a, image_b)
channels = diff.split()
for channel in channels:
if channel.getbbox() is not None:
return False
return True
The reason why we need to split the channels is because of the (possible presence of an) alpha channel. If you have two images that have exactly the same alpha channel (e.g. by both being fully opaque), then ImageChops.difference will produce an image whose alpha channel is zero all over. This means that for the pixel values of the image, we ignore the other channels because we know they final pixel is just black. Thus, the bbox of the difference of two fully opaque images is None, which is not what we want.

Related

How set white color background when concatenating images via python?

import numpy as np
from imageio import imread, imwrite
im1 = imread('https://api.sofascore.app/api/v1/team/2697/image')[...,:3]
im2 = imread('https://api.sofascore.app/api/v1/team/2692/image')[...,:3]
result = np.hstack((im1,im2))
imwrite('result.jpg', result)
Original images opening directly from the url's (I'm trying to concatenate the two images into one and keep the background white):
As can be seen both have no background, but when joining the two via Python, the defined background becomes this moss green:
I tried modifying the color reception:
im1 = imread('https://api.sofascore.app/api/v1/team/2697/image')[...,:1]
im2 = imread('https://api.sofascore.app/api/v1/team/2692/image')[...,:1]
But the result is a Black & White with the background still looking like it was converted from the previous green, even though the PNG's don't have such a background color.
How should I proceed to solve my need?
There is a 4th channel in your images - transparency. You are discarding that channel with [...,:1]. This is a mistake.
If you retain the alpha channel this will work fine:
import numpy as np
from imageio import imread, imwrite
im1 = imread('https://api.sofascore.app/api/v1/team/2697/image')
im2 = imread('https://api.sofascore.app/api/v1/team/2692/image')
result = np.hstack((im1,im2))
imwrite('result.png', result)
However, if you try to make a jpg, you will have a problem:
>>> imwrite('test.jpg', result)
OSError: JPEG does not support alpha channel.
This is correct, as JPGs do not do transparency. If you would like to use transparency and also have your output be a JPG, I suggest a priest.
You can replace the transparent pixels by using np.where and looking for places that the alpha channel is 0:
result = np.hstack((im1,im2))
result[np.where(result[...,3] == 0)] = [255, 255, 255, 255]
imwrite('result.png', result)
If you want to improve image quality, here is a solution. #Brondy
# External libraries used for
# Image IO
from PIL import Image
# Morphological filtering
from skimage.morphology import opening
from skimage.morphology import disk
# Data handling
import numpy as np
# Connected component filtering
import cv2
black = 0
white = 255
threshold = 160
# Open input image in grayscale mode and get its pixels.
img = Image.open("image.jpg").convert("LA")
pixels = np.array(img)[:,:,0]
# Remove pixels above threshold
pixels[pixels > threshold] = white
pixels[pixels < threshold] = black
# Morphological opening
blobSize = 1 # Select the maximum radius of the blobs you would like to remove
structureElement = disk(blobSize) # you can define different shapes, here we take a disk shape
# We need to invert the image such that black is background and white foreground to perform the opening
pixels = np.invert(opening(np.invert(pixels), structureElement))
# Create and save new image.
newImg = Image.fromarray(pixels).convert('RGB')
newImg.save("newImage1.PNG")
# Find the connected components (black objects in your image)
# Because the function searches for white connected components on a black background, we need to invert the image
nb_components, output, stats, centroids = cv2.connectedComponentsWithStats(np.invert(pixels), connectivity=8)
# For every connected component in your image, you can obtain the number of pixels from the stats variable in the last
# column. We remove the first entry from sizes, because this is the entry of the background connected component
sizes = stats[1:,-1]
nb_components -= 1
# Define the minimum size (number of pixels) a component should consist of
minimum_size = 100
# Create a new image
newPixels = np.ones(pixels.shape)*255
# Iterate over all components in the image, only keep the components larger than minimum size
for i in range(1, nb_components):
if sizes[i] > minimum_size:
newPixels[output == i+1] = 0
# Create and save new image.
newImg = Image.fromarray(newPixels).convert('RGB')
newImg.save("new_img.PNG")
If you want to change the background of a Image, pixellib is the best solution because it seemed the most reasonable and easy library to use.
import pixellib
from pixellib.tune_bg import alter_bg
change_bg = alter_bg()
change_bg.load_pascalvoc_model("deeplabv3_xception_tf_dim_ordering_tf_kernels.h5")
change_bg.color_bg("sample.png", colors=(255,255,255), output_image_name="colored_bg.png")
This code requires pixellib to be higher or the same as 0.6.1

Find If Image Is Bright Or Dark

I would like to know how to write a function in Python 3 using OpenCV which takes in an image and a threshold and returns either 'dark' or 'light' after heavily blurring it and reducing quality (faster the better). This might sound vague , but anything that just works will do.
You could try this :
import imageio
import numpy as np
f = imageio.imread(filename, as_gray=True)
def img_estim(img, thrshld):
is_light = np.mean(img) > thrshld
return 'light' if is_light else 'dark'
print(img_estim(f, 127))
Personally, I would not bother writing any Python, or loading up OpenCV for such a simple operation. If you absolutely have to use Python, please just disregard this answer and select a different one.
You can just use ImageMagick at the command-line in your Terminal to get the mean brightness of an image as a percentage, where 100 means "fully white" and 0 means "fully black", like this:
convert someImage.jpg -format "%[fx:int(mean*100)]" info:
Alternatively, you can use libvips which is less common, but very fast and very lightweight:
vips avg someImage.png
The vips answer is on a scale of 0..255 for 8-bit images.
Note that both these methods will work for many image types, from PNG, through GIF, JPEG and TIFF.
However, if you really want Python/OpenCV code, I note none of the existing answers do that - some use a different library, some are incomplete, some do superfluous blurring and some read video cameras for some unknown reason, and none handle more than one image or errors. So, here's what you actually asked for:
#!/usr/bin/env python3
import cv2
import sys
import numpy as np
# Iterate over all arguments supplied
for filename in sys.argv[1:]:
# Load image as greyscale
im = cv2.imread(filename, cv2.IMREAD_GRAYSCALE)
if im is None:
print(f'ERROR: Unable to load {filename}')
continue
# Calculate mean brightness as percentage
meanpercent = np.mean(im) * 100 / 255
classification = "dark" if meanpercent < 50 else "light"
print(f'{filename}: {classification} ({meanpercent:.1f}%)')
Sample Output
OpenCVBrightOrDark.py g*png nonexistant
g30.png: dark (30.2%)
g80.png: light (80.0%)
ERROR: Unable to load nonexistant
You could try this, considering image is a grayscale image -
blur = cv2.blur(image, (5, 5)) # With kernel size depending upon image size
if cv2.mean(blur) > 127: # The range for a pixel's value in grayscale is (0-255), 127 lies midway
return 'light' # (127 - 255) denotes light image
else:
return 'dark' # (0 - 127) denotes dark image
Refer to these -
Smoothing, Mean, Thresholding
import numpy as np
import cv2
def algo_findDark(image):
blur = cv2.blur(image, (5, 5))
mean = np.mean(blur)
if mean > 85:
return 'light'
else:
return 'dark'
cam = cv2.VideoCapture(0)
while True:
check, frame = cam.read()
frame_gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
ans = algo_findDark(frame_gray)
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(frame, ans, (10, 450), font, 3, (0, 0, 255), 2, cv2.LINE_AA)
cv2.imshow('video', frame)
key = cv2.waitKey(1)
if key == 27:
break
cam.release()
cv2.destroyAllWindows()

Python invert binary image; .invert() fails

I have a binary image, dimensions 64x63 where every pixel is a 1 or 0.
import struct
from PIL import Image
import numpy
...
i1 = Image.frombytes('1', (64, 63), r[3], 'raw')
How can I invert this image?
Edit
I attempted the suggested solution:
from PIL import Image
import PIL.ImageOps
i1 = PIL.ImageOps.invert(i1)
However, this resulted in the error:
raise IOError("not supported for this image mode")
IOError: not supported for this image mode
And this, I believe, is due to the fact that the image is neither RGB nor L(greyscale). Instead, it is a binary image file where each pixel is only either 0 or 1.
Another option is to convert the image to a supported inversion format, before inverting it, ie:
import PIL.ImageOps
inverted_image = PIL.ImageOps.invert( image.convert('RGB') )
If you're willing to convert i1 to a numpy array you can just do
i1 = 1 - numpy.asarray(i1)
I came across the exact same problem. There is a solution without going to the trouble of using numpy.
you can use ImageMath:
from PIL import ImageMath
i2 = ImageMath.eval('255-(a)',a=i1)
This way you can invert a binary (mode 1 in PIL) image.
For more an ImageMath refer to here.
The easiest solution is to do what ImageOps.invert itself does:
i1 = i1.point(lambda x: 255-x)
ImageOps.invert checks the mode and refuses to work if it's anything other than 'L' or 'RGB', but this method actually works with many other modes, with a few caveats:
If there's an alpha channel, it will invert it too.
If it's a palette image, it will invert the palette indexes, not the palette entries.
Here's a more complete implementation of invert that tries to do the right thing with all common modes:
def invert(im):
if im.mode in {'P', 'PA'}:
pmode, pal = im.palette.getdata()
pal = Image.frombytes(pmode, (len(pal) // len(pmode), 1), pal)
im = im.copy()
im.palette.palette = invert(pal).tobytes()
return im
elif im.mode in {'LA', 'La', 'RGBA', 'RGBa', 'RGBX'}:
return im.point([*range(255, -1, -1)] * (len(im.mode) - 1) + [*range(256)])
else:
# This may fail
return im.point(lambda x: 255-x)

Python PIL 0.5 opacity, transparency, alpha

Is there any way to make an image half transparent?
the pseudo code is something like this:
from PIL import Image
image = Image.open('image.png')
image = alpha(image, 0.5)
I googled it for a couple of hours but I can't find anything useful.
I realize this question is really old, but with the current version of Pillow (v4.2.1), there is a function called putalpha. It seems to work fine for me. I don't know if will work for every situation where you need to change the alpha, but it does work. It sets the alpha value for every pixel in the image. It seems, though that you can use a mask: http://www.leancrew.com/all-this/2013/11/transparency-with-pil/.
Use putalpha like this:
from PIL import Image
img = Image.open(image)
img.putalpha(127) # Half alpha; alpha argument must be an int
img.save(dest)
Could you do something like this?
from PIL import Image
image = Image.open('image.png') #open image
image = image.convert("RGBA") #convert to RGBA
rgb = image.getpixel(x,y) #Get the rgba value at coordinates x,y
rgb[3] = int(rgb[3] / 2) or you could do rgb[3] = 50 maybe? #set alpha to half somehow
image.putpixel((x,y), rgb) #put back the modified reba values at same pixel coordinates
Definitely not the most efficient way of doing things but it might work. I wrote the code in browser so it might not be error free but hopefully it can give you an idea.
EDIT: Just noticed how old this question was. Leaving answer anyways for future help. :)
I put together Pecan's answer and cr333's question from this question:
Using PIL to make all white pixels transparent?
... and came up with this:
from PIL import Image
opacity_level = 170 # Opaque is 255, input between 0-255
img = Image.open('img1.png')
img = img.convert("RGBA")
datas = img.getdata()
newData = []
for item in datas:
newData.append((0, 0, 0, opacity_level))
else:
newData.append(item)
img.putdata(newData)
img.save("img2.png", "PNG")
In my case, I have text with black background and wanted only the background semi-transparent, in which case:
from PIL import Image
opacity_level = 170 # Opaque is 255, input between 0-255
img = Image.open('img1.png')
img = img.convert("RGBA")
datas = img.getdata()
newData = []
for item in datas:
if item[0] == 0 and item[1] == 0 and item[2] == 0:
newData.append((0, 0, 0, opacity_level))
else:
newData.append(item)
img.putdata(newData)
img.save("img2.png", "PNG")
I had an issue, where black boxes were appearing around my image when applying putalpha().
This workaround (applying alpha in a copied layer) solved it for me.
from PIL import Image
with Image.open("file.png") as im:
im2 = im.copy()
im2.putalpha(180)
im.paste(im2, im)
im.save("file2.png")
Explanation:
Like I said, putalpha modifies all pixels by setting their alpha value, so fully transparent pixels become only partially transparent. The code I posted above first sets (putalpha) all pixels to semi-transparent in a copy, then copies (paste) all pixels to the original image using the original alpha values as a mask. This means that fully transparent pixels in the original image are skipped during the paste.
Credit: https://github.com/nulano # https://github.com/python-pillow/Pillow/issues/4687#issuecomment-643567573
I just did this by myself...even though my code maybe a little bit weird...But it works fine. So I share it here. Hopes it could help anybody. =)
The idea: To transparent a pic means lower alpha which is the 4th element in the tuple.
my frame code:
from PIL import Image
img=open(image)
img=img.convert('RGBA') #you can make sure your pic is in the right mode by check img.mode
data=img.getdata() #you'll get a list of tuples
newData=[]
for a in data:
a=a[:3] #you'll get your tuple shorten to RGB
a=a+(100,) #change the 100 to any transparency number you like between (0,255)
newData.append(a)
img.putdata(newData) #you'll get your new img ready
img.save(filename.filetype)
I didn't find the right command to fulfil this job automatically, so I write this by myself. Hopes it'll help again. XD
This method helps to reduce opacity of logo with transparency before pasting it over image
# pip install Pillow
# PIL.__version__ is 9.3.0
from PIL import Image, ImageEnhance
im = Image.open('logo.png').convert('RGBA')
alpha = im.split()[3]
alpha = ImageEnhance.Brightness(alpha).enhance(.5)
im.putalpha(alpha)

How to invert colors of image with PIL (Python-Imaging)?

I need to convert series of images drawn as white on black background letters to images where white and black are inverted (as negative). How can I achieve this using PIL?
Try the following from the docs: https://pillow.readthedocs.io/en/stable/reference/ImageOps.html
from PIL import Image
import PIL.ImageOps
image = Image.open('your_image.png')
inverted_image = PIL.ImageOps.invert(image)
inverted_image.save('new_name.png')
Note: "The ImageOps module contains a number of 'ready-made' image processing operations. This module is somewhat experimental, and most operators only work on L and RGB images."
If the image is RGBA transparent this will fail... This should work though:
from PIL import Image
import PIL.ImageOps
image = Image.open('your_image.png')
if image.mode == 'RGBA':
r,g,b,a = image.split()
rgb_image = Image.merge('RGB', (r,g,b))
inverted_image = PIL.ImageOps.invert(rgb_image)
r2,g2,b2 = inverted_image.split()
final_transparent_image = Image.merge('RGBA', (r2,g2,b2,a))
final_transparent_image.save('new_file.png')
else:
inverted_image = PIL.ImageOps.invert(image)
inverted_image.save('new_name.png')
For anyone working with an image in "1" mode (i.e., 1-bit pixels, black and white, stored with one pixel per byte -- see docs), you need to convert it into "L" mode before calling PIL.ImageOps.invert.
Thus:
im = im.convert('L')
im = ImageOps.invert(im)
im = im.convert('1')
now ImageOps must be:
PIL.ImageChops.invert(PIL.Image.open(imagepath))
note that this works for me in python 3.8.5
In case someone is inverting a CMYK image, the current implementations of PIL and Pillow don't seem to support this and throw an error. You can, however, easily circumvent this problem by inverting your image's individual bands using this handy function (essentially an extension of Greg Sadetsky's post above):
def CMYKInvert(img) :
return Image.merge(img.mode, [ImageOps.invert(b.convert('L')) for b in img.split()])
Of course ImageOps does its job well, but unfortunately it can't work with some modes like 'RGBA'. This code will solve this problem.
def invert(image: Image.Image) -> Image.Image:
drawer = ImageDraw.Draw(image)
pixels = image.load()
for x in range(image.size[0]):
for y in range(image.size[1]):
data = pixels[x, y]
if data != (0, 0, 0, 0) and isinstance(data, tuple):
drawer.point((x, y), (255 - data[0], 255 - data[1], 255 - data[2], data[3]))
return image
from PIL import Image
img = Image.open("archive.extension")
pixels = img.load()
for i in range(img.size[0]):
for j in range(img.size[1]):
x,y,z = pixels[i,j][0],pixels[i,j][1],pixels[i,j][2]
x,y,z = abs(x-255), abs(y-255), abs(z-255)
pixels[i,j] = (x,y,z)
img.show()
`

Categories