Source code for node_lines

# This file is part of Sympathy for Data.
# Copyright (c) 2017, Combine Control Systems AB
#
# SYMPATHY FOR DATA COMMERCIAL LICENSE
# You should have received a link to the License with Sympathy for Data.

from sympathy.api import node, exceptions
from sympathy.api.nodeconfig import Port, Ports, Tag, Tags

import numpy as np
import math


[docs] class UniqueLinesNode(node.Node): """ Filters a table with lines to only keep the longest ones that are not contained by any other. """ author = 'Mathias Broxvall' icon = 'image_uniquelines.svg' description = ('Filters a table with lines (x0,y0, x1,y1) to only keep ' 'the longest ones that are not contained by any other. ' 'Commonly used after a Hough transform (see Extract ' 'Image node)') name = 'Filter unique lines' tags = Tags(Tag.ImageProcessing.Extract) nodeid = 'com.sympathyfordata.imageanalysis.filter_unique_lines' parameters = node.parameters() parameters.set_string( 'x0', value='x0', description='Name of column containing X-coordiantes for the starting ' 'points', label='Starting X') parameters.set_string( 'y0', value='y0', description='Name of column containing Y-coordiantes for the starting ' 'points', label='Starting Y') parameters.set_string( 'x1', value='x1', description='Name of column containing X-coordiantes for the ending ' 'points', label='Ending X') parameters.set_string( 'y1', value='y1', description='Name of column containing Y-coordiantes for the ending ' 'points', label='Ending Y') parameters.set_integer( 'distance', value=10, description='Distance in pixels around which smaller lines are ' 'rejected', label='distance') inputs = Ports([ Port.Table('Input lines', name='lines'), ]) outputs = Ports([ Port.Table('Filtered lines', name='result'), ]) @staticmethod def sq_dist(x0, y0, x1, y1): return (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) @staticmethod def point_contained(_A, _B, _C, th): # Returns true if point C is within the given distance from any point # on the line AB A = np.array([_A[0], _A[1]]) B = np.array([_B[0], _B[1]]) C = np.array([_C[0], _C[1]]) AB = B - A BA = A - B AC = C - A BC = C - B # Cp is the projected point of C onto line A-B length = np.linalg.norm(AB) * np.linalg.norm(AC) cosA = np.dot(AB, AC) / length Cp = C - cosA * AB/np.linalg.norm(AB) if np.linalg.norm(C - A) < th: return True if np.linalg.norm(C - B) < th: return True if np.linalg.norm(C - Cp) > th: return False if np.dot(AC, AB) < 0: return False if np.dot(BC, BA) < 0: return False return True def execute(self, node_context): in_lines = node_context.input['lines'] result = node_context.output['result'] if in_lines.number_of_rows() == 0: raise exceptions.SyDataError("Empty table") x0_name = node_context.parameters['x0'].value x1_name = node_context.parameters['x1'].value y0_name = node_context.parameters['y0'].value y1_name = node_context.parameters['y1'].value if any(elem == '' for elem in [x0_name, x1_name, y0_name, y1_name]): raise exceptions.SyConfigurationError( "At least one column name is empty.") X0 = in_lines.col(x0_name).data X1 = in_lines.col(x1_name).data Y0 = in_lines.col(y0_name).data Y1 = in_lines.col(y1_name).data length = np.sqrt(self.sq_dist(X0, Y0, X1, Y1)) lines = [(x0, y0, x1, y1, line, idx) for idx, (x0, y0, x1, y1, line) in enumerate(zip(X0, Y0, X1, Y1, length))] lines.sort(key=lambda tpl: -tpl[4]) kept_lines = [] th = node_context.parameters['distance'].value for line_1 in lines: C0 = line_1[0:2] C1 = line_1[2:4] for line_2 in kept_lines: A = line_2[0:2] B = line_2[2:4] if (self.point_contained(A, B, C0, th) and self.point_contained(A, B, C1, th)): break else: kept_lines.append(line_1) # Note reversed sign for Y due to convention of "positive Y is down" # in images orientation = [math.atan2(y1-y0, x0-x1) for x0, y0, x1, y1, _, _ in kept_lines] result.set_column_from_array( 'x0', np.array([line[0] for line in kept_lines])) result.set_column_from_array( 'y0', np.array([line[1] for line in kept_lines])) result.set_column_from_array( 'x1', np.array([line[2] for line in kept_lines])) result.set_column_from_array( 'y1', np.array([line[3] for line in kept_lines])) result.set_column_from_array( 'length', np.array([line[4] for line in kept_lines])) result.set_column_from_array( 'orientation', np.array(orientation)) idx = [line[5] for line in kept_lines] for col in in_lines.cols(): if col.name not in [x0_name, x1_name, y0_name, y1_name]: result.set_column_from_array(col.name, col.data[idx])