# This file is part of Sympathy for Data.
# Copyright (c) 2017, Combine Control Systems AB
#
# Sympathy for Data is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# Sympathy for Data is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Sympathy for Data. If not, see <http://www.gnu.org/licenses/>.
from sympathy.api import node
from sympathy.api.nodeconfig import Ports, Tag, Tags
import numpy as np
from skimage import morphology, filters
from sylib.imageprocessing.image import Image
from sylib.imageprocessing.algorithm_selector import ImageFiltering_abstract
[docs]class ImageMorphology(ImageFiltering_abstract, node.Node):
name = 'Morphological Image Operations'
author = 'Mathias Broxvall'
version = '0.1'
icon = 'image_morphology.svg'
description = (
'Performs one of a selection of morphological or rank operations on a '
'target image (top) that uses a given structuring element (bottom). '
'Other morphological operations can be found under the "filter" node')
nodeid = 'syip.imagemorphology'
tags = Tags(Tag.ImageProcessing.ImageManipulation)
dilation = (
'Morphological dilation sets a pixel at *(i,j)* to the maximum over '
'all pixels in the neighborhood defined by\nthe structuring element '
'centered at *(i,j)*. Dilation enlarges bright regions and shrinks '
'dark regions.')
erosion = ('Morphological erosion sets a pixel at *(i,j)* to the minimum '
'over all pixels in the neighborhood centered at *(i,j)*. '
'Erosion shrinks bright regions and enlarges dark regions.')
closing = ('The morphological closing on an image is defined as a '
'dilation followed by an erosion.\nClosing can remove small '
'dark spots and connect small bright cracks.')
opening = ('The morphological opening on an image is defined as a erosion '
'followed by a dilation.\nOpening can remove small white spots '
'and connect small dark cracks.')
def alg_median(target, se, par):
if np.issubdtype(target.dtype, np.float):
maxval = np.max(target)
minval = np.min(target)
target = (target - minval) * 256 / (maxval - minval)
result = filters.median(target.astype('uint8'), se)
return result * (maxval - minval) / 256.0 + minval
else:
return filters.median(target, se)
api = 'http://scikit-image.org/docs/0.13.x/api/'
algorithms = {
'binary, closing': {
'description': (
'Perform morphological closing on image. Target and '
'structuring element must be binary.\n\n' + closing),
'multi_chromatic': False,
'algorithm': (
lambda target, se, par: morphology.binary_closing(target, se))
},
'binary, dilation': {
'description': (
'Perform morphological dilation on image. Target and '
'structuring element must be binary.\n\n' + dilation),
'multi_chromatic': False,
'algorithm': (
lambda target, se, par: morphology.binary_dilation(target, se))
},
'binary, erosion': {
'description': (
'Perform morphological erosion on image. Target and '
'structuring element must be binary.\n\n' + erosion),
'multi_chromatic': False,
'algorithm': (
lambda target, se, par: morphology.binary_erosion(target, se))
},
'binary, opening': {
'description': (
'Perform morphological opening on image. Target and '
'structuring element must be binary\n\n'+opening),
'multi_chromatic': False,
'algorithm': (
lambda target, se, par: morphology.binary_opening(target, se))
},
'grey, closing': {
'description': (
'Perform greyscale morphological closing on image.\n\n' +
closing),
'multi_chromatic': False,
'algorithm': lambda target, se, par: morphology.closing(target, se)
},
'grey, opening': {
'description': (
'Perform greyscale morphological opening on image.\n\n' +
opening),
'multi_chromatic': False,
'algorithm': lambda target, se, par: morphology.opening(target, se)
},
'grey, dilation': {
'description': (
'Return greyscale morphological dilation of an image.\n\n' +
dilation),
'multi_chromatic': False,
'algorithm': lambda target, se, par: morphology.dilation(
target, se)
},
'grey, erosion': {
'description': (
'Return greyscale morphological erosion of an image.\n\n' +
erosion),
'multi_chromatic': False,
'algorithm': lambda target, se, par: morphology.erosion(target, se)
},
'tophat, black': {
'description': (
'Return morphological black-tophat of image.\n\nThe black top '
'hat of an image is defined as its morphological closing '
'minus the original image.\nThis operation returns the dark '
'spots of the image that are smaller than the structuring '
'element.\nNote that dark spots in the original image are '
'bright spots after the black top hat.'),
'multi_chromatic': False,
'algorithm': (
lambda target, se, par: morphology.black_tophat(target, se))
},
'tophat, white': {
'description': (
'Return morphological white-tophat of image.\n\n'
'The white top hat of an image is defined as the image minus '
'its morphological opening.\nThis operation returns the white '
'spots of the image that are smaller than the structuring '
'element.'),
'multi_chromatic': False,
'algorithm': (
lambda target, se, par: morphology.white_tophat(target, se))
},
'median': {
'description': (
'Return the median of the neighborhood defined by sweeping '
'the structuring element over the image.\nWhen used on '
'non-integer datatypes they are first cast into uint8.\nThis '
'operation functions as a filter suitable to removing '
'salt-and-pepper noise.'),
'multi_chromatic': False,
'algorithm': alg_median
},
'autolevel': {
'description': (
'Stretches the local histogram defined by structuring element '
'to cover the full range.'),
'p0': 'Defines the lower percentile (0..1) included in histogram',
'p1': 'Defines the upper percentile (0..1) included in histogram',
'multi_chromatic': False,
'url': (
api + 'skimage.filters.rank.html' +
'#skimage.filters.rank.autolevel'),
'algorithm': (lambda target, se, par:
filters.rank.autolevel_percentile(
target, se, p0=par['p0'].value,
p1=par['p1'].value) / 256.0)
},
}
options_list = ['p0', 'p1', ]
options_types = {
'p0': float,
'p1': float,
}
options_default = {
'p0': 0.25,
'p1': 0.75,
}
parameters = node.parameters()
parameters.set_string(
'algorithm',
value=next(iter(algorithms)), description='', label='Algorithm')
ImageFiltering_abstract.generate_parameters(
parameters, options_types, options_default)
inputs = Ports([
Image('Target image', name='target'),
Image('Structuring element', name='structuring_element'),
])
outputs = Ports([
Image('Resulting image', name='output'),
])
__doc__ = ImageFiltering_abstract.generate_docstring(
description, algorithms, options_list, inputs, outputs)
def execute(self, node_context):
target = node_context.input['target'].get_image()
se = node_context.input['structuring_element'].get_image()
params = node_context.parameters
alg_name = params['algorithm'].value
alg = self.algorithms[alg_name]['algorithm']
if len(target.shape) == 3 and target.shape[2] > 1:
multichannel_image = True
else:
multichannel_image = False
alg = self.algorithms[alg_name]['algorithm']
if len(se.shape) == 3 and se.shape[2] == 1:
se = se.reshape(se.shape[:2])
if len(target.shape) == 3 and target.shape[2] == 1:
target = target.reshape(target.shape[:2])
if (multichannel_image and
not self.algorithms[alg_name]['multi_chromatic']):
im1 = alg(target[:, :, 0], se, params)
im = np.zeros(im1.shape[:2] + (target.shape[2],))
im[:, :, 0] = im1
for channel in range(1, target.shape[2]):
im[:, :, channel] = alg(target[:, :, channel], se, params)
else:
im = alg(target, se, params)
node_context.output['output'].set_image(im)