function [no,el,regions,holes]=vol2surf(img,ix,iy,iz,opt,dofix,method,isovalues)
%
% [no,el,regions,holes]=vol2surf(img,ix,iy,iz,opt,dofix,method,isovalues)
%
% converting a 3D volumetric image to surfaces
%
% author: Qianqian Fang (q.fang at neu.edu)
%
% input:
% img: a volumetric binary image; if img is empty, vol2surf will
% return user defined surfaces via opt.surf if it exists
% ix,iy,iz: subvolume selection indices in x,y,z directions
% opt: function parameters
% if method is 'cgalsurf' or 'cgalpoly':
% opt=a float number>1: max radius of the Delaunay sphere(element size)
% opt.radbound: same as above, max radius of the Delaunay sphere
% opt.distbound: maximum deviation from the specified isosurfaces
% opt(1,2,...).radbound: same as above, for each levelset
% if method is 'simplify':
% opt=a float number<1: compression rate for surf. simplification
% opt.keepratio=a float less than 1: same as above, same for all surf.
% opt(1,2,..).keepratio: setting compression rate for each levelset
% opt(1,2,..).maxsurf: 1 - only use the largest disjointed surface
% 0 - use all surfaces for that levelset
% opt(1,2,..).side: - 'upper': threshold at upper interface
% 'lower': threshold at lower interface
% opt(1,2,..).maxnode: - the maximum number of surface node per levelset
% opt(1,2,..).holes: user specified holes interior pt list
% opt(1,2,..).regions: user specified regions interior pt list
% opt(1,2,..).surf.{node,elem}: add additional surfaces
% opt(1,2,..).{A,B}: linear transformation for each surface
% opt.autoregion: if set to 1, vol2surf will try to determine
% the interior points for each closed surface automatically
% dofix: 1: perform mesh validation&repair, 0: skip repairing
% method: - if method is 'simplify', iso2mesh will first call
% binsurface to generate a voxel-based surface mesh and then
% use meshresample/meshcheckrepair to create a coarser mesh;
% - if method is 'cgalsurf', iso2mesh will call the surface
% extraction program from CGAL to make surface mesh
% - if method is not specified, 'cgalsurf' is assumed by default
% isovalues: a list of isovalues where the levelset is defined
%
% output:
% no: list of nodes on the resulting suface mesh, 3 columns for x,y,z
% el: list of trianglular elements on the surface, [n1,n2,n3,region_id]
% regions: list of interior points for all sub-region, [x,y,z]
% holes: list of interior points for all holes, [x,y,z]
%
% -- this function is part of iso2mesh toolbox (http://iso2mesh.sf.net)
%
fprintf(1,'extracting surfaces from a volume ...\n');
el=[];
no=[];
if(isstruct(opt) && isfield(opt,'holes'))
holes=opt.holes;
else
holes=[];
end
if(isstruct(opt) && isfield(opt,'regions'))
regions=opt.regions;
else
regions=[];
end
maxlevel=0;
if(~isempty(img))
img=img(ix,iy,iz);
dim=size(img);
newdim=dim+[2 2 2];
newimg=zeros(newdim);
newimg(2:end-1,2:end-1,2:end-1)=img;
if(nargin<8)
maxlevel=max(newimg(:));
isovalues=1:maxlevel;
else
isovalues=unique(sort(isovalues));
maxlevel=length(isovalues);
end
for i=1:maxlevel
if(i<maxlevel)
levelmask=int8(newimg>=isovalues(i) & newimg<isovalues(i+1));
else
levelmask=int8(newimg>=isovalues(i));
end
[levelno,levelel]=binsurface(levelmask);
if(~isempty(levelel))
if(isstruct(opt) && isfield(opt,'autoregion'))
if(opt.autoregion)
seeds=surfseeds(levelno,levelel);
else
seeds=surfinterior(levelno,levelel);
end
else
seeds=surfinterior(levelno,levelel);
end
if(~isempty(seeds))
disp([sprintf('region %d centroid :',i) sprintf('\t%f %f %f\n', seeds')]);
if(~isempty(regions))
regions(end+1:end+size(seeds,1),:)=seeds;
else
regions=seeds;
end
end
end
end
for i=1:maxlevel
fprintf(1,'processing threshold level %d...\n',i);
if(nargin>=7 && strcmp(method,'simplify'))
[v0,f0]=binsurface(newimg>=isovalues(i)); % not sure if binsurface works for multi-value arrays
% with binsurface, I think the following line is not needed anymore
% v0(:,[1 2])=v0(:,[2 1]); % isosurface(V,th) assumes x/y transposed
if(dofix) [v0,f0]=meshcheckrepair(v0,f0); end
if(isstruct(opt) && length(opt)==maxlevel) keepratio=opt(i).keepratio;
elseif (isstruct(opt) && length(opt)==1) keepratio=opt.keepratio;
else keepratio=opt; end;
% first, resample the surface mesh with cgal
fprintf(1,'resampling surface mesh for level %d...\n',i);
[v0,f0]=meshresample(v0,f0,keepratio);
% iso2mesh is not stable for meshing small islands,remove them (max 3x3x3 voxels)
f0=removeisolatedsurf(v0,f0,3);
if(dofix) [v0,f0]=meshcheckrepair(v0,f0); end
elseif(nargin<7 || strcmp(method,'cgalsurf') || strcmp(method,'cgalpoly'))
if(isstruct(opt) && length(opt)==maxlevel) radbound=opt(i).radbound;
elseif (isstruct(opt) && length(opt)==1) radbound=opt.radbound;
else radbound=opt; end;
maxsurfnode=40000; % maximum node numbers for each level
if(isstruct(opt) && length(opt)==maxlevel)
if(isfield(opt(i),'maxnode')) maxsurfnode=opt(i).maxnode; end
elseif (isstruct(opt) && length(opt)==1 )
if(isfield(opt(1),'maxnode'))
maxsurfnode=opt.maxnode;
end
end
distbound=radbound;
if(isstruct(opt) && length(opt)==maxlevel)
if(isfield(opt(i),'distbound')) distbound=opt(i).distbound; end
elseif (isstruct(opt) && length(opt)==1 )
if(isfield(opt(1),'distbound')) distbound=opt.distbound; end
end
surfside='';
if(isstruct(opt) && length(opt)==maxlevel)
if(isfield(opt(i),'side')) surfside=opt(i).side; end
elseif (isstruct(opt) && length(opt)==1)
if(isfield(opt(1),'side')) surfside=opt(1).side; end
end
if(~isempty(surfside))
newimg0=newimg;
end
if(strcmp(surfside,'upper'))
newimg(find(newimg<=isovalues(i)-1e-9))=isovalues(i)-1e-9;
elseif(strcmp(surfside,'lower'))
newimg(find(newimg>=isovalues(i)+1e-9))=isovalues(i)+1e-9;
end
perturb=1e-4*abs(max(isovalues));
if(all(newimg>isovalues(i)-perturb)) perturb=-perturb; end
[v0,f0]=vol2restrictedtri(newimg,isovalues(i)-perturb,regions(i,:),...
sum(newdim.*newdim)*2,30,radbound,distbound,maxsurfnode);
if(~isempty(surfside))
newimg=newimg0;
clear newimg0;
end
else
error('method can only be one of "cgalsurf", "cgalpoly" or "simplify".');
end
% if use defines maxsurf=1, take only the largest closed surface
if(isstruct(opt))
if( (isfield(opt,'maxsurf') && length(opt)==1 && opt.maxsurf==1) || ...
(length(opt)==maxlevel && isfield(opt(i),'maxsurf') && opt(i).maxsurf==1))
f0=maxsurf(finddisconnsurf(f0));
end
end
% if a transformation matrix/offset vector supplied, apply them
if(isstruct(opt) && length(opt)==maxlevel)
if(isfield(opt(i),'A') && isfield(opt(i),'B'))
v0=(opt(i).A*v0'+repmat(opt(i).B(:),1,size(v0,1)))';
end
elseif (isstruct(opt) && length(opt)==1)
if(isfield(opt,'A') && isfield(opt,'B'))
v0=(opt.A*v0'+repmat(opt.B(:),1,size(v0,1)))';
end
end
% if user specified holelist and regionlist, append them
if(isstruct(opt) && length(opt)==maxlevel)
if(isfield(opt(i),'hole'))
holes=[holes;opt(i).hole]
end
if(isfield(opt(i),'region'))
regions=[regions;opt(i).region]
end
end
if(i==0)
el=[f0 (i+1)*ones(size(f0,1),1)];
no=v0;
else
el=[el;f0+length(no) (i+1)*ones(size(f0,1),1)];
no=[no;v0];
end
end
%some final fix and scaling
no(:,1:3)=no(:,1:3)-1; % because we padded the image with a 1 voxel thick null layer in newimg
no(:,1)=no(:,1)*(max(ix)-min(ix)+1)/dim(1)+(min(ix)-1);
no(:,2)=no(:,2)*(max(iy)-min(iy)+1)/dim(2)+(min(iy)-1);
no(:,3)=no(:,3)*(max(iz)-min(iz)+1)/dim(3)+(min(iz)-1);
end % if not isempty(img)
if(isstruct(opt) && isfield(opt,'surf'))
for i=1:length(opt.surf)
opt.surf(i).elem(:,4)=maxlevel+i;
el=[el;opt.surf(i).elem+length(no)];
no=[no;opt.surf(i).node];
end
end
fprintf(1,'surface mesh generation is complete\n');