image-processingbitmapbitmapdata

Image is not being padded correctly


Output

enter image description here

I think the following code isn't giving the correct result.

What's wrong withe following code?

public class ImagePadder
{
    public static Bitmap Pad(Bitmap image, int newWidth, int newHeight)
    {
        int width = image.Width;
        int height = image.Height;

        if (width >= newWidth) throw new Exception("New width must be larger than the old width");
        if (height >= newHeight) throw new Exception("New height must be larger than the old height");

            Bitmap paddedImage = Grayscale.CreateGrayscaleImage(newWidth, newHeight);

            BitmapLocker inputImageLocker = new BitmapLocker(image);
            BitmapLocker paddedImageLocker = new BitmapLocker(paddedImage);

            inputImageLocker.Lock();
            paddedImageLocker.Lock();

            //Reading row by row
            for (int y = 0; y < image.Height; y++)
            {
                for (int x = 0; x < image.Width; x++)
                {     
                    Color col = inputImageLocker.GetPixel(x, y);

                    paddedImageLocker.SetPixel(x, y, col);
                }
            }

            string str = string.Empty;                

            paddedImageLocker.Unlock();
            inputImageLocker.Unlock();

            return paddedImage;            
    }
}

Relevant Source Code:

public class BitmapLocker : IDisposable
{
    //private properties
    Bitmap _bitmap = null;
    BitmapData _bitmapData = null;
    private byte[] _imageData = null;

    //public properties
    public bool IsLocked { get; set; }
    public IntPtr IntegerPointer { get; private set; }
    public int Width { get { return _bitmap.Width; } }
    public int Height { get { return _bitmap.Height; } }
    public int Stride { get { return _bitmapData.Stride; } }
    public int ColorDepth { get { return Bitmap.GetPixelFormatSize(_bitmap.PixelFormat); } }
    public int Channels { get { return ColorDepth / 8; } }
    public int PaddingOffset { get { return _bitmapData.Stride - (_bitmap.Width * Channels); } }
    public PixelFormat ImagePixelFormat { get { return _bitmap.PixelFormat; } }
    public bool IsGrayscale { get { return Grayscale.IsGrayscale(_bitmap); } }

    //Constructor
    public BitmapLocker(Bitmap source)
    {
        IsLocked = false;
        IntegerPointer = IntPtr.Zero;
        this._bitmap = source;
    }

    /// Lock bitmap
    public void Lock()
    {
        if (IsLocked == false)
        {
            try
            {
                // Lock bitmap (so that no movement of data by .NET framework) and return bitmap data
                _bitmapData = _bitmap.LockBits(
                                                new Rectangle(0, 0, _bitmap.Width, _bitmap.Height),
                                                ImageLockMode.ReadWrite,
                                                _bitmap.PixelFormat);

                // Create byte array to copy pixel values
                int noOfBitsNeededForStorage = _bitmapData.Stride * _bitmapData.Height;

                int noOfBytesNeededForStorage = noOfBitsNeededForStorage / 8;

                _imageData = new byte[noOfBytesNeededForStorage * ColorDepth];//# of bytes needed for storage

                IntegerPointer = _bitmapData.Scan0;

                // Copy data from IntegerPointer to _imageData
                Marshal.Copy(IntegerPointer, _imageData, 0, _imageData.Length);

                IsLocked = true;
            }
            catch (Exception)
            {
                throw;
            }
        }
        else
        {
            throw new Exception("Bitmap is already locked.");
        }
    }

    /// Unlock bitmap
    public void Unlock()
    {
        if (IsLocked == true)
        {
            try
            {
                // Copy data from _imageData to IntegerPointer
                Marshal.Copy(_imageData, 0, IntegerPointer, _imageData.Length);

                // Unlock bitmap data
                _bitmap.UnlockBits(_bitmapData);

                IsLocked = false;
            }
            catch (Exception)
            {
                throw;
            }
        }
        else
        {
            throw new Exception("Bitmap is not locked.");
        }
    }

    public Color GetPixel(int x, int y)
    {
        Color clr = Color.Empty;

        // Get color components count
        int cCount = ColorDepth / 8;

        // Get start index of the specified pixel
        int i = (Height - y - 1) * Stride + x * cCount;

        int dataLength = _imageData.Length - cCount;

        if (i > dataLength)
        {
            throw new IndexOutOfRangeException();
        }

        if (ColorDepth == 32) // For 32 bpp get Red, Green, Blue and Alpha
        {
            byte b = _imageData[i];
            byte g = _imageData[i + 1];
            byte r = _imageData[i + 2];
            byte a = _imageData[i + 3]; // a
            clr = Color.FromArgb(a, r, g, b);
        }
        if (ColorDepth == 24) // For 24 bpp get Red, Green and Blue
        {
            byte b = _imageData[i];
            byte g = _imageData[i + 1];
            byte r = _imageData[i + 2];
            clr = Color.FromArgb(r, g, b);
        }
        if (ColorDepth == 8)
        // For 8 bpp get color value (Red, Green and Blue values are the same)
        {
            byte c = _imageData[i];
            clr = Color.FromArgb(c, c, c);
        }
        return clr;
    }

    public void SetPixel(int x, int y, Color color)
    {

        // Get color components count
        int cCount = ColorDepth / 8;

        // Get start index of the specified pixel
        int i = (Height - y - 1) * Stride + x * cCount;

        try
        {
            if (ColorDepth == 32) // For 32 bpp set Red, Green, Blue and Alpha
            {
                _imageData[i] = color.B;
                _imageData[i + 1] = color.G;
                _imageData[i + 2] = color.R;
                _imageData[i + 3] = color.A;
            }
            if (ColorDepth == 24) // For 24 bpp set Red, Green and Blue
            {
                _imageData[i] = color.B;
                _imageData[i + 1] = color.G;
                _imageData[i + 2] = color.R;
            }
            if (ColorDepth == 8)
            // For 8 bpp set color value (Red, Green and Blue values are the same)
            {
                _imageData[i] = color.B;
            }
        }
        catch (Exception ex)
        {
            throw new Exception("(" + x + ", " + y + "), " + _imageData.Length + ", " + ex.Message + ", i=" + i);
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // free managed resources
            _bitmap = null;
            _bitmapData = null;
            _imageData = null;
            IntegerPointer = IntPtr.Zero;
        }
    }
}

Solution

  • The layout of a Windows bitmap is different than you might expect. The bottom line of the image is the first line in memory, and continues backwards from there. It can also be laid out the other way when the height is negative, but those aren't often encountered.

    Your calculation of an offset into the bitmap appears to take that into account, so your problem must be more subtle.

    int i = (Height - y - 1) * Stride + x * cCount;
    

    The problem is that the BitmapData class already takes this into account and tries to fix it for you. The bitmap I described above is a bottom-up bitmap. From the documentation for BitmapData.Stride:

    The stride is the width of a single row of pixels (a scan line), rounded up to a four-byte boundary. If the stride is positive, the bitmap is top-down. If the stride is negative, the bitmap is bottom-up.

    It is intended to be used with the Scan0 property to access the bitmap in a consistent fashion whether it's top-down or bottom-up.