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