C# 使用位图 LockBits 处理 bmp 图像。尝试更改所有字节,但保存后其中一些字节的值仍然为 0。为什么?

标签 c# bitmap bmp .net-4.6 lockbits

我需要使用 LockBits 进行图像处理,而不是 GetPixel/SetPixel 来减少处理时间。 但最终它并没有保存所有更改。

重现问题的步骤:

  • 从初始bmp文件中读取所有字节;
  • 更改所有这些字节并将其保存到新文件中;
  • 读取保存的文件并将其数据与您尝试保存的数据进行比较。

预期:它们是相等的,但实际上并不相等。

我注意到(参见输出):

  • 所有错误的值都等于 0,
  • 错误值的索引如下:x1, x1+1, x2, x2+1, ...,
  • 对于某些文件,它可以按预期工作。

还有一个令人困惑的事情:我的初始测试文件是灰色的,但在 HexEditor 中的预期有效负载值中我们可以看到值“00 00”。请参阅here这是什么意思?

我也在 Stack Overflow、MSDN、CodeProject 和其他网站上阅读并尝试了很多 LockBits 教程,但最终效果是一样的。

所以,这是代码示例。 MyGetByteArrayByImageFileUpdateAllBytes的代码直接来自msdn文章"How to: Use LockBits" (方法 MakeMoreBlue)有一些小变化:硬编码像素格式被替换,每个字节都在变化(而不是每 6 个字节)。

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

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

输出示例:

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

如果有人能解释为什么会发生这种情况以及如何解决这个问题,我将不胜感激。非常感谢。


解决方案

基于Joshua Webb answer我更改了代码:

在文件 Program.cs 中,我仅更改了方法 UpdateAllBytes 以使用新的扩展方法 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.");
        }
    }
}

此代码通过了图像测试:

  • 8、24、32 bpp;
  • 当需要而不需要填充字节时(例如, 宽度 = 63,bpp = 24 和宽度 = 64,bpp = 24,...)。

在我的任务中,我不必关心我正在更改哪些值,但如果您需要计算红色、绿色、蓝色值的确切位置,请记住字节按 BGR 顺序排列,如中所述答案。

最佳答案

您的问题中唯一缺少的是示例输入文件,根据您的输出,我推断该文件是 54 x 23 像素 24bpp 位图。

与您设置的值不匹配的字节是填充字节。

来自Wikipedia :

对于宽度为 54 像素、每像素 24 位的图像,这给出 楼层 ((24 * 54 + 31)/32) * 4 = 楼层 (1327/32) * 4 = 41 * 4 = 164

在此示例中,164Stride 属性的值,请注意,这比您的原始 RGB 值多 2 个字节。可能期望 54 * 3 = 162

此填充可确保每行(像素线)以 4 的倍数开始。

当我在保存文件后检查文件的原始字节时,在我的计算机上,所有字节都与魔数(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}");
}

同时,使用扩展方法读取字节会产生不同的填充字节(如 0s)。

如果我随后将 savedImage 保存到新目标,并检查其字节,则填充字节现在为 0

我认为这是由于 C#/.NET 在加载文件时实际上没有读回填充字节。

这些填充字节始终位于每行的末尾,如果您逐像素操作图像,您可以/应该跳过每行的最后 0-3 个字节,具体取决于每个像素的位数和宽度,您的图像无论如何都不代表您图像的有效像素;更好的是,根据 准确计算要操作的每个像素的 RedGreenBlue 偏移量图像的 >PixelFormatWidthHeight

不幸的是,您链接到的 MSDN 示例具有误导性,它们的示例图像的宽度在乘以 BitsPerPixel 时恰好可以被 32 整除(即 >(24 * 100)/32 = 75),因此不需要填充字节,并且逐字复制时问题不会显现出来。

然而,使用不同尺寸的源图像会揭示这个问题。通过添加 6,他们试图移动到每隔一个像素的蓝色值;字节按 BGR 顺序排列,因此从 0 开始就可以,添加 6 将跳过 Green0Red0Blue1Green1Red1 并落在 Blue2 值上。正如预期的那样,第一行继续如此,但是,一旦到达 162 处的行末尾,+6 就不再考虑填充字节了。两个,这意味着它现在正在更新每个其他像素的Green值,直到它到达下一组填充字节,这会将其转换为Red值,依此类推.

     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

      ...

从白色 54 x 23 像素位图开始,将“每隔 6 个字节”设置为 43 即可得到此结果

for(int counter = 0; counter < bytes.Length; counter+=6)
    bytes[counter] = MagicNumber;

MSDN Example

而不是这个

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

关于C# 使用位图 LockBits 处理 bmp 图像。尝试更改所有字节,但保存后其中一些字节的值仍然为 0。为什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51565842/

相关文章:

android - 以编程方式更改小部件的渐变背景

file-format - 位图文件头大小

android - setXfermode(new PorterDuffXfermode(Mode.SRC_OUT)) 给出错误

c# - 从没有丑陋的 "Module"后缀的 C# 调用 F# 函数

c# - 如何在.NET Core中的自定义内容赘述格式化程序中正常抛出异常

c# - 初学者 - C# 遍历目录以生成文件列表

在 C 中创建简单的位图(没有外部库)

c++ - 读取bmp文件中的像素值

c++ - 如何在 GLUT 上加载 bmp 以将其用作纹理?

c# - 字典如果键存在追加如果不添加新元素C#