This tutorial provides a quick introduction to the use of Blepo. To begin, let us load and display an image in a window:
#include "blepo.h" using namespace blepo; { ImgGray img; // an 8-bit-per-pixel grayscale image Figure fig; // a figure Load("image.bmp", &img); // load file into image fig.Draw(img); // draw image in figure }This example illustrates the simplicity of the library. There is a single header file, and the image data are allocated and deallocated automatically. All functions and classes in the library are scoped by the blepo namespace, so you have the choice of either scoping each call with the prefix blepo:: or calling using namespace blepo, as is done here. In a console-based application, be sure to call EventLoop() before returning from main() or else the program will exit before the figure has the chance to display.
In this example, the first line of code simply instantiates a grayscale image. This line incurs essentially no overhead because no memory is allocated for the image data itself (since the size of the image is not given). The second line instantiates a figure, which causes a small window to display on the screen. The Load function loads an image from a file, automatically resizing the img object to hold it, and converting from color to grayscale if necessary. The last line copies the image to the figure, which causes the image to be drawn in the window (and redrawn whenever the window needs to be repainted). When the closing brace is encountered, the image and figure objects fall out of scope, and the image destructor causes all of the memory for the image data to be automatically deallocated. By default, the figure destructor does nothing, so that the window persists on the screen until the user closes the window or the application exits. This behavior makes it easy to rapidly inspect images and data for debugging purposes, but it can be overridden by a parameter to the Figure contructor if desired.
{ ImgGray img; // an empty image img.Reset(320, 240); // allocates memory for a 320 x 240 image img.Reset(160, 120); // reallocates memory for a 160 x 120 image ImgGray img2(320, 240); // a 320 x 240 image img2.Reset(160, 120); // reallocates memory for a 160 x 120 image img2.Reset(160, 120); // does nothing (no penalty incurred) because data already allocated } // all memory is freed when images fall out of scopeThus, the user should never have to worry about explicitly allocating or deallocating memory, because the library takes care of those details automatically. And yet the memory management is done in a transparent way, so that the user remains in complete control. Those familiar with the Standard Template Library (STL) will notice the similarity.
Each image manages its own data. Therefore the assignment operator and copy constructor make an exact copy of the image dimensions and data. Subsequent changes to one image have no effect on the other image.
ImgGray img2(img); // copy constructor img2 = img; // assignment operatorAs a sidenote, be sure when writing your own functions never to pass an image "by value," which will cause the copy constructor to be called (an expensive operation). For efficiency reasons, images should always be passed "by reference" or "by pointer," as they are in Blepo.
unsigned char val; val = img(33, 22); // get pixel at column 34, row 23 -- since the top-left pixel is at (0,0) img(33, 22) = val; // set pixel at column 34, row 23To change all the pixels in the image, simply iterate through them:
unsigned char val = 255; // new value for (y=0 ; y<img.Height() ; y++) { for (x=0 ; x<img.Width() ; x++) { img(x,y) = val); } }
The parenthesis operator is inlined, so no function overhead is incurred. Moreover, bounds checking is performed using assert statements so that they disappear in Release mode, thus incurring no run-time penalty except during development. Nevertheless, the operator is fairly inefficient, since it uses arithmetic to compute the offset from the first pixel -- i.e., img(x,y) computes dataptr+y*width+x. A more efficient way to access pixels is to use pointers to the data:
unsigned char* p; for (p = img.Begin() ; p != img.End() ; p++) *p = 15; // set all pixels to value 15The Begin method returns a pointer to the first pixel, while End returns a pointer to the byte just after the last pixel. Like Begin, End returns a precomputed value and is inlined, so there is no penalty for calling this method each time through the loop.
To get read-only access to the pixels, use the const keyword. Although this is not, strictly speaking, necessary, it does improve code readability and provides for additional compile-time checking:
const unsigned char* p = img.Begin(); unsigned char val = *p; // get value of pixel
To get a pointer to a particular pixel, pass the coordinates of the pixel to the Begin method. This operation can be useful when processing a subarray within an image.
unsigned char* p = img.Begin(x, y); // returns a pointer to pixel (x, y)
ImgGray img, img2; Rect r(10, 10, 50, 50); // a rect structure (left, top, right, bottom) Point p(20, 30); // a point structure (x, y) Set(&img, 15); // set all pixels to value 15 Set(&img, r, 15); // set all pixels within a rectangle to value 15 Set(&img, p, img2); // copy entire 'img2' to 'img' at (20, 30) // ('img2' is smaller than 'img') Set(&img, p, img2, r); // copy rect of 'img2' to 'img' at (20, 30) Extract(img2, r, &img); // extract rectangle from 'img2'In addition, there are built-in functions for performing arithmetic and logical operations (And, Or, Xor, ...), along with functions for comparing images (Equal, LessThan, GreaterThan, IsSameSize, IsIdentical, ...).
Load("image.bmp", &img); // 'img' is output (img.Reset is called) Extract(img2, r, &img); // 'img2' is input, 'img' is output (img.Reset is called) Set(&img, 15); // 'img' is input/output; img.Reset is NOT calledThese simple conventions make it easy to remember how to call a function. They also greatly improve code readability, because they make it possible to distinguish immediately the inputs from the outputs simply by looking at a function is called, without having to know anything about the semantics of the function itself.
Images are large data structures. For efficiency reasons, you do not want to pass them around by value. Instead, you should pass them as pointers or by reference. Here is an example of a good function prototype:
void f(const ImgGray& a, ImgGray* b); // 'a' is input, 'b' is output
In this function, the parameter a is passed as a const reference. The const keyword indicates that the function has read-only access to the variable a. The ampersand (&) indicates that it is passed as a reference, which means that the image itself is not copied; rather, inside the function a is simply another name (an alias) for the image that was passed in. The variable b is passed as a pointer, which also means that the image itself is not copied; rather, b is a pointer that can be dereferenced to access the image that was passed in.
To call this function, pass it two images:
ImgGray a, b; f(a, &b);Note that pass by reference and pass as pointer achieve the same behavior with different syntax. With the former the ampersand (meaning 'reference') is in the prototype, while with the latter the ampersand (meaning 'take the address of') must be used each time the function is called. Which approach to use is largely a matter of style -- the library adopts the convention that inputs are passed using const references while outputs are passed using pointers.
ImgX img; // an image ImgX::Pixel val; // a pixel ImgX::Iterator p; // an iterator (pointer) ImgX::ConstIterator q; // a const iterator (pointer) p = img.Begin(); // get an iterator to the first pixel val = *p; // get the value val = *q; // get the value *p = val; // set the value *q = val; // error! cannot set using const iteratorwhere ImgX can be replaced by any of the image classes. For most of the classes, this interface is achieved using typedef. For example, ImgGray::Pixel is simply typedefed to unsigned char, an ImgInt pixel is an int, an ImgFloat pixel is a float, and an ImgBgr pixel is a Bgr struct that contains three unsigned chars:
struct Bgr { unsigned char b, g, r; };For any of these classes you are free to use either the basic type or the uniform interface, whichever you prefer. The only exception to this is that you must use ImgBinary::Iterator and ImgBinary::ConstIterator, because these are special internal classes that overcome the problem that in C++ bool* is not a pointer to a bit. To convert between images of different types, simply call Convert:
ImgGray img_gray; ImgBgr img_bgr; Convert(img_gray, &img_bgr);
To access pixels in one of these other image classes, simply replace unsigned char with the pixel type. For example, pixels in a BGR image are accessed in the following manner:
Bgr val; val = img(33, 22); // get pixel at column 34, row 23 -- since the top-left pixel is at (0,0) val.b = 255; val.g = 0; val.r = 0; img(33, 22) = val; // set pixel at column 34, row 23 to blue
and so on.
ImgGray img, gradx, grady, gradmag, img_smooth, filled; unsigned char new_color = 255; Point seed(20, 30); std::vector<Rect> rectlist; // array of rectangles Prewitt(img, &gradx, &grady); // compute Prewitt edges Sobel(img, &gradx, &grady); // compute Sobel edges PrewittMag(img, &gradmag); // compute magnitude of Prewitt edges Gradient(img, &gradx, &grady); // compute gradient using Gaussian derivative SmoothGauss5x5(img, &img_smooth); // smooth image by convolving with 5x5 Gaussian FloodFill4(img, new_color, seed.x, seed.y, &filled); // floodfill (4-connected) FaceDetector det; det.DetectFaces(img, &rectlist); // detect facesSome of these functions are optimized to use SIMD.(MMX/SSE/SSE2) operations, while others are unoptimized. Some of these functions are written from scratch for Blepo, while others are wrappers around code obtained from other libraries.
MatDbl mat; // an empty matrix MatDbl mat2(15, 10); // a matrix with 15 columns, 10 rows mat.Reset(15, 10); // reallocate for 15 columns, 10 rows double val; val = mat(3, 5); // get value at column 3, row 5 mat(3, 5) = val; // set value at column 3, row 5Note that the interface is not consistent with standard linear algebra notation in two ways:
MatDbl mat; // an empty matrix MatDbl mat2(10, 15, "ij"); // a matrix with 10 rows, 15 columns mat.ijReset(10, 15); // reallocate for 10 rows, 15 columns double val; val = mat.ij(5, 3); // get value at row 5, column 3 mat.ij(5, 3) = val; // set value at row 5, column 3
Basic matrix operations, such as add, multiply, transpose, creating identity matrices, etc., can be called as follows:
MatDbl a, b, c; MatDbl d(4); // create vertical vector (4 rows) double eps = 0.01; bool s; Eye(4, &a); // create identity matrix (4 columns, 4 rows) Rand(6, 3); // create random matrix (6 columns, 3 rows) Add(a, b, &c); // compute c = a + b Multiply(a, b, &c); // compute c = a * b (matrix multiply) ???????????????? s = Similar(a, b, eps); // returns whether matrix elements are the same, within 'eps' Transpose(a, &c); // computer c = a^T Diag(d, &c); // create diagonal matrix from vector Diag(c, &d); // create vector by extracting diagonal from matrixLinear algebra routines are provided via wrappers around the GNU Scientific Library (GSL) functions:
MatDbl a, u, s, v, l, p, q, r, eval, evec, x, inv; Svd(a, &u, &s, &v); // singular value decomposition (SVD) (a = u * Diag(s) * v) Lu(a, &l, &u, &p); // LU decomposition (p * a = l * u) Qr(a, &q, &r); // QR factorization (a = q * r) EigenSymm(a, &eval, &evec); // eigenvalues and eigenvectors of symmetric matrix double det = Determinant(a); // determinant SolveLinear(a, b, &x); // solve (possibly overdetermined) A x = b Inverse(a, &inv); // inverse
When efficiency is not important (e.g., when quickly prototyping a new idea), it may be preferable to use more compact notation, which is provided using overloaded operators. For example:
MatDbl a, b, c, d, e; e = a * b + Transpose(c) * Inverse(d); // compute d = a * b + c^T * d^{-1}
ImgGray img; Figure fig; fig.Draw(img); Point pt = fig.GrabMouseClick(); // wait until user clicks the mouse in the figure printf("point = %d, %d\n", pt.x, pt.y);GrabMouseClick draws cross-hairs in the figure as the mouse is moved; when the user clicks inside the figure, then processing resumes with the next line of code (in this case printing the coordinates). In this manner, you can easily get mouse input without having to write your own callback.
Before displaying an image, you can overlay lines, points, rectangles, ellipses, etc. using the following functions:
ImgBgr img; Bgr color(0, 255, 0); // green Point pt1(10, 10), pt2(20, 20); // two points int radius = 15; DrawLine(pt1, pt2, &img, color); // draw line DrawRect(rect, &img, color); // draw rectangle DrawCircle(pt1, radius, &img, color); // draw circleNote that these functions actually change the image itself.
To display an image directly onto an existing window, simply call the Draw function. In this example, MyWindow derives from CWnd:
void MyWindow::Func() { CClientDC dc(this); Draw(img, dc, 0, 0); // draw image in upper-left corner of window }
ImgBgr img; bool new_img; CaptureDirectShow cap; // create capture object cap.BuildGraph(320, 240); // build DirectShow graph cap.Start(); // start live capture new_img = cap.GetImage(&img); // get latest image if (new_img) { // use 'img' }Similar classes exist to capture from other devices.
Errors in Blepo are roughly divided into two categories. Errors which are the result of programmer mistakes (e.g., trying to add two images of different sizes) are often checked only with an assert statement. As a result, researchers are encouraged to use Debug mode whenever possible.
If an error occurs that is system-related (e.g., trying to load from a file that does not exist, or trying to capture from a camera that is not plugged in), an Exception is thrown. Unless you are happy with the application crashing because of an unhandled exception, you should handle these errors by wrapping your code with a try / catch block:
try { // call blepo code } catch (const Exception& e) { e.Display(); // display exception to user in a popup window }The Display method informs the user of the type of error, along with the line and file number where the error occurred. To see which functions may throw an exception, look at the header files.