手动加载位图

Loading bitmap manually

我正在尝试编写一些 C# 代码,它将为磁盘上的 .bmp 命名。 为此,我一直在关注维基百科页面:BMP File Format

所以我创建了 2 个 类 来包含 header。 首先是文件 header :

    class BMPFileHeader
    {
        public BMPFileHeader(Byte[] headerBytes)
        {
            // Position
            int offset = 0;

            // Read 2 byes
            bfType = ((char)headerBytes[0]).ToString();
            bfType += ((char)headerBytes[1]).ToString();
            offset = offset + 2;
            // Read 4 bytes to uint32
            bfSize = BitConverter.ToUInt32(headerBytes, offset);
            offset = offset + sizeof(UInt32);

            // Read 2 bytes to uint16
            bfReserved1 = BitConverter.ToUInt16(headerBytes, offset);
            offset = offset + sizeof(UInt16);

            // Read 2 bytes to uint16
            bfReserved2 = BitConverter.ToUInt16(headerBytes, offset);
            offset = offset + sizeof(UInt16);

            // Read 4 bytes to uint32
            bfOffBits = BitConverter.ToUInt32(headerBytes, offset);
            offset = offset + sizeof(UInt32);
        }

        public string bfType;                      // Ascii characters "BM"
        public UInt32 bfSize;                      // The size of file in bytes
        public UInt16 bfReserved1;                 // Unused, must be zero
        public UInt16 bfReserved2;                 // Same ^^
        public UInt32 bfOffBits;                   // Pixel offset [ where pixel array starts ]
    }

这似乎工作得很好。所以继续下一个 header,图片 Header。 :

    class BMPImageHeader
    {
        public BMPImageHeader(Byte[] headerBytes)
        {
            // Position
            int offset = 0;

            biSize = BitConverter.ToUInt32(headerBytes, offset);
            offset = offset + sizeof(UInt32);

            biWidth = BitConverter.ToUInt32(headerBytes, offset);
            offset = offset + sizeof(UInt32);

            biHeight = BitConverter.ToUInt32(headerBytes, offset);
            offset = offset + sizeof(UInt32);

            biPlanes = BitConverter.ToUInt16(headerBytes, offset);
            offset = offset + sizeof(UInt16);

            biBitCount = BitConverter.ToUInt16(headerBytes, offset);
            offset = offset + sizeof(UInt16);

            biCompression = BitConverter.ToUInt32(headerBytes, offset);
            offset = offset + sizeof(UInt32);

            biSizeImage = BitConverter.ToUInt32(headerBytes, offset);
            offset = offset + sizeof(UInt32);

            XPelsPerMeter = BitConverter.ToUInt32(headerBytes, offset);
            offset = offset + sizeof(UInt32);

            YPelsPerMeter = BitConverter.ToUInt32(headerBytes, offset);
            offset = offset + sizeof(UInt32);

            biClrUsed = BitConverter.ToUInt32(headerBytes, offset);
            offset = offset + sizeof(UInt32);

            biClrImportant = BitConverter.ToUInt32(headerBytes, offset);
            offset = offset + sizeof(UInt32);
        }

        public UInt32 biSize;          // Size of header, must be least 40
        public UInt32 biWidth;         // Image width in pixels
        public UInt32 biHeight;        // Image height in pixels
        public UInt16 biPlanes;        // Must be 1
        public UInt16 biBitCount;      // Bits per pixels.. 1..4..8..16..32    
        public UInt32 biCompression;   // 0 No compression
        public UInt32 biSizeImage;     // Image size, may be zer for   uncompressed
        public UInt32 XPelsPerMeter;   // Preferred resolution in pixels per meter
        public UInt32 YPelsPerMeter;   // Same ^^
        public UInt32 biClrUsed;       // Number color map entries
        public UInt32 biClrImportant;  // Number of significant colors


    }

这似乎也有效..所以现在我有一些重要的信息要显示 放那张照片。

biBitCount = bits per pixel
biHeight = height in pixel
biWidth  = width in pixel
bfOffBit = Pixel array offset

所以我认为我是个聪明人。 我会开始将它们复制到我创建的包含 3 个字节的结构二维数组中。红色、绿色和蓝色。 因此,将文件的所有其余部分读入缓冲区。并且运行一次通过该缓冲区3个字节,并添加一个由这3个字节组成的像素。

    public ImageManipulation(string _name, string _imageLocation)
    {
        // Set name
        Name = _name;

        // Initialize classes
        //fHeader = new BMPFileHeader();
        //iHeader = new BMPImageHeader();

        if (File.Exists(_imageLocation))
        {
            int offset = 0;
            Byte[] fsBuffer;
            FileStream fs = File.Open(_imageLocation, FileMode.Open, FileAccess.Read);

            // Start by reading file header..
            fsBuffer = new Byte[14]; // size 40 bytes
            fs.Read(fsBuffer, 0, 14);
            fHeader = new BMPFileHeader(fsBuffer);

            // Then image header, 40 bytes
            fsBuffer = new Byte[40];
            fs.Read(fsBuffer, 0, 40);
            iHeader = new BMPImageHeader(fsBuffer);

            // Apply pixels
            Pixels = new RGBPixel[iHeader.biHeight, iHeader.biWidth];

            // How many bytes per pixel
            int bpi = iHeader.biBitCount/8;

            // Read pixel array
            long totalBytes = iHeader.biWidth*iHeader.biHeight*bpi;

            if (totalBytes == iHeader.biSizeImage) ;
            if (iHeader.biSizeImage == ( fHeader.bfSize - (iHeader.biSize + 14))) ;

            // Create butter, read data
            fsBuffer = new Byte[iHeader.biWidth*iHeader.biHeight*bpi];
            fs.Read(fsBuffer, 0, (int)fHeader.bfOffBits);

            int RowSize = ((iHeader.biBitCount *(int) iHeader.biWidth + 31) / 32) * 4;


            int x, y;
            x = y = 0;

            long i;
            int lcounter = 0;

            // This reads 3 bytes a time 
            for (i = 0; i < totalBytes; i = i + 3)
            {
                    // Read 3 bytes
                    Pixels[ (iHeader.biHeight-1) - y, x].RED = fsBuffer[i];
                    Pixels[ (iHeader.biHeight-1) - y, x].GREEN = fsBuffer[i + 1];
                    Pixels[ (iHeader.biHeight-1) - y, x].BLUE = fsBuffer[i + 2];

                    // Update position in array
                    x++;
                    if (x == iHeader.biWidth)
                    {
                        y++;
                        x = 0;
                    }
            }
        }
    }   

我在 运行 编译和编译此代码时没有收到任何错误,但是我之后使用 Bitmap.setPixel() 创建的图像几乎只是黑色。所以我以某种方式读取了像素错误,但我无法确定为什么?

我用来测试的图片是

我得到的是:

谢谢 感谢任何帮助

不清楚为什么您认为位图中的像素是以您描述的方式存储的。每六个字节的像素数据之间填充两个字节?

扫描线内的像素之间完全没有填充。如果你有一个 24bpp 的图像,那么像素一次存储三个字节,在一个连续的 three-byte 像素数组中。

也许您混淆了 per-row 填充?整个扫描线 仅在最后 填充为 4 字节(32 位)的倍数。通常,处理此问题的方法是计算位图的 "stride",这只是单个扫描线字节长度的另一种说法。然后,您将在位图中的 xy 坐标上循环,根据 y 值和步幅重新计算每个新行的字节偏移量。

在您引用的维基百科文章中,这在 "Pixel Storage" 的标题下进行了说明。

计算步幅后,实际代码通常如下所示(对于 24bpp):

byte[] buffer = ...;

for (int y = 0, ibRow = 0; y < height; y++, ibRow += stride)
{
    for (int x = 0, ibPixel = ibRow; x < width; x++, ibPixel += 3)
    {
        byte red = buffer[ibPixel],
             green = buffer[ibPixel + 1],
             blue = buffer[ibPixel + 2];

        // do something with pixel data
    }
}

当然,您必须查看位图 header 信息才能正确确定诸如实际 bits-per-pixel 之类的东西(在这种情况下每个像素 3 个字节,因此 ibPixel += 3),和组件顺序(像素字节并不总是 red-green-blue...另一个常见顺序是 blue-green-red)。


综上所述,还有一个问题是您为什么要尝试实现这一点。 .NET 已经有各​​种 类 允许您从文件加载位图图像,甚至可以在需要时访问原始像素数据。为什么要重新发明轮子?

如果只是作为学术练习,那很好,但如果这是用于 real-world 代码,我强烈建议从 .NET 中现有的位图支持 类 之一开始。如果您需要直接访问像素数据,您仍然需要关心实际像素格式、步幅等问题,但位图支持 类 提供了比尝试解释更方便的访问方式直接文件数据本身,并且在任何情况下都会为您提供完全可用的 图像 object。

感谢@Peter_Duniho 向我指出了正确的方法,我对他为我清理的填充感到困惑。

我的错误是非常简单的错误,在 fs.Read() 中我没有告诉它读取图像的大小,而是让它读取偏移量。