简体   繁体   中英

How to convert byte[] to array of struct FAST in C#?

TL; DR: I have byte[] . I want Bgra32Pixel[] . Don't want to copy. If a copy is necessary, I want fastest possible optimized copy, no copying of single bytes. Is it even possible?

Full description:

Here's the struct:

/// <summary>
/// Represents a PixelFormats.Bgra32 format pixel
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct Bgra32Pixel {

    [FieldOffset(0)]
    public readonly int Value;

    [FieldOffset(0)]
    public byte B;

    [FieldOffset(1)]
    public byte G;

    [FieldOffset(2)]
    public byte R;

    [FieldOffset(3)]
    public byte A;

}

I have a byte array, lets call it data . I want to access it as Bgra32Pixel[] . The same bytes in memory. Is it necessary to copy bytes for it?

I wish something like this worked:

var pixels = data as Bgra32Pixel[];

But it doesn't. What is the fastest way to do it?

My guess is to make custom type with indexer returning Bgra32Pixel directly from original byte[] reference. But it wouldn't be very fast. No copying is required for that, but each access would actually create a new struct from 4 bytes. No, seems unnecessary slow. There must be a way to trick C# into thinking byte[] is somehow Bgra32Pixel[].

So here's my solution I found after reading all the answers:

TL;DR: No need for struct.

Conversion to struct would require unsafe context and fixed statement. This is no good for performance. Here's the code for removing background from a bitmap, which assumes the pixel in the left top corner has the background color. This code calls special "color to alpha" voodoo on each pixel:

/// <summary>
/// Extensions for bitmap manipulations.
/// </summary>
static class BitmapSourceExtensions {

    /// <summary>
    /// Removes the background from the bitmap assuming the first pixel is background color.
    /// </summary>
    /// <param name="source">Opaque bitmap.</param>
    /// <returns>Bitmap with background removed.</returns>
    public static BitmapSource RemoveBackground(this BitmapSource source) {
        if (source.Format != PixelFormats.Bgr32) throw new NotImplementedException("Pixel format not implemented.");
        var target = new WriteableBitmap(source.PixelWidth, source.PixelHeight, source.DpiX, source.DpiY, PixelFormats.Bgra32, null);
        var pixelSize = source.Format.BitsPerPixel / 8;
        var pixelCount = source.PixelWidth * source.PixelHeight;
        var pixels = new uint[pixelCount];
        var stride = source.PixelWidth * pixelSize;
        source.CopyPixels(pixels, stride, 0);
        var background = new LABColor(pixels[0]);
        for (int i = 0; i < pixelCount; i++) pixels[i] &= background.ColorToAlpha(pixels[i]);
        var bounds = new Int32Rect(0, 0, source.PixelWidth, source.PixelHeight);
        target.WritePixels(bounds, pixels, stride, 0);
        return target;
    }

}

If you are very curious, what is the voodoo class used, here:

/// <summary>
/// CIE LAB color space structure with BGRA pixel support.
/// </summary>
public struct LABColor {

    /// <summary>
    /// Lightness (0..100).
    /// </summary>
    public readonly double L;

    /// <summary>
    /// A component (0..100)
    /// </summary>
    public readonly double A;

    /// <summary>
    /// B component (0..100)
    /// </summary>
    public readonly double B;

    /// <summary>
    /// Creates CIE LAB color from BGRA pixel.
    /// </summary>
    /// <param name="bgra">Pixel.</param>
    public LABColor(uint bgra) {
        const double t = 1d / 3d;
        double r = ((bgra & 0x00ff0000u) >> 16) / 255d;
        double g = ((bgra & 0x0000ff00u) >> 8) / 255d;
        double b = (bgra & 0x000000ffu) / 255d;
        r = (r > 0.04045 ? Math.Pow((r + 0.055) / 1.055, 2.4) : r / 12.92) * 100d;
        g = (g > 0.04045 ? Math.Pow((g + 0.055) / 1.055, 2.4) : g / 12.92) * 100d;
        b = (b > 0.04045 ? Math.Pow((b + 0.055) / 1.055, 2.4) : b / 12.92) * 100d;
        double x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 95.047;
        double y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 100.000;
        double z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 108.883;
        x = x > 0.0088564516790356311 ? Math.Pow(x, t) : (903.2962962962963 * x + 16d) / 116d;
        y = y > 0.0088564516790356311 ? Math.Pow(y, t) : (903.2962962962963 * y + 16d) / 116d;
        z = z > 0.0088564516790356311 ? Math.Pow(z, t) : (903.2962962962963 * z + 16d) / 116d;
        L = Math.Max(0d, 116d * y - 16d);
        A = 500d * (x - y);
        B = 200d * (y - z);
    }


    /// <summary>
    /// Calculates color space distance between 2 CIE LAB colors.
    /// </summary>
    /// <param name="c">CIE LAB color.</param>
    /// <returns>A color space distance between 2 colors from 0 (same colors) to 100 (black and white)</returns>
    public double Distance(LABColor c) {
        double dl = L - c.L;
        double da = A - c.A;
        double db = B - c.B;
        return Math.Sqrt(dl * dl + da * da + db * db);
    }

    /// <summary>
    /// Calculates bit mask for alpha calculated from difference between this color and another BGRA color.
    /// </summary>
    /// <param name="bgra">Pixel.</param>
    /// <returns>Bit mask for alpha in BGRA pixel format.</returns>
    public uint ColorToAlpha(uint bgra) => 0xffffffu | ((uint)(Distance(new LABColor(bgra)) * 2.55d) << 24);

}

I'm giving back to the community. I found all necessary math on StackOverflow and Github. I guess GIMP is using something very similar for "color to alpha" effect.

The question still remains open: is there a faster way to do that?

There is no need to convert the byte array to Bgra32Pixel objects, and doing so is only going to hurt your performance. To read pixel data from a WriteableBitmap , you can do the following:

unsafe public static BitmapSource GetBgra32(this BitmapSource bmp) 
{
    if (bmp.Format != PixelFormats.Bgr32) 
        throw new NotImplementedException("Pixel format not implemented.");

    var source = new WriteableBitmap(bmp);
    var target = new WriteableBitmap(bmp.PixelWidth, bmp.PixelHeight, bmp.DpiX, bmp.DpiY, PixelFormats.Bgra32, null);

    source.Lock();
    target.Lock();

    var srcPtr = (byte*) source.BackBuffer;
    var trgPtr = (byte*) source.BackBuffer;

    int sIdx,sCol,tIdx,tCol;
    for (int y = 0; y < bmp.PixelHeight; y++)
    {
        sCol = y * source.BackBufferStride;
        tCol = y * target.BackBufferStride;

        for (int x = 0; x < bmp.PixelWidth; x++)
        {
            sIdx = sCol + (x * 3); // Bpp = 3
            tIdx = tCol + (x * 4); // Bpp = 4

            byte b = srcPtr[sIdx];
            byte g = srcPtr[sIdx + 1];
            byte r = srcPtr[sIdx + 2];

            // Do some processing

            trgPtr[tIdx] = bVal;
            trgPtr[tIdx + 1] = gVal;
            trgPtr[tIdx + 2] = rVal;
            trgPtr[tIdx + 3] = aVal;
        }
    }

    source.Unlock();
    target.Unlock();

    return target;
}

The below code can be used to convert an array of byte into an array of Bgra32Pixel .

static T[] ConvertBytesWithGCHandle<T>(byte[] data) where T : struct
{
    int size = Marshal.SizeOf(typeof(T));

    if (data.Length % size != 0) throw new ArgumentOutOfRangeException("data", "Data length must be divisable by " + size + " (struct size).");

    GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
    IntPtr ptr = handle.AddrOfPinnedObject();

    T[] returnData = new T[data.Length / size];
    for (int i = 0; i < returnData.Length; i++)
        returnData[i] = (T)Marshal.PtrToStructure(ptr + i * size, typeof(T));

    handle.Free();
    return returnData;
}

static T[] ConvertBytesWithMarshal<T>(byte[] data) where T : struct
{
    int size = Marshal.SizeOf(typeof(T));

    if (data.Length % size != 0) throw new ArgumentOutOfRangeException("data", "Data length must be divisable by " + size + " (struct size).");

    T[] returnData = new T[data.Length / size];
    for (int i = 0; i < returnData.Length; i++)
    {
        IntPtr ptr = Marshal.AllocHGlobal(size);
        Marshal.Copy(data, i * size, ptr, size);
        returnData[i] = (T)Marshal.PtrToStructure(ptr, typeof(T));
        Marshal.FreeHGlobal(ptr);
    }
    return returnData;
}

For speed tests I also made the following methods:

static Bgra32Pixel[] CopyBytesWithPlain(byte[] data)
{
    if (data.Length % 4 != 0) throw new ArgumentOutOfRangeException("data", "Data length must be divisable by 4 (struct size).");

    Bgra32Pixel[] returnData = new Bgra32Pixel[data.Length / 4];

    for (int i = 0; i < returnData.Length; i++)
        returnData[i] = new Bgra32Pixel()
        {
            B = data[i * 4 + 0],
            G = data[i * 4 + 1],
            R = data[i * 4 + 2],
            A = data[i * 4 + 3]
        };
    return returnData;
}

static Bgra32Pixel[] CopyBytesWithLinq(byte[] data)
{
    if (data.Length % 4 != 0) throw new ArgumentOutOfRangeException("data", "Data length must be divisable by 4 (struct size).");

    return data
        .Select((b, i) => new { Byte = b, Index = i })
        .GroupBy(g => g.Index / 4)
        .Select(g => g.Select(b => b.Byte).ToArray())
        .Select(a => new Bgra32Pixel()
        {
            B = a[0],
            G = a[1],
            R = a[2],
            A = a[3]
        })
        .ToArray();
}

The results of the speed test are as follows:

CopyBytesWithPlain:    00:00:00.1701410 
CopyBytesWithGCHandle: 00:00:02.8298880 
CopyBytesWithMarshal:  00:00:05.5448466
CopyBytesWithLinq:     00:00:33.5987996

Code can be used as follows:

var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 };
var pixels = ConvertBytesWithMarshal<Bgra32Pixel>(bytes);
foreach (var pixel in pixels)
    Console.WriteLine(pixel.Value);

Or

var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 };
var pixels = ConvertBytesWithGCHandle<Bgra32Pixel>(bytes);
foreach (var pixel in pixels)
    Console.WriteLine(pixel.Value);

Code is based on https://stackoverflow.com/a/31047345/637425 as pointed out by Taha Paksu and on https://stackoverflow.com/a/2887/637425 as pointed out by gabriel .

You don't need to convert the byte array to a Bgra32Pixel array, all you need is to access the byte array as it was a Bgra32Pixel array.

The following code creates a buffer for holding 32 pixels and set their color to semitransparent red:

const int numberOfPixels = 32;
var buffer = new byte[numberOfPixels * sizeof(Bgra32Pixel)];
fixed(void* p =  buffer)
{
    var pixels = (Bgra32Pixel*)p;
    for (int i= 0; i < numberOfPixels; i++)
    {
        pixels[i].A = 128;
        pixels[i].R = 255;
    }
}

But it would be even faster if you manage your pixels as whole uint 's

The following code does the same, but faster:

const int numberOfPixels = 32;
var buffer = new byte[numberOfPixels * sizeof(uint)];
fixed(void* p =  buffer)
{
    var pixels = (uint*)p;
    for (int i= 0; i < numberOfPixels; i++)
    {
        pixels[i] = 0x80FF0000; // A: 0x80, R: 0xFF, G: 0x00, B: 0x00
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM