From 9b7200d2b439a4a41411e074e0d6cc453a8e3e57 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Dec 2025 12:50:26 +1100 Subject: [PATCH 1/2] Allow 1 mode images in MorphOp get_on_pixels() --- Tests/test_imagemorph.py | 9 +++++---- src/PIL/ImageMorph.py | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index ca192a809c4..59d1b0645ef 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -188,9 +188,10 @@ def test_corner() -> None: assert len(coords) == 4 assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) - coords = mop.get_on_pixels(Aout) - assert len(coords) == 4 - assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) + for image in (Aout, Aout.convert("1")): + coords = mop.get_on_pixels(image) + assert len(coords) == 4 + assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) def test_mirroring() -> None: @@ -239,7 +240,7 @@ def test_incorrect_mode() -> None: mop.apply(im) with pytest.raises(ValueError, match="Image mode must be L"): mop.match(im) - with pytest.raises(ValueError, match="Image mode must be L"): + with pytest.raises(ValueError, match="Image mode must be 1 or L"): mop.get_on_pixels(im) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index bd70aff7b48..d6d0f829658 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -232,13 +232,13 @@ def match(self, image: Image.Image) -> list[tuple[int, int]]: return _imagingmorph.match(bytes(self.lut), image.getim()) def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: - """Get a list of all turned on pixels in a binary image + """Get a list of all turned on pixels in a 1 or L mode image. Returns a list of tuples of (x,y) coordinates of all matching pixels. See :ref:`coordinate-system`.""" - if image.mode != "L": - msg = "Image mode must be L" + if image.mode not in ("1", "L"): + msg = "Image mode must be 1 or L" raise ValueError(msg) return _imagingmorph.get_on_pixels(image.getim()) From a7047114040f38314a970d271d967cc17004c5e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Dec 2025 14:13:51 +1100 Subject: [PATCH 2/2] Allow 1 mode images in apply() and match() --- Tests/images/morph_a.png | Bin 83 -> 79 bytes Tests/test_imagemorph.py | 38 ++++++++++++++++++-------------------- src/PIL/ImageMorph.py | 8 ++++---- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/Tests/images/morph_a.png b/Tests/images/morph_a.png index 19f6b777ffe17d924b984ba4ae4679257f01d31f..035fbc4bb84d6b67ee99cd6bbb5e8996059dd142 100644 GIT binary patch delta 60 zcmWIcpCDn*$N&UyG_&e}l$fWBV@SoE Image.Image: rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] height = len(rows) width = len(rows[0]) - im = Image.new("L", (width, height)) - for i in range(width): - for j in range(height): - c = rows[j][i] - v = c in "X1" - im.putpixel((i, j), v) - + im = Image.new("1", (width, height)) + for x in range(width): + for y in range(height): + im.putpixel((x, y), rows[y][x] in "X1") return im @@ -42,10 +39,10 @@ def img_to_string(im: Image.Image) -> str: """Turn a (small) binary image into a string representation""" chars = ".1" result = [] - for r in range(im.height): + for y in range(im.height): line = "" - for c in range(im.width): - value = im.getpixel((c, r)) + for x in range(im.width): + value = im.getpixel((x, y)) assert not isinstance(value, tuple) assert value is not None line += chars[value > 0] @@ -165,10 +162,12 @@ def test_edge() -> None: ) -def test_corner() -> None: +@pytest.mark.parametrize("mode", ("1", "L")) +def test_corner(mode: str) -> None: # Create a corner detector pattern mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) - count, Aout = mop.apply(A) + image = A.convert(mode) if mode == "L" else A + count, Aout = mop.apply(image) assert count == 5 assert_img_equal_img_string( Aout, @@ -184,14 +183,13 @@ def test_corner() -> None: ) # Test the coordinate counting with the same operator - coords = mop.match(A) + coords = mop.match(image) assert len(coords) == 4 assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) - for image in (Aout, Aout.convert("1")): - coords = mop.get_on_pixels(image) - assert len(coords) == 4 - assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) + coords = mop.get_on_pixels(Aout) + assert len(coords) == 4 + assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) def test_mirroring() -> None: @@ -233,12 +231,12 @@ def test_negate() -> None: def test_incorrect_mode() -> None: - im = hopper("RGB") + im = hopper() mop = ImageMorph.MorphOp(op_name="erosion8") - with pytest.raises(ValueError, match="Image mode must be L"): + with pytest.raises(ValueError, match="Image mode must be 1 or L"): mop.apply(im) - with pytest.raises(ValueError, match="Image mode must be L"): + with pytest.raises(ValueError, match="Image mode must be 1 or L"): mop.match(im) with pytest.raises(ValueError, match="Image mode must be 1 or L"): mop.get_on_pixels(im) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index d6d0f829658..69b0d170061 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -209,8 +209,8 @@ def apply(self, image: Image.Image) -> tuple[int, Image.Image]: msg = "No operator loaded" raise Exception(msg) - if image.mode != "L": - msg = "Image mode must be L" + if image.mode not in ("1", "L"): + msg = "Image mode must be 1 or L" raise ValueError(msg) outimage = Image.new(image.mode, image.size, None) count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim()) @@ -226,8 +226,8 @@ def match(self, image: Image.Image) -> list[tuple[int, int]]: msg = "No operator loaded" raise Exception(msg) - if image.mode != "L": - msg = "Image mode must be L" + if image.mode not in ("1", "L"): + msg = "Image mode must be 1 or L" raise ValueError(msg) return _imagingmorph.match(bytes(self.lut), image.getim())