File Coverage

blib/lib/Random/PoissonDisc.pm
Criterion Covered Total %
statement 25 91 27.4
branch 1 14 7.1
condition 0 15 0.0
subroutine 6 11 54.5
pod 7 7 100.0
total 39 138 28.2


line stmt bran cond sub pod time code
1             package Random::PoissonDisc;
2 2     2   57743 use strict;
  2         11  
  2         55  
3 2     2   10 use List::Util qw(sum);
  2         4  
  2         166  
4 2     2   1164 use Math::Random::MT::Auto qw(rand gaussian);
  2         97720  
  2         8  
5              
6 2     2   11871 use vars qw($VERSION %grid_neighbours);
  2         4  
  2         1801  
7             $VERSION = '0.03';
8              
9             # %grid_neighbours caches the vectors pointing to
10             # neighbours
11              
12             =head1 NAME
13              
14             Random::PoissonDisc - distribute points aesthetically in R^n
15              
16             =head1 SYNOPSIS
17              
18             my $points = Random::PoissonDisc->points(
19             dimensions => [100,100],
20             r => $r,
21             );
22             print join( ",", @$_),"\n"
23             for @$points;
24              
25             This module allows relatively fast
26             (linear in the number of points generated) generation of random points in
27             I-dimensional space with a distance of
28             at least C between each other. This distribution
29             results in aesthetic so called "blue noise".
30              
31             The algorithm was adapted from a sketch
32             by Robert Bridson
33             in L.
34              
35             =head1 DATA REPRESENTATION
36              
37             All vectors (or points) are represented
38             as anonymous arrays of numbers. All have the same
39             dimension as the cardinality of the C
40             array passed in the C<< ->points >> method.
41              
42             =head2 USER INTERFACE
43              
44             =head3 C<< Random::PoissonDisc->points( %options ) >>
45              
46             Returns a reference to an array of points.
47              
48             Acceptable options are:
49              
50             =over 4
51              
52             =item *
53              
54             C< r > - minimum distance between points.
55              
56             Default is 10 units.
57              
58             =item *
59              
60             C< dimensions > - number of dimensions and respective value range as an arrayref.
61              
62             Default is
63              
64             [ 100, 100 ]
65              
66             meaning all points will be in R^2 , with each coordinate in the
67             range [0, 100).
68              
69             =item *
70              
71             C< candidates > - Number of candidates to inspect before deciding that no
72             ew neighbours can be placed around a point.
73              
74             Default is 30.
75              
76             This number may or may not need to be tweaked if you go further up in
77             dimensionality beyond 3 dimensions. The more candidates you inspect
78             the longer the algorithm will run for generating a number of points.
79              
80             In the algorithm description, this constant is named I.
81              
82             =item *
83              
84             C<< avoid_edge >> - The distance from the edge of the plot.
85              
86             Default is C<0>
87              
88             If greater than zero, this will not plot points within that distance from the edge.
89              
90             =item *
91              
92             C<< center >> - Start adding points at the center of the plot.
93              
94             Default is C<0>
95              
96             If this is set to the default, the initial point will be added at a
97             random position in the plot.
98              
99             =back
100              
101             =cut
102              
103             sub points {
104 0     0 1 0 my ($class,%options) = @_;
105            
106 0   0     0 $options{center} ||= 0;
107 0   0     0 $options{avoid_edge} ||= 0;
108 0   0     0 $options{candidates} ||= 30;
109 0   0     0 $options{dimensions} ||= [100,100]; # do we only create integral points?
110 0   0     0 $options{r} ||= 10;
111             #$options{max} ||= 10; # we want to fill the space instead?!
112 0   0     0 $options{ grid } ||= {};
113            
114 0         0 my $grid_size = $options{ r } / sqrt( 0+@{$options{dimensions}});
  0         0  
115            
116 0         0 my @result;
117             my @work;
118            
119             # Create a first point in our cube - either at random or in the center:
120             my $p = $options{ center }
121 0         0 ? [map { $_ / 2 } @{ $options{ dimensions }}]
  0         0  
122 0 0       0 : [map { rnd(0,$_) } @{ $options{ dimensions }}];
  0         0  
  0         0  
123 0         0 push @result, $p;
124 0         0 push @work, $p;
125 0         0 my $c = grid_coords($grid_size, $p);
126 0         0 $options{ grid }->{ $c } = $p;
127            
128 0         0 while (@work) {
129 0         0 my $origin = splice @work, int rnd(0,$#work), 1;
130 0         0 CANDIDATE: for my $candidate ( 1..$options{ candidates } ) {
131             # Create a random distance between r and 2r
132             # that is, in the annulus with radius (r,2r)
133             # surrounding our current point
134 0         0 my $dist = rnd( $options{r}, $options{r}*2 );
135            
136             # Choose a random angle in which to point
137             # this vector
138 0         0 my $angle = random_unit_vector(0+@{$options{ dimensions}});
  0         0  
139            
140             # Generate a new point by adding the $angle*$dist to $origin
141 0         0 my $p = [map { $origin->[$_] + $angle->[$_]* $dist } 0..$#$angle];
  0         0  
142            
143             # Check whether our point lies within the dimensions
144 0         0 for (0..$#$p) {
145             next CANDIDATE
146             if $p->[$_] >= $options{ dimensions }->[ $_ ] - $options{ avoid_edge }
147             or $p->[$_] < $options{ avoid_edge }
148 0 0 0     0 };
149            
150             # check discs by using the grid
151             # Here we should check the "neighbours" in the grid too
152 0         0 my $c = grid_coords($grid_size, $p);
153 0 0       0 if (! $options{ grid }->{ $c }) {
154 0         0 my @n = neighbour_points($grid_size, $p, $options{ grid });
155 0         0 for my $neighbour (@n) {
156 0 0       0 if( vdist($neighbour, $p) < $options{ r }) {
157 0         0 next CANDIDATE;
158             };
159             };
160            
161             # not already in grid, no close neighbours, add it
162 0         0 push @result, $p;
163 0         0 push @work, $p;
164 0         0 $options{ grid }->{ $c } = $p;
165             #warn "$candidate Taking";
166             } else {
167             #warn "$candidate Occupied";
168             };
169             };
170             };
171            
172             \@result
173 0         0 };
174              
175             =head2 INTERNAL SUBROUTINES
176              
177             These subroutines are used for the algorithm.
178             If you want to port this module to PDL or any other
179             vector library, you will likely have to rewrite these.
180              
181             =head3 C<< rnd( $low, $high ) >>
182              
183             print rnd( 0, 1 );
184              
185             Returns a uniform distributed random number
186             in C<< [ $low, $high ) >>.
187              
188             =cut
189              
190             sub rnd {
191 0     0 1 0 my ($low,$high) = @_;
192 0         0 return $low + rand($high-$low);
193             };
194              
195             =head3 C<< grid_coords( $grid_size, $point ) >>
196              
197             Returns the string representing the coordinates
198             of the grid cell in which C<< $point >> falls.
199              
200             =cut
201              
202             sub grid_coords {
203 0     0 1 0 my ($size,$point) = @_;
204 0         0 join "\t", map { int($_/$size) } @$point;
  0         0  
205             };
206              
207             =head3 C<< norm( @vector ) >>
208              
209             print norm( 1,1 ); # 1.4142
210              
211             Returns the Euclidean length of the vector, passed in as array.
212              
213             =cut
214              
215             sub norm {
216 2000     2000 1 3402 sqrt( sum @{[map {$_**2} @_]} );
  2000         2575  
  11000         18333  
217             };
218              
219             =head3 C<< vdist( $l, $r ) >>
220              
221             print vdist( [1,0], [0,1] ); # 1.4142
222              
223             Returns the Euclidean distance between two points
224             (or vectors)
225              
226             =cut
227              
228             sub vdist {
229 0     0 1 0 my ($l,$r) = @_;
230 0         0 my @connector = map { $l->[$_] - $r->[$_] } 0..$#$l;
  0         0  
231 0         0 norm(@connector);
232             };
233              
234             =head3 C<< neighbour_points( $size, $point, $grid ) >>
235              
236             my @neighbours = neighbour_points( $size, $p, \%grid )
237              
238             Returns the points from the grid that have a distance
239             between 0 and 2r around C<$point>. These points are
240             the candidates to check when trying to insert a new
241             random point into the space.
242              
243             =cut
244              
245             sub neighbour_points {
246 0     0 1 0 my ($size,$point,$grid) = @_;
247            
248 0         0 my $dimension = 0+@$point;
249 0         0 my $vectors;
250 0 0       0 if (! $grid_neighbours{ $dimension }) {
251 0         0 my @elements = (-1,0,1);
252             $grid_neighbours{ $dimension } =
253             # Count up, and use the number in ternary as our odometer
254             [map {
255 0         0 my $val = $_;
  0         0  
256             my $res = [ map {
257 0         0 my $res = $elements[ $val % 3 ];
  0         0  
258 0         0 $val = int($val/3);
259 0         0 $res
260             } 1..$dimension ];
261             } (1..3**$dimension)
262             ];
263             };
264            
265 0         0 my @coords = split /\t/, grid_coords( $size, $point );
266              
267             # Find the elements in the grid according to the offsets
268             map {
269 0         0 my $e = $_;
270 0         0 my $n = join "\t", map { $coords[$_]+$e->[$_] } 0..$#$_;
  0         0  
271             # Negative grid positions never get filled, conveniently!
272 0 0       0 $grid->{ $n } ? $grid->{ $n } : ()
273 0         0 } @{ $grid_neighbours{ $dimension }};
  0         0  
274             };
275              
276             =head3 C<< random_unit_vector( $dimensions ) >>
277              
278             print join ",", @{ random_unit_vector( 2 ) };
279              
280             Returns a vector of unit length
281             pointing in a random uniform distributed
282             I-dimensional direction
283             angle
284             and returns a unit vector pointing in
285             that direction
286              
287             The algorithm used is outlined in
288             Knuth, _The Art of Computer Programming_, vol. 2,
289             3rd. ed., section 3.4.1.E.6.
290             but has not been verified formally or mathematically
291             by the module author.
292              
293             =cut
294              
295             sub random_unit_vector {
296 1000     1000 1 366925 my ($dimensions) = @_;
297 1000         1447 my (@vec,$len);
298            
299             # Create normal distributed coordinates
300             RETRY: {
301 1000         1201 @vec = map { gaussian() } 1..$dimensions;
  1000         1906  
  5500         13567  
302 1000         2033 $len = norm(@vec);
303 1000 50       2487 redo RETRY unless $len;
304             };
305             # Normalize our vector so we get a unit vector
306 1000         1305 @vec = map { $_ / $len } @vec;
  5500         7037  
307            
308             \@vec
309 1000         1939 };
310              
311             1;
312              
313             =head1 TODO
314              
315             The module does not use L or any other
316             vector library.
317              
318             =head1 REPOSITORY
319              
320             The public repository of this module is
321             L.
322              
323             =head1 SUPPORT
324              
325             The public support forum of this module is
326             L.
327              
328             =head1 BUG TRACKER
329              
330             Please report bugs in this module via the RT CPAN bug queue at
331             L
332             or via mail to L.
333              
334             =head1 AUTHOR
335              
336             Max Maischein C
337              
338             =head1 COPYRIGHT (c)
339              
340             Copyright 2011 by Max Maischein C.
341              
342             =head1 LICENSE
343              
344             This module is released under the same terms as Perl itself.
345              
346             =cut