Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
ImageMorph.py318 linesDownload Raw Back to PIL
1# A binary morphology add-on for the Python Imaging Library
2#
3# History:
4#   2014-06-04 Initial version.
5#
6# Copyright (c) 2014 Dov Grobgeld <dov.grobgeld@gmail.com>
7from __future__ import annotations
8
9import re
10
11from . import Image, _imagingmorph
12
13LUT_SIZE = 1 << 9
14
15# fmt: off
16ROTATION_MATRIX = [
17    6, 3, 0,
18    7, 4, 1,
19    8, 5, 2,
20]
21MIRROR_MATRIX = [
22    2, 1, 0,
23    5, 4, 3,
24    8, 7, 6,
25]
26# fmt: on
27
28
29class LutBuilder:
30    """A class for building a MorphLut from a descriptive language
31
32    The input patterns is a list of a strings sequences like these::
33
34        4:(...
35           .1.
36           111)->1
37
38    (whitespaces including linebreaks are ignored). The option 4
39    describes a series of symmetry operations (in this case a
40    4-rotation), the pattern is described by:
41
42    - . or X - Ignore
43    - 1 - Pixel is on
44    - 0 - Pixel is off
45
46    The result of the operation is described after "->" string.
47
48    The default is to return the current pixel value, which is
49    returned if no other match is found.
50
51    Operations:
52
53    - 4 - 4 way rotation
54    - N - Negate
55    - 1 - Dummy op for no other operation (an op must always be given)
56    - M - Mirroring
57
58    Example::
59
60        lb = LutBuilder(patterns = ["4:(... .1. 111)->1"])
61        lut = lb.build_lut()
62
63    """
64
65    def __init__(
66        self, patterns: list[str] | None = None, op_name: str | None = None
67    ) -> None:
68        """
69        :param patterns: A list of input patterns, or None.
70        :param op_name: The name of a known pattern. One of "corner", "dilation4",
71           "dilation8", "erosion4", "erosion8" or "edge".
72        :exception Exception: If the op_name is not recognized.
73        """
74        self.lut: bytearray | None = None
75        if op_name is not None:
76            known_patterns = {
77                "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"],
78                "dilation4": ["4:(... .0. .1.)->1"],
79                "dilation8": ["4:(... .0. .1.)->1", "4:(... .0. ..1)->1"],
80                "erosion4": ["4:(... .1. .0.)->0"],
81                "erosion8": ["4:(... .1. .0.)->0", "4:(... .1. ..0)->0"],
82                "edge": [
83                    "1:(... ... ...)->0",
84                    "4:(.0. .1. ...)->1",
85                    "4:(01. .1. ...)->1",
86                ],
87            }
88            if op_name not in known_patterns:
89                msg = f"Unknown pattern {op_name}!"
90                raise Exception(msg)
91
92            self.patterns = known_patterns[op_name]
93        elif patterns is not None:
94            self.patterns = patterns
95        else:
96            self.patterns = []
97
98    def add_patterns(self, patterns: list[str]) -> None:
99        """
100        Append to list of patterns.
101
102        :param patterns: Additional patterns.
103        """
104        self.patterns += patterns
105
106    def build_default_lut(self) -> bytearray:
107        """
108        Set the current LUT, and return it.
109
110        This is the default LUT that patterns will be applied against when building.
111        """
112        symbols = [0, 1]
113        m = 1 << 4  # pos of current pixel
114        self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE))
115        return self.lut
116
117    def get_lut(self) -> bytearray | None:
118        """
119        Returns the current LUT
120        """
121        return self.lut
122
123    def _string_permute(self, pattern: str, permutation: list[int]) -> str:
124        """Takes a pattern and a permutation and returns the
125        string permuted according to the permutation list.
126        """
127        assert len(permutation) == 9
128        return "".join(pattern[p] for p in permutation)
129
130    def _pattern_permute(
131        self, basic_pattern: str, options: str, basic_result: int
132    ) -> list[tuple[str, int]]:
133        """Takes a basic pattern and its result and clones
134        the pattern according to the modifications described in the $options
135        parameter. It returns a list of all cloned patterns."""
136        patterns = [(basic_pattern, basic_result)]
137
138        # rotations
139        if "4" in options:
140            res = patterns[-1][1]
141            for i in range(4):
142                patterns.append(
143                    (self._string_permute(patterns[-1][0], ROTATION_MATRIX), res)
144                )
145        # mirror
146        if "M" in options:
147            n = len(patterns)
148            for pattern, res in patterns[:n]:
149                patterns.append((self._string_permute(pattern, MIRROR_MATRIX), res))
150
151        # negate
152        if "N" in options:
153            n = len(patterns)
154            for pattern, res in patterns[:n]:
155                # Swap 0 and 1
156                pattern = pattern.replace("0", "Z").replace("1", "0").replace("Z", "1")
157                res = 1 - int(res)
158                patterns.append((pattern, res))
159
160        return patterns
161
162    def build_lut(self) -> bytearray:
163        """Compile all patterns into a morphology LUT, and return it.
164
165        This is the data to be passed into MorphOp."""
166        self.build_default_lut()
167        assert self.lut is not None
168        patterns = []
169
170        # Parse and create symmetries of the patterns strings
171        for p in self.patterns:
172            m = re.search(r"(\w):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
173            if not m:
174                msg = 'Syntax error in pattern "' + p + '"'
175                raise Exception(msg)
176            options = m.group(1)
177            pattern = m.group(2)
178            result = int(m.group(3))
179
180            # Get rid of spaces
181            pattern = pattern.replace(" ", "").replace("\n", "")
182
183            patterns += self._pattern_permute(pattern, options, result)
184
185        # Compile the patterns into regular expressions for speed
186        compiled_patterns = []
187        for pattern in patterns:
188            p = pattern[0].replace(".", "X").replace("X", "[01]")
189            compiled_patterns.append((re.compile(p), pattern[1]))
190
191        # Step through table and find patterns that match.
192        # Note that all the patterns are searched. The last one found takes priority
193        for i in range(LUT_SIZE):
194            # Build the bit pattern
195            bitpattern = bin(i)[2:]
196            bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1]
197
198            for pattern, r in compiled_patterns:
199                if pattern.match(bitpattern):
200                    self.lut[i] = [0, 1][r]
201
202        return self.lut
203
204
205class MorphOp:
206    """A class for binary morphological operators"""
207
208    def __init__(
209        self,
210        lut: bytearray | None = None,
211        op_name: str | None = None,
212        patterns: list[str] | None = None,
213    ) -> None:
214        """Create a binary morphological operator.
215
216        If the LUT is not provided, then it is built using LutBuilder from the op_name
217        or the patterns.
218
219        :param lut: The LUT data.
220        :param patterns: A list of input patterns, or None.
221        :param op_name: The name of a known pattern. One of "corner", "dilation4",
222        "dilation8", "erosion4", "erosion8", "edge".
223        :exception Exception: If the op_name is not recognized.
224        """
225        if patterns is None and op_name is None:
226            self.lut = lut
227        else:
228            self.lut = LutBuilder(patterns, op_name).build_lut()
229
230    def apply(self, image: Image.Image) -> tuple[int, Image.Image]:
231        """Run a single morphological operation on an image.
232
233        Returns a tuple of the number of changed pixels and the
234        morphed image.
235
236        :param image: A 1-mode or L-mode image.
237        :exception Exception: If the current operator is None.
238        :exception ValueError: If the image is not 1 or L mode."""
239        if self.lut is None:
240            msg = "No operator loaded"
241            raise Exception(msg)
242
243        if image.mode not in ("1", "L"):
244            msg = "Image mode must be 1 or L"
245            raise ValueError(msg)
246        outimage = Image.new(image.mode, image.size)
247        count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim())
248        return count, outimage
249
250    def match(self, image: Image.Image) -> list[tuple[int, int]]:
251        """Get a list of coordinates matching the morphological operation on
252        an image.
253
254        Returns a list of tuples of (x,y) coordinates of all matching pixels. See
255        :ref:`coordinate-system`.
256
257        :param image: A 1-mode or L-mode image.
258        :exception Exception: If the current operator is None.
259        :exception ValueError: If the image is not 1 or L mode."""
260        if self.lut is None:
261            msg = "No operator loaded"
262            raise Exception(msg)
263
264        if image.mode not in ("1", "L"):
265            msg = "Image mode must be 1 or L"
266            raise ValueError(msg)
267        return _imagingmorph.match(bytes(self.lut), image.getim())
268
269    def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
270        """Get a list of all turned on pixels in a 1 or L mode image.
271
272        Returns a list of tuples of (x,y) coordinates of all non-empty pixels. See
273        :ref:`coordinate-system`.
274
275        :param image: A 1-mode or L-mode image.
276        :exception ValueError: If the image is not 1 or L mode."""
277
278        if image.mode not in ("1", "L"):
279            msg = "Image mode must be 1 or L"
280            raise ValueError(msg)
281        return _imagingmorph.get_on_pixels(image.getim())
282
283    def load_lut(self, filename: str) -> None:
284        """
285        Load an operator from an mrl file
286
287        :param filename: The file to read from.
288        :exception Exception: If the length of the file data is not 512.
289        """
290        with open(filename, "rb") as f:
291            self.lut = bytearray(f.read())
292
293        if len(self.lut) != LUT_SIZE:
294            self.lut = None
295            msg = "Wrong size operator file!"
296            raise Exception(msg)
297
298    def save_lut(self, filename: str) -> None:
299        """
300        Save an operator to an mrl file.
301
302        :param filename: The destination file.
303        :exception Exception: If the current operator is None.
304        """
305        if self.lut is None:
306            msg = "No operator loaded"
307            raise Exception(msg)
308        with open(filename, "wb") as f:
309            f.write(self.lut)
310
311    def set_lut(self, lut: bytearray | None) -> None:
312        """
313        Set the LUT from an external source
314
315        :param lut: A new LUT.
316        """
317        self.lut = lut
318 
codekingpro/portable-devtools · Team Ai