Wednesday, June 12, 2019

Drawing Lines in .NET Core: Comparing ImageSharp and System.Drawing.Common

This is a follow-up post from an early experiment I did almost 3 years ago: https://build-failed.blogspot.com/2016/08/creating-simple-tileserver-with-net.html

At the time, I was trying to setup a simple Tile Server in C# that ran in .NET Core in Win, Mac and Linux, generating images on-the-fly. As I didn't find any C# lib that supported drawing lines and such, I built it myself (see above blog-post for details) over a lib called "ImageProcessor", which allowed me to set individual pixels.

Now, as I'm doing another project that also requires drawing stuff I decided to see how much the landscape has evolved over the last couple of years.

Spoiler alert, it changed significantly. As a summary:
  • ImageProcessor is now marked as "in soft archive mode", with focus shifting to another library called ImageSharp, from the same author
  • ImageSharp now supports Drawing functionality and actually seems very feature rich on that regard. Also, it's fully managed, not relying on native OS calls. If any of you worked with GDI+ in the past, you'll probably remember the mem leaks and thread-"unsafety" of it, particularly if trying to use it on the web.
  • Meanwhile, Microsoft released a Nuget package called "Microsoft.Drawing.Common", which is part of the Windows Compatibility Pack, aiming to help developers migrate their existing .NET code to .NET Core
  • As opposed to ImageSharp, Microsoft.Drawing.Common acts a a bridge to OS specific logic. In Windows it relies on GDI+, on Linux requires installing libgdiplus (from Mono).
I'll quote Scott Hanselman on this (https://www.hanselman.com/blog/HowDoYouUseSystemDrawingInNETCore.aspx):
There's lots of great options for image processing on .NET Core now! It's important to understand that this System.Drawing layer is great for existing System.Drawing code, but you probably shouldn't write NEW image management code with it. Instead, consider one of the great other open source options.
With that said, I'm very interested to understand how both libs compare, thus I've setup a very, very simple test. I'm not going to test the functional differences between both libs and I'm going to focus on the use-case that's most relevant to me: drawing lines.

All the relevant code is available on the Gitrepo: https://github.com/pmcxs/core-linedrawing-benchmark/

The following options can parametrized: number of lines being generated, image size and line width.

Ok, let's start:

Basic Functionality

Starting with a simple test, just to compare (visually) how both results fare:

Number of Lines: 10
Image Size: 800 px
Line Width: 10 px
(default zoom)
First the good news: both work :)

I did find a small difference on the output though. Although the images are very similar, both libraries handle corners differently (at least on the default behavior).

System.Drawing creates a strange protrusion on sharp edges. Zooming in on the previous image:
(zoomed in)
That seems really strange. Increasing the line thickness the effect looks even worse.

Number of Lines: 10
Image Size: 800 px
Line Width: 50 px

(default zoom)
Yeah, even on the default zoom it looks awful with System.Drawing. Basically all corners converge to a single pixel. Thus, the larger the line, the worse the effect gets.

Performance

The code I've placed above already outputs the duration of both approaches. Unfortunately (although probably expected) System.Drawing is still faster than ImageSharp. Versions being tested:
  • SixLabors.ImageSharp.Drawing: 1.0.0-dev002737
  • System.Drawing.Common: 4.6.0-preview5.19224.8
Before starting, I actually found this page: https://docs.sixlabors.com/articles/ImageSharp/GettingStarted.html and it mentions a couple of potential issues that might cause performance problems:
A few troubleshooting steps to try:
  • Make sure your code runs on 64bit! Older .NET Framework versions are using the legacy runtime on 32 bits, having no built-in SIMD support.

I can confirm my test isn't affect by those problems: That flag returns true and I'm running in 64 bits.

I've varied the number of lines (keeping the line width to 10px and image size to 800x800) and the results are as follows:
line width: 10px
I was slightly surprised by the jump from 10 to 100 lines in ImageSharp, particularly as from 100 lines to 1000 its performance was almost the same.

I then did the same test, but increasing the line width from 10px to 100px:
line width: 100px
I don't understand how ImageSharp is faster when drawing 1000 lines vs the previous test with just 100 lines:
  • 100 lines (10 pixel width): 1315 ms
  • 1000 lines (100 pixel width): 776 ms
The ImageSharp performance seems to get worse the thinner the lines are, which I can try to confirm that by going to an extreme of 1px lines.

But, lets make things even more interesting. I'll include my own custom implementation from 3 years ago to the mix. It didn't compile at first, but was easy enough to update, including using the new recommendation for a Span class to set pixels.

The results were interesting. My custom implementation is at its peak the thinnest the line is, hence it gets some really, really strong results. I'm tempted to say there's a bug or an unintended side-effect from the current implementation in ImageSharp. I'll do some additional experiments before submitting a issue.

I had to change the scale to be logarithmic, as otherwise the results would be hidden because of the strange behavior in ImageSharp, as it takes over 2 minutes to render 1000 lines.
line width: 1px
For actual numbers, with 1000 lines of 1px (from slowest to fastest):
  • ImageSharp (Linux): 140000 ms
  • ImageSharp (Windows): 127000 ms
  • System.Drawing (Linux): 132 ms
  • Custom (Linux): 74 ms
  • System.Drawing (Windows): 64 ms
  • Custom (Windows): 62 ms
Increasing the width to 10px the results aren't as strong, but still quite good:
  • ImageSharp (Linux): 1530 ms
  • ImageSharp (Windows): 1311 ms
  • Custom (Linux): 146 ms
  • Custom (Windows): 127 ms
  • System.Drawing (Linux): 123 ms
  • System.Drawing (Windows): 122 ms
There's a catch though:
  • This is literally the only use case I've built. No polygons, beziers, text, etc
  • Also, my "corner handling" logic is really crappy
Regardless, there could be some potential there, thus I might revisit this topic later on. I've included my custom logic on the repo I've mentioned above: https://github.com/pmcxs/core-linedrawing-benchmark/tree/master/src/ImageSharpCustomDrawing

3 comments:

  1. To control the elbow shape (which defaults to "pointy" with System.Drawing) just modify your pen by setting pen.LineJoin (e.g., to System.Drawing.Drawing2D.LineJoin.Round for rounded corners)

    ReplyDelete
  2. Get the affordable Maid Services in India only at link textCarlanto here we are the Manju placement is one of the finest, top-rated housekeeping placement company in India. We are offering an enormous assortment of services including Maid Services, Cook Placement Services, Home Servant Services, etc.

    ReplyDelete