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 sourceIm
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).
There are three reasons for the artifacts you're seeing:
Graphics.PixelOffsetMode
setting causes the pixel values to be sampled incorrectly, resulting in a slight distortion of the image, particularly around the edges.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.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.
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;
}
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
))
{
It's because sampling was taken from the edges of the photo.
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;
}
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
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.