Skip to content
Snippets Groups Projects
Commit 3237e0ec authored by André Anjos's avatar André Anjos :speech_balloon:
Browse files

Improve tests to use OpenCV python bindings if available; Test if keypoints...

Improve tests to use OpenCV python bindings if available; Test if keypoints are inside bounding-boxes
parent 30a8333f
No related branches found
No related tags found
No related merge requests found
...@@ -49,7 +49,6 @@ static auto s_class = xbob::extension::ClassDoc( ...@@ -49,7 +49,6 @@ static auto s_class = xbob::extension::ClassDoc(
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
FLANDMARK_Model* flandmark; FLANDMARK_Model* flandmark;
double* landmarks;
std::string filename; std::string filename;
} PyBobIpFlandmarkObject; } PyBobIpFlandmarkObject;
...@@ -97,25 +96,15 @@ static int PyBobIpFlandmark_init ...@@ -97,25 +96,15 @@ static int PyBobIpFlandmark_init
return -1; return -1;
} }
//flandmark is now initialized, allocate keypoint place-holder //flandmark is now initialized, set filename
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
self->filename = c_filename; self->filename = c_filename;
//all good, flandmark is initialized //all good, flandmark is ready
return 0; return 0;
} }
static void PyBobIpFlandmark_delete (PyBobIpFlandmarkObject* self) { static void PyBobIpFlandmark_delete (PyBobIpFlandmarkObject* self) {
delete[] self->landmarks;
self->landmarks = 0;
flandmark_free(self->flandmark); flandmark_free(self->flandmark);
self->flandmark = 0; self->flandmark = 0;
Py_TYPE(self)->tp_free((PyObject*)self); Py_TYPE(self)->tp_free((PyObject*)self);
...@@ -139,32 +128,35 @@ static PyObject* call(PyBobIpFlandmarkObject* self, ...@@ -139,32 +128,35 @@ static PyObject* call(PyBobIpFlandmarkObject* self,
for (int i=0; i<nbbx; ++i) { 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; int result = 0;
Py_BEGIN_ALLOW_THREADS Py_BEGIN_ALLOW_THREADS
result = flandmark_detect(image.get(), &bbx[4*i], result = flandmark_detect(image.get(), &bbx[4*i], self->flandmark, buffer);
self->flandmark, self->landmarks);
Py_END_ALLOW_THREADS Py_END_ALLOW_THREADS
PyObject* landmarks = 0;
if (result != NO_ERR) { if (result != NO_ERR) {
Py_INCREF(Py_None); Py_INCREF(Py_None);
landmarks = Py_None; landmarks = Py_None;
} }
else { else {
landmarks = PyTuple_New(self->flandmark->data.options.M); //swap keypoint coordinates (x, y) -> (y, x)
if (!landmarks) return 0; double tmp;
auto landmarks_ = make_safe(landmarks);
for (int k = 0; k < (2*self->flandmark->data.options.M); k += 2) { for (int k = 0; k < (2*self->flandmark->data.options.M); k += 2) {
PyTuple_SET_ITEM(landmarks, k/2, tmp = buffer[k];
Py_BuildValue("dd", buffer[k] = buffer[k+1];
self->landmarks[k+1], //y value buffer[k+1] = tmp;
self->landmarks[k] //x value
)
);
if (!PyTuple_GET_ITEM(landmarks, k/2)) return 0;
} }
Py_INCREF(landmarks); Py_INCREF(landmarks);
} }
PyTuple_SET_ITEM(retval, i, landmarks); PyTuple_SET_ITEM(retval, i, landmarks);
} }
...@@ -192,7 +184,7 @@ static auto s_call = xbob::extension::FunctionDoc( ...@@ -192,7 +184,7 @@ static auto s_call = xbob::extension::FunctionDoc(
"4. Mouth-corner-l (left corner of the mouth)\n" "4. Mouth-corner-l (left corner of the mouth)\n"
"5. Canthus-rr (outer corner of the right eye)\n" "5. Canthus-rr (outer corner of the right eye)\n"
"6. Canthus-ll (outer corner of the left eye)\n" "6. Canthus-ll (outer corner of the left eye)\n"
"7. Nose\n" "7. Nose\n"
"\n" "\n"
"Each point is returned as tuple defining the pixel positions in the form " "Each point is returned as tuple defining the pixel positions in the form "
"(y, x).\n" "(y, x).\n"
...@@ -203,7 +195,7 @@ static auto s_call = xbob::extension::FunctionDoc( ...@@ -203,7 +195,7 @@ static auto s_call = xbob::extension::FunctionDoc(
"The image Flandmark will operate on") "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("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_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, static PyObject* PyBobIpFlandmark_call_single(PyBobIpFlandmarkObject* self,
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
""" """
import os import os
import numpy
import functools
import pkg_resources import pkg_resources
import nose.tools import nose.tools
import xbob.io import xbob.io
...@@ -20,7 +22,7 @@ def F(f): ...@@ -20,7 +22,7 @@ def F(f):
LENA = F('lena.jpg') LENA = F('lena.jpg')
LENA_BBX = [ LENA_BBX = [
(214, 202, 183, 183) [214, 202, 183, 183]
] #from OpenCV's cascade detector ] #from OpenCV's cascade detector
MULTI = F('multi.jpg') MULTI = F('multi.jpg')
...@@ -45,10 +47,75 @@ def opencv_detect(image): ...@@ -45,10 +47,75 @@ def opencv_detect(image):
1.3, #scaleFactor (at each time the image is re-scaled) 1.3, #scaleFactor (at each time the image is re-scaled)
4, #minNeighbors (around candidate to be retained) 4, #minNeighbors (around candidate to be retained)
0, #flags (normally, should be set to zero) 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(): def test_lena_opencv():
img = xbob.io.load(LENA) img = xbob.io.load(LENA)
...@@ -57,7 +124,10 @@ def test_lena_opencv(): ...@@ -57,7 +124,10 @@ def test_lena_opencv():
flm = Flandmark() flm = Flandmark()
keypoints = flm.locate(gray, y, x, height, width) 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(): def test_lena():
...@@ -67,10 +137,12 @@ def test_lena(): ...@@ -67,10 +137,12 @@ def test_lena():
flm = Flandmark() flm = Flandmark()
keypoints = flm.locate(gray, y, x, height, width) keypoints = flm.locate(gray, y, x, height, width)
assert keypoints nose.tools.eq_(keypoints.shape, (8, 2))
nose.tools.eq_(len(keypoints), 8) 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(): def test_multi_opencv():
img = xbob.io.load(MULTI) img = xbob.io.load(MULTI)
...@@ -80,7 +152,10 @@ def test_multi_opencv(): ...@@ -80,7 +152,10 @@ def test_multi_opencv():
flm = Flandmark() flm = Flandmark()
for (x, y, width, height) in bbx: for (x, y, width, height) in bbx:
keypoints = flm.locate(gray, y, x, height, width) 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(): def test_multi():
...@@ -90,5 +165,7 @@ def test_multi(): ...@@ -90,5 +165,7 @@ def test_multi():
flm = Flandmark() flm = Flandmark()
for (x, y, width, height) in MULTI_BBX: for (x, y, width, height) in MULTI_BBX:
keypoints = flm.locate(gray, y, x, height, width) keypoints = flm.locate(gray, y, x, height, width)
assert keypoints nose.tools.eq_(keypoints.shape, (8, 2))
nose.tools.eq_(len(keypoints), 8) nose.tools.eq_(keypoints.dtype, 'float64')
for k in keypoints:
assert is_inside(k, (y, x, height, width), eps=1)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment