出于学习目的在没有 Bitmap 类的情况下读取 BMP 文件,LockBits 问题(带有不安全块的 C#)

问题描述 投票:0回答:1

我有这样的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);
            }
        }
    }
}
c# image-processing bitmap unsafe
1个回答
0
投票

如果我理解正确的话,你只需要计算正确的步幅?
如果是这样,您无需致电

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 值(透明度)。
如果您提供的数据与地址边界(通常不是 4 字节)对齐,则计算机的工作效率非常高。将文件数据填充到该边界可以使数据处理速度更快,因为计算机“更容易”处理数据。填充还允许 BMP 文件格式支持各种不同的颜色空间。

BMP 像素颠倒了

当您第一次实现 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

© www.soinside.com 2019 - 2024. All rights reserved.