Getting familiar with genericity
One of Milena’s main features is its genericity. In order to understand how to
take benefit of it, let’s see what genericity really means for us and how it is
illustrated in the library.
A
Generic algorithm
is written once, without duplicates, and works on
different kinds of input.
Let’s have a look to a small example. In any image processing library, we may be
interested in a small routine to fill an image with data. A common
implementation would look like this one:
void fill(image *ima, unsigned char v)
{
for (int i = 0; i < ima->nrows; ++i)
for (int j = 0; j < ima->ncols; ++j)
ima->data[i][j] = v;
}
See milena/doc/examples/tuto3/fill_non_generic.cc.
In this example, there are a lot of
implicit
assumptions about the input:
-
The input image has to be 2D;
- Its definition domain has to be a rectangle starting at (0,0);
- Data cannot be of a different type than unsigned char;
- Image data need to be stored as a 2D array in RAM.
So, what would happen if we would like to use it for 3D images, use rgb8 as
value or even work on a region of interest?
This implementation would require to be re-implemented and the user would have to
deal with the various versions of the fill routine. For the developer, it is error
prone, redundant and hard to maintain. For the user, it is confusing and forces
to always think about what he is manipulating.
According to our definition, this algorithm is clearly
not
generic.
This is not acceptable and that’s why Milena is developed considering
genericity and user/developer friendliness.
With Milena, the previous example would be written as follow:
template <typename I>
void fill(I& ima, mln_value(I) v)
{
mln_piter(I) p(ima.domain());
for_all(p)
ima(p) = v;
}
In this version, the routine can take any kind of image types as arguments. So
it is for the values: the expected type depends on the value used in the
given image.
The for_all loop is also significantly generic to support any kind of
images since the iterator guarantees it will pass through every sites.
This code is more generic and remains really close to the common description of
the generic algorithm.
As a result, with this algorithm we can fill an image,...
fill(ima, literal::green);
... Or fill only a region of interest (a set of points).
fill((ima | b).rw(), literal::green);
First generic algorithm
In this section, we will introduce several routines/tools which are useful while
writing generic algorithms. It is more important to focus on these
routines/tools than what this program actually does.
Here is the full example:
#include <mln/core/image/image2d.hh>
#include <mln/core/image/dmorph/image_if.hh>
#include <mln/core/alias/neighb2d.hh>
#include <mln/data/fill.hh>
#include <mln/labeling/blobs.hh>
#include <mln/labeling/compute.hh>
#include <mln/labeling/blobs.hh>
#include <mln/data/compare.hh>
#include <mln/util/array.hh>
#include <mln/value/label_8.hh>
#include <mln/accu/math/count.hh>
#include <mln/pw/all.hh>
#include <tests/data.hh>
#include <doc/tools/sample_utils.hh>
namespace mln
{
template <typename I, typename N>
mln_concrete(I)
my_algorithm(const Image<I>& ima_,
const Neighborhood<N>& nbh_)
{
trace::entering("my_algorithm");
const I& ima = exact(ima_);
const N& nbh = exact(nbh_);
mln_precondition(ima.is_valid());
mln_precondition(nbh.is_valid());
V nlabels;
mln_ch_value(I,V) lbl = labeling::blobs(ima, nbh, nlabels);
util::array<unsigned>
count = labeling::compute(accu::meta::math::count(),
lbl,
nlabels);
mln_concrete(I) output;
data::fill(output, literal::one);
for (unsigned i = 1; i <= nlabels; ++i)
if (count[i] < 10u)
data::fill((output | (pw::value(lbl) == pw::cst(i))).rw(),
literal::zero);
trace::exiting("my_algorithm");
return output;
}
}
Let’s see the different parts of the algorithm.
template <typename I, typename N>
mln_concrete(I)
my_algorithm(const Image<I>& ima_,
const Neighborhood<N>& nbh_)
The prototype is restrictive enough, readable and still generic.
We use concepts to statically check that the generic type passed as
parameter is what the routine expects. The “exact” image type is
I. For
instance an image of type
image2d inherits from
Image<image2d>. So
an
image2d is an
Image<I>.
Note that the return type of this function is defined by a macro.
mln_concrete is a macro hiding tricky mechanisms (traits) used in Milena.
The important point to remember is that a generic function should not return
I directly but
mln_concrete(I) instead.
trace::entering("my_algorithm");
Like any Milena’s routine, note that we use
trace. This debugging tool
will be detailed in section
Debug hints .
const I& ima = exact(ima_);
const N& nbh = exact(nbh_);
mln_precondition(ima.is_valid());
mln_precondition(nbh.is_valid());
Since the function take some arguments as concept objects, these object cannot
be used as such. Indeed, concepts are empty shells only used for dispatching and
concept checking, that’s the reason why they are parameterized with their exact
type. The exact type let us know what is the real type of the object. To get an
object with the exact type, simply call
exact().
Of course, it is always a good idea to add few preconditions to help during
debug.
typedef value::label_8 V;
V nlabels;
mln_ch_value(I,V) lbl = labeling::blobs(ima, nbh, nlabels);
util::array<unsigned>
count = labeling::compute(accu::meta::math::count(),
lbl,
nlabels);
In this portion of code, the image is labeled and the number of sites per label
is computed. This code does not depend on the image type at all. Again, a macro
mln_ch_value (“mln change value”) helps us.
labeling::blobs is
a routine returning an image of the same kind as the input image but with a
different value.
mln_ch_value enables the possibility of doing that,
whatever the image type
I and whatever its value type, it returns the
same image type with a different value type.
mln_concrete(I) output;
initialize(output, ima);
data::fill(output, literal::one);
The output image is declared here. Like any variable, it must be initialized at
some point. To do so,
initialize() is provided. It is a generic routine
which can initialize the geometry of any image kind with another image of the
same kind.
After this call,
output has a valid domain and is valid. It can be used in
an algorithm, here
data::fill, to have its values modified.
Note that the value passed to
data::fill is also generic. The library
includes few generic common values from which any value type can convert to.
literal::one is one of them. It is a generic one value which can convert
to every value type in the library.
for (unsigned i = 1; i <= nlabels; ++i)
if (count[i] < 10u)
data::fill((output | (pw::value(lbl) == pw::cst(i))).rw(),
literal::zero);
In this part, every region from the labeled image, of which cardinality is lower
than 10 sites, is set to
literal::zero in
output.
Once again, a generic value is used in order to avoid constraints on the image
value type.
trace::exiting("my_algorithm");
return output;
Don’t forget to close the trace before exiting the function. Then return the
result.
Compilation
Include path
If Milena has been installed in a custom directory, e.g. not /usr/include or
/usr/local/include, the path to the library headers must be passed to the
compiler.
With g++ and MinGW, the option is
-I<path>
.
$ g++ -Ipath/to/mln my_program.cc
For other compilers, please look at the documentation and search for “include
path”.
Library linking
As it is usually expected when using a library, no library linking is needed for
the library itself.
Milena is a “header only” library and is compiled “on demand” with your
program.
If you use specific input/output you may need to link your program with the
right graphic library. For more information, please refer to section
Input / Output in the Quick Reference Guide.
Disable Debug
By default, Olena enables a lot of internal pre and post conditions. Usually,
this is a useful feature and it should be enabled. It can heavily slow down a
program though and these tests can be disabled by compiling using -DNDEBUG:
$ g++ -DNDEBUG -Ipath/to/mln my_program.cc
Compiler optimization flags
In this section you will find remarks about the compiler optimization flags and
their impact on the compilation and execution time.
GCC
-
-O0
, combined with -DNDEBUG, it leads to the fastest compilation
time. The execution is somewhat slow though since dispatch functions and one
line members are not inlined by the compiler.
-
-O1
, best compromise between compilation time and execution time.
-
-O2
,
-O3
, combined with -DNDEBUG, it leads to the best execution
time. However these optimizations dramatically slow down the compilation and
requires much more memory at compile time.
Other compilers
Currently, we have not tested different optimization flags with other
compilers. If you did, please report us your results.
Debug hints
Using assertions and GDB
As said above, Milena already includes a lot of post and pre conditions.
Thus, if you made a mistake in your code there is a high probability that it
will be detected at run time. If an assertion fails, we advice you to compile
with the following options:
$ g++ -ggdb -Ipath/to/mln my_program.cc
Note that you
MUST NOT
compile with −DNDEBUG since assertions will be
disabled.
Once compiled, restart the program with GDB.
$ gdb ./my_program
In the GDB console, run it again.
(gdb) run <any parameter you may want to pass to the program>
When an assertion fails, in the GDB console simply type:
(gdb) bt
The full backtrace will be printed out and you will be able to find from where
the error come from. The filenames, the line numbers and the parameters values
are printed out in the backtrace as you can see in the following example:
#0 0xffffe410 in __kernel_vsyscall ()
#1 0xb7d00640 in raise () from /lib/i686/cmov/libc.so.6
#2 0xb7d02018 in abort () from /lib/i686/cmov/libc.so.6
#3 0xb7cf95be in __assert_fail () from /lib/i686/cmov/libc.so.6
#4 0x0804e094 in mln::image2d<bool>::has (this=0xbff32f34, p=@0xbff32f3c)
at /lrde/stockholm/lazzara/svn/olena/git/oln/milena/mln/core/image/image2d.hh:442
#5 0x0804e6d7 in mln::image2d<bool>::operator() (this=0xbff32f34, p=@0xbff32f3c)
at /lrde/stockholm/lazzara/svn/olena/git/oln/milena/mln/core/image/image2d.hh:460
#6 0x080490b0 in main () at test.cc:18
Traces
Sometimes, compiling for GDB without optimization flags and with debug
assertions enabled could lead to execution time dramatically high.
If the function parameter values are not necessary for debugging,
a good alternative is the trace system provided in Milena.
Each time a routine is called, a trace log is written.
This trace allows to follow the stack trace at runtime. It also provides the
time passed in each function call if the call last at least 10ms.
In order to enable traces in a program, set the related global variable to
true:
...
trace::quiet = true;
...
Since it’s a global variable, at anytime in the source code, the trace can be
enabled/disabled.
Traces are enabled:
labeleling::blobs is run and the debug is then disabled.
labeling::blobs(ima,
c4(), nlabels);
trace::quiet = true;
geom::bbox(ima);
The previous code will produce the following trace:
labeling::blobs {
core::initialize {}
data::fill {
data::fill_with_value {
data::impl::fill_with_value_one_block {
data::memset_ {
data::impl::memset_ {}
} data::memset_
} data::impl::fill_with_value_one_block
} data::fill_with_value
} data::fill
} labeling::blobs - 0.08s
As you can see, labeling::blobs is located just after having set
trace::quiet to true so its trace is part of the output.
geom::bbox’s trace is not part of the output though since traces have
been disabled just before it is called.
Debug routines
Milena also provides a lot of debug tools. Here is a small list of the tools:
-
mln::debug::println, print an image in the console.
image2d<int_u8> ima(5,5);
data::fill(ima, 2);
debug::println(ima);
2 2 2 2 2
2 2 2 2 2
2 2 2 2 2
2 2 2 2 2
2 2 2 2 2
- mln::debug::println_with_border, print an image in the console withs
its border.
image2d<int_u8> ima(5,5);
data::fill(ima, 2);
border::fill(ima, 7);
debug::println_with_border(ima);
7 7 7 7 7 7 7 7 7 7 7
7 7 7 7 7 7 7 7 7 7 7
7 7 7 7 7 7 7 7 7 7 7
7 7 7 2 2 2 2 2 7 7 7
7 7 7 2 2 2 2 2 7 7 7
7 7 7 2 2 2 2 2 7 7 7
7 7 7 2 2 2 2 2 7 7 7
7 7 7 2 2 2 2 2 7 7 7
7 7 7 7 7 7 7 7 7 7 7
7 7 7 7 7 7 7 7 7 7 7
7 7 7 7 7 7 7 7 7 7 7
- mln::labeling::colorize, colorize a label image with random colors.
int_u8 vals[25] = { 100, 100, 200, 200, 230,
100, 100, 200, 230, 230,
140, 140, 140, 0, 0,
65, 186, 65, 127, 127,
65, 65, 65, 127, 127 };
image2d<int_u8> ima = make::image2d(vals);
image2d<rgb8> ima_color = labeling::colorize(
rgb8(), ima, 230);
- mln::labeling::superpose, Superpose two images.
- mln::labeling::filename, easily format debug file names.