MagicScaler is a high-performance image processing pipeline for .NET, focused on making complex imaging tasks simple.
It implements best-of-breed algorithms, linear light processing, and sharpening for the best image resizing quality available.
Speed and efficiency are unmatched by anything else on the .NET platform.
MagicScaler runs on Windows and Linux.
Linux hosting requires one or more of the cross-platform codec plugins available on nuget.org. Most common image formats are supported. Notable exceptions are support for BMP and TIFF images.
MagicImageProcessor.ProcessImage(@"\img\big.jpg", @"\img\small.jpg", new ProcessImageSettings { Width = 400 });
The above example will resize big.jpg
to a width of 400 pixels and save the output to small.jpg
. The height will be set automatically to preserve the correct aspect ratio. Default settings are optimized for a balance of speed and image quality.
The MagicScaler pipleline is also customizable if you wish to use an alternate pixel source, capture the output pixels for additional processing, or add custom filtering.
See the full documentation for more details.
Benchmark results in this section come from the tests used in https://blogs.msdn.microsoft.com/dotnet/2017/01/19/net-core-image-processing/ -- updated to use current (Nov 2022) versions of the libraries and runtime. The original benchmark project is also on GitHub.
For these results, the benchmarks were modified to use a constant UnrollFactor
so these runs more accurately report managed memory allocations and GC counts. By default, BenchmarkDotNet targets a run time in the range of 500ms-1s for each iteration. This means it executes slower benchmark methods using a smaller number of operations per iteration, and it can wildly under-report allocation and GCs, as those numbers are extrapolated from the limited iterations it runs. The constant UnrollFactor
ensures all benchmarks' reported memory stats are based on the same run counts. The UnrollFactor
used for each run is listed at the top of each set of results.
This is a semi-real-world image resizing benchmark, in which 12 JPEGs of approximately 1 megapixel each are resized to 150px wide thumbnails and saved back as JPEG. Not all libraries are supported on all platforms. See version notes below.
BenchmarkDotNet=v0.13.2.1974-nightly, OS=Windows 10 (10.0.19043.2006/21H1/May2021Update)
Intel Xeon W-11855M CPU 3.20GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.100-rc.2.22477.23
ShortRun : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2
Job=ShortRun IterationCount=5 LaunchCount=1 UnrollFactor=32 WarmupCount=3
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
|------------------------------------- |----------:|----------:|---------:|------:|--------:|----------:|----------:|----------:|-----------:|------------:|
| *MagicScaler Load, Resize, Save(1) | 46.85 ms | 2.948 ms | 0.456 ms | 0.13 | 0.00 | - | - | - | 42.37 KB | 4.65 |
| System.Drawing Load, Resize, Save(2) | 354.73 ms | 8.168 ms | 1.264 ms | 1.00 | 0.00 | - | - | - | 9.11 KB | 1.00 |
| ImageFlow Load, Resize, Save(3) | 226.48 ms | 2.284 ms | 0.353 ms | 0.64 | 0.00 | 968.7500 | 968.7500 | 968.7500 | 4627.13 KB | 508.17 |
| ImageSharp Load, Resize, Save(4) | 115.90 ms | 12.847 ms | 3.724 ms | 0.33 | 0.04 | 156.2500 | 62.5000 | - | 1532.8 KB | 168.34 |
| ImageMagick Load, Resize, Save(5) | 345.99 ms | 14.847 ms | 3.856 ms | 0.98 | 0.01 | - | - | - | 50.44 KB | 5.54 |
| ImageFree Load, Resize, Save(6) | 212.10 ms | 2.055 ms | 0.318 ms | 0.60 | 0.00 | 6000.0000 | 6000.0000 | 6000.0000 | 91.95 KB | 10.10 |
| SkiaSharp Load, Resize, Save(7) | 117.43 ms | 1.270 ms | 0.330 ms | 0.33 | 0.00 | - | - | - | 84.19 KB | 9.25 |
| NetVips Load, Resize, Save(8) | 100.92 ms | 4.383 ms | 0.678 ms | 0.28 | 0.00 | - | - | - | 115.9 KB | 12.73 |
BenchmarkDotNet=v0.13.2.1974-nightly, OS=Windows 11 (10.0.22621.674)
Snapdragon Compute Platform, 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.402
ShortRun : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD
Job=ShortRun IterationCount=5 LaunchCount=1 UnrollFactor=32 WarmupCount=3
| Method | Mean | Error | StdDev | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio |
|------------------------------------- |----------:|----------:|---------:|------:|---------:|---------:|-----------:|------------:|
| *MagicScaler Load, Resize, Save(1) | 65.72 ms | 0.532 ms | 0.082 ms | 0.17 | - | - | 42.36 KB | 4.65 |
| System.Drawing Load, Resize, Save(2) | 386.78 ms | 2.359 ms | 0.613 ms | 1.00 | - | - | 9.11 KB | 1.00 |
| ImageSharp Load, Resize, Save(4) | 287.06 ms | 3.125 ms | 0.812 ms | 0.74 | 375.0000 | 187.5000 | 1635.48 KB | 179.62 |
| ImageMagick Load, Resize, Save(5) | 588.63 ms | 13.049 ms | 2.019 ms | 1.52 | - | - | 50.44 KB | 5.54 |
| SkiaSharp Load, Resize, Save(7) | 158.27 ms | 1.816 ms | 0.472 ms | 0.41 | - | - | 82.32 KB | 9.04 |
| NetVips Load, Resize, Save(8) | 136.21 ms | 6.125 ms | 1.591 ms | 0.35 | - | - | 115.9 KB | 12.73 |
BenchmarkDotNet=v0.13.2.1974-nightly, OS=ubuntu 20.04
Intel Xeon W-11855M CPU 3.20GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=6.0.402
ShortRun : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2
Job=ShortRun IterationCount=5 LaunchCount=1 UnrollFactor=32 WarmupCount=3
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
|------------------------------------- |---------:|---------:|---------:|------:|--------:|---------:|---------:|---------:|-----------:|------------:|
| MagicScaler Load, Resize, Save(1) | 99.8 ms | 1.91 ms | 0.47 ms | 0.37 | 0.01 | - | - | - | 42.38 KB | 4.51 |
| System.Drawing Load, Resize, Save(2) | 271.7 ms | 12.34 ms | 3.20 ms | 1.00 | 0.00 | - | - | - | 9.4 KB | 1.00 |
| ImageFlow Load, Resize, Save(3) | 321.2 ms | 6.80 ms | 1.77 ms | 1.18 | 0.01 | 968.7500 | 968.7500 | 968.7500 | 4627.46 KB | 492.06 |
| ImageSharp Load, Resize, Save(4) | 226.5 ms | 4.09 ms | 1.06 ms | 0.83 | 0.01 | 156.2500 | 62.5000 | - | 1532.81 KB | 162.99 |
| ImageMagick Load, Resize, Save(5) | 522.6 ms | 27.02 ms | 7.02 ms | 1.92 | 0.04 | - | - | - | 50.84 KB | 5.41 |
| SkiaSharp Load, Resize, Save(7) | 338.4 ms | 38.62 ms | 10.03 ms | 1.25 | 0.04 | - | - | - | 84.48 KB | 8.98 |
| NetVips Load, Resize, Save(8) | 380.5 ms | 11.04 ms | 2.87 ms | 1.40 | 0.02 | - | - | - | 116.48 KB | 12.39 |
BenchmarkDotNet=v0.13.2.1974-nightly, OS=ubuntu 22.04
Unknown processor
.NET SDK=6.0.402
ShortRun : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD
Job=ShortRun IterationCount=5 LaunchCount=1 UnrollFactor=8 WarmupCount=3
| Method | Mean | Error | StdDev | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio |
|------------------------------------- |-----------:|---------:|--------:|------:|----------:|---------:|-----------:|------------:|
| *MagicScaler Load, Resize, Save(1) | 214.7 ms | 4.33 ms | 0.67 ms | 0.18 | - | - | 43.67 KB | 4.42 |
| System.Drawing Load, Resize, Save(2) | 1,205.9 ms | 26.54 ms | 6.89 ms | 1.00 | - | - | 9.87 KB | 1.00 |
| ImageSharp Load, Resize, Save(4) | 997.0 ms | 18.08 ms | 4.70 ms | 0.83 | 1625.0000 | 375.0000 | 1635.48 KB | 165.72 |
| ImageMagick Load, Resize, Save(5) | 1,688.5 ms | 12.84 ms | 3.34 ms | 1.40 | - | - | 51.47 KB | 5.21 |
| SkiaSharp Load, Resize, Save(7) | 279.7 ms | 1.87 ms | 0.48 ms | 0.23 | 125.0000 | - | 81.29 KB | 8.24 |
| NetVips Load, Resize, Save(8) | 421.5 ms | 14.72 ms | 3.82 ms | 0.35 | 125.0000 | - | 117.35 KB | 11.89 |
PhotoSauce.MagicScaler
version 0.13.2 with PhotoSauce.NativeCodecs.Libjpeg
version 2.1.4-preview1.System.Drawing.Common
version 5.0.3.Imageflow.AllPlatforms
Version 0.9.0.SixLabors.ImageSharp
version 2.1.3.Magick.NET-Q8-AnyCPU
version 12.2.0.FreeImage.Standard
version 4.3.8.SkiaSharp
version 2.88.3.NetVips
version 2.2.0 with NetVips.Native
(libvips) version 8.13.2.Note that unmanaged memory usage is not measured by BenchmarkDotNet's MemoryDiagnoser
, nor is managed memory allocated but never released to GC (e.g. pooled objects/buffers). See the MagicScaler Efficiency section for an analysis of total process memory usage for each library.
The performance numbers mostly speak for themselves, but some notes on image quality are warranted. The benchmark suite saves the output so that the visual quality of the output of each library can be compared in addition to the performance. See the MagicScaler Quality section below for details.
Raw speed isn't the only important factor when evaluating performance. As demonstrated in the parallel benchmark results above, some libraries consume extra resources in order to produce a result quickly, at the expense of overall scalability. Particularly when integrating image processing into another application, like a CMS or an E-Commerce site, it is important that your imaging library not steal resources from the rest of the system. That applies to both processor time and memory.
BenchmarkDotNet does a good job of showing relative performance, and its managed memory diagnoser is quite useful for identifying excessive GC allocations, but its default configuration doesn't track actual processor usage or any memory that doesn't show up in GC collections. For example, when it reports a time of 100ms on a benchmark, was that 100ms of a single processor at 100%? More than one processor? Less than 100%? And what about memory allocated but never collected, like object caches and pooled arrays? And what about unmanaged memory? To capture these things, we must use different tools.
Because most of the libraries tested make calls to native libraries internally (ImageSharp is the only pure-managed library in the bunch), measuring only GC memory can be very misleading. And even ImageSharp's memory usage isn't accurately reflected in the BDN MemoryDiagnoser
's numbers, because it holds allocated heap memory in the form of pooled objects and arrays (as does MagicScaler).
In order to accurately measure both CPU time and total memory usage, I devised a more real-world test. The 1-megapixel images in the benchmark test suite make for reasonable benchmark run times, but 1-megapixel is hardly representative of what we see coming from even smartphones now. In order to stress the libraries a bit more, I replaced the input images in the benchmark app's input folder with the Bee Heads album from the USGS Bee Inventory flickr. This collection contains 351 images (350 JPEG, 1 GIF), ranging in size from 2-22 megapixels, with an average of 13.4 megapixels. The total album is just over 2.5 GiB in size, and it can be downloaded directly from flickr.
I re-used the image resizing code from the benchmark app but processed the test images only once, using Parallel.ForEach
to load up the system. Because of the test image set's size, startup and JIT overhead are overshadowed by the actual image processing, and although there may be some variation in times between runs, the overall picture is accurate and is more realistic than the BDN runs that cycle through the same small set of small images. Each library's test was run in isolation so memory stats would include only that library.
This table shows the actual CPU time and peak memory usage as captured by the Windows Performance Toolkit when running the modified benchmark app on .NET 6.0 x64.
Method | Peak Memory | VirtualAlloc Total | CPU Time | Clock Time |
---|---|---|---|---|
*MagicScaler Bee Heads | 420 MB | 2389 MB | 37153 ms | 3.99s |
System.Drawing Bee Heads | 1001 MB | 36449 MB | 156050 ms | 39.58s |
ImageSharp Bee Heads | 1079 MB | 1101 MB | 96510 ms | 10.76s |
ImageFlow Bee Heads | 1485 MB | 28843 MB | 209247 ms | 22.61s |
ImageMagick Bee Heads | 878 MB | 17426 MB | 277780 ms | 29.86s |
FreeImage Bee Heads | 1048 MB | 16398 MB | 198207 ms | 21.77s |
SkiaSharp Bee Heads | 1273 MB | 26371 MB | 110919 ms | 12.13s |
NetVips Bee Heads | 785 MB | 3029 MB | 43515 ms | 5.01s |
It's clear from the CPU time vs wall clock time that System.Drawing is spending a fair amount of its time idle. Its total consumed CPU time is middle of the pack, but its wall clock time shows it to be the slowest by far.
Earlier runs of this test showed extreme total memory use in NetVips and ImageSharp, but they've both brough their memory use way down. MagicScaler still manages clear wins in peak memory and CPU time.
The benchmark application detailed above saves the output from its end-to-end image resizing tests so they can be evaluated for file size and image quality. The images were contributed by @bleroy, who created the original benchmark. They were chosen because of their high-frequency detail which had been observed to be challenging for some image resampling algorithms. The images have a couple of other interesting properties as well.
First, 10 of the 12 images are saved in the Abobe RGB color space, with an embedded color profile. Second, they have a significant amount of metadata related to the source camera and their processing history. Third, one of the images is in the sRGB color space and contains an embedded profile for sRGB, which is relatively large. These factors combine to highlight some of the shortcomings in the tested libraries.
There is one design flaw in the benchmark app that makes it more difficult to judge the output quality, however. The tests were configured to save the thumbnails with a low JPEG quality value and to use 4:2:0 chroma subsampling. While these values are acceptable or even ideal for large images, they will cause compression artifacts in areas of small details. At thumbnail size, the only details are small details, so it is better to use a higher-quality JPEG setting for very small images. MagicScaler performs this adjustment automatically under its default settings, but those are overridden in the benchmark suite to be consistent with the other libraries.
Note that outside the ill-advised overrides to JPEG quality settings, the results discussed here are from MagicScaler's default settings. Not only is MagicScaler faster and more more efficient than the other libraries, it is also easier to use. You'll get better quality with its defaults than can be obtained with lots of extra code in other libraries.
Handling images in a color space other than sRGB can be a challenge, and it's not something most developers are familiar with. Now that all iOS and most Android devices' camera apps default to the Display P3 color space and wide-gamut displays are common, this is a topic of increasing importance.
MagicScaler will automatically normalize the color space of images it processes to maximize compatibility with image consumers while preserving the original gamut of the image. Other software may not be color aware or may make it difficult to process in a color-correct manner.
The color difference between these should be obvious. Compared to the original image, it's easy to see which are correct (unless your browser is busted).
Of the libraries originally tested in the benchmark, only MagicScaler performs the resampling step in linear light. ImageSharp, ImageMagick, and Vips are capable of processing in linear light but would require extra code to do so and would perform significantly worse. ImageFlow is a recent addition which also processes in linear light by default.
Sample Images: | System.Drawing | MagicScaler | ImageSharp | Magick.NET | NetVips | FreeImage | SkiaSharp | ImageFlow | |----------------|-------------|------------|------------|---------|-----------|-----------|-----------| |||||||||In addition to keeping the correct colors, MagicScaler does markedly better at preserving image highlights because of the linear light blending. Notice the highlights on the flowers are a better representation of those in the original image
Most imaging libraries have at least some capability to do high-quality resampling, but not all do. MagicScaler defaults to high-quality, but the other libraries in this test were configured for their best quality as well.
Sample Images: | System.Drawing | MagicScaler | ImageSharp | Magick.NET | NetVips | FreeImage | SkiaSharp | ImageFlow | |----------------|-------------|------------|------------|---------|-----------|-----------|-----------| |||||||||FreeImage and SkiaSharp have particularly poor image quality in this test, with output substantially more blurry than the others. ImageFlow benefits from its linear light processing but also produces blurrier output than others.
Note that when the benchmark and blog post were originally published, Skia supported multiple ways to resize images, which is why there are two Skia benchmark tests. Under the current version of Skia, those two versions have the same benchmark numbers and same output quality, because the Skia internals have been changed to unify its resizing code. The lower-quality version is all that's left.
And here's that original image for reference.
Finally, MagicScaler performs a post-resizing sharpening step to compensate for the natural blurring that occurs when an image is resized. Some of the other libraries would be capable of doing the same, but again, that would require extra code and would negatively impact the performance numbers.
Sample Images: | System.Drawing | MagicScaler | ImageSharp | Magick.NET | NetVips | FreeImage | SkiaSharp | ImageFlow | |----------------|-------------|------------|------------|---------|-----------|-----------|-----------| |||||||||The linear light blending combined with the sharpening work to preserve more details from this original image than the other libraries do. Again, some details are mangled by the poor JPEG settings, so MagicScaler's default settings would do even better.
Also of note is that ImageSharp's and NetVips' thumbnails for this image are roughly twice the size of the others because they have embedded the 3KiB sRGB color profile from the original image unnecessarily.
This project is using semantic versioning. Releases without a Preview/RC tag are considered release quality and are safe for production use. The major version number will remain at 0, however, until the APIs are complete and stabilized.
Contributions are welcome, but please open a new issue or discussion before submitting any pull requests that alter API or functionality. This will hopefully save any wasted or duplicate effort.
PhotoSauce is licensed under the MIT license.