# Efficient Comparison Of Two Images Using Numpy

I want to compare 2 images using numpy. This is what I have got so far. One of the outputs should be a white image with black pixels where pixels are different.

I am sure it's possible to make it more efficient by better use of numpy, e.g. the for loop can be avoided. Or maybe there is a function/package that has implemented something similar already?

import gc
import PIL
import numpy as np

def compare_images(image_to_test_filename, image_benchmark_filename):

print('comparing', image_to_test_filename, 'and', image_benchmark_filename)

assert image_to_test.shape[0] == image_benchmark.shape[0] and image_to_test.shape[1] == image_benchmark.shape[1]

diff_pixel = np.array([0, 0, 0], np.uint8)
true_array =  np.array([True, True, True, True])
diff_black_white = np.zeros([image_benchmark.shape[0], image_benchmark.shape[1], 3], dtype=np.uint8) + 255
is_close_pixel_by_pixel = np.isclose(image_to_test, image_benchmark)
nb_different_rows = 0
for r, row in enumerate(is_close_pixel_by_pixel):
diff_indices = [c for c, elem in enumerate(row) if not np.all(elem == true_array)]
if len(diff_indices):
diff_black_white[r][diff_indices] = diff_pixel
nb_different_rows += 1

dist = np.linalg.norm(image_to_test - image_benchmark) / (image_to_test.shape[0] * image_to_test.shape[1])

if nb_different_rows > 0:
print("IS DIFFERERENT! THE DIFFERENCE IS (% OF ALL PIXELS)", dist * 100)
im = PIL.Image.fromarray(diff_black_white)
im.save(image_to_test_filename+'_diff.png')
del im

del image_benchmark
del image_to_test
del diff_black_white

gc.collect()

return dist, None
• @Reinderien done! Jun 26 at 15:18

First, this of course depends on your definition of different. I believe right now that your comparison is far too strict, given the defaults for isclose. I did a trivial modification of a .jpg, and with one decode/encode pass it still produced 6% of pixels with an RGB distance of more than 20. isclose applies both a relative and absolute tolerance, but it's probable that for your purposes absolute-only is simpler.

I find f-strings a more natural form of string formatting because the in-line field expressions remove any need of your eyes to scan back and forth between field placeholder and expression. This does not impact performance. Also note the use of % in this context removes the need to divide by 100.

PEP484 type hinting also does not impact performance, but makes the code more legible and verifiable.

Note that it's "almost never" appropriate to del and gc yourself. The garbage collector is there for a reason, and in the vast, vast majority of cases, will act reasonably to free nonreferenced memory without you having to intervene. The one thing you should be doing here that you aren't is moving the images to a with for context management, which will guarantee resource cleanup on scope exit.

This problem is fully vectorizable so should see no explicit loops at all. Just calculate a black-and-white distance from an absolute threshold in one pass. Your original implementation was taking longer to execute than I had patience for, but the following suggestion executes in less than a second:

import numpy as np
from PIL import Image

# Maximum allowable Frobenius distance in RGB space
EPSILON = 20

def compare_images(
image_to_test_filename: str,
image_benchmark_filename: str,
) -> float:
print(f'comparing {image_to_test_filename} and {image_benchmark_filename}')

assert image_to_test.shape == image_benchmark.shape

diff_black_white = (
np.linalg.norm(image_to_test - image_benchmark, axis=2) > EPSILON
).astype(np.uint8)
n_different = np.sum(diff_black_white)

if n_different > 0:
diff_fraction = n_different / image_benchmark.size
print(f'IS DIFFERERENT! THE DIFFERENCE OF ALL PIXELS IS {diff_fraction:.2%}')
im = Image.fromarray(diff_black_white * 255)
im.save(f'{image_to_test_filename}_diff.png')

dist = np.linalg.norm(image_to_test - image_benchmark) / image_benchmark.size
return dist
• Thanks! I have found one more actually: image_benchmark.shape[0] * image_benchmark.shape[1] out to be image_benchmark.size, right? (this is 1). ) Jun 27 at 10:48
• 2. Could I ask why you prefer print(f'comparing {i1} and {i2}') over print('comparing', i1, 'and', i2)? Jun 27 at 10:50
• 3. What would be the benefit of typing the inputs - is it the performance improvement? Is it significant in this case? Jun 27 at 10:51
• np.linalg.norm along axis 2 is brilliant, I did not know it can be used like this, thanks! Jun 27 at 10:52
• Yes, size is better for this purpose; edited for the other points. Jun 27 at 13:12