skip to Main Content

My code works for some images but not others.

Image that works:

enter image description here

Image that doesn’t work (same image but smaller):

enter image description here

I first load the image and create an array of raw pixel data that for instance only includes the red channel data.

public byte[] GetPixels(Bitmap bmp)
    {
        BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
        int length = bmpData.Stride * bmpData.Height;
        int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
        byte[] pixelData = new byte[length];
        byte[] newPixelData = new byte[length / bytesPerPixel];

        Marshal.Copy(bmpData.Scan0, pixelData, 0, length);
        for(int i = 0; i < newPixelData.Length; i++)
        {

            newPixelData[i] = pixelData[i*bytesPerPixel];
        }

        bmp.UnlockBits(bmpData);
        bmp.Dispose();
        return newPixelData;
    }

Then I call a method that turns that byte[] into an 8bit per pixel indexed bitmap. I want everything else to be the same so width, height, resolution is all derived from the original image.

   public Bitmap makeBitmap(byte[] bytes)
{
    Bitmap bmp;
    PixelFormat pxFormat = PixelFormat.Format8bppIndexed;
    int stride = img.Width;
    unsafe
    {
        fixed (byte* ptr = bytes)
        {
            bmp = new Bitmap(img.Width, img.Height, stride, pxFormat, (IntPtr)ptr);
        }
    }
    bmp.SetResolution(img.HorizontalResolution,img.VerticalResolution);
    ColorPalette palette = bmp.Palette;
    for (int i = 0; i < 256; i++)
    {
        palette.Entries[i] = Color.FromArgb(i,i,i);
    }
    bmp.Palette = palette;

    return bmp;
}

This program works fine with the 1st image I showed but won’t work with the second. here is the error I get:

enter image description here

The Stride seems to be the problem but i’m not sure how to fix it.. Can anyone advise on how to fix this? Imagine photoshop split channels. That’s sort of what I’m trying to do each channel will be a separate grayscale image). I’m doing calculations with the channel data as well so having the raw pixel data in a byte[] has really helped me cut down on memory usage with the larger images.

2

Answers


  1. As I assumed. It’s all about Stride. This code works for 24-, 32-bit and higher img. The 16-bit format requires bitwise operations.

        public byte[] GetPixels(Bitmap bmp)
        {
            BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
            int length = bmpData.Stride * bmpData.Height;
            int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
            int newStride = ((((bmp.Width * Bitmap.GetPixelFormatSize(PixelFormat.Format8bppIndexed)) + 31) & ~31) >> 3);
            int newLength = newStride * bmpData.Height;
            byte[] pixelData = new byte[length];
            byte[] newPixelData = new byte[newLength];
    
            Marshal.Copy(bmpData.Scan0, pixelData, 0, length);
            for (int y = 0; y < bmpData.Height; y++)
                for (int x = 0; x < bmpData.Width; x++)
                {
                    newPixelData[x + y * newStride] = pixelData[x * bytesPerPixel + y * bmpData.Stride];
                }
    
            bmp.UnlockBits(bmpData);
            bmp.Dispose();
            return newPixelData;
        }
    
        public Bitmap makeBitmap(byte[] bytes)
        {
            Bitmap bmp;
            PixelFormat pxFormat = PixelFormat.Format8bppIndexed;
            int stride = (((img.Width * Bitmap.GetPixelFormatSize(pxFormat)) + 31) & ~31) >> 3;
            unsafe
            {
                fixed (byte* ptr = bytes)
                {
                    bmp = new Bitmap(img.Width, img.Height, stride, pxFormat, (IntPtr)ptr);
                }
            }
            bmp.SetResolution(img.HorizontalResolution, img.VerticalResolution);
            ColorPalette palette = bmp.Palette;
            for (int i = 0; i < 256; i++)
            {
                palette.Entries[i] = Color.FromArgb(i, i, i);
            }
            bmp.Palette = palette;
    
            return bmp;
        }
    
    Login or Signup to reply.
  2. The first problem is the stride, from the documentation:

    Stride

    Integer that specifies the byte offset between the beginning of one scan line and the next. This is usually (but not necessarily) the number of bytes in the pixel format (for example, 2 for 16 bits per pixel) multiplied by the width of the bitmap. The value passed to this parameter must be a multiple of four.

    578 is not an even multiple of four, so will violate this.

    The second problem is the pointer:

    The caller is responsible for allocating and freeing the block of memory specified by the scan0 parameter. However, the memory should not be released until the related Bitmap is released.

    So after the fixed block has ended the memory may move around, and the pointer becomes invalid.

    The correct way to do this is to create the new bitmap with the correct width, height, and pixel format, and write a copy function that copies the data, taking the target and source stride into account. For example something like this:

    public static unsafe void Bgr32ToMono8(
        byte* source,
        byte* target,
        int sourceStride,
        int targetStride,
        int width,
        int height,
        int channel)
    {
        Parallel.For(0, height, y => {
            var sourceRow = source + y * sourceStride;
            var targetRow = target + y * targetStride;
            for (int x = 0; x < width; x += 1)
            {
                var sourceIndex = sourceRow + x * 4;
                var value = sourceIndex[channel];
                targetRow[x] = value;
            }
        });
    }
    

    You seem familiar with how to get the Scan0 pointer, so just create your target bitmap, create bitmapData objects, and feed this function the source and target pointers, strides for each, as well as the image size and a channel parameter to specify what color channel you want.

    Note that is is fairly easy to get the channel order mixed up. PixelFormat.Format32bppArgb and WPFs PixelFormats.Bgra are the same thing, but named differently, due to endianess. Easiest way is to just check the result against a known result to make sure you got it right.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search