diff --git a/MANIFEST.in b/MANIFEST.in index 3b102a792910980e64bed97ee7cd1ee6ee361b2b..d4fcb40cffc78eb9cf58bbbc67a131386a4cf8d0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ include LICENSE README.rst bootstrap.py buildout.cfg recursive-include doc conf.py *.rst recursive-include xbob *.cpp *.h recursive-include xbob/io/test/data *.* +recursive-include xbob/io/fonts *.txt *.ttf diff --git a/setup.py b/setup.py index 29c175f336cac1b2ef732e75f63cdfe97a53ef37..b071fb27b3e22ea35ad79f486a02dde3477b74e1 100644 --- a/setup.py +++ b/setup.py @@ -113,6 +113,12 @@ setup( ), ], + entry_points={ + 'console_scripts': [ + 'xbob_video_test.py = xbob.io.script.video_test:main', + ], + }, + classifiers = [ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', diff --git a/xbob/io/__init__.py b/xbob/io/__init__.py index 8d04c8504d57a16981af06a5f9d767c43080f5d3..8a062569adcbcb8df36e57fdb9a2f4071d622653 100644 --- a/xbob/io/__init__.py +++ b/xbob/io/__init__.py @@ -1,8 +1,178 @@ -from ._library import __version__, __api_version__, file +from ._library import __version__, __api_version__, File + +import os + +def create_directories_save(directory, dryrun=False): + """Creates a directory if it does not exists, with concurrent access support. + This function will also create any parent directories that might be required. + If the dryrun option is selected, it does not actually create the directory, + but just writes the (Linux) command that would have been executed. + + Parameters: + + directory + The directory that you want to create. + + dryrun + Only write the command, but do not execute it. + """ + try: + if dryrun: + print("[dry-run] mkdir -p '%s'" % directory) + else: + if directory and not os.path.exists(directory): os.makedirs(directory) + + except OSError as exc: # Python >2.5 + import errno + if exc.errno != errno.EEXIST: + raise + + +def load(inputs): + """Loads the contents of a file, an iterable of files, or an iterable of + :py:class:`bob.io.File`'s into a :py:class:`numpy.ndarray`. + + Parameters: + + inputs + + This might represent several different entities: + + 1. The name of a file (full path) from where to load the data. In this + case, this assumes that the file contains an array and returns a loaded + numpy ndarray. + 2. An iterable of filenames to be loaded in memory. In this case, this + would assume that each file contains a single 1D sample or a set of 1D + samples, load them in memory and concatenate them into a single and + returned 2D numpy ndarray. + 3. An iterable of :py:class:`bob.io.File`. In this case, this would assume + that each :py:class:`bob.io.File` contains a single 1D sample or a set + of 1D samples, load them in memory if required and concatenate them into + a single and returned 2D numpy ndarray. + 4. An iterable with mixed filenames and :py:class:`bob.io.File`. In this + case, this would returned a 2D :py:class:`numpy.ndarray`, as described + by points 2 and 3 above. + """ + + from collections import Iterable + import numpy + from .utils import is_string + if is_string(inputs): + return File(inputs, 'r').read() + elif isinstance(inputs, Iterable): + retval = [] + for obj in inputs: + if is_string(obj): + retval.append(load(obj)) + elif isinstance(obj, File): + retval.append(obj.read()) + else: + raise TypeError("Iterable contains an object which is not a filename nor a bob.io.File.") + return numpy.vstack(retval) + else: + raise TypeError("Unexpected input object. This function is expecting a filename, or an iterable of filenames and/or bob.io.File's") + +def merge(filenames): + """Converts an iterable of filenames into an iterable over read-only + bob.io.File's. + + Parameters: + + filenames + + This might represent: + + 1. A single filename. In this case, an iterable with a single + :py:class:`bob.io.File` is returned. + 2. An iterable of filenames to be converted into an iterable of + :py:class:`bob.io.File`'s. + """ + + from collections import Iterable + from .utils import is_string + if is_string(filenames): + return [File(filenames, 'r')] + elif isinstance(filenames, Iterable): + return [File(k, 'r') for k in filenames] + else: + raise TypeError("Unexpected input object. This function is expecting an iterable of filenames.") + +def save(array, filename, create_directories = False): + """Saves the contents of an array-like object to file. + + Effectively, this is the same as creating a :py:class:`bob.io.File` object + with the mode flag set to `w` (write with truncation) and calling + :py:meth:`bob.io.File.write` passing `array` as parameter. + + Parameters: + + array + The array-like object to be saved on the file + + filename + The name of the file where you need the contents saved to + + create_directories + Automatically generate the directories if required + """ + # create directory if not existent yet + if create_directories: + create_directories_save(os.path.dirname(filename)) + + return File(filename, 'w').write(array) + +# Just to make it homogenous with the C++ API +write = save + +def append(array, filename): + """Appends the contents of an array-like object to file. + + Effectively, this is the same as creating a :py:class:`bob.io.File` object + with the mode flag set to `a` (append) and calling + :py:meth:`bob.io.File.append` passing `array` as parameter. + + Parameters: + + array + The array-like object to be saved on the file + + filename + The name of the file where you need the contents saved to + """ + return File(filename, 'a').append(array) + +def peek(filename): + """Returns the type of array (frame or sample) saved in the given file. + + Effectively, this is the same as creating a :py:class:`bob.io.File` object + with the mode flag set to `r` (read-only) and returning + :py:attr:`bob.io.File.type`. + + Parameters: + + filename + The name of the file to peek information from + """ + return File(filename, 'r').type + +def peek_all(filename): + """Returns the type of array (for full readouts) saved in the given file. + + Effectively, this is the same as creating a :py:class:`bob.io.File` object + with the mode flag set to `r` (read-only) and returning + :py:attr:`bob.io.File.type_all`. + + Parameters: + + filename + The name of the file to peek information from + """ + return File(filename, 'r').type_all + +# Keeps compatibility with the previously existing API +open = File def get_include(): """Returns the directory containing the C/C++ API include directives""" return __import__('pkg_resources').resource_filename(__name__, 'include') - -__all__ = [] diff --git a/xbob/io/bobskin.cpp b/xbob/io/bobskin.cpp index bcd91acc2b091ccdbe01218b573a0914a6bcbe65..27bca8161bb90023fdcfbbd03c06e708b62257f9 100644 --- a/xbob/io/bobskin.cpp +++ b/xbob/io/bobskin.cpp @@ -5,46 +5,145 @@ * @brief Implementation of our bobskin class */ -#include <numpy/arrayobject.h> +#include <bobskin.h> #include <stdexcept> -bobskin::bobskin(PyObject* array, bob::core::array::ElementType& eltype) { +bobskin::bobskin(PyObject* array, bob::core::array::ElementType eltype) { if (!PyArray_CheckExact(array)) { - PyErr_SetString(PyExc_TypeError, "input object to bobskin constructor is not a numpy.ndarray"); - throw std::runtime_error(); + PyErr_SetString(PyExc_TypeError, "input object to bobskin constructor is not (exactly) a numpy.ndarray"); + throw std::runtime_error("error is already set"); } - m_type.set(eltype, PyArray_NDIM(array), PyArray_DIMS(array), - PyArray_STRIDES(array)); + m_type.set<npy_intp>(eltype, PyArray_NDIM((PyArrayObject*)array), + PyArray_DIMS((PyArrayObject*)array), + PyArray_STRIDES((PyArrayObject*)array)); - m_ptr = PyArray_DATA(array); + m_ptr = PyArray_DATA((PyArrayObject*)array); } +static bob::core::array::ElementType signed_integer_type(int bits) { + switch(bits) { + case 8: + return bob::core::array::t_int8; + case 16: + return bob::core::array::t_int16; + case 32: + return bob::core::array::t_int32; + case 64: + return bob::core::array::t_int64; + default: + PyErr_Format(PyExc_TypeError, "unsupported signed integer element type with %d bits", bits); + } + return bob::core::array::t_unknown; +} + +static bob::core::array::ElementType unsigned_integer_type(int bits) { + switch(bits) { + case 8: + return bob::core::array::t_uint8; + case 16: + return bob::core::array::t_uint16; + case 32: + return bob::core::array::t_uint32; + case 64: + return bob::core::array::t_uint64; + default: + PyErr_Format(PyExc_TypeError, "unsupported unsigned signed integer element type with %d bits", bits); + } + return bob::core::array::t_unknown; +} + +static bob::core::array::ElementType num_to_type (int num) { + switch(num) { + case NPY_BOOL: + return bob::core::array::t_bool; + + //signed integers + case NPY_BYTE: + return signed_integer_type(NPY_BITSOF_CHAR); + case NPY_SHORT: + return signed_integer_type(NPY_BITSOF_SHORT); + case NPY_INT: + return signed_integer_type(NPY_BITSOF_INT); + case NPY_LONG: + return signed_integer_type(NPY_BITSOF_LONG); + case NPY_LONGLONG: + return signed_integer_type(NPY_BITSOF_LONGLONG); + + //unsigned integers + case NPY_UBYTE: + return unsigned_integer_type(NPY_BITSOF_CHAR); + case NPY_USHORT: + return unsigned_integer_type(NPY_BITSOF_SHORT); + case NPY_UINT: + return unsigned_integer_type(NPY_BITSOF_INT); + case NPY_ULONG: + return unsigned_integer_type(NPY_BITSOF_LONG); + case NPY_ULONGLONG: + return unsigned_integer_type(NPY_BITSOF_LONGLONG); + + //floats + case NPY_FLOAT32: + return bob::core::array::t_float32; + case NPY_FLOAT64: + return bob::core::array::t_float64; +#ifdef NPY_FLOAT128 + case NPY_FLOAT128: + return bob::core::array::t_float128; +#endif + + //complex + case NPY_COMPLEX64: + return bob::core::array::t_complex64; + case NPY_COMPLEX128: + return bob::core::array::t_complex128; +#ifdef NPY_COMPLEX256 + case NPY_COMPLEX256: + return bob::core::array::t_complex256; +#endif + + default: + PyErr_Format(PyExc_TypeError, "unsupported NumPy element type (%d)", num); + } + + return bob::core::array::t_unknown; +} + +bobskin::bobskin(PyBlitzArrayObject* array) { + bob::core::array::ElementType eltype = num_to_type(array->type_num); + if (eltype == bob::core::array::t_unknown) { + throw std::runtime_error("error is already set"); + } + m_type.set<Py_ssize_t>(num_to_type(array->type_num), array->ndim, + array->shape, array->stride); + m_ptr = array->data; +} + bobskin::~bobskin() { } void bobskin::set(const interface&) { PyErr_SetString(PyExc_NotImplementedError, "setting C++ bobskin with (const interface&) is not implemented - DEBUG ME!"); - throw std::runtime_error(); + throw std::runtime_error("error is already set"); } -void bobskin::set(boost::shared_ptr<interface> other); +void bobskin::set(boost::shared_ptr<interface>) { PyErr_SetString(PyExc_NotImplementedError, "setting C++ bobskin with (boost::shared_ptr<interface>) is not implemented - DEBUG ME!"); - throw std::runtime_error(); + throw std::runtime_error("error is already set"); } -void bobskin::set (const bob::core::array::typeinfo& req) { +void bobskin::set (const bob::core::array::typeinfo&) { PyErr_SetString(PyExc_NotImplementedError, "setting C++ bobskin with (const typeinfo&) implemented - DEBUG ME!"); - throw std::runtime_error(); + throw std::runtime_error("error is already set"); } boost::shared_ptr<void> bobskin::owner() { PyErr_SetString(PyExc_NotImplementedError, "acquiring non-const owner from C++ bobskin is not implemented - DEBUG ME!"); - throw std::runtime_error(); + throw std::runtime_error("error is already set"); } boost::shared_ptr<const void> bobskin::owner() const { PyErr_SetString(PyExc_NotImplementedError, "acquiring const owner from C++ bobskin is not implemented - DEBUG ME!"); - throw std::runtime_error(); + throw std::runtime_error("error is already set"); } diff --git a/xbob/io/bobskin.h b/xbob/io/bobskin.h index 4b38b4e18c824df1d4d6f7158d0223f657049777..f1afcf16ddac2ebcc972130424838c27542f5c59 100644 --- a/xbob/io/bobskin.h +++ b/xbob/io/bobskin.h @@ -6,9 +6,14 @@ * functionality. */ -#include <Python.h> #include <bob/core/array.h> +extern "C" { +#include <Python.h> +#include <blitz.array/capi.h> +} + + /** * Wraps a PyArrayObject such that we can access it from bob::io */ @@ -21,6 +26,11 @@ class bobskin: public bob::core::array::interface { */ bobskin(PyObject* array, bob::core::array::ElementType eltype); + /** + * @brief Builds a new array an array like object + */ + bobskin(PyBlitzArrayObject* array); + /** * @brief By default, the interface is never freed. You must override * this method to do something special for your class type. diff --git a/xbob/io/file.cpp b/xbob/io/file.cpp index db89459714e9da96298201b21ff6cd134cb08fce..4187cc3632a3a99fda30881ab52e8e605933ec0a 100644 --- a/xbob/io/file.cpp +++ b/xbob/io/file.cpp @@ -10,6 +10,7 @@ #include <bob/io/CodecRegistry.h> #include <bob/io/utils.h> #include <numpy/arrayobject.h> +#include <blitz.array/capi.h> #include <stdexcept> #include <bobskin.h> @@ -46,10 +47,10 @@ static int PyBobIoFile_Init(PyBobIoFileObject* self, PyObject *args, PyObject* k char* mode = 0; int mode_len = 0; char* pretend_extension = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss|s", kwlist, &filename, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss#|s", kwlist, &filename, &mode, &mode_len, &pretend_extension)) return -1; - if (mode_len != 1 || !(mode[0] != 'r' && mode[0] != 'w' && mode[0] != 'a')) { + if (mode_len != 1 || !(mode[0] == 'r' || mode[0] == 'w' || mode[0] == 'a')) { PyErr_Format(PyExc_ValueError, "file open mode string should have 1 element and be either 'r' (read), 'w' (write) or 'a' (append)"); return -1; } @@ -215,33 +216,38 @@ int PyBobIo_AsTypenum (bob::core::array::ElementType type) { static PyObject* PyBobIoFile_GetItem (PyBobIoFileObject* self, Py_ssize_t i) { if (i < 0 || i >= self->f->size()) { - PyErr_SetString(PyExc_IndexError, "file index out of range"); + PyErr_Format(PyExc_IndexError, "file index out of range - `%s' only contains %" PY_FORMAT_SIZE_T "d object(s)", self->f->filename().c_str(), self->f->size()); return 0; } const bob::core::array::typeinfo& info = self->f->type(); npy_intp shape[NPY_MAXDIMS]; - for (i=0; i<info.nd; ++i) shape[i] = info.shape[i]; + for (int k=0; k<info.nd; ++k) shape[k] = info.shape[k]; int type_num = PyBobIo_AsTypenum(info.dtype); if (type_num == NPY_NOTYPE) return 0; ///< failure PyObject* retval = PyArray_SimpleNew(info.nd, shape, type_num); - bobskin skin(retval, info.dtype); + if (!retval) return 0; try { + bobskin skin(retval, info.dtype); self->f->read(skin, i); } catch (std::runtime_error& e) { + if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::runtime_error while reading object #%" PY_FORMAT_SIZE_T "d from file `%s': %s", i, self->f->filename().c_str(), e.what()); + Py_DECREF(retval); return 0; } catch (std::exception& e) { - PyErr_Format(PyExc_RuntimeError, "caught std::exception while reading file `%s': %s", self->f->filename().c_str(), e.what()); + if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::exception while reading object #%" PY_FORMAT_SIZE_T "d from file `%s': %s", i, self->f->filename().c_str(), e.what()); + Py_DECREF(retval); return 0; } catch (...) { - PyErr_Format(PyExc_RuntimeError, "caught unknown while reading file `%s'", self->f->filename().c_str()); + if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught unknown exception while reading object #%" PY_FORMAT_SIZE_T "d from file `%s'", i, self->f->filename().c_str()); + Py_DECREF(retval); return 0; } @@ -257,6 +263,220 @@ static PySequenceMethods PyBobIoFile_Sequence = { 0 /* slice */ }; +static PyObject* PyBobIoFile_Read(PyBobIoFileObject* self, PyObject *args, PyObject* kwds) { + + /* Parses input arguments in a single shot */ + static const char* const_kwlist[] = {"index", 0}; + static char** kwlist = const_cast<char**>(const_kwlist); + + Py_ssize_t i = PY_SSIZE_T_MIN; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|n", kwlist, &i)) return 0; + + if (i != PY_SSIZE_T_MIN) { + + // reads a specific object inside the file + + if (i < 0) i += self->f->size(); + + if (i < 0 || i >= self->f->size()) { + PyErr_Format(PyExc_IndexError, "file index out of range - `%s' only contains %" PY_FORMAT_SIZE_T "d object(s)", self->f->filename().c_str(), self->f->size()); + return 0; + } + + return PyBobIoFile_GetItem(self, i); + + } + + // reads the whole file in a single shot + + const bob::core::array::typeinfo& info = self->f->type_all(); + + npy_intp shape[NPY_MAXDIMS]; + for (int k=0; k<info.nd; ++k) shape[k] = info.shape[k]; + + int type_num = PyBobIo_AsTypenum(info.dtype); + if (type_num == NPY_NOTYPE) return 0; ///< failure + + PyObject* retval = PyArray_SimpleNew(info.nd, shape, type_num); + if (!retval) return 0; + + try { + bobskin skin(retval, info.dtype); + self->f->read_all(skin); + } + catch (std::runtime_error& e) { + if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::runtime_error while reading all contents of file `%s': %s", self->f->filename().c_str(), e.what()); + Py_DECREF(retval); + return 0; + } + catch (std::exception& e) { + if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::exception while reading all contents of file `%s': %s", self->f->filename().c_str(), e.what()); + Py_DECREF(retval); + return 0; + } + catch (...) { + if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught unknown while reading all contents of file `%s'", self->f->filename().c_str()); + Py_DECREF(retval); + return 0; + } + + return retval; + +} + +PyDoc_STRVAR(s_read_str, "read"); +PyDoc_STRVAR(s_read_doc, +"read([index]) -> numpy.ndarray\n\ +\n\ +Reads a specific object in the file, or the whole file.\n\ +\n\ +Parameters:\n\ +\n\ +index\n\ + [int|long, optional] The index to the object one wishes\n\ + to retrieve from the file. Negative indexing is supported.\n\ + If not given, impliess retrieval of the whole file contents.\n\ +\n\ +This method reads data from the file. If you specified an\n\ +index, it reads just the object indicated by the index, as\n\ +you would do using the ``[]`` operator. If an index is\n\ +not specified, reads the whole contents of the file into a\n\ +:py:class:`numpy.ndarray`.\n\ +" +); + +static PyObject* PyBobIoFile_Write(PyBobIoFileObject* self, PyObject *args, PyObject* kwds) { + + /* Parses input arguments in a single shot */ + static const char* const_kwlist[] = {"array", 0}; + static char** kwlist = const_cast<char**>(const_kwlist); + + PyBlitzArrayObject* bz = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&", kwlist, &PyBlitzArray_Converter, &bz)) return 0; + + try { + bobskin skin(bz); + self->f->write(skin); + } + catch (std::runtime_error& e) { + if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::runtime_error while writing to file `%s': %s", self->f->filename().c_str(), e.what()); + return 0; + } + catch (std::exception& e) { + if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::exception while writing to file `%s': %s", self->f->filename().c_str(), e.what()); + return 0; + } + catch (...) { + if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught unknown while writing to file `%s'", self->f->filename().c_str()); + return 0; + } + + Py_RETURN_NONE; + +} + +PyDoc_STRVAR(s_write_str, "write"); +PyDoc_STRVAR(s_write_doc, +"write(array) -> None\n\ +\n\ +Writes the contents of an object to the file.\n\ +\n\ +Parameters:\n\ +\n\ +array\n\ + [array] The array to be written into the file. It can be a\n\ + numpy, a blitz array or any other object which can be\n\ + converted to either of them, as long as the number of\n\ + dimensions and scalar type are supported by\n\ + :py:class:`blitz.array`.\n\ +\n\ +This method writes data to the file. It acts like the\n\ +given array is the only piece of data that will ever be written\n\ +to such a file. No more data appending may happen after a call to\n\ +this method.\n\ +" +); + +static PyObject* PyBobIoFile_Append(PyBobIoFileObject* self, PyObject *args, PyObject* kwds) { + + /* Parses input arguments in a single shot */ + static const char* const_kwlist[] = {"array", 0}; + static char** kwlist = const_cast<char**>(const_kwlist); + + PyBlitzArrayObject* bz = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&", kwlist, &PyBlitzArray_Converter, &bz)) return 0; + Py_ssize_t pos = -1; + + try { + bobskin skin(bz); + pos = self->f->append(skin); + } + catch (std::runtime_error& e) { + if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::runtime_error while appending to file `%s': %s", self->f->filename().c_str(), e.what()); + return 0; + } + catch (std::exception& e) { + if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::exception while appending to file `%s': %s", self->f->filename().c_str(), e.what()); + return 0; + } + catch (...) { + if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught unknown while appending to file `%s'", self->f->filename().c_str()); + return 0; + } + +# if PY_VERSION_HEX >= 0x03000000 + return PyLong_FromSsize_t(pos); +# else + return PyInt_FromSsize_t(pos); +# endif + +} + +PyDoc_STRVAR(s_append_str, "append"); +PyDoc_STRVAR(s_append_doc, +"append(array) -> int\n\ +\n\ +Adds the contents of an object to the file.\n\ +\n\ +Parameters:\n\ +\n\ +array\n\ + [array] The array to be added into the file. It can be a\n\ + numpy, a blitz array or any other object which can be\n\ + converted to either of them, as long as the number of\n\ + dimensions and scalar type are supported by\n\ + :py:class:`blitz.array`.\n\ +\n\ +This method appends data to the file. If the file does not\n\ +exist, creates a new file, else, makes sure that the inserted\n\ +array respects the previously set file structure.\n\ +\n\ +Returns the current position of the newly written array.\n\ +" +); + +static PyMethodDef PyBobIoFile_Methods[] = { + { + s_read_str, + (PyCFunction)PyBobIoFile_Read, + METH_VARARGS|METH_KEYWORDS, + s_read_doc, + }, + { + s_write_str, + (PyCFunction)PyBobIoFile_Write, + METH_VARARGS|METH_KEYWORDS, + s_write_doc, + }, + { + s_append_str, + (PyCFunction)PyBobIoFile_Append, + METH_VARARGS|METH_KEYWORDS, + s_append_doc, + }, + {0} /* Sentinel */ +}; + PyTypeObject PyBobIoFile_Type = { PyObject_HEAD_INIT(0) 0, /*ob_size*/ @@ -286,7 +506,7 @@ PyTypeObject PyBobIoFile_Type = { 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ - 0, //PyBobIoFile_methods, /* tp_methods */ + PyBobIoFile_Methods, /* tp_methods */ 0, /* tp_members */ PyBobIoFile_getseters, /* tp_getset */ 0, /* tp_base */ @@ -300,28 +520,6 @@ PyTypeObject PyBobIoFile_Type = { }; /** -static object file_read_all(bob::io::File& f) { - bob::python::py_array a(f.type_all()); - f.read_all(a); - return a.pyobject(); //shallow copy -} - -static object file_read(bob::io::File& f, size_t index) { - bob::python::py_array a(f.type_all()); - f.read(a, index); - return a.pyobject(); //shallow copy -} - -static void file_write(bob::io::File& f, object array) { - bob::python::py_array a(array, object()); - f.write(a); -} - -static void file_append(bob::io::File& f, object array) { - bob::python::py_array a(array, object()); - f.append(a); -} - static dict extensions() { typedef std::map<std::string, std::string> map_type; dict retval; @@ -338,18 +536,6 @@ void bind_io_file() { .add_property("type", make_function(&bob::io::File::type, return_value_policy<copy_const_reference>()), "Typing information to load the file as an Arrayset") - .def("read", &file_read_all, (arg("self")), "Reads the whole contents of the file into a NumPy ndarray") - .def("write", &file_write, (arg("self"), arg("array")), "Writes an array into the file, truncating it first") - - .def("__len__", &bob::io::File::size, (arg("self")), "Size of the file if it is supposed to be read as a set of arrays instead of performing a single read") - - .def("read", &file_read, (arg("self"), arg("index")), "Reads a single array from the file considering it to be an arrayset list") - - .def("__getitem__", &file_read, (arg("self"), arg("index")), "Reads a single array from the file considering it to be an arrayset list") - - .def("append", &file_append, (arg("self"), arg("array")), "Appends an array to a file. Compatibility requirements may be enforced.") - ; - def("extensions", &extensions, "Returns a dictionary containing all extensions and descriptions currently stored on the global codec registry"); } diff --git a/xbob/io/fonts/bold.ttf b/xbob/io/fonts/bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8f5a1d3f1edd49cf7a630887f2f61dfe6ebd9fc2 Binary files /dev/null and b/xbob/io/fonts/bold.ttf differ diff --git a/xbob/io/fonts/bold_italic.ttf b/xbob/io/fonts/bold_italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..916e9b351d985581d728574bb8f4766e64f7ffde Binary files /dev/null and b/xbob/io/fonts/bold_italic.ttf differ diff --git a/xbob/io/fonts/font_license.txt b/xbob/io/fonts/font_license.txt new file mode 100644 index 0000000000000000000000000000000000000000..69399804ca6c71fd61ed0ae47535baad3bb55e79 --- /dev/null +++ b/xbob/io/fonts/font_license.txt @@ -0,0 +1,97 @@ +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. +Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +associated documentation files (the "Font Software"), to reproduce +and distribute the modifications to the Bitstream Vera Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to +the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the +"Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr. \ No newline at end of file diff --git a/xbob/io/fonts/italic.ttf b/xbob/io/fonts/italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7b8b787525dd555b0c0144d0a8db84f9f317e268 Binary files /dev/null and b/xbob/io/fonts/italic.ttf differ diff --git a/xbob/io/fonts/regular.ttf b/xbob/io/fonts/regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ea0bfd8c276fd20bc0f121bf28c9facbda430905 Binary files /dev/null and b/xbob/io/fonts/regular.ttf differ diff --git a/xbob/io/include/xbob.io/api.h b/xbob/io/include/xbob.io/api.h index b20aca3fbdb5764013254ca7d20f3699c9e171a8..64f54cfe1cbb4ef11bce8823917c37a2da7c15bc 100644 --- a/xbob/io/include/xbob.io/api.h +++ b/xbob/io/include/xbob.io/api.h @@ -12,7 +12,10 @@ #include <bob/io/File.h> #include <boost/preprocessor/stringize.hpp> #include <boost/shared_ptr.hpp> + +extern "C" { #include <Python.h> +} #define XBOB_IO_MODULE_PREFIX xbob.io #define XBOB_IO_MODULE_NAME _library @@ -90,7 +93,7 @@ typedef struct { # if defined(PY_ARRAY_UNIQUE_SYMBOL) # define XBOB_IO_MAKE_API_NAME_INNER(a) XBOB_IO_ ## a # define XBOB_IO_MAKE_API_NAME(a) XBOB_IO_MAKE_API_NAME_INNER(a) -# define PyBlitzArray_API XBOB_IO_MAKE_API_NAME(PY_ARRAY_UNIQUE_SYMBOL) +# define PyXbobIo_API XBOB_IO_MAKE_API_NAME(PY_ARRAY_UNIQUE_SYMBOL) # endif # if defined(NO_IMPORT_ARRAY) @@ -121,7 +124,7 @@ typedef struct { * I/O generic bindings * ************************/ -# define PyBobIo_AsTypenum (*(PyBobIo_AsTypenum_RET (*)PyBobIo_AsTypenum_PROTO) PyBlitzArray_API[PyBobIo_AsTypenum_NUM]) +# define PyBobIo_AsTypenum (*(PyBobIo_AsTypenum_RET (*)PyBobIo_AsTypenum_PROTO) PyXbobIo_API[PyBobIo_AsTypenum_NUM]) /** * Returns -1 on error, 0 on success. PyCapsule_Import will set an exception diff --git a/xbob/io/main.cpp b/xbob/io/main.cpp index e420b670092eab3fa00b391d8f27b76ead297ef5..b5cccefff89679b9c1a9cb7c218e8c25bb68453c 100644 --- a/xbob/io/main.cpp +++ b/xbob/io/main.cpp @@ -7,13 +7,53 @@ #define XBOB_IO_MODULE #include <xbob.io/api.h> +#include <bob/io/CodecRegistry.h> #ifdef NO_IMPORT_ARRAY #undef NO_IMPORT_ARRAY #endif #include <blitz.array/capi.h> +static PyObject* PyBobIo_Extensions(PyObject*) { + + typedef std::map<std::string, std::string> map_type; + const map_type& table = bob::io::CodecRegistry::getExtensions(); + + PyObject* retval = PyDict_New(); + if (!retval) return 0; + + for (auto it=table.begin(); it!=table.end(); ++it) { +# if PY_VERSION_HEX >= 0x03000000 + PyObject* value = PyString_FromString(it->second.c_str()); +# else + PyObject* value = PyUnicode_FromString(it->second.c_str()); +# endif + if (!value) { + Py_DECREF(retval); + return 0; + } + PyDict_SetItemString(retval, it->first.c_str(), value); + Py_DECREF(value); + } + return retval; + +} + +PyDoc_STRVAR(s_extensions_str, "extensions"); +PyDoc_STRVAR(s_extensions_doc, +"as_blitz(x) -> dict\n\ +\n\ +Returns a dictionary containing all extensions and descriptions\n\ +currently stored on the global codec registry\n\ +"); + static PyMethodDef module_methods[] = { + { + s_extensions_str, + (PyCFunction)PyBobIo_Extensions, + METH_NOARGS, + s_extensions_doc, + }, {0} /* Sentinel */ }; @@ -38,7 +78,7 @@ PyMODINIT_FUNC ENTRY_FUNCTION(XBOB_IO_MODULE_NAME) (void) { /* register the types to python */ Py_INCREF(&PyBobIoFile_Type); - PyModule_AddObject(m, "file", (PyObject *)&PyBobIoFile_Type); + PyModule_AddObject(m, "File", (PyObject *)&PyBobIoFile_Type); static void* PyXbobIo_API[PyXbobIo_API_pointers]; @@ -60,7 +100,7 @@ PyMODINIT_FUNC ENTRY_FUNCTION(XBOB_IO_MODULE_NAME) (void) { * I/O generic bindings * ************************/ - PyBlitzArray_API[PyBobIo_AsTypenum_NUM] = (void *)PyBobIo_AsTypenum; + PyXbobIo_API[PyBobIo_AsTypenum_NUM] = (void *)PyBobIo_AsTypenum; /* imports the NumPy C-API */ import_array(); diff --git a/xbob/io/script/__init__.py b/xbob/io/script/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/xbob/io/script/video_test.py b/xbob/io/script/video_test.py new file mode 100644 index 0000000000000000000000000000000000000000..f5a9e56aba9582090e816310a321f9a8d70dfd41 --- /dev/null +++ b/xbob/io/script/video_test.py @@ -0,0 +1,497 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# Andre Anjos <andre.dos.anjos@gmail.com> +# Thu 14 Mar 17:53:16 2013 + +"""This program can run manual tests using any video codec available in Bob. It +can report standard distortion figures and build video test sequences for +manual inspection. It tries to help identifying problems with: + + 1. Color distortion + 2. Frame skipping or delay + 3. Encoding or decoding quality + 4. User test (with a user provided video sample) + +You can parameterize the program with the type of file, (FFmpeg) codec and a +few other parameters. The program then generates artificial input signals to +test for each of the parameters above. +""" + +import os +import sys +import argparse +import numpy + +# internal +from .. import supported_video_codecs, available_video_codecs, supported_videowriter_formats, available_videowriter_formats +from .. import utils, create_directories_save +from ... import version +from .. import save as save_to_file +from ...test import utils as test_utils +from .. import test as io_test + +CODECS = supported_video_codecs() +ALL_CODECS = available_video_codecs() + +def list_codecs(*args, **kwargs): + retval = """\ + Supported Codecs: + -----------------\n""" + + for k in sorted(CODECS.keys()): + retval += (" %-20s %s\n" % (k, CODECS[k]['long_name']))[:80] + + return retval[:-1] + +def list_all_codecs(*args, **kwargs): + retval = """\ + Available Codecs: + -----------------\n""" + + for k in sorted(ALL_CODECS.keys()): + retval += (" %-20s %s\n" % (k, ALL_CODECS[k]['long_name']))[:80] + + return retval[:-1] + +FORMATS = supported_videowriter_formats() +ALL_FORMATS = available_videowriter_formats() + +def list_formats(*args, **kwargs): + + retval = """\ + Supported Formats: + ------------------\n""" + + for k in sorted(FORMATS.keys()): + retval += (" %-20s %s\n" % (k, FORMATS[k]['long_name']))[:80] + + return retval[:-1] + +def list_all_formats(*args, **kwargs): + + retval = """\ + Available Formats: + ------------------\n""" + + for k in sorted(ALL_FORMATS.keys()): + retval += (" %-20s %s\n" % (k, ALL_FORMATS[k]['long_name']))[:80] + + return retval[:-1] + +__epilog__ = """Example usage: + +1. Check for color distortion using H.264 codec in a .mov video container: + + $ %(prog)s --format='mov' --codec='h264' color + +2. Check for frame skipping using MJPEG codec in an .avi video container: + + $ %(prog)s --format='avi' --codec='mjpeg' frameskip + +3. Check for encoding/decoding quality using a FFV1 codec in a '.flv' video +container (not supported - note the usage of the '--force' flag): + + $ %(prog)s --force --format='flv' --codec='ffv1' noise + +4. To run-only the user-video test and provide a test video: + + $ %(prog)s --format='mov' --user-video=test_sample.avi user + +5. To list all available codecs: + + $ %(prog)s --list-codecs + +6. To list all available formats: + + $ %(prog)s --list-formats + +7. Run all tests for all **supported** codecs and formats: + + $ %(prog)s +""" % { + 'prog': os.path.basename(sys.argv[0]), + } + +def user_video(original, max_frames, format, codec, filename): + """Returns distortion patterns for a set of frames with moving colors. + + Keyword parameters: + + original + The name (path) to the original user file that will be used for the test + + max_frames + The maximum number of frames to read from user input + + format + The string that identifies the format to be used for the output file + + codec + The codec to be used for the output file + + filename + The name (path) of the file to use for encoding the test + """ + + from .. import VideoReader, VideoWriter + vreader = VideoReader(original, check=True) + orig = vreader[:max_frames] + + # rounding frame rate - some older codecs do not accept random frame rates + framerate = vreader.frame_rate + if codec in ('mpegvideo', 'mpeg1video', 'mpeg2video'): + import math + framerate = math.ceil(vreader.frame_rate) + + vwriter = VideoWriter(filename, vreader.height, vreader.width, + framerate, codec=codec, format=format, check=False) + for k in orig: vwriter.append(k) + del vwriter + return orig, framerate, VideoReader(filename, check=False) + +def summarize(function, shape, framerate, format, codec, output=None): + """Summarizes distortion patterns for a given set of video settings and + for a given input function. + + Keyword parameters: + + shape (int, int, int) + The length (number of frames), height and width for the generated sequence + + format + The string that identifies the format to be used for the output file + + codec + The codec to be used for the output file + + output + If set, the video is not created on the temporary directory, but it is + saved on the advised location. This must be a filename. + + Returns a single a single string summarizing the distortion results + """ + + length, height, width = shape + + if output: + fname = output + else: + fname = test_utils.temporary_filename(suffix='.%s' % format) + + retval = "did not run" + + try: + # Width and height should be powers of 2 as the encoded image is going + # to be approximated to the closest one, would not not be the case. + # In this case, the encoding is subject to more noise as the filtered, + # final image that is encoded will contain added noise on the extra + # borders. + orig, framerate, encoded = function(shape, framerate, format, codec, fname) + + tmp = [] + for k, of in enumerate(orig): + tmp.append(abs(of.astype('float64')-encoded[k].astype('float64')).sum()) + size = numpy.prod(orig[0].shape) + S = sum(tmp)/size + M = S/len(tmp) + Min = min(tmp)/size + Max = max(tmp)/size + ArgMin = tmp.index(min(tmp)) + ArgMax = tmp.index(max(tmp)) + retval = "%.3f min=%.3f@%d max=%.3f@%d" % (M, Min, ArgMin, Max, ArgMax) + if abs(encoded.frame_rate - framerate) > 0.01: + retval += " !FR(%g)" % abs(encoded.frame_rate - framerate) + if len(encoded) != len(orig): + retval += " !LEN(%d)" % len(encoded) + + finally: + + if os.path.exists(fname) and output is None: os.unlink(fname) + + if output: + return retval, orig, encoded + else: + return retval + +def detail(function, shape, framerate, format, codec, outdir): + """Summarizes distortion patterns for a given set of video settings and + for a given input function. + + Keyword parameters: + + shape (int, int, int) + The length (number of frames), height and width for the generated sequence + + format + The string that identifies the format to be used for the output file + + codec + The codec to be used for the output file + + outdir + We will save all analysis for this sequence on the given output directory. + + Returns a single a single string summarizing the distortion results. + """ + + length, height, width = shape + + text_format = "%%0%dd" % len(str(length-1)) + + output = os.path.join(outdir, "video." + format) + retval, orig, encoded = summarize(function, shape, framerate, + format, codec, output) + + length, _, height, width = orig.shape + + # save original, reloaded and difference images on output directories + for i, orig_frame in enumerate(orig): + out = numpy.ndarray((3, height, 3*width), dtype='uint8') + out[:,:,:width] = orig_frame + out[:,:,width:(2*width)] = encoded[i] + diff = abs(encoded[i].astype('int64')-orig_frame.astype('int64')) + diff[diff>0] = 255 #binary output + out[:,:,(2*width):] = diff.astype('uint8') + save_to_file(out, os.path.join(outdir, 'frame-' + (text_format%i) + '.png')) + + return retval + +def main(user_input=None): + + parser = argparse.ArgumentParser(description=__doc__, epilog=__epilog__, + formatter_class=argparse.RawDescriptionHelpFormatter) + + name = os.path.basename(os.path.splitext(sys.argv[0])[0]) + version_info = 'Video Encoding/Decoding Test Tool v%s (%s)' % (version, name) + parser.add_argument('-V', '--version', action='version', version=version_info) + + test_choices = [ + 'color', + 'frameskip', + 'noise', + 'user', + ] + + parser.add_argument("test", metavar='TEST', type=str, nargs='*', + default=test_choices, help="The name of the test or tests you want to run. Choose between `%s'. If none given, run through all." % ('|'.join(test_choices))) + + supported_codecs = sorted(CODECS.keys()) + available_codecs = sorted(ALL_CODECS.keys()) + + parser.add_argument("-c", "--codec", metavar='CODEC', type=str, nargs='*', + default=supported_codecs, choices=available_codecs, help="The name of the codec you want to test with. For a list of available codecs, look below. If none given, run through all.") + parser.add_argument("--list-codecs", action="store_true", default=False, + help="List all supported codecs and exits") + parser.add_argument("--list-all-codecs", action="store_true", default=False, + help="List all available codecs and exits") + + supported_formats = sorted(FORMATS.keys()) + available_formats = sorted(ALL_FORMATS.keys()) + parser.add_argument("-f", "--format", metavar='FORMAT', type=str, nargs='*', + default=supported_formats, choices=available_formats, help="The name of the format you want to test with. For a list of available formats, look below. If none given, run through all.") + parser.add_argument("--list-formats", action="store_true", default=False, + help="List all supported formats and exits") + parser.add_argument("--list-all-formats", action="store_true", default=False, + help="List all available formats and exits") + + parser.add_argument("-F", "--force", action="store_true", default=False, + help="Force command execution (possibly generating an error) even if the format or the combination of format+codec is not supported. This flag is needed in case you need to test new formats or combinations of formats and codecs which are unsupported by the build") + + parser.add_argument("-t", "--height", metavar="INT", type=int, + default=128, help="Height of the test video (defaults to %(default)s pixels). Note this number has to be even.") + + parser.add_argument("-w", "--width", metavar="INT", type=int, + default=128, help="Width of the test video (defaults to %(default)s pixels). Note this number has to be even.") + + parser.add_argument("-l", "--length", metavar="INT", type=int, + default=30, help="Length of the test sequence (defaults to %(default)s frames). The longer, the more accurate the test becomes.") + + parser.add_argument("-r", "--framerate", metavar="FLOAT", type=float, + default=30., help="Framerate to be used on the test videos (defaults to %(default)s Hz).") + + parser.add_argument("-o", "--output", type=str, + help="If set, then videos created for the tests are stored on the given directory. By default this option is empty and videos are created on a temporary directory and deleted after tests are done. If you set it, we also produced detailed output analysis for manual inspection.") + + parser.add_argument("-u", "--user-video", type=str, metavar="PATH", + help="Set the path to the user video that will be used for distortion tests (if not set use default test video)") + + parser.add_argument("-n", "--user-frames", type=int, default=10, metavar="INT", help="Set the number of maximum frames to read from the user video (reads %(default)s by default)") + + args = parser.parse_args(args=user_input) + + # manual check because of argparse limitation + for t in args.test: + if t not in test_choices: + parser.error("invalid test choice: '%s' (choose from %s)" % \ + (t, ", ".join(["'%s'" % k for k in test_choices]))) + + if not args.test: args.test = test_choices + + if args.list_codecs: + print(list_codecs()) + sys.exit(0) + + if args.list_all_codecs: + print(list_all_codecs()) + sys.exit(0) + + if args.list_formats: + print(list_formats()) + sys.exit(0) + + if args.list_all_formats: + print(list_all_formats()) + sys.exit(0) + + if 'user' in args.test and args.user_video is None: + # in this case, take our standard video test + args.user_video = test_utils.datafile('test.mov', io_test.__name__) + + def wrap_user_function(shape, framerate, format, codec, filename): + return user_video(args.user_video, args.user_frames, format, codec, filename) + + # mapping between test name and function + test_function = { + 'color': (utils.color_distortion, 'C'), + 'frameskip': (utils.frameskip_detection, 'S'), + 'noise': (utils.quality_degradation, 'N'), + 'user': (wrap_user_function, 'U'), + } + + # result table + table = {} + + # report results in a readable way + print(version_info) + print("Settings:") + print(" Width : %d pixels" % args.width) + print(" Height : %d pixels" % args.height) + print(" Length : %d frames" % args.length) + print(" Framerate: %f Hz" % args.framerate) + + print("Legend:") + for k, (f, code) in test_function.items(): + print(" %s: %s test" % (code, k.capitalize())) + + sys.stdout.write("Running %d test(s)..." % + (len(args.test)*len(args.format)*len(args.codec))) + sys.stdout.flush() + + # run tests + need_notes = False + for test in args.test: + test_table = table.setdefault(test, {}) + f, code = test_function[test] + + for format in args.format: + format_table = test_table.setdefault(format, {}) + + for codec in args.codec: + + # cautionary settings + notes = "" + if format not in FORMATS: + if args.force: + notes += "[!F] " + need_notes = True + + else: + sys.stdout.write(code) + sys.stdout.flush() + format_table[codec] = "unsupported format" + continue + + else: + if codec not in FORMATS[format]['supported_codecs']: + if args.force: + notes += "[!F+C] " + need_notes = True + + else: + sys.stdout.write(code) + sys.stdout.flush() + format_table[codec] = "format+codec unsupported" + continue + + if args.output: + + size = '%dx%dx%d@%gHz' % (args.length, args.height, args.width, + args.framerate) + outdir = os.path.join(args.output, test, codec, size, format) + create_directories_save(outdir) + + try: + result = detail(f, (args.length, args.height, args.width), + args.framerate, format, codec, outdir) + sys.stdout.write(code) + sys.stdout.flush() + except Exception as e: + result = str(e) + sys.stdout.write(code) + sys.stdout.flush() + finally: + format_table[codec] = notes + result + + else: + + try: + result = summarize(f, (args.length, args.height, args.width), + args.framerate, format, codec) + sys.stdout.write(code) + sys.stdout.flush() + except Exception as e: + result = str(e) + sys.stdout.write(code) + sys.stdout.flush() + finally: + format_table[codec] = notes + result + + sys.stdout.write("\n") + sys.stdout.flush() + + # builds a nicely organized dynamically sized table + test_size = max([len(k) for k in args.test] + [len('test')]) + fmt_size = max([len(k) for k in args.format] + [len('fmt')]) + codec_size = max([len(k) for k in args.codec] + [len('codec')]) + figure_size = 79 - (test_size + 3 + fmt_size + 3 + codec_size + 3 + 2) + if figure_size <= 0: figure_size = 40 + + test_cover = (test_size + 2) * '=' + fmt_cover = (fmt_size + 2) * '=' + codec_cover = (codec_size + 2) * '=' + figure_cover = (figure_size + 2) * '=' + + sep = test_cover + ' ' + fmt_cover + ' ' + codec_cover + ' ' + figure_cover + line = " %s %s %s %s" + + print("") + print(sep) + print(line % ( + 'test'.ljust(test_size), + 'fmt'.center(fmt_size), + 'codec'.center(codec_size), + 'figure (lower means better quality)'.ljust(figure_size), + )) + print(sep) + + for test in sorted(table.keys()): + test_table = table[test] + for format in sorted(test_table.keys()): + format_table = test_table[format] + for codec in sorted(format_table.keys()): + figure = format_table[codec] + print(line % ( + test.ljust(test_size), + format.center(fmt_size), + codec.ljust(codec_size), + figure.ljust(figure_size), + )) + + print(sep) + + # only printed if unsupported combinations of formats and codecs are used + if need_notes: + print("") + print("Notes:") + print(" [!F] Format is available, but not supported by this build") + print(" [!F+C] Format is supported, but not in combination with this codec") diff --git a/xbob/io/test/test_examples.py b/xbob/io/test/test_examples.py deleted file mode 100644 index 3b1afdb4d88297ef542d57cf387612a4ae4a28c8..0000000000000000000000000000000000000000 --- a/xbob/io/test/test_examples.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python -# vim: set fileencoding=utf-8 : -# Andre Anjos <andre.anjos@idiap.ch> -# Tue 21 Aug 2012 13:20:38 CEST - -"""Tests various examples for bob.io -""" - -from ...test import utils - -@utils.ffmpeg_found() -def test_video2frame(): - - movie = utils.datafile('test.mov', __name__) - - from ..example.video2frame import main - cmdline = ['--self-test', movie] - assert main(cmdline) == 0 diff --git a/xbob/io/test/test_file.py b/xbob/io/test/test_file.py index a5934f255176c62c3b4c45c611bffa72a4e4f198..643bc7c02f35c2d5cd1bbcb03c8fb0bd8cc7afb5 100644 --- a/xbob/io/test/test_file.py +++ b/xbob/io/test/test_file.py @@ -4,18 +4,6 @@ # Wed Nov 16 13:27:15 2011 +0100 # # Copyright (C) 2011-2013 Idiap Research Institute, Martigny, Switzerland -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. """A combined test for all built-in types of Array/interaction in python. @@ -27,7 +15,7 @@ import numpy import nose.tools from .. import load, write, File -from ...test import utils as testutils +from . import utils as testutils def transcode(filename): """Runs a complete transcoding test, to and from the binary format.""" diff --git a/xbob/io/test/test_image.py b/xbob/io/test/test_image.py index 4810b37e1817086fecddafd93dc1aefcc5d934b5..5d3dc242c62e2d98054447212b0ea91964921739 100644 --- a/xbob/io/test/test_image.py +++ b/xbob/io/test/test_image.py @@ -20,11 +20,7 @@ """Runs some image tests """ -import os -import sys -import numpy - -from ...test import utils as testutils +from . import utils as testutils # These are some global parameters for the test. PNG_INDEXED_COLOR = testutils.datafile('img_indexed_color.png', __name__) diff --git a/xbob/io/test/test_video.py b/xbob/io/test/test_video.py index 5b640c16312696d67bc0ccfcd9590526de27b90c..65991b00f6ef5cdbcaa9d07563dc3873485b150a 100644 --- a/xbob/io/test/test_video.py +++ b/xbob/io/test/test_video.py @@ -23,8 +23,8 @@ import os import sys import numpy -from ...test import utils as testutils -from .. import supported_videowriter_formats +from . import utils as testutils +from .._library import supported_videowriter_formats from ..utils import color_distortion, frameskip_detection, quality_degradation # These are some global parameters for the test. diff --git a/xbob/io/test/utils.py b/xbob/io/test/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..34f103ced60ae27a56c8bba549f70d9159f861bf --- /dev/null +++ b/xbob/io/test/utils.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# Andre Anjos <andre.anjos@idiap.ch> +# Thu Feb 7 09:58:22 2013 + +"""Re-usable decorators and utilities for xbob test code +""" + +import os +import functools +import nose.plugins.skip +from distutils.version import StrictVersion as SV + +def datafile(f, module=None, path='data'): + """Returns the test file on the "data" subdirectory of the current module. + + Keyword attributes + + f: str + This is the filename of the file you want to retrieve. Something like + ``'movie.avi'``. + + package: string, optional + This is the python-style package name of the module you want to retrieve + the data from. This should be something like ``xbob.io.test``, but you + normally refer it using the ``__name__`` property of the module you want to + find the path relative to. + + path: str, optional + This is the subdirectory where the datafile will be taken from inside the + module. Normally (the default) ``data``. It can be set to ``None`` if it + should be taken from the module path root (where the ``__init__.py`` file + sits). + + Returns the full path of the file. + """ + + resource = __name__ if module is None else module + final_path = f if path is None else os.path.join(path, f) + return __import__('pkg_resources').resource_filename(resource, final_path) + +def temporary_filename(prefix='bobtest_', suffix='.hdf5'): + """Generates a temporary filename to be used in tests""" + + (fd, name) = __import__('tempfile').mkstemp(suffix, prefix) + os.close(fd) + os.unlink(name) + return name + +# Here is a table of ffmpeg versions against libavcodec, libavformat and +# libavutil versions +ffmpeg_versions = { + '0.5': [ SV('52.20.0'), SV('52.31.0'), SV('49.15.0') ], + '0.6': [ SV('52.72.2'), SV('52.64.2'), SV('50.15.1') ], + '0.7': [ SV('52.122.0'), SV('52.110.0'), SV('50.43.0') ], + '0.8': [ SV('53.7.0'), SV('53.4.0'), SV('51.9.1') ], + '0.9': [ SV('53.42.0'), SV('53.24.0'), SV('51.32.0') ], + '0.10': [ SV('53.60.100'), SV('53.31.100'), SV('51.34.101') ], + '0.11': [ SV('54.23.100'), SV('54.6.100'), SV('51.54.100') ], + '1.0': [ SV('54.59.100'), SV('54.29.104'), SV('51.73.101') ], + '1.1': [ SV('54.86.100'), SV('54.59.106'), SV('52.13.100') ], + } + +def ffmpeg_version_lessthan(v): + '''Returns true if the version of ffmpeg compiled-in is at least the version + indicated as a string parameter.''' + + from ..io._io import version + avcodec_inst= SV(version['FFmpeg']['avcodec']) + avcodec_req = ffmpeg_versions[v][0] + return avcodec_inst < avcodec_req + +def ffmpeg_found(version_geq=None): + '''Decorator to check if a codec is available before enabling a test + + To use this, decorate your test routine with something like: + + .. code-block:: python + + @ffmpeg_found() + + You can pass an optional string to require that the FFMpeg version installed + is greater or equal that version identifier. For example: + + .. code-block:: python + + @ffmpeg_found('0.10') #requires at least version 0.10 + + Versions you can test for are set in the ``ffmpeg_versions`` dictionary in + this module. + ''' + + def test_wrapper(test): + + @functools.wraps(test) + def wrapper(*args, **kwargs): + try: + from ..io._io import version + avcodec_inst= SV(version['FFmpeg']['avcodec']) + avformat_inst= SV(version['FFmpeg']['avformat']) + avutil_inst= SV(version['FFmpeg']['avutil']) + if version_geq is not None: + avcodec_req,avformat_req,avutil_req = ffmpeg_versions[version_geq] + if avcodec_inst < avcodec_req: + raise nose.plugins.skip.SkipTest('FFMpeg/libav version installed (%s) is smaller than required for this test (%s)' % (version['FFmpeg']['ffmpeg'], version_geq)) + return test(*args, **kwargs) + except KeyError: + raise nose.plugins.skip.SkipTest('FFMpeg was not available at compile time') + + return wrapper + + return test_wrapper + +def codec_available(codec): + '''Decorator to check if a codec is available before enabling a test''' + + def test_wrapper(test): + + @functools.wraps(test) + def wrapper(*args, **kwargs): + from ..io import supported_video_codecs + d = supported_video_codecs() + if codec in d and d[codec]['encode'] and d[codec]['decode']: + return test(*args, **kwargs) + else: + raise nose.plugins.skip.SkipTest('A functional codec for "%s" is not installed with FFmpeg' % codec) + + return wrapper + + return test_wrapper + +def extension_available(extension): + '''Decorator to check if a extension is available before enabling a test''' + + def test_wrapper(test): + + @functools.wraps(test) + def wrapper(*args, **kwargs): + from .._library import extensions + if extension in extensions(): + return test(*args, **kwargs) + else: + raise nose.plugins.skip.SkipTest('Extension to handle "%s" files was not available at compile time' % extension) + + return wrapper + + return test_wrapper diff --git a/xbob/io/utils.py b/xbob/io/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a60aec7b9e513be8553ca027a2fa842dc0332cdf --- /dev/null +++ b/xbob/io/utils.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# Andre Anjos <andre.dos.anjos@gmail.com> +# Thu 14 Mar 17:00:58 2013 + +"""Some utilities to generate fake patterns +""" + +import numpy + +DEFAULT_FONT = __import__('pkg_resources').resource_filename(__name__, + __import__('os').path.join("fonts", "regular.ttf")) + +def estimate_fontsize(height, width, format): + """Estimates the best fontsize to fit into a image that is (height, width)""" + + try: + # if PIL is installed this works: + import Image, ImageFont, ImageDraw + except ImportError: + # if Pillow is installed, this works better: + from PIL import Image, ImageFont, ImageDraw + + best_size = min(height, width) + fit = False + while best_size > 0: + font = ImageFont.truetype(DEFAULT_FONT, best_size) + (text_width, text_height) = font.getsize(format % 0) + if text_width < width and text_height < height: break + best_size -= 1 + + if best_size <= 0: + raise RuntimeError("Cannot find best size for font") + + return best_size + +def print_numbers(frame, counter, format, fontsize): + """Generates an image that serves as a test pattern for encoding/decoding and + accuracy tests.""" + + try: + # if PIL is installed this works: + import Image, ImageFont, ImageDraw + except ImportError: + # if Pillow is installed, this works better: + from PIL import Image, ImageFont, ImageDraw + + _, height, width = frame.shape + + # text at the center, indicating the frame number + text = format % counter + dim = min(width, height) + font = ImageFont.truetype(DEFAULT_FONT, fontsize) + (text_width, text_height) = font.getsize(text) + x_pos = int((width - text_width) / 2) + y_pos = int((height - text_height) / 2) + # this is buggy in Pillow-2.0.0, so we do it manually + #img = Image.fromarray(frame.transpose(1,2,0)) + img = Image.fromstring('RGB', (frame.shape[1], frame.shape[2]), frame.transpose(1,2,0).tostring()) + draw = ImageDraw.Draw(img) + draw.text((x_pos, y_pos), text, font=font, fill=(255,255,255)) + return numpy.asarray(img).transpose(2,0,1) + +def generate_colors(height, width, shift): + """Generates an image that serves as a test pattern for encoding/decoding and + accuracy tests.""" + + retval = numpy.ndarray((3, height, width), dtype='uint8') + + # standard color test pattern + w = width / 7; w2 = 2*w; w3 = 3*w; w4 = 4*w; w5 = 5*w; w6 = 6*w + retval[0,:,0:w] = 255; retval[1,:,0:w] = 255; retval[2,:,0:w] = 255; + retval[0,:,w:w2] = 255; retval[1,:,w:w2] = 255; retval[2,:,w:w2] = 0; + retval[0,:,w2:w3] = 0; retval[1,:,w2:w3] = 255; retval[2,:,w2:w3] = 255; + retval[0,:,w3:w4] = 0; retval[1,:,w3:w4] = 255; retval[2,:,w3:w4] = 0; + retval[0,:,w4:w5] = 255; retval[1,:,w4:w5] = 0; retval[2,:,w4:w5] = 255; + retval[0,:,w5:w6] = 255; retval[1,:,w5:w6] = 0; retval[2,:,w5:w6] = 0; + retval[0,:,w6:] = 0; retval[1,:,w6:] = 0; retval[2,:,w6:] = 255; + + # rotate by 'shift' + retval = numpy.roll(retval, shift, axis=2) + return retval + +def color_distortion(shape, framerate, format, codec, filename): + """Returns distortion patterns for a set of frames with moving colors. + + Keyword parameters: + + shape (int, int, int) + The length (number of frames), height and width for the generated sequence + + format + The string that identifies the format to be used for the output file + + codec + The codec to be used for the output file + + filename + The name of the file to use for encoding the test + """ + + length, height, width = shape + from . import VideoReader, VideoWriter + outv = VideoWriter(filename, height, width, framerate, codec=codec, + format=format, check=False) + orig = [] + text_format = "%%0%dd" % len(str(length-1)) + fontsize = estimate_fontsize(height, width, text_format) + fontsize = int(fontsize/4) + for i in range(0, length): + newframe = generate_colors(height, width, i%width) + newframe = print_numbers(newframe, i, text_format, fontsize) + outv.append(newframe) + orig.append(newframe) + outv.close() + orig = numpy.array(orig, dtype='uint8') + return orig, framerate, VideoReader(filename, check=False) + +def frameskip_detection(shape, framerate, format, codec, filename): + """Returns distortion patterns for a set of frames with big numbers. + + Keyword parameters: + + shape (int, int, int) + The length (number of frames), height and width for the generated sequence + + format + The string that identifies the format to be used for the output file + + codec + The codec to be used for the output file + + filename + The name of the file to use for encoding the test + """ + + length, height, width = shape + from . import VideoReader, VideoWriter + text_format = "%%0%dd" % len(str(length-1)) + fontsize = estimate_fontsize(height, width, text_format) + outv = VideoWriter(filename, height, width, framerate, codec=codec, + format=format, check=False) + orig = [] + for i in range(0, length): + newframe = numpy.zeros((3, height, width), dtype='uint8') + newframe = print_numbers(newframe, i, text_format, fontsize) + outv.append(newframe) + orig.append(newframe) + outv.close() + orig = numpy.array(orig, dtype='uint8') + return orig, framerate, VideoReader(filename, check=False) + +def quality_degradation(shape, framerate, format, codec, filename): + """Returns noise patterns for a set of frames. + + Keyword parameters: + + shape (int, int, int) + The length (number of frames), height and width for the generated sequence + + format + The string that identifies the format to be used for the output file + + codec + The codec to be used for the output file + + filename + The name of the file to use for encoding the test + """ + + length, height, width = shape + from . import VideoReader, VideoWriter + text_format = "%%0%dd" % len(str(length-1)) + fontsize = estimate_fontsize(height, width, text_format) + fontsize = int(fontsize/4) + outv = VideoWriter(filename, height, width, framerate, codec=codec, + format=format, check=False) + orig = [] + for i in range(0, length): + newframe = numpy.random.randint(0, 256, (3, height, width)).astype('uint8') + newframe = print_numbers(newframe, i, text_format, fontsize) + outv.append(newframe) + orig.append(newframe) + outv.close() + orig = numpy.array(orig, dtype='uint8') + return orig, framerate, VideoReader(filename, check=False) + +def is_string(s): + """Returns ``True`` if the given object is a string + + This method can be used with Python-2.x or 3.x and returns a string + respecting each environment's constraints. + """ + + from sys import version_info + + return (version_info[0] < 3 and isinstance(s, (str, unicode))) or \ + isinstance(s, (bytes, str))