Reputation: 4443
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:
Upvotes: 13
Views: 13942
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).
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;
}
Upvotes: 8
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
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
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
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
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
Reputation: 16435
It's because sampling was taken from the edges of the photo.
Upvotes: 1
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