I have a service that resizes/crops images on demand by loading a full size file and cropping it, resizing it to the requested dimensions and requested quality.
public static byte[] Resize(Image sourceImage, int? targetWidth, int? targetHeight, int quality);
However the problem that I'm facing is that for certain dimensions the output image contains some 'noise' in the form of some faint keyline along the edges.
You can see an example here of how the image edges should be
and here how are actually returned
It adds a keyline only for certain pair of dimensions, the error is consistent and is independent from the quality (1-100) passed.
Following is the resizing code, also here a simple playground https://github.com/gromag/ImageResizeTest
Any advice?
using System;
using System.Collections.Generic;
using System.Drawing.Drawing2D;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
namespace grom.lib.graphics
{
public class ImageResizer
{
/// <summary>
/// A quick lookup for getting image encoders
/// </summary>
private static Dictionary<string, ImageCodecInfo> encoders = null;
/// <summary>
/// A quick lookup for getting image encoders
/// </summary>
public static Dictionary<string, ImageCodecInfo> Encoders
{
//get accessor that creates the dictionary on demand
get
{
//if the quick lookup isn't initialised, initialise it
if (encoders == null)
{
encoders = new Dictionary<string, ImageCodecInfo>();
}
//if there are no codecs, try loading them
if (encoders.Count == 0)
{
//get all the codecs
foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageEncoders())
{
//add each codec to the quick lookup
encoders.Add(codec.MimeType.ToLower(), codec);
}
}
//return the lookup
return encoders;
}
}
/// <summary>
/// *************************************************************
/// Resizes or crops an images to the requested width and height.
/// *************************************************************
/// If any dimension is not passed, the function will calculate the missed dimensions
/// If both dimensions are not passed, the function will return a *copy* of the original image
/// If any of the requested dimensions exceeds the original one's, this function will return null
/// If dimensions' ratio does not match the original ratio, clipping will occur.
/// </summary>
/// <param name="sourceImage"></param>
/// <param name="targetWidth"></param>
/// <param name="targetHeight"></param>
/// <returns>A bitmap, you will **need** to dispose of such image</returns>
public static byte[] Resize(Image sourceImage, int? targetWidth, int? targetHeight, int quality)
{
//w, h source width and height
int w = sourceImage.Size.Width;
int h = sourceImage.Size.Height;
//wt, ht requested width and height
int wt = 1;
int ht = 1;
//The new image would exceed the max boundary of the source image
if (targetWidth > w || targetHeight > h) return null;
if (targetWidth == null && targetHeight == null)
{
wt = w;
ht = h;
}
var sourceRatio = (double)w / (double)h;
//if no target width expressed then
//if w = sourceRatio * h
//then
//wt = sourceRatio * ht
wt = (int)(targetWidth ?? sourceRatio * ht);
//if no target height expressed then
//if h = w/sourceRatio
//then
//ht = wt/sourceRatio
ht = (int)(targetHeight ?? wt / sourceRatio);
var targetRatio = (double)wt / (double)ht;
#region ***Clipping explaination in visual terms
//Clip applied to original image before scaling
// If proportions are as follow:
//
// target 2:1 source 1:1
// ___________ _______________
// | | | |
// |___________| | |
// | |
// | |
// |_______________|
// Then we will clip as follows:
//
// clip to source
// _______________
// |_ _ _ _ _ _ _ _|
// | |
// | |
// |_ _ _ _ _ _ _ _|
// |_______________|
// or vertical clip instead if proportions are as follow:
//
// target 1:2 source 1:1
// ___ _______________
// | | | |
// | | | |
// |___| | |
// | |
// |_______________|
// Then we will clip as follows:
//
// clip to source
// _______________
// | ! ! |
// | ! ! |
// | ! ! |
// | ! ! |
// |____!_____!____|
#endregion
Rectangle clip;
if (targetRatio >= sourceRatio)
{
//The image requested is more elungated than the original one
//therefore we clip the height
//targetRatio = wt/ht
//ht = wt/targetRatio
//hClip = w/targetRatio
var hClip = (int)Math.Ceiling((double) w / (double)targetRatio);
//Rectangle pars are: x, y, width, height
clip = new Rectangle(0, (h - hClip) / 2, w, hClip);
}
else
{
//The image requested is more stretched in height than the original one
//therefore we clip the width
//targetRatio = wt/ht
//wt = targetRatio * ht
//hClip = targetRatio * h
var wClip = (int)Math.Ceiling((double)h * (double)targetRatio);
//Rectangle pars are: x, y, width, height
clip = new Rectangle((w - wClip) / 2, 0, wClip, h);
}
var targetImage = new Bitmap(wt, ht);
using (var g = Graphics.FromImage(targetImage))
{
g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
g.SmoothingMode = SmoothingMode.HighQuality;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
var targetRectangle = new Rectangle(0, 0, wt, ht);
g.DrawImage(sourceImage, targetRectangle, clip, GraphicsUnit.Pixel);
}
var bytes = ImageToByteArray(targetImage, ImageFormat.Jpeg, quality);
targetImage.Dispose();
return bytes;
}
/// <summary>
/// Given a System.Drawing.Image it will return the corresponding byte array given
/// a format.
/// </summary>
/// <param name="imageIn"></param>
/// <param name="format"></param>
/// <returns></returns>
public static byte[] ImageToByteArray(System.Drawing.Image imageIn, ImageFormat format, int quality)
{
//create an encoder parameter for the image quality
EncoderParameter qualityParam = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
//get the jpeg codec
ImageCodecInfo jpegCodec = GetEncoderInfo("image/jpeg");
//create a collection of all parameters that we will pass to the encoder
EncoderParameters encoderParams = new EncoderParameters(1);
//set the quality parameter for the codec
encoderParams.Param[0] = qualityParam;
//save the image using the codec and the parameters
var ms = new MemoryStream();
imageIn.Save(ms, jpegCodec, encoderParams);
return ms.ToArray();
}
/// <summary>
/// Converts a byte array to System.Drawing.Image.
/// IMPORTANT: You must dispose of the returned image.
/// </summary>
/// <param name="byteArrayIn"></param>
/// <returns>Returns an image of type System.Drawing.Image, you will have to take care of disposing it</returns>
public static Image ByteArrayToImage(byte[] byteArrayIn)
{
var ms = new MemoryStream(byteArrayIn);
var returnImage = Image.FromStream(ms);
return returnImage;
}
/// <summary>
/// Returns the image codec with the given mime type
/// </summary>
public static ImageCodecInfo GetEncoderInfo(string mimeType)
{
//do a case insensitive search for the mime type
string lookupKey = mimeType.ToLower();
//the codec to return, default to null
ImageCodecInfo foundCodec = null;
//if we have the encoder, get it to return
if (Encoders.ContainsKey(lookupKey))
{
//pull the codec from the lookup
foundCodec = Encoders[lookupKey];
}
return foundCodec;
}
}
}
EDIT:
Images uploaded above were closeup screenshots of the actual images, I'm adding here another couple of examples of the actual source and output image.
Source:
Output at 403x305 (keyline alongside the left edge):
The problem already starts from your source image. If you take a close look at it in Photoshop (or another editor), you'll notice that those artifacts are in the source image but much fainter. When scaling images, source pixels are not just copied to their new position, but interpolated. It means that in some cases the colors might change quite drastically. I changed the color levels a little to make it clear.
Here's a zoom in of the top part of the image:
Now the thing is, you obviously want to minimize the effects of bad source images getting their colors changed, thus making compression artifacts stand out even more. You should try to play around with the InterpolationMode and maybe some other quality settings.
Another idea would be to try using ImageMagick. Here's a .NET wrapper around it.