# 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.
import warnings
from packaging import version as pversion
from sympathy.api import node
from sympathy.api.nodeconfig import Port, Ports, Tag, Tags, adjust
import numpy as np
import skimage
import skimage.draw
import skimage.measure
import skimage.segmentation as seg
from sympathy.api import table
from sympathy.api.exceptions import SyConfigurationError
from sylib.imageprocessing.image import ImagePort
class ActiveContourSegmentationBase(node.Node):
author = 'Mathias Broxvall'
icon = 'image_active_contour.svg'
description = (
'Takes a starting contour (snake) and uses active contours to\n'
'optimise its shape. '
'Takes a table(s) with coordinates of the initial contour, '
'returns a table(s) with coordinates of optimised contour.')
tags = Tags(Tag.ImageProcessing.Segmentation)
parameters = node.parameters()
parameters.set_string(
'X', value='', label='X or R column',
editor=node.editors.combo_editor(edit=False),
description='Column names with contour X coordinates')
parameters.set_string(
'Y', value='', label='Y or C column',
editor=node.editors.combo_editor(edit=False),
description='Column names with contour Y coordinates')
parameters.set_string(
'coordinates', value='rc', label='Coordinates type',
editor=node.editors.combo_editor(options=['xy', 'rc']),
description='Coordinate tpes for the input columns')
bc_options = ['periodic', 'free', 'fixed', 'fixed-free', 'free-fixed']
parameters.set_string(
'boundary_condition', value=bc_options[0], label='Boundary condition',
editor=node.editors.combo_editor(options=bc_options),
description=(
'Boundary conditions for worm (first and last point). Periodic '
'attaches the two ends, fixed holds them in place and free allows '
'them to move.'))
parameters.set_float(
'alpha', value=0.01, label='Length parameter',
description=(
'Snake length shape parameter. Higher values makes snake '
'contract faster.'))
parameters.set_float(
'beta', value=0.1, label='Smoothness parameter',
description=(
'Snake smoothness shape parameter. Higher values makes snake '
'smoother.'))
parameters.set_float(
'w_line', value=0, label='Brightness parameter',
description=(
'Controls attraction to brightness. Use negative values to '
'attract to dark regions.'))
parameters.set_float(
'w_edge', value=1, label='Edge parameter',
description=(
'Controls attraction to edges. Use negative values to repel snake '
'from edges.'))
parameters.set_float(
'gamma', value=0.01, label='Gamma parameter',
description=(
'Controls attraction to edges. Use negative values to repel snake '
'from edges.'))
parameters.set_float(
'max_px_move', value=1.0, label='Max move',
description=(
'Maximum distance in number of pixels to move per iteration.'))
parameters.set_float(
'convergence', value=0.1, label='Convergence',
description=(
'Convergence criteria.'))
parameters.set_integer(
'max_iterations', value=2500, label='Max iterations',
description=(
'Maximum number of iterations'))
__doc__ = description
def active_contours(self, params, image, in_table, out_table):
x_col_name = params['X'].value
y_col_name = params['Y'].value
if x_col_name not in in_table.column_names():
raise SyConfigurationError(
'Configured column {} does not exist in dataset'
.format(x_col_name))
if y_col_name not in in_table.column_names():
raise SyConfigurationError(
'Configured column {} does not exist in dataset'
.format(y_col_name))
xs = in_table[x_col_name]
ys = in_table[y_col_name]
if len(image.shape) == 3 and image.shape[-1] == 1:
image = image[:, :, 0]
kwargs = dict(
alpha=params['alpha'].value,
beta=params['beta'].value,
gamma=params['gamma'].value,
w_line=params['w_line'].value,
w_edge=params['w_edge'].value,
convergence=params['convergence'].value,
max_iterations=params['max_iterations'].value,
max_px_move=params['max_px_move'].value,
)
skimage_version = pversion.parse(skimage.__version__)
kwargs['boundary_condition'] = params['boundary_condition'].value
# Parameter 'max_iterations' changed to 'max_num_iter' in 0.19.0
if skimage_version >= pversion.Version('0.19.0'):
kwargs['max_num_iter'] = kwargs.pop('max_iterations')
# Before skimage version 0.16.0 coordinates were always in 'xy' format.
# The parameter coordinates was introduced in 0.16.0 with a default of
# 'xy', but alternative value 'rc' which swaps the input columns as
# well as the output columns. In 0.18.0 the default for coordinates
# changed to 'rc' and the 'xy' option was removed.
default_coordinates = 'rc'
snake = np.column_stack((xs, ys))
# We currently support both types of coordinates for all scikit-image
# version and so we simply swap the coordinates ourselves if needed.
if params['coordinates'].value != default_coordinates:
snake = snake[:, ::-1]
# scikit-image versions between 0.16 and 0.18 would produce a warning
# when using the default parameter. According to the warning the proper
# way of silencing it should be to explicitly pass coordinates='xy' to
# active_contour(), but this leads to UnboundLocalVariableError in all
# of these versions, so we silence the warning manually.
with warnings.catch_warnings():
warnings.filterwarnings(
'ignore',
'The coordinates used by `active_contour` '
'will change from xy coordinates',
category=FutureWarning)
xy = seg.active_contour(image, snake, **kwargs)
if params['coordinates'].value != default_coordinates:
xy = xy[:, ::-1]
out_table.set_column_from_array(x_col_name, xy[:, 0])
out_table.set_column_from_array(y_col_name, xy[:, 1])
[docs]
class ActiveContourSegmentation(ActiveContourSegmentationBase):
name = 'Active contour segmentation'
nodeid = 'com.sympathyfordata.imageanalysis.active_contours'
inputs = Ports([
ImagePort('Source image', name='source'),
Port.Table('Starting contour', name='contours'),
])
outputs = Ports([
Port.Table('Resulting contour', name='result'),
])
def adjust_parameters(self, node_context):
adjust(node_context.parameters['X'], node_context.input['contours'])
adjust(node_context.parameters['Y'], node_context.input['contours'])
def execute(self, node_context):
params = node_context.parameters
image = node_context.input['source'].get_image()
in_table = node_context.input['contours']
out_table = node_context.output['result']
self.active_contours(params, image, in_table, out_table)
[docs]
class ActiveContourSegmentationList(ActiveContourSegmentationBase):
name = 'Active contour segmentation (list)'
nodeid = 'com.sympathyfordata.imageanalysis.active_contours_list'
inputs = Ports([
ImagePort('Source image', name='source'),
Port.Tables('Starting contour', name='contours'),
])
outputs = Ports([
Port.Tables('Resulting contour', name='result'),
])
def adjust_parameters(self, node_context):
adjust(node_context.parameters['X'], node_context.input['contours'])
adjust(node_context.parameters['Y'], node_context.input['contours'])
def execute(self, node_context):
params = node_context.parameters
image = node_context.input['source'].get_image()
result = node_context.output['result']
for in_table in node_context.input['contours']:
out_table = table.File()
self.active_contours(params, image, in_table, out_table)
result.append(out_table)
[docs]
class ContourToLabels(node.Node):
author = 'Mathias Broxvall'
icon = 'image_contours_to_labels.svg'
description = (
'Takes a contour and creates integer labels for an image based on '
'a list of contours. Use the node Image to List to extract '
'each region from these labels')
name = 'Contours to labels'
tags = Tags(Tag.ImageProcessing.Segmentation)
nodeid = 'com.sympathyfordata.imageanalysis.contours_to_labels'
inputs = Ports([
ImagePort('Image to mask', name='image'),
Port.Tables('Contours', name='contours'),
])
outputs = Ports([
ImagePort('Resulting mask', name='result'),
])
parameters = node.parameters()
parameters.set_string(
'X', value='', label='X column',
editor=node.editors.combo_editor(edit=False),
description='Column names with contour X coordinates')
parameters.set_string(
'Y', value='', label='Y column',
editor=node.editors.combo_editor(edit=False),
description='Column names with contour Y coordinates')
parameters.set_string(
'close', value='closed', label='Type',
editor=node.editors.combo_editor(options=['closed', 'open']),
description=(
'Select if the countour should be closed'))
__doc__ = description
def adjust_parameters(self, node_context):
try:
adjust(node_context.parameters['X'],
node_context.input['contours'][0])
adjust(node_context.parameters['Y'],
node_context.input['contours'][0])
except Exception:
pass
def execute(self, node_context):
params = node_context.parameters
source = node_context.input['image'].get_image()
contours = node_context.input['contours']
result = node_context.output['result']
x_col_name = params['X'].value
y_col_name = params['Y'].value
mask = np.full(source.shape[:2], False)
for table_ in contours:
Xs = table_[x_col_name].astype(int)
Ys = table_[y_col_name].astype(int)
Xs = np.clip(Xs, 0, mask.shape[1]-1)
Ys = np.clip(Ys, 0, mask.shape[0]-1)
if x_col_name not in table_.column_names():
raise SyConfigurationError(
'Configured column {} does not exist in dataset'
.format(x_col_name))
if y_col_name not in table_.column_names():
raise SyConfigurationError(
'Configured column {} does not exist in dataset'
.format(y_col_name))
if len(Xs) >= 2:
x0, y0 = Xs[0], Ys[0]
for row in range(1, len(Xs)):
x1, y1 = Xs[row], Ys[row]
if (x1, y1) != (x0, y0):
line = skimage.draw.line(y0, x0, y1, x1)
mask[line] = True
x0, y0 = x1, y1
if params['close'].value == 'closed':
if (x0, y0) != (Xs[0], Ys[0]):
line = skimage.draw.line(y0, x0, Ys[0], Xs[0])
mask[line] = True
labels = skimage.measure.label(mask*1, connectivity=1, background=-1)
labels *= ~mask
labels = skimage.measure.label(labels, connectivity=1, background=0)
result.set_image(labels)
[docs]
class FindContours(node.Node):
author = 'Mathias Broxvall'
icon = 'image_find_contour.svg'
description = (
'Extracts iso-contours based on intensity levels in the image. Uses '
'a 2D case of the marching cubes algorithm.')
name = 'Find contours'
tags = Tags(Tag.ImageProcessing.Segmentation)
nodeid = 'com.sympathyfordata.imageanalysis.find_contours'
inputs = Ports([
ImagePort('Image to extract contours from', name='image'),
])
outputs = Ports([
Port.Tables('Contours', name='contours'),
])
__doc__ = description
parameters = node.parameters()
parameters.set_float(
'level', value=0.5, label='Level',
description='The intensity level at which to extract contours')
def execute(self, node_context):
params = node_context.parameters
source = node_context.input['image'].get_image()
contours = node_context.output['contours']
# Only apply calcultions to first channel
if len(source.shape) == 3:
source = source[:, :, 0]
level = params['level'].value
contour_list = skimage.measure.find_contours(source, level)
for contour in contour_list:
tbl = table.File()
tbl.set_column_from_array("X", contour[:, 1])
tbl.set_column_from_array("Y", contour[:, 0])
contours.append(tbl)