Ievgen
Ievgen

Reputation: 4443

C# Resized images have black borders

I have a problem with image scaling in .NET. I use the standard Graphics type to resize images like in this example:

public static Image Scale(Image sourceImage, int destWidth, int destHeight)
{
        Bitmap toReturn = new Bitmap(sourceImage, destWidth, destHeight);

        toReturn.SetResolution(sourceImage.HorizontalResolution, sourceImage.VerticalResolution);

        using (Graphics graphics = Graphics.FromImage(toReturn))
        {
            graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
            graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.DrawImage(sourceImage, 0, 0, destWidth, destHeight);
        }
        return toReturn;
    }

But I have a big problem with resized images: they have gray and black borders and it's extremely important to make have images without them.

Why do they appear and what I can to do make them disappear?

Sample Output:

sample output

Upvotes: 13

Views: 13942

Answers (9)

saucecontrol
saucecontrol

Reputation: 1506

A correct answer can be pieced together from some of the other responses, but none of them is complete and some present some very bad ideas (like drawing the image twice).

The problem

There are three reasons for the artifacts you're seeing:

  1. The default Graphics.PixelOffsetMode setting causes the pixel values to be sampled incorrectly, resulting in a slight distortion of the image, particularly around the edges.
  2. InterpolationMode.HighQualityBicubic samples pixels from beyond the image edge, which are transparent by default. Those transparent pixels are mixed with the edge pixels by the sampler, resulting in semi-transparent edges.
  3. When you save a semi-transparent image in a format that doesn't support transparency (e.g. JPEG), the transparent values are replaced by black.

That all adds up to semi-black (i.e. grey) edges.

There are a few other issues with the code you posted as well:

The Bitmap constructor you used is initializing the new Bitmap by resizing the original image, so you're doing the resize operation twice. You should use a constructor overload with just the desired dimensions to create a blank canvas.

Remember that the Bitmap class represents an unmanaged copy of the image in memory. It needs to be disposed so that GDI+ can be told to release that memory when you're done with it. I assume you're doing that in the code that receives the return Image, but I point that out in case anyone else borrows this code.

The CompositingQuality.HighQuality setting used in your code will have no visual effect if you get the other settings right and will actually hurt performance fairly significantly in combination with the default value of CompositingMode.SourceOver. You can omit the CompositingQuality setting and set CompositingMode.SourceCopy to get the same results with better performance.

The SmoothingMode setting used in your code has no impact at all on DrawImage(), so it can be removed.

Solution

The correct way to remove those artifacts is to use PixelOffsetMode.Half and to use an ImageAttributes object to specify edge tiling so the HighQualityBicubic sampler has something other than transparent pixels to sample.

You can read more about the Graphics class settings and their impact on image quality and performance here: http://photosauce.net/blog/post/image-scaling-with-gdi-part-3-drawimage-and-the-settings-that-affect-it

The revised code should look something like this:

public static Image Scale(Image sourceImage, int destWidth, int destHeight)
{
    var toReturn = new Bitmap(destWidth, destHeight);

    using (var graphics = Graphics.FromImage(toReturn))
    using (var attributes = new ImageAttributes())
    {
        toReturn.SetResolution(sourceImage.HorizontalResolution, sourceImage.VerticalResolution);

        attributes.SetWrapMode(WrapMode.TileFlipXY);

        graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
        graphics.PixelOffsetMode = PixelOffsetMode.Half;
        graphics.CompositingMode = CompositingMode.SourceCopy;
        graphics.DrawImage(sourceImage, Rectangle.FromLTRB(0, 0, destWidth, destHeight), 0, 0, sourceImage.Width, sourceImage.Height, GraphicsUnit.Pixel, attributes);
    }

    return toReturn;
}

Upvotes: 8

Stefan Steiger
Stefan Steiger

Reputation: 82406

None of these worked for me.

However, changing the format from

System.Drawing.Imaging.PixelFormat.Format24bppRgb

to

System.Drawing.Imaging.PixelFormat.Format32bppArgb 

did solve the problem

using (System.Drawing.Bitmap newImage = new System.Drawing.Bitmap(newWidth, newHeight,
                // System.Drawing.Imaging.PixelFormat.Format24bppRgb // OMG bug
                    System.Drawing.Imaging.PixelFormat.Format32bppArgb 
                ))
            {

Upvotes: 1

marapet
marapet

Reputation: 56586

The real solution is to use an overload of the DrawImage which allows you to pass a ImageAttributes object.

On the ImageAttributes instance, call the following method before passing it to DrawImage:

using (var ia = new ImageAttributes())
{
    ia.SetWrapMode(WrapMode.TileFlipXY);
    aGraphic.DrawImage(..., ia);
}

See also this answer

Upvotes: 8

Mark Ransom
Mark Ransom

Reputation: 308548

Try:

graphic.CompositingMode = CompositingMode.SourceCopy;

Upvotes: 6

Camilo Martin
Camilo Martin

Reputation: 37918

This can be caused by pixels around the edges being wrongly interpolated. I'd call this a bug.

Here's the solution, though:

graphics.CompositingMode = CompositingMode.SourceCopy;
graphics.PixelOffsetMode = PixelOffsetMode.Half;
graphics.InterpolationMode = InterpolationMode.NearestNeighbor;

// Draw your image here.

graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;

// Draw it again.

What this does is first drawing a "background" with the edges correctly-filled, and then draw it again with interpolation. If you don't need interpolation, then this is not necessary.

Upvotes: 7

Jeroen
Jeroen

Reputation: 4023

The problem lies in the fact that your bitmap toReturn has a black background by default. Copying a new image over it makes black or gray borders.

The solution is to remove the black default background, by calling:

toReturn.MakeTransparent();

Since after this line you'll be drawing on a new image without any background color the borders will disappear.

Upvotes: 6

Lucero
Lucero

Reputation: 60276

This is because of the smoothing (blending with the background) on the edges when drawing the image.

You could maybe draw it twice, once without and one with smoothing enabled. Or you could draw it a little bigger. Or if the original background color is known, you could first fill the image with the background color.

Upvotes: 0

Chuck Conway
Chuck Conway

Reputation: 16435

It's because sampling was taken from the edges of the photo.

Upvotes: 1

Dov
Dov

Reputation: 16186

How does the following work for you? This is the code I've used to do the same thing. The main difference I notice is that I don't use SetResolution (and I assume a square input and output, since that was the case for me).

/// <summary>
/// Resizes a square image
/// </summary>
/// <param name="OriginalImage">Image to resize</param>
/// <param name="Size">Width and height of new image</param>
/// <returns>A scaled version of the image</returns>
internal static Image ResizeImage( Image OriginalImage, int Size )
{
    Image finalImage = new Bitmap( Size, Size );

    Graphics graphic = Graphics.FromImage( finalImage );

    graphic.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighSpeed;
    graphic.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighSpeed;
    graphic.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;

    Rectangle rectangle = new Rectangle( 0, 0, Size, Size );

    graphic.DrawImage( OriginalImage, rectangle );

    return finalImage;
}

Upvotes: 1

Related Questions