2

On a custom control I've a series of LED objects that should be turned on according to a given GraphicsPath (e.g., see image below). Currently I'm using graphicsPath.IsVisible(ledPoint), however since I have many LEDs, the iteration through all of them can be very slow, especially if the path is complex (e.g. the inverse of the path in the example).

Does any of you has an idea toward something more clever to speed up iteration? If it is too complex to make an example, it may work to redirect me toward proper resources. Please consider that the control is in GDI+ an re-engineering into another engine is not an option.

enter image description here

EDIT


on my PC (i7 3.6GHz) when as GraphicsPath I've just a simple rectangle 100x100 pixels and then I compute the inverse on my Control that is sizing about 500x500 pixels (hence, the resulting GraphicsPath will be a 500x500 rectangle with an 'hole' of 100x100), to test 6000 LEDs takes about 1.5sec, which will be too much affecting user experience.

Following Matthew Watson reply, I'm detailing more on my example:

//------ Base path test
GraphicsPath path = new GraphicsPath();
path.AddRectangle(new Rectangle(100, 100, 100, 100));

var sw = System.Diagnostics.Stopwatch.StartNew();
for (int x = 0; x < 500; ++x)
    for (int y = 0; y < 500; ++y)
        path.IsVisible(x, y);
Console.WriteLine(sw.ElapsedMilliseconds);

//------ Inverse path test
GraphicsPath clipRect = new GraphicsPath();
clipRect.AddRectangle(new Rectangle(0, 0, 500, 500));
GraphicsPath inversePath = Utility.CombinePath(path, clipRect, CombineMode.Complement);

sw.Restart();
for (int x = 0; x < 500; ++x)
   for (int y = 0; y < 500; ++y)
       inversePath.IsVisible(x, y);
Console.WriteLine(sw.ElapsedMilliseconds);

On my PC I have ~725ms on the first test and ~5000ms on the second. And this is just a fairly simple path. The GraphicsPath is generated by user's mouse movements and the user can perform several paths combinations (inverting, union, intersection,.. I use GPC for this). Hence, testing for inversion by testing the negation of GraphicsPath.IsVisible() can be tricky.

The inversePath returned from Utility.CombinePath is quite simple and has following points (left PathPoints, right PathTypes):

enter image description here

Mauro Ganswer
  • 1,379
  • 1
  • 19
  • 33
  • What is the source of the GraphicsPath? Also: What exactly is the problem? Is it really too slow? How many paths do you have? can't you cache their hits in a List or Dictionary? – TaW Jul 12 '16 at 07:51
  • I did some test, please see my edits. I cannot cache, because the GraphicsPath changes continously – Mauro Ganswer Jul 12 '16 at 08:55
  • In addition to Matthews answer: How do those continously changing GraphicsPaths get created?? – TaW Jul 12 '16 at 09:13
  • @TaW please check again my edits – Mauro Ganswer Jul 12 '16 at 10:23
  • What is `Utility.CombinePath()` ? – Matthew Watson Jul 12 '16 at 10:28
  • @MatthewWatson it is a method from my library using the C# wrapper for [GPC](http://www.cs.man.ac.uk/~toby/gpc/). Cannot post the code because too long, but I've added the result of `inversePath` in a further edit – Mauro Ganswer Jul 12 '16 at 10:46
  • Hm, just a dumb question: in your code you are testing every pixel-point instead of every led-point, right? So you are doing 250k tests, which is a lot more than 6k as you wrote in the question text..? – TaW Jul 12 '16 at 10:49
  • @TaW in latest tests I'm using the same code of Matthew (as in my edits), hence yes 250k tests. In my previous tests (with 6k tests), I used another code and LED positions that I have in my code defined with PointF, so maybe there were other things affecting. But still looking at the code inspired bye Matthew, the increased time span seems high to me – Mauro Ganswer Jul 12 '16 at 10:56

2 Answers2

3

One optimization is to test only within the effective bounds rectangle:

Rectangle r = Rectangle.Round( path.GetBounds() );
for (int x = r.X; x < r.Width; ++x)
     for (int y = r.Y; y < r.Height; ++y)
         if (path.IsVisible( x, y ))..

Another trick is to flatten the path, so that it consists of line segments only:

Matrix m = new Matrix();
path.Flatten(m, 2);

Picking a larger flatness also helps. I found with 1 or 2 the speed easily doubled.

I didn't use your testbeds, but the results should help:

Testing this path:

enter image description here

The time went down:

25781 (without bounds)

7929 (only within bounds)

3067 (within bounds &flattend by 2)

Note the the first one will only help if you didn't invert by the clientrectangle to begin with and the second will only help if the path actually contains curves.

Update:

Taking up on Hans's suggestion, this is by far the most effective 'optimization':

Region reg = new Region(path);
for (int x = r.X; x < r.Width; ++x)
    for (int y = r.Y; y < r.Height; ++y)
        reg.IsVisible(x, y);

It takes my timings down to 10-20ms (!)

So it isn't really just an 'opimization'; it is avoiding a most awful waste of time, read: basically no time at all went into the testing and all went into setting up the test region.

From Hans Passant's comment:

GraphicsPath.IsVisible requires the path to be converted to a region under the hood. Do that up front with the Region(GraphicsPath) constructor so you don't pay that cost for every single point.

Note that compared to the other paths, mine is very complex; therefore my savings are much larger than those you can expect from a rectangle with a rectanglular hole or such. User drawn paths like mine (or the one from the OP) easily tend to be made up of hundreds of segments, not just a 4-8..

TaW
  • 53,122
  • 8
  • 69
  • 111
1

I think there must be something else taking the time, because my tests show that I can test 250,000 points in less than a second:

GraphicsPath path = new GraphicsPath();

path.AddLine(  0,   0, 100,   0);
path.AddLine(100,   0, 100, 100);
path.AddLine(100, 100,   0, 100);
path.AddLine(  0, 100,   0,   0);

var sw = Stopwatch.StartNew();

for (int x = 0; x < 500; ++x)
    for (int y = 0; y < 500; ++y)
        path.IsVisible(x, y);

Console.WriteLine(sw.ElapsedMilliseconds);

The code above gives results of less than 900ms. (I'm running on an old processor with a speed of less than 3GHz.)

You should be able to test 6000 points in less than 25ms.

This seems to indicate that the time is being taken elsewhere.

(Note that to test the inverse all you have to do is use !path.IsVisible(x, y) instead of path.IsVisible(x, y).)


In response to the edited question:

As I said, to test the inverse all you need to do is use !Path.IsVisible(x,y). You seem to be inverting it by adding a containing rectangle - which works, but is not necessary and slows things down.

The following code demonstrates what I mean - note the Trace.Assert():

GraphicsPath path = new GraphicsPath();
path.AddRectangle(new Rectangle(100, 100, 100, 100));

GraphicsPath inversePath = new GraphicsPath();
inversePath.AddRectangle(new Rectangle(100, 100, 100, 100));
inversePath.AddRectangle(new Rectangle(0, 0, 500, 500));

for (int x = 0; x < 500; ++x)
    for (int y = 0; y < 500; ++y)
        Trace.Assert(inversePath.IsVisible(x, y) == !path.IsVisible(x, y));
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • I edited to show you the problem arising with a more complex path like an inverted one. – Mauro Ganswer Jul 12 '16 at 10:24
  • I see your point, but please consider that mine was just an example and that the path can be more complicated by user's actions like unions, intersections, exclusions...So at the end who knows whether testing `path.IsVisible()` is faster or slower than the negation `!path.IsVisible()`? What causes the path to take more or less time? Maybe an idea could be to measure the speed on the first point to test (testing both `IsVisible()` and !IsVisible()`) and then to take the fastest of the two for following points. What do you think? – Mauro Ganswer Jul 12 '16 at 11:04
  • Good idea imo. Take a center point (see my answer about getting that), not (0,0), though, to be sure no hapstance shortcut messes up the result! – TaW Jul 12 '16 at 15:20
  • 5
    Keep in mind that your GraphicsPath is too simple so perf guarantees are rather iffy. This code can however easily be sped up. GraphicsPath.IsVisible requires the path to be converted to a region under the hood. Do that up front with the Region(GraphicsPath) constructor so you don't pay that cost for every single point. Makes your code 6 times faster when I try it. – Hans Passant Jul 12 '16 at 16:19
  • 1
    @Hans's suggestion makes my code 1000x faster, or, maybe more likely, takes it out of the realm of relevance of the stopwatch speed timing. In any case, this ought to resolve all time problems..! – TaW Jul 13 '16 at 11:32
  • 1
    @HansPassant Fantastic as usual Hans, thanks! Your suggestion really sped up the code. I obtained also some more speed by taking a pixel and testing it against the normal path and the inverted one. However, your suggestion has a much higher impact. If you wish to add it to replies I'd like to mark it as the answer – Mauro Ganswer Jul 13 '16 at 15:55