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