8
$\begingroup$

Imagine I have an arbitrary concave polygon. I need to find/generate a point inside of it, efficiently.

RandomPoint@Polygon@points is relatively fast only for simple such polygons.

n=1;(* toy polygon complexity *)

points=Join[Transpose[{Range[2*n+1]*5,Drop[Flatten@ConstantArray[{0,45},n+1],-1]}],Transpose[{Reverse@Range[2*n+1]*5-5,Drop[Flatten@ConstantArray[{55,10},n+1],-1]}]];

insidepoint=RandomPoint@Polygon@points;//RepeatedTiming

Show[
Graphics@Thread@Line@Transpose[{points,RotateLeft@points}],
Graphics@{Red,PointSize[Medium],Point@insidepoint}
]

n=1

> {0.00244676, Null}

enter image description here

n=100

{0.233274, Null} enter image description here

n=1000

{2.84268, Null}

So, for complex polygons,RandomPoint@Polygon is very, very slow.

Is there a method to find a point inside a convex polygon efficiently, no matter the complexity?

UPD.

I’ve added timings here:

timings

$\endgroup$
0

4 Answers 4

13
$\begingroup$

We can use mesh regions instead. For $n = 1000$ here's timings on my machine:

SeedRandom[1];
RandomPoint@Polygon[points]; // AbsoluteTiming
{0.930068, Null}
SeedRandom[1];
RandomPoint@MeshRegion[points, 
  Polygon[Range[Length[points]]]]; // AbsoluteTiming
{0.018052, Null}
SeedRandom[1];
RandomPoint@BoundaryMeshRegion[points, 
  Line[Append[Range[Length[points]], 1]]]; // AbsoluteTiming
{0.004879, Null}
$\endgroup$
7
$\begingroup$
  • decomposition the polygon into several triangles (or convex set) before select points.
triangles = PolygonDecomposition[Polygon@points, "Triangle"];
areas = Area /@ triangles;
(pt = RandomPoint@RandomChoice[areas -> triangles]) // RepeatedTiming
RegionMember[Polygon@points, pt]

True.

$\endgroup$
7
  • 3
    $\begingroup$ For RandomChoice[triangles], you might want to weight each triangle by its area. This will let you sample uniformly across the full region. $\endgroup$
    – Greg Hurst
    Commented Nov 18 at 18:18
  • $\begingroup$ @GregHurst Thanks! $\endgroup$
    – cvgmt
    Commented Nov 18 at 18:22
  • $\begingroup$ @cvgmt very nice! Agree re: area weighting. Always learn from your answers! $\endgroup$
    – ubpdqn
    Commented Nov 19 at 8:56
  • $\begingroup$ Timing measurement incorrect. Could you include triangles and areas inside RepeatedTiming, as well as rectangle, pts, counts and inner ? $\endgroup$
    – Anton
    Commented Nov 26 at 13:39
  • $\begingroup$ Just as compile method, we not always care about the compile time. $\endgroup$
    – cvgmt
    Commented Nov 26 at 13:49
2
$\begingroup$

I posted a hasty answer which without having Mathematica. I have deleted that one. Here is a better version if someone else wants a go.

Take two points A, B on a polygon side. If A is B and the side has no length, go on to the next side.

Get the mid-point M = (A+B)/2;

Get a small perpendicular vector V = (A-B)*[0,1;-1,0]*0.0001:

Here are two points just off the mid-point...

P0 = M+V;

P1 = M-V;

If the winding number for P0 is non-zero then P0 is inside the polygon. If it is zero then P1 is inside the polygon unless you are really unlucky.

$\endgroup$
4
  • 1
    $\begingroup$ Posting a Mathematica code for your algo would be really nice of you. $\endgroup$
    – Anton
    Commented Nov 21 at 9:35
  • $\begingroup$ @Anton I had Mathematica once and loved it, but that was at a previous job over 20 years ago. I am now on the free stuff. Octave is MATLAB, and in my haste to be helpful I read Mathematica as MATLAB. $\endgroup$ Commented Nov 21 at 9:57
  • $\begingroup$ I see. Check out my solution, instead of going perpendicular the side segment, I go bisector closest to 90 degrees. $\endgroup$
    – Anton
    Commented Nov 23 at 12:38
  • 1
    $\begingroup$ It seems sensible. There are lots of ways of doing this. The winding count is very quick to calculate. I picked the first edge and invoked it once, so I can't see a way of getting it any faster, but there is a small chance that the second point won't be inside the shape if it is fantastically skinny. I don't say the point will be a particularly good inside point: if you had a circle then the point would be right next to the edge. But it is inside. $\endgroup$ Commented Nov 23 at 15:49
2
$\begingroup$

Here's a fast algo: 1) find convex vertices, 2) from those find one with the maximum Cross between its segments, 3) then pick a point using bisector and a small distance. This approach remotely resembles @Richard Kirk ‘s idea.

Testing code:

(* Generate a convex polygon *)
    n = 1;(* 1 | 100 | 1000 | 5000 | 10000 *)
points = 
  Join[Transpose[{Range[2*n + 1]*5, 
     Drop[Flatten@ConstantArray[{0, 45}, n + 1], -1]}], 
   Transpose[{Reverse@Range[2*n + 1]*5 - 5, 
     Drop[Flatten@ConstantArray[{55, 10}, n + 1], -1]}]];
(* OP slow solution *)
{t0, pt0} = RandomPoint@Polygon[points] // RepeatedTiming;
(* Greg  Hurst 1 - RandomPoint@MeshRegion *)
{t1, pt1} = 
  RandomPoint@MeshRegion[points, Polygon[Range[Length[points]]]] // 
   RepeatedTiming;
(* Greg  Hurst 2 - RandomPoint@
    BoundaryMeshRegion *)
{t2, pt2} = 
  RandomPoint@
    BoundaryMeshRegion[points, 
     Line[Append[Range[Length[points]], 1]]] // RepeatedTiming;
(* cvgmt *)
{t3, nothing} = RepeatedTiming[
   triangles = PolygonDecomposition[Polygon@points, "Triangle"];
   areas = Area /@ triangles;
   pt3 = RandomPoint@RandomChoice[areas -> triangles];
   ];
(* Anton *)
distance = 1*^-9;

generateInsidePoint[points_, distance_] := 
 Module[{segVectors, svNorm, svNormRL, cross, rotations, ccws, 
   ccwcrosses, vertexPos, vertex}, 
  segVectors = RotateLeft@points - points;
  svNorm = 
   segVectors/(segVectors[[All, 1]]^2 + segVectors[[All, 2]]^2)^.5;
  svNormRL = RotateLeft@svNorm;
  cross = 
   Abs[-svNorm[[All, 2]]*svNormRL[[All, 1]] + 
     svNorm[[All, 1]]*svNormRL[[All, 2]]];
  rotations = isitclockwise@points;
  ccws = Pick[Range@Length@points, rotations, -1];
  ccwcrosses = cross[[ccws]];
  vertexPos = ccws[[Ordering[ccwcrosses][[-1]]]];
  vertex = points[[vertexPos]];
  vertex + 
   Normalize[
     Mean@{vertex - 
         Normalize[vertex - RotateRight[points][[vertexPos]]], 
        vertex + 
         Normalize[RotateLeft[points][[vertexPos]] - vertex]} - 
      vertex]*distance]

generateInsidePoint2[points_, distance_] := 
 Module[{segVectors, svNorm, svNormRL, cross, rotations, ccws, 
   ccwcrosses, vertexPos, vertex, rtL, rtR,
   triples, t1, t2},
  
  segVectors = RotateLeft@points - points;
  svNorm = 
   segVectors/(segVectors[[All, 1]]^2 + segVectors[[All, 2]]^2)^.5;
  svNormRL = RotateLeft@svNorm;
  cross = 
   Abs[-svNorm[[All, 2]]*svNormRL[[All, 1]] + 
     svNorm[[All, 1]]*svNormRL[[All, 2]]];
  
  rtR = RotateRight@points;
  rtL = RotateLeft@points;
  triples = Transpose[{rtR, points, rtL}];
  t1 = triples[[All, All, 1]];
  t2 = triples[[All, All, 2]];
  rotations = 
   Sign[Total /@ ((RotateLeft /@ t1 - t1)*(RotateLeft /@ t2 + t2))];
  
  ccws = Pick[Range@Length@points, rotations, -1];
  ccwcrosses = cross[[ccws]];
  vertexPos = ccws[[Ordering[ccwcrosses][[-1]]]];
  vertex = points[[vertexPos]];
  
  vertex + 
   Normalize[
     Mean@{vertex - 
         Normalize[vertex - RotateRight[points][[vertexPos]]], 
        vertex + 
         Normalize[RotateLeft[points][[vertexPos]] - vertex]} - 
      vertex]*distance
  
  ]

(* Anton 1 - we do this one for those of us who speaks of themselves in the plural *)
{t4, nothing} = RepeatedTiming[
   isitclockwise = 
    Compile[{{pts, _Real, 2}}, Module[{rtR, rtL, triples, t1, t2},
      rtR = RotateRight@pts;
      rtL = RotateLeft@pts;
      triples = Transpose[{rtR, pts, rtL}];
      t1 = triples[[All, All, 1]];
      t2 = triples[[All, All, 2]];
      Sign[
       Total /@ ((RotateLeft /@ t1 - t1)*(RotateLeft /@ t2 + t2))]], 
     RuntimeOptions -> "Speed"];
   pt4 = generateInsidePoint[points, 1*^-9];
   ];

(* Anton 2 - using compiled function but not being dumb af *)
{t5, pt5} = generateInsidePoint[points, 1*^-9] // RepeatedTiming;

(* Anton 3 - without compiled functions *)
{t6, pt6} = generateInsidePoint2[points, 1*^-9] // RepeatedTiming;

Timings in log scale:

enter image description here

Timings in regular scale:

enter image description here

$\endgroup$
4
  • $\begingroup$ Why not test n=1000 and enclose all the code by RepeatedTiming? $\endgroup$
    – cvgmt
    Commented Nov 28 at 7:10
  • $\begingroup$ My code is contained by functions and their execution is enclosed by RepeatedTiming and you're posting right under For n=1000 {0.00325786, Null} test result. LMFAO that's why. $\endgroup$
    – Anton
    Commented Nov 28 at 8:13
  • $\begingroup$ isitclockwise=Compile... need time to compile, do you know? $\endgroup$
    – cvgmt
    Commented Nov 28 at 8:39
  • $\begingroup$ no sweat, really? $\endgroup$
    – Anton
    Commented Nov 28 at 9:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.