17
$\begingroup$

I don't very content with current method.So a better solution is expected still.I hope it meet two conditions in following.

  1. That space is approximately equivalence.
  2. We can control how many points to produce.

v11.1 provides a new function SpherePoints. As the Details

SpherePoints[n] gives exactly equally spaced points in certain cases for small n. In other cases, it places points so they are approximately equally spaced.

Can we achieve the same goal i.e. approximately equally spaced points in an arbitrary 2D Region?

The following is my attempt based on Union:

SeedRandom[1]
region = ConvexHullMesh[RandomReal[1, {100, 2}]];
UniformPts = 
  Union[RandomPoint[region, 50000], 
   SameTest -> (EuclideanDistance[#1, #2] < .1 &)];
Show[region, Graphics[Point[UniformPts]]]

Nevertheless, this approach has two weakness:

  1. It is slow with a large number of pre-generated points i.e. the 2nd argument of RandomPoint, while the space won't be uniform enough if I don't pre-generate enough points, here's an example:

    enter image description here

  2. The number of resulting points isn't controllable.

$\endgroup$
7

5 Answers 5

22
+100
$\begingroup$

Annealing

Found this to be an interesting question and immediately I thought it to be a good application for simulated annealing.

Here's a little unoptimized annealing function I wrote. The idea is that your points move around like atoms in random directions but they "cool down" over time and move less and settle into a minimum energy configuration state.

My rules are:

  1. plan a move in a random direction and random distance of maximum length step
  2. move only if the distance to the nearest point increases
  3. move only if the new location is inside the region

Assumes rm is a globally defined RegionMember function.

anneal[pts_, step_] := 
 Module[{np, nn, test1, test2, pl, potentialMoves},
  pl = Length@pts;
  np = Nearest@pts;
  nn = np[#, 2][[2]] & /@ pts;
  potentialMoves = RandomReal[step, pl]*RandomPoint[Circle[], pl];
  test1 = 
   Boole@Thread[
     MapThread[EuclideanDistance[#1, #2] &, {pts, nn}] < 
      MapThread[
       EuclideanDistance[#1, #2] &, {pts + potentialMoves, nn}]];
  test2 = Boole[rm /@ (pts + potentialMoves)];
  pts + potentialMoves*test1*test2]

Here is an example with 200 pts, 1000 steps and an anneal rate of .995. Initial step should be on the order of the region size:

Clear[x,y];reg=ImplicitRegion[x^2-y^2<=1,{{x,-3,3},{y,-3,3}}];
rm=RegionMember[reg];
pts=RandomPoint[reg,200];
step=1;
Do[pts=anneal[pts,step=.995*step],1000];
Show[RegionPlot[reg],Graphics[{Black,Point/@pts}]]

stippling example

Here is an animation of the process:

enter image description here

$\endgroup$
3
  • 2
    $\begingroup$ It is not a high efficient solution,but it is the answer which I content with indeed.Thanks very much. $\endgroup$
    – yode
    Commented Apr 11, 2017 at 19:27
  • $\begingroup$ Now that I think about it, one might even consider a hybrid of kirma's and Kelly's approaches: start with annealing for a few steps, and accelerate convergence near the end with Lloyd. $\endgroup$ Commented Apr 13, 2017 at 1:23
  • $\begingroup$ I think we should make the move have a rough but not a random direction maybe will be better. $\endgroup$
    – yode
    Commented Apr 13, 2017 at 13:01
22
$\begingroup$

Many solutions similar to how to get $n$ equidistributed points on the unit sphere are possible, especially if one can accept that points are not on the edges of a region. For instance, one can use analytical Lloyd's method:

With[{reg = RegularPolygon[5]}, 
 Nest[RegionNearest[reg][
     RegionCentroid@RegionIntersection[reg, #] & /@ 
      MeshPrimitives[VoronoiMesh@#, 2]] &, RandomPoint[reg, 20], 20] //
   Graphics[{LightBlue, reg, Black, Point@#}] &]

enter image description here

This is awfully slow, though.

A much faster variant is to use a Monte Carlo method to estimate Voronoi cell centroids:

With[{reg = RegularPolygon[5]}, 
 With[{points = 500, samples = 40000, iterations = 20}, 
   Nest[With[{randoms = Join[#, RandomPoint[reg, samples]]}, 
      RegionNearest[reg][
       Mean@randoms[[#]] & /@ 
        Values@PositionIndex@Nearest[#, randoms]]] &, 
    RandomPoint[reg, points], iterations]] // 
  Graphics[{LightBlue, reg, Black, Point@#}] &]

enter image description here

A more complex concave example (which benefits from higher iteration count) as suggested by @J.M.:

With[{reg = 
   Cases[Graphics`PolygonUtils`PolygonData[
      "HexaSpiral"], _Polygon, \[Infinity]][[1]]}, 
 With[{points = 500, samples = 40000, iterations = 200}, 
   Nest[With[{randoms = Join[#, RandomPoint[reg, samples]]}, 
      RegionNearest[reg][
       Mean@randoms[[#]] & /@ 
        Values@PositionIndex@Nearest[#, randoms]]] &, 
    RandomPoint[reg, points], iterations]] // 
  Graphics[{LightBlue, reg, Black, Point@#}] &]

enter image description here

$\endgroup$
7
  • 1
    $\begingroup$ Good work,and have you seen this blog? $\endgroup$
    – yode
    Commented Mar 29, 2017 at 2:36
  • 1
    $\begingroup$ This seems to do well with concave regions, too. Try, for instance, reg = Cases[Graphics`PolygonUtils`PolygonData["HexaSpiral"], _Polygon, ∞][[1]] $\endgroup$ Commented Mar 29, 2017 at 3:32
  • $\begingroup$ @yode Yes, I've seen it. Lloyd's method is a nice trick people tend to find when thinking of these things. I'm not certain how I found it out, but it's also featured in Mma documentation of VoronoiMesh. $\endgroup$
    – kirma
    Commented Mar 29, 2017 at 4:09
  • 3
    $\begingroup$ @yode @ kirma Regarding the performance and generalization to arbitrary region, I had some significant improvements. The key-points are vectorization and compilation. While getting time and appropriate chance, I'd definitely like to share them. $\endgroup$
    – Silvia
    Commented Apr 13, 2017 at 8:59
  • 2
    $\begingroup$ @yode I'm sincerely sorry to say that I might not be able to find some time until at least 1 month later... Also I think, in principle, my method would have no huge differences from kirma's. It's just the old blog with some numerical optimizations. $\endgroup$
    – Silvia
    Commented Apr 13, 2017 at 15:38
10
$\begingroup$

This does not give you real control over the number of points, but its one way to get somewhat equally spaced distribution of points:

region = ConvexHullMesh[RandomReal[1, {100, 2}]];
<< NDSolve`FEM`
elementMesh = ToElementMesh[region, MeshQualityGoal -> 0];
coordinates = elementMesh[[1]];
triangles = elementMesh[[2, 1, 1]];
centers = 
  Table[
    Sum[{coordinates[[triangles[[i, j]], 1]],coordinates[[triangles[[i, j]], 2]]}, {j, 1, 3}]/3
   , {i, 1, Length[triangles]}];
Show[region, Graphics[Point[centers]]]

enter image description here

Update

Trivial solution, but seems to be relatively quick:

numberOfPoints = 500;
region = BoundaryDiscretizeGraphics[Text["K"], _Text];
<< NDSolve`FEM`
elementMesh = ToElementMesh[region, MeshQualityGoal -> 0];
coordinates = Transpose[elementMesh[[1]]];
Xmin = Min[coordinates[[1]]];
Xmax = Max[coordinates[[1]]];
Ymin = Min[coordinates[[2]]];
Ymax = Max[coordinates[[2]]];
scale = (Xmax - Xmin + Ymax - Ymin)/2;
spacing = scale/40;
test[x_] := RegionMember[region, x];
points := DeleteCases[
  Flatten[Table[ If[test[{i, j}], {i, j}, -1], {i, Xmin, Xmax, spacing}, {j, Ymin, Ymax, spacing}], 1] 
  , -1];
(
 data = Table[{1/(spacing = ii scale/300), points // Length} // Reverse, {ii, 10, 20}];
 fun[x_] = NonlinearModelFit[data, a + b x^(1/2), {a, b}, x] // Normal;
 spacing = 1/fun[numberOfPoints];
 (pts = points) // Length
) // AbsoluteTiming
Show[region, Graphics[Point[pts]]]

{0.187837, 505}

enter image description here

We can see that within 0.188 seconds we have generated a distribution of 505 points where we tried to generate a 500 points distribution. The error in number of points is always just a few percent at the most.

$\endgroup$
8
  • $\begingroup$ Thanks for the new update. :)And it's seem cannot control the number of points still? $\endgroup$
    – yode
    Commented Mar 28, 2017 at 16:37
  • $\begingroup$ For given region, you could get desired number of points by fine tuning the spacing variable. Requires some trial and error, or you could write a short minimization routine... Would make it slower though. $\endgroup$
    – Kagaratsch
    Commented Mar 28, 2017 at 16:41
  • 5
    $\begingroup$ @yode Honestly, density of points might be a more objective measure than number of points, since you never know what total area the region has before you generate it. So being able to adjust the spacing is probably more useful than number of points. Don't you agree? $\endgroup$
    – Kagaratsch
    Commented Mar 28, 2017 at 16:45
  • 1
    $\begingroup$ Actually don't very agree. :) $\endgroup$
    – yode
    Commented Mar 28, 2017 at 16:47
  • $\begingroup$ @yode see the update of the trivial solution above. This allows to generate a distribution of approximately the desired amount of points. The error is always within a few percent, as far as I have tested. $\endgroup$
    – Kagaratsch
    Commented Apr 11, 2017 at 22:37
5
$\begingroup$

Since Mma v12.2 spatial point processes have opened yet another possibility with HardcorePointProcess, which prevents processes from resulting points being pairwise closer than a specified distance from each other. This is not to such an extent a "packing" as many other answers to this question, since only minimum distance is constrained, but it does demonstrate that there are multiple ways to approach the question. Consider, for instance:

With[{reg = 
   Cases[
     Graphics`PolygonUtils`PolygonData[
      "HexaSpiral"], _Polygon, \[Infinity]][[1]]}, 
 Graphics[{LightBlue, reg, Black,
   Point@RandomPointConfiguration[
      HardcorePointProcess[10000, 2, 2],
      reg, Method -> {"LengthOfRun" -> 10000000}]["Points"]}]]

enter image description here

Here the number of points produced is somewhat random, but depends on the region area, the specified point density (the first argument to HardcorePointProcess) and the minimum distance constraint (the second argument).

$\endgroup$
3
$\begingroup$

Rebuilt and little refine for this answer

equPoint[region_, pointNum_] := 
 Module[{pts, stepDist, rm, regionRange, beFar, InRegion, moveDir}, 
  pts = RandomPoint[region, pointNum];
  rm = RegionMember[region];
  regionRange = 
   EuclideanDistance @@ Transpose[RegionBounds[region]]/100;
  stepDist = Subdivide[regionRange, 0, 500];
  moveDir = 
   RandomReal[#, pointNum]*RandomPoint[Circle[], pointNum] & /@ 
    stepDist;
  Fold[(beFar = 
      Boole@Thread[
        EuclideanDistance @@@ Nearest[#, #, 2] < 
         EuclideanDistance @@@ Nearest[#1 + #2, #1 + #2, 2]];
     InRegion = Boole[rm /@ (#1 + #2)];
     # + #2*beFar*InRegion) &, N[pts], moveDir]]

Explanation

I have not iterated 1000 times,but make the difference of the last two times less the pre,whose default value is 0.001,after 500 times.And you don't need to consider the size of region.

Example

region = ImplicitRegion[x^2 - y^2 <= 1, {{x, -3, 3}, {y, -3, 3}}];
BoundaryDiscretizeRegion[region, 
 Epilog -> {Point[equPoint[region, 200]]}]

Mathematica graphics

Here is an animation of the process: enter image description here

$\endgroup$

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.