Application Center - Maplesoft

App Preview:

2-D Voronoi Diagrams with Byers' Method

You can switch back to the summary page by clicking here.

Learn about Maple
Download Application


 

voronoi.mws

2D Voronoi Diagrams using a Method by Byers

Bruno Guerrieri

Florida A&M University

Tallahassee, FL 32312

Given N points (sites) in the plane (a 1x1 square) we would like to generate a tessellation of that domain by assigning every point in the plane to its nearest site.  The process, as presented by Byers [1], is to select a particular site, call it C, and determine its Voronoi polygon.  This is achieved by a) generating all segments from C to the remaining sites, b) considering all bisecting lines of each of these segments (in other words, the normal to the segment through the midpoint), c) determining all intersections points of these 'bisectors", d) eliminate those intersection points not in the 1x1 square, and e) determine which ones of the remaining intersection points are "closest" to C.  Their convex hull is the Voronoi cell we are looking for.  We then repeat the process for each of the remaining sites.  We are mainly interested in the discovery process and, therefore, are not always concerned with efficiency or storage minimization.

> restart: interface( warnlevel=0 ):

> with(plots): with(plottools): with(ListTools): with(LinearAlgebra):

Procedures

Randomly Selected Sites

Procedure for randomly generating N points (sites) in a 1x1 square.  We will try to have the selected sites not"fall" too close to any of the edges.

> site_position := proc()

>     global N;

>     local k, toss, site_pos;

>     toss := rand(5..949);

>     for k from 1 to N do

>          site_pos[k] := [toss()/10^3,toss()/10^3];

>     end do;

>     RETURN(site_pos);

> end:

 Remove Elements from a List

Once a point has been found to belong to the convex hull, we want to eliminate it from further consideration.  We also will be removing all the "interior" points that have been "passed by".

> removal := proc(a,m)

>   local i, k, running_list;

>   running_list := [];

>   for i from 1 to nops(a) do

>      if i <> m then running_list := [op(running_list),a[i]]; fi:

>   end do:

>   RETURN(running_list);

> end:

 Is Intersection Point a Candidate?

It is a candidate if there is no bisecting line strictly between that intersection point  and the site (xc,yc).

> sift := proc()

>    global running_list,new_list,xc,yc,N;

>    local m,k2,candidates,val1,val2,test_val;
   candidates := [];

>    test_val := (xm,ym,x,y) -> 2*(xc-xm)*x+2*(yc-ym)*y+xm^2-xc^2+ym^2-yc^2;

>    for m from 1 to N-1 do

>        val1 := test_val(running_list[m][1],running_list[m][2],xc,yc);

>        for k2 from 1 to nops(new_list) do

>            val2 := test_val(running_list[m][1],running_list[m][2],new_list[k2][1],new_list[k2][2]);

>            if ((abs(val2) < 0.00001) or (val1*val2 > 0)) then candidates := [op(candidates),new_list[k2]]; break; end if;

>        end do;

>    end do;

>    RETURN(candidates);

> end:

Elevation

The formula we use for the arctan is one that returns angles in interval (-Pi,Pi) (Consult [2]).  "a" is the list of candidates and "qg" is the observation point (site whose Voronoi cell we are trying to determine) (xc,yc)

> elevation := proc(a,qg)

>   local i, theta, dx, dy;

>   for i from 1 to nops(a) do
              dx := (a[i][1]-qg[1]); dy := (a[i][2]-qg[2]);

>               theta[i] := 2 * arctan(dy/(dx + sqrt((dx)^2 + (dy)^2)));

>   end do:
  RETURN(seq(theta[i],i=1..nops(a)));

> end:

>

Voronoi

This is the condensed version of the process.  It will be used in Main 2.  In Main 1 (see below), one finds all the ingredients found in procedure Voronoi, shown one step at a time.

> Voronoi := proc(k)
      global N, original_list;

      local xc,yc,M,running_list,intersection_pairs,j,A,B,temp,m,xm,ym,fx,fy,filtered_pairs,point_intersect,p_line,

      pict,candidates,test_val,xj,yj,logical1,val1,val2,i,ival,angle_list,sorted_angles,aa,bb,corners,NC,counter,

      on_edge,cell;

      running_list := original_list:      

>       xc := original_list[k][1]: yc := original_list[k][2]:

>       running_list := removal(running_list,k): M := nops(running_list):

>       intersection_pairs := []:

>       for j from 1 to M do

>            for i from j+1 to M do
                A := Matrix([[2*(xc-running_list[i][1]),2*(yc-running_list[i][2])],

                         [2*(xc-running_list[j][1]),2*(yc-running_list[j][2])]]);

                B := Matrix([ [(yc)^2-(running_list[i][2])^2 + (xc)^2-(running_list[i][1])^2],

                         [(yc)^2-(running_list[j][2])^2 + (xc)^2-(running_list[j][1])^2]   ]);

>                 temp := MatrixInverse(A).B;
                intersection_pairs := [op(intersection_pairs),[temp[1,1],temp[2,1]]];

>            end do;

>       end do:

>       for m from 1 to M do
              xm := running_list[m][1]: ym := running_list[m][2]:

              fx := y -> solve(2*(xc-xm)*x+2*(yc-ym)*y+xm^2-xc^2+ym^2-yc^2 = 0,x);

              fy := x -> solve(2*(xc-xm)*x+2*(yc-ym)*y+xm^2-xc^2+ym^2-yc^2 = 0,y);

              if (xc <> xm) then intersection_pairs := [op(intersection_pairs),[fx(0),0]];

                       intersection_pairs := [op(intersection_pairs),[fx(1),1]]; end if;

              if (yc <> ym) then intersection_pairs := [op(intersection_pairs),[0,fy(0)]];

                       intersection_pairs := [op(intersection_pairs),[1,fy(1)]]; end if;

>       end do:

>       filtered_pairs := []:

>       for m from 1 to nops(intersection_pairs) do

>               if ((intersection_pairs[m][1] >= 0) and (intersection_pairs[m][1] <= 1)
                   and (intersection_pairs[m][2] >= 0) and (intersection_pairs[m][2] <= 1))

>               then filtered_pairs := [op(filtered_pairs),intersection_pairs[m]]; end if;

>       end do:

>       candidates := []:

>       test_val := (xp,yp,x,y) -> 2*(xc-xp)*x+2*(yc-yp)*y+xp^2-xc^2+yp^2-yc^2:

>       for j from 1 to nops(filtered_pairs) do
              xj := filtered_pairs[j][1]: yj := filtered_pairs[j][2]:

              logical1 := true;

>               for m from 1 to M while (logical1 = true) do
                   xm := running_list[m][1]: ym := running_list[m][2]:

                   val1 := test_val(xm,ym,xc,yc):

                   val2 := test_val(xm,ym,xj,yj);

>                    if ((abs(val2) > 0.00001) and (val1*val2 < 0)) then logical1 := false; ival := j; end if;

>               end do;
              if (logical1 = true) then candidates := [op(candidates),[filtered_pairs[j][1],filtered_pairs[j][2]]]; end if;

>       end do:

>       candidates := convert(convert(candidates,set),list):

>       angle_list := [elevation(candidates,[xc,yc])]:

>       sorted_angles := sort(angle_list): map(x -> evalf(x*180.0/Pi),sorted_angles):

>       for m from 1 to nops(angle_list) do
               aa[m] := BinarySearch(sorted_angles,angle_list[m]);

               bb[aa[m]] := m:

>       end do:

>       corners :=[seq(candidates[bb[m]],m=1..nops(candidates))]: NC := nops(corners):

>       counter := 1:

>       on_edge := false:

>       for m from 1 to NC do

>            if ((corners[m][1]*corners[m][2] = 0) or ((corners[m][1]-1)*(corners[m][2]-1) = 0)) then on_edge := true; end if;

>       end do:

>       for m from 1 to NC do

>             if ((corners[1][1]*corners[1][2] <> 0)
                  and (corners[1][1]-1)*(corners[1][2]-1) <> 0)

>             then corners := Rotate(corners,1); end if;

>       end do:

>       if ((corners[2][1]*corners[2][2]=0 or
              (corners[2][1]-1)*(corners[2][2]-1)=0)) then

              corners := Rotate(corners,1);

>       fi:

>       if (on_edge) then cell := PLOT(CURVES(corners,THICKNESS(1))):
            else

               cell := PLOT(POLYGONS(corners,THICKNESS(1))):

      fi:

      RETURN(cell);

end:

Main 1

Initially, let us select a certain number of random points that will become the sites.  Let us record their coordinates and plot them as well.  To determine the Voronoi diagram, we are first going to determine the equations of all the "bisecting" lines.  Then we will determine all the points of intersection of these different lines and then determine which of these intersection points belong to the Voronoi diagram.  N represents the number of sites.  We will maintain a copy of the original list of sites in "original_list'.  We will also have a "running_list" of sites which will dynamically change through the process.

> N := 6;

N := 6

> sites := site_position():

> sites_coordinates := evalf(seq(sites[i],i=1..N)): original_list := [sites_coordinates]; running_list := original_list:

original_list := [[.11600000000000000000, .40000000000000000000], [.21200000000000000000, .92800000000000000000], [.36600000000000000000, .65800000000000000000], [.32500000000000000000, .6730000000000...

> p_points := PLOT(POINTS(sites_coordinates,SYMBOL(DIAMOND))): cadre := PLOT(POLYGONS([[0,0],[1,0],[1,1],[0,1]],COLOR(HUE,0.5))):

> display(p_points,cadre);

[Plot]

At this moment, we will proceed slowly.  Once the sequence of steps to follow has proven successful to generate a particular Voronoi cell, we will aggregate all commands into a single procedure..  We are going to select one of our original sites (the k-th one), call it (xc,yc),  as the one under scrutiny at the moment.  In the graphics we will identify it by circling it in red. We wish to determine its Voronoi cell. Let us find the equations for all the "bisecting" lines about (xc,yc).

> toss := rand(1..N): k := toss(); xc := original_list[k][1]; yc := original_list[k][2];
bulleye := circle([xc,yc],0.02,color=red,thickness=3):

k := 4

xc := .32500000000000000000

yc := .67300000000000000000

> running_list := removal(running_list,k); M := nops(running_list);

running_list := [[.11600000000000000000, .40000000000000000000], [.21200000000000000000, .92800000000000000000], [.36600000000000000000, .65800000000000000000], [.12600000000000000000, .53400000000000...

M := 5

Next, we calculate the equations of the lines that are perpendicular bisectors between (xc,yc), the chosen point, and each of its neighbors (as well as the four boundary lines).  Once that is done, we will calculate all the possible intersection points.  We are doing so by solving systems of 2x2 equations in matrix form.

> intersection_pairs := []:

> for j from 1 to M do

>    for i from j+1 to M do
           A := Matrix([[2*(xc-running_list[i][1]),2*(yc-running_list[i][2])],

                 [2*(xc-running_list[j][1]),2*(yc-running_list[j][2])]]);

           B := Matrix([ [(yc)^2-(running_list[i][2])^2 + (xc)^2-(running_list[i][1])^2],

                         [(yc)^2-(running_list[j][2])^2 + (xc)^2-(running_list[j][1])^2]   ]);

>            temp := MatrixInverse(A).B;
           intersection_pairs := [op(intersection_pairs),[temp[1,1],temp[2,1]]];

>    end do;

> end do:

Let us convert this mass of matrices into a list of ordered pairs representing the intersection points.

> intersection_pairs; nops(intersection_pairs);

[[0.1968225898459783230e-1, .69023958927552766687], [.28128098827470686770, .48996803461753210498], [.3318342300996993195, .4512661022313657224], [.46354265402843601893, .3504343784177907401], [.41934...

10

We need to also include the points of intersection that are the result of intersection between bisecting lines and boundary lines.

We have a problem here if some of the calculations returns no intersection points!!!!!!

> for m from 1 to M do
    xm := running_list[m][1]: ym := running_list[m][2]:

    fx := y -> solve(2*(xc-xm)*x+2*(yc-ym)*y+xm^2-xc^2+ym^2-yc^2 = 0,x);

    fy := x -> solve(2*(xc-xm)*x+2*(yc-ym)*y+xm^2-xc^2+ym^2-yc^2 = 0,y);

    if (xc <> xm) then intersection_pairs := [op(intersection_pairs),[fx(0),0]];

                       intersection_pairs := [op(intersection_pairs),[fx(1),1]]; end if;

    if (yc <> ym) then intersection_pairs := [op(intersection_pairs),[0,fy(0)]];

                       intersection_pairs := [op(intersection_pairs),[1,fy(1)]]; end if;

> end do:

> intersection_pairs; nops(intersection_pairs);

[[0.1968225898459783230e-1, .69023958927552766687], [.28128098827470686770, .48996803461753210498], [.3318342300996993195, .4512661022313657224], [.46354265402843601893, .3504343784177907401], [.41934...[[0.1968225898459783230e-1, .69023958927552766687], [.28128098827470686770, .48996803461753210498], [.3318342300996993195, .4512661022313657224], [.46354265402843601893, .3504343784177907401], [.41934...[[0.1968225898459783230e-1, .69023958927552766687], [.28128098827470686770, .48996803461753210498], [.3318342300996993195, .4512661022313657224], [.46354265402843601893, .3504343784177907401], [.41934...

30

We are going to filter out all the points of intersection that are outside of the [0,1] x [0,1] square and keep the remaining candidates in "filtered_pairs".

> filtered_pairs := []:

> for m from 1 to nops(intersection_pairs) do

>      if ((intersection_pairs[m][1] >= 0) and (intersection_pairs[m][1] <= 1)
          and (intersection_pairs[m][2] >= 0) and (intersection_pairs[m][2] <= 1))

>           then filtered_pairs := [op(filtered_pairs),intersection_pairs[m]]; end if;

> end do;

> filtered_pairs; nops(filtered_pairs);

[[0.1968225898459783230e-1, .69023958927552766687], [.28128098827470686770, .48996803461753210498], [.3318342300996993195, .4512661022313657224], [.46354265402843601893, .3504343784177907401], [.41934...[[0.1968225898459783230e-1, .69023958927552766687], [.28128098827470686770, .48996803461753210498], [.3318342300996993195, .4512661022313657224], [.46354265402843601893, .3504343784177907401], [.41934...

19

> point_intersect := PLOT(POINTS(op(filtered_pairs),SYMBOL(CROSS))):

Let us show graphically the situation as it is so far.  We will "draw" the sites as well as the bisecting lines and the relevant intersection points that will help use determine the Voronoi cell associated with (xc,yc) (circled in red).  Please, recall that the bisecting lines you see are only the ones involving (xc,yc) with the rest of the sites.  By then, the eye can pretty much figure out the edges of the polygonal cell about (xc,yc).

> p_line := []:

> for m from 1 to M do
   xm := running_list[m][1]: ym := running_list[m][2]:

   pict := plot((xm^2-xc^2+ym^2-yc^2+2*(xc-xm)*x)/2/(ym-yc),x=0..1,y=0..1):

>    p_line := [op(p_line),pict];

> end do:

> display(p_points,cadre,point_intersect,p_line,bulleye);

[Plot]

What needs to be done now? We need to find out which intersection points, in the filtered list, need to be retained because they are on the "same" side as (xc,yc) with respect to the bisecting lines.  This is the crux of a second filtering process.  An intersection point will be retained if there is no "bisecting" line strictly between it and (xc,yc).  We say strictly because the intersection point under scrutiny will be itself on a bisecting line.

> candidates := []:

> test_val := (xp,yp,x,y) -> 2*(xc-xp)*x+2*(yc-yp)*y+xp^2-xc^2+yp^2-yc^2:

> for j from 1 to nops(filtered_pairs) do
       xj := filtered_pairs[j][1]: yj := filtered_pairs[j][2]:

       logical1 := true;

>        for m from 1 to M while (logical1 = true) do
           xm := running_list[m][1]: ym := running_list[m][2]:

           val1 := test_val(xm,ym,xc,yc):

           val2 := test_val(xm,ym,xj,yj);

>            if ((abs(val2) > 0.00001) and (val1*val2 < 0)) then logical1 := false; ival := j; end if;

>        end do;
       if (logical1 = true) then candidates := [op(candidates),[filtered_pairs[j][1],filtered_pairs[j][2]]]; end if;

> end do:

> candidates;

[[.41934589041095890417, .86734543378995433789], [.19280354696300806825, .76695608159537220278], [.31425750300120048019, .58010384153661464586]]

We would like to, once more, represent the situation graphically, circling the vertives of the polygonal cell forming the Voronoi cell.  Depending on the point (xc,yc) being analyzed, it may tun out that the cell is fully contained within the square while, at other times, the boundary will "intervene".  We will, therefore have to analyze subcases since the polygonal contours will be sometime "closed" and sometime "open".

> candidates := convert(convert(candidates,set),list); nops(candidates);

candidates := [[.31425750300120048019, .58010384153661464586], [.41934589041095890417, .86734543378995433789], [.19280354696300806825, .76695608159537220278]]

3

> p_circle := []:

> for m from 1 to nops(candidates) do

>    temp := circle([candidates[m][1],candidates[m][2]],0.02,color=blue,thickness=3);
   p_circle := [op(p_circle),temp];

> end do:

> display(p_points,cadre,point_intersect,p_line,bulleye,p_circle);

[Plot]

Here is a little technicality. We need to "order" the vertices in a clockwise manner so as to draw the "correct" (neigbhor to neighbor) polygonal contour.We will achieve this by calculating the polar angle for each of the verticeses and use these values to order them.

> angle_list := [elevation(candidates,[xc,yc])];

angle_list := [-1.6859247972215365158, 1.1188525782678557119, 2.5237011038612238832]

> sorted_angles := sort(angle_list); map(x -> evalf(x*180.0/Pi),sorted_angles);

sorted_angles := [-1.6859247972215365158, 1.1188525782678557119, 2.5237011038612238832]

[-96.596375457243178028, 64.105530632078741247, 144.59742200375514985]

> for m from 1 to nops(angle_list) do
      aa[m] := BinarySearch(sorted_angles,angle_list[m]);

      bb[aa[m]] := m:

> end do:

> corners :=[seq(candidates[bb[m]],m=1..nops(candidates))]; NC := nops(corners);

corners := [[.31425750300120048019, .58010384153661464586], [.41934589041095890417, .86734543378995433789], [.19280354696300806825, .76695608159537220278]]

NC := 3

What do we want to do now.  It seems that whenever the cell has an edge on the boundary (whether along an edge or in a corner) all we need to do is rotate the list "corner" to the left until the first pair containing a 0 in the x-position or the y-position is on the far right.  We think the second edge point will have naturally migrated to the head of the pack!

> on_edge := false:

> for m from 1 to NC do

>     if ((corners[m][1]*corners[m][2] = 0) or ((corners[m][1]-1)*(corners[m][2]-1) = 0)) then on_edge := true; end if;

> end do;

> for m from 1 to NC do

>    if ((corners[1][1]*corners[1][2] <> 0)
       and (corners[1][1]-1)*(corners[1][2]-1) <> 0)

>        then corners := Rotate(corners,1); end if;

> end do;

> if ((corners[2][1]*corners[2][2]=0 or
   (corners[2][1]-1)*(corners[2][2]-1)=0)) then

          corners := Rotate(corners,1);

> end if;

> if (on_edge) then cell := PLOT(CURVES(corners,THICKNESS(4))):
else

    cell := PLOT(POLYGONS(corners,THICKNESS(4))):

fi:

> display(p_points,cadre,point_intersect,p_line,bulleye,p_circle,cell);

[Plot]

Main 2

We have automatized the process.  We call the procedure Voronoi to generate all Voronoi cells, one at a time, and plot the final result.  All this was developed on Maple purely for pedagogical results and within the context of a Research Experience for Undergraduates. .  Neither storage nor efficiency of processes have been optimized. Note that, if you wish to run Main 2 for a different value of  N, then remove the "comment indicator" # on the next line.  Choosing a large value for N (say 20) will result in delays that grow exponentially with N.

> #N := 10;sites := site_position():sites_coordinates := evalf(seq(sites[i],i=1..N)): original_list := [sites_coordinates]: running_list := original_list:

> p_points := PLOT(POINTS(sites_coordinates,SYMBOL(DIAMOND))): cadre := PLOT(POLYGONS([[0,0],[1,0],[1,1],[0,1]],COLOR(HUE,0.5))):

> for k from 1 to N do
   cell[k] := Voronoi(k):

end do:

> display(p_points,cadre,seq(cell[m],m=1..N));

[Plot]

To display Voronoi cell 3, look at the relevant frame.

> display(p_points,cadre,cell[3]);

[Plot]

>

 Bibliography

[1] Byers J.A.,"Dirichlet Tessellation of Bark Beetle Spatial Attack Points", Journal of Animal Ecology, 61(1992) p. 759-768.

[2] Baker J.A. and Sluis W., "Arg", Mathematics Magazine, 70 (1997), no. 4, p. 291.