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.cm

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 Image
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 = matplotlib.cm.get_cmap(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]
    minv = np.nanmin(im)
    maxv = np.nanmax(im)
    im[np.isnan(im)] = minv
    im = np.round((len(cols)-1) * (im-minv) / (maxv-minv)).astype(np.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[:,:,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([ Image('source image to filter', name='source'), ]) outputs = Ports([ Image('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([ Image('source image to filter', name='source'), ]) outputs = Ports([ Image('result after filtering', name='result'), ]) __doc__ = ImageFiltering_abstract.generate_docstring( description, algorithms, options_list, inputs, outputs)