我有这样的Windows Forms项目,它读取24位RGB BMP文件。除了打印位图形成的方法之外,一切都工作正常。有一行带注释的代码,它完全取代了该行下面的整个循环,但正如注释所述,它仅在文件中的行没有填充时才有效(例如,当我尝试加载 1920x1080 的桌面屏幕截图时)。但是,当我尝试打开行大小不能被四整除的任何文件时,图像看起来很奇怪 - 它变成灰色,并且像素沿对角线移动。所以问题是,我将指针分配给数组时做错了什么,为什么它在某些情况下有效?另外,还有一些改进代码的技巧值得赞赏。谢谢!
public sealed unsafe partial class Form1 : Form
{
private byte[,,] imageData;
private BMPHeader header;
private readonly FieldInfo[] headerFields;
private readonly int headerSize;
public Form1()
{
InitializeComponent();
panel.Paint += Panel_Paint;
headerSize = sizeof(BMPHeader);
headerFields = typeof(BMPHeader).GetFields();
}
private void openFileButton_Click(object sender, EventArgs e)
{
using var openFileDialog = new OpenFileDialog()
{
InitialDirectory = Environment.CurrentDirectory,
};
var dialogResult = openFileDialog.ShowDialog();
if (dialogResult is DialogResult.OK)
{
ParseFile(openFileDialog.FileName);
}
}
private void ParseFile(string filename)
{
using var fileStream = new FileStream(filename, FileMode.Open);
if (fileStream.Length <= headerSize)
{
MessageBox.Show("Incorrect file format");
return;
}
Span<byte> headerBuffer = stackalloc byte[headerSize];
fileStream.ReadExactly(headerBuffer);
fixed (byte* ptr = headerBuffer)
{
header = *(BMPHeader*)ptr;
}
foreach (var field in headerFields)
{
fileInfo.AppendText($"{field.Name}: {field.GetValue(header)}{Environment.NewLine}");
}
var pixelSize = header.bpp / 8;
var rowSize = (int)Math.Ceiling((double)header.bpp * header.width / 32) * 4;
var dataSizeCalculated = rowSize * header.height;
var dataSizeFromHeader = header.image_size;
var dataSizeStreamInfo = fileStream.Length - headerSize;
if (dataSizeCalculated != dataSizeFromHeader || dataSizeFromHeader != dataSizeStreamInfo)
{
MessageBox.Show("Incorrect file format");
fileInfo.Clear();
return;
}
imageData = new byte[header.height, header.width, pixelSize];
fixed (byte* ptr = imageData)
{
for (int i = header.height - 1; i >= 0; i--)
{
var rowPtr = new Span<byte>(ptr + (i * header.width * pixelSize), rowSize);
fileStream.ReadExactly(rowPtr);
}
}
panel.Width = header.width;
panel.Height = header.height;
panel.Invalidate();
}
private void transformGrayButton_Click(object sender, EventArgs e)
{
Parallel.For(0, header.height, (i) =>
{
for (int j = 0; j < header.width; j++)
{
var grayPixel = 0.299 * imageData[i, j, 0] + 0.587 * imageData[i, j, 1] + 0.114 * imageData[i, j, 2];
for (int c = 0; c < 3; c++)
imageData[i, j, c] = (byte)grayPixel;
}
});
panel.Invalidate();
}
private void createBorderButton_Click(object sender, EventArgs e)
{
const int BorderSize = 15;
if (header.height < BorderSize * 2 || header.width < BorderSize * 2)
return;
var rnd = Random.Shared;
var color = Color.FromArgb(rnd.Next(0, 255), rnd.Next(0, 255), rnd.Next(0, 255));
for (int i = 0; i < BorderSize; i++)
{
for (int j = 0; j < header.width; j++)
{
SetPixel(i, j, color);
SetPixel(header.height - i - 1, j, color);
}
for (int j = BorderSize; j < header.height - BorderSize; j++)
{
SetPixel(j, i, color);
SetPixel(j, header.width - i - 1, color);
}
}
panel.Invalidate();
}
private void SetPixel(int x, int y, Color color)
{
imageData[x, y, 0] = color.B;
imageData[x, y, 1] = color.G;
imageData[x, y, 2] = color.R;
}
private void Panel_Paint(object sender, PaintEventArgs e)
{
if (imageData is null)
return;
using var bmp = new Bitmap(header.width, header.height);
var bits = bmp.LockBits(new Rectangle(0, 0, header.width, header.height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
fixed (byte* ptr = imageData)
{
//bits.Scan0 = (nint)ptr; // works only when rows have no padding
for (int i = 0; i < header.height; i++)
{
byte* destRow = (byte*)bits.Scan0 + (i * bits.Stride);
byte* sourceRow = ptr + (i * header.width * 3);
Buffer.MemoryCopy(sourceRow, destRow, header.width * 3, header.width * 3);
}
}
bmp.UnlockBits(bits);
e.Graphics.DrawImage(bmp, 0, 0);
}
}
编辑1:尝试在paint中创建另一张图像,保存为24bppRGB bmp格式,大小为27x27。程序直接关闭,没有任何异常或
bits.UnlockBits
之后的情况。另外,尝试调试,不确定在哪里可以看到这些类背后发生的事情
编辑2:这是我正在使用的标题
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct BMPHeader
{
public ushort type;
public uint size;
public ushort reserved1;
public ushort reserved2;
public uint offset;
public uint dib_size;
public int width;
public int height;
public ushort planes;
public ushort bpp;
public uint compression;
public uint image_size;
public uint x_ppm;
public uint y_ppm;
public uint num_colors;
public uint important_colors;
};
编辑3:我现在尝试用此代码写回打开的文件(看起来类似于读取):
var writeFs = new FileStream("testWrite.bmp", FileMode.Create, FileAccess.ReadWrite);
writeFs.Write(headerBuffer);
fixed (byte* ptr = imageData)
{
for (int i = header.height - 1; i >= 0; i--)
{
var rowPtr = new Span<byte>(ptr + (i * header.width * pixelSize), rowSize);
writeFs.Write(rowPtr);
}
}
writeFs.Close();
新文件已在 Windows 查看应用程序中正确打开。
编辑4:我在寻找失败方面取得了进展。这是一个 3D 数组,仅使用 HxWx3 尺寸。可能没有办法使用 3D 数组来考虑填充,并且锯齿状 3D 也不是一个选项,因为维度不会在内存中对齐以与指针一起操作,所以我将其更改为 2D 数组。填充和未填充的图像现在都可以正确打开。但是,应用程序静默关闭的问题仍然存在。它仅发生在小尺寸位图(大约 150x150 及以下)中,没有发生异常或其他情况,应用程序在处理
bits.UnlockBits
行(我已经在编辑 1 中提到)后关闭,更大的图像打开没有问题。这是更新后的代码:
public sealed unsafe partial class Form1 : Form
{
private byte[,] imageData;
private BMPHeader header;
private readonly FieldInfo[] headerFields;
private readonly int headerSize;
public Form1()
{
InitializeComponent();
panel.Paint += Panel_Paint;
headerSize = sizeof(BMPHeader);
headerFields = typeof(BMPHeader).GetFields();
}
private void openFileButton_Click(object sender, EventArgs e)
{
using var openFileDialog = new OpenFileDialog()
{
InitialDirectory = Environment.CurrentDirectory,
};
var dialogResult = openFileDialog.ShowDialog();
if (dialogResult is DialogResult.OK)
{
ParseFile(openFileDialog.FileName);
}
}
private void ParseFile(string filename)
{
using var fileStream = new FileStream(filename, FileMode.Open);
if (fileStream.Length <= headerSize)
{
return;
}
Span<byte> headerBuffer = stackalloc byte[headerSize];
fileStream.ReadExactly(headerBuffer);
fixed (byte* ptr = headerBuffer)
{
header = *(BMPHeader*)ptr;
}
foreach (var field in headerFields)
{
fileInfo.AppendText($"{field.Name}: {field.GetValue(header)}{Environment.NewLine}");
}
var pixelSize = header.bpp / 8;
var rowSize = (int)Math.Ceiling((double)header.bpp * header.width / 32) * 4;
var dataSizeCalculated = rowSize * header.height;
var dataSizeFromHeader = header.image_size;
var dataSizeStreamInfo = fileStream.Length - headerSize;
if (dataSizeCalculated != dataSizeFromHeader || dataSizeFromHeader != dataSizeStreamInfo)
{
fileInfo.Clear();
return;
}
imageData = new byte[header.height, rowSize]; //array is now 2D
fixed (byte* ptr = imageData)
{
for (int i = header.height - 1; i >= 0; i--)
{
var rowPtr = new Span<byte>(ptr + i * rowSize, rowSize); //row pointer is based on rowSize now
fileStream.ReadExactly(rowPtr);
}
}
panel.Width = header.width;
panel.Height = header.height;
panel.Invalidate();
}
private void transformGrayButton_Click(object sender, EventArgs e)
{
Parallel.For(0, header.height, (i) =>
{
for (int j = 0; j < header.width; j++)
{
var grayPixel = 0.299 * imageData[i, j * 3] + 0.587 * imageData[i, j * 3 + 1] + 0.114 * imageData[i, j * 3 + 2]; //fixed here
for (int c = 0; c < 3; c++)
imageData[i, j * 3 + c] = (byte)grayPixel;
}
});
panel.Invalidate();
}
private void createBorderButton_Click(object sender, EventArgs e)
{
const int BorderSize = 15;
if (header.height < BorderSize * 2 || header.width < BorderSize * 2)
return;
var rnd = Random.Shared;
var color = Color.FromArgb(rnd.Next(0, 255), rnd.Next(0, 255), rnd.Next(0, 255));
for (int i = 0; i < BorderSize; i++)
{
for (int j = 0; j < header.width; j++)
{
SetPixel(i, j, color);
SetPixel(header.height - i - 1, j, color);
}
for (int j = BorderSize; j < header.height - BorderSize; j++)
{
SetPixel(j, i, color);
SetPixel(j, header.width - i - 1, color);
}
}
panel.Invalidate();
}
private void SetPixel(int x, int y, Color color)
{
imageData[x, y * 3] = color.B; //fixed here
imageData[x, y * 3 + 1] = color.G;
imageData[x, y * 3 + 2] = color.R;
}
private void Panel_Paint(object sender, PaintEventArgs e)
{
if (imageData is null)
return;
using var bmp = new Bitmap(header.width, header.height);
var bits = bmp.LockBits(new Rectangle(0, 0, header.width, header.height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
fixed (byte* ptr = imageData)
{
try
{
bits.Scan0 = (nint)ptr;
bmp.UnlockBits(bits);
e.Graphics.DrawImage(bmp, 0, 0);
}
catch (Exception ex)
{
// doesn't fall here
MessageBox.Show(ex.Message);
}
}
}
}
如果我理解正确的话,你只需要计算正确的步幅?
如果是这样,您无需致电
Math
。
uint stride = (((header.biWidth * header.biBitPerPixel + 31) / 32) * 4);
位图是 Microsoft 创建的一种文件格式,仅适用于
WORD
和 DWORD
类型。 WORD
只是 16 位数据类型,DWORD
只是 32 位数据类型。两者都用于存储不需要特定类型的任何内容。位图的像素存储在 DWORDS 中,因此需要填充到 32 位。
现在,
stride
是什么?
步长描述了存储图像的单行所需的字节数。一个像素可以是黑白的,只需要一个字节,但它也可以是 RGB,需要 3 个字节,或者,对于 PNG 文件来说,也可以是类似 ARGB 的,需要 4 个字节。第 4 个字节用于存储 alpha 值(透明度)。当您第一次实现 BMP 解析器时,您可能会注意到像素数据旋转了 180 度,这是为什么?
是为了兼容OS/2
在 OS/2 中 演示管理器 y 轴从屏幕左下角开始,这意味着 y 向屏幕顶部增加。虽然在数学上是正确的,但我不知道有任何其他操作系统这样做。所有其他似乎都从左上角开始 y 轴,其中 y 向屏幕底部增加。
结论 您需要旋转像素数据、旋转输入,或者只是从上到下读取数据并从下到上写入数据。
这是我最初用 C 语言编写的基本实现,所以这是一个快速而肮脏的翻译。
[StructLayout(LayoutKind.Explicit)]
public struct BITMAPHEADER
{
public const int BITMAP_SIG = 0x4d42;
public const int BFH_SIG = 0x0000;
public const int BFH_FILESIZE = 0x0002;
public const int BFH_RESERVED = 0x0006;
public const int BFH_DATAOFFSET = 0x000a;
public const int BFH_BYTECOUNT = 0x000e;
public const int BFH_HEADERSIZE = BFH_BYTECOUNT * sizeof(byte);
public const int BIH_SIZE = 0x000e;
public const int BIH_WIDTH = 0x0012;
public const int BIH_HEIGHT = 0x0016;
public const int BIH_PLANES = 0x001a;
public const int BIH_BPP = 0x001c;
public const int BIH_COMPRESSION = 0x001e;
public const int BIH_IMAGESIZE = 0x0022;
public const int BIH_XPIXELSPERM = 0x0026;
public const int BIH_YPIXELSPERM = 0x002a;
public const int BIH_COLORSUSED = 0x002e;
public const int BIH_IMPORTANTCOLORS = 0x0032;
public const int BIH_BYTECOUNT = 0x0028;
public const int BIH_HEADERSIZE = BIH_BYTECOUNT * sizeof(byte);
[FieldOffset(BFH_SIG)]
public UInt16 bfSig;
[FieldOffset(BFH_FILESIZE)]
public UInt32 bfSize;
[FieldOffset(BFH_RESERVED)]
public UInt32 bfReserved;
[FieldOffset(BFH_DATAOFFSET)]
public UInt32 bfDataOffset;
[FieldOffset(BIH_SIZE)]
public UInt32 biSize;
[FieldOffset(BIH_WIDTH)]
public UInt32 biWidth;
[FieldOffset(BIH_HEIGHT)]
public UInt32 biHeight;
[FieldOffset(BIH_PLANES)]
public UInt16 biPlanes;
[FieldOffset(BIH_BPP)]
public UInt16 biBitPerPixel;
[FieldOffset(BIH_COMPRESSION)]
public UInt32 biCompression;
[FieldOffset(BIH_IMAGESIZE)]
public UInt32 biImageSize;
[FieldOffset(BIH_XPIXELSPERM)]
public UInt32 biXpixelsPerM;
[FieldOffset(BIH_YPIXELSPERM)]
public UInt32 biYpixelsPerM;
[FieldOffset(BIH_COLORSUSED)]
public UInt32 biColorsUsed;
[FieldOffset(BIH_IMPORTANTCOLORS)]
public UInt32 biImportantColors;
}
public static class BitmapHeaderParser
{
private static BITMAPHEADER BufferToHeader(byte[] buffer)
{
GCHandle gchHeader = GCHandle.Alloc(buffer, GCHandleType.Pinned);
BITMAPHEADER hdr = Marshal.PtrToStructure<BITMAPHEADER>(gchHeader.AddrOfPinnedObject());
gchHeader.Free();
return hdr;
}
public static BITMAPHEADER GetHeader(string fileName)
{
byte[] buffer = new byte[0];
using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
buffer = new byte[BITMAPHEADER.BFH_HEADERSIZE + BITMAPHEADER.BIH_HEADERSIZE];
stream.Read(buffer, 0, buffer.Length);
}
BITMAPHEADER header = default(BITMAPHEADER);
try {
header = BufferToHeader(buffer);
}
catch {
header = default(BITMAPHEADER);
}
if (header.bfSig != BITMAPHEADER.BITMAP_SIG) {
throw new InvalidDataException("Invalid header signature");
}
return header;
}
public static byte[][] LoadBitmapPixel(string fileName, BITMAPHEADER header)
{
uint h = header.biHeight;
uint stride = (((header.biWidth * header.biBitPerPixel + 31) / 32) * 4);
byte[][] pixels = new byte[h][];
using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
stream.Seek(header.bfDataOffset, SeekOrigin.Begin);
for (uint y = 0, ydest = h - 1; y < h; y++, ydest--)
{
System.Diagnostics.Debug.WriteLine($"{y} - {ydest}");
pixels[ydest] = new byte[stride];
stream.Read(pixels[ydest], 0, (int)stride);
}
System.Diagnostics.Debug.WriteLine(stream.Position);
}
return pixels;
}
}
我很懒,所以这里是在控制台中将像素写为 ASCII 的示例:
static void Main(string[] args)
{
var header = BitmapHeaderParser.GetHeader(@"C:\Users\user\pictures\bmp_24.bmp");
var px = BitmapHeaderParser.LoadBitmapPixel(@"C:\Users\user\pictures\bmp_24.bmp", header);
int xIncrement = header.biBitPerPixel / 8;
for (int y = 0; y < header.biHeight; y++)
{
for (int x = 0; x < px[y].Length; x+=xIncrement)
{
float avg = (
px[y][x + 2] * 0.299f +
px[y][x + 1] * 0.587f +
px[y][x + 0] * 0.114f
);
if (avg > 120) {
Console.Write("@");
}
else if (avg < 120 && avg > 70) {
Console.Write("o");
}
else {
Console.Write(".");
}
}
Console.WriteLine("");
}
Console.ReadLine();
}
我使用此图像来测试翻译后的代码:https://people.math.sc.edu/Burkardt/data/bmp/bmp_24.bmp