Tales from the Evil Empire

Bertrand Le Roy's blog

News


Bertrand Le Roy

BoudinFatal's Gamercard

Tales from the Evil Empire - Blogged

Blogs I read

My other stuff

Archives

State of .NET Image Resizing: how does imageresizer do?

ThumbI've written several times before about image resizing in .NET and how the various built-in solutions (GDI, WPF and WIC) compare in terms of quality, speed and size. I'll put the links to my previous articles at the end of this post for reference.

Several readers have since pointed me to the imageresizer library, which is pure .NET and thus has no problems running in medium trust. Medium trust is an issue that has plagued existing options, preventing many people from using the best available approach. I was doubtful though that a purely managed library could come anywhere near the native Windows libraries in terms of performance. The best way to find out, of course, is to run a benchmark. Fortunately, I still had the code for my previous benchmarks so I just had to add imageresizer code to it.

I excluded IO from this test by measuring the processing time for each library working on binary streams. This also levels the playing field as some of them start processing while they are reading a file, making it harder to separate file reading overhead from image processing. By working with streams, the comparison is more meaningful. The timing code is wrapped around only the meaningful code.

The first problem that I hit with imageresizer is that it has a strong dependency on System.Web. This is really unfortunate and very probably unjustified. The library should be split between the pure image processing bits, which should have no System.Web dependency, and the request handling and caching bits, which can depend on System.Web all they want.

Since I'm mostly interested in web applications, I changed my command-line code into an ASP.NET Pages application, which probably saved me some configuration headache. The code is linked to at the end of this post. One thing I have to say is that the API feels good, natural and reasonably concise, a nice change when compared with the noisy Interop-like code we had to write before. It's nice to have something that is native .NET rather than a thin layer on top of Windows APIs that stink of C.

var settings = new ResizeSettings {
    MaxWidth = thumbnailSize,
    MaxHeight = thumbnailSize,
    Format = "jpg"
};
settings.Add("quality", quality.ToString());
ImageBuilder.Current.Build(inStream, outStream, settings);
resized = outStream.ToArray();

I really liked that I don't have to do size calculations myself, and I can just give it a square or rectangle inside of which it has to fit. You have many options to customize that. It's unfortunate that the quality setting has to be set this way instead of being promoted to a 1st class property like other settings but I can live with it. Notice how the library can work with streams, which is very important for web apps. One thing I really didn't like though was that some APIs, such as Build, take untyped object parameters and then decide internally at runtime what to do with them. Why the author didn't use strongly-typed overloads here is a mystery to me.

So how does imageresizer do then? Well, mostly as expected. In terms of size, I found it to be a little greedier than its peers, but really nothing dramatic:Size of resized images against jpg quality

The quality of the images is where imageresizer shines. See for yourself (quality 85% for those images):

imageresizer

WIC

Copenhagen_85 Copenhagen_85
IMG_2300_85 IMG_2300_85
IMG_2305_85 IMG_2305_85
IMG_2317_85 IMG_2317_85
IMG_2325_85 IMG_2325_85
IMG_2351_85 IMG_2351_85
IMG_2443_85 IMG_2443_85
IMG_2445_85 IMG_2445_85
IMG_2446_85 IMG_2446_85
IMG_2525_85 IMG_2525_85
IMG_2565_85 IMG_2565_85
IMG_2734_85 IMG_2734_85

The images look sharper (in particular the hummingbird one), and a little bit more saturated. Because of that, some moiré effects seem more visible (the shot before last makes that the most obvious. Still, the resizing is overall the best I've seen so far.

Now of course, while managed code is usually quite fast, image processing is heavy in the sort of operation where native code has a definitive advantage. That is reflected in the results I got in terms of processing speed:Time to resize against quality

Imageresizer is clearly the slowest. That was expected, but now we know by how much. It is 31% slower than GDI (I excluded the 50 quality point here), and almost 4 times slower than WIC.

Of course, unless your application is doing lots and lots of resizing operations, speed may not be that important, in particular if you are doing appropriate disk caching. Imageresizer shines in that department by making disk caching super-easy. The library, after all, is very web centric and tries hard to do everything right for the web scenarios. We should not underestimate how hard it is to get web image resizing right.

That leaves us with the unfortunate conclusion that we still don't have an image resizing silver bullet. I'll try to summarize all that we know in a table that will hopefully help you make a decision in the context of your specific application:

  Imageresizer WIC WPF GDI
Speed * **** **** **
Quality ***** **** **** ***
Medium trust Yes No No Yes
Supported By author By MS No No
.NET friendly **** * **** ***
Web centric Yes No No No

Benchmark code:
http://weblogs.asp.net/blogs/bleroy/ImageResizeBenchmark.zip

Results:
http://weblogs.asp.net/blogs/bleroy/ImageResizerBenchResults.zip

Previous articles:

The imageresizer library:
http://imageresizing.net/

Comments

Christopher Edwards said:

Very useful, thank you.  Please keep it up if you see any other options for image resizing in the future!

# October 24, 2011 4:58 AM

Chris Pont said:

I brought a copy of imageresizer a while back and as the above shows, it's not the quickest, but it is super easy to implement. Unless you're really doing a lot of intensive image processing, it's a great choice.

# October 25, 2011 6:23 AM

Alexandre Jobin said:

thank you for your review. I was looking for this kind of solution. I will give it a try!

# October 26, 2011 9:20 AM

Axel said:

Did you post the original images somewhere?

# October 26, 2011 9:57 AM

Mike said:

If it's impossible to get native code faster, then there is no silver bullet. Also, with caching to disk an image should only need to be resized once to each size you need. So performance is mostly an issue when doing batches. And if you are doing batches, then you should definitely not do that on the web server process. offload it to a service that can use native code and wait for the result, problem solved no?

I think imageresizer looks like a great tool, possibly the best.

Thanks for the write up.

# October 26, 2011 11:03 AM

Jeff said:

This is good stuff. What's really unfortunate, as you said, is the dependencies on System.Web. If you remove that, you have a solution that works in Silverlight, which frankly we are dying for in a lot of different cases.

# October 26, 2011 12:51 PM

Bertrand Le Roy said:

@Axel: some of them are on my Flickr (www.flickr.com/.../boudin). If you need a specific one just tell me and I'll make it available.

# October 26, 2011 1:01 PM

Axel said:

Yes the original hummingbird that you used please, this is the only image where I see a notable sharpness issue (one that can be seen without the need for side-by-side comparision).

There is a little more aliasing on the WIC side, visible on IMG_2351_85 and IMG_2565_85 but, overall, nothing worth a 4x slowdown in my opinion.

I'd like to try resizing the hummingbird with a couple of desktop apps of mine.

# October 26, 2011 7:44 PM

Peter said:

Thanks, very useful information and benchmarks.

# October 27, 2011 11:41 AM

Bertrand Le Roy said:

@Axel: you can doanload the hummingbird image from here: www.flickr.com/.../photostream

# October 27, 2011 8:11 PM

nathanael.jones@gmail.com said:

Hey, thanks for the article :) I'll try to answer your questions.

The default ImageResizer pipeline is GDI, and GDI = 1.3xGDI is an impossible equation, even if you grant 1ms for the processor to perform a couple dozen math operations.

The performance difference between the image resizer and GDI is likely due to different quality settings. The download doesn't seem to include the code you used to benchmark them, so I can't be sure - but it's consistent with the 30% performance improvement provided by the SpeedOrQuality plugin. So if you prefer to trade, the image resizer gives you that option via speed=0..3.   The ImageResizer really deserves 2 stars here  - not fair to punish it for trading speed for quality :)

Also, it's worth taking note of the new FreeImage plugins. I've been working with the authors of libjpeg-turbo to get the world's fasted jpeg encoder merged into FreeImage (which is already integrated with the image resizer via a plugin).

I'm seeing a 4x improvement in jpeg encoding speed, ignoring the overhead caused by the bitmap data conversion, and the file sizes are looking better as well.  I decided to pursue this path instead of WIC because it offered better cross-platform support (Multithreaded WIC is only WS2008+) and much better file format support. I may resume work on a WIC plugin if I can't beat WIC's speed with FreeImage.

I use untyped source and destination objects because I offer support for .NET 4 datatypes, yet can't reference them directly from .NET 2.0. Also, the permutations would require 4x10x3= 120 overloads.  The untyped API is also COM-compatible (COM doesn't support overloads).

Regarding the System.Web dependency - switching from Client Profile to the full .NET framework in Build Properties would have resolved the issue - no configuration issues :)  I have considered creating a separate version of the product that doesn't reference System.Web, but splitting them would be bad for 95% of users. Perhaps in v4 I'll be able to do this, but right now the code isn't organized that way. Silverlight doesn't support System.Drawing, sorry to disappoint you, Jeff.

The ImageResizer does allow alternate pipelines to be used (like FreeImage), so a WIC pipeline is definitely a possibility. I posted the idea on the http://resizer.uservoice.com/ forum back in april http://resizer.uservoice.com/forums/108373-image-resizer-v3/suggestions/1758591-fulltrustwicplugin, but nobody seemed interested.

I suspect it's mostly due to the full trust/WS2008 requirement.

Anyhow,

Thanks for getting involved and taking the time to benchmark the software. It does seem to be slower than WIC's lowest quality mode, but in a cached scenario it's quite difficult to beat context.RewritePath :) I do take performance very seriously, and I have hundreds of benchmarks that I run each time I change relevant code - tests that break the performance down to a granular level and let me know exactly which GDI calls are fastest under which conditions, with which file formats.

I also look at performance statistics for more than simple resizing. The image resizer is optimized to handle complex operations and coalesce them into a 1 or 2 GDI calls - cropping actually makes the image resizer faster, and rotation adds almost no overhead.

I'm currently working on V3.1, which includes the SpeedOrQuality plugin, and includes a libjpeg-turbo version of FreeImage and both encoder and decoder plugins for it. Overall performance speedups range from 10% to 200% for jpegs, but for tiffs it can be 4 to 12x.

It's sad how benchmarks seem to get outdated before you upload them :)

# October 29, 2011 12:27 AM

Bertrand Le Roy said:

@Nathanael: thanks for the comments. The benchmark code is actually available up there. I think the comparison is fair as the GDI code is not using the default settings, but rather my own custom configuration of it (bicubic high quality). I'd be interested to know where the difference comes from, if you can profile the benchmark code, that would probably give some interesting insights.

I will try that plugin when I get the time.

Careful with WIC, as you will lose medium trust if you go this way, but maybe that's already the case with FreeImage?

It will also be interesting to see what things look like on Win8 as we should have new managed APIs with hardware acceleration.

The thing about .net 4 types is unfortunate, but I still think having overloads with the most common usages would be better than the current situation. Also, options structures are a good way of alleviating combinatorial problems like the one you're facing.

Let me know when 3.1 is available and I'll gladly post an update.

# October 29, 2011 1:48 AM

Bertrand Le Roy said:

@nathanael: I haven't seen GC problems, the code I've been using is pretty close to what's in the benchmark, but I think that's where the real value of your library is: you've figured it all out for all of us ;)

# October 31, 2011 5:53 PM

nathanael.jones@gmail.com said:

Saturday I posted a (long) comment in between the two you've approved above. Is the comment still sitting in your moderation queue? I'll repost it again in case it somehow vanished from the database.

# October 31, 2011 8:15 PM

nathanael.jones@gmail.com said:

[Repost - originally posted Sat, Oct 29, 2011 at 9:11 PM GMT]

It looks like the first time I downloaded it I got a corrupted zip, but it's working now.

Your code is not using bicubic high quality - it is using Bicubic. Bicubic is a single-pass algorithm good for small resolution changes. BicubicHighQuality is a 2-pass algorithm that pre-filters the image, and is suitable for making thumbnails.

Naturally (and as your charts show) 2-pass filters are slower than 1-pass filters. It's not exactly a fair comparison. There were also some inconsistencies in the benchmark, but they're not probably that significant (although I did rewrite it to eliminate them all).

Another note: Improved resizing quality is often compounded by  slower encoding as the image is more detailed and requires more CPU cycles (and space) to encode it. It's quite apparent once you eliminate all the other factors except the resizing filter - the same encoder, same code, same settings, same file can have a 10% file size variation based on the image resampling filter used.

To see how the ImageResizer would fare under WIC, I wrote a basic WicBuilderPlugin and ran the benchmarks. It seems to be a bit faster than your WIC and WPF code.

And, using the Speed plugin to adjust the resampling to Bilinear, it beats all the GDI benchmarks by a wide margin.

The chart:

downloads.imageresizing.net/Oct29-2011-comparison.png

The spreadsheet:

downloads.imageresizing.net/Oct29-2011-Comparison.xlsx

Note that the FreeImage plugin is actually the slowest by a small margin. While FreeImage has fast encoders and decoders, its resizing algorithms (while the best of the bunch - catmullrom/lanczos3) are very, very slow. FreeImage is still being improved, so it's possible that may change shortly.

I also added a 600x600 resize to the chart so you can see how things change when the output size increases. It's interesting to note that quality=100 at 150x150 is slower than quality=10 for 600x600. That's true across the board for every encoder (WIC, GDI, and FreeImage).  Jpeg quality=100 is definitely much slower than 90, and it's not always better quality on GDI, although in theory it should be.

Anyhow, I think it's safe to say that you can get any balance of speed and quality you want out of the ImageResizer, and while I don't believe in silver bullets, it's probably the closest thing available.

Question:

Is the WIC interop code under the MsPl?  If so, I can go ahead and make the WicPlugin available as an alpha plugin in the next release.

Also, if your benchmarking code (default.cshtml and Utils.cs) is under the BSD/MIT/MsPl (or something similar), I can upload my rewritten version so you can take a look at the code that generated the results. Do note you need to watch memory usage with the benchmark app, after a few minutes the GC starts cleaning upp all those MemoryStreams and it affects the results very significantly.

# October 31, 2011 8:16 PM

Bertrand Le Roy said:

@Nathanael: that is great to know. To explain a little more why I chose those parameters, those are the ones I care about, the ones I use in my own private photo album. I use 150x150 thumbs, and I set the quality and compression to what I considered was the best compromise between speed and quality. I have to admit I didn't configure imageresizer and left the default parameters. I'll do a follow-up and an update when I get the time to re-run the benchmark.

Yes, the wic plugin stuff was made by the wic team and is absolutely free to use (it was released as a sample a while ago). My benchmark code is public domain, please feel free to use, modify, whatever you want. I'd love to get a corrected version from you.

Thanks.

# November 1, 2011 1:27 AM

Axel said:

Minor point: 150px is not the best choice for any JPEG. Try 144 or 152.

Thanks for the pointer to the humminbird image. I was hoping to get the original full-res image, not a Flickr re-compressed/rescaled one, but anyway I did not see the blurring that you got.

Besides that I believe that what you (all) refer to as GDI is actually GDI+, an entirely different API!

# November 1, 2011 8:01 PM

nathanael.jones@gmail.com said:

@Bertrand

Apologies that I haven't uploaded the benchmark code yet; I've been down with the flu since the 1st.

As I plan on releasing at least an alpha version of V3.1 in the next two weeks, I think I will include both the WicPlugin and the updated benchmark code in that version. This way everyone will be able to see the full source code for all involved software in the benchmark - right now I'm benching against development version, since the public release doesn't have the SpeedOrQuality or WicPlugins yet. (I started working on the SpeedOrQuality plugin a month ago, but this post definitely gave it more attention).

I'd be happy to post a link here first so you can error-check me; I'm as prone to benchmarking errors as the next guy, and it's hard enough to get repeatable results  on a multithreaded OS even if your benchmark code is perfect :)

I'm going to try adding pauses between the loops and forcing a GC regularly so results don't get upset by a GC of the MemoryStream instances or the async writes to the filesystem. Actually, I might make filesystem writes optional, so the filesystem driver doesn't mess with the results (hey, writing 10,000+ files makes a queue)...  I'd like to get perfectly stable results before I re-release the code, otherwise it will probably cause confusion. The chart I posted above I had to re-run several times to get a stable/sensible result without any spikes.

@Axel

Yeah, in .NET we work with GDI+ exclusively, so somewhere along the way from brain to keyboard the + symbol falls off...

I noticed you develop WIC-compatible codecs? I'd be interested in benchmarking them and ensuring they're compatible with the ImageResizer. Is there a way to enable/disable them via a registry key?  Also, what do you charge for server licenses?

# November 4, 2011 6:00 PM

nathanael.jones@gmail.com said:

Update on WIC and ReleaseComObject:

I've been reading all of the WIC documentation and all the sample code, and it seems Microsoft's pattern is to religiously call Marshal.ReleaseComObject on every single WIC object they use. That's in a client app, even - not a server situation where memory leaks are a far bigger concern.

They even made an extension method to make it easier (reformatted):

public static void ReleaseComObject(this object o)

{

   if (o == null) return;

   if (o.GetType().IsArray) { foreach (object i in o as Array) i.ReleaseComObject(); }

   else Marshal.ReleaseComObject(o);

}

So, I think the advice here is that .ReleaseComObject() is probably necessary.  It'd be nice to get the WIC team to confirm, but their example code is enough proof for me: archive.msdn.microsoft.com/wictools

# November 8, 2011 7:44 PM

nathanael.jones@gmail.com said:

Hey, is the comment I posted 2 days ago regarding WIC and Marshal.ReleaseComObject still in moderation, or are some comments getting lost?

# November 10, 2011 8:45 AM

Bertrand Le Roy said:

@Nathanael: your comments are going directly to the spam folder for some reason, sorry about that. I just rescued two of your comments from there.

# November 10, 2011 3:54 PM

Nathanael Jones said:

What spam filtering system does it use?

# November 10, 2011 5:21 PM

Bertrand Le Roy said:

I don't know, it's whatever Community Server uses. In other words it's a piece of crap.

# November 14, 2011 6:39 PM

nathanael.jones@gmail.com said:

Hi Bertrand, just wanted to let you know that V3.1 has been released at imageresizing.net/.../3-1-alpha-2

Looking forward to your update :)

# December 12, 2011 1:57 AM

Leoo said:

thanks for these great articles - very helpful!

the imageresizing.net library sounds great, and I'd love to give it a try.

one little question though:

it seems to be using GDI internally by default. Does this imply it's not officially supported by MS for use with ASP.NET, as mentioned in your GDI imaging resizing article?

In any case, if imageresizer is indeed using GDI then it does seem to provide some empirical evidence that, with careful design, GDI-based image resizing can be safe enough in an ASP.NET environment? Would you agree?

# April 27, 2012 6:07 AM

Bertrand Le Roy said:

@Leoo: it's not a Microsoft library so of course it's not supported by Microsoft. It does use GDI internally but the author has made an outstanding job ironing out the problematic scenarios. In summary you could say that it's GDI that works. So yes I would agree.

# May 1, 2012 9:54 PM