Search code examples
c#imageimage-processingxnamonogame

Using LockBits/Marshal.Copy to convert from S.D.Bitmap to MonoGame Texture2D - Result is distorted


I am trying to write a function that takes a System.Drawing.Bitmap, and spits out a Monogame Texture2D as quickly as possible.

I've managed to make it work using GetPixel on the bitmap, which is a no-brainer, but it's far too slow, of course.

I dug around and discovered a much faster technique for quickly accessing the pixel values using LockBits and Marshal.Copy. The problem is, it works perfectly on some images, while others end up looking like this: Example Image

From what I've been able to gather, this has something to do with not properly compensating for Stride, but I am new to directly working with image data at this level, and I am completely lost. What am I doing wrong here that leads to this greyscale-ish slanted result?

When this works, which it does for most images, it is very fast and exactly what I want. If it matters, I'm exclusively going to be handling pretty typical JPG/PNG images off the web.

public static Texture2D FromBitmap(MagickImage input, GraphicsDevice graphicsDevice)
        {
            var bounds = new Rectangle(0, 0, input.Width, input.Height);

            var bitmap = input.ToBitmap();

            var output = new Texture2D(graphicsDevice, bitmap.Width, bitmap.Height);

            var outputArray = new Color[bitmap.Width * bitmap.Height];

            var bitmapBits = bitmap.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
            var bitmapDepth = Image.GetPixelFormatSize(bitmapBits.PixelFormat) / 8;
            var bitmapBuffer = new byte[bitmapBits.Stride * bitmapBits.Height];

            Marshal.Copy(bitmapBits.Scan0, bitmapBuffer, 0, bitmapBuffer.Length);

            for (var y = 0; y < bitmap.Height; y++)
            {
                for (var x = 0; x < bitmap.Width; x++)
                {
                    var arrayPoint = x + (y * bitmap.Width);

                    outputArray[arrayPoint].R = bitmapBuffer[(arrayPoint * bitmapDepth) + 2];
                    outputArray[arrayPoint].G = bitmapBuffer[(arrayPoint * bitmapDepth) + 1];
                    outputArray[arrayPoint].B = bitmapBuffer[(arrayPoint * bitmapDepth) + 0];
                }
            }

            output.SetData(outputArray);

            return output;
        }
    }

Edit: I had a simple mistake in pointing to the arrayPoint within outputArray, a leftover from a previous attempt. Correcting this + rewriting the arrayPoint definition fixed it. The below works:

var arrayPoint = (y * bitmapBits.Stride) + (x * bitmapDepth);

outputArray[(y * bitmap.Width) + x].R = bitmapBuffer[arrayPoint + 2];
outputArray[(y * bitmap.Width) + x].G = bitmapBuffer[arrayPoint + 1];
outputArray[(y * bitmap.Width) + x].B = bitmapBuffer[arrayPoint + 0];

Solution

  • You're ignoring the stride, and that means that some images will work and some won't.

    Stride is NOT bitmap.Width * bitmapDepth.
    The stride is the width of a single row of pixels (a scan line), rounded up to a four-byte boundary.

    So you always have to increase your arrayPoint by stride for each Row, and not some manual calculation.