Harry Harry - 13 days ago 9
C# Question

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?

Answer

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;
}