#!/usr/bin/env python
# -*- coding: utf-8 -*-
# (C) 2017 David Toro <davsamirtor@gmail.com>
# compatibility with python 2 and 3
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import
from builtins import object
# import build-in modules
import sys
# import third party modules
import cv2
import numpy as np
from numbers import Number
#from RRtoolbox.lib.arrayops import overlay
# special variables
# __all__ = []
__author__ = "David Toro"
# __copyright__ = "Copyright 2017, The <name> Project"
# __credits__ = [""]
__license__ = "GPL"
# __version__ = "1.0.0"
__maintainer__ = "David Toro"
__email__ = "davsamirtor@gmail.com"
# __status__ = "Pre-release"
[docs]def is_numpy(obj):
"""
returns True if object is obj numpy object
"""
return type(obj).__module__ == np.__name__
[docs]def convert(points, roll=None, _type=np.int, shift=None, apply="contours"):
"""
Convert contour or points.
:param points: contours or points
:param roll: roll points
:param _type: convert points to type
:param shift: shift points to (x, y)
:param apply: apply operations the combination "contours-points-list-array"
:return: converted points
"""
# normalize them to array
conv = np.array(points, _type)
assert not len(conv) or 2 == conv[0].size
# roll array
if roll is not None:
conv = np.roll(conv, roll * 2)
# shift array
if shift is not None:
conv += shift
if apply is None:
# determine operations by inspecting data
apply = ""
if not is_numpy(points):
apply += "list"
if len(points) and is_numpy(points[0]):
apply += "-contours"
else:
apply += "-points"
if "point" in apply:
conv = conv.reshape(-1, 2)
if "list" in apply:
return [tuple(i) for i in conv]
elif "contour" in apply:
conv = conv.reshape(-1, 1, 2)
if "list" in apply:
return list(conv)
#elif "array" in apply:
# pass
return conv
[docs]def norm_range(vec, lower=0, upper=255, type=int):
"""
clips vector to range [lower, upper] and a tuple with integers
:param vec: vector
:param lower: 0
:param upper: 255
:param type: int
:return:
"""
newvec = []
upper = type(upper)
lower = type(lower)
for i in vec:
if i > upper:
newvec.append(upper)
elif i < lower:
newvec.append(lower)
else:
newvec.append(type(i))
return tuple(newvec)
[docs]def draw_contour_groups(contour_groups, shape=None, binary=False):
"""
draw contours in separate colors
:param contour_groups: list of contours
:param shape: shape to draw on. If None shape is calculated.
:param binary: True to draw with Ones, False to draw with colors.
:return: image
"""
if shape is None:
# get the maximum coordinates in x, y
max_ = np.max([np.max([np.max(contours,0) for contours in group],0)
for group in contour_groups],0).reshape(2)
# calculate pad to correct small contours
pad = 1 #max_*np.exp(-max_*0.1) + 1
# get fitting window in all contours
x, y = max_ + pad
# get fitting shape
shape = int(y), int(x)
if binary:
img = np.zeros(shape[:2])
else:
img = np.zeros(shape[:2]+(3,))
vis = img.copy()
for contours in contour_groups:
if binary:
color = 1
else:
color = norm_range(np.random.rand(3) * 255, lower=20)
over = cv2.fillPoly(img.copy(), contours, color, cv2.LINE_4)
#over = cv2.drawContours(img.copy(), contours, -1, color, 0)
if binary:
vis += over
else:
vis = overlay(vis, over, 0.3)
#vis = cv2.addWeighted(vis, 0.5, over, 0.5, 0.0)
return vis
[docs]def find_roll_inv(ans, expected):
"""
roll an array until it is best matched to an expected array
:param ans: array to roll
:param expected: array which ans must be rolled to
:return: best result, inversion, roll, alltrue
"""
assert ans.shape == expected.shape
assert ans.size == expected.size
best = 0
best_ans = None
inv = False
roll = 0
for inv in (False, True):
if inv:
ans = ans[::-1] # copy it
for roll in range(ans.size):
comp = np.roll(ans, roll) == expected
ind = np.sum(comp)
# equivalent to np.alltrue(comp)
if comp.min() == True:
return comp, inv, roll, True
if ind > best:
best_ans = comp
best = ind
return best_ans, inv, roll, False
[docs]def check_contours(ans, expected, ignore_shape=False):
"""
check to contours are the same in an strict or lazy way.
:param ans: contours to check
:param expected: expected contours or ground truth
:param ignore_shape: True to ignore order, shapes and check
by approximation if ans yields similar results as expected.
if ignore_shape is a number then it will be the threshold which
is 0.1 when ignore_shape = True.
:return:
"""
if ignore_shape:
if not isinstance(ignore_shape, Number):
ignore_shape = 0.1
# check that cnts are equal by comparing their moments
if len(ans) == 0 and len(expected) == 0:
# both are empty
return True
elif len(ans) == 0 or len(expected) == 0:
# one of them is empty
return False
temp = draw_contour_groups([ans, expected], binary=True)
body = (temp != 0).sum() + 0.0000001
not_overlaped = (temp == 1).sum()
ret = not_overlaped / body
if ret > ignore_shape:
return False
# potentially risky if cnts are un ordered
#for ans_c, exp_c in zip(ans, expected):
# ret = cv2.matchShapes(ans_c, exp_c, 1, 0.0)
# if ret > 0.01:
# return False
else:
if len(ans) != len(expected):
return False
for ans_c, exp_c in zip(ans, expected):
# normalize them to points
ans_c = np.array([i for i in convert(ans_c).reshape(-1, 2)])
exp_c = np.array([i for i in convert(exp_c).reshape(-1, 2)])
# if after normalization they are not the same shape
# then they should not be equal
if ans_c.shape != exp_c.shape:
return False
if not find_roll_inv(ans_c, exp_c)[3]:
return False
return True