I need to use image processing with LockBits instead of GetPixel/SetPixel to decrease the processing time. But in the end it saves not all changes.
Steps to reproduce the problem:
Expected: they are equal, but actually they are not.
I noticed that (See output):
One more confusing thing: my initial test file is gray, but in HexEditor among the expected payload values we can see values "00 00". See here. What does that mean?
I have read and tried lots of LockBits tutorials on Stack Overflow, MSDN, CodeProject and other sites as well, but in the end it works the same way.
So, here is the code example. Code of MyGetByteArrayByImageFile
and UpdateAllBytes
comes directly from msdn article "How to: Use LockBits" (method MakeMoreBlue
) with small changes: hardcoded pixel format replaced and every byte is changing (instead of every 6th).
Content of Program.cs
:
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace Test
{
class Program
{
const int MagicNumber = 43;
static void Main(string[] args)
{
//Arrange
string containerFilePath = "d:/test-images/initial-5.bmp";
Bitmap containerImage = new Bitmap(containerFilePath);
//Act
Bitmap resultImage = UpdateAllBytes(containerImage);
string resultFilePath = "d:/test-result-images/result-5.bmp";
resultImage.Save(resultFilePath, ImageFormat.Bmp);
//Assert
var savedImage = new Bitmap(resultFilePath);
byte[] actualBytes = savedImage.MyGetByteArrayByImageFile(ImageLockMode.ReadOnly).Item1;
int count = 0;
for (int i = 0; i < actualBytes.Length; i++)
{
if (actualBytes[i] != MagicNumber)
{
count++;
Debug.WriteLine($"Index: {i}. Expected value: {MagicNumber}, but was: {actualBytes[i]};");
}
}
Debug.WriteLine($"Amount of different bytes: {count}");
}
private static Bitmap UpdateAllBytes(Bitmap bitmap)
{
Tuple<byte[], BitmapData> containerTuple = bitmap.MyGetByteArrayByImageFile(ImageLockMode.ReadWrite);
byte[] bytes = containerTuple.Item1;
BitmapData bitmapData = containerTuple.Item2;
// Manipulate the bitmap, such as changing all the values of every pixel in the bitmap
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = MagicNumber;
}
// Copy the RGB values back to the bitmap
Marshal.Copy(bytes, 0, bitmapData.Scan0, bytes.Length);
// Unlock the bits.
bitmap.UnlockBits(bitmapData);
return bitmap;
}
}
}
Content of ImageExtensions.cs
:
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace Test
{
public static class ImageExtensions
{
public static Tuple<byte[], BitmapData> MyGetByteArrayByImageFile(this Bitmap bmp, ImageLockMode imageLockMode = ImageLockMode.ReadWrite)
{
// Specify a pixel format.
PixelFormat pxf = bmp.PixelFormat;//PixelFormat.Format24bppRgb;
// Lock the bitmap's bits.
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, imageLockMode, pxf);
// Get the address of the first line.
IntPtr ptr = bmpData.Scan0;
// Declare an array to hold the bytes of the bitmap.
// int numBytes = bmp.Width * bmp.Height * 3;
int numBytes = bmpData.Stride * bmp.Height;
byte[] rgbValues = new byte[numBytes];
// Copy the RGB values into the array.
Marshal.Copy(ptr, rgbValues, 0, numBytes);
return new Tuple<byte[], BitmapData>(rgbValues, bmpData);
}
}
}
Output example:
Index: 162. Expected value: 43, but was: 0;
Index: 163. Expected value: 43, but was: 0;
Index: 326. Expected value: 43, but was: 0;
Index: 327. Expected value: 43, but was: 0;
Index: 490. Expected value: 43, but was: 0;
Index: 491. Expected value: 43, but was: 0;
Index: 654. Expected value: 43, but was: 0;
...
Index: 3606. Expected value: 43, but was: 0;
Index: 3607. Expected value: 43, but was: 0;
Index: 3770. Expected value: 43, but was: 0;
Index: 3771. Expected value: 43, but was: 0;
Amount of different bytes: 46
I'd appreciate if someone could explain why that's happening and how to fix that. Thank you very much.
Based on Joshua Webb answer I changed the code:
In file Program.cs
I have only changed method UpdateAllBytes
to use new extension method UpdateBitmapPayloadBytes
:
private static Bitmap UpdateAllBytes(Bitmap bitmap)
{
Tuple<byte[], BitmapData> containerTuple = bitmap.MyGetByteArrayByImageFile(ImageLockMode.ReadWrite);
byte[] bytes = containerTuple.Item1;
BitmapData bitmapData = containerTuple.Item2;
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = MagicNumber;
}
bitmap.UpdateBitmapPayloadBytes(bytes, bitmapData);
return bitmap;
}
ImageExtensions.cs:
public static class ImageExtensions
{
public static Tuple<byte[], BitmapData> MyGetByteArrayByImageFile(this Bitmap bmp, ImageLockMode imageLockMode = ImageLockMode.ReadWrite)
{
PixelFormat pxf = bmp.PixelFormat;
int depth = Image.GetPixelFormatSize(pxf);
CheckImageDepth(depth);
int bytesPerPixel = depth / 8;
// Lock the bitmap's bits.
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bitmapData = bmp.LockBits(rect, imageLockMode, pxf);
// Get the address of the first line.
IntPtr ptr = bitmapData.Scan0;
// Declare an array to hold the bytes of the bitmap.
int rowPayloadLength = bitmapData.Width * bytesPerPixel;
int payloadLength = rowPayloadLength * bmp.Height;
byte[] payloadValues = new byte[payloadLength];
// Copy the values into the array.
for (var r = 0; r < bmp.Height; r++)
{
Marshal.Copy(ptr, payloadValues, r * rowPayloadLength, rowPayloadLength);
ptr += bitmapData.Stride;
}
return new Tuple<byte[], BitmapData>(payloadValues, bitmapData);
}
public static void UpdateBitmapPayloadBytes(this Bitmap bmp, byte[] bytes, BitmapData bitmapData)
{
PixelFormat pxf = bmp.PixelFormat;
int depth = Image.GetPixelFormatSize(pxf);
CheckImageDepth(depth);
int bytesPerPixel = depth / 8;
IntPtr ptr = bitmapData.Scan0;
int rowPayloadLength = bitmapData.Width * bytesPerPixel;
if(bytes.Length != bmp.Height * rowPayloadLength)
{
//to prevent ArgumentOutOfRangeException in Marshal.Copy
throw new ArgumentException("Wrong bytes length.", nameof(bytes));
}
for (var r = 0; r < bmp.Height; r++)
{
Marshal.Copy(bytes, r * rowPayloadLength, ptr, rowPayloadLength);
ptr += bitmapData.Stride;
}
// Unlock the bits.
bmp.UnlockBits(bitmapData);
}
private static void CheckImageDepth(int depth)
{
//Because my task requires such restrictions
if (depth != 8 && depth != 24 && depth != 32)
{
throw new ArgumentException("Only 8, 24 and 32 bpp images are supported.");
}
}
}
This code passes tests for images:
In my task I don't have to care about which values I am changing, but if you need to calculate where exactly Red, Green, Blue values are, keep in mind that the bytes are in BGR order, as mentioned in the answer.
The only thing missing from your question is a sample input file, which based on your output I infer is a 54 x 23 pixel 24bpp bitmap.
The bytes that don't match the value you have set are padding bytes.
From Wikipedia:
For an image with a width of 54 pixels with 24 bits per pixel, this gives
Floor ((24 * 54 + 31) / 32) * 4
= Floor (1327 / 32) * 4
= 41 * 4
= 164
In this example, 164
is the value of the Stride
property, note that this is 2
bytes more than the raw RGB values you may expect of 54 * 3
= 162
.
This padding ensures that each row (line of pixels) starts on a multiple of 4
.
When I inspect the raw bytes of the file after it has been saved, on my machine all of the bytes match the magic number
using (var fs = new FileStream(resultFilePath, FileMode.Open))
{
// Skip the bitmap header
fs.Position = 54;
int rawCount = 0;
int i = 0;
int byteValue;
while ((byteValue = fs.ReadByte()) != -1)
{
if (byteValue != MagicNumber)
{
rawCount++;
Debug.WriteLine($"Index: {i}. Expected value: {MagicNumber}, but was: {byteValue};");
}
}
Debug.WriteLine($"Amount of different bytes: {rawCount}");
}
While, reading the bytes using your extension method yields the differing padding bytes (as 0
s).
If I then save the savedImage
to a new destination, and inspect the bytes of that, the padding bytes are now 0
.
I assume this is due to C# / .NET not actually reading the padding bytes back in when it loads the file.
These padding bytes are always at the end of each row, if you are manipulating the image pixel by pixel you can/should skip the last 0-3 bytes of each row depending on how many bits per pixel, and how wide, your image is as they do not represent valid pixels of your image anyway; better yet, calculate exactly where the Red
, Green
and Blue
offsets are going to be for each pixel you want to manipulate based on the PixelFormat
, Width
, and Height
of your image.
The MSDN sample you linked to is unfortunately misleading, their example image is of a width that happens to be cleanly divisible by 32
when multiplied by BitsPerPixel
(i.e. (24 * 100) / 32 = 75
) so there are no padding bytes required, and the issue doesn't manifest itself when copied verbatim.
Using a source image with different dimensions, however, reveals the issue. By adding 6
they are trying to move to blue value of every other pixel; the bytes are in BGR order so starting at 0
is fine, and adding 6
will skip Green0
, Red0
, Blue1
, Green1
, Red1
and land on the Blue2
value. This continues for the first row as expected, however, once it reaches the end of the row at 162
the +6
doesn't account for the padding bytes is now off by two, which means it is now updating the Green
value for every other pixel until it reaches the next set of padding bytes which will shift it into the Red
values, and so on.
Row
Col 0 0 1 2 3 4 5 6 7 8 ... 159 160 161 162 163
B0 G0 R0 B1 G1 R1 B2 G2 R2 B53 G53 R53 P P
1 164 165 166 167 168 169 ...
B54 G54 R54 B55 G56 B56
...
Starting with a white 54 x 23 pixel bitmap, setting "every other 6th byte" to 43
gives us this
for(int counter = 0; counter < bytes.Length; counter+=6)
bytes[counter] = MagicNumber;
Instead of this
for (var r = 0; r < bitmapData.Height; r++)
{
for (var c = 0; c < bitmapData.Width * 3; c += 6)
{
bytes[r * bitmapData.Stride + c] = MagicNumber;
}
}