Categories

Thursday, January 19, 2017

.NET Core Image Processing

Image processing, and in particular image resizing, is a common requirement for web applications. As such, I wanted to paint a panorama of the options that exist for .NET Core to process images. For each option, I’ll give a code sample for image resizing, and I’ll outline interesting features. I’ll conclude with a comparison of the performance of the libraries, in terms of speed, size, and quality of the output.

CoreCompat.System.Drawing

If you have existing code relying on System.Drawing, using this library is clearly your fastest path to .NET Core and cross-platform bliss: the performance and quality are fine, and the API is exactly the same. The built-in System.Drawing APIs are the easiest way to process images with .NET Framework, but they rely on the GDI+ features from Windows, which are not included in .NET Core, and are a client technology that was never designed for multi-threaded server environments. There are locking issues that may make this solution unsuitable for your applications.
CoreCompat.System.Drawing is a .NET Core port of the Mono implementation of System.Drawing. Like System.Drawing in .NET Framework and in Mono, CoreCompat.System.Drawing also relies on GDI+ on Windows. Caution is therefore advised, for the same reasons.
Also be careful when using the library cross-platform, to include the runtime.osx.10.10-x64.CoreCompat.System.Drawing and / or runtime.linux-x64.CoreCompat.System.Drawing packages.


using System.Drawing;


const int size = 150;

const int quality = 75;


using (var image = new Bitmap(System.Drawing.Image.FromFile(inputPath)))

{

int width, height;

if (image.Width > image.Height)

{

width = size;

height = Convert.ToInt32(image.Height * size / (double)image.Width);

}

else

{

width = Convert.ToInt32(image.Width * size / (double)image.Height);

height = size;

}

var resized = new Bitmap(width, height);

using (var graphics = Graphics.FromImage(resized))

{

graphics.CompositingQuality = CompositingQuality.HighSpeed;

graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;

graphics.CompositingMode = CompositingMode.SourceCopy;

graphics.DrawImage(image, 0, 0, width, height);

using (var output = File.Open(

OutputPath(path, outputDirectory, SystemDrawing), FileMode.Create))

{

var qualityParamId = Encoder.Quality;

var encoderParameters = new EncoderParameters(1);

encoderParameters.Param[0] = new EncoderParameter(qualityParamId, quality);

var codec = ImageCodecInfo.GetImageDecoders()

.FirstOrDefault(codec => codec.FormatID == ImageFormat.Jpeg.Guid);

resized.Save(output, codec, encoderParameters);

}

}

}

ImageSharp

ImageSharp is a brand new, pure managed code, and cross-platform image processing library. Its performance is not as good as that of libraries relying on native OS-specific dependencies, but it remains very reasonable. Its only dependency is .NET itself, which makes it extremely portable: there is no additional package to install, just reference ImageSharp itself, and you’re done.
If you decide to use ImageSharp, don’t include the package that shows on NuGet: that’s going to be an empty placeholder until the first official release of ImageSharp ships. For the moment, you need to get a nightly build from a MyGet feed. This can be done by adding the following NuGet.config to the root directory of the project:


<?xml version="1.0" encoding="utf-8"?>

<configuration>

<packageSources>

<add key="ImageSharp Nightly" value="https://www.myget.org/F/imagesharp/api/v3/index.json" />

</packageSources>

</configuration>
Resizing an image with ImageSharp is very simple.


using ImageSharp;


const int size = 150;

const int quality = 75;


Configuration.Default.AddImageFormat(new JpegFormat());


using (var input = File.OpenRead(inputPath))

{

using (var output = File.OpenWrite(outputPath))

{

var image = new Image(input)

.Resize(new ResizeOptions

{

Size = new Size(size, size),

Mode = ResizeMode.Max

});

image.ExifProfile = null;

image.Quality = quality;

image.Save(output);

}

}
view raw ImageSharp.cs hosted with ❤ by GitHub
For a new codebase, the library is surprisingly complete. It includes all the filters you’d expect to treat images, and even includes very comprehensive support for reading and writing EXIF tags (that code is shared with Magick.NET):


var exif = image.ExifProfile;

var description = exif.GetValue(ImageSharpExifTag.ImageDescription);

var yearTaken = DateTime.ParseExact(

(string)exif.GetValue(ImageSharpExifTag.DateTimeOriginal).Value,

"yyyy:MM:dd HH:mm:ss",

CultureInfo.InvariantCulture)

.Year;

var author = exif.GetValue(ImageSharpExifTag.Artist);

var copyright = $"{description} (c) {yearTaken} {author}";

exif.SetValue(ImageSharpExifTag.Copyright, copyright);
view raw ImageSharpExif.cs hosted with ❤ by GitHub
Note that the latest builds of ImageSharp are more modular than they used to, and if you’re going to use image formats such as Jpeg, or image processing capabilities such as Resize, you need to import additional packages in addition to the core ImageSharp package (respectively ImageSharp.Processing and ImageSharp.Formats.Jpeg).

Magick.NET

Magick.NET is the .NET wrapper for the popular ImageMagick library. ImageMagick is an open-source, cross-platform library that focuses on image quality, and on offering a very wide choice of supported image formats. It also has the same support for EXIF as ImageSharp.
The .NET Core build of Magick.NET currently only supports Windows. The author of the library, Dirk Lemstra is looking for help with converting build scripts for the native ImageMagick dependency, so if you have some expertise building native libraries on Mac or Linux, this is a great opportunity to help an awesome project.
Magick.NET has the best image quality of all the libraries discussed in this post, as you can see in the samples below, and it performs relatively well. It also has a very complete API, and the best support for exotic file formats.


using ImageMagick;


const int size = 150;

const int quality = 75;


using (var image = new MagickImage(inputPath))

{

image.Resize(size, size);

image.Strip();

image.Quality = quality;

image.Write(outputPath);

}
view raw Magick.NET.cs hosted with ❤ by GitHub

SkiaSharp

SkiaSharp is the .NET wrapper for Google’s Skia cross-platform 2D graphics library, that is maintained by the Xamarin team. SkiaSharp is now compatible with .NET Core, and is extremely fast. As it relies on native libraries, its installation can be tricky, but I was able to make it work easily on Windows and macOS. Linux is currently more challenging, as it’s necessary to build some native libraries from code, but the team is working on ironing out those speedbumps, so SkiaSharp should soon be a very interesting option.


using SkiaSharp;


const int size = 150;

const int quality = 75;


using (var input = File.OpenRead(inputPath))

{

using (var inputStream = new SKManagedStream(input))

{

using (var original = SKBitmap.Decode(inputStream))

{

int width, height;

if (original.Width > original.Height)

{

width = size;

height = original.Height * size / original.Width;

}

else

{

width = original.Width * size / original.Height;

height = size;

}


using (var resized = original

.Resize(new SKImageInfo(width, height), SKBitmapResizeMethod.Lanczos3))

{

if (resized == null) return;


using (var image = SKImage.FromBitmap(resized))

{

using (var output =

File.OpenWrite(OutputPath(path, outputDirectory, SkiaSharpBitmap)))

{

image.Encode(SKImageEncodeFormat.Jpeg, Quality)

.SaveTo(output);

}

}

}

}

}

}
view raw SkiaSharp.cs hosted with ❤ by GitHub

FreeImage-dotnet-core

This library is to the native FreeImage library what Magick.NET is to ImageMagick: a .NET Core wrapper. It offers a nice choice of image formats, good performance, and good visual quality. Cross-platform support at this point is not perfect however, as I was unable to save images to disk on Linux and macOS. Hopefully that is fixed soon.


using FreeImageAPI;


const int size = 150;


using (var original = FreeImageBitmap.FromFile(path))

{

int width, height;

if (original.Width > original.Height)

{

width = size;

height = original.Height * size / original.Width;

}

else

{

width = original.Width * size / original.Height;

height = size;

}

var resized = new FreeImageBitmap(original, width, height);

// JPEG_QUALITYGOOD is 75 JPEG.

// JPEG_BASELINE strips metadata (EXIF, etc.)

resized.Save(OutputPath(path, outputDirectory, FreeImage), FREE_IMAGE_FORMAT.FIF_JPEG,

FREE_IMAGE_SAVE_FLAGS.JPEG_QUALITYGOOD |

FREE_IMAGE_SAVE_FLAGS.JPEG_BASELINE);

}
view raw FreeImage.cs hosted with ❤ by GitHub

MagicScaler

MagicScaler is a Windows-only library that relies on Windows Image Components (WIC) for handling the images, but applies its own algorithms for very high quality resampling. It’s not a general purpose 2D library, but one that focuses exclusively on image resizing. As you can see in the gallery below, the results are impressive: the library is extremely fast, and achieves unparalleled quality. The lack of cross-platform support is going to be a showstopper to many applications, but if you can afford to run on Windows only, and only need image resizing, this is a superb choice.


using PhotoSauce.MagicScaler;


const int size = 150;

const int quality = 75;


var settings = new ProcessImageSettings() {

Width = size,

Height = size,

ResizeMode = CropScaleMode.Max,

SaveFormat = FileFormat.Jpeg,

JpegQuality = quality,

JpegSubsampleMode = ChromaSubsampleMode.Subsample420

};


using (var output = new FileStream(OutputPath(path, outputDirectory, MagicScaler), FileMode.Create))

{

MagicImageProcessor.ProcessImage(path, output, settings);

}
view raw MagicScaler.cs hosted with ❤ by GitHub

Performance comparison

The first benchmark loads, resizes, and saves images on disk as Jpegs with a a quality of 75. I used 12 images with a good variety of subjects, and details that are not too easy to resize, so that defects are easy to spot. The images are roughly one megapixel JPEGs, except for one of the images that is a little smaller. Your mileage may vary, depending on what type of image you need to work with. I’d recommend you try to reproduce these results with a sample of images that corresponds to your own use case.
For the second benchmark, an empty megapixel image is resized to a 150 pixel wide thumbnail, without disk access.
The benchmarks use .NET Core 1.0.3 (the latest LTS at this date) for CoreCompat.System.Drawing, ImageSharp, and Magick.NET, and Mono 4.6.2 for SkiaSharp.
I ran the benchmarks on Windows on a HP Z420 workstation with a quad-core Xeon E5-1620 processor, 16GB of RAM, and the built-in Radeon GPU. For Linux, the results are for the same machine as Windows, but in a 4GB VM, so lower performance does not mean anything regarding Windows vs. Linux performance, and only library to library comparison should be considered meaningful. The macOS numbers are on an iMac with a 1.4GHz Core i5 processor, 8GB of RAM, and the built-in Intel HD Graphics 5000 GPU, running macOS Sierra.
Results are going to vary substantially depending on hardware: usage and performance of the GPU and of SIMD depends on both what’s available on the machine, and on the usage the library is making of it. Developers wanting to get maximum performance should further experiment. I should mention that I had to disable OpenCL on Magick.NET (OpenCL.IsEnabled = false;), as I was getting substantially worse performance with it enabled on that workstation than on my laptop.

LibraryLoad, resize, save (ms per image)Resize (ms per image)
CoreCompat.System.Drawing34 ± 116.0 ± 0.6
ImageSharp45 ± 110.1 ± 0.2
Magick.NET56 ± 224.1 ± 0.3
SkiaSharp20 ± 17.8 ± 0.1
FreeImage39 ± 112.9 ± 0.1
MagicScaler20 ± 1n/a
For both metrics, lower is better.
Image Resizing Performance (macOS)
LibraryLoad, resize, save (ms per image)Resize (ms per image)
CoreCompat.System.Drawing93 ± 171.5 ± 0.3
ImageSharp70.3 ± 0.327.6 ± 0.2
SkiaSharp15.9 ± 0.17.6 ± 0.1
FreeImagen/a12.8 ± 0.1
For both metrics, lower is better.
Image Resizing Performance (Linux)
LibraryLoad, resize, save (ms per image)Resize (ms per image)
CoreCompat.System.Drawing114 ± 592 ± 1
ImageSharp384 ± 5128 ± 1
FreeImagen/a29.7 ± 2
For both metrics, lower is better.
File Size
LibraryFile Size (average kB per image)
CoreCompat.System.Drawing4.0
ImageSharp4.0
Magick.NET4.2
SkiaSharp3.6
FreeImage3.6
MagicScaler4.3
Lower is better. Note that file size is affected by the quality of the subsampling that’s being performed, so size comparisons should take into account the visual quality of the end result.

Quality comparison

Here are the resized images. As you can see, the quality varies a lot from one image to the next, and between libraries. Some images show dramatic differences in sharpness, and some moirĂ© effects can be seen in places. You should make a choice based on the constraints of your project, and on the performance vs. quality trade-offs you’re willing to make.

1 comment:

  1. At the moment working with imageresizing.net looking into magicscaler. Maybe i will test it against MagicScaler

    ReplyDelete