C# 使用 Bitmap LockBits 处理 bmp 图像。尝试更改所有字节,但其中一些字节的值在保存后仍然为 0。为什么?
C# Processing bmp images using Bitmap LockBits. Trying to change ALL the bytes, but values of some of them after saving remain 0. Why?
我需要使用带 LockBits 的图像处理而不是 GetPixel/SetPixel 来减少处理时间。 但最终并没有保存所有的更改。
重现问题的步骤:
- 从初始bmp文件中读取所有字节;
- 更改所有这些字节并将其保存到新文件;
- 读取保存的文件并将其数据与您尝试保存的数据进行比较。
预期:它们是相等的,但实际上它们不是。
我注意到(查看输出):
- 所有错误的值都等于0,
- 错误值的索引如下:x1, x1+1, x2, x2+1, ...,
- 对于某些文件,它可以按预期工作。
一件更令人困惑的事情:我的初始测试文件是灰色的,但在 HexEditor 中,我们可以在预期的有效负载值中看到值“00 00”。参见 here。
这是什么意思?
我也在 Stack Overflow、MSDN、CodeProject 和其他网站上阅读并尝试了很多 LockBits 教程,但最终它的工作方式是一样的。
所以,这是代码示例。 MyGetByteArrayByImageFile
和 UpdateAllBytes
的代码直接来自 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
如果有人能解释为什么会这样以及如何解决这个问题,我将不胜感激。非常感谢。
解决方案
基于我修改了代码:
在文件 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 位的图像,这给出
Floor ((24 * 54 + 31) / 32) * 4
= Floor (1327 / 32) * 4
= 41 * 4
= 164
在此示例中,164
是 Stride
属性 的值,请注意,这比您可能期望的原始 RGB 值多 2
个字节54 * 3
= 162
.
此填充确保每一行(像素行)开始于 4
.
的倍数
当我检查文件保存后的原始字节时,在我的机器上所有字节都匹配幻数
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}");
}
同时,使用您的扩展方法读取字节会产生不同的填充字节(如 0
s)。
如果我随后将 savedImage
保存到新目的地,并检查其字节,则填充字节现在为 0
。
我认为这是由于 C#/.NET 在加载文件时实际上没有读回填充字节。
这些填充字节总是在每行的末尾,如果您逐像素处理图像,您 can/should 根据每个像素的位数跳过每行的最后 0-3 个字节,以及您的图像有多宽,因为它们无论如何都不代表图像的有效像素;更好的是,根据 PixelFormat
、Width
准确计算要处理的每个像素的 Red
、Green
和 Blue
偏移量, 和 Height
你的图片。
不幸的是,您链接到的 MSDN 示例具有误导性,他们的示例图像的宽度恰好在乘以 BitsPerPixel
时可以被 32
整除(即 (24 * 100) / 32 = 75
)因此不需要填充字节,并且逐字复制时问题不会出现。
但是,使用不同尺寸的源图像揭示了这个问题。通过添加 6
他们试图移动到每隔一个像素的蓝色值;字节按 BGR 顺序排列,因此从 0
开始很好,添加 6
将跳过 Green0
、Red0
、Blue1
、Green1
, Red1
并落在 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 像素位图开始,将 "every other 6th byte" 设置为 43
给我们这个
for(int counter = 0; counter < bytes.Length; counter+=6)
bytes[counter] = MagicNumber;
而不是这个
for (var r = 0; r < bitmapData.Height; r++)
{
for (var c = 0; c < bitmapData.Width * 3; c += 6)
{
bytes[r * bitmapData.Stride + c] = MagicNumber;
}
}
我需要使用带 LockBits 的图像处理而不是 GetPixel/SetPixel 来减少处理时间。 但最终并没有保存所有的更改。
重现问题的步骤:
- 从初始bmp文件中读取所有字节;
- 更改所有这些字节并将其保存到新文件;
- 读取保存的文件并将其数据与您尝试保存的数据进行比较。
预期:它们是相等的,但实际上它们不是。
我注意到(查看输出):
- 所有错误的值都等于0,
- 错误值的索引如下:x1, x1+1, x2, x2+1, ...,
- 对于某些文件,它可以按预期工作。
一件更令人困惑的事情:我的初始测试文件是灰色的,但在 HexEditor 中,我们可以在预期的有效负载值中看到值“00 00”。参见 here。 这是什么意思?
我也在 Stack Overflow、MSDN、CodeProject 和其他网站上阅读并尝试了很多 LockBits 教程,但最终它的工作方式是一样的。
所以,这是代码示例。 MyGetByteArrayByImageFile
和 UpdateAllBytes
的代码直接来自 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
如果有人能解释为什么会这样以及如何解决这个问题,我将不胜感激。非常感谢。
解决方案
基于
在文件 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 位的图像,这给出
Floor ((24 * 54 + 31) / 32) * 4
= Floor (1327 / 32) * 4
= 41 * 4
= 164
在此示例中,164
是 Stride
属性 的值,请注意,这比您可能期望的原始 RGB 值多 2
个字节54 * 3
= 162
.
此填充确保每一行(像素行)开始于 4
.
当我检查文件保存后的原始字节时,在我的机器上所有字节都匹配幻数
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}");
}
同时,使用您的扩展方法读取字节会产生不同的填充字节(如 0
s)。
如果我随后将 savedImage
保存到新目的地,并检查其字节,则填充字节现在为 0
。
我认为这是由于 C#/.NET 在加载文件时实际上没有读回填充字节。
这些填充字节总是在每行的末尾,如果您逐像素处理图像,您 can/should 根据每个像素的位数跳过每行的最后 0-3 个字节,以及您的图像有多宽,因为它们无论如何都不代表图像的有效像素;更好的是,根据 PixelFormat
、Width
准确计算要处理的每个像素的 Red
、Green
和 Blue
偏移量, 和 Height
你的图片。
不幸的是,您链接到的 MSDN 示例具有误导性,他们的示例图像的宽度恰好在乘以 BitsPerPixel
时可以被 32
整除(即 (24 * 100) / 32 = 75
)因此不需要填充字节,并且逐字复制时问题不会出现。
但是,使用不同尺寸的源图像揭示了这个问题。通过添加 6
他们试图移动到每隔一个像素的蓝色值;字节按 BGR 顺序排列,因此从 0
开始很好,添加 6
将跳过 Green0
、Red0
、Blue1
、Green1
, Red1
并落在 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 像素位图开始,将 "every other 6th byte" 设置为 43
给我们这个
for(int counter = 0; counter < bytes.Length; counter+=6)
bytes[counter] = MagicNumber;
而不是这个
for (var r = 0; r < bitmapData.Height; r++)
{
for (var c = 0; c < bitmapData.Width * 3; c += 6)
{
bytes[r * bitmapData.Stride + c] = MagicNumber;
}
}