Skip to content

Switch all resize tests to bicubic interpolation#17

Merged
bleroy merged 2 commits into
bleroy:masterfrom
kleisauke:bicubic-interpolation
Oct 16, 2019
Merged

Switch all resize tests to bicubic interpolation#17
bleroy merged 2 commits into
bleroy:masterfrom
kleisauke:bicubic-interpolation

Conversation

@kleisauke

Copy link
Copy Markdown
Contributor

This switches all resize tests to use bicubic interpolation. See the relevant discussion here: #15 (comment).

I've also slightly adapted the NetVips resize benchmark, it's now using the convenient vips_resize instead of the low-level vips_reduce operation. The CopyMemory() operations introduced in PR #16 doesn't seem quite right to me. I changed it to WriteToMemory() at the end of the chain to have libvips actually process pixels.

Benchmark results on Linux

Runtime information

BenchmarkDotNet=v0.11.5, OS=fedora 30
Intel Core i5-8600K CPU 3.60GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical cores
.NET Core SDK=3.0.100
  [Host]            : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT
  .Net Core 3.0 CLI : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT

Job=.Net Core 3.0 CLI  Toolchain=.NET Core 3.0  IterationCount=5  
LaunchCount=1  WarmupCount=5  

Before

|                    Method |      Mean |     Error |    StdDev | Ratio |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------------------- |----------:|----------:|----------:|------:|---------:|------:|------:|----------:|
|   'System.Drawing Resize' | 50.686 ms | 0.3015 ms | 0.0783 ms |  1.00 |        - |     - |     - |     128 B |
|       'ImageSharp Resize' |  5.599 ms | 0.1256 ms | 0.0326 ms |  0.11 |        - |     - |     - |    7440 B |
|      'ImageMagick Resize' | 31.789 ms | 0.2118 ms | 0.0550 ms |  0.63 |        - |     - |     - |    5656 B |
|        'FreeImage Resize' |  7.302 ms | 0.0637 ms | 0.0165 ms |  0.14 | 500.0000 |     - |     - |     136 B |
| 'SkiaSharp Canvas Resize' |  1.344 ms | 0.0053 ms | 0.0014 ms |  0.03 |        - |     - |     - |     636 B |
| 'SkiaSharp Bitmap Resize' |  1.326 ms | 0.0013 ms | 0.0003 ms |  0.03 |        - |     - |     - |     842 B |
|          'NetVips Resize' |  2.727 ms | 0.0116 ms | 0.0030 ms |  0.05 |        - |     - |     - |    5320 B |

After

|                    Method |        Mean |      Error |      StdDev | Ratio |    Gen 0 |  Gen 1 | Gen 2 | Allocated |
|-------------------------- |------------:|-----------:|------------:|------:|---------:|-------:|------:|----------:|
|   'System.Drawing Resize' | 50,828.5 us | 617.288 us | 160.3078 us |  1.00 |        - |      - |     - |     128 B |
|       'ImageSharp Resize' |  5,708.7 us |  82.080 us |  21.3159 us |  0.11 |        - |      - |     - |    7440 B |
|      'ImageMagick Resize' | 31,754.7 us | 254.287 us |  66.0376 us |  0.62 |        - |      - |     - |    5656 B |
|        'FreeImage Resize' |  7,286.5 us |  28.773 us |   7.4723 us |  0.14 | 500.0000 |      - |     - |     136 B |
| 'SkiaSharp Canvas Resize' |  1,338.3 us |   6.414 us |   1.6658 us |  0.03 |        - |      - |     - |     636 B |
| 'SkiaSharp Bitmap Resize' |    628.7 us |   2.284 us |   0.5933 us |  0.01 |        - |      - |     - |     841 B |
|          'NetVips Resize' |  1,107.2 us |  52.898 us |  13.7374 us |  0.02 |   3.9063 | 1.9531 |     - |   20360 B |
Benchmark results on Windows

Runtime information

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i5-8600K CPU 3.60GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical cores
.NET Core SDK=3.0.100
  [Host]            : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT
  .Net Core 3.0 CLI : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT

Job=.Net Core 3.0 CLI  Toolchain=.NET Core 3.0  IterationCount=5  
LaunchCount=1  WarmupCount=5  

Before

|                    Method |      Mean |     Error |    StdDev | Ratio | RatioSD |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------------------- |----------:|----------:|----------:|------:|--------:|---------:|------:|------:|----------:|
|   'System.Drawing Resize' | 10.369 ms | 0.1315 ms | 0.0341 ms |  1.00 |    0.00 |        - |     - |     - |     136 B |
|       'ImageSharp Resize' |  5.676 ms | 0.0196 ms | 0.0051 ms |  0.55 |    0.00 |        - |     - |     - |    7440 B |
|      'ImageMagick Resize' | 43.927 ms | 0.2877 ms | 0.0747 ms |  4.24 |    0.02 |        - |     - |     - |    5656 B |
|        'FreeImage Resize' |  7.884 ms | 0.0350 ms | 0.0091 ms |  0.76 |    0.00 | 500.0000 |     - |     - |     136 B |
|      'MagicScaler Resize' |  3.360 ms | 0.0777 ms | 0.0202 ms |  0.32 |    0.00 |  11.7188 |     - |     - |   59525 B |
| 'SkiaSharp Canvas Resize' |  4.958 ms | 0.0235 ms | 0.0061 ms |  0.48 |    0.00 |        - |     - |     - |     653 B |
| 'SkiaSharp Bitmap Resize' |  4.940 ms | 0.0222 ms | 0.0058 ms |  0.48 |    0.00 |        - |     - |     - |     848 B |
|          'NetVips Resize' |  4.901 ms | 0.1076 ms | 0.0279 ms |  0.47 |    0.00 |        - |     - |     - |    5320 B |

After

|                    Method |      Mean |     Error |    StdDev | Ratio |    Gen 0 |  Gen 1 | Gen 2 | Allocated |
|-------------------------- |----------:|----------:|----------:|------:|---------:|-------:|------:|----------:|
|   'System.Drawing Resize' | 10.344 ms | 0.1375 ms | 0.0357 ms |  1.00 |        - |      - |     - |     136 B |
|       'ImageSharp Resize' |  5.644 ms | 0.1020 ms | 0.0265 ms |  0.55 |        - |      - |     - |    7440 B |
|      'ImageMagick Resize' | 43.887 ms | 0.4111 ms | 0.1068 ms |  4.24 |        - |      - |     - |    5656 B |
|        'FreeImage Resize' |  7.887 ms | 0.0708 ms | 0.0184 ms |  0.76 | 500.0000 |      - |     - |     136 B |
|      'MagicScaler Resize' |  3.351 ms | 0.0059 ms | 0.0015 ms |  0.32 |  11.7188 |      - |     - |   59525 B |
| 'SkiaSharp Canvas Resize' |  4.973 ms | 0.0114 ms | 0.0030 ms |  0.48 |        - |      - |     - |     650 B |
| 'SkiaSharp Bitmap Resize' |  2.461 ms | 0.3670 ms | 0.0953 ms |  0.24 |        - |      - |     - |     844 B |
|          'NetVips Resize' |  1.157 ms | 0.0052 ms | 0.0014 ms |  0.11 |   3.9063 | 1.9531 |     - |   20360 B |

/cc @jcupitt, because he might also be interested in the numbers above.

Comment thread NetCore/Resize.cs Outdated
// until you write to an output file, buffer or memory
var memory = resized.WriteToMemory();

return (resized.Width, resized.Height, memory);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I returned the memory here to avoid dead code elimination: https://benchmarkdotnet.org/articles/guides/good-practices.html#avoid-dead-code-elimination

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't necessary in this instance, because the JIT has no way of knowing whether WriteToMemory mutates resized. The code will always run, whether you capture the result in a variable or not -- or return it or not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, resolved within: 8fbc3c7.

@jcupitt

jcupitt commented Oct 15, 2019

Copy link
Copy Markdown

Huh the benchmarks look nice Kleis. It's great you get the same time for NetVips on linux and win now.

@kleisauke

Copy link
Copy Markdown
Contributor Author

@jcupitt This is the 'simple' resize benchmark results, here are the 'Load, Resize, Save' benchmark results:

'Load, Resize, Save' benchmark results on Linux
|                                Method |      Mean |     Error |    StdDev | Ratio |     Gen 0 | Gen 1 | Gen 2 |  Allocated |
|-------------------------------------- |----------:|----------:|----------:|------:|----------:|------:|------:|-----------:|
|   'System.Drawing Load, Resize, Save' | 252.97 ms | 0.3161 ms | 0.0821 ms |  1.00 |         - |     - |     - |    9.47 KB |
|       'ImageSharp Load, Resize, Save' | 255.27 ms | 0.6467 ms | 0.1680 ms |  1.01 |         - |     - |     - | 1106.15 KB |
|      'ImageMagick Load, Resize, Save' | 349.53 ms | 2.9678 ms | 0.7707 ms |  1.38 |         - |     - |     - |    55.5 KB |
|        'ImageFree Load, Resize, Save' | 204.31 ms | 1.0210 ms | 0.2651 ms |  0.81 | 6000.0000 |     - |     - |   98.59 KB |
| 'SkiaSharp Canvas Load, Resize, Save' | 130.89 ms | 3.5666 ms | 0.9262 ms |  0.52 |         - |     - |     - |  189.43 KB |
| 'SkiaSharp Bitmap Load, Resize, Save' | 130.21 ms | 0.5455 ms | 0.1417 ms |  0.51 |         - |     - |     - |  185.57 KB |
|          'NetVips Load, Resize, Save' |  97.06 ms | 0.7699 ms | 0.1999 ms |  0.38 |         - |     - |     - |   92.27 KB |
'Load, Resize, Save' benchmark results on Windows
|                                Method |      Mean |    Error |    StdDev | Ratio |     Gen 0 | Gen 1 | Gen 2 |  Allocated |
|-------------------------------------- |----------:|---------:|----------:|------:|----------:|------:|------:|-----------:|
|   'System.Drawing Load, Resize, Save' | 376.67 ms | 2.716 ms | 0.7054 ms |  1.00 |         - |     - |     - |   10.02 KB |
|       'ImageSharp Load, Resize, Save' | 241.92 ms | 8.708 ms | 2.2616 ms |  0.64 |         - |     - |     - | 1103.41 KB |
|      'ImageMagick Load, Resize, Save' | 399.66 ms | 6.163 ms | 1.6004 ms |  1.06 |         - |     - |     - |   55.66 KB |
|        'ImageFree Load, Resize, Save' | 259.29 ms | 1.085 ms | 0.2818 ms |  0.69 | 6000.0000 |     - |     - |   95.67 KB |
|      'MagicScaler Load, Resize, Save' |  88.93 ms | 8.158 ms | 2.1187 ms |  0.24 |         - |     - |     - |  343.49 KB |
| 'SkiaSharp Canvas Load, Resize, Save' | 186.92 ms | 2.022 ms | 0.5250 ms |  0.50 |         - |     - |     - |  185.47 KB |
| 'SkiaSharp Bitmap Load, Resize, Save' | 186.67 ms | 2.204 ms | 0.5723 ms |  0.50 |         - |     - |     - |   182.3 KB |
|          'NetVips Load, Resize, Save' | 146.28 ms | 3.662 ms | 0.9510 ms |  0.39 |         - |     - |     - |   92.42 KB |

NetVips is still about 1.5 times slower on Windows than on Linux, but maybe we can't do much about it. The decrease in performance on Windows is also noticeable on the other image processing benchmarks.

Note that this is without the 'fair-competition patch' (YCbCr vs RGB – Catmull-Rom vs Lanczos3) mentioned in #15, that's why MagicScaler differs from the rest.

@jcupitt

jcupitt commented Oct 15, 2019

Copy link
Copy Markdown

Ah, interesting, I wondered if file IO was in there too.

I did notice a small performance issue yesterday with libpng read in libvips -- it's doing many more syscalls than it should. I don't suppose this is using libpng?

@kleisauke

Copy link
Copy Markdown
Contributor Author

Only jpeg images are tested with this benchmark, so this is using libjpeg-turbo. Regarding libpng, you might also be interested in this issue: lovell/sharp-libvips#25. See the relevant benchmarks: https://gist.github.com/kleisauke/879e3075a675c6f945dff3772d2ef0a3.

Hopefully libspng will stabilize soon, it should be up to 35% faster than libpng(!).

Comment thread NetCore/Resize.cs
using (var image = new ImageSharpImage(Width, Height))
{
image.Mutate(i => i.Resize(ResizedWidth, ResizedHeight));
image.Mutate(i => i.Resize(ResizedWidth, ResizedHeight, KnownResamplers.Bicubic));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little uncomfortable with this and I'd prefer to hear @JimBobSquarePants ' opinion on this line change: I'm all in favor of comparing oranges to oranges, but we're comparing images that will have some aesthetic differences in the end even if we use bicubic everywhere. Benchmark numbers are not fully meaningful unless you also consider the visual quality of the results, which is harder to quantify. As a consequence, it's possible that by forcing all benchmarks to use bicubic, we may just be degrading performance without necessarily bringing significantly better picture quality. As such, I'm hesitant to take this change unless there's evidence that this brings more homogeneity in image quality across libraries.

@antonfirsov antonfirsov Oct 15, 2019

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bicubic is the default for ImageSharp, so there is no actual change on this code line. This explains why ImageSharp before/after values remained unchanged.

On the other hand, we need to investigate if the current comparison is fair. If output quality is different and/or not all libraries are capable to handle certain cases, that should be pointed out in benchmark (result) comments or the blog post. For example: do all libraries implement premultiplication to prevent alpha bleeding?

@JimBobSquarePants is planning to have a look soon and come back with more specific details.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark numbers are not fully meaningful unless you also consider the visual quality of the results, which is harder to quantify.

I couldn't agree more here. Operations should consider far more than just the raw speed. Size, quality, correctness for example.

Here's a test image which demonstrates issues with several of the libraries. Only MagicScaler, ImageSharp, and System.Drawing get that right.

Note: I couldn't even get FreeImage to save an input png to jpeg with the current code so I'd consider that useless.

Input
kaboom

MagicScaler
kaboom-MagicScaler

NetVips
kaboom-NetVips

SkiaSharpBitmap
kaboom-SkiaSharpBitmap

SkiaSharpCanvas
kaboom-SkiaSharpCanvas

SystemDrawing
kaboom-SystemDrawing

ImageSharp
kaboom-ImageSharp

MagickNET
kaboom-MagickNET

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to say, the System.Drawing code is using some sort of sharpening via InterpolationMode.HighQualityBicubic. It also doesn't all subsampling so image quality at 75 will appear sharper than many of the others.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think most of the differences here are due to the background colour.

This test PNG has transparent pixels in a checkerboard pattern, so when you make a JPG thumbnail, the colour you get depends on what background colour you set. NetVips defaults to black (like skia I guess), so it does look rather dark. If you set a white background, or thumbnail to PNG, NetVips and MagickNET look the same (I suppose skia would too).

NetVips has an option for linear light downsampling, but I don't think this particular test image is a great way to show that. It seems to have a surprise hidden in the RGB of the transparent pixels, so I think this image is probably for testing premultiplication.

@bleroy

bleroy commented Oct 15, 2019

Copy link
Copy Markdown
Owner

Thanks for the contribution. I'm happy to take most of it, but I'd like @JimBobSquarePants ' opinion on the bicubic change to ImageSharp before I merge.

@saucecontrol saucecontrol left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kleisauke thanks for taking a look at this, and thanks for adding the NetVips benchmarks.

Can you give some explanation of the timing discrepancy between your version of the benchmark and mine? In my tests (on Windows 10), materializing only the original (black) image takes ~0.8ms. Materializing only the resized image takes ~1.5ms. Those timings were consistent whether I used CopyMemory or WriteMemory. It would seem to me that doing both should take ~2.3ms or less, but in fact my benchmark doing both takes ~5.2ms. That led me to believe that without materializing the original bitmap, the pixels don't exist and some shortcut is being taken internally. If you're confident the work is actually being done with the revised version, that's fine, but I'm not sure how the numbers can be that far off if that's the case.

For that matter, Skia also takes a shortcut in this benchmark. Because we start with a blank Canvas, it can create the blank image directly in video memory and do the scaling entirely in video, which is not possible with an actual decoded image. That's why its numbers are so low here (particularly in your Linux results) while being comparatively poor in the real-world tests. Skia is a poor choice for imaging tasks anyway, as explained in mono/SkiaSharp#520, but it would be nice if we could compare fairly.

And on the subject of fairness, I disagree with your characterization of the MagicScaler Y'CbCr JPEG processing as not fair. That falls under the category of "work smarter, not harder", and the output images prove that. It's not necessary to do the same work as long as the work that's done is correct.

And finally on that topic, it's worth pointing out that as of now, only System.Drawing, ImageSharp, and MagicScaler have correct output in the load/resize/save tests. All the others mangle the colors by dropping the embedded Adobe RGB ICC profiles in 10 of the 12 images. I see that libvips has the ability to do color profile conversions, and it would be nice to see its numbers when handling the colors correctly. I've been meaning to update the others as well. Skia and ImageMagick can also do color conversions -- not sure about FreeImage.

Comment thread NetCore/Resize.cs Outdated
Comment on lines +62 to +64
// 'Catrom' is generally imprecisely known as 'BiCubic' interpolation
image.FilterType = FilterType.Catrom;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment doesn't make sense, as Catrom is most certainly a cubic filter. Also, ImageMagick has a cardinal cubic filter as you've specified in the other tests: FilterType.Cubic

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why I missed the FilterType.Cubic filter. Resolved in: 8fbc3c7.

Note that ImageMagick seems to alias BiCubic to Catrom:

After IM v6.7.7-7 'BiCubic' is simply an alias to 'Catrom', which is typically regarded as a good 'cubic interpolator' (b=0, c=1/2). You should use the name 'Catrom' rather than 'BiCubic' so as to be clear what you are using for interpolation.

http://www.imagemagick.org/Usage/misc/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see what you meant now :)

Comment thread NetCore/Resize.cs Outdated
// until you write to an output file, buffer or memory
var memory = resized.WriteToMemory();

return (resized.Width, resized.Height, memory);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't necessary in this instance, because the JIT has no way of knowing whether WriteToMemory mutates resized. The code will always run, whether you capture the result in a variable or not -- or return it or not.

Comment thread NetCore/Resize.cs Outdated
using (var original = NetVips.Image.Black(Width, Height).CopyMemory())
using (var resized = original.Reduce(xFactor, yFactor, kernel: Enums.Kernel.Cubic).CopyMemory())
using (var original = NetVips.Image.Black(Width, Height))
using (var resized = original.Resize(1.0 / xFactor, vscale: 1.0 / yFactor, kernel: Enums.Kernel.Cubic))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just swap the numerator/denominator in the xFactor and yFactor calculations above?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, nice catch! Done in 8fbc3c7.

Comment thread NetCore/Resize.cs Outdated
{
using (var original = new SKBitmap(Width, Height))
using (var resized = original.Resize(new SKImageInfo(ResizedWidth, ResizedHeight), SKFilterQuality.High))
using (var resized = original.Resize(new SKImageInfo(ResizedWidth, ResizedHeight), SKFilterQuality.Medium))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find any documentation that describes what the SKFilterQuality values mean in terms of resampling kernel. How did you determine Medium refers to a cubic kernel?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. It seems that SkiaSharp does not have a cardinal cubic filter available:
https://github.com/mono/SkiaSharp/blob/9d3d4f577f1e78c1c64e03f5c4b95e3976fb0856/binding/Binding/SKBitmap.cs#L22-L33

Switched back to SKFilterQuality.High in 8fbc3c7.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find. Those mappings are perplexing. It seems there's no way of knowing what you're actually going to get, regardless of the fact they mapped the old cubic kernels to SKFilterQuality.High. For sure, given the poor quality of Skia's image interpolation, anything other than their highest quality would be unacceptable to most people.

- ImageMagick has a cardinal cubic filter available, so use that instead of Catrom.
- Use SKFilterQuality.High instead of SKFilterQuality.Medium.
- Simplify the xFactor and yFactor calculations.
- Just capture the result of WriteToMemory in a variable, there is no need to return it.
@saucecontrol

saucecontrol commented Oct 15, 2019

Copy link
Copy Markdown
Contributor

Based on mono/SkiaSharp#520 (comment) and the fact that the timing numbers and image quality are the same now, it seems that there is no longer any reason to have two versions of each Skia test. They appear to be doing the same thing internally at this point.

@kleisauke

Copy link
Copy Markdown
Contributor Author

Hi @saucecontrol,

Thank you for your comments and code suggestions! I guess the shortcut what you're seeing is due to the operation cache. Every time you call an operation, libvips searches the cache for a previous call to the same operation with the same arguments. If it finds a match, you get the previous result again. You can disable this behavior with:

NetVips.CacheSetMax(0);

You could also try to disable the run-time vector code generation and set the the number of worker threads to 1:

NetVips.VectorSet(false); 
NetVips.ConcurrencySet(1);

I should not call it 'fair-competition patch', sorry about that. I just wanted to explain why MagicScaler is a bit faster on Windows with the 'Load, Resize, Save' benchmark. The reason that libvips is slower here is because of the threading, thumbnailing to 150x150 pixels can be done quite quickly, so a large thread pool could slow down processing overall.

Regarding the correct output in the load/resize/save tests, could you try this patch?:

diff --git a/NetCore/LoadResizeSave.cs b/NetCore/LoadResizeSave.cs
index 1111111..2222222 100644
--- a/NetCore/LoadResizeSave.cs
+++ b/NetCore/LoadResizeSave.cs
@@ -187,9 +187,6 @@ namespace ImageProcessing
                 // Resize it to fit a 150x150 square
                 image.Resize(ThumbnailSize, ThumbnailSize);
 
-                // Reduce the size of the file
-                image.Strip();
-
                 // Set the quality
                 image.Quality = Quality;
 
@@ -331,7 +328,7 @@ namespace ImageProcessing
             using (var thumb = NetVipsImage.Thumbnail(input, ThumbnailSize, ThumbnailSize))
             {
                 // Save the results
-                thumb.Jpegsave(OutputPath(input, NetVips), q: Quality, strip: true);
+                thumb.Jpegsave(OutputPath(input, NetVips), q: Quality);
             }
         }
     }

Note that this also increases the file size, because EXIF data, IPTC data, or XMP metadata are now also being saved.

@jcupitt

jcupitt commented Oct 15, 2019

Copy link
Copy Markdown

This is an interesting discussion, thank you!

Re. the operation cache: all libvips operations are functional, that is, they have no side-effects. This means you can replace the result of any operation with the result of a previous call with the same arguments. By default, libvips aims to keep about 100mb of cached operations.

This can give misleading benchmark results if you loop several times over the same image. I usually try to have N distinct input images rather than looping N times over the same image.

@saucecontrol

Copy link
Copy Markdown
Contributor

Thanks for the info, guys! It sounds like from what @jcupitt said, the most correct thing to do is to call CopyMemory() on the Image.Black instance because that would, in fact, create a distinct source per operation, which is what all the others do and which is more in line with real-world use.

The worker thread issue is another matter, as I believe ImageSharp still does parallel processing internally as well. The load/resize/save parallel benchmark shows a severe per-image perf drop for NetVips, while ImageSharp suffers less, so the degree of internal parallel operation is definitely different. It would be good to normalize that.

@kleisauke I tried disabling the metadata stripping feature, but that took the thumbnail size from 4-5KiB per image to 35-50KiB. Although that did preserve the color profile, carrying the XMP data with it isn't optimal. Is there a way to only preserve metadata necessary for proper image rendering? I believe that's how ImageSharp is currently handling it. I also recall @bleroy encountering a similar issue with ImageMagick, which was preserving all metadata by default. That code was modified to strip metadata as well, with the same result (incorrect colors). If it's not possible to preserve just the color profile, it would be preferable to convert to sRGB, which is what the System.Drawing sample does and what MagicScaler does by default.

@antonfirsov

Copy link
Copy Markdown

@saucecontrol there is no parallel processing in ImageSharp resize since SixLabors/ImageSharp#888. (Except with NearestNeighbour resampler, but it's not being used in this benchmark).

@saucecontrol

saucecontrol commented Oct 16, 2019

Copy link
Copy Markdown
Contributor

Thanks, @antonfirsov, that's good to know! BTW, the ImageSharp performance gains since the first version of this benchmark project are really impressive. If I'm not mistaken, @JimBobSquarePants contributed this Resize benchmark in order to show that ImageSharp's resampling was faster than System.Drawing's even if it was a bit slower (at the time) in the end-to-end load/resize/save benchmark. I've always questioned the value of this specific benchmark given the many ways to cheat on it, and I'd personally be in favor of trashing it. The end-to-end processing is not only more representative of real-world scenarios, it's also easy to visualize the differences in processing output. Here -- just resizing empty pixels -- it's very difficult to tell what work is being done and what impact it would have on image quality, as evidenced by this thread.

@JimBobSquarePants

JimBobSquarePants commented Oct 16, 2019

Copy link
Copy Markdown
Contributor

@bleroy Bicubic is our default sampler so that's fine by me.

@saucecontrol

I've always questioned the value of this specific benchmark given the many ways to cheat on it, and I'd personally be in favor of trashing it.

I concur. It's not meaningful and barring exceptions the numbers are so small they should be considered "fast enough" for most users.

Thanks for the compliment regarding performance gains also! We've come a long way and I believe that once we begin implementing the Runtime.Intrinsics APIs we will be able to get very close to the best performing libraries.

@antonfirsov

antonfirsov commented Oct 16, 2019

Copy link
Copy Markdown

[Counterargument]

I think if we could follow a strict and systematic approach, we could ensure that the comparison is relatively fair:

  1. (Re)define the set of input images including corner cases (eg. the Davinci image posted by @JimBobSquarePants or any other image @saucecontrol or @jcupitt could contribute.)
  2. Harmonize the resize code even more, even if it does not affect performance in theory. (Eg. by harmonizing the default background color.)
  3. Compare the output images (by eye and/or by a tool), identify differences in quality (if any)

With this approach, I would say there is nothing wrong with these benchmarks, and they actually communicate useful information to library users, even if the performance differences are small.

@bleroy

bleroy commented Oct 16, 2019

Copy link
Copy Markdown
Owner

It's tangential to this particular PR, but yes, redefining the set of images used is a good idea. The original set was from my own collection, selected because they were problematic in one way or another with System.Drawing at the time. That was years ago, and it's time to revise that. It would be good to use images that library authors use for their own testing to assert quality. And of course, the images must be public domain or under a friendly license.

@saucecontrol

Copy link
Copy Markdown
Contributor

I totally agree this project could use a more diverse set of test images. In particular, I'd like to include a (maybe separate) suite of higher-resolution images more representative of today's cameras -- 8-12 megapixels for phones and 20+ megapixels for dedicated cameras.

As far as this PR goes, this is my understanding of the changes:

  1. Added explicit settings for Cubic filtering to MagicScaler, ImageSharp, and ImageMagick.

    • By default, MagicScaler uses a Catmull-Rom cubic resampler for jobs in this ratio range, so this is changing only the polynomials with no impact on performance.
    • By default, ImageSharp uses a cardinal cubic resampler, so this change is only making the default explicit.
    • By default, ImageMagick uses a Mitchell-Netravali cubic resampler for RGBA images, so this is changing only the polynomials with no impact on performance.

    As evidenced by the fact the perf numbers did not change for any of the above libraries, these changes were unnecessary but benign.

  2. Removed CopyMemory() call from original image in NetVips test.

    This change apparently enables a shortcut in libvips by allowing it to re-use the cached results of processing Image.Black between benchmark iterations, skipping the actual processing of its pixels. This should not be done.

  3. Removed CopyMemory() call from resized image in NetVips test and replaced with WriteToMemory().

    This essentially does the same work, so no harm in this.

  4. Replaced Reduce() call with Resize() in NetVips test.

    This seems like a perfectly reasonable change, as it's what a normal user would likely do. My understanding is that the mechanics of Resize() are slightly different, but as long as the result is good, it's a fair change.

@bleroy bleroy merged commit 3e29538 into bleroy:master Oct 16, 2019
@bleroy

bleroy commented Oct 16, 2019

Copy link
Copy Markdown
Owner

Thanks for the contribution and for the good discussion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants