diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 790619882e..cf1b4cba80 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,6 +8,7 @@ repos:
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
+ args: ['--maxkb=3000']
- id: check-symlinks
- id: check-case-conflict
- repo: https://github.com/codespell-project/codespell
diff --git a/ChangeLog.md b/ChangeLog.md
index fc415639cd..5b138d84d5 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -6,6 +6,9 @@
- pre-commit hooks have been activated for sanity checks on files before running the git commit (David Coeurjolly, [#1835](https://github.com/DGtal-team/DGtal/pull/1835))
- Add pre-commit setup instructions to contributing documentation (David Coeurjolly, [#1836](https://github.com/DGtal-team/DGtal/pull/1836))
+- *Geometry*
+ - New multithread version of Integral Invariant estimators using a DomainSplitter. A new parameter has been added in the II shortcuts to enable multithreading when OpenMP has been activated --DGTAL_WITH_OPENMP flag-- (Bastien Doignies, David Coeurjolly, [#1842](https://github.com/DGtal-team/DGtal/pull/1842))
+
## Changes
- *Documentation*
diff --git a/cmake/CheckDGtalOptionalDependencies.cmake b/cmake/CheckDGtalOptionalDependencies.cmake
index a972a38f84..eaa1ee2fd7 100644
--- a/cmake/CheckDGtalOptionalDependencies.cmake
+++ b/cmake/CheckDGtalOptionalDependencies.cmake
@@ -230,6 +230,8 @@ endif()
set(OPENMP_FOUND_DGTAL 0)
if(DGTAL_WITH_OPENMP)
include(openmp)
+ target_link_libraries(DGtal PUBLIC OpenMP::OpenMP_CXX)
+ target_compile_definitions(DGtal PUBLIC -DDGTAL_WITH_OPENMP)
set(DGtalLibDependencies ${DGtalLibDependencies} OpenMP::OpenMP_CXX)
set(OPENMP_FOUND_DGTAL 1)
set(DGTAL_WITH_OPENMP 1)
diff --git a/examples/geometry/surfaces/CMakeLists.txt b/examples/geometry/surfaces/CMakeLists.txt
index c09750d060..012eb5cd8c 100644
--- a/examples/geometry/surfaces/CMakeLists.txt
+++ b/examples/geometry/surfaces/CMakeLists.txt
@@ -22,6 +22,7 @@ if ( DGTAL_WITH_POLYSCOPE_VIEWER )
dvcm-3d
examplePlaneProbingSurfaceLocalEstimator
exampleMaximalSegmentSliceEstimation
+ parallelIIShortcuts
)
foreach(FILE ${DGTAL_EXAMPLES_POLYSCOPE_SRC})
DGtal_add_example(${FILE})
diff --git a/examples/geometry/surfaces/parallelIIShortcuts.cpp b/examples/geometry/surfaces/parallelIIShortcuts.cpp
new file mode 100644
index 0000000000..c1cc1f9aa6
--- /dev/null
+++ b/examples/geometry/surfaces/parallelIIShortcuts.cpp
@@ -0,0 +1,132 @@
+/**
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ **/
+
+/**
+ * @file
+ * @ingroup Examples
+ * @author David Coeurjolly (david.coeurjolly@cnrs.fr)
+ * Laboratoire d'InfoRmatique en Image et Systèmes d'information - LIRIS (CNRS, UMR 5205), INSA-Lyon, France
+ *
+ * @date 2026/06/08
+ *
+ *
+ * This file is part of the DGtal library.
+ */
+
+///////////////////////////////////////////////////////////////////////////////
+#include
+#include "ConfigExamples.h"
+
+// Helpers
+#include "DGtal/base/Common.h"
+#include "DGtal/helpers/StdDefs.h"
+
+#include "DGtal/helpers/Shortcuts.h"
+#include "DGtal/helpers/ShortcutsGeometry.h"
+
+
+#ifdef DGTAL_WITH_POLYSCOPE_VIEWER
+// Visualization
+#include "DGtal/io/viewers/PolyscopeViewer.h"
+#include "DGtal/io/colormaps/GradientColorMap.h"
+#endif
+
+///////////////////////////////////////////////////////////////////////////////
+
+using namespace DGtal;
+
+// Using standard 3D digital space.
+typedef Shortcuts SH3;
+typedef ShortcutsGeometry SHG3;
+///////////////////////////////////////////////////////////////////////////////
+
+int main()
+{
+
+ //! [Parallel-instantiation]
+ auto params = SH3::defaultParameters() | SHG3::defaultParameters();
+ params( "polynomial", "goursat" )( "gridstep", 0.125 );
+ auto implicit_shape = SH3::makeImplicitShape3D ( params );
+ auto digitized_shape = SH3::makeDigitizedImplicitShape3D( implicit_shape, params );
+ auto K = SH3::getKSpace( params );
+ auto surface = SH3::makeDigitalSurface( digitized_shape, K, params );
+ auto surfels = SH3::getSurfelRange( surface, params );
+ //! [Parallel-instantiation]
+
+ //! [Parallel-run]
+ trace.info()<< "Input vol domain: "<< digitized_shape->getDomain() << std::endl;
+ //Sequential
+ trace.beginBlock("Single thread");
+ auto curv = SHG3::getIIMeanCurvatures( digitized_shape, surfels, params );
+ trace.endBlock();
+
+ //Parallel
+ trace.beginBlock("4 threads on default axis");
+ auto curv_par4 = SHG3::getIIMeanCurvatures( digitized_shape, surfels,
+ params( "ii-thread-number", 4 ));
+ trace.endBlock();
+
+ //Parallel
+ trace.beginBlock("8 threads on axis 0");
+ auto curv_par8_0 = SHG3::getIIMeanCurvatures( digitized_shape, surfels,
+ params( "ii-thread-number", 8 )
+ ( "ii-split-axis", 0 ) );
+ trace.endBlock();
+
+ //Parallel
+ trace.beginBlock("8 threads on axis 1");
+ auto curv_par8_1 = SHG3::getIIMeanCurvatures( digitized_shape, surfels,
+ params( "ii-thread-number", 8 )
+ ( "ii-split-axis", 1 ) );
+ trace.endBlock();
+
+ //Parallel
+ trace.beginBlock("8 threads on axis 2");
+ auto curv_par8_2 = SHG3::getIIMeanCurvatures( digitized_shape, surfels,
+ params( "ii-thread-number", 8 )
+ ( "ii-split-axis", 2 ) );
+ trace.endBlock();
+
+ //Parallel
+ trace.beginBlock("16 threads on axis 2");
+ auto curv_par16 = SHG3::getIIMeanCurvatures( digitized_shape, surfels,
+ params( "ii-thread-number", 16 )
+ ( "ii-split-axis", 2 ) );
+ trace.endBlock();
+ //! [Parallel-run]
+
+#ifdef DGTAL_WITH_POLYSCOPE_VIEWER
+ PolyscopeViewer viewer;
+
+ std::string objectName = "Surfels";
+ viewer.draw(surfels, objectName); // Draws the object independently
+ viewer.addQuantity(objectName, "Mean curvature", curv_par8_0);
+
+ AxisDomainSplitter splitter(0);
+ AxisDomainSplitter::SplitDomainsInfo splits = splitter(digitized_shape->getDomain(), 8);
+ HueShadeColorMap cmap(0,(unsigned int)splits.size());
+ for(auto i=0; i< splits.size(); ++i)
+ {
+ viewer << cmap(i);
+ viewer << splits[i].domain;
+ }
+ viewer.show();
+#endif
+
+ return 0;
+}
+// //
+///////////////////////////////////////////////////////////////////////////////
diff --git a/examples/tutorial-examples/shortcuts-geometry.cpp b/examples/tutorial-examples/shortcuts-geometry.cpp
index c28f27b013..4520f6c379 100644
--- a/examples/tutorial-examples/shortcuts-geometry.cpp
+++ b/examples/tutorial-examples/shortcuts-geometry.cpp
@@ -57,6 +57,8 @@ int main( int /* argc */, char** /* argv */ )
//! [dgtal_shortcuts_ssec2_1_6s]
auto params = SH3::defaultParameters() | SHG3::defaultParameters();
params( "colormap", "Tics" );
+ // To request the parallel II estimator when DGtal is built with OpenMP:
+ // params( "ii-thread-number", 4 )( "ii-split-axis", 1 );
auto bimage = SH3::makeBinaryImage( examplesPath + "samples/Al.100.vol", params );
auto K = SH3::getKSpace( bimage, params );
auto surface = SH3::makeDigitalSurface( bimage, K, params );
diff --git a/src/DGtal/doc/tutorials/moduleShortcuts.dox b/src/DGtal/doc/tutorials/moduleShortcuts.dox
index 50c9d1d7fa..8d49e5d273 100644
--- a/src/DGtal/doc/tutorials/moduleShortcuts.dox
+++ b/src/DGtal/doc/tutorials/moduleShortcuts.dox
@@ -150,7 +150,17 @@ You may choose your traversal order ("Default", "DepthFirst", "BreadthFirst").
@subsubsection dgtal_shortcuts_ssec2_1_6 -> build digital surface -> estimate curvatures -> save OBJ.
-This example requires ShortcutsGeometry. It shows how tu use the integral invariant curvature estimator on a digital shape model to estimate its mean or Gaussian curvature.
+This example requires ShortcutsGeometry. It shows how to use the integral invariant curvature estimator on a digital shape model to estimate its mean or Gaussian curvature.
+
+The same shortcut methods may also use the parallel integral invariant
+estimator when DGtal is built with OpenMP support (`DGTAL_WITH_OPENMP` option). To request it, set
+parameter `ii-thread-number` to a value different from `1` before
+calling `ShortcutsGeometry::getIINormalVectors`,
+`ShortcutsGeometry::getIIMeanCurvatures`,
+`ShortcutsGeometry::getIIGaussianCurvatures`, or
+`ShortcutsGeometry::getIIPrincipalCurvaturesAndDirections`.
+The main axis of the default `AxisDomainSplitter` can be selected with
+parameter `ii-split-axis` (default value `0`).
\snippet examples/tutorial-examples/shortcuts-geometry.cpp dgtal_shortcuts_ssec2_1_6s
@@ -552,10 +562,10 @@ Shortcuts::Point is Z3i::KSpace::Point.
- ShortcutsGeometry::getTrivialNormalVectors: returns the trivial (Trivial) normal vectors to the given surfel range
- ShortcutsGeometry::getCTrivialNormalVectors: returns the convolved trivial (CTrivial) normal vectors to the given surfel range
- ShortcutsGeometry::getVCMNormalVectors: returns the Voronoi Covariance Measure (VCM) normal vectors to the given surfel range
- - ShortcutsGeometry::getIINormalVectors: returns the Integral Invariant (II) normal vectors to the given surfel range (embedded in a binary image or a digitized implicit shape)
- - ShortcutsGeometry::getIIMeanCurvatures: returns the Integral Invariant (II) mean curvatures onto the given surfel range (embedded in a binary image or a digitized implicit shape)
- - ShortcutsGeometry::getIIGaussianCurvatures: returns the Integral Invariant (II) Gaussian curvatures onto the given surfel range (embedded in a binary image or a digitized implicit shape)
- - ShortcutsGeometry::getIIPrincipalCurvaturesAndDirections: returns the Integral Invariant (II) principal curvatures values and directions for the given surfel range (embedded in a binary image or a digitized implicit shape)
+ - ShortcutsGeometry::getIINormalVectors: returns the Integral Invariant (II) normal vectors to the given surfel range (embedded in a binary image or a digitized implicit shape), optionally through the parallel II estimator when `ii-thread-number != 1` (when `DGTAL_WITH_OPENMP` has been set to true).
+ - ShortcutsGeometry::getIIMeanCurvatures: returns the Integral Invariant (II) mean curvatures onto the given surfel range (embedded in a binary image or a digitized implicit shape), optionally through the parallel II estimator when `ii-thread-number != 1` (when `DGTAL_WITH_OPENMP` has been set to true).
+ - ShortcutsGeometry::getIIGaussianCurvatures: returns the Integral Invariant (II) Gaussian curvatures onto the given surfel range (embedded in a binary image or a digitized implicit shape), optionally through the parallel II estimator when `ii-thread-number != 1` (when `DGTAL_WITH_OPENMP` has been set to true).
+ - ShortcutsGeometry::getIIPrincipalCurvaturesAndDirections: returns the Integral Invariant (II) principal curvatures values and directions for the given surfel range (embedded in a binary image or a digitized implicit shape), optionally through the parallel II estimator when `ii-thread-number != 1` (when `DGTAL_WITH_OPENMP` has been set to true).
- ShortcutsGeometry::getCNCMeanCurvatures: returns the Corrected Normal Current (CNC) mean curvatures onto the given faces (as ids of the mesh).
- ShortcutsGeometry::getCNCGaussianCurvatures: returns the Corrected Normal Current (CNC) gaussian curvatures onto the given faces (as ids of the mesh).
- ShortcutsGeometry::getCNCPrincipalCurvaturesAndDirections: returns the Corrected Normal Current (CNC) principal curvatures values and directions for the given face range (as ids of the mesh).
diff --git a/src/DGtal/geometry/doc/images/parallelsplit.jpg b/src/DGtal/geometry/doc/images/parallelsplit.jpg
new file mode 100644
index 0000000000..8e855616b3
Binary files /dev/null and b/src/DGtal/geometry/doc/images/parallelsplit.jpg differ
diff --git a/src/DGtal/geometry/doc/moduleIntegralInvariant.dox b/src/DGtal/geometry/doc/moduleIntegralInvariant.dox
index 8a9362a2d4..3169155d62 100644
--- a/src/DGtal/geometry/doc/moduleIntegralInvariant.dox
+++ b/src/DGtal/geometry/doc/moduleIntegralInvariant.dox
@@ -24,7 +24,7 @@ namespace DGtal {
[TOC]
-@author Jérémy Levallois
+@author Jérémy Levallois, David Coeurjolly
\section II_sectOverview Overview
@@ -199,6 +199,72 @@ Here is some results in 2d and 3d :
@image html Bunny_128_mean.png "Mean curvature mapped on a Stanford bunny (credit is given to the Stanford Computer Graphics Laboratory https://graphics.stanford.edu/data/3Dscanrep/)"
@image html Bunny_64_k1.png "First principal curvature direction mapped on a Stanford bunny (credit is given to the Stanford Computer Graphics Laboratory https://graphics.stanford.edu/data/3Dscanrep/)"
-*/
+
+\section II_sectParallel Parallel Integral Invariant estimators
+
+Each Integral Invariant estimator can run in parallel in DGtal, provided the library is compiled with OpenMP support (`DGTAL_WITH_OPENMP` option).
+The interface for computation and parameter setting is the same as for every other integral invariant estimator and can
+be used as a drop-in replacement in most cases.
+
+The key idea behind the parallel execution is to first split the surface domain into subdomains, and then run convolutions concurrently
+on each of these smaller domains. The splitter type is still specified in the template parameters, while the configured splitter
+instance is now passed to the `ParallelIIEstimator` constructor together with the requested thread count. Currently,
+DGtal provides RegularDomainSplitter, which splits the outer domain regularly, and AxisDomainSplitter, which splits domains along a
+specified dimension. See kernel/domains/DomainSplitter.h for more information and the expected interface.
+
+At the low-level API, a typical construction is:
+
+@snippet tests/geometry/surfaces/testParallelIntegralInvariantEstimator.cpp exampleParallelII-construction
+
+
+A complete example is detailed in parallelIIShortcuts.cpp using the DGtal Shortcuts (see @ref moduleShortcuts). For example, using this setting:
+
+@snippet geometry/surfaces/parallelIIShortcuts.cpp Parallel-instantiation
+
+Parallel Integral Invariant curvature estimation can be obtained using:
+
+@snippet geometry/surfaces/parallelIIShortcuts.cpp Parallel-run
+
+On an Apple M1 (8 cores), the output shows a speed-up up to 8 threads as expected:
+```
+Input vol domain: [HyperRectDomain] = [(-85, -85, -85)]x[(85, 85, 85)]
+New Block [Single thread]
+ - II mean curvature alpha=0.33
+ - II mean curvature r=1.51043 (continuous) 12.0835 (discrete)
+EndBlock [Single thread] (25956.2 ms)
+New Block [4 threads on default axis]
+ - II mean curvature alpha=0.33
+ - II mean curvature r=1.51043 (continuous) 12.0835 (discrete)
+ - II mean curvature uses ParallelIIEstimator with thread request=4 and split axis=0
+EndBlock [4 threads on default axis] (8295.22 ms)
+New Block [8 threads on axis 0]
+ - II mean curvature alpha=0.33
+ - II mean curvature r=1.51043 (continuous) 12.0835 (discrete)
+ - II mean curvature uses ParallelIIEstimator with thread request=8 and split axis=0
+EndBlock [8 threads on axis 0] (7084.68 ms)
+New Block [8 threads on axis 1]
+ - II mean curvature alpha=0.33
+ - II mean curvature r=1.51043 (continuous) 12.0835 (discrete)
+ - II mean curvature uses ParallelIIEstimator with thread request=8 and split axis=1
+EndBlock [8 threads on axis 1] (6740.38 ms)
+New Block [8 threads on axis 2]
+ - II mean curvature alpha=0.33
+ - II mean curvature r=1.51043 (continuous) 12.0835 (discrete)
+ - II mean curvature uses ParallelIIEstimator with thread request=8 and split axis=2
+EndBlock [8 threads on axis 2] (4311.78 ms)
+New Block [16 threads on axis 2]
+ - II mean curvature alpha=0.33
+ - II mean curvature r=1.51043 (continuous) 12.0835 (discrete)
+ - II mean curvature uses ParallelIIEstimator with thread request=16 and split axis=2
+EndBlock [16 threads on axis 2] (4717 ms)
+```
+
+For 8 threads with splits along the 0-axis, we obtain:
+
+@image html parallelsplit.jpg "Parallel computation of integral invariants (8-threads)"
+
+
+
+*/
}
diff --git a/src/DGtal/geometry/surfaces/estimation/ParallelIIEstimator.h b/src/DGtal/geometry/surfaces/estimation/ParallelIIEstimator.h
new file mode 100644
index 0000000000..f37440b3e1
--- /dev/null
+++ b/src/DGtal/geometry/surfaces/estimation/ParallelIIEstimator.h
@@ -0,0 +1,191 @@
+/**
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ **/
+
+/**
+ * @file
+ * @author Bastien Doignies
+ * Laboratoire d'InfoRmatique en Image et Systèmes d'information - LIRIS (CNRS, UMR 5205), INSA-Lyon, France
+ *
+ * @date 2026/05/01
+ *
+ * Header file for module ParallelEstimator.ih
+ *
+ * This file is part of the DGtal library.
+ */
+
+#include
+#include
+
+#ifndef DGTAL_WITH_OPENMP
+#error You need to have activated OpenMP (DGTAL_WITH_OPENMP) to include this file.
+#endif
+
+// We require openmp for this estimator, even though it could run without
+#include
+
+namespace DGtal
+{
+ /**
+ * @brief Run an Integral Invariant estimator in parallel
+ *
+ * This class is meant as an almost perfect replacement of
+ * other IIEstimator. The only difference is the constructor,
+ * which needs both a domain splitter instance and the number
+ * of threads.
+ *
+ * TODO: Value to control output (ie. value + loc, just value or ordered value)
+ * TODO: Surface construction on subdomain fonctor ?
+ *
+ * @tparam TEstimator The model of Estimator
+ * @tparam TSplitter The function to split the domain
+ */
+ template
+ class ParallelIIEstimator
+ {
+ public:
+ using Estimator = TEstimator;
+ using Splitter = TSplitter;
+ using Domain = typename TEstimator::Domain;
+ using Scalar = typename TEstimator::Scalar;
+
+ using KSpace = typename TEstimator::KSpace;
+ using PointPredicate = typename TEstimator::PointPredicate;
+ using Surfel = typename KSpace::Surfel;
+ using SurfelSet = typename KSpace::SurfelSet;
+ using EstimatorQuantity = typename TEstimator::Quantity;
+ using Quantity = EstimatorQuantity;
+
+ // Building the surface
+ using Boundary = LightImplicitDigitalSurface;
+ using Surface = DigitalSurface;
+ using Visitor = DepthFirstVisitor;
+ using VisitorRange = GraphVisitorRange;
+
+
+ /**
+ * @brief Constructor
+ *
+ * @tparam Args The estimator constructor arguments
+ *
+ * @param splitter The domain splitter instance used to partition the domain.
+ * @param nbThread The number of thread to run in parallel. -1 means as many as possible
+ * @param args Constructor arguments to underlying estimators
+ */
+ template
+ ParallelIIEstimator(Splitter splitter, int32_t nbThread, Args&&... args);
+
+ /**
+ * Clears the object. It is now invalid.
+ */
+ void clear();
+
+ /// @return the grid step.
+ Scalar h() const;
+
+ /**
+ * Attach a shape, defined as a functor spel -> boolean
+ *
+ * @param[in] K the cellular grid space in which the shape is defined.
+ * @param aPointPredicate the shape of interest. The alias can be secured
+ * if a some counted pointer is handed.
+ */
+ void attach(ConstAlias K,
+ ConstAlias aPointPredicate);
+
+ /**
+ * Set specific parameters: the radius of the ball.
+ *
+ * @param[in] dRadius the "digital" radius of the kernel (buy may be non integer).
+ */
+ void setParams(double dRadius);
+
+
+ /**
+ * Checks the validity/consistency of the object.
+ * @return 'true' if the object is valid, 'false' otherwise.
+ */
+ bool isValid() const;
+
+ /**
+ * Model of CDigitalSurfaceLocalEstimator. Initialisation.
+ *
+ * @tparam SurfelConstIterator any model of forward readable iterator on Surfel.
+ * @param[in] h_ grid size (must be >0).
+ * @param[in] ite iterator on the first surfel of the surface.
+ * @param[in] itb iterator after the last surfel of the surface.
+ */
+ template
+ void init(double h_, ItA ite, ItB itb);
+
+ /**
+ * -- Estimation --
+ *
+ * Compute the integral invariant volume at surfel *it of
+ * a shape, then apply the VolumeFunctor to extract some
+ * geometric information.
+ *
+ * @tparam SurfelConstIterator type of Iterator on a Surfel
+ *
+ * @param[in] it iterator pointing on the surfel of the shape where
+ * we wish to evaluate some geometric information.
+ *
+ * @return quantity (normal vector) at surfel *it
+ */
+ template
+ Quantity eval(It it);
+
+ /**
+ * -- Estimation --
+ *
+ * Compute the integral invariant volume for a range of
+ * surfels [itb,ite) on a shape, then apply the
+ * VolumeFunctor to extract some geometric information.
+ * Return the result on an OutputIterator (param).
+ *
+ * @tparam OutputIterator type of Iterator of an array of Quantity
+ * @tparam SurfelConstIterator type of Iterator on a Surfel
+ *
+ * @param[in] itb iterator defining the start of the range of surfels
+ * where we wish to compute some geometric information.
+ *
+ * @param[in] ite iterator defining the end of the range of surfels
+ * where we wish to compute some geometric information.
+ *
+ * @param[in] result output iterator of results of the computation.
+ * @return the updated output iterator after all outputs.
+ */
+ template
+ Oit eval(It itb, It ite, Oit result);
+
+ /**
+ * Writes/Displays the object on an output stream.
+ * @param out the output stream where the object is written.
+ */
+ void selfDisplay ( std::ostream & out ) const;
+
+ private:
+ std::vector myEstimators;
+ Splitter mySplitter;
+
+ CountedConstPtrOrConstPtr myPointPredicate;
+ CountedConstPtrOrConstPtr myKSpace;
+
+ double myH;
+ double myRadius;
+ };
+}
+
+#include "ParallelIIEstimator.ih"
diff --git a/src/DGtal/geometry/surfaces/estimation/ParallelIIEstimator.ih b/src/DGtal/geometry/surfaces/estimation/ParallelIIEstimator.ih
new file mode 100644
index 0000000000..257b541b00
--- /dev/null
+++ b/src/DGtal/geometry/surfaces/estimation/ParallelIIEstimator.ih
@@ -0,0 +1,181 @@
+/**
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ **/
+
+/**
+ * @file ParallelIIEstimator.ih
+ * @author Bastien Doignies
+ * Laboratoire d'InfoRmatique en Image et Systèmes d'information - LIRIS (CNRS, UMR 5205), INSA-Lyon, France
+ *
+ * @date 2026/05/01
+ *
+ * Header file for module ParallelEstimator.ih
+ *
+ * This file is part of the DGtal library.
+ */
+
+
+#include
+#include
+#include
+
+namespace DGtal
+{
+ template
+ template
+ ParallelIIEstimator::
+ ParallelIIEstimator(Splitter splitter, int32_t nbThread, Args&&... args)
+ : mySplitter(std::move(splitter))
+ {
+ if (nbThread <= 0)
+ nbThread = std::max(1, omp_get_max_threads());
+
+ // Note: We can not use std::vector constructor or resize
+ // because it will copy the estimator but it can have shared
+ // state.
+ // We rather build a new one in a loop by calling the constructor
+ // implictely with emplace_back
+ myEstimators.reserve(nbThread);
+ for (int i = 0; i < nbThread; ++i)
+ myEstimators.emplace_back(std::forward(args)...);
+ }
+
+ template
+ void ParallelIIEstimator::clear()
+ {
+ for (auto& estim : myEstimators)
+ estim.clear();
+ }
+
+ template
+ typename ParallelIIEstimator::Scalar
+ ParallelIIEstimator::h() const { return myEstimators[0].h(); }
+
+ template
+ void ParallelIIEstimator::attach(
+ ConstAlias K,
+ ConstAlias pp)
+ {
+ clear();
+
+ myKSpace = K;
+ myPointPredicate = pp;
+ }
+
+ template
+ void ParallelIIEstimator::setParams(double dRadius)
+ {
+ myRadius = dRadius;
+ }
+
+ template
+ bool ParallelIIEstimator::isValid() const
+ {
+ bool valid = true;
+ for (const auto& estim : myEstimators)
+ valid = valid && estim.isValid();
+
+ return valid;
+ }
+
+ template
+ template
+ void ParallelIIEstimator::init(double h_, ItA, ItB)
+ {
+ myH = h_;
+ }
+
+ template
+ template
+ typename ParallelIIEstimator::Quantity
+ ParallelIIEstimator::eval(It it)
+ {
+ // TODO: Initialize properly the estimator (attaching shape, ...)
+ if (!myEstimators[0].isValid())
+ {
+ auto ite = it; ite++;
+ myEstimators[0].init(myH, it, ite);
+ }
+
+ return {
+ .location = *it,
+ .value = myEstimators[0].eval(it)
+ };
+ }
+
+ template
+ template
+ Oit ParallelIIEstimator::eval(It itb, It ite, Oit rslt)
+ {
+ Domain mainDomain(myKSpace->lowerBound(), myKSpace->upperBound());
+ std::vector> domains = mySplitter(mainDomain, myEstimators.size());
+
+ using Result = std::pair>;
+ std::deque result;
+ std::vector> resultLocation(domains.size());
+ for ( auto it = itb; it != ite; ++it )
+ {
+ result.push_back( { *it, std::nullopt } );
+ Result* current = &result.back();
+
+ for (unsigned int j = 0; j < domains.size(); ++j)
+ {
+ // Should be true for only one of the domain. The break ensure
+ // each surfel belongs to one and only one domain, meaning there
+ // will be no concurrent write afterward
+ if (domains[j].domain.isInside(myKSpace->interiorVoxel(*it)))
+ {
+ resultLocation[j].push_back( current );
+ break;
+ }
+ }
+ }
+
+ #pragma omp parallel for
+ for (uint32_t i = 0; i < domains.size(); ++i)
+ {
+ auto surfels = resultLocation[i] | std::views::transform([](const auto* kv) -> const Surfel&
+ {
+ return kv->first;
+ });
+ auto values = resultLocation[i] | std::views::transform([](auto* kv) -> std::optional&
+ {
+ return kv->second;
+ });
+
+ auto& estim = myEstimators[i];
+
+ estim.attach(*myKSpace, *myPointPredicate);
+ estim.setParams(myRadius);
+
+ estim.init(myH, surfels.begin(), surfels.end());
+ estim.eval(surfels.begin(), surfels.end(), values.begin());
+ }
+
+ for ( const auto& entry : result )
+ *rslt++ = *( entry.second );
+
+ return rslt;
+ }
+
+ template
+ void
+ ParallelIIEstimator::selfDisplay( std::ostream & out ) const
+ {
+ out << "[ParallelIIEstimator Estim=";
+ myEstimators[0].selfDisplay(out);
+ out << "]";
+ }
+}
diff --git a/src/DGtal/helpers/ShortcutsGeometry.h b/src/DGtal/helpers/ShortcutsGeometry.h
index fb5fe6320d..840f054647 100644
--- a/src/DGtal/helpers/ShortcutsGeometry.h
+++ b/src/DGtal/helpers/ShortcutsGeometry.h
@@ -51,6 +51,10 @@
#include "DGtal/geometry/surfaces/estimation/IIGeometricFunctors.h"
#include "DGtal/geometry/surfaces/estimation/IntegralInvariantVolumeEstimator.h"
#include "DGtal/geometry/surfaces/estimation/IntegralInvariantCovarianceEstimator.h"
+#ifdef DGTAL_WITH_OPENMP
+#include "DGtal/geometry/surfaces/estimation/ParallelIIEstimator.h"
+#include "DGtal/kernel/domains/DomainSplitter.h"
+#endif
#include "DGtal/geometry/meshes/CorrectedNormalCurrentComputer.h"
#include "DGtal/dec/DiscreteExteriorCalculusFactory.h"
@@ -975,6 +979,12 @@ namespace DGtal
/// - r-radius [ 3.0]: the constant for kernel radius parameter r in r(h)=r h^alpha (VCM,II,Trivial).
/// - kernel [ "hat"]: the kernel integration function chi_r, either "hat" or "ball". )
/// - alpha [ 0.33]: the parameter alpha in r(h)=r h^alpha (VCM, II)."
+ /// - ii-thread-number[ 1]: number of threads for II estimators;
+ /// 1 keeps the sequential estimator, any other
+ /// value requests the parallel estimator when
+ /// OpenMP support is available.
+ /// - ii-split-axis [ 0]: main axis used by AxisDomainSplitter
+ /// for parallel II estimators.
/// - surfelEmbedding [ 0]: the surfel -> point embedding for VCM estimator: 0: Pointels, 1: InnerSpel, 2: OuterSpel.
/// - unit_u [0]: Use unit normals for (CNC) curvature computations.
static Parameters parametersGeometryEstimation()
@@ -986,6 +996,8 @@ namespace DGtal
( "R-radius", 10.0 )
( "r-radius", 3.0 )
( "alpha", 0.33 )
+ ( "ii-thread-number", 1 )
+ ( "ii-split-axis", 0 )
( "surfelEmbedding", 0 )
( "unit_u" , 0 );
}
@@ -1170,6 +1182,12 @@ namespace DGtal
/// - verbose [ 1]: verbose trace mode 0: silent, 1: verbose.
/// - r-radius [ 3.0]: the constant for kernel radius parameter r in r(h)=r h^alpha (VCM,II,Trivial).
/// - alpha [ 0.33]: the parameter alpha in r(h)=r h^alpha (VCM, II)."
+ /// - ii-thread-number[ 1]: number of threads for II estimators;
+ /// 1 keeps the sequential estimator, any other
+ /// value requests the parallel estimator when
+ /// OpenMP support is available.
+ /// - ii-split-axis [ 0]: main axis used by AxisDomainSplitter
+ /// for parallel II estimators.
/// - gridstep [ 1.0]: the digitization gridstep (often denoted by h).
///
/// @return the vector containing the estimated normals, in the
@@ -1205,6 +1223,12 @@ namespace DGtal
/// - verbose [ 1]: verbose trace mode 0: silent, 1: verbose.
/// - r-radius [ 3.0]: the constant for kernel radius parameter r in r(h)=r h^alpha (VCM,II,Trivial).
/// - alpha [ 0.33]: the parameter alpha in r(h)=r h^alpha (VCM, II)."
+ /// - ii-thread-number[ 1]: number of threads for II estimators;
+ /// 1 keeps the sequential estimator, any other
+ /// value requests the parallel estimator when
+ /// OpenMP support is available.
+ /// - ii-split-axis [ 0]: main axis used by AxisDomainSplitter
+ /// for parallel II estimators.
/// - gridstep [ 1.0]: the digitization gridstep (often denoted by h).
/// - minAABB [ -10.0]: the min value of the AABB bounding box (domain)
/// - maxAABB [ 10.0]: the max value of the AABB bounding box (domain)
@@ -1246,6 +1270,12 @@ namespace DGtal
/// - verbose [ 1]: verbose trace mode 0: silent, 1: verbose.
/// - r-radius [ 3.0]: the constant for kernel radius parameter r in r(h)=r h^alpha (VCM,II,Trivial).
/// - alpha [ 0.33]: the parameter alpha in r(h)=r h^alpha (VCM, II)."
+ /// - ii-thread-number[ 1]: number of threads for II estimators;
+ /// 1 keeps the sequential estimator, any other
+ /// value requests the parallel estimator when
+ /// OpenMP support is available.
+ /// - ii-split-axis [ 0]: main axis used by AxisDomainSplitter
+ /// for parallel II estimators.
/// - gridstep [ 1.0]: the digitization gridstep (often denoted by h).
///
/// @return the vector containing the estimated normals, in the
@@ -1271,10 +1301,12 @@ namespace DGtal
IINormalEstimator;
RealVectors n_estimations;
- int verbose = params[ "verbose" ].as();
- Scalar h = params[ "gridstep" ].as();
- Scalar r = params[ "r-radius" ].as();
- Scalar alpha = params[ "alpha" ].as();
+ int verbose = params[ "verbose" ].as();
+ int ii_thread_number = params[ "ii-thread-number" ].as();
+ auto ii_split_axis = getIIParallelSplitAxis( params );
+ Scalar h = params[ "gridstep" ].as();
+ Scalar r = params[ "r-radius" ].as();
+ Scalar alpha = params[ "alpha" ].as();
if ( alpha != 1.0 ) r *= pow( h, alpha-1.0 );
if ( verbose > 0 )
{
@@ -1284,12 +1316,40 @@ namespace DGtal
}
IINormalFunctor functor;
functor.init( h, r*h );
- IINormalEstimator ii_estimator( functor );
- ii_estimator.attach( K, shape );
- ii_estimator.setParams( r );
- ii_estimator.init( h, surfels.begin(), surfels.end() );
- ii_estimator.eval( surfels.begin(), surfels.end(),
- std::back_inserter( n_estimations ) );
+ bool use_parallel = false;
+#ifdef DGTAL_WITH_OPENMP
+ if ( ii_thread_number != 1 )
+ {
+ use_parallel = true;
+ if ( verbose > 0 )
+ trace.info() << "- II normal uses ParallelIIEstimator with thread request="
+ << ii_thread_number << " and split axis="
+ << ii_split_axis << std::endl;
+ typedef AxisDomainSplitter Splitter;
+ typedef ParallelIIEstimator ParallelEstimator;
+ Splitter splitter( ii_split_axis );
+ ParallelEstimator ii_estimator( splitter, ii_thread_number, functor );
+ ii_estimator.attach( K, shape );
+ ii_estimator.setParams( r );
+ ii_estimator.init( h, surfels.begin(), surfels.end() );
+ ii_estimator.eval( surfels.begin(), surfels.end(),
+ std::back_inserter( n_estimations ) );
+ }
+#else
+ if ( ( ii_thread_number != 1 ) && ( verbose > 0 ) )
+ trace.warning() << "- II normal requested parallel execution but DGtal was built without OpenMP; "
+ << "falling back to the sequential estimator."
+ << std::endl;
+#endif
+ if ( ! use_parallel )
+ {
+ IINormalEstimator ii_estimator( functor );
+ ii_estimator.attach( K, shape );
+ ii_estimator.setParams( r );
+ ii_estimator.init( h, surfels.begin(), surfels.end() );
+ ii_estimator.eval( surfels.begin(), surfels.end(),
+ std::back_inserter( n_estimations ) );
+ }
const RealVectors n_trivial = getTrivialNormalVectors( K, surfels );
orientVectors( n_estimations, n_trivial );
return n_estimations;
@@ -1307,6 +1367,12 @@ namespace DGtal
/// - verbose [ 1]: verbose trace mode 0: silent, 1: verbose.
/// - r-radius [ 3.0]: the constant for kernel radius parameter r in r(h)=r h^alpha (VCM,II,Trivial).
/// - alpha [ 0.33]: the parameter alpha in r(h)=r h^alpha (VCM, II)."
+ /// - ii-thread-number[ 1]: number of threads for II curvature estimators;
+ /// 1 keeps the sequential estimator, any other
+ /// value requests the parallel estimator when
+ /// OpenMP support is available.
+ /// - ii-split-axis [ 0]: main axis used by AxisDomainSplitter
+ /// for parallel II estimators.
/// - gridstep [ 1.0]: the digitization gridstep (often denoted by h).
///
/// @return the vector containing the estimated mean curvatures, in the
@@ -1338,6 +1404,12 @@ namespace DGtal
/// - verbose [ 1]: verbose trace mode 0: silent, 1: verbose.
/// - r-radius [ 3.0]: the constant for kernel radius parameter r in r(h)=r h^alpha (VCM,II,Trivial).
/// - alpha [ 0.33]: the parameter alpha in r(h)=r h^alpha (VCM, II)."
+ /// - ii-thread-number[ 1]: number of threads for II curvature estimators;
+ /// 1 keeps the sequential estimator, any other
+ /// value requests the parallel estimator when
+ /// OpenMP support is available.
+ /// - ii-split-axis [ 0]: main axis used by AxisDomainSplitter
+ /// for parallel II estimators.
/// - gridstep [ 1.0]: the digitization gridstep (often denoted by h).
/// - minAABB [ -10.0]: the min value of the AABB bounding box (domain)
/// - maxAABB [ 10.0]: the max value of the AABB bounding box (domain)
@@ -1377,6 +1449,12 @@ namespace DGtal
/// - verbose [ 1]: verbose trace mode 0: silent, 1: verbose.
/// - r-radius [ 3.0]: the constant for kernel radius parameter r in r(h)=r h^alpha (VCM,II,Trivial).
/// - alpha [ 0.33]: the parameter alpha in r(h)=r h^alpha (VCM, II)."
+ /// - ii-thread-number[ 1]: number of threads for II curvature estimators;
+ /// 1 keeps the sequential estimator, any other
+ /// value requests the parallel estimator when
+ /// OpenMP support is available.
+ /// - ii-split-axis [ 0]: main axis used by AxisDomainSplitter
+ /// for parallel II estimators.
/// - gridstep [ 1.0]: the digitization gridstep (often denoted by h).
///
/// @return the vector containing the estimated mean curvatures, in the
@@ -1396,28 +1474,8 @@ namespace DGtal
typedef functors::IIMeanCurvature3DFunctor IIMeanCurvFunctor;
typedef IntegralInvariantVolumeEstimator
IIMeanCurvEstimator;
-
- Scalars mc_estimations;
- int verbose = params[ "verbose" ].as();
- Scalar h = params[ "gridstep" ].as();
- Scalar r = params[ "r-radius" ].as();
- Scalar alpha = params[ "alpha" ].as();
- if ( alpha != 1.0 ) r *= pow( h, alpha-1.0 );
- if ( verbose > 0 )
- {
- trace.info() << "- II mean curvature alpha=" << alpha << std::endl;
- trace.info() << "- II mean curvature r=" << (r*h) << " (continuous) "
- << r << " (discrete)" << std::endl;
- }
- IIMeanCurvFunctor functor;
- functor.init( h, r*h );
- IIMeanCurvEstimator ii_estimator( functor );
- ii_estimator.attach( K, shape );
- ii_estimator.setParams( r );
- ii_estimator.init( h, surfels.begin(), surfels.end() );
- ii_estimator.eval( surfels.begin(), surfels.end(),
- std::back_inserter( mc_estimations ) );
- return mc_estimations;
+ return getIICurvatureEstimation
+ ( "mean curvature", shape, K, surfels, params );
}
/// Given a digital shape \a bimage, a sequence of \a surfels,
@@ -1431,6 +1489,12 @@ namespace DGtal
/// - verbose [ 1]: verbose trace mode 0: silent, 1: verbose.
/// - r-radius [ 3.0]: the constant for kernel radius parameter r in r(h)=r h^alpha (VCM,II,Trivial).
/// - alpha [ 0.33]: the parameter alpha in r(h)=r h^alpha (VCM, II)."
+ /// - ii-thread-number[ 1]: number of threads for II curvature estimators;
+ /// 1 keeps the sequential estimator, any other
+ /// value requests the parallel estimator when
+ /// OpenMP support is available.
+ /// - ii-split-axis [ 0]: main axis used by AxisDomainSplitter
+ /// for parallel II estimators.
/// - gridstep [ 1.0]: the digitization gridstep (often denoted by h).
///
/// @return the vector containing the estimated Gaussian curvatures, in the
@@ -1462,6 +1526,12 @@ namespace DGtal
/// - verbose [ 1]: verbose trace mode 0: silent, 1: verbose.
/// - r-radius [ 3.0]: the constant for kernel radius parameter r in r(h)=r h^alpha (VCM,II,Trivial).
/// - alpha [ 0.33]: the parameter alpha in r(h)=r h^alpha (VCM, II)."
+ /// - ii-thread-number[ 1]: number of threads for II curvature estimators;
+ /// 1 keeps the sequential estimator, any other
+ /// value requests the parallel estimator when
+ /// OpenMP support is available.
+ /// - ii-split-axis [ 0]: main axis used by AxisDomainSplitter
+ /// for parallel II estimators.
/// - gridstep [ 1.0]: the digitization gridstep (often denoted by h).
/// - minAABB [ -10.0]: the min value of the AABB bounding box (domain)
/// - maxAABB [ 10.0]: the max value of the AABB bounding box (domain)
@@ -1520,28 +1590,8 @@ namespace DGtal
typedef functors::IIGaussianCurvature3DFunctor IIGaussianCurvFunctor;
typedef IntegralInvariantCovarianceEstimator
IIGaussianCurvEstimator;
-
- Scalars mc_estimations;
- int verbose = params[ "verbose" ].as();
- Scalar h = params[ "gridstep" ].as();
- Scalar r = params[ "r-radius" ].as();
- Scalar alpha = params[ "alpha" ].as();
- if ( alpha != 1.0 ) r *= pow( h, alpha-1.0 );
- if ( verbose > 0 )
- {
- trace.info() << "- II Gaussian curvature alpha=" << alpha << std::endl;
- trace.info() << "- II Gaussian curvature r=" << (r*h) << " (continuous) "
- << r << " (discrete)" << std::endl;
- }
- IIGaussianCurvFunctor functor;
- functor.init( h, r*h );
- IIGaussianCurvEstimator ii_estimator( functor );
- ii_estimator.attach( K, shape );
- ii_estimator.setParams( r );
- ii_estimator.init( h, surfels.begin(), surfels.end() );
- ii_estimator.eval( surfels.begin(), surfels.end(),
- std::back_inserter( mc_estimations ) );
- return mc_estimations;
+ return getIICurvatureEstimation
+ ( "Gaussian curvature", shape, K, surfels, params );
}
/// Given a digital shape \a bimage, a sequence of \a surfels,
@@ -1556,6 +1606,12 @@ namespace DGtal
/// - verbose [ 1]: verbose trace mode 0: silent, 1: verbose.
/// - r-radius [ 3.0]: the constant for kernel radius parameter r in r(h)=r h^alpha (VCM,II,Trivial).
/// - alpha [ 0.33]: the parameter alpha in r(h)=r h^alpha (VCM, II)."
+ /// - ii-thread-number[ 1]: number of threads for II curvature estimators;
+ /// 1 keeps the sequential estimator, any other
+ /// value requests the parallel estimator when
+ /// OpenMP support is available.
+ /// - ii-split-axis [ 0]: main axis used by AxisDomainSplitter
+ /// for parallel II estimators.
/// - gridstep [ 1.0]: the digitization gridstep (often denoted by h).
///
/// @return the vector containing the estimated Gaussian curvatures, in the
@@ -1588,6 +1644,12 @@ namespace DGtal
/// - verbose [ 1]: verbose trace mode 0: silent, 1: verbose.
/// - r-radius [ 3.0]: the constant for kernel radius parameter r in r(h)=r h^alpha (VCM,II,Trivial).
/// - alpha [ 0.33]: the parameter alpha in r(h)=r h^alpha (VCM, II)."
+ /// - ii-thread-number[ 1]: number of threads for II curvature estimators;
+ /// 1 keeps the sequential estimator, any other
+ /// value requests the parallel estimator when
+ /// OpenMP support is available.
+ /// - ii-split-axis [ 0]: main axis used by AxisDomainSplitter
+ /// for parallel II estimators.
/// - gridstep [ 1.0]: the digitization gridstep (often denoted by h).
/// - minAABB [ -10.0]: the min value of the AABB bounding box (domain)
/// - maxAABB [ 10.0]: the max value of the AABB bounding box (domain)
@@ -1627,6 +1689,12 @@ namespace DGtal
/// - verbose [ 1]: verbose trace mode 0: silent, 1: verbose.
/// - r-radius [ 3.0]: the constant for kernel radius parameter r in r(h)=r h^alpha (VCM,II,Trivial).;
/// - alpha [ 0.33]: the parameter alpha in r(h)=r h^alpha (VCM, II)."
+ /// - ii-thread-number[ 1]: number of threads for II curvature estimators;
+ /// 1 keeps the sequential estimator, any other
+ /// value requests the parallel estimator when
+ /// OpenMP support is available.
+ /// - ii-split-axis [ 0]: main axis used by AxisDomainSplitter
+ /// for parallel II estimators.
/// - gridstep [ 1.0]: the digitization gridstep (often denoted by h).
///
/// @return the vector containing the estimated principal curvatures and directions,
@@ -1644,29 +1712,9 @@ namespace DGtal
| parametersKSpace() )
{
typedef functors::IIPrincipalCurvaturesAndDirectionsFunctor IICurvFunctor;
- typedef IntegralInvariantCovarianceEstimator IICurvEstimator;
-
- CurvatureTensorQuantities mc_estimations;
- int verbose = params[ "verbose" ].as();
- Scalar h = params[ "gridstep" ].as();
- Scalar r = params[ "r-radius" ].as();
- Scalar alpha = params[ "alpha" ].as();
- if ( alpha != 1.0 ) r *= pow( h, alpha-1.0 );
- if ( verbose > 0 )
- {
- trace.info() << "- II principal curvatures and directions alpha=" << alpha << std::endl;
- trace.info() << "- II principal curvatures and directions r=" << (r*h) << " (continuous) "
- << r << " (discrete)" << std::endl;
- }
- IICurvFunctor functor;
- functor.init( h, r*h );
- IICurvEstimator ii_estimator( functor );
- ii_estimator.attach( K, shape );
- ii_estimator.setParams( r );
- ii_estimator.init( h, surfels.begin(), surfels.end() );
- ii_estimator.eval( surfels.begin(), surfels.end(),
- std::back_inserter( mc_estimations ) );
- return mc_estimations;
+ typedef IntegralInvariantCovarianceEstimator IICurvEstimator;
+ return getIICurvatureEstimation
+ ( "principal curvatures and directions", shape, K, surfels, params );
}
/// @}
@@ -2478,12 +2526,82 @@ namespace DGtal
// ------------------------- Private Data --------------------------------
private:
+ template
+ static std::vector
+ getIICurvatureEstimation( const char* description,
+ const TPointPredicate& shape,
+ const KSpace& K,
+ const SurfelRange& surfels,
+ const Parameters& params )
+ {
+ using Quantities = std::vector;
+ Quantities estimations;
+ int verbose = params[ "verbose" ].as();
+ int ii_thread_number = params[ "ii-thread-number" ].as();
+ auto ii_split_axis = getIIParallelSplitAxis( params );
+ Scalar h = params[ "gridstep" ].as();
+ Scalar r = params[ "r-radius" ].as();
+ Scalar alpha = params[ "alpha" ].as();
+ if ( alpha != 1.0 ) r *= pow( h, alpha-1.0 );
+ if ( verbose > 0 )
+ {
+ trace.info() << "- II " << description << " alpha=" << alpha << std::endl;
+ trace.info() << "- II " << description << " r=" << (r*h) << " (continuous) "
+ << r << " (discrete)" << std::endl;
+ }
+ TFunctor functor;
+ functor.init( h, r*h );
+#ifdef DGTAL_WITH_OPENMP
+ if ( ii_thread_number != 1 )
+ {
+ if ( verbose > 0 )
+ trace.info() << "- II " << description
+ << " uses ParallelIIEstimator with thread request="
+ << ii_thread_number << " and split axis="
+ << ii_split_axis << std::endl;
+ typedef AxisDomainSplitter Splitter;
+ typedef ParallelIIEstimator ParallelEstimator;
+ Splitter splitter( ii_split_axis );
+ ParallelEstimator ii_estimator( splitter, ii_thread_number, functor );
+ ii_estimator.attach( K, shape );
+ ii_estimator.setParams( r );
+ ii_estimator.init( h, surfels.begin(), surfels.end() );
+ ii_estimator.eval( surfels.begin(), surfels.end(),
+ std::back_inserter( estimations ) );
+ return estimations;
+ }
+#else
+ if ( ( ii_thread_number != 1 ) && ( verbose > 0 ) )
+ trace.warning() << "- II " << description
+ << " requested parallel execution but DGtal was built without OpenMP; "
+ << "falling back to the sequential estimator."
+ << std::endl;
+#endif
+ TEstimator ii_estimator( functor );
+ ii_estimator.attach( K, shape );
+ ii_estimator.setParams( r );
+ ii_estimator.init( h, surfels.begin(), surfels.end() );
+ ii_estimator.eval( surfels.begin(), surfels.end(),
+ std::back_inserter( estimations ) );
+ return estimations;
+ }
+
// ------------------------- Hidden services ------------------------------
protected:
// ------------------------- Internals ------------------------------------
private:
+ static typename Domain::Dimension
+ getIIParallelSplitAxis( const Parameters& params )
+ {
+ const auto requested_axis = params[ "ii-split-axis" ].as();
+ if ( requested_axis <= 0 ) return 0;
+ if ( requested_axis >= static_cast( Domain::dimension ) )
+ return static_cast( Domain::dimension - 1 );
+ return static_cast( requested_axis );
+ }
+
}; // end of class ShortcutsGeometry
diff --git a/src/DGtal/kernel/domains/DomainSplitter.h b/src/DGtal/kernel/domains/DomainSplitter.h
new file mode 100644
index 0000000000..94d366c780
--- /dev/null
+++ b/src/DGtal/kernel/domains/DomainSplitter.h
@@ -0,0 +1,185 @@
+/**
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ **/
+
+#pragma once
+
+/**
+ * @file DomainSplitter.h
+ * @author Bastien DOIGNIES (\c bastien.doignies@liris.cnrs.fr )
+ * LIRIS (CNRS, UMR 5205), University of Lyon, France
+ *
+ * @date 2026/04/29
+ *
+ * This file is part of the DGtal library.
+ */
+
+#include "DGtal/kernel/domains/CDomain.h"
+#include
+
+namespace DGtal
+{
+ /**
+ * @brief Data structure returned by Domain splitters
+ *
+ * This class offers the ability for the splitter to give information
+ * about the surfaces or object within the splits.
+ * For example, the hintVoxelCount field may allow to pre-allocate buffers.
+ */
+ template
+ struct SplitInfo
+ {
+ BOOST_CONCEPT_ASSERT(( concepts::CDomain< Domain > ));
+
+ Domain domain; //< The actual split domain
+ uint32_t hintVoxelCount = 0; //< An expected guess for the number of voxel
+ };
+
+ /**
+ * @brief Splits a domain evenly along all dimensions
+ *
+ * @tparam The model of domain to split
+ */
+ template
+ struct RegularDomainSplitter
+ {
+ BOOST_CONCEPT_ASSERT(( concepts::CDomain< Domain > ));
+
+ //Output spllitted domain type
+ typedef std::vector> SplitDomainsInfo;
+
+ /**
+ * @brief Splits a domain
+ *
+ * This functions may split the domain in fewer part than wanted to ensure
+ * even subdomains.
+ *
+ * @param d The domain to split
+ * @param splitHint The targeted number of splits
+ */
+ SplitDomainsInfo operator()(const Domain& d, uint32_t splitHint) const
+ {
+ // Find best match possible for even splitting
+ const uint32_t splitCount = std::floor(std::log(splitHint) / std::log(Domain::dimension));
+ const uint32_t totalSplits = std::pow(splitCount, Domain::dimension);
+
+ if (splitCount == 0)
+ return { SplitInfo{d, 0} };
+
+ auto splitSize = (d.upperBound() - d.lowerBound()) / (int32_t)splitCount;
+ SplitDomainsInfo result;
+ result.reserve(totalSplits);
+
+ for (uint32_t i = 0; i < totalSplits; ++i)
+ {
+ auto start = d.lowerBound();
+ auto idx = i;
+ for (uint32_t j = 0; j < Domain::dimension; ++j)
+ {
+ auto k = idx % splitCount;
+ start[j] += k * splitSize[j] + k; // +k ensure no overlap between domains
+ idx /= splitCount;
+ }
+
+ // Make correction to ensure it remains within the domain
+ auto end = start + splitSize;
+ for (uint32_t j = 0; j < Domain::dimension; ++j)
+ end[j] = std::clamp(end[j], d.lowerBound()[j], d.upperBound()[j]);
+
+ result.emplace_back(Domain(start, end), 0);
+ }
+
+ return result;
+ };
+ };
+
+
+ /**
+ * @brief Splits a domain along one of the domain grid axis.
+ *
+ * @tparam The model of domain to split
+ */
+ template
+ struct AxisDomainSplitter
+ {
+
+ BOOST_CONCEPT_ASSERT(( concepts::CDomain< Domain > ));
+
+ //Output spllitted domain type
+ typedef std::vector> SplitDomainsInfo;
+ typedef typename Domain::Dimension Dimension;
+
+ /// The axis used for the default split operator.
+ Dimension axis;
+
+ /**
+ * @brief Constructor
+ *
+ * @param dim the split axis (default: 0)
+ */
+ explicit AxisDomainSplitter( Dimension dim = 0 )
+ : axis( dim )
+ {}
+
+ /**
+ * @brief Regularly splits a domain along one axis
+ *
+ * @param d The domain to split
+ * @param splitHint The targeted number of splits (clamped to the width of the domain)
+ */
+ SplitDomainsInfo operator()( const Domain& d, uint32_t splitHint ) const
+ {
+ return (*this)( d, splitHint, axis );
+ }
+
+ /**
+ * @brief Regularly splits a domain along one axis
+ *
+ * @param d The domain to split
+ * @param splitHint The targeted number of splits (clamped to the width of the domain)
+ * @param dim the split axis (default: 0)
+ */
+ SplitDomainsInfo operator()(const Domain& d, uint32_t splitHint, Dimension dim) const
+ {
+ SplitDomainsInfo result;
+ if (splitHint == 0)
+ return result;
+
+ auto lower = d.lowerBound();
+ auto upper = d.upperBound();
+ auto length = upper[dim] - lower[dim] + 1;
+ uint32_t splitCount = splitHint;
+ if (splitCount > length)
+ splitCount = length;
+
+ result.reserve(splitCount);
+ auto base = length / splitCount;
+ auto rem = length % splitCount;
+
+ auto start = lower;
+ for (uint32_t i = 0; i < splitCount; ++i)
+ {
+ auto size = base + (i < rem ? 1 : 0);
+ auto end = upper;
+ end[dim] = start[dim] + size - 1;
+ result.emplace_back(Domain(start, end), 0);
+ start[dim] = end[dim] + 1;
+ }
+
+ return result;
+ };
+ };
+
+}
diff --git a/tests/geometry/surfaces/CMakeLists.txt b/tests/geometry/surfaces/CMakeLists.txt
index 50d555053f..95b7f92c36 100644
--- a/tests/geometry/surfaces/CMakeLists.txt
+++ b/tests/geometry/surfaces/CMakeLists.txt
@@ -1,16 +1,9 @@
-set(TESTS_SRC
- testArithmeticalDSSComputerOnSurfels
+set(TESTS_SURFACES_SRC
+ testArithmeticalDSSComputerOnSurfels
testChordGenericStandardPlaneComputer
testDigitalPlanePredicate
testPlaneProbingTetrahedronEstimator
testPlaneProbingParallelepipedEstimator
- )
-
-foreach(FILE ${TESTS_SRC})
- DGtal_add_test(${FILE})
-endforeach()
-
-set(TESTS_SURFACES_SRC
testIntegralInvariantShortcuts
testNormalVectorEstimatorEmbedder
testIntegralInvariantVolumeEstimator
@@ -28,8 +21,12 @@ foreach(FILE ${TESTS_SURFACES_SRC})
DGtal_add_test(${FILE})
endforeach()
+if ( DGTAL_WITH_OPENMP )
+ DGtal_add_test(testParallelIntegralInvariantEstimator)
+endif()
+
-if ( DGTAL_WITH_CGAL )
+if ( DGTAL_WITH_CGAL )
set(CGAL_TESTS_SRC
testMonge )
foreach(FILE ${CGAL_TESTS_SRC})
@@ -38,7 +35,7 @@ if ( DGTAL_WITH_CGAL )
endif()
-if ( DGTAL_WITH_POLYSCOPE_VIEWER )
+if ( DGTAL_WITH_POLYSCOPE_VIEWER )
set(POLYSCOPE_VIEWER_TESTS_SRC
testLocalConvolutionNormalVectorEstimator
testTensorVotingViewer)
@@ -48,8 +45,6 @@ if ( DGTAL_WITH_POLYSCOPE_VIEWER )
endforeach()
endif()
-
-
if ( DGTAL_WITH_PONCA )
set(PONCA_TESTS_SRC
testSphereFitting )
diff --git a/tests/geometry/surfaces/testIntegralInvariantShortcuts.cpp b/tests/geometry/surfaces/testIntegralInvariantShortcuts.cpp
index a5aa693b7b..c555cd6c1a 100644
--- a/tests/geometry/surfaces/testIntegralInvariantShortcuts.cpp
+++ b/tests/geometry/surfaces/testIntegralInvariantShortcuts.cpp
@@ -68,8 +68,17 @@ TEST_CASE( "Testing IntegralInvariant Shortcuts API" )
params("r-radius", 3.0);
//We compute the curvature tensor, the mean and the Gaussian curvature
+ auto Hcurv = SHG3::getIIMeanCurvatures( binary_image, surfels, params );
auto Tcurv = SHG3::getIIPrincipalCurvaturesAndDirections(binary_image, surfels, params);
auto Kcurv = SHG3::getIIGaussianCurvatures( binary_image, surfels, params);
+ auto Ncurv = SHG3::getIINormalVectors( binary_image, surfels, params );
+
+ auto params_parallel = params;
+ params_parallel( "ii-thread-number", 4 )( "ii-split-axis", 2 );
+ auto HcurvParallel = SHG3::getIIMeanCurvatures( binary_image, surfels, params_parallel );
+ auto TcurvParallel = SHG3::getIIPrincipalCurvaturesAndDirections( binary_image, surfels, params_parallel );
+ auto KcurvParallel = SHG3::getIIGaussianCurvatures( binary_image, surfels, params_parallel );
+ auto NcurvParallel = SHG3::getIINormalVectors( binary_image, surfels, params_parallel );
std::vector k1,k2,G;
for(auto &result: Tcurv)
@@ -85,6 +94,45 @@ TEST_CASE( "Testing IntegralInvariant Shortcuts API" )
REQUIRE( Kcurv[i] == Approx( G[i] ) );
}
+ SECTION("Testing that requesting the parallel II shortcut preserves curvature values")
+ {
+ REQUIRE( HcurvParallel.size() == Hcurv.size() );
+ REQUIRE( KcurvParallel.size() == Kcurv.size() );
+ REQUIRE( TcurvParallel.size() == Tcurv.size() );
+
+ for ( std::size_t i = 0; i < Hcurv.size(); ++i )
+ REQUIRE( HcurvParallel[ i ] == Approx( Hcurv[ i ] ) );
+
+ for ( std::size_t i = 0; i < Kcurv.size(); ++i )
+ REQUIRE( KcurvParallel[ i ] == Approx( Kcurv[ i ] ) );
+
+ for ( std::size_t i = 0; i < Tcurv.size(); ++i )
+ {
+ REQUIRE( std::get<0>( TcurvParallel[ i ] ) == Approx( std::get<0>( Tcurv[ i ] ) ) );
+ REQUIRE( std::get<1>( TcurvParallel[ i ] ) == Approx( std::get<1>( Tcurv[ i ] ) ) );
+ }
+ }
+
+ SECTION("Testing that requesting the parallel II shortcut preserves normal vectors")
+ {
+ REQUIRE( NcurvParallel.size() == Ncurv.size() );
+
+ for ( std::size_t i = 0; i < Ncurv.size(); ++i )
+ for ( std::size_t d = 0; d < 3; ++d )
+ REQUIRE( NcurvParallel[ i ][ d ] == Approx( Ncurv[ i ][ d ] ) );
+ }
+
+ SECTION("Testing that ii-split-axis accepts out-of-range values by clamping to a valid axis")
+ {
+ auto params_parallel_clamped = params;
+ params_parallel_clamped( "ii-thread-number", 4 )( "ii-split-axis", 9 );
+ auto HcurvParallelClamped = SHG3::getIIMeanCurvatures( binary_image, surfels, params_parallel_clamped );
+
+ REQUIRE( HcurvParallelClamped.size() == Hcurv.size() );
+ for ( std::size_t i = 0; i < Hcurv.size(); ++i )
+ REQUIRE( HcurvParallelClamped[ i ] == Approx( Hcurv[ i ] ) );
+ }
+
SECTION("Testing on shifted domains")
{
auto SHIFT=512;
diff --git a/tests/geometry/surfaces/testParallelIntegralInvariantEstimator.cpp b/tests/geometry/surfaces/testParallelIntegralInvariantEstimator.cpp
new file mode 100644
index 0000000000..02febc80db
--- /dev/null
+++ b/tests/geometry/surfaces/testParallelIntegralInvariantEstimator.cpp
@@ -0,0 +1,185 @@
+/**
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ **/
+
+/**
+ * @file testIntegralInvariantVolumeEstimator.cpp
+ * @ingroup Tests
+ * @author Bastien DOIGNIES (\c bastien.doignies@liris.cnrs.fr )
+ * Laboratoire d'InfoRmatique en Image et Systèmes d'information - LIRIS (CNRS, UMR 5205), INSA-Lyon, France
+ * LAboratoire de MAthématiques - LAMA (CNRS, UMR 5127), Université de Savoie, France
+ *
+ * @date 2014/06/26
+ *
+ * Functions for testing class IntegralInvariantVolumeEstimator and IIGeometricFunctor.
+ *
+ * This file is part of the DGtal library.
+ */
+
+///////////////////////////////////////////////////////////////////////////////
+#include
+#include
+#include "DGtal/base/Common.h"
+
+/// Shape
+#include "DGtal/shapes/implicit/ImplicitBall.h"
+
+/// Digitization
+#include "DGtal/shapes/GaussDigitizer.h"
+#include "DGtal/topology/LightImplicitDigitalSurface.h"
+#include "DGtal/topology/DigitalSurface.h"
+#include "DGtal/graph/DepthFirstVisitor.h"
+#include "DGtal/graph/GraphVisitorRange.h"
+
+/// Estimator
+#include "DGtal/geometry/surfaces/estimation/IIGeometricFunctors.h"
+#include "DGtal/geometry/surfaces/estimation/IntegralInvariantVolumeEstimator.h"
+#include "DGtal/geometry/surfaces/estimation/ParallelIIEstimator.h"
+#include "DGtal/kernel/domains/DomainSplitter.h"
+
+
+///////////////////////////////////////////////////////////////////////////////
+
+
+using namespace DGtal;
+
+///////////////////////////////////////////////////////////////////////////////
+// Functions for testing class IntegralInvariantVolumeEstimator and IIGeometricFunctor.
+///////////////////////////////////////////////////////////////////////////////
+
+bool testCurvature2dP ( double h )
+{
+ typedef ImplicitBall ImplicitShape;
+ typedef GaussDigitizer DigitalShape;
+ typedef LightImplicitDigitalSurface Boundary;
+ typedef DigitalSurface< Boundary > MyDigitalSurface;
+ typedef DepthFirstVisitor< MyDigitalSurface > Visitor;
+ typedef GraphVisitorRange< Visitor > VisitorRange;
+ typedef VisitorRange::ConstIterator VisitorConstIterator;
+
+ typedef functors::IICurvatureFunctor MyIICurvatureFunctor;
+ typedef IntegralInvariantVolumeEstimator< Z2i::KSpace, DigitalShape, MyIICurvatureFunctor > MyIICurvatureEstimator;
+
+ //! [exampleParallelII-type]
+ typedef RegularDomainSplitter> Splitter;
+ typedef ParallelIIEstimator MyIICurvatureEstimatorP;
+ //! [exampleParallelII-type]
+
+ typedef MyIICurvatureEstimator::Quantity Value;
+
+ static_assert(std::is_same_v);
+
+ double re = 10;
+ double radius = 15;
+
+ trace.beginBlock( "[PARALLEL] Shape initialisation ..." );
+
+ ImplicitShape ishape( Z2i::RealPoint( 0, 0 ), radius );
+ DigitalShape dshape;
+ dshape.attach( ishape );
+ dshape.init( Z2i::RealPoint( -20.0, -20.0 ), Z2i::RealPoint( 20.0, 20.0 ), h );
+
+ Z2i::KSpace K;
+ if ( !K.init( dshape.getLowerBound(), dshape.getUpperBound(), true ) )
+ {
+ trace.error() << "Problem with Khalimsky space" << std::endl;
+ return false;
+ }
+
+ Z2i::KSpace::Surfel bel = Surfaces::findABel( K, dshape, 10000 );
+ Boundary boundary( K, dshape, SurfelAdjacency( true ), bel );
+ MyDigitalSurface surf ( boundary );
+
+ trace.endBlock();
+
+ trace.beginBlock( "Curvature estimator initialisation ...");
+
+ // Visitor ranges are typically unique and single pass. We need
+ // to create one for each estimator in this case
+ VisitorRange range( new Visitor( surf, *surf.begin() ));
+ VisitorConstIterator ibegin = range.begin();
+ VisitorConstIterator iend = range.end();
+ // Parallel iterations
+ VisitorRange rangeP( new Visitor( surf, *surf.begin() ));
+ VisitorConstIterator ibeginP = rangeP.begin();
+ VisitorConstIterator iendP = rangeP.end();
+
+ MyIICurvatureFunctor curvatureFunctor;
+ curvatureFunctor.init( h, re );
+
+ //! [exampleParallelII-construction]
+ MyIICurvatureEstimator curvatureEstimator ( curvatureFunctor);
+ curvatureEstimator.attach( K, dshape );
+ curvatureEstimator.setParams( re/h );
+ curvatureEstimator.init( h, ibegin, iend );
+
+ // Parallel version expects a domain splitter instance and a number of
+ // threads. Subsequent arguments are forwarded to the underlying estimator.
+ Splitter splitter;
+ MyIICurvatureEstimatorP curvatureEstimatorP( splitter, 4, curvatureFunctor );
+ curvatureEstimatorP.attach( K, dshape );
+ curvatureEstimatorP.setParams( re/h );
+ curvatureEstimatorP.init( h, ibeginP, iendP );
+ //! [exampleParallelII-construction]
+
+ trace.endBlock();
+
+ trace.beginBlock( "Curvature estimator evaluation ...");
+
+ std::vector< Value > results, resultsP;
+ std::back_insert_iterator< std::vector< Value > > resultsIt ( results );
+ std::back_insert_iterator< std::vector< Value > > resultsItP( resultsP );
+
+ curvatureEstimator .eval( ibegin , iend , resultsIt );
+ curvatureEstimatorP.eval( ibeginP, iendP, resultsItP );
+
+ trace.endBlock();
+
+ trace.beginBlock ( "Comparing results of integral invariant 2D curvature ..." );
+
+ unsigned int rsize = results.size();
+ unsigned int rsizeP = resultsP.size();
+
+ if (rsize != rsizeP)
+ {
+ trace.error() << "Size mismatch between parallel and non-parallel versions: " << rsize << " / " << rsizeP;
+ trace.endBlock();
+ return false;
+ }
+
+ for ( unsigned int i = 0; i < rsize; ++i )
+ {
+ if (std::abs(results[i] - resultsP[i]) >= 1e-2)
+ {
+ trace.error() << "Result mismatch between parallel and non-parallel versions at voxel " << i << ": " << results[i] << " / " << resultsP[i] << "\n";
+ trace.endBlock();
+ return false;
+ }
+ }
+ trace.endBlock();
+ return true;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Standard services - public :
+int main( int /*argc*/, char** /*argv*/ )
+{
+ trace.beginBlock ( "Testing class ParrallelIIEstimator with IntegralInvariantVolumeEstimator in 2d" );
+
+ bool res = testCurvature2dP( 0.05 );
+ trace.emphase() << ( res ? "Passed." : "Error." ) << std::endl;
+ trace.endBlock();
+ return res ? 0 : 1;
+}
diff --git a/tests/kernel/CMakeLists.txt b/tests/kernel/CMakeLists.txt
index b3a6c10a44..a27dc318a2 100644
--- a/tests/kernel/CMakeLists.txt
+++ b/tests/kernel/CMakeLists.txt
@@ -18,6 +18,7 @@ set(DGTAL_TESTS_SRC_KERNEL
testLatticeSetByIntervals
testDGtalBigInteger
testDigitalSetByOctree
+ testDomainSplitter
)
diff --git a/tests/kernel/testDomainSplitter.cpp b/tests/kernel/testDomainSplitter.cpp
new file mode 100644
index 0000000000..dbe4d18a66
--- /dev/null
+++ b/tests/kernel/testDomainSplitter.cpp
@@ -0,0 +1,116 @@
+/**
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ **/
+
+/**
+ * @file testDomainSplitter.cpp
+ * @ingroup Tests
+ * @author David Coeurjolly (\c david.coeurjolly@cnrs.fr)
+ * Laboratoire d'InfoRmatique en Image et Systèmes d'information - LIRIS (CNRS, UMR 5205), INSA-Lyon, France
+ *
+ * @date 2026/04/03
+ *
+ * Functions for testing class DomainSplitter
+ *
+ * This file is part of the DGtal library.
+ */
+
+///////////////////////////////////////////////////////////////////////////////
+#include
+#include "DGtal/base/Common.h"
+#include "DGtalCatch.h"
+
+#include "DGtal/helpers/StdDefs.h"
+#include "DGtal/kernel/domains/DomainSplitter.h"
+
+#ifdef DGTAL_TESTS_WITH_VIEWER
+#ifdef DGTAL_WITH_POLYSCOPE_VIEWER
+#include "DGtal/io/colormaps/HueShadeColorMap.h"
+#include "DGtal/io/viewers/PolyscopeViewer.h"
+#endif
+#endif
+
+///////////////////////////////////////////////////////////////////////////////
+
+using namespace DGtal;
+using namespace Z3i;
+
+TEST_CASE( "Domain Regular Grid Splitter tests" )
+{
+ Domain domain(Point(0,0,0), Point(16,32,64));
+
+ RegularDomainSplitter splitter;
+
+ RegularDomainSplitter::SplitDomainsInfo output = splitter(domain,12);
+ REQUIRE( output.size() <= 12);
+
+ trace.info() << "Original domain: "< cmap(0,(unsigned int)output.size());
+ for(auto i=0; i< output.size(); ++i)
+ {
+ viewer << cmap(i);
+ viewer << output[i].domain;
+ }
+ viewer.show();
+#endif
+#endif
+}
+
+TEST_CASE( "Domain Axis Splitter tests" )
+{
+ Domain domain(Point(0,0,0), Point(16,32,64));
+
+ AxisDomainSplitter splitter;
+
+ AxisDomainSplitter::SplitDomainsInfo output = splitter(domain,3,0);
+ REQUIRE( output.size() == 3);
+
+ trace.info() << "Original domain: "< cmap(0,(unsigned int)output.size());
+ for(auto i=0; i< output.size(); ++i)
+ {
+ viewer << cmap(i);
+ viewer << output[i].domain;
+ }
+ viewer.show();
+#endif
+#endif
+}
+
+TEST_CASE( "Domain Axis Splitter tests (another direction)" )
+{
+ Domain domain(Point(10,10,10), Point(16,32,64));
+
+ AxisDomainSplitter splitter;
+
+ AxisDomainSplitter::SplitDomainsInfo output = splitter(domain,2,1);
+ REQUIRE( output.size() == 2);
+
+ trace.info() << "Original domain: "< im, const SH3::SurfelRange& range,
+ const Parameters& params) {
+ return SHG3::getIIPrincipalCurvaturesAndDirections(im, range, params);
+ });
+ mg.def("getIIPrincipalCurvaturesAndDirections", [](
+ CountedPtr im, const SH3::SurfelRange& range,
+ const Parameters& params) {
+ return SHG3::getIIPrincipalCurvaturesAndDirections(im, range, params);
+ });
mg.def("getPositions", [](
CountedPtr shape, const SH3::KSpace& K,
const SH3::SurfelRange& surfels, const Parameters& params) {
diff --git a/wrap/helpers/helpers_init.cpp b/wrap/helpers/helpers_init.cpp
index c21d36ffd7..8b3b239cb6 100644
--- a/wrap/helpers/helpers_init.cpp
+++ b/wrap/helpers/helpers_init.cpp
@@ -36,6 +36,11 @@ void init_dgtal_helpers(py::module& m) {
auto m_helpers = m.def_submodule("helpers", "Submodule for DGtal helpers");
m_helpers.attr("SAMPLES_PATH") = py::str(std::string(DGTAL_PATH) + "/examples/samples/");
+#ifdef DGTAL_WITH_OPENMP
+ m_helpers.attr("DGTAL_WITH_OPENMP") = py::bool_(true);
+#else
+ m_helpers.attr("DGTAL_WITH_OPENMP") = py::bool_(false);
+#endif
// Bind parameter class
py::class_(m_helpers, "Parameters")
@@ -49,6 +54,14 @@ void init_dgtal_helpers(py::module& m) {
.def("set", [](Parameters& params, const std::string& name, std::string value) {
return params(name, ParameterValue(value));
}, py::return_value_policy::reference)
+ .def("count", &Parameters::count)
+ .def("__contains__", [](const Parameters& params, const std::string& name) {
+ return params.count(name);
+ })
+ .def("__or__", [](const Parameters& lhs, const Parameters& rhs) {
+ return lhs | rhs;
+ })
+ .def("isValid", &Parameters::isValid)
.def("__str__", [](const Parameters& params) {
std::stringstream ss;
params.selfDisplay(ss);
diff --git a/wrap/tests/CMakeLists.txt b/wrap/tests/CMakeLists.txt
index 4986298a55..c732b09e4c 100644
--- a/wrap/tests/CMakeLists.txt
+++ b/wrap/tests/CMakeLists.txt
@@ -7,3 +7,4 @@ add_subdirectory(kernel)
add_subdirectory(topology)
add_subdirectory(images)
add_subdirectory(io)
+add_subdirectory(helpers)
diff --git a/wrap/tests/helpers/CMakeLists.txt b/wrap/tests/helpers/CMakeLists.txt
new file mode 100644
index 0000000000..e0652f540b
--- /dev/null
+++ b/wrap/tests/helpers/CMakeLists.txt
@@ -0,0 +1,20 @@
+set(python_tests_
+ test_Shortcuts.py
+ )
+
+get_filename_component(module_name_ ${CMAKE_CURRENT_SOURCE_DIR} NAME)
+set(test_folder "${CMAKE_CURRENT_SOURCE_DIR}")
+# test files should start with "test_"
+# unittest functions (in .py) should start with "test_" for discover to work
+foreach(python_test ${python_tests_})
+ set(python_test_name_ python||${module_name_}||${python_test})
+ add_test(NAME ${python_test_name_}
+ COMMAND
+ ${PYTHON_EXECUTABLE}
+ -m pytest
+ ${pytest_options}
+ ${test_folder}/${python_test}
+ # Execute the tests from the right directory to allow `import dgtal` to work
+ WORKING_DIRECTORY "${CMAKE_BUILD_PYTHONLIBDIR}/.."
+ )
+endforeach()
diff --git a/wrap/tests/helpers/test_Shortcuts.py b/wrap/tests/helpers/test_Shortcuts.py
new file mode 100644
index 0000000000..6b01269690
--- /dev/null
+++ b/wrap/tests/helpers/test_Shortcuts.py
@@ -0,0 +1,82 @@
+import pytest
+
+from dgtal import SH3
+from dgtal import helpers
+
+
+def _make_surface_data():
+ params = SH3.defaultParameters()
+ params.set("verbose", 0)
+ bimage = SH3.makeBinaryImage(helpers.SAMPLES_PATH + "/Al.100.vol", params)
+ K = SH3.getKSpace(bimage, params)
+ surface = SH3.makeLightDigitalSurface(bimage, K, params)
+ surfels = SH3.getSurfelRange(surface, params)
+ return params, bimage, surfels
+
+
+def _make_parallel_params():
+ params = SH3.defaultParameters()
+ params.set("verbose", 0)
+ params.set("ii-thread-number", 4)
+ params.set("ii-split-axis", 2)
+ return params
+
+
+def test_geometry_estimation_parameters_expose_ii_controls():
+ ii_params = SH3.parametersGeometryEstimation()
+
+ assert "ii-thread-number" in ii_params
+ assert "ii-split-axis" in ii_params
+ assert ii_params.count("ii-thread-number") == 1
+ assert ii_params.count("ii-split-axis") == 1
+ assert ii_params.isValid()
+
+ merged = SH3.defaultParameters() | ii_params
+ merged.set("ii-thread-number", 2)
+ merged.set("ii-split-axis", 1)
+
+ assert "ii-thread-number" in merged
+ assert "ii-split-axis" in merged
+ assert helpers.DGTAL_WITH_OPENMP in (True, False)
+
+
+def test_ii_mean_gaussian_and_normal_estimators_accept_parallel_parameters():
+ params, bimage, surfels = _make_surface_data()
+ parallel_params = _make_parallel_params()
+
+ mean_curvatures = SH3.getIIMeanCurvatures(bimage, surfels, params)
+ mean_curvatures_parallel = SH3.getIIMeanCurvatures(bimage, surfels, parallel_params)
+ gaussian_curvatures = SH3.getIIGaussianCurvatures(bimage, surfels, params)
+ gaussian_curvatures_parallel = SH3.getIIGaussianCurvatures(bimage, surfels, parallel_params)
+ normal_vectors = SH3.getIINormalVectors(bimage, surfels, params)
+ normal_vectors_parallel = SH3.getIINormalVectors(bimage, surfels, parallel_params)
+
+ assert len(mean_curvatures_parallel) == len(mean_curvatures)
+ assert len(gaussian_curvatures_parallel) == len(gaussian_curvatures)
+ assert len(normal_vectors_parallel) == len(normal_vectors)
+
+ for parallel_value, sequential_value in zip(mean_curvatures_parallel, mean_curvatures):
+ assert parallel_value == pytest.approx(sequential_value)
+
+ for parallel_value, sequential_value in zip(gaussian_curvatures_parallel, gaussian_curvatures):
+ assert parallel_value == pytest.approx(sequential_value)
+
+ for parallel_vector, sequential_vector in zip(normal_vectors_parallel, normal_vectors):
+ for axis in range(3):
+ assert parallel_vector[axis] == pytest.approx(sequential_vector[axis])
+
+
+def test_ii_principal_curvatures_and_directions_are_bound():
+ params, bimage, surfels = _make_surface_data()
+ parallel_params = _make_parallel_params()
+
+ principal_curvatures = SH3.getIIPrincipalCurvaturesAndDirections(bimage, surfels, params)
+ principal_curvatures_parallel = SH3.getIIPrincipalCurvaturesAndDirections(
+ bimage, surfels, parallel_params
+ )
+
+ assert len(principal_curvatures_parallel) == len(principal_curvatures)
+
+ for parallel_value, sequential_value in zip(principal_curvatures_parallel, principal_curvatures):
+ assert parallel_value[0] == pytest.approx(sequential_value[0])
+ assert parallel_value[1] == pytest.approx(sequential_value[1])