
ITL is a C++ image library developed at Enseed Inc. that relies heavily on templates to define efficient image representation in system memory. Thanks to the power of C++ templates, a maximum of information can be used at compile time to facilitate the use of various image representations, minimize errors, maximize compatibility and yet leave little to no footprint at compile time.
Some of the abstractions in ITL include:
- Abstraction from image orientations through smart row and column iterators.
- Abstraction from color spaces (e.g. sRGB vs YCbCr-601).
- Abstraction from component model (RGB vs CMY).
- Abstraction from planar representation.
- Abstraction from multiple pixel in a single packet (e.g. YCbCr422 or 1 bit black & white).
- Abstraction from component range (e.g. with video YCbCr where 16/255 is black and 235/255 is white).
- Abstraction from component ordering (an algorithm that works on RGB will also work on BGR).
- Abstraction from component size (working with 1, 2, 4, 8, 16, 32, 64 bit components is all the same)
Structure
ITL can be used from four different levels:
Image: A template-less wrapper over the PixMap to simplify image manipulation.- PixelPacket: A group of pixels sharing the same storage.
- PixMap: A collection of PixelPacket representing an image.
- Pixels: The smallest element of a picture.
Examples
Working with Pixels
Since the component ordering and the component size is abstracted, one can write a pixel loop that returns luminance from any RGB source, RGB pixel, whether it is 1, 8, 32 or 64 bit per components, and no matter if it is RGB, or BGR, no matter if it is packed or planar, and whether it also contains alpha or not:
float LuminanceFromRGB(const PIXELTYPE &pixel)
{
const float redMin = PIXELTYPE::RedStorageType::min();
const float redMax = PIXELTYPE::RedStorageType::max();
float red = (float(pixel->red()) - redMin) / (redMax - redMin);
const float greenMin = PIXELTYPE::GreenStorageType::min();
const float green Max = PIXELTYPE::GreenStorageType::max();
float green = (float(pixel->green()) - greenMin) / (greenMax - greenMin);
const float blueMin = PIXELTYPE::BlueStorageType::min();
const float blue Max = PIXELTYPE::BlueStorageType::max();
float blue = (float(pixel->blue()) - blueMin) / (blueMax - blueMin);
// Following CCIR-601 for luminance from RGB
return red * 0.299 + green * 0.587 + blue * 0.114;
}
Since all ITL pixels have built-in conversion, the previous code can be shortened if you convert to a float RGB pixel:
float LuminanceFromRGB(const PIXELTYPE &pixel)
{
RGB32fPixel fPixel;
fPixel.convert(pixel);
// Following CCIR-601 for luminance from RGB
return fPixel.red() * 0.299 + fPixel.green() * 0.587 + fPixel.blue() * 0.114;
}
And while we’re at it, there is a pixel type defined for luminance:
float LuminanceFromRGB(const PIXELTYPE &pixel)
{
Lum32fPixel lum;
lum.convert(pixel);
return lum.luminance();
}
Using template specialization, one can write several algorithm depending on the various details of the pixel. For instance, one could process pixels from one color space differently than pixels from another colorspace:
void ProcessPixel(const PIXELTYPE &pixel);
template<class PIXELTYPE>
void ProcessPixel<PIXELTYPE, ColorSpace_sRGB>(const PIXELTYPE &pixel)
{ /* Process a pixel that is defined in the sRGB color space */ }
template<class PIXELTYPE>
void ProcessPixel<PIXELTYPE, ColorSpace_YCbCr601>(const PIXELTYPE &pixel)
{ /* Process a pixel that is defined in the YCbCr BT.601 color space */ }
// Calling ProcessPixel<PIXELTYPE>(pixel) will automatically choose the correct function.
Or process a pixel differently if it has an alpha component or not:
void ProcessPixel(const PIXELTYPE &pixel);
template<class PIXELTYPE, true>
void ProcessPixel(const PIXELTYPE &pixel)
{ /* process a pixel that has an alpha component - i.e. pixel.alpha() is valid */ }
template<class PIXELTYPE, false>
void ProcessPixel(const PIXELTYPE &pixel)
{
// process a pixel that has does not have an alpha component
// i.e. pixel.alpha() is invalid and would generate an error
}
// Calling ProcessPixel<PIXELTYPE>(pixel) will automatically choose the correct function.
Working with PixelPackets
Pixel packets are used when a single storage element holds multiple pixels. This
is the case, for instance, with YCbCr422 pixels or 1, 2 and 4 bit per component
pixels. In the case of a 1 bit b&w pixel, a single byte holds 8 pixels:
cout << "This packet holds " << Lum1Pixel::PIXEL_PER_PACKET << " pixels" << endl;
// This packet holds 8 pixels
Special care must be given to these types of pixels. They can be set in one call
(bw.setLuminance(1, 0, 0, 1, 1, 1, 1, 0);), but they must be read
individually:
int bw1 = bw.pixel1().luminance();
int bw2 = bw.pixel2().luminance();
...
In the case of YCbCr422, you can thus access the two pixels like this:
yuv422.setYCbYCr(255, 128, 192, 64);
RGB8Pixel rgb1;
rgb1.convert(yuv422.pixel0());
RGB8Pixel rgb2;
rgb2.convert(yuv422.pixel1());
In case you are wondering, pixel0 and pixel1 refer to the same address (same storage) but provide a different interpretation of the storage using a type cast.
Working with PixMap
PixMap can be iterated such that you do not have to worry whether they are top down or bottom up:
PixMap<RGB8Pixel, PixMapDrawsFromBottom> rgbPixMap(256, 256);
// an 8 bit RGB pixmap, the first pixel is at the top of the image
PixMap<RGB8Pixel, PixMapDrawsFromTop> rgbPixMap(256, 256);
// by default, an image is drawn from top (the most common case)
PixMap<RGB8Pixel> rgbPixMap(256, 256);
// Iterate in the native direction
PixMap<RGB8Pixel>::Iterator iter = rgbPixMap.begin();
// Iterate from top to bottom
PixMap<RGB8Pixel>::TopToBottomIterator iter = rgbPixMap.beginFromTop();
// Iterate from bottom to top
PixMap<RGB8Pixel>::BottomToTopIterator iter = rgbPixMap.beginFromBottom();
This is useful when you are dealing with two images and want to make sure you are iterating over them in the same direction. Iteration has been pixmaps simplified using functors. For instance, the following code would save any pixmap (RGB, YUV, CMYK, 1bbp, 8bbp, 64bbp, planar, packed, upside down, etc..) to a ppm file:
{
public:
SaveToPPM(FILE *file) : _file(file) {}
template<class PIXELTYPE>
void operator()(const PIXELTYPE &pixel) const
{
RGB8Pixel rgbPixel;
rgbPixel.convert(pixel);
fwrite(&rgbPixel, 3, 1, _file);
}
private:
FILE *_file;
};
PixMap<YCbYCr8Pixel> yuv422PixMap(512, 512);
FILE *f = fopen("img.ppm", "w");
fprintf(f, "P6\n%d %d 255\n", yuv422PixMap.width(), yuv422PixMap.height());
yuv422PixMap.forEachPixel(SaveToPPM(f));
f.close();
