Introduction
Using Bitmap
class and its methods GetPixel
, SetPixel
for accessing bitmap's pixels is slow. In this article, we will see how to increase the speed of accessing bitmap's pixels with the help of FastBitmap
class.
As some readers of Fast image processing in C# suggested, we will also write 3 similar programs and compare their performance. The programs replace the center of an image with its grey version of it:
- Program #1 will use the
FastBitmap
class - Program #2 will use
Bitmap
class and its methods GetPixel
, SetPixel()
- Program #3 will use C++ and opencv
About FastBitmap
The FastBitmap
allows to access the raw memory of bitmap data. For convenience, the class implements
IDisposable
Interface which allows to easily manage the unmanaged raw memory data.
The class allows to process a rectangle in the bitmap. We can use the following properties to locate pixel in this rectangle:
YY,XX
are the top left point of the rectangle Width
, Height
are the dimensions of the rectangle PixelSize
is size of pixel in bytes Scan0
is a pointer to the location of the top left pixel of the rectangle Stride
is the width in bytes of row of pixels of the image
The constructor locks the raw memory of bitmap and allows us to access it via pointers. It takes the bitmap and an optional rectangle we want to process. The Dispose
frees the unmanaged resources and unlocks the memory - You should call it when you finish to process the bitmap.
For example:
fb = new FastBitmap(bitmap,3,1,4,3);
...
fb.Dispose()
Suppose the bitmap is color image in 24bpp-BGR and its dimensions are 9x6.
fb.PixelSize
is 3 since each pixel has 3 bytes. - We process the
4x3
rectangle located at (3,1)
therefore fb.Width
=4, fb.Height
=3, fb.XX
=3 , fb.YY
=1 - The
fb.Stride
is fb.PixelSize
* bitmap's width
so fb.Stride=
9*3=27 - The
fb.scan0
will point to pixel located at (3,1)
.
To access pixel (xx,yy) in the rectangle, we use the following formula:
Location(xx,yy) = fb.Scan0 + yy * fb.Stride + xx * fb.PixelSize
Using FastBitmap
The following demo shows how can we use this class - the demo will replace the center of an image with its grey version of it.
if (bitmap.PixelFormat != PixelFormat.Format24bppRgb &&
bitmap.PixelFormat != PixelFormat.Format32bppArgb) {
return;
}
int ww = bitmap.Width / 8;
int hh = bitmap.Height / 8;
using (FastBitmap fbitmap = new FastBitmap(bitmap, ww, hh,
bitmap.Width - 2 * ww, bitmap.Height - 2 * hh)) {
unsafe {
byte* row = (byte*)fbitmap.Scan0, bb = row;
for ( int yy = 0; yy < fbitmap.Height; yy++,
bb = (row += fbitmap.Stride)) {
for (int xx = 0; xx < fbitmap.Width ; xx++,
bb += fbitmap.PixelSize) {
byte gray = (byte)((1140 * *(bb + 0) +
5870 * *(bb + 1) +
2989 * *(bb + 2)) / 10000);
*(bb + 0) = *(bb + 1) = *(bb + 2) = gray;
}
}
}
}
In Z1
, we check that the bitmap
format is color image in 24bpp-BGR or 32bpp-BGRA format.
- In 24bpp-BGR format, each pixel is stored in 24 bits (3 bytes) per pixel. Each pixel component is stored in 8 bits(1 byte) in the following order:
B
component of the pixel is stored in byte 0 (Blue) G
component of the pixel is stored in byte 1 (Green) R
component of the pixel is stored in byte 2 (Red)
- In 32bpp-BGR format, each pixel is stored in 32 bits(4 bytes) per pixel. Each pixel component is stored in 8 bits(1 byte) in the following order:
B
component of the pixel is stored in byte 0 (Blue) G
component of the pixel is stored in byte 1 (Green) R
component of the pixel is stored in byte 2 (Red) A
component of the pixel is stored in byte 3 (Alpha)
We can see the 32bpp-BGRA format extends 24bpp-BGR format with an extra alpha component. We can use this observation to process those images with the same code.
In Z2
, we are using the using
block. This block ensures invoking the Dispose
method on the FastBitmap
object, we create in the beginning of the block, at the end of this block. In this case, the rectangle width and height is 3/4 of the bitmap and its left top pixel is located (1/8 of bitmap width, 1/8 of bitmap height).
In Z3
, we start a unsafe
block which allows us to use pointers inside it. Please note that we also need to compile with unsafe
compiler flag.
In Z4
, we declare two byte pointers. Those pointers will be used to access the pixels, pixel by pixel.
row
points to the first pixel in the current row bb
points to the current pixel.
In Z5
, we loop for each row (top to bottom) and updates row
pointer to point to the first pixel in the current row (Note the use of the Stride
property).
In Z6
, we loop for each pixel in the current row and update the bb
pointer (note the use of the PixelSize
property).
In Z7
, we process the pixel using the current pixel pointed by bb
.
In this demo, we convert the color of the pixel to grey scale color by setting all pixel’s components to the value of the following formula:
- 0.1140 * (Blue component) + 0.5870 * (Green component) + 0.2989 * (Red component)
Let's see how the demo works on the left image and produces the right one:
Benchmarks
Readers suggested to measure the performance against other image processing alternatives.
Bitmap GetPixel and SetPixel Approach
The demo is similar to the original demo program. But, instead of using pointers, we will use SetPixel
and GetPixel
methods of the Bitmap
class.
if (bitmap.PixelFormat != PixelFormat.Format24bppRgb &&
bitmap.PixelFormat != PixelFormat.Format32bppArgb) {
return;
}
int w0 = bitmap.Width / 8;
int h0 = bitmap.Height / 8;
int x1 = w0;
int y1 = h0;
int xn = x1 + bitmap.Width - 2 * w0;
int yn = y1 + bitmap.Height - 2 * h0;
Color gray, cc;
for ( int yy = y1; yy < yn; yy++) {
for (int xx = x1; xx < xn; xx++) {
cc = bitmap.GetPixel(xx, yy);
byte gg = (byte)((cc.B * 1140 +
cc.G * 5870 +
cc.R * 2989) / 10000);
gray = Color.FromArgb( gg,gg,gg);
bitmap.SetPixel(xx, yy, gray);
}
}
- In
Z1
, we loop over all the pixels’ locations of the rectangle, pixel by pixel (top to down and left to right). - In
Z2
, we use GetPixel
method to get the Color
of the current pixel. - In
Z3
, we create grey Color
by setting all its components to the above grey level formula. - In
Z4
, we use SetPixel
method to set the pixel to grey Color
we found in the above step.
C++ and opencv Approach
The demo is similar to the original demo program, But instead of using pointers, we will use C++ and opencv library. I tried to follow the ideas of FastBitmap
class.
Mat bitmap = imread(argv[1], CV_LOAD_IMAGE_COLOR);
...
int ww = bitmap.cols - 2 * bitmap.cols / 8;
int hh = bitmap.rows - 2 * bitmap.rows / 8;
int x1 = bitmap.cols / 8;
int y1 = bitmap.rows / 8;
int pixelSize = bitmap.channels();
int stride = pixelSize * bitmap.cols;
uchar* scan0 = bitmap.ptr<uchar>(0) + (y1 * stride) + x1 * pixelSize;
uchar* row = scan0, *bb = row;
for ( int yy = 0; yy < hh; yy++, bb = (row += stride)) {
for (int xx = 0; xx < ww; xx++, bb += pixelSize ) {
uchar gray = ((1140 * *(bb + 0) +
5870 * *(bb + 1) +
2989 * *(bb + 2)) / 10000);
*(bb + 0) = *(bb + 1) = *(bb + 2) = gray;
}
}
...
imwrite(argv[2], bitmap, compression_params);
</uchar>
- In
Z1
, we load the image data from file to memory. - In
Z2
, we find the rectangle we want to process. - In
Z3
, we find scan0
, pixelSize
and the stride
of the bitmap. - The image processing code in
Z4
is the same code from our first C# programs. - In
Z5
, we write the image to file.
Benchmarks
I ran each program 1000 times with jpg image 800x600 and measure the average time each program last in system ticks (1 millisecond = 10000 ticks).
When I run the benchmark on my computer, Intel Core i7(6700K) machine, I get the following results:
As we can see:
- The
C#(FastBitmap and pointers)
approach improve the speed of C#(Bitmap GetPixel and SetPixel)
approach by 80%. - It seems that the
C++(opencv)
approach has a negligible speed improvement (less than 2% improvement) from C#(FastBitmap and pointers)
approach.
History
- Version #4 - Updated to VS2019, moved code to Github
- Version #3 - Updated code and samples
- Version #2 - Added Benchmarks
- Version #1 - Republished Fast image processing in C#