Source code for node_colors

# 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/>.
import numpy as np
import matplotlib as mpl

from sympathy.api import node
from sympathy.api.nodeconfig import Ports
from sympathy.api.exceptions import sywarn, SyDataError

from skimage import color, exposure
from sylib.imageprocessing.image import ImagePort
from sylib.imageprocessing.algorithm_selector import ImageFiltering_abstract
from sylib.imageprocessing.generic_filtering import GenericImageFiltering
from sylib.imageprocessing.color import grayscale_transform


API_URL = 'http://scikit-image.org/docs/0.13.x/api/'


def alg_greyscale(im, params):
    if len(im.shape) == 2:
        result = im
    elif im.shape[2] == 3 and params['luminance preserving'].value:
        result = im.dot(grayscale_transform)
    elif (im.shape[2] == 4 and
          params['luminance preserving'].value and
          params['preserve alpha'].value):
        result = np.zeros(im.shape[:2]+(2,))
        result[:, :, 0] = im[:, :, :3].dot(grayscale_transform)
        result[:, :, 1] = im[:, :, 3]
    elif im.shape[2] == 4 and not params['preserve alpha'].value:
        result = im[:, :, :3].dot(grayscale_transform)
    else:
        result = im.mean(axis=2).reshape(im.shape[:2]+(1,))
    return result


def alg_colourmap(im, params):
    if len(im.shape) >= 3 and im.shape[2] != 1:
        sywarn('Colourmap expects a single-channel input')
    if len(im.shape) >= 3:
        im = im[:, :, 0]
    cmap = mpl.colormaps[params['cmap'].value]
    try:
        cols = np.array(cmap.colors)
    except AttributeError:
        cols = cmap(np.linspace(0.0, 1.0, 256))
        if cols.shape[-1] == 4:
            cols = cols[:, :3]
    im = np.copy(im)
    minv = np.nanmin(im)
    maxv = np.nanmax(im)
    im[np.isnan(im)] = minv
    im = np.round((len(cols)-1) * (im-minv) / (maxv-minv)).astype(int)
    return cols[im.ravel()].reshape(im.shape[:2]+(cols.shape[-1],))


def alg_hsv2rgb(im, params):
    if len(im.shape) != 3 or im.shape[2] != 3:
        raise SyDataError('Invalid number of channels in input image. '
                          'Must have exactly 3 channels.')
    max_val = np.max(im[:, :, 2])
    if max_val >= 1.0 and max_val <= 1.0+1e-5:
        # Avoid error with value channel very close to 1.0
        # For this case the user probably meant to have it clipped
        # instead of wrap around
        im = np.copy(im)
        im[:, :, 2] = np.minimum(im[:, :, 2], 1.0 - 1e-5)
    return color.hsv2rgb(im)


COLORSPACE_CONVERTERS = {
    'hsv2rgb': {
        'description': (
            'Interprets input channels as Hue-Saturation-Value (HSV) '
            'and outputs Red-Green-Blue (RGB) channels.'),
        'multi_chromatic': True,
        'url': API_URL+'skimage.color.html#skimage.color.hsv2rgb',
        'algorithm': alg_hsv2rgb
    },
    'rgb2hsv': {
        'description': (
            'Interprets input channels as Red-Green-Blue (RGB) '
            'and outputs Hue-Saturation-Value (HSV) channels.'),
        'multi_chromatic': True,
        'url': API_URL+'skimage.color.html#skimage.color.rgb2hsv',
        'algorithm': lambda im, par: color.rgb2hsv(im)
    },
    'rgb2xyz': {
        'description': (
            'Interprets input channels as sRGB and outputs '
            'CIE XYZ channels.'),
        'multi_chromatic': True,
        'url': API_URL+'skimage.color.html#skimage.color.rgb2xyz',
        'algorithm': lambda im, par: color.rgb2xyz(im)
    },
    'xyz2rgb': {
        'description': ('Interprets input channels as CIE XYZ '
                        'and outputs sRGB channels.'),
        'multi_chromatic': True,
        'url': API_URL+'skimage.color.html#skimage.color.xyz2rgb',
        'algorithm': lambda im, par: color.xyz2rgb(im)
    },
    'rgb2lab': {
        'description': (
            'Interprets input channels as sRGB and outputs '
            'CIE LAB channels.'),
        'multi_chromatic': True,
        'url': API_URL+'skimage.color.html#skimage.color.rgb2lab',
        'illuminant': 'CIE standard illumination spectrum',
        'observer': 'Aperture angle of observer',
        'algorithm': lambda im, par: (
            color.rgb2lab(im,
                          illuminant=par['illuminant'].value,
                          observer=par['observer'].value)),
    },
    'lab2rgb': {
        'description': (
            'Interprets input channels as sRGB and outputs '
            'CIE LAB channels.'),
        'multi_chromatic': True,
        'url': API_URL+'skimage.color.html#skimage.color.rgb2lab',
        'illuminant': 'CIE standard illumination spectrum',
        'observer': 'Aperture angle of observer',
        'algorithm': lambda im, par: (
            color.lab2rgb(im,
                          illuminant=par['illuminant'].value,
                          observer=par['observer'].value)),
    },
    'grey2cmap': {
        'description': (
            'Converts greyscale values after normalization and scaling '
            'from 0 - 255 into a matplotlib colourmap.'),
        'cmap': 'The colormap to use in conversion',
        'multi_chromatic': True,
        'algorithm': alg_colourmap,
        'url': ('https://matplotlib.org/'
                'examples/color/colormaps_reference.html')
    },
    'greyscale': {
        'description': 'Transforms RGB images into greyscale',
        'luminance preserving': (
            'Use weighted average based on separate luminosity of '
            'red-green-blue receptors in human eye.\nOnly works for three '
            'channel images'),
        'preserve alpha': (
            'Passes through channel 4 (alpha), '
            'otherwise it is treated as another channel affecting output'),
        'multi_chromatic': True,
        'algorithm': alg_greyscale
    },
}

COLORRANGE_CONVERTERS = {
    'gamma correction': {
        'description': (
            'Applies the correction:  '
            'Vout = scale Vin^gamma\nProcesses each channel separately'
        ),
        'scale': 'Constant scale factor applied after gamma correction',
        'gamma': (
            'Gamma factor applied to image.\n<1 increases intensities of '
            'mid-tones,\n>1 decreases intensities of mid-tones'
        ),
        'multi_chromatic': False,
        'url': API_URL+'skimage.exposure.html#skimage.exposure.adjust_gamma',
        'algorithm': (
            lambda im, par: exposure.adjust_gamma(
                im, gamma=par['gamma'].value, gain=par['scale'].value))
    },
    'log correction': {
        'description': (
            'Applies the correction:  '
            'Vout = scale log(1 + Vin)\n'
            'Processes each channel separately'),
        'scale': 'Constant scale factor applied after gamma correction',
        'inverse': (
            'Perform inverse log-correction instead (default false):\n'
            'Vout = scale (2^Vin - 1)'),
        'multi_chromatic': False,
        'url': API_URL+'skimage.exposure.html#skimage.exposure.adjust_log',
        'algorithm': (
            lambda im, par: exposure.adjust_log(
                im, gain=par['scale'].value, inv=par['inverse'].value))
    },
    'sigmoid': {
        'description': (
            'Performs Sigmoid correction on input image. '
            'Also known as contrast adjustment.\n'
            'Vout = 1/(1+exp(gain*(cutoff-Vin)))\n'
            'Processes each channel separately'),
        'cutoff': (
            'Shifts the characteristic curve for the sigmoid horizontally'
            '(default 0.5)'),
        'gain': (
            'Gain of sigmoid, affects rise time of curve (default 10.0)'),
        'inverse': (
            'Perform negative sigmoid correction instead (default false)'),
        'multi_chromatic': False,
        'url': API_URL+'skimage.exposure.html#skimage.exposure.adjust_sigmoid',
        'algorithm': (
            lambda im, par: exposure.adjust_sigmoid(
                im, gain=par['gain'].value, cutoff=par['cutoff'].value,
                inv=par['inverse'].value))
    },
    'histogram equalization': {
        'description': (
            'Improves contrast by stretching and equalizing the histogram'
        ),
        'bins': 'Number of bins in computed histogram (default 256)',
        'multi_chromatic': True,
        'url': API_URL+'skimage.exposure.html#skimage.exposure.equalize_hist',
        'algorithm': (
            lambda im, par: exposure.equalize_hist(
                im, nbins=par['bins'].value))
    },
    'adaptive histogram': {
        'description': (
            'Improves contrast by stretching and equalizing the histogram'
            'in a sliding window over the image'),
        'adaptive kernel size': (
            'Size of the sliding window. '
            'Must evenly divide both image width and height.'),
        'sigma': ('Clipping limit (normalized between 0 and 1). '
                  'Higher values give more contrast. (default 1.0)'),
        'bins': 'Number of bins in computed histogram (default 256)',
        'multi_chromatic': True,
        'url': (
            API_URL + 'skimage.exposure.html' +
            '#skimage.exposure.equalize_adapthist'),
        'algorithm': (
            lambda im, par: exposure.equalize_adapthist(
                im, kernel_size=par['adaptive kernel size'].value,
                clip_limit=par['sigma'].value, nbins=par['bins'].value))
    },
}

COLORSPACE_PARAMETERS = [
    'cmap', 'luminance preserving', 'illuminant', 'observer']
COLORSPACE_TYPES = {
    'cmap': ['viridis', 'Accent', 'Blues', 'BrBG', 'BuGn', 'BuPu',
             'CMRmap', 'Dark2', 'GnBu', 'Greens', 'Greys', 'OrRd',
             'Oranges', 'PRGn', 'Paired', 'Pastel1', 'Pastel2', 'PiYG',
             'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy',
             'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Set1', 'Set2', 'Set3',
             'Spectral', 'Vega10', 'Vega20', 'Vega20b', 'Vega20c',
             'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot',
             'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm',
             'copper', 'cubehelix', 'gist_earth', 'gist_gist_gray',
             'gist_gist_heat', 'gist_gist_ncar', 'gist_nbow', 'gist_stern',
             'gist_gist_yarg', 'gist_gnuplot', 'gnuplot2', 'gray', 'hot',
             'hsv', 'inferno', 'jet', 'magma', 'nipy_spectral',
             'nipy_ocean', 'pink', 'plasma', 'prism', 'rainbow',
             'seismic', 'spectral', 'spring', 'summer', 'tab10',
             'tab20', 'tab20b', 'tab20c', 'terrain', 'winter'],
    'illuminant': ['A', 'D50', 'D55', 'D65', 'D75', 'E'],
    'observer': ['2', '10'],
    'luminance preserving': bool,
}
COLORSPACE_DEFAULTS = {
    'cmap': 'viridis', 'luminance preserving': True,
    'illuminant': 'D65', 'observer': '2',
}

COLORRANGE_PARAMETERS = [
    'scale', 'gamma', 'inverse', 'cutoff', 'gain', 'bins',
    'adaptive kernel size', 'sigma',
]
COLORRANGE_TYPES = {
    'gamma': float, 'inverse': bool,
    'adaptive kernel size': int, 'cutoff': float, 'scale': float,
    'gain': float, 'sigma': float, 'bins': int}
COLORRANGE_DEFAULTS = {
    'gamma': 1.0, 'inverse': False,
    'adaptive kernel size': 4, 'cutoff': 0.5,
    'scale': 1.0, 'gain': 10.0, 'sigma': 1.0, 'bins': 256
}


[docs]class ColorSpaceConversion(ImageFiltering_abstract, GenericImageFiltering, node.Node): name = 'Color space conversion' icon = 'image_colorspace.svg' description = ( 'Converts each pixel in a multi-channel image into another ' 'colour space') nodeid = 'syip.color_space_conversion' algorithms = COLORSPACE_CONVERTERS options_list = COLORSPACE_PARAMETERS options_types = COLORSPACE_TYPES options_default = COLORSPACE_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 filter', name='source'), ]) outputs = Ports([ ImagePort('result after filtering', name='result'), ]) __doc__ = ImageFiltering_abstract.generate_docstring( description, algorithms, options_list, inputs, outputs)
[docs]class ColorRangeConversion(ImageFiltering_abstract, GenericImageFiltering, node.Node): name = 'Color range conversion' icon = 'image_color_range.svg' description = ( 'Changes the range and distribution of values for all pixels') nodeid = 'syip.color_range_conversion' algorithms = COLORRANGE_CONVERTERS options_list = COLORRANGE_PARAMETERS options_types = COLORRANGE_TYPES options_default = COLORRANGE_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 filter', name='source'), ]) outputs = Ports([ ImagePort('result after filtering', name='result'), ]) __doc__ = ImageFiltering_abstract.generate_docstring( description, algorithms, options_list, inputs, outputs)