Metric Trees and Voronoi Diagrams
Grant Schindler, 2008


Tree Structure: Click icons below image to change tree branching structure.

Randomize Tree: Click anywhere else to build a new randomized tree.

This browser does not have a Java Plug-in.
Get the latest Java Plug-in here.



Description A number of tree-based data structures implicitly define a series of nested Voronoi diagrams, and may be visualized as such. Here we visualize metric trees of varying depth and branching factor, but with a constant number of leaf nodes.

Motivation Tree-based data structures are often used to perform fast approximate (or exact) nearest neighbor search, and the branching factor of the tree can be chosen arbitrarily. The visualization technique here was developed in order to develop intuition for (e.g.) how a tree with 5 branches at each level carves up a search space differently than a tree with 2 branches at each level. (This paper (1.4MB PDF) uses the visualization in the context of city-scale location recognition from images [3].)

Technical Details In the above images, a number of random points are hierarchically clustered into a metric tree structure. Each pixel is then rendered with a single search down the tree -- the pixel's intensity is determined by the ratio of distances to the two nearest Voronoi cells at each level of the tree, as per Steven Worley's cellular texture basis function [1]. (See "Voronoi Drawing" and "Metric Trees" comments in code below for more details.)

Source Code Source code is provided below for educational purposes. Complete source files for the project are here: voronoiTrees.zip (28KB). Built with Processing.

References [1] Steven Worley. A Cellular Texture Basis Function. 1996.

[2] David Nister and Henrik Stewenius. Scalable Recognition with a Vocabulary Tree. 2006.

[3] Grant Schindler, Matthew Brown, Richard Szeliski. City-Scale Location Recognition. 2007.




//Metric Trees and Voronoi Diagrams
//Grant Schindler, 2008

int N = 512; //Image Size

//Tree Size
int nrLevels = 5; //Tree Levels/Depth
int nrBranches = 8; //Tree Branches at Each Level
int[] treeLevels = {15, 5, 3}; //Preset Trees of Different Sizes
int[] treeBranches = { 2, 8, 32};
int treeMode = 1; //Selects Between Above 3 Tree Sizes

//Points (To Be Stored in Tree)
int nrPoints = 32768; //Number of Points to Be Clustered Into Tree
float[][] gPoints = new float[nrPoints][2]; //The 2D Points Themselves
int[] gIndices = new int[nrPoints]; //Point Indices (Passed to Recursive buildTree)

//Tree Representation
float[][] gNodes; //Internal Nodes in the Tree (2D Cluster Centers)
int[][] gLeaf; //Leaf Node Storage of Point Indices
int[] gLeafCounts; //Count of Points Stored at Each Leaf Node
int[] tCount; //Temporary Count of Assigned Points to Each Node While Clustering/Building Tree
int maxLeafCount = 0; //Max #Points @Each Leaf Node (make non-zero if actually using tree for storage/retrieval)

//----------------------------------------------------------------------------------
//-- Voronoi Drawing ---------------------------------------------------------------
//--
//-- 1. computeLevelWeights - We're blending together [nrLevels] different Voronoi
//-- diagrams, but want to emphasize the low-frequency diagrams more heavily, as in:
//-- pixelValue = (1/2)*voronoi1 + (1/4)*voronoi2 + (1/8)*voronoi3 + ...
//--
//-- 2. drawVoronoi - For a single tree level, the Voronoi diagram emerges when
//-- for each pixel, we find the closest and 2nd-closest nodes (cluster centers)
//-- and plot the ratio of distances: (distClosest/dist2ndClosest)
//--
//----------------------------------------------------------------------------------

int pRow = 0, pCol = 0, pIteration = 1, pMax = 2, iterations; //Pixel-Rendering Order
int pickupLevel = 0, pickupBranch = 0, pickupStart = 0; //Tree-Search Resumption
float levelWeightSum = 0.0;
float[] levelWeightTable;
float gRatio; //Ratio of Closest:2nd-Closest Cluster Center for A Searched Point at Specific Level
boolean odd(int x) {return x % 2 != 0;}

//Pre-compute blending weights for each Voronoi level
void computeLevelWeights(){
levelWeightSum = 0.0;
float base = 1.2 + 0.8 * min(1.0, ((float)(20 - nrLevels)/(20.0 - 2.0))); //Different Base Weight for Deeper Trees
for (int l = 0; l < nrLevels; l++){
levelWeightTable[l] = pow(base, (float)(nrLevels-1) - (float)l); //More Weight for Levels Near Root
levelWeightSum += levelWeightTable[l]; //Sum -> Normalize Weights Over All Levels
}
}

void drawVoronoi(){
iterations = 0;
while (iterations < pMax){

//Pixel-Rendering Order: Compute (x,y) from pIteration, pRow, and pCol
if (pCol >= pMax) {pRow++; pCol = 0;
if (pRow >= pMax) {pIteration++; pRow = 0; pMax = int(pow(2,pIteration));}}
boolean pNeedsDrawing = (pIteration == 1 || odd(pRow) || (!odd(pRow) && odd(pCol)));
int x = pCol * (N/pMax); int y = pRow * (N/pMax);
pCol++; //Increment Position for Next Run Through

if (pNeedsDrawing){
iterations++;
float fx = (float)x/(float)N - 0.5; //Set Pixel Position
float fy = (float)y/(float)N - 0.5;
float[] p = {fx,fy};

float pixel = 0.0;
pickupBranch = 0; pickupStart = 0; pickupLevel = 0; //Reset Search
for (int l = 0; l < nrLevels; l++){
int node = search(p,l,pickupLevel,pickupBranch,pickupStart); //sets gRatio = distClosest / dist2ndClosest
float a = pow(gRatio,5.0); //Raise Ratio to Some Power (Adjust Gamma)
float fraction = levelWeightTable[l]/levelWeightSum; //Compute Weight for Current Level
pixel += fraction * (1.0-a); //Level-Weighted Sum of Ratios
}
float rgb = 255.0 * pow(pixel,3.0); //Final Gamma Adjustment
stroke(rgb); fill(rgb);
rect(x,y,(N/pMax)-1,(N/pMax)-1);
}
}
if (pRow == N-1) {empty = false; println("Done");}
}

//----------------------------------------------------------------------------------
//-- Metric Trees ------------------------------------------------------------------
//--
//-- This is a dumb implementation of hierarchically clustering a set of random
//-- points, saving the cluster centers as nodes in a tree, then for all the points
//-- assigned to each node, recursively clustering again down to the leaves.
//-- For speed, we never even iterate the k-means clustering. We just pick a
//-- random initial cluster center and keep it. It works here because our data
//-- is uniformly distributed. For example:
//-- Given 30,000 points, pick 5 of them at random to be "cluster centers". Assign
//-- each of the 30,000 points to the whichever of the 5 cluster centers is closest.
//-- You now have 5 groups of roughly 6,000 points. Use the same procedure to divide
//-- each group into 5 sub-groups, and so on, recursively.
//--
//----------------------------------------------------------------------------------

void newTree(){
initTree();
initPoints();
buildTree(gIndices,nrPoints,0,0,0);
computeLevelWeights();
}

void initTree(){
int nrNodes = 0;
for( int i = 0; i < nrLevels; i++){
nrNodes += (int)(pow(nrBranches,i+1));
}
gNodes = new float [nrNodes][2];
gLeaf = new int [(int)pow(nrBranches,nrLevels)][maxLeafCount];
gLeafCounts = new int[(int)pow(nrBranches,nrLevels)];
tCount = new int[nrBranches];
levelWeightTable = new float[nrLevels];
}

void initPoints(){
for (int i = 0; i < nrPoints; i++){
gPoints[i] = rand2(0.5);
gIndices[i] = i;}
}

void buildTree(int[] points, int n, int level, int branch, int start){
initNodes(points,n,level,branch,start); //Initialize Cluster Centers
int[][] localAssigned = assignNodes(points,n,level,branch,start); //Divide Points into [nrBranches] Clusters.
int[] localCount = new int[nrBranches];
for (int i = 0; i < nrBranches; i++){localCount[i] = tCount[i];} //Number of Points in Each Cluster

if (level < nrLevels-1){ //Recurse
start += pow(nrBranches,level+1);
for (int i = 0; i < nrBranches; i++){
buildTree(localAssigned[i],localCount[i], level+1, branch * nrBranches + i, start);
}
}
else{ //Store Assigned Points at Leaf Nodes
for (int i = 0; i < nrBranches; i++){
int node = branch*nrBranches+i; //(Ignore start, this is Leaf Nodes Only)
gLeafCounts[node] = min(maxLeafCount,localCount[i]);
for (int j = 0; j < gLeafCounts[node]; j++){
gLeaf[node][j] = localAssigned[i][j];
}
}
}
}

void initNodes(int[] points, int n, int level, int branch, int start){
for (int i = 0; i < nrBranches; i++){
if (n > 0){ gNodes[start+branch*nrBranches+i] = gPoints[points[i * n/nrBranches]];} //Init to Random Point
else{ gNodes[start+branch*nrBranches+i] = gPoints[0];} //Fill with First Point (Not Even in Cluster)
}
}

int[][] assignNodes(int[] points, int n, int level, int branch, int start)
{
//Initialize Counts
int[][] tAssigned = new int[nrBranches][n];
for (int c = 0; c < nrBranches; c++){tCount[c] = 0;}

//For each point, find nearest center, add to list
for (int i = 0; i < n; i++){
int c = search(gPoints[points[i]], level, level, branch, start);
tAssigned[c][tCount[c]] = points[i];
tCount[c]++;
}
return tAssigned;
}

//Recursively find closest cluster center up to given depth
int search(float[] p, int depth, int level, int branch, int start){
float minDist = -1.0, minDist2 = -1.0;
int node = 0;

for (int i = 0; i < nrBranches; i++){
float d = sqDist2(p,gNodes[start+branch*nrBranches+i]);
if (d < minDist2 || minDist2 < 0.0){
if (d < minDist || minDist < 0.0){
minDist2 = minDist;
minDist = d;
node = i;
}
else minDist2 = d;
}
}

//Next Search Level
pickupLevel = level+1;
pickupBranch = branch * nrBranches + node;
pickupStart = start + (int)pow(nrBranches,level+1);

if (depth == level){
gRatio = sqrt(minDist/minDist2);
return node;
}
else return search(p,depth, pickupLevel, pickupBranch, pickupStart);
}

float sqDist2(float[] a, float[] b) {return sq(a[0]-b[0]) + sq(a[1]-b[1]);}

float[] rand2(float s){
float a = N/2;
float[] rand = {floor(0.5 + random(-s,s) * a)/(a+1.0) ,floor(0.5 + random(-s,s) * a)/(a+1.0)};
return rand;
}

//----------------------------------------------------------------------------------
// -- User Interface ---------------------------------------------------------------
//----------------------------------------------------------------------------------
PFont font10, font12;
PImage[] img = new PImage[3];
String[] jpgs = {"2_15.jpg","8_5.jpg","32_3.jpg"};
int tLeft = 280, buttonsY = N+23, buttonsX[] = new int[3];
boolean empty = true, drawUI = true;

void setup(){
size(N,N+48);
frameRate(99999);
randomSeed(8); //Ensure Same Initial Points Every Run
for (int i=0; i < 3; i++) {buttonsX[i]=42 + i*40; img[i]=loadImage(jpgs[i]);} //Image Buttons
font12 = loadFont("Helvetica-Bold-12.vlw"); font10 = loadFont("Helvetica-Bold-10.vlw"); //Fonts
newTree();
}

void draw(){
if (empty) {drawVoronoi();} else {frameRate(10);}
if (drawUI) drawInterface();
}

void drawInterface(){
stroke(221,221,204); fill(221,221,204); rect(0,N,N,48); //Clear Background
stroke(0);
for (int i=0; i < 3; i++){
rect(buttonsX[i]-16,buttonsY-16,33,33);
tint((treeMode==i) ? 255: 100);
image(img[i],buttonsX[i]-16 + 1,buttonsY-16 + 1); //Draw Image Buttons
}
textFont(font12); textAlign(RIGHT); fill(128); text("Branches",tLeft,buttonsY + 9);
textFont(font10); textAlign(LEFT); fill(128); text("Levels",tLeft,buttonsY + 2);
textFont(font12); textAlign(RIGHT); fill(0); text(treeBranches[treeMode],tLeft+74,buttonsY + 9);
textFont(font10); textAlign(LEFT); fill(0); text(treeLevels[treeMode], tLeft+74 ,buttonsY + 2);
textFont(font12); textAlign(LEFT); fill(128); text("= = 32768 Leaf Nodes" ,tLeft+45,buttonsY + 9);
drawUI = false;
}

void mouseClicked(){
if (mouseY > N) { //Clicked Buttons
for (int i=0; i < 3; i++){
if (sq(mouseX-buttonsX[i]) < sq(16)){treeMode = i;}}}
nrLevels = treeLevels[treeMode];
nrBranches = treeBranches[treeMode]; //New Parameters
newTree();
pRow = 0; pCol = 0; pIteration = 1; pMax = 2; //Reset Drawing
empty = true; drawUI = true;
frameRate(99999);
}