demo20 of Im2mesh package
demo20 - Create tetrahedral mesh based on 3d voxel image
Note
In Im2mesh package, the workflow of creating tetrahedral mesh is the same as the workflow in JM Hestroffer's XtalMesh ToolKit (see link below). The mesh generator of XtalMesh is fTetWild, which has very high meshing success rate due to its envelope-based meshing algorithm. Note that XtalMesh uses Python and cannot use 3d voxel image as input.
I spent several days to rewrite the XtalMesh's code to make it work for MATLAB and 3d voxel image. In this demo, we also use fTetWild as mesh generator.
Dependencies
To run demo20, we need to install fTetWild.
No need to install any MATLAB toolboxes.
Installation of fTetWild via Docker
2. Run Docker. If you have trouble in running Docker, you can ask Gemini for help. In my persional windows PC, I need to enable virtualization in BIOS to run Docker.
3. Open your command-line shell, such as Windows PowerShell.
4. Type the following code within command-line shell to install my Docker image of fTetWild (1.7 GB).
docker pull mjx0799/my_ftetwild_image
5. The installation may take a while (15-30 mins). After installation, your should be able to see the installed image in your Docker Desktop application.
Setup
Before we start, open Docker Desktop application. Let it run in the background.
Open MATLAB, please set folder "Im2mesh_Matlab" as your current folder of MATLAB.
Clear variables.
Set default image size.
x = 250; y = 250; width = 250; height = 250;
set(groot, 'DefaultFigurePosition', [x,y,width,height])
% set(groot, 'DefaultFigurePosition', 'factory')
Define a subfolder for saving mesh data.
% Define the name of subfolder
myMeshFolder = fullfile(pwd, folderName);
% Creates the folder if it doesn't exist
if ~exist(myMeshFolder, 'dir')
STEP 1. Import 3d voxel image
We can import 3d image using function importImSeqs or function importImStack. We have demostrated these two functions in the beginning of demo19.
imageFoloder = 'test_image_sequences';
im = importImSeqs( imageFoloder );
Use function plotVolFaces to plot faces.
We can use the function 'unique' to list all the grayscale intensity in the image.
We can check the totoal number of grayscale intensity in the image.
We identifies different phases in a image by their grayscales. Different grayscales correspond to different phases. If we have 4 level of grayscales in a image, the resulted meshes will contain 4 phases.
STEP 2. Convert to triangular surface mesh
We need to define the voxel spacing in the 3d voxel image. In the X-ray CT image of materials, the z spacing is usually different from the x and y spacing. We will set the actual phyiscal dimension of a voxel in STEP 10.
% Define the voxel spacing (default is 1x1x1)
voxelSpacing = [1.0, 1.0, 1.0];
We can convert 3d voxel image to triangular surface mesh.
% Extract triangular surface mesh
[nodes, triangles, phaseIDs] = extractPhaseMesh(im, voxelSpacing);
% Convert surface mesh to the output format of Dream.3D
[V, F, ntype, flabel] = convert2dream3d(nodes, triangles, phaseIDs);
STEP 3. Smooth surface mesh
We will use constraint Taubin smoothing to smooth the 'blocky' triangular surface mesh. You can change 'num_iters' to tune surface smoothness. We can obtain smoother surface with a higher 'num_iters'. The typical value of 'num_iters' is 20-60.
The smoothing process may take some time. Wait until "Smoothing Done!" show up.
disp('//////////////// Smoothing ////////////////');
//////////////// Smoothing ////////////////
minX = min(V(:,1)); maxX = max(V(:,1));
minY = min(V(:,2)); maxY = max(V(:,2));
minZ = min(V(:,3)); maxZ = max(V(:,3));
geom = [double(V(:,1) > minX & V(:,1) < maxX), ...
double(V(:,2) > minY & V(:,2) < maxY), ...
double(V(:,3) > minZ & V(:,3) < maxZ)]; % N x 3
% fprintf('Smooth Exterior Triple Lines\n');
V = graph_smooth_taubin(V, F, 'ext_triple', ntype, geom, lamda, mu, num_iters);
% fprintf('Smooth Interior Triple Lines\n');
V = graph_smooth_taubin(V, F, 'int_triple', ntype, geom, lamda, mu, num_iters);
% fprintf('Smooth Boundaries\n');
V = graph_smooth_taubin(V, F, 'bound', ntype, geom, lamda, mu, num_iters);
disp('//////////////// Smoothing Done! ////////////////');
//////////////// Smoothing Done! ////////////////
STEP 4. Select phases (optional)
The input 3d voxel image usually has multiple phases. Different grayscales in the image correspond to different phases.
We can use the following function the select the phases we desire. If we don't need to select phases, we can delete STEP 4 or set grayscale_we_like=[];
grayscale_we_like = [ 0, 134, 239 ];
[V, F, flabel] = selectPhase( V, F, flabel, grayscale_we_like );
STEP 5. Save surfaces as OFF file
Extract surfaces for each phase. We will use them later.
% Get surface mesh for each phase
phaseFaces = extractPhaseFaces(F, flabel);
We use function writeOFF to save surfaces as OFF file.
offFileName = 'surface.off';
offFilePath = fullfile(myMeshFolder, offFileName);
writeOFF(offFilePath, V, F);
//////////////// writeOFF Done! ////////////////
We can save these variables to mat file for backup.
save( 'surf.mat', 'V', 'F', 'phaseFaces' );
STEP 6. Visualize surface mesh
We use function plotSurface to visualize surface mesh. Function plotSurface is much faster when running via m file script than mlx file.
plotSurface( V, phaseFaces, color_code, opt );
If the input 3d image is large, function plotSurface could be very slow. I usually down-sample the surface to make the rendering faster. Down-sampling is achieved by setting 'opt.sampleRatio'.
'opt.sampleRatio = 1' means no down-sampling.
'opt.sampleRatio = 0.1' means roundly 10% down-sampling before plotting surface.
plotSurface( V, phaseFaces, color_code, opt );
STEP 7. Generate tetrahedral mesh via fTetWild
To generate mesh via fTetWild, we need to construct a command and send the command to fTetWild via Docker. There are two major parameters for fTetWild: Ideal edge length, Envelope of size epsilon. You can check Figure 5 in JM Hestroffer's paper about the effect of these two parameters. Using smaller ideal edge length gives a denser mesh but also takes longer time.
Using smaller envelope preserves features better but also takes longer time.
According to fTetWild, we can use the following command line to set these two parameters. For other command lines, see fTetWild's github.
-a,--la Ideal edge length not scaled by diag_of_bbox. Excludes: --lr. (double, optional)
-l,--lr ideal_edge_length = diag_of_bbox * L. Excludes: --la. (double, optional, default: 0.05)
-e,--epsr epsilon = diag_of_bbox * EPS. (double, optional, default: 1e-3)
Here is an example. The input is .off file. The output is .msh file (Gmsh format). You may ask Gemini to construct a Docker command.
idealEdgeLength = '5e-2'; % Ideal edge length
epsilon = '5e-3'; % Envelope of size epsilon
% Construct the Docker Command
'docker run --rm -v "', myMeshFolder, ':/data" -w /data mjx0799/my_ftetwild_image ' ...
'/fTetWild/build/FloatTetwild_bin ' ...
'--input ', offFileName, ' ' ...
'--output ', mshFileName, ' ' ...
'--manifold-surface ' ...
'-l ', idealEdgeLength, ' ' ...
We send the command to fTetWild via Docker. The mesh generation process could take a while (5-180 mins depending on input). Wait until "fTetWild Done!" show up. A .msh file (Gmsh) will be generated in the subfolder we created before.
disp('//////////////// fTetWild via Docker ////////////////');
//////////////// fTetWild via Docker ////////////////
% Execute with '-echo' to see the real-time console output
[status, cmdout] = system(dockerCmd, '-echo');
6 10 12 2 9 5 16 13 0 14 4 1 19 8 18 11 3 17 15 7
TBB threads 4
bbox_diag_length = 51.9615
ideal_edge_length = 2.59808
stage = 2
eps_input = 0.259808
eps = 0.143827
eps_simplification = 0.115061
eps_coplanar = 5.19615e-05
dd = 0.173205
dd_simplification = 0.138564
collapsing 0.934324
swapping 0.070626
#boundary_e1 = 0
#boundary_e2 = 1
#boundary_e1 = 0
#boundary_e2 = 1
known_surface_fs.size = 0
known_not_surface_fs.size = 0
initializing...
edge collapsing...
fixed 0 tangled element
success(env) = 6571
success = 7423(37202)
success(env) = 306
success = 337(17476)
success(env) = 17
success = 17(7574)
success(env) = 2
success = 2(796)
success(env) = 1
success = 1(170)
success(env) = 0
success = 0(65)
edge collapsing done!
time = 0.728217s
#v = 3850
#t = 21979
max_energy = 109.441
avg_energy = 6.15203
//////////////// pass 0 ////////////////
edge splitting...
fixed 0 tangled element
success = 11155(11155)
edge splitting done!
time = 0.06375s
#v = 15005
#t = 77963
max_energy = 109.441
avg_energy = 5.59359
edge collapsing...
fixed 0 tangled element
success(env) = 574
success = 7805(54335)
success(env) = 116
success = 605(26932)
success(env) = 17
success = 83(10635)
success(env) = 2
success = 20(1888)
success(env) = 0
success = 4(298)
success(env) = 0
success = 0(41)
edge collapsing done!
time = 0.645912s
#v = 6488
#t = 35290
max_energy = 109.441
avg_energy = 4.80773
edge swapping...
fixed 0 tangled element
success3 = 1572
success4 = 3141
success5 = 240
success = 4953(30106)
edge swapping done!
time = 0.260261s
#v = 6488
#t = 33958
max_energy = 35.7229
avg_energy = 4.31709
vertex smoothing...
success = 3404(5240)
vertex smoothing done!
time = 0.145738s
#v = 6488
#t = 33958
max_energy = 34.1758
avg_energy = 4.06044
//////////////// pass 1 ////////////////
edge splitting...
fixed 0 tangled element
success = 3420(3420)
edge splitting done!
time = 0.028654s
#v = 9908
#t = 48247
max_energy = 34.1758
avg_energy = 4.10964
edge collapsing...
fixed 0 tangled element
success(env) = 339
success = 3122(38289)
success(env) = 32
success = 171(19980)
success(env) = 2
success = 21(3091)
success(env) = 0
success = 5(310)
success(env) = 0
success = 4(67)
success(env) = 0
success = 0(18)
edge collapsing done!
time = 0.363486s
#v = 6585
#t = 34344
max_energy = 34.1758
avg_energy = 4.0196
edge swapping...
fixed 0 tangled element
success3 = 196
success4 = 1205
success5 = 88
success = 1489(22979)
edge swapping done!
time = 0.183411s
#v = 6585
#t = 34236
max_energy = 34.1758
avg_energy = 3.97134
vertex smoothing...
success = 3239(5241)
vertex smoothing done!
time = 0.134639s
#v = 6585
#t = 34236
max_energy = 33.9459
avg_energy = 3.88412
updating sclaing field ...
filter_energy = 8
is_hit_min_edge_length = 0
enlarge envelope, eps = 0.159808
//////////////// pass 2 ////////////////
edge splitting...
fixed 0 tangled element
success = 7502(7502)
edge splitting done!
time = 0.049927s
#v = 14087
#t = 70403
max_energy = 34.1077
avg_energy = 4.10882
edge collapsing...
fixed 0 tangled element
success(env) = 702
success = 5187(47903)
success(env) = 64
success = 363(24130)
success(env) = 13
success = 44(4916)
success(env) = 3
success = 9(589)
success(env) = 0
success = 1(162)
success(env) = 0
success = 0(13)
edge collapsing done!
time = 0.555589s
#v = 8483
#t = 44669
max_energy = 15.4887
avg_energy = 3.87486
edge swapping...
fixed 0 tangled element
success3 = 172
success4 = 1493
success5 = 70
success = 1735(31310)
edge swapping done!
time = 0.340697s
#v = 8483
#t = 44567
max_energy = 15.4887
avg_energy = 3.83211
vertex smoothing...
success = 5042(6997)
vertex smoothing done!
time = 0.216121s
#v = 8483
#t = 44567
max_energy = 11.505
avg_energy = 3.71342
//////////////// pass 3 ////////////////
edge splitting...
fixed 0 tangled element
success = 3415(3415)
edge splitting done!
time = 0.035064s
#v = 11898
#t = 59036
max_energy = 11.505
avg_energy = 3.82455
edge collapsing...
fixed 0 tangled element
success(env) = 429
success = 3323(37930)
success(env) = 27
success = 132(17873)
success(env) = 5
success = 15(1892)
success(env) = 3
success = 4(214)
success(env) = 0
success = 0(60)
edge collapsing done!
time = 0.357744s
#v = 8424
#t = 44298
max_energy = 11.505
avg_energy = 3.7199
edge swapping...
fixed 0 tangled element
success3 = 64
success4 = 821
success5 = 34
success = 919(29694)
edge swapping done!
time = 0.207983s
#v = 8424
#t = 44268
max_energy = 11.505
avg_energy = 3.70803
vertex smoothing...
success = 4743(6907)
vertex smoothing done!
time = 0.146176s
#v = 8424
#t = 44268
max_energy = 10.8067
avg_energy = 3.65507
//////////////// pass 4 ////////////////
edge splitting...
fixed 0 tangled element
success = 2847(2847)
edge splitting done!
time = 0.033261s
#v = 11271
#t = 55844
max_energy = 10.8067
avg_energy = 3.75852
edge collapsing...
fixed 0 tangled element
success(env) = 326
success = 2748(35067)
success(env) = 16
success = 86(14700)
success(env) = 0
success = 3(1162)
success(env) = 0
success = 0(52)
edge collapsing done!
time = 0.277362s
#v = 8434
#t = 44378
max_energy = 10.8067
avg_energy = 3.6658
edge swapping...
fixed 0 tangled element
success3 = 28
success4 = 580
success5 = 23
success = 631(29267)
edge swapping done!
time = 0.200207s
#v = 8434
#t = 44373
max_energy = 10.8067
avg_energy = 3.65901
vertex smoothing...
success = 4565(6918)
vertex smoothing done!
time = 0.148199s
#v = 8434
#t = 44373
max_energy = 10.3985
avg_energy = 3.62948
updating sclaing field ...
filter_energy = 8
is_hit_min_edge_length = 0
//////////////// pass 5 ////////////////
edge splitting...
fixed 0 tangled element
success = 1549(1549)
edge splitting done!
time = 0.031318s
#v = 9983
#t = 49656
max_energy = 11.036
avg_energy = 3.68188
edge collapsing...
fixed 0 tangled element
success(env) = 91
success = 1575(54516)
success(env) = 5
success = 29(12409)
success(env) = 2
success = 6(511)
success(env) = 0
success = 1(82)
success(env) = 0
success = 0(29)
edge collapsing done!
time = 0.308522s
#v = 8372
#t = 44061
max_energy = 7.97519
avg_energy = 3.63374
edge swapping...
fixed 0 tangled element
success3 = 23
success4 = 302
success5 = 7
success = 332(28598)
edge swapping done!
time = 0.171808s
#v = 8372
#t = 44045
max_energy = 7.97519
avg_energy = 3.63043
vertex smoothing...
success = 4293(6856)
vertex smoothing done!
time = 0.143682s
#v = 8372
#t = 44045
max_energy = 7.97519
avg_energy = 3.61685
//////////////// postprocessing ////////////////
edge collapsing...
fixed 0 tangled element
success(env) = 15
success = 65(52186)
success(env) = 0
success = 4(2338)
success(env) = 0
success = 0(71)
edge collapsing done!
time = 0.227345s
#v = 8303
#t = 43717
max_energy = 7.97519
avg_energy = 3.61599
1951
0
% Stop the timer and store elapsed time
% Check if it was successful
disp('//////////////// fTetWild Done! ////////////////');
fprintf('//////////////// Time = %.2fs ////////////////\n', executionTime);
disp('//////////////// Error occurred ////////////////');
end
//////////////// fTetWild Done! ////////////////
//////////////// Time = 14.52s ////////////////
STEP 8. Import .msh file & assign phase labels
We use function readMsh to import the .msh file into MATLAB.
mshFilePath = fullfile( myMeshFolder, mshFileName );
[vert, ele] = readMsh( mshFilePath );
Loaded 5416 nodes and 25756 elements.
We use function label_tetrahedrons to assign phase labels.
tnum = label_tetrahedrons(vert, ele, phaseFaces, V);
//////////////// Phase labeling ////////////////
Phase 1 / 3 ...
Phase 2 / 3 ...
Phase 3 / 3 ...
//////////////// Labeling complete! ////////////////
% Remove background tetrahedrons
We got 3 variables: vert, ele, tnum
vert: Mesh nodes. It’s a Nn-by-3 matrix, where Nn is the number of nodes in the mesh. Each row of vert contains the x, y, z coordinates for that mesh node.
ele: Mesh elements. Each row in tria contains the indices of the nodes for that mesh element.
tnum: Label of phase. Ne-by-1 array, where Ne is the number of elements.
tnum(j,1) = k; means the j-th element belongs to the k-th phase.
We use function tetcost to check mesh quality. The computation method in function tetcost is the same as MATLAB built-in function meshQuality. tetcost(vert, ele);
--- Tetrahedral Mesh Quality Summary ---
Total Elements: 25755
Mean Quality (Q): 0.8302
Min Quality (Q): 0.2831
Max Quality (Q): 0.9988
Poor Elements (<0.1): 0
----------------------------------------
Plot mesh quality.
histogram(Q, 'BinLimits', [0,1], 'EdgeColor', 'none')
xlabel( "Element Shape Quality", "fontweight", "b" )
ylabel( "Number of Elements", "fontweight", "b" )
save('tet.mat', 'vert', 'ele', 'tnum');
STEP 9. Visualize tetrahedral mesh
We use function plotMesh3d to plot mesh.
plotMesh3d(vert, ele, tnum);
Other settings.
plotMesh3d( vert, ele, tnum, color_code, opt );
Define cutting plane to check interior. You can ask Gemini to define more setting.
opt.cutPlaneYZ = 15; % plot elements where x <= 15
plotMesh3d(vert, ele, tnum, 1, opt);
We can plot the mesh for one of the phases. For example, the 2nd phase.
ele_temp = ele( tnum == index, : );
[ vert_temp, ele_temp ] = delRedundantVertex( vert, ele_temp );
plotMesh3d( vert_temp, ele_temp, [], color_code, opt );
STEP 10. Export mesh as inp or bdf file
Set up scale.
% physical dimensions of a voxel in the image
Scale node coordinates according to dx.
% scale node coordinates of linear elements
Export mesh as bdf file (Nastran bulk data)
file_name = 'test_3d.bdf';
printBdf3d( vert, ele, tnum, [], precision, file_name );
printBdf3d Done! Check the bdf file!
Export mesh as inp file (Abaqus)
Linear element
file_name = 'test_linear.inp';
opt.tf_printMaxMinNode = 0;
opt.tf_printInterfNode = 0;
printInp3d( vert, ele, tnum, ele_type, precision, file_name, opt );
printInp3d Done! Check the inp file!
Quadratic element
We use function insertNode to convert the linear elements to quadratic elements.
% vert2, ele2 define quadratic elements
[vert2, ele2] = insertNode3d(vert, ele);
Export
file_name = 'test_quadratic.inp';
opt.tf_printMaxMinNode = 0;
opt.tf_printInterfNode = 0;
printInp3d( vert2, ele2, tnum, ele_type, precision, file_name, opt );
printInp3d Done! Check the inp file!
set(groot, 'DefaultFigurePosition', 'factory')