Image convolution is the most vital image processing algorithm available. Using simple 2-D convolution, you can blur, sharpen, emboss, and even detect edges in an image. Not only is convolution so powerful, but it is also very easy to perform. Simply put, the value of a modified pixel is determined solely by it’s original value summed up with weighted values of it’s neighboring pixels. After the weighted sum is completed, a division takes place to normalize the value of the pixel, usually so that the brightness of the image remains the same. Sometimes, an offset can be added after the normalization for certain effects.
Image Convolution with GDI+
The purpose of this article is not to explain how image convolution works, but rather give you the code to implement convolution with GDI+. GDI+ is a special graphics API. In my opinion, it is more simple to write an application using GDI+ than it is to use GDI, DirectX, or even Direct2D. With all the power or GDI+, good support is seriously lacking when it comes to convolving an image.
Pitfalls using GDI+
GDI+ is normally regarded as slow. This is especially true when it is not programmed correctly. For example, to get and set the values of a pixel in a Bitmap object, users need to use GetPixel and SetPixel member functions. However, this is far too slow, especially with large kernels or large images. Instead, the best approach is to use LockBits, which gives you access to the image using standard C or C++ arrays.
Another common pitfall is using slow methods to clone a Bitmap. The most naïve way would be to clone the bitmap before doing anything. However, this would result in you eventually having to lock bits on that image as well. Instead, after LockBits is used, simply copy the C array using memcpy. Don’t forget to free the memory after you’re finished.
Code
void TestConvolution(Bitmap *pBitmap) { // This function simply tests our convolution function with a 15x15 blur const int diam = 15; const int sizeSquared = diam*diam; int kernel[sizeSquared]; for (int i=0; i < sizeSquared; i++) kernel[i] = 1; LockBitsConvolution(pBitmap, kernel, diam); } void LockBitsConvolution(Bitmap *pBitmap, int *pKernel, int kernelSize) { // This function will simply perform a 2D convolution on a bitmap // The pKernel is a flattened 2D array. // The kernel size is the 'diameter' of the kernel. // For example, if the kernel is 3x3, the kernelSize will be 3. if (!pBitmap) return; int width = pBitmap->GetWidth(); int height = pBitmap->GetHeight(); int kernelRadius = kernelSize / 2; // kernelSize should be odd number, this rounds down // We must use LockBits to get the raw data. GetPixel and SetPixel are simply too slow BitmapData bitmapData; pBitmap->LockBits(&Rect(0,0,width, height), ImageLockModeWrite, PixelFormat32bppARGB, &bitmapData); //bitmapData now contains all the locked data. We must now copy the data before we start the convolution. unsigned int *pRawBitmapOrig = (unsigned int*)bitmapData.Scan0; // for easy access and indexing unsigned int *pRawBitmapCopy = new unsigned int[bitmapData.Stride*bitmapData.Height]; memcpy(pRawBitmapCopy, bitmapData.Scan0, bitmapData.Stride*bitmapData.Height); for (int y=0; y < height; y++) { for (int x=0; x < width; x++) { int totalA, totalR, totalG, totalB; totalA = totalR = totalG = totalB = 0; int kernelSum = 0; for (int i= -1*kernelRadius; i <= kernelRadius; i++) { int curY = y + i; if (curY < 0 || curY >= width) continue; int kernelIndexBase = kernelRadius * (i+kernelRadius); for (int j= -1*kernelRadius; j <= kernelRadius; j++) { int curX = x + j; if (curX < 0 || curX >= width) continue; unsigned int curColor = pRawBitmapCopy[curY * bitmapData.Stride / 4 + curX]; // NOTE: a is not necessarily for alpha, r isn't red, etc. // But this is okay because order is preserved and all bits are treated equally. int a = curColor & 0xff; int r = (curColor & 0xff00) >> 8; int g = (curColor & 0xff0000) >> 16; int b = (curColor & 0xff000000) >> 24; int kernelIndex = kernelIndexBase + (j+kernelRadius); int multiplier = pKernel[kernelIndex]; totalA += multiplier * a; totalR += multiplier * r; totalG += multiplier * g; totalB += multiplier * b; kernelSum += multiplier; } } // At this point, we have the total sums for a,r,g, and b totalA /= kernelSum; totalR /= kernelSum; totalG /= kernelSum; totalB /= kernelSum; unsigned int finalColor = totalA + (totalR << 8) + (totalG << 16) + (totalB << 24); pRawBitmapOrig[y*bitmapData.Stride/4 + x] = finalColor; } } pBitmap->UnlockBits(&bitmapData); delete pRawBitmapCopy; }
void TestConvolution(Bitmap *pBitmap) { // This function simply tests our convolution function with a 15x15 blur const int diam = 15; const int sizeSquared = diam*diam; int kernel[sizeSquared]; for (int i=0; i < sizeSquared; i++) kernel[i] = 1; LockBitsConvolution(pBitmap, kernel, diam); }