Add topology verifier

This commit is contained in:
Julian Stahl 2024-12-19 19:01:06 +01:00
parent a534ef374a
commit f0e2bb33c9
4 changed files with 306 additions and 50 deletions

View File

@ -12,6 +12,7 @@ include(CGAL_TBB_support)
create_single_source_cgal_program("test_marching_cubes.cpp")
create_single_source_cgal_program("test_topology.cpp")
create_single_source_cgal_program("verifier.cpp")
create_single_source_cgal_program("test_tmc_csg.cpp")
if(TARGET CGAL::Eigen3_support)
@ -45,4 +46,5 @@ endif()
if(TARGET CGAL::TBB_support)
target_link_libraries(test_marching_cubes PRIVATE CGAL::TBB_support)
target_link_libraries(test_topology PRIVATE CGAL::TBB_support)
target_link_libraries(verifier PRIVATE CGAL::TBB_support)
endif()

View File

@ -27,33 +27,6 @@
namespace IS = CGAL::Isosurfacing;
template <typename PolygonMesh>
std::size_t connected_components(PolygonMesh& mesh)
{
using face_descriptor = typename boost::graph_traits<PolygonMesh>::face_descriptor;
auto fccmap = mesh.template add_property_map<face_descriptor, std::size_t>("f:CC").first;
return CGAL::Polygon_mesh_processing::connected_components(mesh, fccmap);
}
template <typename PolygonMesh>
std::size_t euler_characteristic(const PolygonMesh& mesh)
{
return mesh.number_of_vertices() - mesh.number_of_edges() + mesh.number_of_faces();
}
template <typename PolygonMesh>
std::size_t boundary_components(const PolygonMesh& mesh)
{
using halfedge_descriptor = typename boost::graph_traits<PolygonMesh>::halfedge_descriptor;
std::vector<halfedge_descriptor> border_cycles;
CGAL::Polygon_mesh_processing::extract_boundary_cycles(mesh, std::back_inserter(border_cycles));
return border_cycles.size();
}
template <typename Grid>
bool check_closed_not_empty(const Grid& grid, const IS::Interpolated_discrete_values_3<Grid>& values, const typename Grid::Geom_traits::FT iso = 0)
{
@ -441,29 +414,6 @@ FT generate_predefined_plane(const PlaneCase topology_case, std::array<FT, 8>& v
return iso;
}
template<typename Domain>
void write_debug_grid(const Domain& domain, const std::string& filename) {
using Point = typename Domain::Geom_traits::Point_3;
using Mesh = CGAL::Surface_mesh<Point>;
Mesh debug_grid;
auto debug_grid_creator = [&](const typename Domain::cell_descriptor& c)
{
std::vector<typename Mesh::Vertex_index> cell_vertices;
for (const auto& v : domain.cell_vertices(c)) {
cell_vertices.push_back(debug_grid.add_vertex(domain.point(v)));
}
debug_grid.add_face(cell_vertices[6], cell_vertices[2], cell_vertices[0], cell_vertices[4]);
debug_grid.add_face(cell_vertices[1], cell_vertices[3], cell_vertices[7], cell_vertices[5]);
debug_grid.add_face(cell_vertices[0], cell_vertices[1], cell_vertices[5], cell_vertices[4]);
debug_grid.add_face(cell_vertices[6], cell_vertices[7], cell_vertices[3], cell_vertices[2]);
debug_grid.add_face(cell_vertices[2], cell_vertices[3], cell_vertices[1], cell_vertices[0]);
debug_grid.add_face(cell_vertices[4], cell_vertices[5], cell_vertices[7], cell_vertices[6]);
};
domain.template for_each_cell<CGAL::Sequential_tag>(debug_grid_creator);
CGAL::IO::write_OFF(filename, debug_grid);
}
template<typename KERNEL>
void compare_tmc_mc_trilinear(const std::array<typename KERNEL::FT, 8>& case_values, typename KERNEL::FT iso)
{

View File

@ -7,6 +7,7 @@
#include <CGAL/Polygon_mesh_processing/repair.h>
#include <CGAL/Polygon_mesh_processing/repair_degeneracies.h>
#include <CGAL/Polygon_mesh_processing/repair_polygon_soup.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Named_function_parameters.h>
#include <CGAL/boost/graph/named_params_helper.h>
@ -59,6 +60,48 @@ bool has_degenerate_faces(PolygonMesh& pmesh)
return !dfs.empty();
}
template <typename PolygonMesh>
int connected_components(PolygonMesh& mesh)
{
using face_descriptor = typename boost::graph_traits<PolygonMesh>::face_descriptor;
auto fccmap = mesh.template add_property_map<face_descriptor, std::size_t>("f:CC").first;
return CGAL::Polygon_mesh_processing::connected_components(mesh, fccmap);
}
template <typename PolygonMesh>
int euler_characteristic(const PolygonMesh& mesh)
{
return mesh.number_of_vertices() - mesh.number_of_edges() + mesh.number_of_faces();
}
template <typename PolygonMesh>
int boundary_components(const PolygonMesh& mesh)
{
using halfedge_descriptor = typename boost::graph_traits<PolygonMesh>::halfedge_descriptor;
std::vector<halfedge_descriptor> border_cycles;
CGAL::Polygon_mesh_processing::extract_boundary_cycles(mesh, std::back_inserter(border_cycles));
return border_cycles.size();
}
template <typename PolygonMesh>
int betti_0(PolygonMesh& mesh) {
return connected_components(mesh);
}
template <typename PolygonMesh>
int betti_2(PolygonMesh& mesh) {
return connected_components(mesh); // only for closed surfaces
}
template <typename PolygonMesh>
int betti_1(PolygonMesh& mesh) {
return betti_0(mesh) + betti_2(mesh) - euler_characteristic(mesh);
}
// template <typename PolygonMesh>
// bool check_mesh_distance(const PolygonMesh& m0, const PolygonMesh& m1)
// {
@ -68,4 +111,27 @@ bool has_degenerate_faces(PolygonMesh& pmesh)
// return true;
// }
template<typename Domain>
void write_debug_grid(const Domain& domain, const std::string& filename) {
using Point = typename Domain::Geom_traits::Point_3;
using Mesh = CGAL::Surface_mesh<Point>;
Mesh debug_grid;
auto debug_grid_creator = [&](const typename Domain::cell_descriptor& c)
{
std::vector<typename Mesh::Vertex_index> cell_vertices;
for (const auto& v : domain.cell_vertices(c)) {
cell_vertices.push_back(debug_grid.add_vertex(domain.point(v)));
}
debug_grid.add_face(cell_vertices[6], cell_vertices[2], cell_vertices[0], cell_vertices[4]);
debug_grid.add_face(cell_vertices[1], cell_vertices[3], cell_vertices[7], cell_vertices[5]);
debug_grid.add_face(cell_vertices[0], cell_vertices[1], cell_vertices[5], cell_vertices[4]);
debug_grid.add_face(cell_vertices[6], cell_vertices[7], cell_vertices[3], cell_vertices[2]);
debug_grid.add_face(cell_vertices[2], cell_vertices[3], cell_vertices[1], cell_vertices[0]);
debug_grid.add_face(cell_vertices[4], cell_vertices[5], cell_vertices[7], cell_vertices[6]);
};
domain.template for_each_cell<CGAL::Sequential_tag>(debug_grid_creator);
CGAL::IO::write_OFF(filename, debug_grid);
}
#endif // CGAL_ISOSURFACING_TEST_UTIL_H

View File

@ -0,0 +1,238 @@
#include "test_util.h"
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Isosurfacing_3/Cartesian_grid_3.h>
#include <CGAL/Isosurfacing_3/marching_cubes_3.h>
#include <CGAL/Isosurfacing_3/Marching_cubes_domain_3.h>
#include <CGAL/Isosurfacing_3/Interpolated_discrete_values_3.h>
#include <CGAL/Isosurfacing_3/Value_function_3.h>
#include <CGAL/IO/polygon_soup_io.h>
/*
Verifier for topological correctness
Based on: Topology Verification for Isosurface Extraction, Tiago Etiene
Data sets: [Marching Cubes cases] [Randomly generated grids] from http://liscustodio.github.io/C_MC33/
*/
namespace IS = CGAL::Isosurfacing;
template <typename Grid, typename Values>
void read_iso_volume(const std::string& filename, Grid& grid, Values& values) {
using Geom_traits = typename Grid::Geom_traits;
using FT = typename Geom_traits::FT;
using Iso_cuboid_3 = typename Geom_traits::Iso_cuboid_3;
typename Geom_traits::Construct_point_3 point = grid.geom_traits().construct_point_3_object();
typename Geom_traits::Construct_iso_cuboid_3 iso_cuboid = grid.geom_traits().construct_iso_cuboid_3_object();
std::ifstream file(filename, std::ios::binary);
if (!file.is_open()) {
throw std::runtime_error("Cannot open file: " + filename);
}
// Read the three dimensions
int nx, ny, nz;
file.read(reinterpret_cast<char*>(&nx), sizeof(int));
file.read(reinterpret_cast<char*>(&ny), sizeof(int));
file.read(reinterpret_cast<char*>(&nz), sizeof(int));
// Read the bounding box
// (xmin, xmax, ymin, ymax, zmin, zmax)
std::array<float, 6> bbox;
for (int i = 0; i < 6; ++i) {
file.read(reinterpret_cast<char*>(&bbox[i]), sizeof(float));
}
Iso_cuboid_3 span = iso_cuboid(point(bbox[0], bbox[2], bbox[4]),
point(bbox[1], bbox[3], bbox[5]));
grid = Grid { span, CGAL::make_array<std::size_t>(nx, ny, nz) };
// Calculate total number of data points
const std::size_t total_points = nx * ny * nz;
// Read the volume data (32-bit floats)
std::vector<float> volume_data(total_points);
for (std::size_t i = 0; i < total_points; ++i) {
file.read(reinterpret_cast<char*>(&volume_data[i]), sizeof(float));
std::cout << volume_data[i] << std::endl;
}
for(int x=0; x<nx; ++x)
for(int y=0; y<ny; ++y)
for(int z=0; z<nz; ++z)
values(x, y, z) = volume_data[x + y * nx + z * nx * ny];
file.close();
}
int read_euler(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("Cannot open file: " + filename);
}
int euler;
file >> euler;
return euler;
}
std::array<int, 2> read_betti(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("Cannot open file: " + filename);
}
int b0, b1;
file >> b0;
file >> b1;
return {b0, b1};
}
template <typename K>
void verify_euler() {
using FT = typename K::FT;
using Point = typename K::Point_3;
using Vector = typename K::Vector_3;
using Mesh = CGAL::Surface_mesh<Point>;
using Grid = IS::Cartesian_grid_3<K>;
using Values = IS::Interpolated_discrete_values_3<Grid>;
using Domain = IS::Marching_cubes_domain_3<Grid, Values>;
using Point_range = std::vector<Point>;
using Polygon_range = std::vector<std::vector<std::size_t> >;
using Mesh = CGAL::Surface_mesh<Point>;
const std::size_t num_tests = 10000;
for (std::size_t i = 0; i < num_tests; i++) {
Grid grid;
Values values { grid };
Domain domain { grid, values };
read_iso_volume("data/MarchingCubes_cases/Grids/" + std::to_string(i) + "-scalar_field.iso", grid, values);
Point_range points;
Polygon_range triangles;
IS::marching_cubes<CGAL::Sequential_tag>(domain, 0, points, triangles, CGAL::parameters::use_topologically_correct_marching_cubes(true));
assert(points.size() && triangles.size());
assert(!has_duplicate_points(points, triangles));
assert(!has_duplicate_polygons(points, triangles));
assert(!has_isolated_vertices(points, triangles));
assert(CGAL::Polygon_mesh_processing::is_polygon_soup_a_polygon_mesh(triangles));
Mesh m;
CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh(points, triangles, m);
const int euler = euler_characteristic(m);
const int solution = read_euler("data/MarchingCubes_cases/Grid_invariants/" + std::to_string(i) + "-euler_number.txt");
if (euler != solution)
std::cout << "error in test " << i << ": euler " << euler << " != " << solution << std::endl;
}
}
template <typename K>
void verify_betti() {
using FT = typename K::FT;
using Point = typename K::Point_3;
using Vector = typename K::Vector_3;
using Mesh = CGAL::Surface_mesh<Point>;
using Grid = IS::Cartesian_grid_3<K>;
using Values = IS::Interpolated_discrete_values_3<Grid>;
using Domain = IS::Marching_cubes_domain_3<Grid, Values>;
using Point_range = std::vector<Point>;
using Polygon_range = std::vector<std::vector<std::size_t> >;
using Mesh = CGAL::Surface_mesh<Point>;
const std::size_t num_tests = 10000;
for (std::size_t i = 0; i < num_tests; i++) {
Grid grid;
Values values { grid };
Domain domain { grid, values };
read_iso_volume("data/Closed_Surfaces/Grids/" + std::to_string(i) + "-scalar_field.iso", grid, values);
Point_range points;
Polygon_range triangles;
IS::marching_cubes<CGAL::Sequential_tag>(domain, 0, points, triangles, CGAL::parameters::use_topologically_correct_marching_cubes(true));
assert(points.size() && triangles.size());
assert(!has_duplicate_points(points, triangles));
assert(!has_duplicate_polygons(points, triangles));
assert(!has_isolated_vertices(points, triangles));
assert(CGAL::Polygon_mesh_processing::is_polygon_soup_a_polygon_mesh(triangles));
Mesh m;
CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh(points, triangles, m);
const int b0 = betti_0(m);
const int b1 = betti_1(m);
const auto solution = read_betti("data/Closed_Surfaces/InvariantsGrid/" + std::to_string(i) + "-invariant_grid.txt");
if (b0 != solution[0])
std::cout << "error in test " << i << ": b0 " << b0 << " != " << solution[0] << std::endl;
if (b1 != solution[1])
std::cout << "error in test " << i << ": b1 " << b1 << " != " << solution[1] << std::endl;
}
}
template <typename K>
void compare_to_reference(const std::string& filename) {
using FT = typename K::FT;
using Point = typename K::Point_3;
using Vector = typename K::Vector_3;
using Mesh = CGAL::Surface_mesh<Point>;
using Grid = IS::Cartesian_grid_3<K>;
using Values = IS::Interpolated_discrete_values_3<Grid>;
using Domain = IS::Marching_cubes_domain_3<Grid, Values>;
using Point_range = std::vector<Point>;
using Polygon_range = std::vector<std::vector<std::size_t> >;
using Mesh = CGAL::Surface_mesh<Point>;
Grid grid;
Values values { grid };
Domain domain { grid, values };
read_iso_volume(filename, grid, values);
Point_range points;
Polygon_range triangles;
IS::marching_cubes<CGAL::Sequential_tag>(domain, 0, points, triangles, CGAL::parameters::use_topologically_correct_marching_cubes(true));
CGAL::IO::write_polygon_soup("verify_tmc.off", points, triangles, CGAL::parameters::stream_precision(17));
Grid grid_high_res { grid.span().min(), grid.span().max(), std::array<std::size_t, 3>{151, 151, 151} };
IS::Value_function_3<Grid> values_high_res { values, grid_high_res };
IS::Marching_cubes_domain_3<Grid, IS::Value_function_3<Grid>> domain_high_res { grid_high_res, values_high_res };
Point_range points_high_res;
Polygon_range triangles_high_res;
IS::marching_cubes<CGAL::Parallel_if_available_tag>(domain_high_res, 0, points_high_res, triangles_high_res, CGAL::parameters::use_topologically_correct_marching_cubes(false));
CGAL::IO::write_polygon_soup("verify_reference.off", points_high_res, triangles_high_res);
write_debug_grid(domain, "verify_cell.off");
}
int main(int, char**)
{
using K = CGAL::Simple_cartesian<double>;
using FT = typename K::FT;
// verify_euler<K>();
// verify_betti<K>();
compare_to_reference<K>("data/MarchingCubes_cases/Grids/" + std::to_string(100) + "-scalar_field.iso");
// compare_to_reference<K>("data/Closed_Surfaces/Grids/" + std::to_string(0) + "-scalar_field.iso");
}