Clip d3 voronoi with d3 hull - d3.js

I would like to draw a d3 voronoi diagram and clip it like d3 hull borders do bound a collection of nodes or any similar clipping.
The red line in this Screenshot shows what i would like to achieve.
How can it be done ?

The d3.geom.hull function will find a polygon that contains all your nodes tightly, with no extra spacing. That of course would cancel out much of the purpose of the Voronoi regions, which are intended to add some active space around the nodes. So what you need to calculate is a polygon that is a certain padding distance larger than the convex hull polygon on all sides.
My recommended algorithm:
Use d3.geom.hull(nodes) to calculate the array of vertices that define the tight boundary of your nodes.
Use those vertices to create a d3 polygon object.
Calculate the center of that polygon with .centroid().
For each vertex in your convex hull, calculate a point that is padding distance farther away from the center of the polygon.
Use this expanded polygon to clip all the polygons in the array returned by Voronoi function.
Sample code:
var hullFunction = d3.geom.hull()
.x(/*x accessor function*/)
.y(/*y accessor function*/);
var tightHull = hullFunction(nodes); //returns an array of vertices
var centerPoint = d3.geom.polygon(tightHullArray).centroid();
var expandedHull = tightHullArray.map( function(vertex) {
//Create a new array of vertices, each of which is the result
//of running this function on the corresponding vertex of the
//original hull.
//Each vertex is of the form [x,y]
var vector = [vertex[0] - centerPoint[0],
vertex[1] - centerPoint[1] ];
//the vector representing the line from center to this point
var vectorLength = Math.sqrt(vector[0]*vector[0]
+ vector[1]*vector[1]);
//Pythagorus' theorem to get the length of the line
var normalizedVector = [vector[0] / vectorLength,
vector[1] / vectorLength];
//the vector scaled down to length 1, but with the same angle
//as the original vector
return [vertex[0] + normalizedVector[0]*padding,
vertex[1] + normalizedVector[1]*padding ];
//use the normalized vector to adjust the vertex point away from
//the center point by a distance of `padding`
});
var clippedVoronoi = voronoiPolygons.map(function(voronoi) {
//voronoiPolygons would be the array returned by the voronoi function
return expandedHull.clip(voronoi);
//I think this is correct; if you get weird results, try
// return voronoi.clip(expandedHull);
});

I recently made an example to illustrate to myself how polygon clipping works:
http://tributary.io/inlet/8263747
You can see the clipping code in the update function, and the rendering code in the process function. drag the points around to see how the clipping will be affected.
A couple things to watch out for:
the order of the points in the "hull" (or clipping path in my example) matters. Your polygon has to be counter-clockwise as well as convex (no caves). If these conditions aren't met there is no error, you just get back an empty array.
the polygon operations manipulate your point arrays in place, if you don't want your geometry itself clipped, but want a copy you need to make a copy first.

Related

Point in Polygon 3d (same plane) algorithm

I have a point and a polygon in the same plane in 3d space and now I want to check whether or not the point is in the polygon or not.
Is there an easy way to change the algorithm from this thread Point in Polygon Algorithm to work for 3d space?
Or are there other algorithms that can solve this problem easily?
If there are not, would the following idea work:
Check if the plane is the XZ-plane or the YZ-plane, if yes, ignore the other axis (i.e. for the XZ-plane ignore the y values) and use the pip algorithm from the before mentioned thread. And if no, just ignore the z values of the point and the polygon and use the pip algorithm.
there are 2 "basic" ways of testing planar concave polygon:
convert to set of convex ones and test direction of cross product between point and all faces
the conversion to convex polygon is not as easy but its doable either by triangulation or clipping ear or what ever method... After that just check the cross products... so if your convex polygon has vertexes p0,p1,p2,...,p(n-1) and testing point p then
d0 = cross( p-p0 , p0-p(n-1) );
for (i=1;i<n;i++)
{
di = cross( p-p(i), p(i)-p(i-1) );
if ( dot ( d0 , di ) <=0.0 ) return false;
}
return true;
so just check all the polygons and return OR of the subresults
use hit test
You cast ray from your point in any direction parallel to your plane and count the number of hits you ray has done with the edges of polygon. If its even point is outside if its odd point is inside. The link in your question uses this algo. However in 3D you need to change the direction so it still is inside plane... for example by using single edge of polygon dir=p1-p0 as your direction. You also have to code some rules for cases when your ray hits Vertex directly so its counted just once instead of multiple times. Also the hit must be computed in 3D so you need axis/line intersection. It can be found here:
Cone to box collision
just look for line closest(axis a0,line l1) function. It returns line that is the closest connection between line and axis. Then just simply check if the two points are the same or not (length of the line is zero).
Now to simplify this you might port your 3D data into 2D
That can get rid of some accuracy problems related to rounding to the plane...
You are doing this by just ignoring one coordinate. That is simple but it might bring some rounding problems also the result has different shape (scaled differently in each axis) so the metrics is not the same anymore which might bring other problems latter if this is used for other purposes or any kind of thresholding is used.
There is a better method. We need any 2 basis vectors u,v that are perpendicular to each and are inside your plane and one point o inside the plane. That is easy just:
o = p0; // any point from the polygon
u = p1-p0; // any edge of polygon
u /= |u|; // normalize
v = p2-p1; // any other edge of polygon
v /= |v|; // normalize
for (;fabs(dot(u,v)>0.75;) // if too parallel
{
v=(p(1+rand(n-1))-p0); // chose random "edge"
v /= |v|; // normalize
}
v=cross(u,v); // make u,v perpendicular
v=cross(v,u); // and inside the plane
v /= |v|; // normalize just in case because of rounding the size might not be unit anymore
Now to convert point p(x,y,z) from 3D to 2D (x,y) just do:
x = dot(p-o,u);
y = dot(p-o,v);
to convert back to 3D:
p = o + x*u + y*v;
With this way of conversion the metrics is the same so the length of polygon edges and size of polygon will not change ...

Computing points within an angular distance in spherical coordinates

Suppose I have a graticule (say 1 degree resolution) in spherical coordinates.
Now, assuming that a certain spherical object is in a known direction (known phi and lambda) and has a known angular diameter as viewed from the origin of the coordinate system, how can I efficiently compute a list of graticule points that "end up" on the object?
The naive algorithm is of course to calculate the angle between every graticule direction and the known (phi/lambda) direction of the object, and include the point if it is less than the angular radius. This is very slow if the graticule is fine.
What algorithms can I use to speed up this computation?
Let's you have base coordinates (lat0, lon0), grid step da and max big circle arc angle AA (half of your "known angular diameter").
You can use algorithm similar to rasterization at Cartesian plane: enumeration of integer points on every scanline (here parallels) inside a circle.
Start from the central (lat0, lon0) point. Walk along meridian up and down and get points at the parallel inside given angular radius
Edit: big circle arc calculation has been rewritten
For every point at meridian (lat[i], lon0) get list of points with the same latitude usinf BigArc calculation from here (distance section).
var a = Math.sin(diflat/2) * Math.sin(diflat/2) +
Math.cos(lat0) * Math.cos(alat) *
Math.sin(diflon/2) * Math.sin(diflon/2);
var BigArc = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
(This method might fail when pole lies inside radius)
meridiansteps = Floor(AA / da)
for i = - meridiansteps to meridiansteps:
diflat = da * i
alat = lat0 + diflat
addpoint(alat, lon0)
diflon = da
while BigArc <= AA:
addpoint(alat, lon0 + diflon)
addpoint(alat, lon0 - diflon) //symmetric
diflon = diflon + da
If you can find the closest point on the graticule to the object, for instance by projecting the direction to the object onto the orientation of the graticule, then you have one point which is either on the graticule or not, and if it is not on the graticule, no point is.
Because the object is convex, its projection onto the graticule should be convex, so the points on the graticule which end up on the object should be contiguous. With one point on the object, you can use binary chop to find the two points on either side of it which are only just on the object.
The set of graticule points on the object is then all graticule points between these two furthest points.

Dodecahedron (or any platonic solids) uniform rotations, so that the vertices do not overlap any of the previous rotations

How to rotate an object, so that its vertices never overlap with any of the other rotations? With a predefined number of rotations.
Idea:
It can be achieved with relaxation. (Idea comes from Greg Turk's paper: Generating Textures on Arbitrary Surfaces Using Reaction-Diffusion)
Steps:
Generate x dodecahedrons or any object symmetric to its centre
point.
These objects should be identical in position, orientation
and size. (so we can create an easy relation between vertices =>
ones that overlap at the beginning are related)
Create a function that calculates the distance between the related points.
Maximize the average distance between related points. (every point has x-1
related points)
Problems:
This is not a simple relaxation problem with points. Here, due to the dodecahedron constraint, I cannot just translate around. Rotation matrixes/quaternions are needed.
Possible solution with Brute force
Summary:Rotate randomly until desired average distance is achieved.
Explanation:Every dodecahedron is rotated until it's vertices do not overlap any vertices of the other dodecahedrons.
Then average distance is calculated and checked against the best (minimal) so far. Save all vertex positions, and the rotation quaternion, that will turn the base dodecahedron into the rotated one.
float minThreshold <- user defined
int iterationThreshold <- user defined
float minAveDistance = Infinity;
while (minAveDistance > minThreshold || maxIteration > iterationThreshold) {
Foreach (dodecahedron) { // except the first one, that can stay as is
// rotate randomly until there are no overlapping positions with the other dodecahedrons
while (checkOverlappingWithOtherDodecahedrons(dodecahedron)) {
rotateRandomly(dodecahedron);
}
}
float aveDistance = CalculateAverageDistanceBetweenAllPointsOfDodecahedrons();
if (aveDistance < minAveDistance) {
minAveDistance = aveDistance;
SaveAllPositions(); // e.g.: to a file
SaveAllRotationQuaternionsFromStartOrientation(); // e.g.: to a file
}
}

Location of highest density on a sphere

I have a lot of points on the surface of the sphere.
How can I calculate the area/spot of the sphere that has the largest point density?
I need this to be done very fast. If this was a square for example I guess I could create a grid and then let the points vote which part of the grid is the best.
I have tried with transforming the points to spherical coordinates and then do a grid, both this did not work well since points around north pole are close on the sphere but distant after the transform.
Thanks
There is in fact no real reason to partition the sphere into a regular non-overlapping mesh, try this:
partition your sphere into semi-overlapping circles
see here for generating uniformly distributed points (your circle centers)
Dispersing n points uniformly on a sphere
you can identify the points in each circle very fast by a simple dot product..it really doesn't matter if some points are double counted, the circle with the most points still represents the highest density
mathematica implementation
this takes 12 seconds to analyze 5000 points. (and took about 10 minutes to write )
testcircles = { RandomReal[ {0, 1}, {3}] // Normalize};
Do[While[ (test = RandomReal[ {-1, 1}, {3}] // Normalize ;
Select[testcircles , #.test > .9 & , 1] ) == {} ];
AppendTo[testcircles, test];, {2000}];
vmax = testcircles[[First#
Ordering[-Table[
Count[ (testcircles[[i]].#) & /# points , x_ /; x > .98 ] ,
{i, Length[testcircles]}], 1]]];
To add some other, alternative schemes to the mix: it's possible to define a number of (almost) regular grids on sphere-like geometries by refining an inscribed polyhedron.
The first option is called an icosahedral grid, which is a triangulation of the spherical surface. By joining the centres of the triangles about each vertex, you can also create a dual hexagonal grid based on the underlying triangulation:
Another option, if you dislike triangles (and/or hexagons) is the cubed-sphere grid, formed by subdividing the faces of an inscribed cube and projecting the result onto the spherical surface:
In either case, the important point is that the resulting grids are almost regular -- so to evaluate the region of highest density on the sphere you can simply perform a histogram-style analysis, counting the number of samples per grid cell.
As a number of commenters have pointed out, to account for the slight irregularity in the grid it's possible to normalise the histogram counts by dividing through by the area of each grid cell. The resulting density is then given as a "per unit area" measure. To calculate the area of each grid cell there are two options: (i) you could calculate the "flat" area of each cell, by assuming that the edges are straight lines -- such an approximation is probably pretty good when the grid is sufficiently dense, or (ii) you can calculate the "true" surface areas by evaluating the necessary surface integrals.
If you are interested in performing the requisite "point-in-cell" queries efficiently, one approach is to construct the grid as a quadtree -- starting with a coarse inscribed polyhedron and refining it's faces into a tree of sub-faces. To locate the enclosing cell you can simply traverse the tree from the root, which is typically an O(log(n)) operation.
You can get some additional information regarding these grid types here.
Treating points on a sphere as 3D points might not be so bad.
Try either:
Select k, do approximate k-NN search in 3D for each point in the data or selected point of interest, then weight the result by their distance to the query point. Complexity may vary for different approximate k-NN algorithms.
Build a space-partitioning data structure like k-d Tree, then do approximate (or exact) range counting query with a ball range centered at each point in the data or selected point of interest. Complexity is O(log(n) + epsilon^(-3)) or O(epsilon^(-3)*log(n)) for each approximate range query with state of the art algorithms, where epsilon is the range error threshold w.r.t. the size of the querying ball. For exact range query, the complexity is O(n^(2/3)) for each query.
Partition the sphere into equal-area regions (bounded by parallels and meridians) as described in my answer there and count the points in each region.
The aspect ratio of the regions will not be uniform (the equatorial regions will be more "squarish" when N~M, while the polar regions will be more elongated).
This is not a problem because the diameters of the regions go to 0 as N and M increase.
The computational simplicity of this method trumps the better uniformity of domains in the other excellent answers which contain beautiful pictures.
One simple modification would be to add two "polar cap" regions to the N*M regions described in the linked answer to improve the numeric stability (when the point is very close to a pole, its longitude is not well defined). This way the aspect ratio of the regions is bounded.
You can use the Peters projection, which preserves the areas.
This will allow you to efficiently count the points in a grid, but also in a sliding window (box Parzen window) by using the integral image trick.
If I understand correctly, you are trying to find the densepoint on sphere.
if points are denser at some point
Consider Cartesian coordinates and find the mean X,Y,Z of points
Find closest point to mean X,Y,Z that is on sphere (you may consider using spherical coordinates, just extend the radius to original radius).
Constraints
If distance between mean X,Y,Z and the center is less than r/2, then this algorithm may not work as desired.
I am not master of mathematics but may be it can solve by analytical way as:
1.Short the coordinate
2.R=(Σ(n=0. n=max)(Σ(m=0. M=n)(1/A^diff_in_consecative))*angle)/Σangle
A=may any constant
This is really just an inverse of this answer of mine
just invert the equations of equidistant sphere surface vertexes to surface cell index. Don't even try to visualize the cell different then circle or you go mad. But if someone actually do it then please post the result here (and let me now)
Now just create 2D cell map and do the density computation in O(N) (like histograms are done) similar to what Darren Engwirda propose in his answer
This is how the code looks like in C++
//---------------------------------------------------------------------------
const int na=16; // sphere slices
int nb[na]; // cells per slice
const int na2=na<<1;
int map[na][na2]; // surface cells
const double da=M_PI/double(na-1); // latitude angle step
double db[na]; // longitude angle step per slice
// sherical -> orthonormal
void abr2xyz(double &x,double &y,double &z,double a,double b,double R)
{
double r;
r=R*cos(a);
z=R*sin(a);
y=r*sin(b);
x=r*cos(b);
}
// sherical -> surface cell
void ab2ij(int &i,int &j,double a,double b)
{
i=double(((a+(0.5*M_PI))/da)+0.5);
if (i>=na) i=na-1;
if (i< 0) i=0;
j=double(( b /db[i])+0.5);
while (j< 0) j+=nb[i];
while (j>=nb[i]) j-=nb[i];
}
// sherical <- surface cell
void ij2ab(double &a,double &b,int i,int j)
{
if (i>=na) i=na-1;
if (i< 0) i=0;
a=-(0.5*M_PI)+(double(i)*da);
b= double(j)*db[i];
}
// init variables and clear map
void ij_init()
{
int i,j;
double a;
for (a=-0.5*M_PI,i=0;i<na;i++,a+=da)
{
nb[i]=ceil(2.0*M_PI*cos(a)/da); // compute actual circle cell count
if (nb[i]<=0) nb[i]=1;
db[i]=2.0*M_PI/double(nb[i]); // longitude angle step
if ((i==0)||(i==na-1)) { nb[i]=1; db[i]=1.0; }
for (j=0;j<na2;j++) map[i][j]=0; // clear cell map
}
}
//---------------------------------------------------------------------------
// this just draws circle from point x0,y0,z0 with normal nx,ny,nz and radius r
// need some vector stuff of mine so i did not copy the body here (it is not important)
void glCircle3D(double x0,double y0,double z0,double nx,double ny,double nz,double r,bool _fill);
//---------------------------------------------------------------------------
void analyse()
{
// n is number of points and r is just visual radius of sphere for rendering
int i,j,ii,jj,n=1000;
double x,y,z,a,b,c,cm=1.0/10.0,r=1.0;
// init
ij_init(); // init variables and map[][]
RandSeed=10; // just to have the same random points generated every frame (do not need to store them)
// generate draw and process some random surface points
for (i=0;i<n;i++)
{
a=M_PI*(Random()-0.5);
b=M_PI* Random()*2.0 ;
ab2ij(ii,jj,a,b); // cell corrds
abr2xyz(x,y,z,a,b,r); // 3D orthonormal coords
map[ii][jj]++; // update cell density
// this just draw the point (x,y,z) as line in OpenGL so you can ignore this
double w=1.1; // w-1.0 is rendered line size factor
glBegin(GL_LINES);
glColor3f(1.0,1.0,1.0); glVertex3d(x,y,z);
glColor3f(0.0,0.0,0.0); glVertex3d(w*x,w*y,w*z);
glEnd();
}
// draw cell grid (color is function of density)
for (i=0;i<na;i++)
for (j=0;j<nb[i];j++)
{
ij2ab(a,b,i,j); abr2xyz(x,y,z,a,b,r);
c=map[i][j]; c=0.1+(c*cm); if (c>1.0) c=1.0;
glColor3f(0.2,0.2,0.2); glCircle3D(x,y,z,x,y,z,0.45*da,0); // outline
glColor3f(0.1,0.1,c ); glCircle3D(x,y,z,x,y,z,0.45*da,1); // filled by bluish color the more dense the cell the more bright it is
}
}
//---------------------------------------------------------------------------
The result looks like this:
so now just see what is in the map[][] array you can find the global/local min/max of density or whatever you need... Just do not forget that the size is map[na][nb[i]] where i is the first index in array. The grid size is controlled by na constant and cm is just density to color scale ...
[edit1] got the Quad grid which is far more accurate representation of used mapping
this is with na=16 the worst rounding errors are on poles. If you want to be precise then you can weight density by cell surface size. For all non pole cells it is simple quad. For poles its triangle fan (regular polygon)
This is the grid draw code:
// draw cell quad grid (color is function of density)
int i,j,ii,jj;
double x,y,z,a,b,c,cm=1.0/10.0,mm=0.49,r=1.0;
double dx=mm*da,dy;
for (i=1;i<na-1;i++) // ignore poles
for (j=0;j<nb[i];j++)
{
dy=mm*db[i];
ij2ab(a,b,i,j);
c=map[i][j]; c=0.1+(c*cm); if (c>1.0) c=1.0;
glColor3f(0.2,0.2,0.2);
glBegin(GL_LINE_LOOP);
abr2xyz(x,y,z,a-dx,b-dy,r); glVertex3d(x,y,z);
abr2xyz(x,y,z,a-dx,b+dy,r); glVertex3d(x,y,z);
abr2xyz(x,y,z,a+dx,b+dy,r); glVertex3d(x,y,z);
abr2xyz(x,y,z,a+dx,b-dy,r); glVertex3d(x,y,z);
glEnd();
glColor3f(0.1,0.1,c );
glBegin(GL_QUADS);
abr2xyz(x,y,z,a-dx,b-dy,r); glVertex3d(x,y,z);
abr2xyz(x,y,z,a-dx,b+dy,r); glVertex3d(x,y,z);
abr2xyz(x,y,z,a+dx,b+dy,r); glVertex3d(x,y,z);
abr2xyz(x,y,z,a+dx,b-dy,r); glVertex3d(x,y,z);
glEnd();
}
i=0; j=0; ii=i+1; dy=mm*db[ii];
ij2ab(a,b,i,j); c=map[i][j]; c=0.1+(c*cm); if (c>1.0) c=1.0;
glColor3f(0.2,0.2,0.2);
glBegin(GL_LINE_LOOP);
for (j=0;j<nb[ii];j++) { ij2ab(a,b,ii,j); abr2xyz(x,y,z,a-dx,b-dy,r); glVertex3d(x,y,z); }
glEnd();
glColor3f(0.1,0.1,c );
glBegin(GL_TRIANGLE_FAN); abr2xyz(x,y,z,a ,b ,r); glVertex3d(x,y,z);
for (j=0;j<nb[ii];j++) { ij2ab(a,b,ii,j); abr2xyz(x,y,z,a-dx,b-dy,r); glVertex3d(x,y,z); }
glEnd();
i=na-1; j=0; ii=i-1; dy=mm*db[ii];
ij2ab(a,b,i,j); c=map[i][j]; c=0.1+(c*cm); if (c>1.0) c=1.0;
glColor3f(0.2,0.2,0.2);
glBegin(GL_LINE_LOOP);
for (j=0;j<nb[ii];j++) { ij2ab(a,b,ii,j); abr2xyz(x,y,z,a-dx,b+dy,r); glVertex3d(x,y,z); }
glEnd();
glColor3f(0.1,0.1,c );
glBegin(GL_TRIANGLE_FAN); abr2xyz(x,y,z,a ,b ,r); glVertex3d(x,y,z);
for (j=0;j<nb[ii];j++) { ij2ab(a,b,ii,j); abr2xyz(x,y,z,a-dx,b+dy,r); glVertex3d(x,y,z); }
glEnd();
the mm is the grid cell size mm=0.5 is full cell size , less creates a space between cells
If you want a radial region of the greatest density, this is the robust disk covering problem with k = 1 and dist(a, b) = great circle distance (a, b) (see https://en.wikipedia.org/wiki/Great-circle_distance)
https://www4.comp.polyu.edu.hk/~csbxiao/paper/2003%20and%20before/PDCS2003.pdf
Consider using a geographic method to solve this. GIS tools, geography data types in SQL, etc. all handle curvature of a spheroid. You might have to find a coordinate system that uses a pure sphere instead of an earthlike spheroid if you are not actually modelling something on Earth.
For speed, if you have large numbers of points and want the densest location of them, a raster heatmap type solution might work well. You could create low resolution rasters, then zoom to areas of high density and create higher resolution only cells that you care about.

How to detect a hole in the triangular mesh?

Actually, I can detect border or edges of a convex triangular mesh by checking which edge of the triangle does not have any neighbor. So, if a mesh has some holes, then we can highlight that part easily because we have edge vertices.
But the issue is, if we just have the edge vertices or borders, how can we know that the mesh has some holes ? and how many holes the mesh has ?
I thought enough about this issue, but unable to get it, any idea ? What should be the condition or check for hole detection ?
After detection of a hole I want to fill it. But first thing is to detect it ?
Thanks.
Assuming that the mesh is connected and you can highlight all the boundaries. You are left with all the holes + one additional boundary which is that of mesh itself. You can just discard the boundary with the biggest length of them and get all the holes.
A triangular mesh derived from a scanner (eg. Kinect) can have small fragments (isolated patches) as well as small holes. I suggest a hole can generally be detected by counting the number of vertices that neighbor the vertices on the boundary. It is not a hole if there are fewer neighboring vertices than boundary vertices.
My answer will only work for a closed mesh, but it will handle the case of concave and convex holes.
For the sake of explanation, lets imagine a 2D mesh.
Calculate a bounding box for the mesh. In our example the bounding box needs to store min and max values for the X and Y axis, and a corresponding vertex index for each value:
struct BoundingBox
{
float minX,maxX,minY,maxY;
int vminX,vmaxX,vminY,vmaxY;
}
Iterate over every vertex in the mesh, growing the bounding box as you add each point. When a vertex is responsible for changing one of the min/max values, store or overwrite the corresponding vmin/vmax value with the vertex mesh index.
E.g.
BoundingBox bounds;
bounds.minX = verts[0].X;
bounds.maxX = verts[0].X;
bounds.minY = verts[0].Y;
bounds.maxY = verts[0].Y;
bounds.vminX = bounds.vmaxX = bounds.vminY = bounds.vmaxY = 0;
for (int i = 1; i < numVerts; i++)
{
Vertex v = verts[i];
if (v.X < bounds.minX) { bounds.minX = v.X; bounds.vminX = i; }
if (v.X > bounds.maxX) { bounds.maxX = v.X; bounds.vmaxX = i; }
if (v.Y < bounds.minY) { bounds.minY = v.Y; bounds.vminY = i; }
if (v.Y > bounds.maxY) { bounds.maxY = v.Y; bounds.vmaxY = i; }
}
Now iterate over your boundaries until you find one which contains ALL the vertices you gathered in the bounding box. This is your outer boundary. The remaining boundaries are holes within the mesh.
Indeed, a hole in triangular mesh is a closed loop of adjacent oriented boundary edges, each of which does not have a triangle from one side (e.g. from left).
So the algorithm to enumerate all holes can be as follows
Find all boundary edges (the edges without left triangle) and compose a set with them.
Take any boundary edge from the set, then search for the next edge in the set originating in the vertex where the previous edge terminates, remove it from the set as well and continue until edge loop is closed. This will give you one of the holes.
Repeat step 2. until the set is empty.
Some operations here can be optimized by using specialized data structures for mesh representation, e.g. half-edges.
The animation below demonstrates how starting from an arbitrary boundary edge (under the cursor), the whole hole can be found and highlighted:
(captured in MeshInspector application)

Resources