Source code for node_contours

# 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)