blob: 1b0e3e09bcd8755aa0177017f1649c3d375dce5c [file] [log] [blame]
"""
Copyright (c) 2019, OptoFidelity OY
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software must display the following acknowledgement: This product includes software developed by the OptoFidelity OY.
4. Neither the name of the OptoFidelity OY nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
import cv2
import numpy as np
import logging
from xml.dom.minidom import parseString as minidom_parseString
from svgpathtools.svg2paths import polyline2pathd, parse_path, svg2paths
log = logging.getLogger(__name__)
class SvgRegion:
"""
Use svg vector graphic file to describe DUT shape.
- test if points are inside or outside of a test region
- test if a shape can be drawn on screen to certain place
- etc.
The svg file should include at least shapes with name
'test_region': the area that can be touched with robot finger
'analysis_region: the area that should be included in analysis
'bounds': the rectangular area containing the screen completely
"""
def __init__(self):
self.bounding_box = None
self.region = {}
def load_file(self, filename):
"""
Load a svg file with filename
:param filename: filename with path
"""
paths, attributes = svg2paths(filename)
self._load_svg(paths, attributes)
def load_string(self, svg_string):
"""
Load a svg file from string
:param svg_string: svg file as string
"""
paths, attributes = self.__svgstring2paths(svg_string)
self._load_svg(paths, attributes)
def _load_svg(self, paths, attributes):
"""
The actual load function
Loads the needed regions to member variables
:param paths: svg paths in svgpathtools internal paths type
:param attributes: svg attributes in svgpathtools internal paths type
"""
# get REGION, BOUNDS from svg
bounding_box = None
self.region = {}
def bounds(path):
x0, x1, y0, y1 = path.bbox()
return x0, y0, x1, y1
def join_bounds(b0, b1):
xcoords = np.array([b0[0], b0[2], b1[0], b1[2]])
ycoords = np.array([b0[1], b0[3], b1[1], b1[3]])
x0 = float(np.min(xcoords))
x1 = float(np.max(xcoords))
y0 = float(np.min(ycoords))
y1 = float(np.max(ycoords))
return x0, y0, x1, y1
for p, a in zip(paths, attributes):
rid = a.get('id', "")
self.region[rid] = p
if bounding_box is None:
bounding_box = bounds(p)
else:
bounding_box = join_bounds(bounding_box, bounds(p))
# translate to x/y 0,0 origin
for region_id in self.region:
self.region[region_id] = self.region[region_id].translated(complex(-bounding_box[0], -bounding_box[1]))
bounding_box = [0, 0, bounding_box[2] - bounding_box[0], bounding_box[3] - bounding_box[1]]
self.bounding_box = bounding_box
@staticmethod
def __svgstring2paths(svg_string,
convert_lines_to_paths=True,
convert_polylines_to_paths=True,
convert_polygons_to_paths=True,
return_svg_attributes=False):
"""
Not in svgpathtools so added here as private function
Converts an SVG string into a list of Path objects and a list of
dictionaries containing their attributes. This currently supports
SVG Path, Line, Polyline, and Polygon elements.
This function originally missing from SvgPathTools, added for dut purposes
:param svg_string: the location of the svg file
:param convert_lines_to_paths: Set to False to disclude SVG-Line objects
(converted to Paths)
:param convert_polylines_to_paths: Set to False to disclude SVG-Polyline
objects (converted to Paths)
:param convert_polygons_to_paths: Set to False to disclude SVG-Polygon
objects (converted to Paths)
:param return_svg_attributes: Set to True and a dictionary of
svg-attributes will be extracted and returned
:return: list of Path objects, list of path attribute dictionaries, and
(optionally) a dictionary of svg-attributes
"""
doc = minidom_parseString(svg_string)
def dom2dict(element):
"""Converts DOM elements to dictionaries of attributes."""
keys = list(element.attributes.keys())
values = [val.value for val in list(element.attributes.values())]
return dict(list(zip(keys, values)))
# Use minidom to extract path strings from input SVG
paths = [dom2dict(el) for el in doc.getElementsByTagName('path')]
d_strings = [el['d'] for el in paths]
attribute_dictionary_list = paths
# if pathless_svg:
# for el in doc.getElementsByTagName('path'):
# el.parentNode.removeChild(el)
# Use minidom to extract polyline strings from input SVG, convert to
# path strings, add to list
if convert_polylines_to_paths:
plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')]
d_strings += [polyline2pathd(pl['points']) for pl in plins]
attribute_dictionary_list += plins
# Use minidom to extract polygon strings from input SVG, convert to
# path strings, add to list
if convert_polygons_to_paths:
pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')]
d_strings += [polyline2pathd(pg['points']) + 'z' for pg in pgons]
attribute_dictionary_list += pgons
if convert_lines_to_paths:
lines = [dom2dict(el) for el in doc.getElementsByTagName('line')]
d_strings += [('M' + l['x1'] + ' ' + l['y1'] +
'L' + l['x2'] + ' ' + l['y2']) for l in lines]
attribute_dictionary_list += lines
# if pathless_svg:
# with open(pathless_svg, "wb") as f:
# doc.writexml(f)
if return_svg_attributes:
svg_attributes = dom2dict(doc.getElementsByTagName('svg')[0])
doc.unlink()
path_list = [parse_path(d) for d in d_strings]
return path_list, attribute_dictionary_list, svg_attributes
else:
doc.unlink()
path_list = [parse_path(d) for d in d_strings]
return path_list, attribute_dictionary_list
@property
def size(self):
"""
dut size as width, height in millimeters
uses the 'bounding_box' region
:return: width, height
"""
if self.bounding_box is None:
return 0, 0
return self.bounding_box[2], self.bounding_box[3]
@staticmethod
def region_to_contour(region, numpoints=100):
"""
Create OpenCV-compatible contour
Can be used for region comparison functions in OpenCV
Can be used to draw the region to bitmap with OpenCV
:param region: region to convert
:param numpoints: number of points in contour
:return: the contour, numpy array of numpy.float32 values
"""
contour = []
for t in np.linspace(0.0, 1.0, numpoints):
pt = region.point(t)
pt = pt.real, pt.imag
contour.append(pt)
contour = np.array(contour)
contour = np.round(contour, 4).astype(np.float32) # conversion a must, otherwise unknown funny type
return contour
def region_str_to_contour(self, region_str):
"""
Create OpenCV-compatible contour for specified region
:param region_str: region name as string
:return: contour
"""
region = self.region[region_str]
return self.region_to_contour(region)
def filter_points(self, points, region, margin):
"""
Filter list of points that fit inside given region with given margin
:param points: list of (x, y) points
:param region: region to use
:param margin: margin to add inside the region
:return: list of points inside the given region
"""
contour = self.region_to_contour(region, 2000)
results = []
for pt in points:
if cv2.pointPolygonTest(contour, tuple(pt), True) >= margin:
results.append(pt)
return results
def filter_points_contour(self, points, margin, contour):
"""
Filter points based on the given contour. With this one can save
the contour elsewhere and then do the filtering faster
:param points: list of points[(x1, y1), (x2, y2),...]
:param margin: margin to add inside the region
:param contour: OpenCV-compatible contour
:return: filtered list of points
"""
results = []
for pt in points:
if cv2.pointPolygonTest(contour, tuple(pt), True) >= margin:
results.append(pt)
return results
def filter_points_str_region(self, points, region_str, margin):
"""
Filter list of points that fit inside given region with given margin
:param points: list of (x, y) points
:param region: name of the region to use
:param margin: margin to add inside the region
:return: list of points inside the given region
"""
region = self.region[region_str]
return self.filter_points(points, region, margin)
def filter_line(self, line, contour, margin):
"""
Filter line and give back pieces that are inside the given region with given margin
:param line: (start x, start y, end x, end y)
:param contour: contour to use as filter (use region_to_contour to use with regions)
:param margin: margin to add inside the contour
:return: list of lines inside the given contour after cutting with the given contour.
"""
# resolution of the filtering, in millimeters
# changing this affects accuracy and speed of the operation
mm_resolution = 0.001
p0 = np.array([line[0], line[1]])
p1 = np.array([line[2], line[3]])
line_len = np.linalg.norm(p1-p0)
# line unit vector
pv = (p1-p0) / line_len
pos = 0
end_pos = line_len
lp0 = None
lp1 = None
lines = []
while True:
# current point on the line
pt = p0 + pos * pv
distance_to_shape = cv2.pointPolygonTest(contour, tuple(pt), True)
if distance_to_shape >= margin:
# we are inside the shape by margin
if lp0 is None:
lp0 = pt
else:
lp1 = pt
else:
# we are outside of the shape by margin
if lp1 is not None:
lines.append((lp0[0], lp0[1], lp1[0], lp1[1]))
lp0 = None
lp1 = None
# it is safe to skip at least to next margin of distance (-mm_resolution, which is added anyways)
skip = abs(distance_to_shape) - margin - mm_resolution
if skip > 0:
pos += skip
# advance on the line
pos += mm_resolution
if pos > end_pos:
break
# just one more line possible
if lp1 is not None:
lines.append((lp0[0], lp0[1], lp1[0], lp1[1]))
return lines
def filter_lines(self, lines, region, margin):
"""
Filter list of lines so that only pieces inside the given region are left.
:param lines: list of lines [(start x, start y, end x, end y)]
:param region: region to use for filtering
:param margin: margin to add inside the region
:return: list of list of lines. Every given line results as list of (zero or more) lines.
"""
contour = self.region_to_contour(region, 2000)
results = []
for line in lines:
results.append(self.filter_line(line, contour, margin))
return results
def filter_lines_str_region(self, lines, region_str, margin):
"""
Filter list of lines so that only pieces inside the given region are left.
:param lines: list of lines [(start x, start y, end x, end y)]
:param region: name of the region to use for filtering
:param margin: margin to add inside the region
:return: list of list of lines. Every given line results as list of (zero or more) lines.
"""
region = self.region[region_str]
return self.filter_lines(lines, region, margin)