From ab4d3753491c4ebf36a4709ecdbe56d0b7b4c370 Mon Sep 17 00:00:00 2001 From: Vladislav Sovrasov Date: Thu, 28 Jan 2016 15:43:08 +0300 Subject: [PATCH] Add new tests on python --- modules/python/test/test.py | 55 ++----- modules/python/test/test_calibration.py | 69 ++++++++ modules/python/test/test_digits.py | 197 +++++++++++++++++++++++ modules/python/test/test_facedetect.py | 102 ++++++++++++ modules/python/test/test_fitline.py | 66 ++++++++ modules/python/test/test_gaussian_mix.py | 60 +++++++ modules/python/test/test_houghcircles.py | 57 +++++++ modules/python/test/test_houghlines.py | 65 ++++++++ modules/python/test/test_squares.py | 96 +++++++++++ modules/python/test/test_texture_flow.py | 52 ++++++ modules/python/test/tests_common.py | 56 +++++++ samples/data/kate.jpg | Bin 0 -> 40791 bytes samples/python/demo.py | 5 +- 13 files changed, 839 insertions(+), 41 deletions(-) mode change 100644 => 100755 modules/python/test/test.py create mode 100644 modules/python/test/test_calibration.py create mode 100644 modules/python/test/test_digits.py create mode 100644 modules/python/test/test_facedetect.py create mode 100644 modules/python/test/test_fitline.py create mode 100644 modules/python/test/test_gaussian_mix.py create mode 100644 modules/python/test/test_houghcircles.py create mode 100644 modules/python/test/test_houghlines.py create mode 100644 modules/python/test/test_squares.py create mode 100644 modules/python/test/test_texture_flow.py create mode 100644 modules/python/test/tests_common.py create mode 100644 samples/data/kate.jpg diff --git a/modules/python/test/test.py b/modules/python/test/test.py old mode 100644 new mode 100755 index 093979abaf..074b6edad8 --- a/modules/python/test/test.py +++ b/modules/python/test/test.py @@ -1,6 +1,8 @@ #!/usr/bin/env python + from __future__ import print_function + import unittest import random import time @@ -17,51 +19,24 @@ import numpy as np import cv2 import argparse +# local test modules +from test_digits import digits_test +from test_calibration import calibration_test +from test_squares import squares_test +from test_texture_flow import texture_flow_test +from test_fitline import fitline_test +from test_houghcircles import houghcircles_test +from test_houghlines import houghlines_test +from test_gaussian_mix import gaussian_mix_test +from test_facedetect import facedetect_test + # Python 3 moved urlopen to urllib.requests try: from urllib.request import urlopen except ImportError: from urllib import urlopen -class NewOpenCVTests(unittest.TestCase): - - # path to local repository folder containing 'samples' folder - repoPath = None - # github repository url - repoUrl = 'https://raw.github.com/Itseez/opencv/master' - - def get_sample(self, filename, iscolor = cv2.IMREAD_COLOR): - if not filename in self.image_cache: - filedata = None - if NewOpenCVTests.repoPath is not None: - candidate = NewOpenCVTests.repoPath + '/' + filename - if os.path.isfile(candidate): - with open(candidate, 'rb') as f: - filedata = f.read() - if filedata is None: - filedata = urlopen(NewOpenCVTests.repoUrl + '/' + filename).read() - self.image_cache[filename] = cv2.imdecode(np.fromstring(filedata, dtype=np.uint8), iscolor) - return self.image_cache[filename] - - def setUp(self): - self.image_cache = {} - - def hashimg(self, im): - """ Compute a hash for an image, useful for image comparisons """ - return hashlib.md5(im.tostring()).digest() - - if sys.version_info[:2] == (2, 6): - def assertLess(self, a, b, msg=None): - if not a < b: - self.fail('%s not less than %s' % (repr(a), repr(b))) - - def assertLessEqual(self, a, b, msg=None): - if not a <= b: - self.fail('%s not less than or equal to %s' % (repr(a), repr(b))) - - def assertGreater(self, a, b, msg=None): - if not a > b: - self.fail('%s not greater than %s' % (repr(a), repr(b))) +from tests_common import NewOpenCVTests # Tests to run first; check the handful of basic operations that the later tests rely on @@ -167,4 +142,4 @@ if __name__ == '__main__': NewOpenCVTests.repoPath = args.repo random.seed(0) unit_argv = [sys.argv[0]] + other; - unittest.main(argv=unit_argv) + unittest.main(argv=unit_argv) \ No newline at end of file diff --git a/modules/python/test/test_calibration.py b/modules/python/test/test_calibration.py new file mode 100644 index 0000000000..af8d0fcea0 --- /dev/null +++ b/modules/python/test/test_calibration.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +''' +camera calibration for distorted images with chess board samples +reads distorted images, calculates the calibration and write undistorted images +''' + +# Python 2/3 compatibility +from __future__ import print_function + +import numpy as np +import cv2 + + +from tests_common import NewOpenCVTests + +class calibration_test(NewOpenCVTests): + + def test_calibration(self): + + from glob import glob + + img_mask = '../../../samples/data/left*.jpg' # default + img_names = glob(img_mask) + + square_size = 1.0 + pattern_size = (9, 6) + pattern_points = np.zeros((np.prod(pattern_size), 3), np.float32) + pattern_points[:, :2] = np.indices(pattern_size).T.reshape(-1, 2) + pattern_points *= square_size + + obj_points = [] + img_points = [] + h, w = 0, 0 + img_names_undistort = [] + for fn in img_names: + img = cv2.imread(fn, 0) + if img is None: + continue + + h, w = img.shape[:2] + found, corners = cv2.findChessboardCorners(img, pattern_size) + if found: + term = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 0.1) + cv2.cornerSubPix(img, corners, (5, 5), (-1, -1), term) + + if not found: + continue + + img_points.append(corners.reshape(-1, 2)) + obj_points.append(pattern_points) + + # calculate camera distortion + rms, camera_matrix, dist_coefs, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, (w, h), None, None, flags = 0) + + eps = 0.01 + normCamEps = 10.0 + normDistEps = 0.01 + + cameraMatrixTest = [[ 532.80992189, 0., 342.4952186 ], + [ 0., 532.93346422, 233.8879292 ], + [ 0., 0., 1. ]] + + distCoeffsTest = [ -2.81325576e-01, 2.91130406e-02, + 1.21234330e-03, -1.40825372e-04, 1.54865844e-01] + + self.assertLess(abs(rms - 0.196334638034), eps) + self.assertLess(cv2.norm(camera_matrix - cameraMatrixTest, cv2.NORM_L1), normCamEps) + self.assertLess(cv2.norm(dist_coefs - distCoeffsTest, cv2.NORM_L1), normDistEps) \ No newline at end of file diff --git a/modules/python/test/test_digits.py b/modules/python/test/test_digits.py new file mode 100644 index 0000000000..9d1d2557c9 --- /dev/null +++ b/modules/python/test/test_digits.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python + +''' +SVM and KNearest digit recognition. + +Sample loads a dataset of handwritten digits from '../data/digits.png'. +Then it trains a SVM and KNearest classifiers on it and evaluates +their accuracy. + +Following preprocessing is applied to the dataset: + - Moment-based image deskew (see deskew()) + - Digit images are split into 4 10x10 cells and 16-bin + histogram of oriented gradients is computed for each + cell + - Transform histograms to space with Hellinger metric (see [1] (RootSIFT)) + + +[1] R. Arandjelovic, A. Zisserman + "Three things everyone should know to improve object retrieval" + http://www.robots.ox.ac.uk/~vgg/publications/2012/Arandjelovic12/arandjelovic12.pdf + +''' + + +# Python 2/3 compatibility +from __future__ import print_function + +# built-in modules +from multiprocessing.pool import ThreadPool + +import cv2 + +import numpy as np +from numpy.linalg import norm + + +SZ = 20 # size of each digit is SZ x SZ +CLASS_N = 10 +DIGITS_FN = '../../../samples/data/digits.png' + +def split2d(img, cell_size, flatten=True): + h, w = img.shape[:2] + sx, sy = cell_size + cells = [np.hsplit(row, w//sx) for row in np.vsplit(img, h//sy)] + cells = np.array(cells) + if flatten: + cells = cells.reshape(-1, sy, sx) + return cells + +def load_digits(fn): + digits_img = cv2.imread(fn, 0) + digits = split2d(digits_img, (SZ, SZ)) + labels = np.repeat(np.arange(CLASS_N), len(digits)/CLASS_N) + return digits, labels + +def deskew(img): + m = cv2.moments(img) + if abs(m['mu02']) < 1e-2: + return img.copy() + skew = m['mu11']/m['mu02'] + M = np.float32([[1, skew, -0.5*SZ*skew], [0, 1, 0]]) + img = cv2.warpAffine(img, M, (SZ, SZ), flags=cv2.WARP_INVERSE_MAP | cv2.INTER_LINEAR) + return img + +class StatModel(object): + def load(self, fn): + self.model.load(fn) # Known bug: https://github.com/Itseez/opencv/issues/4969 + def save(self, fn): + self.model.save(fn) + +class KNearest(StatModel): + def __init__(self, k = 3): + self.k = k + self.model = cv2.ml.KNearest_create() + + def train(self, samples, responses): + self.model.train(samples, cv2.ml.ROW_SAMPLE, responses) + + def predict(self, samples): + retval, results, neigh_resp, dists = self.model.findNearest(samples, self.k) + return results.ravel() + +class SVM(StatModel): + def __init__(self, C = 1, gamma = 0.5): + self.model = cv2.ml.SVM_create() + self.model.setGamma(gamma) + self.model.setC(C) + self.model.setKernel(cv2.ml.SVM_RBF) + self.model.setType(cv2.ml.SVM_C_SVC) + + def train(self, samples, responses): + self.model.train(samples, cv2.ml.ROW_SAMPLE, responses) + + def predict(self, samples): + return self.model.predict(samples)[1].ravel() + + +def evaluate_model(model, digits, samples, labels): + resp = model.predict(samples) + err = (labels != resp).mean() + + confusion = np.zeros((10, 10), np.int32) + for i, j in zip(labels, resp): + confusion[i, j] += 1 + + return err, confusion + +def preprocess_simple(digits): + return np.float32(digits).reshape(-1, SZ*SZ) / 255.0 + +def preprocess_hog(digits): + samples = [] + for img in digits: + gx = cv2.Sobel(img, cv2.CV_32F, 1, 0) + gy = cv2.Sobel(img, cv2.CV_32F, 0, 1) + mag, ang = cv2.cartToPolar(gx, gy) + bin_n = 16 + bin = np.int32(bin_n*ang/(2*np.pi)) + bin_cells = bin[:10,:10], bin[10:,:10], bin[:10,10:], bin[10:,10:] + mag_cells = mag[:10,:10], mag[10:,:10], mag[:10,10:], mag[10:,10:] + hists = [np.bincount(b.ravel(), m.ravel(), bin_n) for b, m in zip(bin_cells, mag_cells)] + hist = np.hstack(hists) + + # transform to Hellinger kernel + eps = 1e-7 + hist /= hist.sum() + eps + hist = np.sqrt(hist) + hist /= norm(hist) + eps + + samples.append(hist) + return np.float32(samples) + +from tests_common import NewOpenCVTests + +class digits_test(NewOpenCVTests): + + def test_digits(self): + + digits, labels = load_digits(DIGITS_FN) + + # shuffle digits + rand = np.random.RandomState(321) + shuffle = rand.permutation(len(digits)) + digits, labels = digits[shuffle], labels[shuffle] + + digits2 = list(map(deskew, digits)) + samples = preprocess_hog(digits2) + + train_n = int(0.9*len(samples)) + digits_train, digits_test = np.split(digits2, [train_n]) + samples_train, samples_test = np.split(samples, [train_n]) + labels_train, labels_test = np.split(labels, [train_n]) + errors = list() + confusionMatrixes = list() + + model = KNearest(k=4) + model.train(samples_train, labels_train) + error, confusion = evaluate_model(model, digits_test, samples_test, labels_test) + errors.append(error) + confusionMatrixes.append(confusion) + + model = SVM(C=2.67, gamma=5.383) + model.train(samples_train, labels_train) + error, confusion = evaluate_model(model, digits_test, samples_test, labels_test) + errors.append(error) + confusionMatrixes.append(confusion) + + eps = 0.001 + normEps = len(samples_test) * 0.02 + + confusionKNN = [[45, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 57, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 59, 1, 0, 0, 0, 0, 1, 0], + [ 0, 0, 0, 43, 0, 0, 0, 1, 0, 0], + [ 0, 0, 0, 0, 38, 0, 2, 0, 0, 0], + [ 0, 0, 0, 2, 0, 48, 0, 0, 1, 0], + [ 0, 1, 0, 0, 0, 0, 51, 0, 0, 0], + [ 0, 0, 1, 0, 0, 0, 0, 54, 0, 0], + [ 0, 0, 0, 0, 0, 1, 0, 0, 46, 0], + [ 1, 1, 0, 1, 1, 0, 0, 0, 2, 42]] + + confusionSVM = [[45, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 57, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 59, 2, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 43, 0, 0, 0, 1, 0, 0], + [ 0, 0, 0, 0, 40, 0, 0, 0, 0, 0], + [ 0, 0, 0, 1, 0, 50, 0, 0, 0, 0], + [ 0, 0, 0, 0, 1, 0, 51, 0, 0, 0], + [ 0, 0, 1, 0, 0, 0, 0, 54, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 47, 0], + [ 0, 1, 0, 1, 0, 0, 0, 0, 1, 45]] + + self.assertLess(cv2.norm(confusionMatrixes[0] - confusionKNN, cv2.NORM_L1), normEps) + self.assertLess(cv2.norm(confusionMatrixes[1] - confusionSVM, cv2.NORM_L1), normEps) + + self.assertLess(errors[0] - 0.034, eps) + self.assertLess(errors[1] - 0.018, eps) \ No newline at end of file diff --git a/modules/python/test/test_facedetect.py b/modules/python/test/test_facedetect.py new file mode 100644 index 0000000000..7fe64e2072 --- /dev/null +++ b/modules/python/test/test_facedetect.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +''' +face detection using haar cascades +''' + +# Python 2/3 compatibility +from __future__ import print_function + +import numpy as np +import cv2 + +def intersectionRate(s1, s2): + + x1, y1, x2, y2 = s1 + s1 = [[x1, y1], [x2,y1], [x2, y2], [x1, y2] ] + + x1, y1, x2, y2 = s2 + s2 = [[x1, y1], [x2,y1], [x2, y2], [x1, y2] ] + + area, intersection = cv2.intersectConvexConvex(np.array(s1), np.array(s2)) + return 2 * area / (cv2.contourArea(np.array(s1)) + cv2.contourArea(np.array(s2))) + +def detect(img, cascade): + rects = cascade.detectMultiScale(img, scaleFactor=1.3, minNeighbors=4, minSize=(30, 30), + flags=cv2.CASCADE_SCALE_IMAGE) + if len(rects) == 0: + return [] + rects[:,2:] += rects[:,:2] + return rects + +from tests_common import NewOpenCVTests + +class facedetect_test(NewOpenCVTests): + + def test_facedetect(self): + import sys, getopt + + cascade_fn = "../../../data/haarcascades/haarcascade_frontalface_alt.xml" + nested_fn = "../../../data/haarcascades/haarcascade_eye.xml" + + cascade = cv2.CascadeClassifier(cascade_fn) + nested = cv2.CascadeClassifier(nested_fn) + + dirPath = '../../../samples/data/' + samples = ['lena.jpg', 'kate.jpg'] + + faces = [] + eyes = [] + + testFaces = [ + #lena + [[218, 200, 389, 371], + [ 244, 240, 294, 290], + [ 309, 246, 352, 289]], + + #kate + [[207, 89, 436, 318], + [245, 161, 294, 210], + [343, 139, 389, 185]] + ] + + for sample in samples: + + img = cv2.imread(dirPath + sample) + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + gray = cv2.GaussianBlur(gray, (3, 3), 1.1) + + rects = detect(gray, cascade) + faces.append(rects) + + if not nested.empty(): + for x1, y1, x2, y2 in rects: + roi = gray[y1:y2, x1:x2] + subrects = detect(roi.copy(), nested) + + for rect in subrects: + rect[0] += x1 + rect[2] += x1 + rect[1] += y1 + rect[3] += y1 + + eyes.append(subrects) + + faces_matches = 0 + eyes_matches = 0 + + eps = 0.8 + + for i in range(len(faces)): + for j in range(len(testFaces)): + if intersectionRate(faces[i][0], testFaces[j][0]) > eps: + faces_matches += 1 + #check eyes + if len(eyes[i]) == 2: + if intersectionRate(eyes[i][0], testFaces[j][1]) > eps and intersectionRate(eyes[i][1], testFaces[j][2]): + eyes_matches += 1 + elif intersectionRate(eyes[i][1], testFaces[j][1]) > eps and intersectionRate(eyes[i][0], testFaces[j][2]): + eyes_matches += 1 + + self.assertEqual(faces_matches, 2) + self.assertEqual(eyes_matches, 2) \ No newline at end of file diff --git a/modules/python/test/test_fitline.py b/modules/python/test/test_fitline.py new file mode 100644 index 0000000000..7de9573385 --- /dev/null +++ b/modules/python/test/test_fitline.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +''' +Robust line fitting. +================== + +Example of using cv2.fitLine function for fitting line +to points in presence of outliers. + +Switch through different M-estimator functions and see, +how well the robust functions fit the line even +in case of ~50% of outliers. + +''' + +# Python 2/3 compatibility +from __future__ import print_function +import sys +PY3 = sys.version_info[0] == 3 + +import numpy as np +import cv2 + +from tests_common import NewOpenCVTests + +w, h = 512, 256 + +def toint(p): + return tuple(map(int, p)) + +def sample_line(p1, p2, n, noise=0.0): + np.random.seed(10) + p1 = np.float32(p1) + t = np.random.rand(n,1) + return p1 + (p2-p1)*t + np.random.normal(size=(n, 2))*noise + +dist_func_names = ['DIST_L2', 'DIST_L1', 'DIST_L12', 'DIST_FAIR', 'DIST_WELSCH', 'DIST_HUBER'] + +class fitline_test(NewOpenCVTests): + + def test_fitline(self): + + noise = 5 + n = 200 + r = 5 / 100.0 + outn = int(n*r) + + p0, p1 = (90, 80), (w-90, h-80) + line_points = sample_line(p0, p1, n-outn, noise) + outliers = np.random.rand(outn, 2) * (w, h) + points = np.vstack([line_points, outliers]) + + lines = [] + + for name in dist_func_names: + func = getattr(cv2, name) + vx, vy, cx, cy = cv2.fitLine(np.float32(points), func, 0, 0.01, 0.01) + line = [float(vx), float(vy), float(cx), float(cy)] + lines.append(line) + + eps = 0.05 + + refVec = (np.float32(p1) - p0) / cv2.norm(np.float32(p1) - p0) + + for i in range(len(lines)): + self.assertLessEqual(cv2.norm(refVec - lines[i][0:2], cv2.NORM_L2), eps) \ No newline at end of file diff --git a/modules/python/test/test_gaussian_mix.py b/modules/python/test/test_gaussian_mix.py new file mode 100644 index 0000000000..58802d4c14 --- /dev/null +++ b/modules/python/test/test_gaussian_mix.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +# Python 2/3 compatibility +from __future__ import print_function +import sys +PY3 = sys.version_info[0] == 3 + +if PY3: + xrange = range + +import numpy as np +from numpy import random +import cv2 + +def make_gaussians(cluster_n, img_size): + points = [] + ref_distrs = [] + for i in xrange(cluster_n): + mean = (0.1 + 0.8*random.rand(2)) * img_size + a = (random.rand(2, 2)-0.5)*img_size*0.1 + cov = np.dot(a.T, a) + img_size*0.05*np.eye(2) + n = 100 + random.randint(900) + pts = random.multivariate_normal(mean, cov, n) + points.append( pts ) + ref_distrs.append( (mean, cov) ) + points = np.float32( np.vstack(points) ) + return points, ref_distrs + +from tests_common import NewOpenCVTests + +class gaussian_mix_test(NewOpenCVTests): + + def test_gaussian_mix(self): + + np.random.seed(10) + cluster_n = 5 + img_size = 512 + + points, ref_distrs = make_gaussians(cluster_n, img_size) + + em = cv2.ml.EM_create() + em.setClustersNumber(cluster_n) + em.setCovarianceMatrixType(cv2.ml.EM_COV_MAT_GENERIC) + em.trainEM(points) + means = em.getMeans() + covs = em.getCovs() # Known bug: https://github.com/Itseez/opencv/pull/4232 + found_distrs = zip(means, covs) + + matches_count = 0 + + meanEps = 0.05 + covEps = 0.1 + + for i in range(cluster_n): + for j in range(cluster_n): + if (cv2.norm(means[i] - ref_distrs[j][0], cv2.NORM_L2) / cv2.norm(ref_distrs[j][0], cv2.NORM_L2) < meanEps and + cv2.norm(covs[i] - ref_distrs[j][1], cv2.NORM_L2) / cv2.norm(ref_distrs[j][1], cv2.NORM_L2) < covEps): + matches_count += 1 + + self.assertEqual(matches_count, cluster_n) \ No newline at end of file diff --git a/modules/python/test/test_houghcircles.py b/modules/python/test/test_houghcircles.py new file mode 100644 index 0000000000..dc4284a41f --- /dev/null +++ b/modules/python/test/test_houghcircles.py @@ -0,0 +1,57 @@ +#!/usr/bin/python + +''' +This example illustrates how to use cv2.HoughCircles() function. +''' + +# Python 2/3 compatibility +from __future__ import print_function + +import cv2 +import numpy as np +import sys + +from tests_common import NewOpenCVTests + +class houghcircles_test(NewOpenCVTests): + + def test_houghcircles(self): + + fn = "../../../samples/data/board.jpg" + + src = cv2.imread(fn, 1) + img = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) + img = cv2.medianBlur(img, 5) + + circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1, 10, np.array([]), 100, 30, 1, 30)[0] + + testCircles = [[38, 181, 17.6], + [99.7, 166, 13.12], + [142.7, 160, 13.52], + [223.6, 110, 8.62], + [79.1, 206.7, 8.62], + [47.5, 351.6, 11.64], + [189.5, 354.4, 11.64], + [189.8, 298.9, 10.64], + [189.5, 252.4, 14.62], + [252.5, 393.4, 15.62], + [602.9, 467.5, 11.42], + [222, 210.4, 9.12], + [263.1, 216.7, 9.12], + [359.8, 222.6, 9.12], + [518.9, 120.9, 9.12], + [413.8, 113.4, 9.12], + [489, 127.2, 9.12], + [448.4, 121.3, 9.12], + [384.6, 128.9, 8.62]] + + eps = 7 + matches_counter = 0 + + for i in range(len(testCircles)): + for j in range(len(circles)): + if cv2.norm(testCircles[i] - circles[j], cv2.NORM_L2) < eps: + matches_counter += 1 + + self.assertGreater(float(matches_counter) / len(testCircles), .5) + self.assertLess(float(len(circles) - matches_counter) / len(circles), .7) \ No newline at end of file diff --git a/modules/python/test/test_houghlines.py b/modules/python/test/test_houghlines.py new file mode 100644 index 0000000000..b779129796 --- /dev/null +++ b/modules/python/test/test_houghlines.py @@ -0,0 +1,65 @@ +#!/usr/bin/python + +''' +This example illustrates how to use Hough Transform to find lines +''' + +# Python 2/3 compatibility +from __future__ import print_function + +import cv2 +import numpy as np +import sys +import math + +from tests_common import NewOpenCVTests + +def linesDiff(line1, line2): + + norm1 = cv2.norm(line1 - line2, cv2.NORM_L2) + line3 = line1[2:4] + line1[0:2] + norm2 = cv2.norm(line3 - line2, cv2.NORM_L2) + + return min(norm1, norm2) + +class houghlines_test(NewOpenCVTests): + + def test_houghlines(self): + + fn = "../../../samples/data/pic1.png" + + src = cv2.imread(fn) + dst = cv2.Canny(src, 50, 200) + + lines = cv2.HoughLinesP(dst, 1, math.pi/180.0, 40, np.array([]), 50, 10)[:,0,:] + + eps = 5 + testLines = [ + #rect1 + [ 232, 25, 43, 25], + [ 43, 129, 232, 129], + [ 43, 129, 43, 25], + [232, 129, 232, 25], + #rect2 + [251, 86, 314, 183], + [252, 86, 323, 40], + [315, 183, 386, 137], + [324, 40, 386, 136], + #triangle + [245, 205, 377, 205], + [244, 206, 305, 278], + [306, 279, 377, 205], + #rect3 + [153, 177, 196, 177], + [153, 277, 153, 179], + [153, 277, 196, 277], + [196, 177, 196, 277]] + + matches_counter = 0 + + for i in range(len(testLines)): + for j in range(len(lines)): + if linesDiff(testLines[i], lines[j]) < eps: + matches_counter += 1 + + self.assertGreater(float(matches_counter) / len(testLines), .7) \ No newline at end of file diff --git a/modules/python/test/test_squares.py b/modules/python/test/test_squares.py new file mode 100644 index 0000000000..937b526b08 --- /dev/null +++ b/modules/python/test/test_squares.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +''' +Simple "Square Detector" program. + +Loads several images sequentially and tries to find squares in each image. +''' + +# Python 2/3 compatibility +import sys +PY3 = sys.version_info[0] == 3 + +if PY3: + xrange = range + +import numpy as np +import cv2 + + +def angle_cos(p0, p1, p2): + d1, d2 = (p0-p1).astype('float'), (p2-p1).astype('float') + return abs( np.dot(d1, d2) / np.sqrt( np.dot(d1, d1)*np.dot(d2, d2) ) ) + +def find_squares(img): + img = cv2.GaussianBlur(img, (5, 5), 0) + squares = [] + for gray in cv2.split(img): + for thrs in xrange(0, 255, 26): + if thrs == 0: + bin = cv2.Canny(gray, 0, 50, apertureSize=5) + bin = cv2.dilate(bin, None) + else: + retval, bin = cv2.threshold(gray, thrs, 255, cv2.THRESH_BINARY) + bin, contours, hierarchy = cv2.findContours(bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) + for cnt in contours: + cnt_len = cv2.arcLength(cnt, True) + cnt = cv2.approxPolyDP(cnt, 0.02*cnt_len, True) + if len(cnt) == 4 and cv2.contourArea(cnt) > 1000 and cv2.isContourConvex(cnt): + cnt = cnt.reshape(-1, 2) + max_cos = np.max([angle_cos( cnt[i], cnt[(i+1) % 4], cnt[(i+2) % 4] ) for i in xrange(4)]) + if max_cos < 0.1 and filterSquares(squares, cnt): + squares.append(cnt) + + return squares + +def intersectionRate(s1, s2): + area, intersection = cv2.intersectConvexConvex(np.array(s1), np.array(s2)) + return 2 * area / (cv2.contourArea(np.array(s1)) + cv2.contourArea(np.array(s2))) + +def filterSquares(squares, square): + + for i in range(len(squares)): + if intersectionRate(squares[i], square) > 0.95: + return False + + return True + +from tests_common import NewOpenCVTests + +class squares_test(NewOpenCVTests): + + def test_squares(self): + + img = cv2.imread('../../../samples/data/pic1.png') + squares = find_squares(img) + + testSquares = [ + [[43, 25], + [43, 129], + [232, 129], + [232, 25]], + + [[252, 87], + [324, 40], + [387, 137], + [315, 184]], + + [[154, 178], + [196, 180], + [198, 278], + [154, 278]], + + [[0, 0], + [400, 0], + [400, 300], + [0, 300]] + ] + + matches_counter = 0 + for i in range(len(squares)): + for j in range(len(testSquares)): + if intersectionRate(squares[i], testSquares[j]) > 0.9: + matches_counter += 1 + + self.assertGreater(matches_counter / len(testSquares), 0.9) + self.assertLess( (len(squares) - matches_counter) / len(squares), 0.2) \ No newline at end of file diff --git a/modules/python/test/test_texture_flow.py b/modules/python/test/test_texture_flow.py new file mode 100644 index 0000000000..46d680a7f4 --- /dev/null +++ b/modules/python/test/test_texture_flow.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +''' +Texture flow direction estimation. + +Sample shows how cv2.cornerEigenValsAndVecs function can be used +to estimate image texture flow direction. +''' + +# Python 2/3 compatibility +from __future__ import print_function + +import numpy as np +import cv2 +import sys + +from tests_common import NewOpenCVTests + + +class texture_flow_test(NewOpenCVTests): + + def test_texture_flow(self): + + fn = '../../../samples/data/pic6.png' + img = cv2.imread(fn) + + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + h, w = img.shape[:2] + + eigen = cv2.cornerEigenValsAndVecs(gray, 15, 3) + eigen = eigen.reshape(h, w, 3, 2) # [[e1, e2], v1, v2] + flow = eigen[:,:,2] + + vis = img.copy() + vis[:] = (192 + np.uint32(vis)) / 2 + d = 80 + points = np.dstack( np.mgrid[d/2:w:d, d/2:h:d] ).reshape(-1, 2) + + textureVectors = [] + + for x, y in np.int32(points): + textureVectors.append(np.int32(flow[y, x]*d)) + + eps = 0.05 + + testTextureVectors = [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], + [-38, 70], [-79, 3], [0, 0], [0, 0], [-39, 69], [-79, -1], + [0, 0], [0, 0], [0, -79], [17, -78], [-48, -63], [65, -46], + [-69, -39], [-48, -63], [-45, 66]] + + for i in range(len(textureVectors)): + self.assertLessEqual(cv2.norm(textureVectors[i] - testTextureVectors[i], cv2.NORM_L2), eps) \ No newline at end of file diff --git a/modules/python/test/tests_common.py b/modules/python/test/tests_common.py new file mode 100644 index 0000000000..6ab26050b3 --- /dev/null +++ b/modules/python/test/tests_common.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import unittest +import sys +import hashlib +import os +import numpy as np +import cv2 + +# Python 3 moved urlopen to urllib.requests +try: + from urllib.request import urlopen +except ImportError: + from urllib import urlopen + +class NewOpenCVTests(unittest.TestCase): + + # path to local repository folder containing 'samples' folder + repoPath = None + # github repository url + repoUrl = 'https://raw.github.com/Itseez/opencv/master' + + def get_sample(self, filename, iscolor = cv2.IMREAD_COLOR): + if not filename in self.image_cache: + filedata = None + if NewOpenCVTests.repoPath is not None: + candidate = NewOpenCVTests.repoPath + '/' + filename + if os.path.isfile(candidate): + with open(candidate, 'rb') as f: + filedata = f.read() + if filedata is None: + filedata = urlopen(NewOpenCVTests.repoUrl + '/' + filename).read() + self.image_cache[filename] = cv2.imdecode(np.fromstring(filedata, dtype=np.uint8), iscolor) + return self.image_cache[filename] + + def setUp(self): + self.image_cache = {} + + def hashimg(self, im): + """ Compute a hash for an image, useful for image comparisons """ + return hashlib.md5(im.tostring()).digest() + + if sys.version_info[:2] == (2, 6): + def assertLess(self, a, b, msg=None): + if not a < b: + self.fail('%s not less than %s' % (repr(a), repr(b))) + + def assertLessEqual(self, a, b, msg=None): + if not a <= b: + self.fail('%s not less than or equal to %s' % (repr(a), repr(b))) + + def assertGreater(self, a, b, msg=None): + if not a > b: + self.fail('%s not greater than %s' % (repr(a), repr(b))) \ No newline at end of file diff --git a/samples/data/kate.jpg b/samples/data/kate.jpg new file mode 100644 index 0000000000000000000000000000000000000000..40ab433bfde29689600f3d2a9308e0745f7fe988 GIT binary patch literal 40791 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 z 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', self.on_demo_select) self.cmd_entry = cmd_entry = tk.Entry(right)