Turning int array into BMP in C#

为君一笑 提交于 2020-05-15 08:52:50

问题


I'm having problems converting a grayscale array of ints (int32[,]) into BMP format in C#. I tried cycling through the array to set pixel color in the BMP, it does work but it ends up being really slow and practically unusable. I did a lot of googling but I cannot find the answer to my question. I need to put that image in a PictureBox in real time so the method needs to be fast.

Relevant discussion here

Edit: the array is 8bit depth but stored as int32

Edit2: Just found this code

    private unsafe Task<Bitmap> BitmapFromArray(Int32[,] pixels, int width, int height)
    {
        return Task.Run(() =>
         {
             Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb);
             BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
             for (int y = 0; y < height; y++)
             {
                 byte* row = (byte*)bitmapData.Scan0 + bitmapData.Stride * y;
                 for (int x = 0; x < width; x++)
                 {
                     byte grayShade8bit = (byte)(pixels[x, y] >> 4);
                     row[x * 3 + 0] = grayShade8bit;
                     row[x * 3 + 1] = grayShade8bit;
                     row[x * 3 + 2] = grayShade8bit;
                 }
             }
             bitmap.UnlockBits(bitmapData);
             return bitmap;
         });
    }

Seems to work fast enough but the image is almost black. If I remove the top of the camera the Image should be completely white but it just displays a really dark grey. I guess it's interpreting the pixel value as 32bit, not 8bit. Then tried to cast (ushort)pixels[x, y] but doesn't work


回答1:


I actually wrote a universally usable BuildImagefunction here on SO to build an image out of a byte array, but of course, you're not starting from a byte array, you're starting from a two-dimensional Int32 array. The easy way to get around it is simply to transform it in advance.

Your array of bytes-as-integers is a rather odd thing. If this is read from a grayscale image I'd rather assume this is 32-bit ARGB data, and you're just using the lowest component of each value (which would be the blue one), but if downshifting the values by 4 bits produced uniformally dark values I'm inclined to take your word for that; otherwise the bits of the next colour component (green) would bleed in, giving bright colours as well.

Anyway, musing and second-guessing aside, here's my actual answer.

You may think each of your values, when poured into an 8-bit image, is simply the brightness, but this is actually false. There is no specific type in the System.Drawing pixel formats to indicate 8-bit grayscale, and 8-bit images are paletted, which means that each value on the image refers to a colour on the colour palette. So, to actually make an 8-bit grayscale image where your byte values indicate the pixel's brightness, you'll need to explicitly define a colour palette where the indices of 0 to 255 on the palette contain gray colours going from (0,0,0) to (255,255,255). Of course, this is pretty easy to generate.

This code will transform your array into an 8-bit image. It uses the aforementioned BuildImage function. Note that that function uses no unsafe code. The use of Marshal.Copy means raw pointers are never handled directly, making the code completely managed.

public static Bitmap FromTwoDimIntArrayGray(Int32[,] data)
{
    // Transform 2-dimensional Int32 array to 1-byte-per-pixel byte array
    Int32 width = data.GetLength(0);
    Int32 height = data.GetLength(1);
    Int32 byteIndex = 0;
    Byte[] dataBytes = new Byte[height * width];
    for (Int32 y = 0; y < height; y++)
    {
        for (Int32 x = 0; x < width; x++)
        {
            // logical AND to be 100% sure the int32 value fits inside
            // the byte even if it contains more data (like, full ARGB).
            dataBytes[byteIndex] = (Byte)(((UInt32)data[x, y]) & 0xFF);
            // More efficient than multiplying
            byteIndex++;
        }
    }
    // generate palette
    Color[] palette = new Color[256];
    for (Int32 b = 0; i < 256; b++)
        palette[b] = Color.FromArgb(b, b, b);
    // Build image
    return BuildImage(dataBytes, width, height, width, PixelFormat.Format8bppIndexed, palette, null);
}

Note, even if the integers were full ARGB values, the above code would still work exactly the same; if you only use the lowest of the four bytes inside the integer, as I said, that'll simply be the blue component of the full ARGB integer. If the image is grayscale, all three colour components should be identical, so you'll still get the same result.

Assuming you ever find yourself with the same kind of byte array where the integers actually do contain full 32bpp ARGB data, you'd have to shift out all four byte values, and there would be no generated gray palette, but besides that, the code would be pretty similar. Just, handling 4 bytes per X iteration.

    public static Bitmap fromTwoDimIntArrayGray(Int32[,] data)
    {
        Int32 width = data.GetLength(0);
        Int32 height = data.GetLength(1);
        Int32 stride = width * 4;
        Int32 byteIndex = 0;
        Byte[] dataBytes = new Byte[height * stride];
        for (Int32 y = 0; y < height; y++)
        {
            for (Int32 x = 0; x < width; x++)
            {
                // UInt32 0xAARRGGBB = Byte[] { BB, GG, RR, AA }
                UInt32 val = (UInt32)data[x, y];
                // This code clears out everything but a specific part of the value
                // and then shifts the remaining piece down to the lowest byte
                dataBytes[byteIndex + 0] = (Byte)(val & 0x000000FF); // B
                dataBytes[byteIndex + 1] = (Byte)((val & 0x0000FF00) >> 08); // G
                dataBytes[byteIndex + 2] = (Byte)((val & 0x00FF0000) >> 16); // R
                dataBytes[byteIndex + 3] = (Byte)((val & 0xFF000000) >> 24); // A
                // More efficient than multiplying
                byteIndex+=4;
            }
        }
        return BuildImage(dataBytes, width, height, stride, PixelFormat.Format32bppArgb, null, null);
    }

Of course, if you want this without transparency, you can either go with three bytes as you did, or simply change PixelFormat.Format32bppArgb in the final call to PixelFormat.Format32bppRgb, which changes the meaning of the fourth byte from alpha to mere padding.




回答2:


Solved (had to remove the four bits shift):

    private unsafe Task<Bitmap> BitmapFromArray(Int32[,] pixels, int width, int height)
    {
        return Task.Run(() =>
         {
             Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb);
             BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
             for (int y = 0; y < height; y++)
             {
                 byte* row = (byte*)bitmapData.Scan0 + bitmapData.Stride * y;
                 for (int x = 0; x < width; x++)
                 {
                     byte grayShade8bit = (byte)(pixels[x, y]);
                     row[x * 3 + 0] = grayShade8bit;
                     row[x * 3 + 1] = grayShade8bit;
                     row[x * 3 + 2] = grayShade8bit;
                 }
             }
             bitmap.UnlockBits(bitmapData);
             return bitmap;
         });
    }

Still not sure why substituting Format24bppRgb with Format8bppIndexed doesn't work. Any clue?



来源:https://stackoverflow.com/questions/49046376/turning-int-array-into-bmp-in-c-sharp

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!