The code snippets in this tip will show you how to read/write image data using the .NET Bitmap class in VB.NET.
Background
I was looking for a solution in VB.NET to speed up image read/write operations by using pointers to a bitmap instead of accessing the pixel object.
Using the Code
a) Reading in Bitmap Data in VB.NET Works as Follows
- Read in the bitmap data into a bitmap object.
- Get the bitmap data and lock its position in memory, so the garbage collection cannot move the data around anymore.
- Get a pointer to the first byte of the image data, which is
bmpData.Scan0
. - Calculate the offset of the pixel from the first byte as
(bmpData.Stride * row) + (4 * column)
. - Read out the bytes corresponding to the individual R, G, B channels of the pixel. Here, the pointer position is calculated for the
32bppArgb
format in which 1 bytes is reserved for each channel in the order B, G, R, transparency (alpha channel). In this example, the channel intensities are converted to a grayscale value. - Unlock the bitmap data.
- Do some calculation with the data in the array.
fs = New FileStream(m_imgpath, FileMode.Open, FileAccess.Read)
bm = New Bitmap(fs)
fs.Close()
Dim grayval as double
Dim arr(bm.height, bm.width) as Double
Dim pixptr As Integer
Dim wb, wg, wr as double
wr = 0.2126 : wg = 0.7125 : wb = 1 - (wr + wg)
Dim rect As New Rectangle(0, 0, bm.Width, bm.Height)
Dim bmpData As System.Drawing.Imaging.BitmapData = bm.LockBits
(rect, Drawing.Imaging.ImageLockMode.ReadWrite, Imaging.PixelFormat.Format32bppArgb)
For i = 0 To bmpData.Height - 1
For j = 0 To bmpData.Width - 1
pixptr = (bmpData.Stride * i) + (4 * j)
grayval = wb*CType(Marshal.ReadByte(bmpData.Scan0, pixptr), Integer)
grayval += wg*CType(Marshal.ReadByte(bmpData.Scan0, pixptr + 1), Integer)
grayval += wr*CType(Marshal.ReadByte(bmpData.Scan0, pixptr + 2), Integer)
arr(i, j) = grayval
Next
Next
bm.UnlockBits(bmpData)
b) Writing Image Data to a Bitmap Using VB.NET Works in a Similar Way
- Create the bitmap to write to.
- Get the
bitmapdata
and lock it in memory. - Get the pointer to the first byte.
- Get the offset of the actual pixel to write to.
- Get the offset of the pixel's channel.
- Normalize the value to the interval [0, 255].
- Write the normalized value to the channel byte.
- Unlock the bitmap data.
bm = New Bitmap(Width, Height)
Dim rect As New Rectangle(0, 0, bm.Width, bm.Height)
Dim bmpData As System.Drawing.Imaging.BitmapData = bmPhase.LockBits
(rect, Drawing.Imaging.ImageLockMode.ReadWrite, Imaging.PixelFormat.Format32bppArgb)
maxval = max(arr)
For i = 0 To bm.Height - 1
For j = 0 To bm.Width - 1
byteval = CByte(Math.Abs(arr(i, j)) / maxval * 255)
pixptr = (bmpData.Stride * i) + (4 * j)
Marshal.WriteByte(bmpData.Scan0, pixptr, byteval)
Marshal.WriteByte(bmpData.Scan0, pixptr + 1, byteval)
Marshal.WriteByte(bmpData.Scan0, pixptr + 2, byteval)
Marshal.WriteByte(bmpData.Scan0, pixptr + 3, 255)
Next
Next
bm.UnlockBits(bmpData)
c) Writing bitmap in C++/CLI
For comparison, I include the same steps for writing image data in C++/CLI. C++ allows the use of pointers. As a result, the code is shorter and faster. Herein, "col
" is a struct
that contains the RGB values and "RainbowNumberToColor
" computes a color coding. I took this function from C# helper (Map numeric values to and from colors in a color gradientC# Helper (csharphelper.com)).
Bitmap^ output = gcnew Bitmap(width, height);
Imaging::BitmapData^ bitmapData1 = output->LockBits(Rectangle(0, 0, width, height),
Imaging::ImageLockMode::ReadOnly, Imaging::PixelFormat::Format32bppArgb);
IntPtr^ pbm = bitmapData1->Scan0;
Byte* imagePointer1 = (Byte*)pbm->ToPointer();
double max = max(array);
double value;
for (i = 0; i < height; i++)
{
for (j = 0; j < width; j++)
{
value = array[i,j]/max;
col = Utilities::RainbowNumberToColor(value);
imagePointer1[0] = (Byte)col.B;
imagePointer1[1] = (Byte)col.G;
imagePointer1[2] = (Byte)col.R;
imagePointer1[3] = 255;
imagePointer1 += 4;
}
imagePointer1 += (bitmapData1->Stride - (bitmapData1->Width * 4));
}
output->UnlockBits(bitmapData1);
Points of Interest
VB.NET does not have pointer arithmetic or unsafe code. As a result, I had to use the read/write functions in the namespace "Marshal
" instead. This is somewhat slower than direct pointer arithmetics but still much faster than accessing the pixel object. The functions in the namespace Marshal
also help to translate code from C# or C++ which uses pointer arithmetics to VB.NET.
History
- 25th February, 2021: Initial version
- 27th February, 2021: C++/CLI example added