Skip to content

Commit 3036094

Browse files
committed
add initial imgdiff to calculate differences in images across different pillow 'modes'
1 parent 74eeb6a commit 3036094

File tree

5 files changed

+513
-1
lines changed

5 files changed

+513
-1
lines changed

blendmodes/imgdiff.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import numpy as np
2+
from PIL import Image
3+
4+
5+
6+
def is_x_diff(
7+
img1in: Image.Image,
8+
img2in: Image.Image,
9+
compare_mode: str = "RGBA",
10+
cmp_diff: float = 0,
11+
tolerance: float = 0,
12+
*,
13+
percentage: bool = True,
14+
) -> bool:
15+
"""
16+
Compare two images and return True/False if the image is within `tolerance` of
17+
`cmp_diff`.
18+
19+
For example, a black and white image compared in 'RGB' mode would
20+
return a value of 100, which would then be checked if its between
21+
`cmp_diff - tolerance` and `cmp_diff + tolerance`
22+
23+
:param Image.Image img1in: image 1 to compare
24+
:param Image.Image img2in: image 2 to compare
25+
:param str compare_mode: how should the pillow images be compared? eg RGBA, RGB, L etc
26+
:param float cmp_diff: how 'unequal' should the images be? 0 for identical, 1 (or 100)
27+
for completely different (eg black + white in L mode)
28+
param float tolerance: what tolerance should we accept on the inequality?
29+
param bool percentage: are we comparing in percentage mode vs 0-1 mode?
30+
31+
:return bool: True/False if the images are within `tolerance` of
32+
`cmp_diff`.
33+
34+
Example Use
35+
-----------
36+
37+
>>> img1 = Image.new("RGB", (100, 100), "red")
38+
>>> img2 = Image.new("RGB", (100, 100), "blue")
39+
>>> is_x_diff(img1, img2, compare_mode="RGB", cmp_diff=33, tolerance=1)
40+
True
41+
42+
>>> img1 = Image.new("RGB", (100, 100), "white")
43+
>>> img2 = Image.new("RGB", (100, 100), "black")
44+
>>> is_x_diff(img1, img2, compare_mode="RGB", cmp_diff=100, tolerance=1)
45+
True
46+
47+
>>> img1 = Image.new("RGB", (100, 100), "red")
48+
>>> img2 = Image.new("RGB", (100, 100), "blue")
49+
>>> is_x_diff(img1, img2, compare_mode="L", cmp_diff=18, tolerance=1)
50+
True
51+
52+
"""
53+
compare_res = image_diff(img1in, img2in, compare_mode, percentage=percentage)
54+
return cmp_diff - tolerance <= compare_res <= cmp_diff + tolerance
55+
56+
def is_equal(
57+
img1in: Image.Image,
58+
img2in: Image.Image,
59+
compare_mode: str = "RGBA",
60+
tolerance: float = 0,
61+
*,
62+
percentage: bool = True,
63+
):
64+
"""
65+
Compare two images and return True/False if the image is within `tolerance` of
66+
`cmp_diff`.
67+
68+
For example, a black and white image compared in 'RGB' mode would
69+
return a value of 100, which would then be checked if its between
70+
`cmp_diff - tolerance` and `cmp_diff + tolerance`
71+
72+
:param Image.Image img1in: image 1 to compare
73+
:param Image.Image img2in: image 2 to compare
74+
:param str compare_mode: how should the pillow images be compared? eg RGBA, RGB, L etc
75+
param float tolerance: what tolerance should we accept on any inequality?
76+
param bool percentage: are we comparing in percentage mode vs 0-1 mode?
77+
78+
:return bool: if the images are equal with a given tolerance
79+
80+
Example Use
81+
-----------
82+
83+
>>> img1 = Image.new("RGB", (100, 100), "red")
84+
>>> img2 = Image.new("RGB", (100, 100), "blue")
85+
>>> is_equal(img1, img2, compare_mode="RGB", tolerance=1)
86+
False
87+
88+
>>> img1 = Image.new("RGB", (100, 100), "white")
89+
>>> img2 = Image.new("RGB", (100, 100), "black")
90+
>>> is_equal(img1, img2, compare_mode="RGB", tolerance=1)
91+
False
92+
93+
>>> img1 = Image.new("RGB", (100, 100), "red")
94+
>>> img2 = Image.new("RGB", (100, 100), "blue")
95+
>>> is_equal(img1, img2, compare_mode="L", tolerance=1)
96+
False
97+
98+
"""
99+
compare_res = image_diff(img1in, img2in, compare_mode, percentage=percentage)
100+
return compare_res <= tolerance
101+
102+
103+
def image_diff(
104+
img1in: Image.Image, img2in: Image.Image, compare_mode: str = "RGBA", *, percentage: bool = True
105+
) -> float:
106+
"""
107+
Compare two images and return the difference as a value between 0 and 1, or
108+
if percentage: 0 and 100.
109+
110+
For example, a black and white image compared in 'RGB' mode would
111+
return a value of 100, which would then be checked if its between
112+
`cmp_diff - tolerance` and `cmp_diff + tolerance`
113+
114+
:param Image.Image img1in: image 1 to compare
115+
:param Image.Image img2in: image 2 to compare
116+
:param str compare_mode: how should the pillow images be compared? eg RGBA, RGB, L etc
117+
param bool percentage: are we comparing in percentage mode vs 0-1 mode?
118+
119+
:return float: value representing how different the images are
120+
121+
Example Use
122+
-----------
123+
124+
>>> img1 = Image.new("RGB", (100, 100), "red")
125+
>>> img2 = Image.new("RGB", (100, 100), "blue")
126+
>>> res = image_diff(img1, img2, compare_mode="RGB")
127+
>>> int(res)
128+
33
129+
130+
>>> img1 = Image.new("RGB", (100, 100), "white")
131+
>>> img2 = Image.new("RGB", (100, 100), "black")
132+
>>> image_diff(img1, img2, compare_mode="RGB")
133+
100.0
134+
135+
>>> img1 = Image.new("RGB", (100, 100), "red")
136+
>>> img2 = Image.new("RGB", (100, 100), "blue")
137+
>>> res = image_diff(img1, img2, compare_mode="L")
138+
>>> int(res)
139+
18
140+
141+
"""
142+
img1 = img1in.convert(mode=compare_mode)
143+
img2 = img2in.convert(mode=compare_mode)
144+
return image_diff_array(img1, img2) * (100 if percentage else 1)
145+
146+
147+
def image_diff_array(
148+
img1in: Image.Image | np.ndarray, img2in: Image.Image | np.ndarray
149+
) -> float:
150+
"""
151+
Compare two images and return difference between 0, and 1.
152+
Supports both PIL Images and NumPy arrays.
153+
154+
Both images must be in the same mode/ shape
155+
156+
:param Image.Image | np.ndarray img1in: image 1 to compare
157+
:param Image.Image | np.ndarray img2in: image 2 to compare
158+
:return float: value representing how different the images are. between 0, and 1
159+
160+
161+
Example Use
162+
-----------
163+
164+
>>> img1 = Image.new("RGB", (100, 100), "red")
165+
>>> img2 = Image.new("RGB", (100, 100), "blue")
166+
>>> res = image_diff(img1, img2)
167+
>>> int(res)
168+
25
169+
170+
>>> img1 = Image.new("RGB", (100, 100), "white")
171+
>>> img2 = Image.new("RGB", (100, 100), "black")
172+
>>> image_diff(img1, img2)
173+
75.0
174+
175+
"""
176+
# Convert PIL images to NumPy arrays if needed
177+
img1 = np.array(img1in) if isinstance(img1in, Image.Image) else img1in
178+
img2 = np.array(img2in) if isinstance(img2in, Image.Image) else img2in
179+
# Ensure images have the same dimensions
180+
if img1.shape != img2.shape:
181+
msg = "Images must have the same dimensions for comparison."
182+
raise ValueError(msg)
183+
184+
# Compute absolute difference
185+
difference = np.abs(img1 - img2)
186+
187+
# Sum the differences and normalize to get a percentage
188+
total_diff = np.sum(difference)
189+
return float(total_diff / img1.size / 255)
190+
191+
192+
if __name__ == "__main__":
193+
import doctest
194+
doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE)
195+
doctest.testmod()

documentation/reference/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ A full list of `Blendmodes` project modules.
77
- [Blendmodes](blendmodes/index.md#blendmodes)
88
- [Blend](blendmodes/blend.md#blend)
99
- [BlendType](blendmodes/blendtype.md#blendtype)
10+
- [Imgdiff](blendmodes/imgdiff.md#imgdiff)

0 commit comments

Comments
 (0)