From 3237e0ec769414e5cffba0cbf110aa9a050b6f3f Mon Sep 17 00:00:00 2001 From: Andre Anjos <andre.dos.anjos@gmail.com> Date: Thu, 24 Apr 2014 11:51:16 +0200 Subject: [PATCH] Improve tests to use OpenCV python bindings if available; Test if keypoints are inside bounding-boxes --- xbob/ip/flandmark/flandmark.cpp | 48 +++++++--------- xbob/ip/flandmark/test.py | 97 +++++++++++++++++++++++++++++---- 2 files changed, 107 insertions(+), 38 deletions(-) diff --git a/xbob/ip/flandmark/flandmark.cpp b/xbob/ip/flandmark/flandmark.cpp index 9d9d316..2b47b3c 100644 --- a/xbob/ip/flandmark/flandmark.cpp +++ b/xbob/ip/flandmark/flandmark.cpp @@ -49,7 +49,6 @@ static auto s_class = xbob::extension::ClassDoc( typedef struct { PyObject_HEAD FLANDMARK_Model* flandmark; - double* landmarks; std::string filename; } PyBobIpFlandmarkObject; @@ -97,25 +96,15 @@ static int PyBobIpFlandmark_init return -1; } - //flandmark is now initialized, allocate keypoint place-holder - self->landmarks = new double[2*self->flandmark->data.options.M]; - if (!self->landmarks) { - flandmark_free(self->flandmark); - PyErr_Format(PyExc_RuntimeError, "`%s' could not allocate memory for %d keypoint place-holders for model `%s'", Py_TYPE(self)->tp_name, 2*self->flandmark->data.options.M, c_filename); - return -1; - } - - //set filename + //flandmark is now initialized, set filename self->filename = c_filename; - //all good, flandmark is initialized + //all good, flandmark is ready return 0; } static void PyBobIpFlandmark_delete (PyBobIpFlandmarkObject* self) { - delete[] self->landmarks; - self->landmarks = 0; flandmark_free(self->flandmark); self->flandmark = 0; Py_TYPE(self)->tp_free((PyObject*)self); @@ -139,32 +128,35 @@ static PyObject* call(PyBobIpFlandmarkObject* self, for (int i=0; i<nbbx; ++i) { + //allocate output array _and_ Flandmark buffer within a single structure + Py_ssize_t shape[2]; + shape[0] = self->flandmark->data.options.M; + shape[1] = 2; + PyObject* landmarks = PyArray_SimpleNew(2, shape, NPY_FLOAT64); + if (!landmarks) return 0; + auto landmarks_ = make_safe(landmarks); + double* buffer = reinterpret_cast<double*>(PyArray_DATA((PyArrayObject*)landmarks)); + int result = 0; Py_BEGIN_ALLOW_THREADS - result = flandmark_detect(image.get(), &bbx[4*i], - self->flandmark, self->landmarks); + result = flandmark_detect(image.get(), &bbx[4*i], self->flandmark, buffer); Py_END_ALLOW_THREADS - PyObject* landmarks = 0; if (result != NO_ERR) { Py_INCREF(Py_None); landmarks = Py_None; } else { - landmarks = PyTuple_New(self->flandmark->data.options.M); - if (!landmarks) return 0; - auto landmarks_ = make_safe(landmarks); + //swap keypoint coordinates (x, y) -> (y, x) + double tmp; for (int k = 0; k < (2*self->flandmark->data.options.M); k += 2) { - PyTuple_SET_ITEM(landmarks, k/2, - Py_BuildValue("dd", - self->landmarks[k+1], //y value - self->landmarks[k] //x value - ) - ); - if (!PyTuple_GET_ITEM(landmarks, k/2)) return 0; + tmp = buffer[k]; + buffer[k] = buffer[k+1]; + buffer[k+1] = tmp; } Py_INCREF(landmarks); } + PyTuple_SET_ITEM(retval, i, landmarks); } @@ -192,7 +184,7 @@ static auto s_call = xbob::extension::FunctionDoc( "4. Mouth-corner-l (left corner of the mouth)\n" "5. Canthus-rr (outer corner of the right eye)\n" "6. Canthus-ll (outer corner of the left eye)\n" - "7. Nose\n" + "7. Nose\n" "\n" "Each point is returned as tuple defining the pixel positions in the form " "(y, x).\n" @@ -203,7 +195,7 @@ static auto s_call = xbob::extension::FunctionDoc( "The image Flandmark will operate on") .add_parameter("y, x", "int", "The top left-most corner of the bounding box containing the face image you want to locate keypoints on.") .add_parameter("height, width", "int", "The dimensions accross ``y`` (height) and ``x`` (width) for the bounding box, in number of pixels.") - .add_return("landmarks", "tuple", "A sequence of tuples, each containing locations in the format ``(y, x)``, for each of the key-points defined above and in that order.") + .add_return("landmarks", "array (2D, float64)", "Each row in the output array contains the locations of keypoints in the format ``(y, x)``") ; static PyObject* PyBobIpFlandmark_call_single(PyBobIpFlandmarkObject* self, diff --git a/xbob/ip/flandmark/test.py b/xbob/ip/flandmark/test.py index 8c3ef1c..67a4d75 100644 --- a/xbob/ip/flandmark/test.py +++ b/xbob/ip/flandmark/test.py @@ -7,6 +7,8 @@ """ import os +import numpy +import functools import pkg_resources import nose.tools import xbob.io @@ -20,7 +22,7 @@ def F(f): LENA = F('lena.jpg') LENA_BBX = [ - (214, 202, 183, 183) + [214, 202, 183, 183] ] #from OpenCV's cascade detector MULTI = F('multi.jpg') @@ -45,10 +47,75 @@ def opencv_detect(image): 1.3, #scaleFactor (at each time the image is re-scaled) 4, #minNeighbors (around candidate to be retained) 0, #flags (normally, should be set to zero) - (20,20), #(minSize, maxSize) (of detected objects) + (20,20), #(minSize, maxSize) (of detected objects on that scale) ) -@nose.tools.nottest +def pnpoly(point, vertices): + """Python translation of the C algorithm taken from: + http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html + """ + + (x, y) = point + j = vertices[-1] + c = False + for i in vertices: + if ( (i[1] > y) != (j[1] > y) ) and \ + ( x < (((j[0]-i[0]) * (y-i[1]) / (j[1]-i[1])) + i[0]) ): + c = not c + j = i + + return c + +def is_inside(point, box, eps=1e-5): + """Calculates, using matplotlib, if a point lies inside a bounding box""" + + (y, x, height, width) = box + #note: vertices must be organized clockwise + vertices = numpy.array([ + (x-eps, y-eps), + (x+width+eps, y-eps), + (x+width+eps, y+height+eps), + (x-eps, y+height+eps), + ], dtype=float) + return pnpoly((point[1], point[0]), vertices) + +def opencv_available(test): + """Decorator for detecting if OpenCV/Python bindings are available""" + + @functools.wraps(test) + def wrapper(*args, **kwargs): + try: + import cv2 + return test(*args, **kwargs) + except ImportError: + raise SkipTest("The cv2 module is not available") + + return wrapper + +def test_is_inside(): + + box = (0, 0, 1, 1) + + # really inside + assert is_inside((0.5, 0.5), box, eps=1e-10) + + # on the limit of the box + assert is_inside((0.0, 0.0), box, eps=1e-10) + assert is_inside((1.0, 1.0), box, eps=1e-10) + assert is_inside((1.0, 0.0), box, eps=1e-10) + assert is_inside((0.0, 1.0), box, eps=1e-10) + +def test_is_outside(): + + box = (0, 0, 1, 1) + + # really outside the box + assert not is_inside((1.5, 1.0), box, eps=1e-10) + assert not is_inside((0.5, 1.5), box, eps=1e-10) + assert not is_inside((1.5, 1.5), box, eps=1e-10) + assert not is_inside((-0.5, -0.5), box, eps=1e-10) + +@opencv_available def test_lena_opencv(): img = xbob.io.load(LENA) @@ -57,7 +124,10 @@ def test_lena_opencv(): flm = Flandmark() keypoints = flm.locate(gray, y, x, height, width) - assert keypoints + nose.tools.eq_(keypoints.shape, (8, 2)) + nose.tools.eq_(keypoints.dtype, 'float64') + for k in keypoints: + assert is_inside(k, (y, x, height, width), eps=1) def test_lena(): @@ -67,10 +137,12 @@ def test_lena(): flm = Flandmark() keypoints = flm.locate(gray, y, x, height, width) - assert keypoints - nose.tools.eq_(len(keypoints), 8) + nose.tools.eq_(keypoints.shape, (8, 2)) + nose.tools.eq_(keypoints.dtype, 'float64') + for k in keypoints: + assert is_inside(k, (y, x, height, width), eps=1) -@nose.tools.nottest +@opencv_available def test_multi_opencv(): img = xbob.io.load(MULTI) @@ -80,7 +152,10 @@ def test_multi_opencv(): flm = Flandmark() for (x, y, width, height) in bbx: keypoints = flm.locate(gray, y, x, height, width) - assert keypoints + nose.tools.eq_(keypoints.shape, (8, 2)) + nose.tools.eq_(keypoints.dtype, 'float64') + for k in keypoints: + assert is_inside(k, (y, x, height, width), eps=1) def test_multi(): @@ -90,5 +165,7 @@ def test_multi(): flm = Flandmark() for (x, y, width, height) in MULTI_BBX: keypoints = flm.locate(gray, y, x, height, width) - assert keypoints - nose.tools.eq_(len(keypoints), 8) + nose.tools.eq_(keypoints.shape, (8, 2)) + nose.tools.eq_(keypoints.dtype, 'float64') + for k in keypoints: + assert is_inside(k, (y, x, height, width), eps=1) -- GitLab