Source code for node_transform

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

from sympathy.api import node
from sympathy.api.nodeconfig import Ports

from skimage import transform
from sylib.imageprocessing.image import ImagePort
from sylib.imageprocessing.algorithm_selector import ImageFiltering_abstract
from sylib.imageprocessing.generic_filtering import GenericImageFiltering

from packaging import version
skimage_version = version.Version(skimage.__version__)

# Test for deprecation of criterion arguments
check_19 = skimage_version >= version.Version('0.19.0')


def resize_args(im, params):
    if not check_19:
        return transform.rescale(
                im, (params['scale y'].value, params['scale x'].value),
                mode='constant',
                multichannel=True,
                anti_aliasing=True,
                order=params['interpolation degree'].value)
    else:
        return transform.rescale(
                im, (params['scale y'].value, params['scale x'].value),
                mode='constant',
                channel_axis=2,
                anti_aliasing=True,
                order=params['interpolation degree'].value)


def alg_resize(im, params):
    req_h = params['height'].value
    req_w = params['width'].value
    h, w = req_h, req_w
    if params['aspect'].value:
        aspect = im.shape[1] / float(im.shape[0])
        size = min(req_w / aspect, req_h)
        w = int(size * aspect)
        h = int(size)

    shape = (h, w) + im.shape[2:]
    result_im = transform.resize(
        im, shape, order=params['interpolation degree'].value,
        mode='constant', anti_aliasing=True)
    if params['padding'].value:
        pad_h = req_h - h
        pad_w = req_w - w
        padded_im = np.zeros((req_h, req_w) + im.shape[2:])
        x0 = int(pad_w/2)
        x1 = x0 + result_im.shape[1]
        y0 = int(pad_h/2)
        y1 = y0 + result_im.shape[0]
        padded_im[y0:y1, x0:x1] = result_im
        return padded_im
    return result_im


def alg_padding(im, params):
    if len(im.shape) < 3:
        im = im.reshape(im.shape+(1,))
    add_alpha = params['add alpha'].value
    px, py = params['x'].value, params['y'].value
    px, py = int(px), int(py)
    k = params['k'].value
    max_x = im.shape[1]+abs(px)
    max_y = im.shape[0]+abs(py)
    result = np.full((max_y, max_x, im.shape[2]+add_alpha), k)
    if add_alpha:
        result[max(0, py):max(0, py)+im.shape[0],
               max(0, px):max(0, px)+im.shape[1], :-1] = im
        result[max(0, py):max(0, py)+im.shape[0],
               max(0, px):max(0, px)+im.shape[1], -1] = np.ones(im.shape[:2])
    else:
        result[max(0, py):max(0, py)+im.shape[0],
               max(0, px):max(0, px)+im.shape[1]] = im
    return result


def alg_crop_image(im, params):
    x, y = params['x'].value, params['y'].value
    w, h = params['width'].value, params['height'].value
    shape = im.shape
    x, y = min(x, shape[1]), min(y, shape[0])
    w, h = min(w, shape[1]-x), min(h, shape[0]-y)
    return im[y:y+h, x:x+w]


API_URL = 'http://scikit-image.org/docs/0.13.x/api/'
INTERPOLATION_DEGREE_DESC = (
    'Degree of polynomial (0 - 5) used for interpolation.\n'
    '0 - no interpolation, 1 - bi-linear interpolation, '
    '3 - bi-cubic interpolation'
)

TRANSFORM_ALGS = {
    'resize': {
        'description': 'Resizes an image to match the given dimensions',
        'width': 'The new width of the image',
        'height': 'The new height of the image',
        'interpolation degree': INTERPOLATION_DEGREE_DESC,
        'multi_chromatic': True,
        'aspect': 'Preserve aspect ratio (gives smaller size on one axis)',
        'padding': (
            'Adds padding to fill out full width/height after '
            'aspect-correct scaling'),
        'url': API_URL+'skimage.transform.html#skimage.transform.resize',
        'algorithm': alg_resize
    },
    'rescale': {
        'description': 'Rescales an image by a given factor',
        'scale x': 'Scale factor along X direction (horizontal)',
        'scale y': 'Scale factor along Y direction (vertical)',
        'interpolation degree': INTERPOLATION_DEGREE_DESC,
        'multi_chromatic': True,
        'url': API_URL+'skimage.transform.html#skimage.transform.rescale',
        'algorithm': resize_args
    },
    'rotate': {
        'description': 'Rotates an image',
        'angle': 'Angular degrees to rotate counterclockwise',
        'resize': (
            'If true new image dimensions are calculated '
            'to exactly fit the image'),
        'multi_chromatic': True,
        'url': API_URL+'skimage.transform.html#skimage.transform.rotate',
        'algorithm': lambda im, par: transform.rotate(
            np.copy(im), par['angle'].value, resize=par['resize'].value)
    },
    'padding': {
        'description': 'Adds a padding to an image',
        'x': (
            'If positive, amount of padding added on the left side. '
            'If negative the amount of padding added on the right side.'),
        'y': ('If positive, amount of padding added on the top.'
              'If negative the amount of padding added on the bottom.'),
        'k': 'Constant value used in padded areas',
        'add alpha': (
            'Adds an alpha with value 1.0 inside image, 0.0 outside'),
        'multi_chromatic': True,
        'algorithm': alg_padding,
    },
    'crop': {
        'description': 'Crops the image to the given rectanglular area',
        'x': 'Left edge of image',
        'y': 'Top edge of image',
        'width': 'Width of image',
        'height': 'Height of image',
        'multi_chromatic': False,
        'algorithm': alg_crop_image,
    },
}

TRANSFORM_PARAMETERS = [
    'x', 'y', 'width', 'height', 'padding', 'aspect', 'interpolation degree',
    'scale x', 'scale y', 'angle', 'resize', 'k', 'add alpha'
]
TRANSFORM_TYPES = {
    'angle': float, 'scale y': float, 'scale x': float, 'k': float,
    'height': int, 'padding': bool, 'width': int, 'aspect': bool,
    'y': int, 'x': int, 'interpolation degree': int,
    'add alpha': bool, 'resize': bool
}
TRANSFORM_DEFAULTS = {
    'angle': 0.0, 'scale y': 1.0, 'scale x': 1.0, 'k': 0.05, 'height': 512,
    'padding': False, 'width': 512, 'aspect': False, 'y': 0, 'x': 0,
    'interpolation degree': 3, 'add alpha': False, 'resize': True
}


[docs]class TransformFilter(ImageFiltering_abstract, GenericImageFiltering, node.Node): name = 'Transform image' icon = 'image_transform.svg' description = ( 'Transforms and image into another shape') nodeid = 'syip.transform' algorithms = TRANSFORM_ALGS options_list = TRANSFORM_PARAMETERS options_types = TRANSFORM_TYPES options_default = TRANSFORM_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)