Source code for node_morphology

# 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 ImagePort
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.floating): 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([ ImagePort('Target image', name='target'), ImagePort('Structuring element', name='structuring_element'), ]) outputs = Ports([ ImagePort('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)