Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
61d7830
code cleanup, new tests, more pythonic
Mattwmaster58 May 7, 2020
760047c
support for output being a PIL.Image
Mattwmaster58 May 7, 2020
f835664
update tests
Mattwmaster58 May 7, 2020
96797e9
fix pixelmatch implementation
Mattwmaster58 May 7, 2020
3e62e5f
more descriptive error messages
Mattwmaster58 May 7, 2020
96a6b40
bump version
Mattwmaster58 May 7, 2020
12e822a
blackify
Mattwmaster58 May 7, 2020
25d82e7
add docstrings
Mattwmaster58 May 7, 2020
9e4c4b4
update docs
Mattwmaster58 May 7, 2020
fd64d7a
code cleanup/refactoring, remove PIL.Image support in favor of moving…
Mattwmaster58 May 8, 2020
91ec2b5
code cleanup/refactoring, remove PIL.Image support in favor of moving…
Mattwmaster58 May 8, 2020
f9b941d
Merge remote-tracking branch 'origin/master'
Mattwmaster58 May 8, 2020
fd62dbc
remove out of scope test
Mattwmaster58 May 8, 2020
33d60a9
update docs to reflect non-support of PIL by default
Mattwmaster58 May 8, 2020
d62a2f9
update type hints
Mattwmaster58 May 8, 2020
8155bc1
blackify
Mattwmaster58 May 8, 2020
9abfc19
remove unused files
Mattwmaster58 May 8, 2020
c8978b7
fix int | float typing errors w/ mypy, pyright
Mattwmaster58 May 8, 2020
a81f78d
fix bad typing + replace missing changelog item
Mattwmaster58 May 8, 2020
d3a387d
fix bad typing + replace missing changelog item
Mattwmaster58 May 8, 2020
5c72e66
Merge remote-tracking branch 'origin/master'
Mattwmaster58 May 8, 2020
cb63e9a
Merge remote-tracking branch 'origin_upstream/master'
Mattwmaster58 May 8, 2020
42c36b4
fix mypy error
Mattwmaster58 May 8, 2020
cf3f10d
blackify
Mattwmaster58 May 8, 2020
8a9e1d4
correct changelog
Mattwmaster58 May 8, 2020
4b59c16
remove unused impport
Mattwmaster58 May 8, 2020
4f91532
Merge remote-tracking branch 'origin_upstream/master'
Mattwmaster58 May 8, 2020
6154308
add PIL.Image support
Mattwmaster58 May 8, 2020
f856334
add documentation
Mattwmaster58 May 8, 2020
d024217
Merge remote-tracking branch 'origin/master'
Mattwmaster58 May 8, 2020
9505f0f
update docs, further; blacken
Mattwmaster58 May 8, 2020
2408304
ignore type of untyped class (PIL.Image.Image)
Mattwmaster58 May 8, 2020
affdc47
prefer mypy config over inline type checking overideing, run black
Mattwmaster58 May 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 26 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pixelmatch-py

Python port of https://github.com/mapbox/pixelmatch.
Python port of https://github.com/mapbox/pixelmatch with additional support of PIL.Image instances.

A fast pixel-level image comparison library, originally created to compare screenshots in tests.

Expand Down Expand Up @@ -40,39 +40,40 @@ python -m pip install pixelmatch

Compares two images, writes the output diff and returns the number of mismatched pixels.

## Example usage
### contrib.PIL.pixelmatch

Compares two images, writes the output diff and returns the number of mismatched pixels. Exact same API as `pixelmatch.pixelmatch` except for the important fact that it takes instances of PIL.Image for image parameters (`img1`, `img2`, and `output`) and the width/size need not be specified.

### PIL
## Example usage

### PIL.Image comparison
```python
from PIL import Image

from pixelmatch import pixelmatch


def pil_to_flatten_data(img):
"""
Convert data from [(R1, G1, B1, A1), (R2, G2, B2, A2)] to [R1, G1, B1, A1, R2, G2, B2, A2]
"""
return [x for p in img.convert("RGBA").getdata() for x in p]
from pixelmatch.contrib.PIL import pixelmatch

img_a = Image.open("a.png")
img_b = Image.open("b.png")
width, height = img_a.size
img_diff = Image.new("RGBA", img_a.size)

# note how there is no need to specify dimensions
mismatch = pixelmatch(img_a, img_b, img_diff, includeAA=True)

img_diff.save("diff.png")
```

data_a = pil_to_flatten_data(img_a)
data_b = pil_to_flatten_data(img_b)
data_diff = [0] * len(data_a)

mismatch = pixelmatch(data_a, data_b, width, height, data_diff, includeAA=True)
### Raw Image Data Comparison
```python
from pixelmatch import pixelmatch

img_diff = Image.new("RGBA", img_a.size)
width, height = 1920, 1080
img_a = [R1, G1, B1, A1, R2, B2, G2, A2, ...]
img_b = [R1, G1, B1, A1, R2, B2, G2, A2, ...]

def flatten_data_to_pil(data):
return list(zip(data[::4], data[1::4], data[2::4], data[3::4]))
data_diff = [0] * len(img_a)

img_diff.putdata(flatten_data_to_pil(data_diff))
img_diff.save("diff.png")
mismatch = pixelmatch(img_a, img_b, width, height, data_diff, includeAA=True)
```

## Example output
Expand All @@ -86,10 +87,13 @@ img_diff.save("diff.png")

## Changelog

### vNEXT

- ft: add function to compare PIL.Image instances through contrib.PIL.pixelmatch [#42](https://github.com/whtsky/pixelmatch-py/pull/42)

### v0.2.0

- BREAKING CHANGE: remove `options` parameter [#38](https://github.com/whtsky/pixelmatch-py/pull/38)

- docs: use absolute url for images in README

### v0.1.1
Expand Down
3 changes: 2 additions & 1 deletion mypy.ini
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
[mypy]
warn_unused_configs = True
warn_unused_ignores = True
warn_unreachable = True
warn_return_any = True

[mypy-pytest]
ignore_missing_imports = True

[mypy-PIL]
[mypy-PIL,PIL.*]
ignore_missing_imports = True
93 changes: 93 additions & 0 deletions pixelmatch/contrib/PIL.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Functions to facilitate direct comparison of PIL.Image instances"""
from typing import Optional, List, Tuple

from PIL.Image import Image

from pixelmatch import (
pixelmatch as base_pixelmatch,
ImageSequence,
MutableImageSequence,
)
from pixelmatch import RGBTuple


def pixelmatch(
img1: Image,
img2: Image,
output: Optional[Image] = None,
threshold: float = 0.1,
includeAA: bool = False,
alpha: float = 0.1,
aa_color: RGBTuple = (255, 255, 0),
diff_color: RGBTuple = (255, 0, 0),
diff_mask: bool = False,
) -> int:
"""
Compares two images, writes the output diff and returns the number of mismatched pixels.
Serves the same purpose as pixelmatch.pixelmatch, but takes PIL.Images as input instead
of raw image data.

:param img1: PIL.Image data to compare with img2. Must be the same size as img2
:param img2: PIL.Image data to compare with img2. Must be the same size as img1
:param output: Image data to write the diff to. Should be the same size as
:param threshold: matching threshold (0 to 1); smaller is more sensitive, defaults to 1
:param includeAA: whether or not to skip anti-aliasing detection, ie if includeAA is True,
detecting and ignoring anti-aliased pixels is disabled. Defaults to False
:param alpha: opacity of original image in diff output, defaults to 0.1
:param aa_color: tuple of RGB color of anti-aliased pixels in diff output,
defaults to (255, 255, 0) (yellow)
:param diff_color: tuple of RGB color of the color of different pixels in diff output,
defaults to (255, 0, 0) (red)
:param diff_mask: whether or not to draw the diff over a transparent background (a mask),
defaults to False
:return: number of pixels that are different
"""
width, height = img1.size
img1 = from_PIL_to_raw_data(img1)
img2 = from_PIL_to_raw_data(img2)

if output is not None:
raw_output: Optional[MutableImageSequence] = from_PIL_to_raw_data(output)
else:
raw_output = None

diff_pixels = base_pixelmatch(
img1,
img2,
width,
height,
raw_output,
threshold=threshold,
includeAA=includeAA,
alpha=alpha,
aa_color=aa_color,
diff_color=diff_color,
diff_mask=diff_mask,
)

if raw_output is not None and output is not None:
output.putdata(to_PIL_from_raw_data(raw_output))

return diff_pixels


def from_PIL_to_raw_data(pil_img: Image) -> MutableImageSequence:
"""
Converts a PIL.Image object from [(R1, B1, A1, A1), (R2, ...), ...] to our raw data format
[R1, G1, B1, A1, R2, ...].

:param pil_img:
:return:
"""
return [item for sublist in pil_img.convert("RGBA").getdata() for item in sublist]


def to_PIL_from_raw_data(
raw_data: ImageSequence,
) -> List[Tuple[float, float, float, float]]:
"""
Converts from the internal raw data format of [R1, G1, B1, A1, R2, ...] to PIL's raw data format, ie
[(R1, B1, A1, A1), (R2, ...), ...]
:return: raw image data in a PIL appropriate format
"""
return [*zip(raw_data[::4], raw_data[1::4], raw_data[2::4], raw_data[3::4])]
Empty file added pixelmatch/contrib/__init__.py
Empty file.
25 changes: 25 additions & 0 deletions test_pixelmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import pytest
from PIL import Image
from pixelmatch.contrib import PIL

from pixelmatch import pixelmatch

Expand Down Expand Up @@ -77,3 +78,27 @@ def test_pixelmatch(
assert diff_data == pil_to_flatten_data(expected_diff), "diff image"
assert mismatch == expected_mismatch, "number of mismatched pixels"
assert mismatch == mismatch2, "number of mismatched pixels without diff"


@pytest.mark.parametrize(
"img_path_1,img_path_2,diff_path,options,expected_mismatch", testdata
)
def test_PIL_pixelmatch(
img_path_1: str,
img_path_2: str,
diff_path: str,
options: Dict,
expected_mismatch: int,
):
img1 = read_img(img_path_1)
img2 = read_img(img_path_2)
diff_data = Image.new("RGBA", img1.size)

mismatch = PIL.pixelmatch(img1, img2, diff_data, **options)
mismatch2 = PIL.pixelmatch(img1, img2, **options)

assert pil_to_flatten_data(diff_data) == pil_to_flatten_data(
read_img(diff_path)
), "diff image"
assert mismatch == expected_mismatch, "number of mismatched pixels"
assert mismatch == mismatch2, "number of mismatched pixels without diff"