Skip to content

Commit f130c10

Browse files
authored
Allow 1 mode images in MorphOp (#9348)
2 parents a868c29 + 6b892c4 commit f130c10

File tree

4 files changed

+30
-30
lines changed

4 files changed

+30
-30
lines changed

Tests/images/morph_a.png

-4 Bytes
Loading

Tests/test_imagemorph.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,10 @@ def string_to_img(image_string: str) -> Image.Image:
1515
rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)]
1616
height = len(rows)
1717
width = len(rows[0])
18-
im = Image.new("L", (width, height))
19-
for i in range(width):
20-
for j in range(height):
21-
c = rows[j][i]
22-
v = c in "X1"
23-
im.putpixel((i, j), v)
24-
18+
im = Image.new("1", (width, height))
19+
for x in range(width):
20+
for y in range(height):
21+
im.putpixel((x, y), rows[y][x] in "X1")
2522
return im
2623

2724

@@ -42,10 +39,10 @@ def img_to_string(im: Image.Image) -> str:
4239
"""Turn a (small) binary image into a string representation"""
4340
chars = ".1"
4441
result = []
45-
for r in range(im.height):
42+
for y in range(im.height):
4643
line = ""
47-
for c in range(im.width):
48-
value = im.getpixel((c, r))
44+
for x in range(im.width):
45+
value = im.getpixel((x, y))
4946
assert not isinstance(value, tuple)
5047
assert value is not None
5148
line += chars[value > 0]
@@ -165,10 +162,12 @@ def test_edge() -> None:
165162
)
166163

167164

168-
def test_corner() -> None:
165+
@pytest.mark.parametrize("mode", ("1", "L"))
166+
def test_corner(mode: str) -> None:
169167
# Create a corner detector pattern
170168
mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"])
171-
count, Aout = mop.apply(A)
169+
image = A.convert(mode) if mode == "L" else A
170+
count, Aout = mop.apply(image)
172171
assert count == 5
173172
assert_img_equal_img_string(
174173
Aout,
@@ -184,7 +183,7 @@ def test_corner() -> None:
184183
)
185184

186185
# Test the coordinate counting with the same operator
187-
coords = mop.match(A)
186+
coords = mop.match(image)
188187
assert len(coords) == 4
189188
assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4))
190189

@@ -232,14 +231,14 @@ def test_negate() -> None:
232231

233232

234233
def test_incorrect_mode() -> None:
235-
im = hopper("RGB")
234+
im = hopper()
236235
mop = ImageMorph.MorphOp(op_name="erosion8")
237236

238-
with pytest.raises(ValueError, match="Image mode must be L"):
237+
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
239238
mop.apply(im)
240-
with pytest.raises(ValueError, match="Image mode must be L"):
239+
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
241240
mop.match(im)
242-
with pytest.raises(ValueError, match="Image mode must be L"):
241+
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
243242
mop.get_on_pixels(im)
244243

245244

docs/reference/ImageMorph.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
================================
66

77
The :py:mod:`~PIL.ImageMorph` module allows `morphology`_ operators ("MorphOp") to be
8-
applied to L mode images::
8+
applied to 1 or L mode images::
99

1010
from PIL import Image, ImageMorph
1111
img = Image.open("Tests/images/hopper.bw")

src/PIL/ImageMorph.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -233,14 +233,15 @@ def apply(self, image: Image.Image) -> tuple[int, Image.Image]:
233233
Returns a tuple of the number of changed pixels and the
234234
morphed image.
235235
236+
:param image: A 1-mode or L-mode image.
236237
:exception Exception: If the current operator is None.
237-
:exception ValueError: If the image is not L mode."""
238+
:exception ValueError: If the image is not 1 or L mode."""
238239
if self.lut is None:
239240
msg = "No operator loaded"
240241
raise Exception(msg)
241242

242-
if image.mode != "L":
243-
msg = "Image mode must be L"
243+
if image.mode not in ("1", "L"):
244+
msg = "Image mode must be 1 or L"
244245
raise ValueError(msg)
245246
outimage = Image.new(image.mode, image.size)
246247
count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim())
@@ -253,29 +254,29 @@ def match(self, image: Image.Image) -> list[tuple[int, int]]:
253254
Returns a list of tuples of (x,y) coordinates of all matching pixels. See
254255
:ref:`coordinate-system`.
255256
256-
:param image: An L-mode image.
257+
:param image: A 1-mode or L-mode image.
257258
:exception Exception: If the current operator is None.
258-
:exception ValueError: If the image is not L mode."""
259+
:exception ValueError: If the image is not 1 or L mode."""
259260
if self.lut is None:
260261
msg = "No operator loaded"
261262
raise Exception(msg)
262263

263-
if image.mode != "L":
264-
msg = "Image mode must be L"
264+
if image.mode not in ("1", "L"):
265+
msg = "Image mode must be 1 or L"
265266
raise ValueError(msg)
266267
return _imagingmorph.match(bytes(self.lut), image.getim())
267268

268269
def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
269-
"""Get a list of all turned on pixels in a grayscale image
270+
"""Get a list of all turned on pixels in a 1 or L mode image.
270271
271272
Returns a list of tuples of (x,y) coordinates of all non-empty pixels. See
272273
:ref:`coordinate-system`.
273274
274-
:param image: An L-mode image.
275-
:exception ValueError: If the image is not L mode."""
275+
:param image: A 1-mode or L-mode image.
276+
:exception ValueError: If the image is not 1 or L mode."""
276277

277-
if image.mode != "L":
278-
msg = "Image mode must be L"
278+
if image.mode not in ("1", "L"):
279+
msg = "Image mode must be 1 or L"
279280
raise ValueError(msg)
280281
return _imagingmorph.get_on_pixels(image.getim())
281282

0 commit comments

Comments
 (0)