Skip to content

Commit 0f3deff

Browse files
committed
Add function and CLI flag to check mesh for holes
1 parent 86be974 commit 0f3deff

3 files changed

Lines changed: 115 additions & 12 deletions

File tree

splashsurf/src/reconstruction.rs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use anyhow::{anyhow, Context};
33
use arguments::{
44
ReconstructionRunnerArgs, ReconstructionRunnerPathCollection, ReconstructionRunnerPaths,
55
};
6-
use log::info;
6+
use log::{error, info};
77
use rayon::prelude::*;
88
use splashsurf_lib::coarse_prof::profile;
99
use splashsurf_lib::mesh::PointCloud3d;
@@ -95,6 +95,9 @@ pub struct ReconstructSubcommandArgs {
9595
/// Set the number of threads for the worker thread pool
9696
#[structopt(long, short = "-n")]
9797
num_threads: Option<usize>,
98+
/// Whether to check the final mesh for problems such as holes (note that when stitching is disabled this will lead to a lot of reported problems)
99+
#[structopt(long, default_value = "off", possible_values = &["on", "off"], case_insensitive = true)]
100+
check_mesh: Switch,
98101
}
99102

100103
arg_enum! {
@@ -124,7 +127,7 @@ pub fn reconstruct_subcommand(cmd_args: &ReconstructSubcommandArgs) -> Result<()
124127

125128
let result = if cmd_args.parallelize_over_files.into_bool() {
126129
paths.par_iter().try_for_each(|path| {
127-
reconstruction_entry_point(path, &args)
130+
reconstruction_pipeline(path, &args)
128131
.with_context(|| {
129132
format!(
130133
"Error while processing input file '{}' from a file sequence",
@@ -140,7 +143,7 @@ pub fn reconstruct_subcommand(cmd_args: &ReconstructSubcommandArgs) -> Result<()
140143
} else {
141144
paths
142145
.iter()
143-
.try_for_each(|path| reconstruction_entry_point(path, &args))
146+
.try_for_each(|path| reconstruction_pipeline(path, &args))
144147
};
145148

146149
if result.is_ok() {
@@ -166,6 +169,7 @@ mod arguments {
166169
pub struct ReconstructionRunnerArgs {
167170
pub params: splashsurf_lib::Parameters<f64>,
168171
pub use_double_precision: bool,
172+
pub check_mesh: bool,
169173
pub io_params: io::FormatParameters,
170174
}
171175

@@ -239,6 +243,7 @@ mod arguments {
239243
Ok(ReconstructionRunnerArgs {
240244
params,
241245
use_double_precision: args.use_double_precision.into_bool(),
246+
check_mesh: args.check_mesh.into_bool(),
242247
io_params: io::FormatParameters::default(),
243248
})
244249
}
@@ -473,32 +478,39 @@ mod arguments {
473478
}
474479

475480
/// Calls the reconstruction pipeline for single or double precision depending on the runtime parameters
476-
pub(crate) fn reconstruction_entry_point(
481+
pub(crate) fn reconstruction_pipeline(
477482
paths: &ReconstructionRunnerPaths,
478483
args: &ReconstructionRunnerArgs,
479484
) -> Result<(), anyhow::Error> {
480485
if args.use_double_precision {
481486
info!("Using double precision (f64) for surface reconstruction.");
482-
reconstruction_entry_point_generic::<i64, f64>(paths, &args.params, &args.io_params)?;
487+
reconstruction_pipeline_generic::<i64, f64>(
488+
paths,
489+
&args.params,
490+
&args.io_params,
491+
args.check_mesh,
492+
)?;
483493
} else {
484494
info!("Using single precision (f32) for surface reconstruction.");
485-
reconstruction_entry_point_generic::<i64, f32>(
495+
reconstruction_pipeline_generic::<i64, f32>(
486496
paths,
487497
&args.params.try_convert().ok_or(anyhow!(
488498
"Unable to convert surface reconstruction parameters from f64 to f32."
489499
))?,
490500
&args.io_params,
501+
args.check_mesh,
491502
)?;
492503
}
493504

494505
Ok(())
495506
}
496507

497508
/// Wrapper for the reconstruction pipeline: loads input file, runs reconstructions, stores output files
498-
pub(crate) fn reconstruction_entry_point_generic<I: Index, R: Real>(
509+
pub(crate) fn reconstruction_pipeline_generic<I: Index, R: Real>(
499510
paths: &ReconstructionRunnerPaths,
500511
params: &splashsurf_lib::Parameters<R>,
501512
io_params: &io::FormatParameters,
513+
check_mesh: bool,
502514
) -> Result<(), anyhow::Error> {
503515
profile!("surface reconstruction cli");
504516

@@ -518,6 +530,14 @@ pub(crate) fn reconstruction_entry_point_generic<I: Index, R: Real>(
518530
let grid = reconstruction.grid();
519531
let mesh = reconstruction.mesh();
520532

533+
if check_mesh {
534+
if let Err(err) = splashsurf_lib::marching_cubes::check_mesh_consistency(grid, mesh) {
535+
error!("{}", err);
536+
} else {
537+
info!("Checked mesh for problems (holes, etc.), no problems were found.");
538+
}
539+
}
540+
521541
// Store the surface mesh
522542
{
523543
profile!("write surface mesh to file");

splashsurf_lib/src/marching_cubes.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::marching_cubes::triangulation::{
1010
use crate::mesh::TriMesh3d;
1111
use crate::uniform_grid::{DummySubdomain, OwningSubdomainGrid, Subdomain};
1212
use crate::{new_map, DensityMap, Index, MapType, Real, UniformGrid};
13+
use nalgebra::Vector3;
1314

1415
pub mod marching_cubes_lut;
1516
mod narrow_band_extraction;
@@ -180,6 +181,87 @@ pub(crate) fn triangulate_density_map_to_surface_patch<I: Index, R: Real>(
180181
}
181182
}
182183

184+
/// Checks the consistency of the mesh (currently only checks for holes) and returns a string with debug information in case of problems
185+
pub fn check_mesh_consistency<I: Index, R: Real>(
186+
grid: &UniformGrid<I, R>,
187+
mesh: &TriMesh3d<R>,
188+
) -> Result<(), String> {
189+
let boundary_edges = mesh.find_boundary_edges();
190+
191+
if boundary_edges.is_empty() {
192+
return Ok(());
193+
}
194+
195+
let mut error_string = String::new();
196+
error_string += &format!("Mesh is not closed. It has {} boundary edges (edges that are connected to only one triangle):", boundary_edges.len());
197+
for (edge, tri_idx, _) in boundary_edges {
198+
let v0 = mesh.vertices[edge[0]];
199+
let v1 = mesh.vertices[edge[1]];
200+
let center = (v0 + v1) / (R::one() + R::one());
201+
let cell = grid.enclosing_cell(&center);
202+
if let Some(cell_index) = grid.get_cell(cell) {
203+
let point_index = grid
204+
.get_point(*cell_index.index())
205+
.expect("Unable to get point index of cell");
206+
let cell_center = grid.point_coordinates(&point_index)
207+
+ &Vector3::repeat(grid.cell_size().times_f64(0.5));
208+
209+
error_string += &format!("\n\tTriangle {}, boundary edge {:?} is located in cell with {:?} with center coordinates {:?} and edge length {}.", tri_idx, edge, cell_index, cell_center, grid.cell_size());
210+
} else {
211+
error_string += &format!(
212+
"\n\tCannot get cell index for boundary edge {:?} of triangle {}",
213+
edge, tri_idx
214+
);
215+
}
216+
}
217+
218+
Err(error_string)
219+
}
220+
221+
/// Same as [`check_mesh_consistency`] but also adds debug information taken from the marching cubes input
222+
#[allow(unused)]
223+
fn check_mesh_with_cell_data<I: Index, R: Real>(
224+
grid: &UniformGrid<I, R>,
225+
marching_cubes_data: &MarchingCubesInput<I>,
226+
mesh: &TriMesh3d<R>,
227+
) -> Result<(), String> {
228+
let boundary_edges = mesh.find_boundary_edges();
229+
230+
if boundary_edges.is_empty() {
231+
return Ok(());
232+
}
233+
234+
let mut error_string = String::new();
235+
error_string += &format!("Mesh is not closed. It has {} boundary edges (edges that are connected to only one triangle):", boundary_edges.len());
236+
for (edge, tri_idx, _) in boundary_edges {
237+
let v0 = mesh.vertices[edge[0]];
238+
let v1 = mesh.vertices[edge[1]];
239+
let center = (v0 + v1) / (R::one() + R::one());
240+
let cell = grid.enclosing_cell(&center);
241+
if let Some(cell_index) = grid.get_cell(cell) {
242+
let point_index = grid
243+
.get_point(*cell_index.index())
244+
.expect("Unable to get point index of cell");
245+
let cell_center = grid.point_coordinates(&point_index)
246+
+ &Vector3::repeat(grid.cell_size().times_f64(0.5));
247+
248+
let cell_data = marching_cubes_data
249+
.cell_data
250+
.get(&grid.flatten_cell_index(&cell_index))
251+
.expect("Unabel to get cell data of cell");
252+
253+
error_string += &format!("\n\tTriangle {}, boundary edge {:?} is located in cell with {:?} with center coordinates {:?} and edge length {}. {:?}", tri_idx, edge, cell_index, cell_center, grid.cell_size(), cell_data);
254+
} else {
255+
error_string += &format!(
256+
"\n\tCannot get cell index for boundary edge {:?} of triangle {}",
257+
edge, tri_idx
258+
);
259+
}
260+
}
261+
262+
Err(error_string)
263+
}
264+
183265
#[allow(unused)]
184266
#[inline(never)]
185267
fn assert_cell_data_point_data_consistency<I: Index, R: Real>(

splashsurf_lib/tests/integration_tests/test_full.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use nalgebra::Vector3;
2+
use splashsurf_lib::marching_cubes::check_mesh_consistency;
23
use splashsurf_lib::{
34
reconstruct_surface, Parameters, Real, SpatialDecompositionParameters, SubdivisionCriterion,
45
};
@@ -107,11 +108,11 @@ macro_rules! generate_test {
107108

108109
if test_for_boundary(&parameters) {
109110
// Ensure that the mesh does not have a boundary
110-
assert_eq!(
111-
reconstruction.mesh().find_boundary_edges(),
112-
vec![],
113-
"Mesh boundary is not empty"
114-
);
111+
if let Err(e) = check_mesh_consistency(reconstruction.grid(), reconstruction.mesh())
112+
{
113+
eprintln!("{}", e);
114+
panic!("Mesh contains boundary edges");
115+
}
115116
}
116117
}
117118
};

0 commit comments

Comments
 (0)