Source code for node_draw

# 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/>.
from sympathy.api import node
from sympathy.api.nodeconfig import Port, Ports, Tag, Tags

import numpy as np
import skimage
from skimage import draw
from sylib.imageprocessing.image import ImagePort
from sylib.imageprocessing.algorithm_selector import ImageFiltering_abstract


[docs]class ImageDraw(ImageFiltering_abstract, node.Node): name = 'Draw on Image' author = 'Mathias Broxvall' version = '0.1' icon = 'image_draw.svg' description = 'Annotates an image with drawings based on tabular data.' nodeid = 'syip.imagedraw' tags = Tags(Tag.ImageProcessing.ImageManipulation) @staticmethod def column_value(s, table, N): """Get the intended parameter value from a string. Convert the given parameter string s to a number if possible, or return the column with that name from the table """ try: return np.ones(N) * float(s) except ValueError: return table.get_column_to_array(s) @staticmethod def get_colors(params, table, N): """Draw coordinates with color given by parameters""" try: red = np.ones(N) * float(params['red color'].value) except ValueError: red = table.get_column_to_array(params['red color'].value) try: green = np.ones(N) * float(params['green color'].value) except ValueError: green = table.get_column_to_array(params['green color'].value) try: blue = np.ones(N) * float(params['blue color'].value) except ValueError: blue = table.get_column_to_array(params['blue color'].value) try: alpha = np.ones(N) * float(params['alpha value'].value) except ValueError: alpha = table.get_column_to_array(params['alpha value'].value) colors = [red, green, blue, alpha] return colors @staticmethod def set_colors(image, coords, row, colors): if len(image.shape) < 3 or image.shape[2] == 1: draw.set_color(image, coords, [colors[0][row]], alpha=colors[3][row]) elif image.shape[2] == 2: draw.set_color( image, coords, [colors[0][row], colors[1][row]], alpha=colors[3][row] ) elif image.shape[2] == 3: draw.set_color( image, coords, [colors[0][row], colors[1][row], colors[2][row]], alpha=colors[3][row] ) elif image.shape[2] == 4: draw.set_color( image, coords, [colors[0][row], colors[1][row], colors[2][row], colors[3][row]], alpha=colors[3][row] ) else: pass def alg_circles(image, table, params): xs = table.get_column_to_array(params['x'].value) ys = table.get_column_to_array(params['y'].value) radii = ImageDraw.column_value( params['radius'].value, table, xs.shape[0] ) colors = ImageDraw.get_colors(params, table, ys.shape[0]) image = np.copy(image) for row in range(ys.shape[0]): x, y = draw.circle_perimeter( int(ys[row]), int(xs[row]), int(radii[row]), shape=image.shape ) ImageDraw.set_colors(image, (x, y), row, colors) return image def alg_filled_circles(image, table, params): xs = table.get_column_to_array(params['x'].value) ys = table.get_column_to_array(params['y'].value) radii = ImageDraw.column_value( params['radius'].value, table, xs.shape[0] ) colors = ImageDraw.get_colors(params, table, ys.shape[0]) image = np.copy(image) for row in range(ys.shape[0]): try: draw.disk except AttributeError: # Deprecated in 0.17. x, y = draw.circle( int(ys[row]), int(xs[row]), int(radii[row]), shape=image.shape ) else: x, y = draw.disk( (int(ys[row]), int(xs[row])), int(radii[row]), shape=image.shape ) ImageDraw.set_colors(image, (x, y), row, colors) return image def alg_lines(image, table, params): x1s = table.get_column_to_array(params['x'].value) y1s = table.get_column_to_array(params['y'].value) x2s = table.get_column_to_array(params['x2'].value) y2s = table.get_column_to_array(params['y2'].value) colors = ImageDraw.get_colors(params, table, x1s.shape[0]) image = np.copy(image) for row in range(y1s.shape[0]): x, y = draw.line( int(y1s[row]), int(x1s[row]), int(y2s[row]), int(x2s[row]) ) ImageDraw.set_colors(image, (x, y), row, colors) return image def alg_symbols(image, table, params): x0s = table.get_column_to_array(params['x'].value) y0s = table.get_column_to_array(params['y'].value) Ns = ImageDraw.column_value( params['N'].value, table, x0s.shape[0] ).astype(int) radii = ImageDraw.column_value( params['radius'].value, table, x0s.shape[0] ) colors = ImageDraw.get_colors(params, table, x0s.shape[0]) image = np.copy(image) for row in range(y0s.shape[0]): N = Ns[row] r = radii[row] x0 = int(x0s[row]) y0 = int(y0s[row]) convex = N < 0 N = np.abs(N) poly_x = np.array([]) poly_y = np.array([]) for n in range(N+1): alpha1 = (2*np.pi*(n+0.5))/N x1, y1 = int(x0+r*np.sin(alpha1)), int(y0+r*np.cos(alpha1)) poly_x = np.append(poly_x, x1) poly_y = np.append(poly_y, y1) if convex: alpha2 = alpha1 + np.pi/N x2 = int(x0+r*np.sin(alpha2)/4.) y2 = int(y0+r*np.cos(alpha2)/4.) poly_x = np.append(poly_x, x2) poly_y = np.append(poly_y, y2) ImageDraw.set_colors( image, draw.polygon(poly_y, poly_x), row, colors ) return image default_parameters = { 'red color': ( 'A number (0-1) or column name containing colors for first image' 'channel.' ), 'green color': ( 'A number (0-1) or column name containing colors for second image' 'channel, ignored for non RGB images' ), 'blue color': ( 'A number (0-1) or column name containing colors for third image' 'channel, ignored for non RGB images' ), 'alpha value': ( 'A number (0-1) or column name containing alpha values used for' 'blending the drawings over the image.\n' ' 1.0 is opaque, 0.0 transparent' ), 'force colors': ( 'If true then forces output to be RGB' ), } algorithms = { 'circle': dict({ 'description': ( 'Draws (non-filled) circles from given X,Y points with given' 'radii' ), 'x': 'Column name containing X-coordinates', 'y': 'Column name containing Y-coordinates', 'radius': 'Column name containing radii', 'algorithm': alg_circles, }, **default_parameters), 'symbols': dict({ 'description': ( 'Draws a symbol defined by number of vertices. Negative values' 'are drawn as convex objects. Eg, triangles have N=3,' 'squares N=4, hexagons N=6, stars N=-6' ), 'x': 'Column name containing X-coordinates', 'y': 'Column name containing Y-coordinates', 'N': 'Column name containing number of vertices', 'radius': 'Column name containing radii', 'algorithm': alg_symbols, }, **default_parameters), 'filled circle': dict({ 'description': ( 'Draws filled circles from given X,Y points with given radii' ), 'x': 'Column name containing X-coordinates', 'y': 'Column name containing Y-coordinates', 'radius': 'Column name containing radii', 'algorithm': alg_filled_circles, }, **default_parameters), 'lines': dict({ 'description': ( 'Draws lines from coordinates (X,Y) to coordinates (X2,Y2)' ), 'x': 'Column name containing X-coordinates', 'y': 'Column name containing Y-coordinates', 'x2': 'Column name containing X2-coordinates', 'y2': 'Column name containing Y2-coordinates', 'algorithm': alg_lines, }, **default_parameters), } options_list = [ 'x', 'y', 'x2', 'y2', 'radius', 'red color', 'green color', 'blue color', 'alpha value', 'N', 'force colors', ] options_types = { 'x': str, 'y': str, 'x2': str, 'y2': str, 'N': str, 'radius': str, 'red color': str, 'green color': str, 'blue color': str, 'alpha value': str, 'force colors': bool, } options_default = { 'x': 'X', 'y': 'Y', 'x2': 'X2', 'y2': 'Y2', 'N': 'N', 'radius': 'radius', 'red color': '1.0', 'green color': '1.0', 'blue color': '0.5', 'alpha value': '1.0', 'force colors': False, } 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('Image to draw on', name='image'), Port.Table('Table used for drawing', name='table'), ]) outputs = Ports([ ImagePort('Resulting image', name='output'), ]) __doc__ = ImageFiltering_abstract.generate_docstring( description, algorithms, options_list, inputs, outputs ) def execute(self, node_context): image = node_context.input['image'].get_image() table = node_context.input['table'] params = node_context.parameters alg_name = params['algorithm'].value alg = self.algorithms[alg_name]['algorithm'] if len(image.shape) < 3: image = image.reshape(image.shape+(1,)) if params['force colors'].value: if image.shape[2] == 1: image = skimage.color.gray2rgb(image[:, :, 0]) elif image.shape[2] >= 3: image = image[:, :, :3] result = alg(image, table, params) node_context.output['output'].set_image(result)