13

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

Camilo Martin
  • 37,236
  • 20
  • 111
  • 154
Ievgen
  • 4,261
  • 7
  • 75
  • 124
  • 2
    What does the HTML look like that's sent to the browser for these images? – DOK Dec 07 '09 at 17:01
  • 2
    What is the original type of the image? – Adriaan Stander Dec 07 '09 at 17:02
  • 1
    I had the same problem and posted my solution. – Jeroen Aug 07 '11 at 17:20
  • 2
    The image is no longer up, but if you were seeing a 1px border on the edge, you fix that by passing an ImageAttributes instance to DrawImage() with TileFlipXY set. That makes the interplation re-use the outer pixel edge instead of averaging it against the background or transparent color. Source: http://imageresizing.net/ – Lilith River Dec 09 '11 at 07:34

9 Answers9

8

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

Community
  • 1
  • 1
marapet
  • 54,856
  • 12
  • 170
  • 184
8

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;
}
Jonas Äppelgran
  • 2,617
  • 26
  • 30
saucecontrol
  • 1,446
  • 15
  • 17
  • i have tired as mentioned above but yet grey border appear :( https://stackoverflow.com/questions/63087257/grey-borders-in-drawimage-net-system-drawing-drawing2d – Salman Jul 25 '20 at 12:25
  • The example you linked does not fill the entire bitmap/canvas, which is a different issue, although the behavior is the same as using an incorrect `PixelOffsetMode`. I left an answer on that question. – saucecontrol Jul 26 '20 at 20:26
  • 2
    This is the only correct answer here right now. Using both WrapMode.TileFlipXY and PixelOffsetMode.Half. I recommend only setting the wrap mode for jpg, otherwise semi-transparency/antialiasing is lost near the edge for png's with transparency. – Jonas Äppelgran Oct 20 '20 at 12:52
  • 1
    Real solution indeed! – hubert17 Apr 21 '21 at 20:18
  • My code run fine on FW 4.8 and get the black line on the top of some images with NET 5. With this solution I solved. Tnx – bertasoft Jun 18 '21 at 11:38
7

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.

Camilo Martin
  • 37,236
  • 20
  • 111
  • 154
  • @Tillito No problem :) Be aware though that this is, albeit effective, not the most optimal solution, performance-wise. It shouldn't matter much unless you have a really big amount of images or you're making a custom component to be distributed, re-used etc., where you may want some more complex method of fixing this, such as using [lockbits](http://msdn.microsoft.com/en-us/library/system.drawing.bitmap.lockbits.aspx) and drawing/interpolating "manually". – Camilo Martin Apr 13 '13 at 00:16
6

Try:

graphic.CompositingMode = CompositingMode.SourceCopy;
userx
  • 3,769
  • 1
  • 23
  • 33
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • 4
    Unfortunately this is no solution at all. It has no effect. The gray border keeps being generated. If it worked for the OP then by mere accident. The solution lies with specifying image attributes as pointed out in this answer: http://stackoverflow.com/questions/2319983/resizing-an-image-in-asp-net-without-losing-the-image-quality/2324414#2324414 –  Mar 23 '11 at 15:29
  • 4
    @Developer Art, is your source 24-bit or 32-bit with alpha? Resizing an image with alpha will result in semi-transparent pixels along the edges; by setting this mode those pixels will be blended with the background, otherwise they're blended with black thus causing the borders. I don't know what happens with 24-bit images. – Mark Ransom Mar 23 '11 at 15:40
  • 1
    @Mark Ransom, See my answer, you're partially right but didn't give the right solution. – Jeroen Aug 07 '11 at 17:14
  • 1
    See the answer by @marapet below. That's the only one that worked for me. – Andy Dec 27 '16 at 03:31
6

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.

Jeroen
  • 4,023
  • 2
  • 24
  • 40
1

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;
}
Dov
  • 15,530
  • 13
  • 76
  • 177
  • 2
    He created an image from scratch which has a default black background. You're loading an image which might have no background. – Jeroen Aug 07 '11 at 17:19
1

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

Chuck Conway
  • 16,287
  • 11
  • 58
  • 101
1

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 
                ))
            {
Stefan Steiger
  • 78,642
  • 66
  • 377
  • 442
0

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.

Lucero
  • 59,176
  • 9
  • 122
  • 152
  • @Downvoter, I'd really appreciate a comment saying why my answer was so inappropriate or bad... – Lucero Jan 03 '12 at 23:46