macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Bitmap Image Filters
Pages: 1, 2, 3

The Way

Repeatedly, I've said that the filter will take a color image and convert it to gray-scale. This operation is a simple one, and in its most basic, it is no more complicated than finding a pixel, determining the values of the red, green, and blue samples, averaging those values, and then setting the average as the value of the white sample for the same in the gray-scale image representation.



This tiny bit of math must be done for every pixel in the image. To code this behavior we will be getting a heavy dose of C pointers and arrays. Since pointers and arrays are so closely related in C, I will use this discussion as an opportunity to show you how they are related. So, the first thing we need is a couple of loops that will scan through the entire image data buffer to access each pixel. This is where our x and y variables come in, as pixel numbers.

for ( y = 0; y < h; y++ ) {
    for ( x = 0; x < w; x++ ) {
        // Do the magic
    }
}

The action that goes on inside the loop reads the values of the red, green, and blue components of the current pixel, averages them together, and then sets the corresponding destination pixel equal to that averaged value. To accomplish this we need pointers to the beginning of the source and destination image data buffers. These pointers are conveniently obtained with bitmapData messages to srcImageRep and destImageRep. bitmapData method returns a character pointer, which is type unsigned char *. This is convenient as unsigned char is the same size as our samples, 8-bits. Putting these last couple of pieces together with our existing code gives us the following:

- (NSImage *)filterImage:(NSImage *)srcImage
{
    NSBitmapImageRep *srcImageRep = [NSBitmapImageRep
                    imageRepWithData:[srcImage TIFFRepresentation]];
    NSImage *destImage = [[NSImage alloc] initWithSize:NSMakeSize(w,h)];

    int w = [srcImageRep pixelsWide];
    int h = [srcImageRep pixelsHigh];
    int x, y;
       
    NSBitmapImageRep *destImageRep = [[[NSBitmapImageRep alloc] 
                    initWithBitmapDataPlanes:NULL
                    pixelsWide:w 
                    pixelsHigh:h 
                    bitsPerSample:8 
                    samplesPerPixel:1
                    hasAlpha:NO
                    isPlanar:NO
                    colorSpaceName:NSCalibratedWhiteColorSpace
                    bytesPerRow:NULL 
                    bitsPerPixel:NULL] autorelease];

    unsigned char *srcData = [srcImageRep bitmapData];
    unsigned char *destData = [destImageRep bitmapData];
    unsigned char *p1, *p2;

    for ( y = 0; y < h; y++ ) {
        for ( x = 0; x < w; x++ ) {
            
        }
    }
    
    [destImage addRepresentation:destImageRep];
    return destImage;
}

In addition to declaring the pointers srcData and destData that point to the first element, or the head of the data buffers, we define the pointers p1 and p2, which will be used as working pointers. Think of them as moving cursors set to the location in the array of the pixel we're currently working on in the loop.

To understand how we work with the data, we have to understand more about how the data is organized. In a 24-bit image, where each color sample is 8-bits, with no alpha component, the data is arranged sequentially, where the first byte of the data buffer corresponds to the blue component of the first pixel. The second byte in the data array is the green component of the first pixel, and the third byte is the red component of the first pixel. This sequence continues for each pixel, across the first row of pixels (constant y, increasing x), and then, like a carriage return on a typewriter, we get to the next vertical row (incrementing y, reset x and go down the row again). This accounts for why the x for-loop is nested within the y for-loop.

A Bit About C-Pointer Arithmetic

In C a pointer is a variable that points to a place in memory where a meaningful value is stored. In our case, these meaningful values are the color samples for all of the pixels in our image. In our code, srcData is a pointer that points to the blue sample of the pixel in row 1, column 1. If we want to know the value at this memory location, we would use the dereferencing operator. Thus, *srcData is the value of the pixel sample.

Now, what if we want to know the value of the memory location adjacent to the one pointed to by srcData? What about the value stored in the memory location 2 or 3 or 300 slots beyond srcData? Simple, we just add the number of slots to the pointer and we get a pointer to that memory location. So the memory slot adjacent to srcData is srcData+1, two slots away is srcData+2, and so on. Again, to access the value at these locations use the dereferencing operator. So, *(srcData+1) is the adjacent value, or the value of the green sample of the first pixel, and so on. Notice how we used parentheses. Writing *srcData + 1 gives us the value of the first pixel's blue sample, plus one.

Now, let's see how we use this in the code below:

unsigned char *srcData = [srcImageRep bitmapData];
unsigned char *destData = [destImageRep bitmapData];
unsigned char *p1, *p2;
int n = [srcImageRep bitsPerPixel] / 8;

for ( y = 0; y < h; y++ ) {
    for ( x = 0; x < w; x++ ) {
        p1 = srcData + n * (y * w + x);       
           p2 = destData + y * w + x;
        
        *p2 = (unsigned char)rint((*p1 + *(p1 + 1) + *(p1 + 2)) / 3);
    }
}

Let's take our time and pick this thing apart. The first line in the for-loop says that p1, a pointer, will point to the location in the source image data buffer that corresponds to the xth pixel in the yth row. We get to that pixel by figuring out how many pixels into the data buffer we are, which is the y-value times the width of the image plus the x value, and we multiply that by the number of bytes in each pixel, n. In our code, we expect n to be 3. The resulting number is then added to the address pointed to by srcData.

Next we repeat this calculation for p2 in the destination data buffer. However, since the destination data is in the NSCalibratedWhiteColorspace colorspace, each pixel occupies only one byte of memory (because that is what's needed to represent a gray-scale value). So the calculation is the same as above, except n equals 1.

In the third line we access each of the red, green, and blue bytes of the pixel located by p1. The blue byte is pointed to by p1. Because the green and red bytes occur sequentially after the blue byte, we can increment the address of p1 by 1 to get the green byte location, and by 2 to get the blue byte location. To retrieve the actual value stored in those address locations we have to dereference the pointer. The * operator is applied only to addresses, and will return the value stored at the location pointed to by the address. So, p1, p1+1, and p1+2 are pointers to the red, green, and blue components, and *p1, *(p1+1), and *(p1+2) are the values of those components.

We sum up those three values, average them, round them off as an integer, and then cast it back as a unsigned char (since we're limited to 8-bits of storage space per component), and set the value of the memory pointed to by p2, which is *p2, to the result.

So, there you have it: C pointer arithmetic in a nutshell.

The above could have also been written using C's array notation. Writing our pointer arithmetic in pointer syntax is simple. For example, previously, *(p1+n) referred to the value at the memory location n steps past p1. In array syntax that would have be written as p1[n]. With this syntactical change the previous code can be rewritten as the following:


unsigned char *srcData = [srcImageRep bitmapData];
unsigned char *destData = [destImageRep bitmapData];
unsigned char *p1, *p2;
int n = [srcImageRep bitsPerPixel] / 8;

for ( y = 0; y < height; y++ ) {
    for ( x = 0; x < width; x++ ) {
        p1 = srcData + n * (y * w + x);       
           p2 = destData + y * w + x;
        
        p2[0] = (unsigned char)rint((p1[0] + p1[1] + p1[2]) / 3);
    }
}

Another way we can write the for-loop is to replace the n*y*w with the number of bytes per row. This would simplify the address arithmetic to the following:

unsigned char *srcData = [srcImageRep bitmapData];
unsigned char *destData = [destImageRep bitmapData];
unsigned char *p1, *p2;

int n = [srcImageRep bitsPerPixel] / 8;
int srcBPR = [srcImageRep bytesPerRow];
int destBPR = [destImageRep bytesPerRow];

for ( y = 0; y < height; y++ ) {
    for ( x = 0; x < width; x++ ) {
        p1 = srcData + y * srcBPR + n*x;       
           p2 = destData + y * destBPR + x;
        
        p2[0] = (unsigned char)rint((p1[0] + p1[1] + p1[2]) / 3);
    }
}

So, this is just another way of locating the data. When we put all the pieces together our final -filterImage: method looks like the following:

- (NSImage *)filterImage:(NSImage *)srcImage
{
    NSBitmapImageRep *srcImageRep = [NSBitmapImageRep
                    imageRepWithData:[srcImage TIFFRepresentation]];
    NSImage *destImage = [[NSImage alloc] initWithSize:NSMakeSize(w,h)];

    int w = [srcImageRep pixelsWide];
    int h = [srcImageRep pixelsHigh];
    int x, y;
       
    NSBitmapImageRep *destImageRep = [[[NSBitmapImageRep alloc] 
                    initWithBitmapDataPlanes:NULL
                    pixelsWide:w 
                    pixelsHigh:h 
                    bitsPerSample:8 
                    samplesPerPixel:1
                    hasAlpha:NO
                    isPlanar:NO
                    colorSpaceName:NSCalibratedWhiteColorSpace
                    bytesPerRow:NULL 
                    bitsPerPixel:NULL] autorelease];

    unsigned char *srcData = [srcImageRep bitmapData];
    unsigned char *destData = [destImageRep bitmapData];
    unsigned char *p1, *p2;
    int n = [srcImageRep bitsPerPixel] / 8;

    for ( y = 0; y < height; y++ ) {
        for ( x = 0; x < width; x++ ) {
            p1 = srcData + n * (y * w + x);       
               p2 = destData + y * w + x;
            
            p2[0] = (unsigned char)rint((p1[0] + p1[1] + p1[2]) / 3);
        }
    }
    
    [destImage addRepresentation:destImageRep];
    return destImage;
}

With that we're ready to compile the code, and try it out.

Summary

As always, there are a number of enhancements you can make to this filter method. The implementation we created today is limited to 24-bit RGB images. One first enhancement would be to add support for images with an alpha channel. In the project you can download here you will find my implementation--it provides support for alpha.

You could also use this simple piece of code to make Altivec enhancements to the loop. I've haven't yet taken the time to dive into the Altivec libraries (I've only had a G4 for about six weeks now), but an image filter that uses the Altivec libraries is where I would start. It has all of the characteristics that make it a prime candidate for vectorization, and I imagine there'd be noticeable speed improvement. I leave that as an exercise for you folks.

So, we've seen how to make one image filter. There are hundreds of different operations you can perform on images that would fit well within the framework we've established here. That framework consists of this black-box method, -filterImage, where we pass an image in and get a new one back. The only thing that changes is what happens inside the for-loop. I'm not going to take the time to develop more filter operations, but in the next column I will show you how to define and implement an interface to a plug-in architecture for the application. This plug-in architecture will unload the burden of filter development from you to end users of the application. See you then.

Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.


Read more Programming With Cocoa columns.

Return to the Mac DevCenter.