Pillow/PIL/ImageMorph.py

252 lines
8.1 KiB
Python
Raw Normal View History

# A binary morphology add-on for the Python Imaging Library
#
# History:
# 2014-06-04 Initial version.
#
# Copyright (c) 2014 Dov Grobgeld <dov.grobgeld@gmail.com>
2016-11-26 03:05:56 +03:00
from __future__ import print_function
from PIL import Image
from PIL import _imagingmorph
import re
2014-06-24 10:34:05 +04:00
LUT_SIZE = 1 << 9
2015-05-26 17:07:21 +03:00
class LutBuilder(object):
2014-06-24 10:34:05 +04:00
"""A class for building a MorphLut from a descriptive language
2014-07-12 20:52:04 +04:00
The input patterns is a list of a strings sequences like these::
2014-07-12 20:52:04 +04:00
4:(...
.1.
111)->1
(whitespaces including linebreaks are ignored). The option 4
2014-06-24 10:34:05 +04:00
describes a series of symmetry operations (in this case a
4-rotation), the pattern is described by:
2014-07-12 20:52:04 +04:00
- . or X - Ignore
- 1 - Pixel is on
- 0 - Pixel is off
The result of the operation is described after "->" string.
The default is to return the current pixel value, which is
returned if no other match is found.
Operations:
2014-08-26 17:47:10 +04:00
2014-07-12 20:52:04 +04:00
- 4 - 4 way rotation
- N - Negate
- 1 - Dummy op for no other operation (an op must always be given)
- M - Mirroring
2014-07-12 20:52:04 +04:00
Example::
2014-08-26 17:47:10 +04:00
2014-07-12 20:52:04 +04:00
lb = LutBuilder(patterns = ["4:(... .1. 111)->1"])
lut = lb.build_lut()
2014-06-24 10:34:05 +04:00
"""
2014-06-24 10:34:05 +04:00
def __init__(self, patterns=None, op_name=None):
if patterns is not None:
self.patterns = patterns
else:
self.patterns = []
self.lut = None
if op_name is not None:
known_patterns = {
2014-06-24 10:34:05 +04:00
'corner': ['1:(... ... ...)->0',
'4:(00. 01. ...)->1'],
'dilation4': ['4:(... .0. .1.)->1'],
'dilation8': ['4:(... .0. .1.)->1',
'4:(... .0. ..1)->1'],
'erosion4': ['4:(... .1. .0.)->0'],
'erosion8': ['4:(... .1. .0.)->0',
'4:(... .1. ..0)->0'],
'edge': ['1:(... ... ...)->0',
'4:(.0. .1. ...)->1',
'4:(01. .1. ...)->1']
}
2014-06-24 10:34:05 +04:00
if op_name not in known_patterns:
raise Exception('Unknown pattern '+op_name+'!')
self.patterns = known_patterns[op_name]
def add_patterns(self, patterns):
self.patterns += patterns
def build_default_lut(self):
2014-06-20 00:21:14 +04:00
symbols = [0, 1]
m = 1 << 4 # pos of current pixel
self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE))
def get_lut(self):
return self.lut
def _string_permute(self, pattern, permutation):
"""string_permute takes a pattern and a permutation and returns the
2014-06-24 10:34:05 +04:00
string permuted according to the permutation list.
"""
2014-06-24 10:34:05 +04:00
assert(len(permutation) == 9)
return ''.join(pattern[p] for p in permutation)
def _pattern_permute(self, basic_pattern, options, basic_result):
"""pattern_permute takes a basic pattern and its result and clones
2014-06-24 10:34:05 +04:00
the pattern according to the modifications described in the $options
parameter. It returns a list of all cloned patterns."""
patterns = [(basic_pattern, basic_result)]
# rotations
if '4' in options:
res = patterns[-1][1]
for i in range(4):
patterns.append(
2014-06-24 10:34:05 +04:00
(self._string_permute(patterns[-1][0], [6, 3, 0,
7, 4, 1,
8, 5, 2]), res))
# mirror
if 'M' in options:
n = len(patterns)
2014-06-24 10:34:05 +04:00
for pattern, res in patterns[0:n]:
patterns.append(
2014-06-24 10:34:05 +04:00
(self._string_permute(pattern, [2, 1, 0,
5, 4, 3,
8, 7, 6]), res))
# negate
if 'N' in options:
n = len(patterns)
2014-06-24 10:34:05 +04:00
for pattern, res in patterns[0:n]:
# Swap 0 and 1
pattern = (pattern
2014-06-24 10:34:05 +04:00
.replace('0', 'Z')
.replace('1', '0')
.replace('Z', '1'))
res = '%d' % (1-int(res))
patterns.append((pattern, res))
return patterns
def build_lut(self):
"""Compile all patterns into a morphology lut.
TBD :Build based on (file) morphlut:modify_lut
"""
self.build_default_lut()
patterns = []
# Parse and create symmetries of the patterns strings
for p in self.patterns:
2014-06-24 10:34:05 +04:00
m = re.search(
r'(\w*):?\s*\((.+?)\)\s*->\s*(\d)', p.replace('\n', ''))
if not m:
raise Exception('Syntax error in pattern "'+p+'"')
options = m.group(1)
pattern = m.group(2)
result = int(m.group(3))
# Get rid of spaces
2014-06-24 10:34:05 +04:00
pattern = pattern.replace(' ', '').replace('\n', '')
patterns += self._pattern_permute(pattern, options, result)
# # Debugging
# for p,r in patterns:
# print(p,r)
# print('--')
# compile the patterns into regular expressions for speed
2016-11-06 04:38:59 +03:00
for i, pattern in enumerate(patterns):
p = pattern[0].replace('.', 'X').replace('X', '[01]')
p = re.compile(p)
2016-11-06 04:38:59 +03:00
patterns[i] = (p, pattern[1])
# Step through table and find patterns that match.
# Note that all the patterns are searched. The last one
# caught overrides
for i in range(LUT_SIZE):
# Build the bit pattern
bitpattern = bin(i)[2:]
bitpattern = ('0'*(9-len(bitpattern)) + bitpattern)[::-1]
2014-06-24 10:34:05 +04:00
for p, r in patterns:
if p.match(bitpattern):
2014-06-20 00:21:14 +04:00
self.lut[i] = [0, 1][r]
return self.lut
2014-06-24 10:34:05 +04:00
2015-05-26 17:07:21 +03:00
class MorphOp(object):
"""A class for binary morphological operators"""
def __init__(self,
lut=None,
2014-06-24 10:34:05 +04:00
op_name=None,
patterns=None):
"""Create a binary morphological operator"""
self.lut = lut
if op_name is not None:
2014-06-24 10:34:05 +04:00
self.lut = LutBuilder(op_name=op_name).build_lut()
elif patterns is not None:
2014-06-24 10:34:05 +04:00
self.lut = LutBuilder(patterns=patterns).build_lut()
def apply(self, image):
"""Run a single morphological operation on an image
Returns a tuple of the number of changed pixels and the
morphed image"""
if self.lut is None:
raise Exception('No operator loaded')
2014-06-24 10:34:05 +04:00
if image.mode != 'L':
raise Exception('Image must be binary, meaning it must use mode L')
outimage = Image.new(image.mode, image.size, None)
2014-06-24 10:34:05 +04:00
count = _imagingmorph.apply(
bytes(self.lut), image.im.id, outimage.im.id)
return count, outimage
2014-06-24 10:34:05 +04:00
def match(self, image):
2014-06-24 10:34:05 +04:00
"""Get a list of coordinates matching the morphological operation on
an image.
2014-06-24 10:34:05 +04:00
Returns a list of tuples of (x,y) coordinates
of all matching pixels."""
if self.lut is None:
raise Exception('No operator loaded')
2014-06-24 10:34:05 +04:00
if image.mode != 'L':
raise Exception('Image must be binary, meaning it must use mode L')
2014-06-24 10:34:05 +04:00
return _imagingmorph.match(bytes(self.lut), image.im.id)
def get_on_pixels(self, image):
"""Get a list of all turned on pixels in a binary image
2014-06-24 10:34:05 +04:00
Returns a list of tuples of (x,y) coordinates
of all matching pixels."""
if image.mode != 'L':
raise Exception('Image must be binary, meaning it must use mode L')
2014-06-24 10:34:05 +04:00
return _imagingmorph.get_on_pixels(image.im.id)
def load_lut(self, filename):
"""Load an operator from an mrl file"""
2014-06-24 10:34:05 +04:00
with open(filename, 'rb') as f:
2014-06-20 00:21:40 +04:00
self.lut = bytearray(f.read())
2014-06-24 10:34:05 +04:00
if len(self.lut) != 8192:
self.lut = None
raise Exception('Wrong size operator file!')
def save_lut(self, filename):
2014-06-24 10:51:42 +04:00
"""Save an operator to an mrl file"""
if self.lut is None:
raise Exception('No operator loaded')
2014-06-24 10:34:05 +04:00
with open(filename, 'wb') as f:
2014-06-20 00:21:40 +04:00
f.write(self.lut)
def set_lut(self, lut):
"""Set the lut from an external source"""
self.lut = lut