RJ Cuthbertson
RJ Cuthbertson

Reputation: 1528

Image Manipulation - auto resize / crop for thumbnails

I have hit a brick wall with this... I know my flaw is in the logic somehow but I'll explain where I'm at.

I'm (trying) to write code to dynamically create a square (75x75) pixel thumbnail for a navigation bar along the bottom of my Silverlight app. When I debug my code below I keep getting an "Array index out of bounds" error during the scaling nested loops (I've only debugged with srcWidth > srcHeight, one step at a time)

The source image I'm testing with is 307x162 (49734 pixels) The dest size (per current logic) for this is 150x75 (11250 pixels) Of course after this then I intend to crop the result down to 75x75

It's only getting about half through the source image before hitting 11251 as the index of the destination. I know it's my logic, I just don't know if I went wrong in the sizes I used to construct the destination, or in the approximations used from casting the floats to ints, or maybe it's neither

I don't know, which is why I'm posting here... anyhow, here's my source:

    private void ResizeNavBarImage(Image src)
    {
        const int SizeOfRGB = 4;
        WriteableBitmap bmpSource = new WriteableBitmap((BitmapSource)src.Source);
        int[] iSourcePixels = bmpSource.Pixels;
        int srcWidth = bmpSource.PixelWidth;
        int srcHeight = bmpSource.PixelHeight;

        int destWidth = 75;
        int destHeight = 75;

        float xFactor = srcWidth / destWidth;
        float yFactor = srcHeight / destHeight;

        float xSource, ySource;
        int iApprox;
        int iIndex = 0;

        if (srcWidth > srcHeight)
        {
            WriteableBitmap bmpDest = new WriteableBitmap((int)(destWidth * yFactor), destHeight);
            int[] iDestPixels = bmpDest.Pixels;

            // Resize srcHeight to destHeight, srcWidth to same ratio
            for (int i = 0; i < srcHeight; i++)
            {
                for (int j = 0; j < srcWidth; j++)
                {
                    xSource = j * yFactor;
                    ySource = i * yFactor;
                    iApprox = (int)(ySource * srcWidth + xSource);
                    iDestPixels[iIndex++] = iSourcePixels[iApprox];
                }
            }

            // Crop half of difference from each side of the width
            srcWidth = bmpDest.PixelWidth;
            srcHeight = bmpDest.PixelHeight;
            int xLeftOffset = (srcWidth - srcHeight) / 2;

            WriteableBitmap bmpFinalDest = new WriteableBitmap(destWidth, destHeight);
            for (int iPixelRow = 0; iPixelRow < destHeight; iPixelRow++)
            {
                int srcOffset = (iPixelRow * srcWidth + xLeftOffset) * SizeOfRGB;
                int destOffset = iPixelRow * destWidth * SizeOfRGB;
                Buffer.BlockCopy(bmpDest.Pixels, srcOffset, bmpFinalDest.Pixels, destOffset, destWidth * SizeOfRGB);
            }

            src.Source = (ImageSource)bmpFinalDest;
        }
        else if (srcWidth < srcHeight)
        {
            WriteableBitmap bmpDest = new WriteableBitmap(destWidth, (int)(destHeight * xFactor));
            int[] iDestPixels = bmpDest.Pixels;

            // Resize srcWidth to destWidth, srcHeight to same ratio
            for (int i = 0; i < srcHeight; i++)
            {
                for (int j = 0; j < srcWidth; j++)
                {
                    xSource = j * xFactor;
                    ySource = i * xFactor;
                    iApprox = (int)(ySource * srcWidth + xSource);
                    iDestPixels[iIndex++] = iSourcePixels[iApprox];
                }
            }

            // Crop half of difference from each side of the height
            srcWidth = bmpDest.PixelWidth;
            srcHeight = bmpDest.PixelHeight;
            int yTopOffset = (srcHeight - srcWidth) / 2;

            WriteableBitmap bmpFinalDest = new WriteableBitmap(destWidth, destHeight);
            for (int iPixelRow = yTopOffset; iPixelRow < (destHeight - (yTopOffset * 2)); iPixelRow++)
            {
                int srcOffset = iPixelRow * srcWidth * SizeOfRGB;
                int destOffset = iPixelRow * destWidth * SizeOfRGB;
                Buffer.BlockCopy(bmpDest.Pixels, srcOffset, bmpFinalDest.Pixels, destOffset, destWidth * SizeOfRGB);
            }

            src.Source = (ImageSource)bmpFinalDest;
        }
        else // (srcWidth == srcHeight)
        {
            WriteableBitmap bmpDest = new WriteableBitmap(destWidth, destHeight);
            int[] iDestPixels = bmpDest.Pixels;

            // Resize and set source
            for (var i = 0; i < srcHeight; i++)
            {
                for (var j = 0; j < srcWidth; j++)
                {
                    xSource = j * xFactor;
                    ySource = i * yFactor;
                    iApprox = (int)(ySource * srcWidth + xSource);
                    iDestPixels[iIndex++] = iSourcePixels[iApprox];
                }
            }
            src.Source = (ImageSource)bmpDest;
        }
    }

===============================================================================

Here's my working code (with WriteableBitmapEx) for posterity:

    private void ResizeNavBarImage(Image src)
    {
        WriteableBitmap bmpSource = new WriteableBitmap((BitmapSource)src.Source);
        int srcWidth = bmpSource.PixelWidth;
        int srcHeight = bmpSource.PixelHeight;

        int finalDestWidth = 75;
        int finalDestHeight = 75;

        // Resize
        float xFactor = ((float)finalDestWidth / (float)srcWidth);
        float yFactor = ((float)finalDestHeight / (float)srcHeight);
        float Factor = 0;

        if (xFactor < yFactor)
            Factor = yFactor;
        else
            Factor = xFactor;

        int destWidth = (int)(srcWidth * Factor);
        int destHeight = (int)(srcHeight * Factor);

        if (destWidth < destHeight && destWidth != finalDestWidth)
            destWidth = finalDestWidth;
        else if (destWidth > destHeight && destHeight != finalDestHeight)
            destHeight = finalDestHeight;

        WriteableBitmap bmpDest = bmpSource.Resize(destWidth, destHeight, WriteableBitmapExtensions.Interpolation.Bilinear);

        // Crop
        int Offset;
        WriteableBitmap bmpFinalDest = new WriteableBitmap(finalDestWidth, finalDestHeight);

        if (destWidth > destHeight)
        {
            Offset = (bmpDest.PixelWidth - bmpDest.PixelHeight) / 2;
            if (finalDestWidth % 2 != 0 && Offset % 2 == 0)
                Offset -= 1;
            bmpFinalDest = bmpDest.Crop(Offset, 0, finalDestWidth, finalDestHeight);
        }
        else if (destWidth < destHeight)
        {
            Offset = (bmpDest.PixelHeight - bmpDest.PixelWidth) / 2;
            if (finalDestHeight % 2 != 0 && Offset % 2 == 0)
                Offset -= 1;
            bmpFinalDest = bmpDest.Crop(0, Offset, finalDestWidth, finalDestHeight);
        }
        else
            bmpFinalDest = bmpDest;

        src.Source = (ImageSource)bmpFinalDest;
    }

Upvotes: 0

Views: 3038

Answers (3)

David
David

Reputation: 1062

Why not use the built-in scale functionality of WriteableBitmap?

WriteableBitmap wb = new WriteableBitmap(src, new ScaleTransform() { ScaleX = 0.25, ScaleY = 0.25 });
wb.Invalidate();
src.Source = wb;

Upvotes: 0

Ani
Ani

Reputation: 10906

I agree with Mark Ransom, the correct way to resize the image would be to draw into another image using a Graphics object.

http://www.switchonthecode.com/tutorials/csharp-tutorial-image-editing-saving-cropping-and-resizing

This will allow you to use different, better filters to get good image quality, not to mention speed.

Upvotes: 0

Mark Ransom
Mark Ransom

Reputation: 308402

Your loops for i and j are going to srcHeight and srcWidth instead of destHeight and destWidth.

That's probably not the last bug in this code either.

Upvotes: 1

Related Questions