2014-06-05 00:03:00 +04:00
|
|
|
# Test the ImageMorphology functionality
|
2023-12-21 14:13:31 +03:00
|
|
|
from __future__ import annotations
|
2024-01-20 14:23:03 +03:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
from pathlib import Path
|
|
|
|
|
2020-02-22 16:06:21 +03:00
|
|
|
import pytest
|
2020-09-01 20:16:46 +03:00
|
|
|
|
2018-04-22 19:51:57 +03:00
|
|
|
from PIL import Image, ImageMorph, _imagingmorph
|
2014-06-05 00:03:00 +04:00
|
|
|
|
2021-02-21 14:15:56 +03:00
|
|
|
from .helper import assert_image_equal_tofile, hopper
|
2020-03-22 22:54:54 +03:00
|
|
|
|
|
|
|
|
2024-02-12 13:06:17 +03:00
|
|
|
def string_to_img(image_string: str) -> Image.Image:
|
2020-03-22 22:54:54 +03:00
|
|
|
"""Turn a string image representation into a binary 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)
|
|
|
|
|
|
|
|
return im
|
|
|
|
|
|
|
|
|
|
|
|
A = string_to_img(
|
|
|
|
"""
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
..111..
|
|
|
|
..111..
|
|
|
|
..111..
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-02-12 13:06:17 +03:00
|
|
|
def img_to_string(im: Image.Image) -> str:
|
2020-03-22 22:54:54 +03:00
|
|
|
"""Turn a (small) binary image into a string representation"""
|
|
|
|
chars = ".1"
|
2024-07-05 20:56:24 +03:00
|
|
|
result = []
|
|
|
|
for r in range(im.height):
|
|
|
|
line = ""
|
|
|
|
for c in range(im.width):
|
|
|
|
value = im.getpixel((c, r))
|
2024-08-16 14:52:56 +03:00
|
|
|
assert not isinstance(value, tuple)
|
|
|
|
assert value is not None
|
2024-07-05 20:56:24 +03:00
|
|
|
line += chars[value > 0]
|
|
|
|
result.append(line)
|
|
|
|
return "\n".join(result)
|
2020-03-22 22:54:54 +03:00
|
|
|
|
|
|
|
|
2024-02-12 13:06:17 +03:00
|
|
|
def img_string_normalize(im: str) -> str:
|
2020-03-22 22:54:54 +03:00
|
|
|
return img_to_string(string_to_img(im))
|
|
|
|
|
|
|
|
|
2024-02-12 13:06:17 +03:00
|
|
|
def assert_img_equal_img_string(a: Image.Image, b_string: str) -> None:
|
2022-04-10 22:23:55 +03:00
|
|
|
assert img_to_string(a) == img_string_normalize(b_string)
|
2020-03-22 22:54:54 +03:00
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_str_to_img() -> None:
|
2021-02-21 14:15:56 +03:00
|
|
|
assert_image_equal_tofile(A, "Tests/images/morph_a.png")
|
2020-03-22 22:54:54 +03:00
|
|
|
|
|
|
|
|
2022-10-03 08:57:42 +03:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
"op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
|
|
|
|
)
|
2024-02-12 13:06:17 +03:00
|
|
|
def test_lut(op: str) -> None:
|
2022-10-03 08:57:42 +03:00
|
|
|
lb = ImageMorph.LutBuilder(op_name=op)
|
|
|
|
assert lb.get_lut() is None
|
2020-03-22 22:54:54 +03:00
|
|
|
|
2022-10-03 08:57:42 +03:00
|
|
|
lut = lb.build_lut()
|
|
|
|
with open(f"Tests/images/{op}.lut", "rb") as f:
|
|
|
|
assert lut == bytearray(f.read())
|
2020-03-22 22:54:54 +03:00
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_no_operator_loaded() -> None:
|
2024-03-02 05:12:17 +03:00
|
|
|
im = Image.new("L", (1, 1))
|
2020-03-22 22:54:54 +03:00
|
|
|
mop = ImageMorph.MorphOp()
|
|
|
|
with pytest.raises(Exception) as e:
|
2024-03-02 05:12:17 +03:00
|
|
|
mop.apply(im)
|
2020-03-22 22:54:54 +03:00
|
|
|
assert str(e.value) == "No operator loaded"
|
|
|
|
with pytest.raises(Exception) as e:
|
2024-03-02 05:12:17 +03:00
|
|
|
mop.match(im)
|
2020-03-22 22:54:54 +03:00
|
|
|
assert str(e.value) == "No operator loaded"
|
|
|
|
with pytest.raises(Exception) as e:
|
2024-03-02 05:12:17 +03:00
|
|
|
mop.save_lut("")
|
2020-03-22 22:54:54 +03:00
|
|
|
assert str(e.value) == "No operator loaded"
|
|
|
|
|
|
|
|
|
|
|
|
# Test the named patterns
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_erosion8() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
# erosion8
|
|
|
|
mop = ImageMorph.MorphOp(op_name="erosion8")
|
|
|
|
count, Aout = mop.apply(A)
|
|
|
|
assert count == 8
|
|
|
|
assert_img_equal_img_string(
|
|
|
|
Aout,
|
|
|
|
"""
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
...1...
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_dialation8() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
# dialation8
|
|
|
|
mop = ImageMorph.MorphOp(op_name="dilation8")
|
|
|
|
count, Aout = mop.apply(A)
|
|
|
|
assert count == 16
|
|
|
|
assert_img_equal_img_string(
|
|
|
|
Aout,
|
|
|
|
"""
|
|
|
|
.......
|
|
|
|
.11111.
|
|
|
|
.11111.
|
|
|
|
.11111.
|
|
|
|
.11111.
|
|
|
|
.11111.
|
|
|
|
.......
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_erosion4() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
# erosion4
|
|
|
|
mop = ImageMorph.MorphOp(op_name="dilation4")
|
|
|
|
count, Aout = mop.apply(A)
|
|
|
|
assert count == 12
|
|
|
|
assert_img_equal_img_string(
|
|
|
|
Aout,
|
|
|
|
"""
|
|
|
|
.......
|
|
|
|
..111..
|
|
|
|
.11111.
|
|
|
|
.11111.
|
|
|
|
.11111.
|
|
|
|
..111..
|
|
|
|
.......
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_edge() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
# edge
|
|
|
|
mop = ImageMorph.MorphOp(op_name="edge")
|
|
|
|
count, Aout = mop.apply(A)
|
|
|
|
assert count == 1
|
|
|
|
assert_img_equal_img_string(
|
|
|
|
Aout,
|
|
|
|
"""
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
..111..
|
|
|
|
..1.1..
|
|
|
|
..111..
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_corner() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
# Create a corner detector pattern
|
|
|
|
mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"])
|
|
|
|
count, Aout = mop.apply(A)
|
|
|
|
assert count == 5
|
|
|
|
assert_img_equal_img_string(
|
|
|
|
Aout,
|
|
|
|
"""
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
..1.1..
|
|
|
|
.......
|
|
|
|
..1.1..
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
|
|
|
|
# Test the coordinate counting with the same operator
|
|
|
|
coords = mop.match(A)
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_mirroring() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
# Test 'M' for mirroring
|
|
|
|
mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"])
|
|
|
|
count, Aout = mop.apply(A)
|
|
|
|
assert count == 7
|
|
|
|
assert_img_equal_img_string(
|
|
|
|
Aout,
|
|
|
|
"""
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
..1.1..
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_negate() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
# Test 'N' for negate
|
|
|
|
mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"])
|
|
|
|
count, Aout = mop.apply(A)
|
|
|
|
assert count == 8
|
|
|
|
assert_img_equal_img_string(
|
|
|
|
Aout,
|
|
|
|
"""
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
..1....
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
.......
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_incorrect_mode() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
im = hopper("RGB")
|
|
|
|
mop = ImageMorph.MorphOp(op_name="erosion8")
|
|
|
|
|
2021-04-18 07:47:05 +03:00
|
|
|
with pytest.raises(ValueError) as e:
|
2020-03-22 22:54:54 +03:00
|
|
|
mop.apply(im)
|
2021-04-18 07:47:05 +03:00
|
|
|
assert str(e.value) == "Image mode must be L"
|
|
|
|
with pytest.raises(ValueError) as e:
|
2020-03-22 22:54:54 +03:00
|
|
|
mop.match(im)
|
2021-04-18 07:47:05 +03:00
|
|
|
assert str(e.value) == "Image mode must be L"
|
|
|
|
with pytest.raises(ValueError) as e:
|
2020-03-22 22:54:54 +03:00
|
|
|
mop.get_on_pixels(im)
|
2021-04-18 07:47:05 +03:00
|
|
|
assert str(e.value) == "Image mode must be L"
|
2020-03-22 22:54:54 +03:00
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_add_patterns() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
# Arrange
|
|
|
|
lb = ImageMorph.LutBuilder(op_name="corner")
|
|
|
|
assert lb.patterns == ["1:(... ... ...)->0", "4:(00. 01. ...)->1"]
|
|
|
|
new_patterns = ["M:(00. 01. ...)->1", "N:(00. 01. ...)->1"]
|
|
|
|
|
|
|
|
# Act
|
|
|
|
lb.add_patterns(new_patterns)
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert lb.patterns == [
|
|
|
|
"1:(... ... ...)->0",
|
|
|
|
"4:(00. 01. ...)->1",
|
|
|
|
"M:(00. 01. ...)->1",
|
|
|
|
"N:(00. 01. ...)->1",
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_unknown_pattern() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
with pytest.raises(Exception):
|
|
|
|
ImageMorph.LutBuilder(op_name="unknown")
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_pattern_syntax_error() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
# Arrange
|
|
|
|
lb = ImageMorph.LutBuilder(op_name="corner")
|
|
|
|
new_patterns = ["a pattern with a syntax error"]
|
|
|
|
lb.add_patterns(new_patterns)
|
|
|
|
|
|
|
|
# Act / Assert
|
|
|
|
with pytest.raises(Exception) as e:
|
|
|
|
lb.build_lut()
|
|
|
|
assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"'
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_load_invalid_mrl() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
# Arrange
|
|
|
|
invalid_mrl = "Tests/images/hopper.png"
|
|
|
|
mop = ImageMorph.MorphOp()
|
|
|
|
|
|
|
|
# Act / Assert
|
|
|
|
with pytest.raises(Exception) as e:
|
|
|
|
mop.load_lut(invalid_mrl)
|
|
|
|
assert str(e.value) == "Wrong size operator file!"
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_roundtrip_mrl(tmp_path: Path) -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
# Arrange
|
|
|
|
tempfile = str(tmp_path / "temp.mrl")
|
|
|
|
mop = ImageMorph.MorphOp(op_name="corner")
|
|
|
|
initial_lut = mop.lut
|
|
|
|
|
|
|
|
# Act
|
|
|
|
mop.save_lut(tempfile)
|
|
|
|
mop.load_lut(tempfile)
|
|
|
|
|
|
|
|
# Act / Assert
|
|
|
|
assert mop.lut == initial_lut
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_set_lut() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
# Arrange
|
|
|
|
lb = ImageMorph.LutBuilder(op_name="corner")
|
|
|
|
lut = lb.build_lut()
|
|
|
|
mop = ImageMorph.MorphOp()
|
|
|
|
|
|
|
|
# Act
|
|
|
|
mop.set_lut(lut)
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert mop.lut == lut
|
2017-05-29 08:29:12 +03:00
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_wrong_mode() -> None:
|
2020-03-22 22:54:54 +03:00
|
|
|
lut = ImageMorph.LutBuilder(op_name="corner").build_lut()
|
|
|
|
imrgb = Image.new("RGB", (10, 10))
|
|
|
|
iml = Image.new("L", (10, 10))
|
2018-04-22 19:51:57 +03:00
|
|
|
|
2020-03-22 22:54:54 +03:00
|
|
|
with pytest.raises(RuntimeError):
|
2024-09-02 00:43:38 +03:00
|
|
|
_imagingmorph.apply(bytes(lut), imrgb.im.ptr, iml.im.ptr)
|
2018-04-22 19:51:57 +03:00
|
|
|
|
2020-03-22 22:54:54 +03:00
|
|
|
with pytest.raises(RuntimeError):
|
2024-09-02 00:43:38 +03:00
|
|
|
_imagingmorph.apply(bytes(lut), iml.im.ptr, imrgb.im.ptr)
|
2018-04-22 19:51:57 +03:00
|
|
|
|
2020-03-22 22:54:54 +03:00
|
|
|
with pytest.raises(RuntimeError):
|
2024-09-02 00:43:38 +03:00
|
|
|
_imagingmorph.match(bytes(lut), imrgb.im.ptr)
|
2018-04-22 19:51:57 +03:00
|
|
|
|
2020-03-22 22:54:54 +03:00
|
|
|
# Should not raise
|
2024-09-02 00:43:38 +03:00
|
|
|
_imagingmorph.match(bytes(lut), iml.im.ptr)
|