Search code examples
c#bitmapbmp.net-4.6lockbits

C# Processing bmp images using Bitmap LockBits. Trying to change ALL the bytes, but values of some of them after saving remain 0. Why?


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:

  • Read all the bytes from initial bmp file;
  • Change all these bytes and save it to a new file;
  • Read saved file and compare its data with data you tried to save.

Expected: they are equal, but actually they are not.

I noticed that (See output):

  • all the wrong values are equal to 0,
  • indexes of wrong values are like: x1, x1+1, x2, x2+1, ...,
  • for some files it can work as expected.

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.


Solution

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:

  • 8, 24, 32 bpp;
  • when padding bytes are required and not (for example, width = 63, bpp = 24 and width = 64, bpp = 24, ...).

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.


Solution

  • 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 0s).

    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;
    

    MSDN Example

    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;
        }
    }
    

    Correct Example