Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Binary file modified Tests/images/morph_a.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 16 additions & 17 deletions Tests/test_imagemorph.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@ def string_to_img(image_string: str) -> 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


Expand All @@ -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]
Expand Down Expand Up @@ -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,
Expand All @@ -184,7 +183,7 @@ 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))

Expand Down Expand Up @@ -232,14 +231,14 @@ 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 L"):
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
mop.get_on_pixels(im)


Expand Down
2 changes: 1 addition & 1 deletion docs/reference/ImageMorph.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
================================

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

from PIL import Image, ImageMorph
img = Image.open("Tests/images/hopper.bw")
Expand Down
25 changes: 13 additions & 12 deletions src/PIL/ImageMorph.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,15 @@ def apply(self, image: Image.Image) -> tuple[int, Image.Image]:
Returns a tuple of the number of changed pixels and the
morphed image.

:param image: A 1-mode or L-mode image.
:exception Exception: If the current operator is None.
:exception ValueError: If the image is not L mode."""
:exception ValueError: If the image is not 1 or L mode."""
if self.lut is None:
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)
count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim())
Expand All @@ -253,29 +254,29 @@ def match(self, image: Image.Image) -> list[tuple[int, int]]:
Returns a list of tuples of (x,y) coordinates of all matching pixels. See
:ref:`coordinate-system`.

:param image: An L-mode image.
:param image: A 1-mode or L-mode image.
:exception Exception: If the current operator is None.
:exception ValueError: If the image is not L mode."""
:exception ValueError: If the image is not 1 or L mode."""
if self.lut is None:
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())

def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
"""Get a list of all turned on pixels in a grayscale 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 non-empty pixels. See
:ref:`coordinate-system`.

:param image: An L-mode image.
:exception ValueError: If the image is not L mode."""
:param image: A 1-mode or L-mode image.
:exception ValueError: If the image is not 1 or L mode."""

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())

Expand Down
Loading