Example Image Filter
Introduction
Input: Lenna input Image.
This tutorial introduces basic BoofCV programming concepts through three examples. In BoofCV there are often three ways to invoke a function; 1) procedural, 2) generalized, and 3) abstracted.
Procedural functions are contained in children of the boofcv.alg package, where 'alg' stands for algorithms. These procedural functions are low level implementations which allow greater customization, tighter memory control, and have strong typing. In the same package are generalized versions of procedural functions that provide weaker type checking, but allow generic images to be passed in. Classes which contain generalized functions start with the letter 'G'. For example, BlurImageOps contains a set of functions for each image type and operation pair it supports and GBlurImageOps has a single function for each operation it supports.
Inside the children of 'boofcv.abst' package are abstracted filters. These classes provide an Object Oriented Programming (OOP) interface and handle much of the memory management, but are less flexible. Some times the ability to configure low level parameters has been removed from the algorithms that they are wrappers around to make development more manageable.
Example File: ImageFilterExample
Tutorial Concepts:
- Invoke procedural functions.
- Invoke generalized functions using Java generics.
- Create and invoke abstracted filters.
- Use a Factory.
- Display images.
Relevant Applets:
Procedural
Here an ImageUInt8 image is passed in, which is then blurred, has its derivative computed and displayed in a window. In BoofCV, unlike many other vision libraries, to provide strong type checking at compile time, images with different primitive data types are a different type. The name 'UInt8' indicates that its elements are unsigned 8-bit integers and that it has only a single band. Image derivative require a different data type because the domain of each element includes negative numbers and has a larger domain, which is why they are stored in a ImageSInt16. ImageSInt16 are single band images with signed 16-bit integer pixels.
public static void procedural( ImageUInt8 input )
{
ImageUInt8 blurred = new ImageUInt8(input.width,input.height);
ImageSInt16 derivX = new ImageSInt16(input.width,input.height);
ImageSInt16 derivY = new ImageSInt16(input.width,input.height);
// Gaussian blur: Convolve a Gaussian kernel
BlurImageOps.gaussian(input,blurred,-1,blurRadius,null);
// Calculate image's derivative
GradientSobel.process(blurred, derivX, derivY, FactoryImageBorder.extend(input));
// display the results
BufferedImage outputImage = VisualizeImageData.colorizeSign(derivX,null,-1);
ShowImages.showWindow(outputImage,"Procedural Fixed Type");
}
The two integer parameters in BlurImageOps.gaussian(input,blurred,-1,2,null) specify the blur's sigma and radius. A negative sigma indicates that the sigma should be chosen based upon the kernel's radius. The converse is also true, a negative radius means the radius should be selected based upon the sigma's value. When applying Gaussian blur, additional storage is required. Because null was passed in for the last parameter, it will internally declare that memory. Declaring temporary memory each time a function is invoked is wasteful, but easier to program.
When convolving a kernel across an image its not always clear how image borders should be handled. At the image border convolution kernels will be partially outside of the image. In this example, the gradient is computed using a Sobel kernel, and BorderType.EXTENDED tells it to handle the border by extending border pixels to outside the image.
Finally the output is computed by rendering the image derivative into a BufferedImage. BufferedImages come from Java Swing and is Java's standard way for storing image data. They are not used internally in BoofCV due to their complexity and poor performance. Once rendered, it is displayed in an image using the convenient showWindow() function.
Generalized
The following example is very similar to the first, but it now takes in a generalized image type using Java generics. Type checking is weaker, but different image types can be passed in using the exact same code.
public static <T extends ImageBase, D extends ImageBase>
void generalized( T input )
{
Class<T> inputType = (Class<T>)input.getClass();
Class<D> derivType = GImageDerivativeOps.getDerivativeType(inputType);
T blurred = GeneralizedImageOps.createImage(inputType,input.width, input.height);
D derivX = GeneralizedImageOps.createImage(derivType,input.width, input.height);
D derivY = GeneralizedImageOps.createImage(derivType,input.width, input.height);
// Gaussian blur: Convolve a Gaussian kernel
GBlurImageOps.gaussian(input, blurred, -1, blurRadius, null);
// Calculate image's derivative
GImageDerivativeOps.sobel(blurred, derivX, derivY, BorderType.EXTENDED);
// display the results
BufferedImage outputImage = VisualizeImageData.colorizeSign(derivX,null,-1);
ShowImages.showWindow(outputImage,"Generalized "+inputType.getSimpleName());
}
Since the type is not known at compile type, images are created using GeneralizedImageOps.createImage(). Java generics allow consistency checks to make sure images which should be the same type are the same type. Notice that blurring and image derivatives are computed using classes that begin with the letter 'G', that signifies they are generalized classes.
Abstracted
Instead of invoking functions, filter classes are created here. Filters are even more flexible and allow greater abstraction.
public static <T extends ImageBase, D extends ImageBase>
void filter( T input )
{
Class<T> inputType = (Class<T>)input.getClass();
Class<D> derivType = GImageDerivativeOps.getDerivativeType(inputType);
T blurred = GeneralizedImageOps.createImage(inputType, input.width, input.height);
D derivX = GeneralizedImageOps.createImage(derivType, input.width, input.height);
D derivY = GeneralizedImageOps.createImage(derivType, input.width, input.height);
// declare image filters
BlurFilter<T> filterBlur = FactoryBlurFilter.gaussian(inputType, -1, blurRadius);
ImageGradient<T,D> gradient = FactoryDerivative.sobel(inputType, derivType);
// process the image
filterBlur.process(input,blurred);
gradient.process(blurred,derivX,derivY);
// display the results
BufferedImage outputImage = VisualizeImageData.colorizeSign(derivX,null,-1);
ShowImages.showWindow(outputImage,"Filter "+inputType.getSimpleName());
}
Notice how Factories are used to create filters. Factories provide an easy to use interface for constructing filters and other algorithms. They also provide a single location to look for similar algorithms which implement the same interface. Often they will simplify development by handling all the little details and only let the developer specify major parameters. Notice how FactoryDerivative.sobel() does not allow the user to specify the border type, that's because it assumes it knows the best type and might as well spare the developer from that level of detail.
No Generics
For sake of completeness, it is demonstrated that Java generics are optional when writing generalized code. No type checking is done here, but the code is less verbose.
public static void nogenerics( ImageBase input )
{
Class inputType = input.getClass();
Class derivType = GImageDerivativeOps.getDerivativeType(inputType);
ImageBase blurred = GeneralizedImageOps.createImage(inputType,input.width, input.height);
ImageBase derivX = GeneralizedImageOps.createImage(derivType,input.width, input.height);
ImageBase derivY = GeneralizedImageOps.createImage(derivType,input.width, input.height);
// Gaussian blur: Convolve a Gaussian kernel
GBlurImageOps.gaussian(input, blurred, -1, blurRadius, null);
// Calculate image's derivative
GImageDerivativeOps.sobel(blurred, derivX, derivY, BorderType.EXTENDED);
// display the results
BufferedImage outputImage = VisualizeImageData.colorizeSign(derivX,null,-1);
ShowImages.showWindow(outputImage,"Generalized "+inputType.getSimpleName());
}