Commit 96e1ff59 authored by André Anjos's avatar André Anjos 💬

Testing with OpenCV implemented; All tests passing

parent 22e27a1d
......@@ -11,6 +11,7 @@ auto-checkout = *
develop = src/xbob.extension
src/xbob.blitz
src/xbob.io
src/xbob.ip.color
.
; options for xbob.buildout extension
......@@ -23,6 +24,7 @@ prefixes = /idiap/group/torch5spro/nightlies/last/bob/linux-x86_64-release
xbob.extension = git https://github.com/bioidiap/xbob.extension branch=prototype
xbob.blitz = git https://github.com/bioidiap/xbob.blitz
xbob.io = git https://github.com/bioidiap/xbob.io
xbob.ip.color = git https://github.com/bioidiap/xbob.ip.color
[scripts]
recipe = xbob.buildout:scripts
......@@ -35,6 +35,7 @@ setup(
'setuptools',
'xbob.blitz',
'xbob.io', #for tests
'xbob.ip.color', #for tests
],
namespace_packages=[
......@@ -67,6 +68,7 @@ setup(
include_dirs = include_dirs,
version = version,
packages = packages,
boost_modules = ['system'],
),
],
......
from . import _flandmark
from ._library import *
from . import version
from .version import module as __version__
from pkg_resources import resource_filename
def __filename__(f):
"""Returns the test file on the "data" subdirectory"""
return resource_filename(__name__, f)
class Localizer(_flandmark.Localizer):
"""A fast and effective facial landmark localization framework based on
flandmark
Consult http://cmp.felk.cvut.cz/~uricamic/flandmark/index.php for more
information.
def get_config():
"""Returns a string containing the configuration information.
"""
def __init__(self, detection_model=resource_filename(__name__,
'haarcascade_frontalface_alt.xml'),
localization_model=resource_filename(__name__,
'flandmark_model.dat')):
"""Builds a new facial localization model.
Raises RuntimeError's if the models either don't exist or can't be loaded.
Keyword parameters:
detection_model
An OpenCV (xml) detection model file for a CvHaarClassifierCascade. If not
specified, use a default installed with the package.
localization_model
A flandmark localization model file. If not specified, use a default
installed with the package. The default model provides the following
keypoints, in this order:
[0]
Face center
[1]
Canthus-rl (inner corner of the right eye). Note: The "right eye" means
the right eye at face w.r.t. itself - that is the left eye in the image.
[2]
Canthus-lr (inner corder of the left eye)
[3]
Mouth-corner-r (right corner of the mouth)
[4]
Mouth-corner-l (left corner of the mouth)
[5]
Canthus-rr (outer corner of the right eye)
[6]
Canthus-ll (outer corner of the left eye)
[7]
Nose
"""
super(Localizer, self).__init__(detection_model, localization_model)
import pkg_resources
from .version import externals
def __call__(self, image):
"""Localizes facial keypoints on all faces detected at the input image.
packages = pkg_resources.require(__name__)
this = packages[0]
deps = packages[1:]
Keyword parameters:
retval = "%s: %s (%s)\n" % (this.key, this.version, this.location)
retval += " - c/c++ dependencies:\n"
for k in sorted(externals): retval += " - %s: %s\n" % (k, externals[k])
retval += " - python dependencies:\n"
for d in deps: retval += " - %s: %s (%s)\n" % (d.key, d.version, d.location)
image
Either a gray-scale or colored image where to run the detection and
localization.
return retval.strip()
Returns a tuple composed of dictionaries. Each dictionary in the list has
two entries: ``bbox`` and ``landmark``. The ``bbox`` entry corresponds to
the OpenCV cascade face detected, whereas the ``landmark`` contains a list
of tuples (representing x,y coordinates) with the landmarks.
# gets sphinx autodoc done right - don't remove it
__all__ = [_ for _ in dir() if not _.startswith('_')]
If no faces are detected on the input image, than the returned tuple is
empty.
"""
if image.ndim == 3:
from bob.ip import rgb_to_gray
gray = rgb_to_gray(image)
return super(Localizer, self).__call__(gray)
elif image.ndim == 2:
return super(Localizer, self).__call__(image)
else:
raise TypeError, "Localizer accepts images as numpy.ndarray objects with either 2 or 3 dimensions"
def localize(self, image, b_y, b_x, b_height, b_width):
"""Localizes facial keypoints on all faces detected at the input image.
Keyword parameters:
image
Either a gray-scale or colored image where to run the detection and
localization.
b_y
The top-left y-coordinate of the bounding box
b_x
The top-left x-coordinate of the bounding box
b_height
The height of the bounding box
b_width
The width of the bounding box
Returns the landmarks which are a list of tuples
(representing x,y coordinates) with the landmarks.
"""
if image.ndim == 3:
from bob.ip import rgb_to_gray
gray = rgb_to_gray(image)
return super(Localizer, self).__call__(gray, b_y, b_x, b_height, b_width)
elif image.ndim == 2:
return super(Localizer, self).__call__(image, b_y, b_x, b_height, b_width)
else:
raise TypeError, "Localizer accepts images as numpy.ndarray objects with either 2 or 3 dimensions"
# Setup default model for C-API
from pkg_resources import resource_filename
import os.path
from ._library import __set_default_model__
__set_default_model__(resource_filename(__name__,
os.path.join('data', 'flandmark_model.dat')))
del resource_filename, __set_default_model__, os
This diff is collapsed.
......@@ -146,7 +146,7 @@ static PyObject* call(PyBobIpFlandmarkObject* self,
Py_END_ALLOW_THREADS
PyObject* landmarks = 0;
if (result == NO_ERR) {
if (result != NO_ERR) {
Py_INCREF(Py_None);
landmarks = Py_None;
}
......@@ -156,7 +156,7 @@ static PyObject* call(PyBobIpFlandmarkObject* self,
auto landmarks_ = make_safe(landmarks);
for (int k = 0; k < (2*self->flandmark->data.options.M); k += 2) {
PyTuple_SET_ITEM(landmarks, k/2,
Py_BuildValue("nn",
Py_BuildValue("dd",
self->landmarks[k+1], //y value
self->landmarks[k] //x value
)
......@@ -241,7 +241,17 @@ static PyObject* PyBobIpFlandmark_call_single(PyBobIpFlandmarkObject* self,
bbx[2] = x + width;
bbx[3] = y + height;
return call(self, cv_image, 1, bbx);
PyObject* retval = call(self, cv_image, 1, bbx);
if (!retval) return 0;
//gets the first entry, return it
PyObject* retval0 = PyTuple_GET_ITEM(retval, 0);
if (!retval0) return 0;
Py_INCREF(retval0);
Py_DECREF(retval);
return retval0;
};
......@@ -263,7 +273,7 @@ PyObject* PyBobIpFlandmark_Repr(PyBobIpFlandmarkObject* self) {
* <xbob.ip.flandmark(model='...')>
*/
PyObject* retval = PyUnicode_FromFormat("<%s(model=%s)>",
PyObject* retval = PyUnicode_FromFormat("<%s(model='%s')>",
Py_TYPE(self)->tp_name, self->filename.c_str());
#if PYTHON_VERSION_HEX < 0x03000000
......
......@@ -10,11 +10,38 @@
#endif
#include <xbob.blitz/capi.h>
#include <xbob.blitz/cleanup.h>
#include <xbob.io/api.h>
#include <xbob.extension/documentation.h>
extern PyTypeObject PyBobIpFlandmark_Type;
static auto s_setter = xbob::extension::FunctionDoc(
"__set_default_model__",
"Internal function to set the default model for the Flandmark class"
)
.add_prototype("path", "")
.add_parameter("path", "str", "The path to the new model file")
;
PyObject* set_flandmark_model(PyObject*, PyObject* o) {
int ok = PyDict_SetItemString(PyBobIpFlandmark_Type.tp_dict,
"__default_model__", o);
if (ok == -1) return 0;
Py_RETURN_NONE;
}
static PyMethodDef module_methods[] = {
{0} /* Sentinel */
{
s_setter.name(),
(PyCFunction)set_flandmark_model,
METH_O,
s_setter.doc()
},
{0} /* Sentinel */
};
PyDoc_STRVAR(module_docstr, "Flandmark keypoint localization library");
......@@ -32,6 +59,10 @@ static PyModuleDef module_definition = {
static PyObject* create_module (void) {
//makes sure that PyBobIpFlandmark_Type has a dictionary on tp_dict
PyBobIpFlandmark_Type.tp_dict = PyDict_New();
if (!PyBobIpFlandmark_Type.tp_dict) return 0;
PyBobIpFlandmark_Type.tp_new = PyType_GenericNew;
if (PyType_Ready(&PyBobIpFlandmark_Type) < 0) return 0;
......@@ -52,6 +83,7 @@ static PyObject* create_module (void) {
/* imports xbob.blitz C-API + dependencies */
if (import_xbob_blitz() < 0) return 0;
if (import_xbob_io() < 0) return 0;
Py_INCREF(m);
return m;
......
/**
* @author Andre Anjos <andre.anjos@idiap.ch>
* @date Thu 20 Sep 2012 14:46:35 CEST
*
* @brief Boost.Python extension to flandmark
*/
#include <boost/shared_ptr.hpp>
#include <boost/shared_array.hpp>
#include "flandmark_detector.h"
using namespace boost::python;
static void delete_cascade(CvHaarClassifierCascade* o) {
cvReleaseHaarClassifierCascade(&o);
}
static void delete_flandmark(FLANDMARK_Model* o) {
flandmark_free(o);
}
static void delete_image(IplImage* i) {
i->imageData = 0; ///< never delete blitz::Array data
cvReleaseImage(&i);
}
static void delete_storage(CvMemStorage* s) {
cvClearMemStorage(s);
cvReleaseMemStorage(&s);
}
/**
* A simple wrapper to operate the flandmark library quickly in iterative
* environments like Python.
*/
class Localizer {
public: //api
/**
* Constructor, has to load a boosted cascaded from OpenCV and the
* flandmark model.
*/
Localizer(const std::string& opencv_cascade,
const std::string& flandmark_model) :
m_cascade((CvHaarClassifierCascade*)cvLoad(opencv_cascade.c_str(), 0, 0, 0), std::ptr_fun(delete_cascade)),
m_flandmark(flandmark_init(flandmark_model.c_str()), std::ptr_fun(delete_flandmark)),
m_storage(cvCreateMemStorage(0), std::ptr_fun(delete_storage))
{
if( !m_cascade ) {
PYTHON_ERROR(RuntimeError, "Couldnt load Face detector '%s'", opencv_cascade.c_str());
}
if ( !m_flandmark ) {
PYTHON_ERROR(RuntimeError, "Structure model wasn't created. Corrupted file '%s'", flandmark_model.c_str());
}
m_landmarks.reset(new double[2*m_flandmark->data.options.M]);
}
/**
* Detect and locates the landmarks from an input image
*/
tuple call1(bob::python::const_ndarray input) {
//checks type
const bob::core::array::typeinfo& type = input.type();
if ((type.dtype != bob::core::array::t_uint8) || (type.nd != 2)) {
PYTHON_ERROR(TypeError, "Input data must be a 2D numpy.array with dtype=uint8 (i.e. a gray-scaled image), but you passed %s", type.str().c_str());
}
//converts to IplImage
boost::shared_ptr<IplImage> ipl_image(cvCreateImageHeader(cvSize(type.shape[1], type.shape[0]), IPL_DEPTH_8U, 1), std::ptr_fun(delete_image));
ipl_image->imageData = (char*)input.bz<uint8_t,2>().data();
// Flags for OpenCV face detection
CvSize minFeatureSize = cvSize(40, 40);
int flags = CV_HAAR_DO_CANNY_PRUNING;
float search_scale_factor = 1.1f;
// Detect all the faces in the greyscale image.
CvSeq* rects = cvHaarDetectObjects(ipl_image.get(), m_cascade.get(),
m_storage.get(), search_scale_factor, 2, flags, minFeatureSize);
int nFaces = rects->total;
list retval;
for (int iface = 0; iface < (rects ? nFaces : 0); ++iface) {
CvRect* r = (CvRect*)cvGetSeqElem(rects, iface);
dict det;
det["bbox"] = make_tuple(r->x, r->y, r->width, r->height);
int bbox[4] = {r->x, r->y, r->x + r->width, r->y + r->height};
int flandmark_result;
{
bob::python::no_gil unlock;
flandmark_result = flandmark_detect(ipl_image.get(), bbox, m_flandmark.get(),
m_landmarks.get());
}
list lmlist; ///< landmark list
// do not copy the results when the landmark detector indicates an error.
// otherwise stale results (from a previous invocation) are returned
if (flandmark_result == NO_ERR) {
// The first point represents the center of the bounding box used by
// the flandmark library.
for (int i = 0; i < (2*m_flandmark->data.options.M); i += 2) {
lmlist.append(make_tuple(m_landmarks[i+1], m_landmarks[i]));
}
}
det["landmark"] = tuple(lmlist);
retval.append(det);
}
return tuple(retval);
}
/**
* Detect and locates the landmarks from an input image
*/
object call2(bob::python::const_ndarray input, const int b_y, const int b_x, const int b_height, const int b_width)
{
//checks type
const bob::core::array::typeinfo& type = input.type();
if ((type.dtype != bob::core::array::t_uint8) || (type.nd != 2)) {
PYTHON_ERROR(TypeError, "Input data must be a 2D numpy.array with dtype=uint8 (i.e. a gray-scaled image), but you passed %s", type.str().c_str());
}
//converts to IplImage
boost::shared_ptr<IplImage> ipl_image(cvCreateImageHeader(cvSize(type.shape[1], type.shape[0]), IPL_DEPTH_8U, 1), std::ptr_fun(delete_image));
ipl_image->imageData = (char*)input.bz<uint8_t,2>().data();
int bbox[4] = {b_x, b_y, b_x + b_width, b_y + b_height};
int flandmark_result;
{
bob::python::no_gil unlock;
flandmark_result = flandmark_detect(ipl_image.get(), bbox, m_flandmark.get(),
m_landmarks.get());
}
list lmlist; ///< landmark list
if (flandmark_result == NO_ERR) {
for (int i = 0; i < (2*m_flandmark->data.options.M); i += 2) {
lmlist.append(make_tuple(m_landmarks[i+1], m_landmarks[i]));
}
}
return object(lmlist);
}
private: //representation
boost::shared_ptr<CvHaarClassifierCascade> m_cascade;
boost::shared_ptr<FLANDMARK_Model> m_flandmark;
boost::shared_ptr<CvMemStorage> m_storage;
boost::shared_array<double> m_landmarks;
};
BOOST_PYTHON_MODULE(_flandmark) {
bob::python::setup_python("bindings to flandmark - a library for the localization of facial landmarks");
class_<Localizer>("Localizer", "A key-point localization for faces using flandmark", init<const std::string&, const std::string&>((arg("detector"), arg("localizer")), "Initializes with both an OpenCV face detector model and an flandmark model"))
.def("__call__", &Localizer::call1, (arg("image")), "Locates (possibly multiple) key-points on the given input image. Returns a list of located faces (by OpenCV's model), each attached to a list of key-points.")
.def("__call__", &Localizer::call2, (arg("image"), arg("b_y"), arg("b_x"), arg("b_height"), arg("b_width")), "Locates (possibly multiple) key-points on the given input image, given a bounding box and returns them as a list.")
;
}
......@@ -7,21 +7,88 @@
"""
import os
import bob
import pkg_resources
import nose.tools
import xbob.io
import xbob.ip.color
from . import Localizer
from . import Flandmark
def F(name, f):
def F(f):
"""Returns the test file on the "data" subdirectory"""
return pkg_resources.resource_filename(name, os.path.join('data', f))
return pkg_resources.resource_filename(__name__, os.path.join('data', f))
INPUT_VIDEO = F('xbob.io', 'test.mov')
LENA = F('lena.jpg')
LENA_BBX = [
(214, 202, 183, 183)
] #from OpenCV's cascade detector
def test_video():
MULTI = F('multi.jpg')
MULTI_BBX = [
[326, 20, 31, 31],
[163, 25, 34, 34],
[253, 42, 28, 28],
] #from OpenCV's cascade detector
op = Localizer()
def opencv_detect(image):
"""Detects a face using OpenCV's cascade detector
for f in bob.io.VideoReader(INPUT_VIDEO):
assert op(f)
Returns a list of arrays containing (x, y, width, height) for each detected
face.
"""
from cv2 import CascadeClassifier, cv
cc = CascadeClassifier(F('haarcascade_frontalface_alt.xml'))
return cc.detectMultiScale(
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)
)
@nose.tools.nottest
def test_lena_opencv():
img = xbob.io.load(LENA)
gray = xbob.ip.color.rgb_to_gray(img)
(x, y, width, height) = opencv_detect(gray)[0]
flm = Flandmark()
keypoints = flm.locate(gray, y, x, height, width)
assert keypoints
def test_lena():
img = xbob.io.load(LENA)
gray = xbob.ip.color.rgb_to_gray(img)
(x, y, width, height) = LENA_BBX[0]
flm = Flandmark()
keypoints = flm.locate(gray, y, x, height, width)
assert keypoints
nose.tools.eq_(len(keypoints), 8)
@nose.tools.nottest
def test_multi_opencv():
img = xbob.io.load(MULTI)
gray = xbob.ip.color.rgb_to_gray(img)
bbx = opencv_detect(gray)
flm = Flandmark()
for (x, y, width, height) in bbx:
keypoints = flm.locate(gray, y, x, height, width)
assert keypoints
def test_multi():
img = xbob.io.load(MULTI)
gray = xbob.ip.color.rgb_to_gray(img)
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)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment