'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.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source