# This file is part of Sympathy for Data.
# Copyright (c) 2018, Combine Control Systems AB
#
# SYMPATHY FOR DATA COMMERCIAL LICENSE
# You should have received a link to the License with Sympathy for Data.
from packaging import version
from sympathy.api import node
from sympathy.api.nodeconfig import Ports, Tag, Tags
import skimage
import skimage.segmentation as seg
from sympathy.api.exceptions import SyDataError
from sylib.imageprocessing.image import ImagePort
from sylib.imageprocessing.algorithm_selector import ImageFiltering_abstract
from sylib.imageprocessing.generic_filtering import GenericImageFiltering
skimage_version = version.Version(skimage.__version__)
def alg_kmeans(im, par):
lab = par['CIE Lab'].value and im.shape[-1] == 3
if skimage_version >= version.Version('0.19.0'):
return seg.slic(im,
n_segments=par['n'].value,
max_num_iter=par['iter'].value,
compactness=par['compactness'].value,
sigma=par['sigma'].value,
enforce_connectivity=par['force connectivity'].value,
convert2lab=lab)
else:
return seg.slic(im,
n_segments=par['n'].value,
max_iter=par['iter'].value,
compactness=par['compactness'].value,
sigma=par['sigma'].value,
enforce_connectivity=par['force connectivity'].value,
convert2lab=lab)
def alg_quickshift(im, par):
if im.shape[-1] != 3:
raise SyDataError('Quickshift algorithm requires RGB images as inputs')
lab = par['CIE Lab'].value and im.shape[-1] == 3
return seg.quickshift(im,
ratio=par['ratio'].value,
kernel_size=par['kernel size'].value,
max_dist=par['max dist'].value,
sigma=par['sigma'].value,
convert2lab=lab)
def alg_watershed(im, par):
return seg.watershed(im,
compactness=par['compact'].value,
watershed_line=par['line'].value,
markers=par['n'].value)
def alg_felzenszwalb(im, par):
return seg.felzenszwalb(
im,
scale=par['scale'].value,
sigma=par['sigma'].value,
min_size=par['min size'].value)
def alg_chan_vese(im, par):
kwargs = dict(
mu=par['mu'].value,
lambda1=par['lambda1'].value,
lambda2=par['lambda2'].value,
dt=par['dt'].value,
init_level_set=par['initial level set'].value)
if par['morphological'].value:
return seg.chan_vese(
im, **kwargs)
else:
return seg.morphological_chan_vese(
im, **kwargs)
def alg_watershed_markers(im, markers, par):
return seg.watershed(
im,
compactness=par['compact'].value,
watershed_line=par['line'].value,
markers=markers)
def alg_random_walker(im, markers, par):
if markers.shape != im.shape:
raise SyDataError(
'Random walker algorithm requires identical size '
'and number of channels in input image and in markers')
return seg.random_walker(im, markers, beta=par['beta'].value)
SEGMENTATION_ALGS = {
'K-means': {
'description': (
'Segments image using K-means clustering in color and spatial\n'
'space.'),
'multi_chromatic': True,
'n': 'Number of clusters for K-means',
'compactness': (
'Balances color proximity and space proximity. Higher values\n'
'give more weight to space proximity, making superpixel shapes\n'
'more square/cubic. In SLICO mode, this is the initial \n'
'compactness. This parameter depends strongly on image contrast \n'
'and on the shapes of objects in the image. We recommend\n'
'exploring possible values on a log scale, e.g., 0.01, 0.1, \n'
'1, 10, 100, before refining around a chosen value.'),
'iter': 'Maximum number of iterations of K-means',
'sigma': (
'Width of a gaussian kernel used to smooth image before K-means'
),
'CIE Lab': (
'If true (default) then image is converted to CIE-LAB colorspace\n'
'before K-means, afterwards converted back to RGB. Image must\n'
'be a 3 channel RGB image'
),
'force connectivity': 'Forces the generated segments to be continous',
'algorithm': alg_kmeans,
},
'Quickshift': {
'description': (
'Segments image using quickshift clustering in color and '
'spatial space. Requires RGB images as inputs.'
),
'multi_chromatic': True,
'ratio': (
'A value between 0.0 to 1.0. Balances between color and\n'
'image space proximity'),
'kernel size': (
'Width of gaussian kernel smoothing sample density.\n'
'Higher values mean fewer clusters.'),
'max dist': (
'Cut-off point for data distances. Higher means fewer clusters.'),
'sigma': (
'Width of a gaussian kernel used to smooth image before K-means'),
'CIE Lab': (
'If true (default) then image is converted to CIE-LAB colorspace\n'
'before K-means. Image must be a 3 channel RGB image'
),
'algorithm': alg_quickshift,
},
'Watershed': {
'description': (
'Floods watershed basins based on a set of N markers, suitable\n'
'for grayscale images'),
'multi_chromatic': True,
'n': 'Desired number of markers',
'compact': (
'If not zero then use the compact-watershed algorithm\n'
'giving more regularly shaped basins'),
'line': 'Draws a one-pixel area with value=0 around each region',
'algorithm': alg_watershed,
},
'Felzenszwalb': {
'description': (
'Oversegmentation of a multichannel image based on minimum '
'spanning trees on the image grid.'),
'multi_chromatic': True,
'scale': 'Observation level, higher number means larger clusters',
'sigma': (
'Standard deviation of gaussian kernel used in '
'pre-processing (0.8)'),
'min size': 'Minimum size of each component',
'algorithm': alg_felzenszwalb,
},
'Chan-Vese': {
'description': (
'Active contour model based segmentation starting from an evolving'
' level set. Can be used to segment objects without clearly '
'defined boundaries.'),
'multi_chromatic': False,
'algorithm': alg_chan_vese,
'mu': ('Edge length parameter. '
'Higher values will produce rounder edges while smaller '
'values will detect smaller objects'),
'lambda1': ('Difference-from-average weight parameter. '
'Affects the total area labelled positive'),
'lambda2': ('Difference-from-average weight parameter. '
'Affects the total area labelled negative'),
'iter': 'Maximum number of iterations of algoritm',
'dt': (
'Multiplicative factor speeding up calculation at risk for '
'non-convergence'),
'initial level set': 'Starting level set',
'morphological': 'If true then use morphological Chan-Vese instead',
},
}
SEGMENTATION_PARAMETERS = [
'n', 'compactness', 'compact', 'iter', 'sigma', 'scale',
'ratio', 'kernel size', 'max dist',
'CIE Lab', 'force connectivity', 'line', 'min size',
'mu', 'lambda1', 'lambda2', 'dt', 'initial level set', 'morphological',
]
SEGMENTATION_TYPES = {
'n': int,
'compactness': float,
'compact': float,
'iter': int,
'sigma': float,
'ratio': float,
'kernel size': float,
'max dist': float,
'CIE Lab': bool,
'force connectivity': bool,
'line': bool,
'min size': int,
'scale': float,
'mu': float,
'lambda1': float,
'lambda2': float,
'dt': float,
'initial level set': ['checkerboard', 'disk', 'small disk'],
'morphological': bool,
}
SEGMENTATION_DEFAULTS = {
'n': 100,
'compactness': 10.0,
'compact': 0.0,
'iter': 10,
'sigma': 0.0,
'CIE Lab': True,
'ratio': 1.0,
'kernel size': 5,
'force connectivity': True,
'max dist': 10,
'min size': 20,
'line': False,
'scale': 1.0,
'mu': 0.25,
'lambda1': 1.0,
'lambda2': 1.0,
'dt': 0.5,
'initial level set': 'checkerboard',
'morphological': True,
}
MARKER_SEGMENTATION_ALGS = {
'Watershed': {
'description': (
'Floods watershed basins starting from the labels in the '
'input marker image'),
'multi_chromatic': True,
'compact': (
'If not zero then use the compact-watershed algorithm\n'
'giving more regularly shaped basins'),
'line': 'Draws a one-pixel area with value=0 around each region',
'algorithm': alg_watershed_markers,
},
'Random walker': {
'description': (
'Segments grayscale or color image based on random\n'
'walkers originating from labelled seed markers'),
'beta': ('Penalization for random walker motion, '
'larger values give less diffusion'),
'multi_chromatic': True,
'algorithm': alg_random_walker,
},
}
MARKER_SEGMENTATION_PARAMETERS = [
'compact', 'beta', 'line',
]
MARKER_SEGMENTATION_TYPES = {
'compact': float,
'line': bool,
'beta': float,
}
MARKER_SEGMENTATION_DEFAULTS = {
'compact': 0.0,
'line': False,
'beta': 130,
}
[docs]
class Segmentation(ImageFiltering_abstract, GenericImageFiltering,
node.Node):
author = 'Mathias Broxvall'
icon = 'image_segmentation.svg'
description = (
'Segments an input color or grayscale image into regions\n'
'with integer labels')
name = 'Image segmentation'
tags = Tags(Tag.ImageProcessing.Segmentation)
nodeid = 'com.sympathyfordata.imageanalysis.image_segmentation'
algorithms = SEGMENTATION_ALGS
options_list = SEGMENTATION_PARAMETERS
options_types = SEGMENTATION_TYPES
options_default = SEGMENTATION_DEFAULTS
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('source image to segment', name='source'),
])
outputs = Ports([
ImagePort('result after segmentation', name='result'),
])
__doc__ = ImageFiltering_abstract.generate_docstring(
description, algorithms, options_list, inputs, outputs)
[docs]
class SegmentationWithMarkers(ImageFiltering_abstract,
node.Node):
author = 'Mathias Broxvall'
icon = 'image_segmentation_markers.svg'
description = (
'Segments an input color or grayscale image into regions\n'
'with integer labels starting from an input "marker" image\n'
'giving initial labels')
name = 'Image segmentation with markers'
tags = Tags(Tag.ImageProcessing.Segmentation)
nodeid = (
'com.sympathyfordata.imageanalysis.image_segmentation_with_markers')
algorithms = MARKER_SEGMENTATION_ALGS
options_list = MARKER_SEGMENTATION_PARAMETERS
options_types = MARKER_SEGMENTATION_TYPES
options_default = MARKER_SEGMENTATION_DEFAULTS
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('source image to segment', name='source'),
ImagePort('image with markers to guide segmentation', name='markers'),
])
outputs = Ports([
ImagePort('result after segmentation', name='result'),
])
__doc__ = ImageFiltering_abstract.generate_docstring(
description, algorithms, options_list, inputs, outputs)
def execute(self, node_context):
params = node_context.parameters
source = node_context.input['source'].get_image()
markers = node_context.input['markers'].get_image()
alg_dict = self.algorithms[params['algorithm'].value]
alg = alg_dict['algorithm']
node_context.output['result'].set_image(alg(source, markers, params))
[docs]
class FindBoundaries(node.Node):
author = 'Mathias Broxvall'
icon = 'image_find_boundary.svg'
description = (
'Finds boundaries in a segmented image, returns boolean array with '
'True in the pixels on the boundary between two labels')
name = 'Find label boundaries'
tags = Tags(Tag.ImageProcessing.ImageManipulation)
nodeid = 'com.sympathyfordata.imageanalysis.image_find_boundaries'
parameters = node.parameters()
modes = ['thick', 'inner', 'outer']
# , 'subpixel'] Subpixel is broken right now
parameters.set_string(
'mode', value=modes[0], label='Mode',
editor=node.editors.combo_editor(options=modes),
description=(
'How to mark boundaries: thick marks pixels that are adjacent to\n'
'differing labels. Inner marks first pixel inside the\n'
'objects, leaving background unchanged. Outer outlines the\n'
'boundary of the background pixels to\n'
'non-background. '))
# 'Subpixels creates a supersampled image\n'
# 'with extra pixels in-between the original pixels,\n'
# 'boundaries are marked on the supersampled pixels.'
parameters.set_integer(
'connectivity', value=1, label='Connectivity',
editor=node.editors.combo_editor(options=[1, 2]),
description=(
'Considered connectivity, either direct neighbours (1) or also '
'along diagonals (2)'))
inputs = Ports([
ImagePort('source image to find boundaries in', name='source'),
])
outputs = Ports([
ImagePort('mask with boundaries', name='mask'),
])
def execute(self, node_context):
params = node_context.parameters
source = node_context.input['source'].get_image()
im = seg.find_boundaries(source,
mode=params['mode'].value,
connectivity=params['connectivity'].value)
node_context.output['mask'].set_image(im)