As you already know #1,#2 then lets focus on #3 (detect if 3D voxel cluster has one ore more holes). After some thinking I revised the original algo a bit:
mark border voxels
so any voxel equal to 1 set to 2 if its neigbors any voxel with 0. After this 0 is empty space, 1 is interior, 2 is surface.
use growth fill to create SDR map of your object
so mark all voxels which are set to 1 to 3 if they neighboring voxel set to 2. Then mark with 4 those which neighbors 3 and so on until no voxel set to 1 is left. This will create something like SDR map (distance to surface).
find and count number of local maximums
for objects without holes there should be just one local max however with holes there would be more of them. In edge case few local max voxels could group to small voxel so count those as one.
Here small C++/OpenGL/VCL example:
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
#include "gl_simple.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
const int n=40; // voxel map resolution
int map[n][n][n]; // voxel map color
int max_pos[n][3]; // position of local max
int max_cnt=0; // number of local max
int max_dis=0; // number of distinct local max
int pal[32]= // 0xAABBGGRR
{
0x00808080,
0x00707070,
0x00606060,
0x00505050,
0x00404040,
0x00303030,
0x00202020,
0x00101010,
0x00800000,
0x00700000,
0x00600000,
0x00500000,
0x00400000,
0x00300000,
0x00200000,
0x00100000,
0x00008000,
0x00007000,
0x00006000,
0x00005000,
0x00004000,
0x00003000,
0x00002000,
0x00001000,
0x00000080,
0x00000070,
0x00000060,
0x00000050,
0x00000040,
0x00000030,
0x00000020,
0x00000010,
};
//---------------------------------------------------------------------------
void TForm1::draw()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_CULL_FACE);
glEnable(GL_DEPTH_TEST);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_COLOR_MATERIAL);
// center the view around map[][][]
float a;
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0,0.0,-80.0);
a=-0.4*float(n); glTranslatef(a,a,a);
a=16.0/float(n); glScalef(a,a,a);
// glRotatef( 15.0,1.0,0.0,0.0);
// render map[][][] as cubes (very slow old api version for simplicity)
int x,y,z,i,j;
for (x=0;x<n;x++)
for (y=0;y<n;y++)
for (z=0;z<n;z++)
if (map[x][y][z])
{
glPushMatrix();
glTranslatef(x+x,y+y,z+z);
glColor4ubv((BYTE*)&(pal[map[x][y][z]&31]));
glBegin(GL_QUADS);
for (i=0;i<3*24;i+=3)
{
glNormal3fv(vao_nor+i);
glVertex3fv(vao_pos+i);
}
glEnd();
glPopMatrix();
}
// local max
glDisable(GL_DEPTH_TEST);
glColor4f(0.9,0.2,0.1,1.0);
for (j=0;j<max_cnt;j++)
{
x=max_pos[j][0];
y=max_pos[j][1];
z=max_pos[j][2];
glPushMatrix();
glTranslatef(x+x,y+y,z+z);
glBegin(GL_QUADS);
for (i=0;i<3*24;i+=3)
{
glNormal3fv(vao_nor+i);
glVertex3fv(vao_pos+i);
}
glEnd();
glPopMatrix();
}
glFlush();
SwapBuffers(hdc);
}
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
{
gl_init(Handle);
// init map[][][]
int x,y,z,xx,yy,zz,c0,c1,c2,e;
int x0=n/2,y0=n/2,z0=n/2,rr0=(n/2)-3; rr0*=rr0; // ball
int x1=n/3,y1=n/2,rr1=(n/5); rr1*=rr1; // cylinder hole
for (x=0;x<n;x++)
for (y=0;y<n;y++)
for (z=0;z<n;z++)
{
// clear map
map[x][y][z]=0;
// ball
xx=x-x0; xx*=xx;
yy=y-y0; yy*=yy;
zz=z-z0; zz*=zz;
if (xx+yy+zz<=rr0) map[x][y][z]=1;
// hole
xx=x-x1; xx*=xx;
yy=y-y1; yy*=yy;
if (xx+yy<=rr1) map[x][y][z]=0;
}
// palette
// for (x=0;(x<n)&&(x<32);x++) map[x][n-1][n-1]=x;
// SDR growth fill
c0=0; // what to neighbor
c1=1; // what to fill
c2=2; // recolor to
for (e=1,c0=0,c1=1,c2=2;e;c0=c2,c2++)
for (e=0,x=1;x<n-1;x++)
for (y=1;y<n-1;y++)
for (z=1;z<n-1;z++)
if (map[x][y][z]==c1)
if ((map[x-1][y][z]==c0)
||(map[x+1][y][z]==c0)
||(map[x][y-1][z]==c0)
||(map[x][y+1][z]==c0)
||(map[x][y][z-1]==c0)
||(map[x][y][z+1]==c0)){ map[x][y][z]=c2; e=1; }
// find local max
max_cnt=0;
max_dis=0;
for (x=1;x<n-1;x++)
for (y=1;y<n-1;y++)
for (z=1;z<n-1;z++)
{
// is local max?
c0=map[x][y][z];
if (map[x-1][y][z]>=c0) continue;
if (map[x+1][y][z]>=c0) continue;
if (map[x][y-1][z]>=c0) continue;
if (map[x][y+1][z]>=c0) continue;
if (map[x][y][z-1]>=c0) continue;
if (map[x][y][z+1]>=c0) continue;
// is connected to another local max?
for (e=0;e<max_cnt;e++)
if (abs(max_pos[e][0]-x)+abs(max_pos[e][1]-y)+abs(max_pos[e][2]-z)==1)
{ e=-1; break; }
if (e>=0) max_dis++;
// add position to list
max_pos[max_cnt][0]=x;
max_pos[max_cnt][1]=y;
max_pos[max_cnt][2]=z;
max_cnt++;
}
Caption=AnsiString().sprintf("local max: %i / %i",max_dis,max_cnt);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
gl_exit();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormResize(TObject *Sender)
{
gl_resize(ClientWidth,ClientHeight);
draw();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
{
draw();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
draw();
}
//---------------------------------------------------------------------------
Just ignore the VCL and OpenGL stuff (they are not important) and focus on the stuff marked with // SDR growth fill
and // find local max
comments...
Here preview for ball without and with hole:

The local max are rendered without depth testing in orange color and their count (distinct / all) are printed in window Caption...