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])