1 // Copyright Contributors to the OpenVDB Project
2 // SPDX-License-Identifier: MPL-2.0
3 //
4 /// @author Ken Museth
5 ///
6 /// @file LevelSetMeasure.h
7 
8 #ifndef OPENVDB_TOOLS_LEVELSETMEASURE_HAS_BEEN_INCLUDED
9 #define OPENVDB_TOOLS_LEVELSETMEASURE_HAS_BEEN_INCLUDED
10 
11 #include "openvdb/Types.h"
12 #include "openvdb/Grid.h"
13 #include "openvdb/tree/LeafManager.h"
14 #include "openvdb/tree/ValueAccessor.h"
15 #include "openvdb/math/Math.h"
16 #include "openvdb/math/FiniteDifference.h"
17 #include "openvdb/math/Operators.h"
18 #include "openvdb/math/Stencils.h"
19 #include "openvdb/util/NullInterrupter.h"
20 #include "openvdb/thread/Threading.h"
21 #include <openvdb/openvdb.h>
22 
23 #include <tbb/parallel_for.h>
24 #include <tbb/parallel_sort.h>
25 #include <tbb/parallel_invoke.h>
26 
27 #include <type_traits>
28 
29 namespace openvdb {
30 OPENVDB_USE_VERSION_NAMESPACE
31 namespace OPENVDB_VERSION_NAME {
32 namespace tools {
33 
34 /// @brief Return the surface area of a narrow-band level set.
35 ///
36 /// @param grid          a scalar, floating-point grid with one or more disjoint,
37 ///                      closed level set surfaces
38 /// @param useWorldSpace if true the area is computed in
39 ///                      world space units, else in voxel units.
40 ///
41 /// @throw TypeError if @a grid is not scalar or not floating-point or not a level set or empty.
42 template<class GridType>
43 Real
44 levelSetArea(const GridType& grid, bool useWorldSpace = true);
45 
46 /// @brief Return the volume of a narrow-band level set surface.
47 ///
48 /// @param grid          a scalar, floating-point grid with one or more disjoint,
49 ///                      closed level set surfaces
50 /// @param useWorldSpace if true the volume is computed in
51 ///                      world space units, else in voxel units.
52 ///
53 /// @throw TypeError if @a grid is not scalar or not floating-point or not a level set or empty.
54 template<class GridType>
55 Real
56 levelSetVolume(const GridType& grid, bool useWorldSpace = true);
57 
58 /// @brief Return the Euler Characteristics of a narrow-band level set surface (possibly disconnected).
59 ///
60 /// @param grid          a scalar, floating-point grid with one or more disjoint,
61 ///                      closed level set surfaces
62 ///
63 /// @throw TypeError if @a grid is not scalar or not floating-point or not a level set or empty.
64 template<class GridType>
65 int
66 levelSetEulerCharacteristic(const GridType& grid);
67 
68 /// @brief Return the genus of a narrow-band level set surface.
69 ///
70 /// @param grid          a scalar, floating-point grid with one or more disjoint,
71 ///                      closed level set surfaces
72 /// @warning The genus is only well defined for a single connected surface
73 ///
74 /// @throw TypeError if @a grid is not scalar or not floating-point or not a level set or empty.
75 template<class GridType>
76 int
77 levelSetGenus(const GridType& grid);
78 
79 ////////////////////////////////////////////////////////////////////////////////////////
80 
81 /// @brief Smeared-out and continuous Dirac Delta function.
82 template<typename RealT>
83 class DiracDelta
84 {
85 public:
86     // eps is the half-width of the dirac delta function in units of phi
DiracDelta(RealT eps)87     DiracDelta(RealT eps) : mC(0.5/eps), mD(2*math::pi<RealT>()*mC), mE(eps) {}
88     // values of the dirac delta function are in units of one over the units of phi
operator()89     inline RealT operator()(RealT phi) const { return math::Abs(phi) > mE ? 0 : mC*(1+cos(mD*phi)); }
90 private:
91     const RealT mC, mD, mE;
92 };// DiracDelta functor
93 
94 
95 /// @brief Multi-threaded computation of surface area, volume and
96 /// average mean-curvature for narrow band level sets.
97 ///
98 /// @details To reduce the risk of round-off errors (primarily due to
99 /// catastrophic cancellation) and guarantee determinism during
100 /// multi-threading this class is implemented using parallel_for, and
101 /// delayed reduction of a sorted list.
102 template<typename GridT, typename InterruptT = util::NullInterrupter>
103 class LevelSetMeasure
104 {
105 public:
106     using GridType = GridT;
107     using TreeType = typename GridType::TreeType;
108     using ValueType = typename TreeType::ValueType;
109     using ManagerType = typename tree::LeafManager<const TreeType>;
110 
111     static_assert(std::is_floating_point<ValueType>::value,
112         "level set measure is supported only for scalar, floating-point grids");
113 
114     /// @brief Main constructor from a grid
115     /// @param grid The level set to be measured.
116     /// @param interrupt Optional interrupter.
117     /// @throw RuntimeError if the grid is not a level set or if it's empty.
118     LevelSetMeasure(const GridType& grid, InterruptT* interrupt = nullptr);
119 
120     /// @brief Re-initialize using the specified grid.
121     /// @param grid The level set to be measured.
122     /// @throw RuntimeError if the grid is not a level set or if it's empty.
123     void init(const GridType& grid);
124 
125     /// @brief Destructor
~LevelSetMeasure()126     virtual ~LevelSetMeasure() {}
127 
128      /// @return the grain-size used for multi-threading
getGrainSize()129     int getGrainSize() const { return mGrainSize; }
130 
131     /// @brief Set the grain-size used for multi-threading.
132     /// @note A grain size of 0 or less disables multi-threading!
setGrainSize(int grainsize)133     void setGrainSize(int grainsize) { mGrainSize = grainsize; }
134 
135     /// @brief Compute the surface area of the level set.
136     /// @param useWorldUnits Specifies if the result is in world or voxel units.
137     /// @note Performs internal caching so only the initial call incurs actual computation.
138     Real area(bool useWorldUnits = true);
139 
140     /// @brief Compute the volume of the level set surface.
141     /// @param useWorldUnits Specifies if the result is in world or voxel units.
142     /// @note Performs internal caching so only the initial call incurs actual computation.
143     Real volume(bool useWorldUnits = true);
144 
145     /// @brief Compute the total mean curvature of the level set surface.
146     /// @param useWorldUnits Specifies if the result is in world or voxel units.
147     /// @note Performs internal caching so only the initial call incurs actual computation.
148     Real totMeanCurvature(bool useWorldUnits = true);
149 
150     /// @brief Compute the total gaussian curvature of the level set surface.
151     /// @param useWorldUnits Specifies if the result is in world or voxel units.
152     /// @note Performs internal caching so only the initial call incurs actual computation.
153     Real totGaussianCurvature(bool useWorldUnits = true);
154 
155     /// @brief Compute the average mean curvature of the level set surface.
156     /// @param useWorldUnits Specifies if the result is in world or voxel units.
157     /// @note Performs internal caching so only the initial call incurs actual computation.
158     Real avgMeanCurvature(bool useWorldUnits = true) {return this->totMeanCurvature(useWorldUnits) / this->area(useWorldUnits);}
159 
160     /// @brief Compute the average gaussian curvature of the level set surface.
161     /// @param useWorldUnits Specifies if the result is in world or voxel units.
162     /// @note Performs internal caching so only the initial call incurs actual computation.
163     Real avgGaussianCurvature(bool useWorldUnits = true) {return this->totGaussianCurvature(useWorldUnits) / this->area(useWorldUnits); }
164 
165     /// @brief Compute the Euler characteristic of the level set surface.
166     /// @note Performs internal caching so only the initial call incurs actual computation.
167     int eulerCharacteristic();
168 
169     /// @brief Compute the genus of the level set surface.
170     /// @warning The genus is only well defined for a single connected surface.
171     /// @note Performs internal caching so only the initial call incurs actual computation.
genus()172     int genus() { return 1 - this->eulerCharacteristic()/2;}
173 
174 private:
175 
176     using LeafT = typename TreeType::LeafNodeType;
177     using VoxelCIterT = typename LeafT::ValueOnCIter;
178     using LeafRange = typename ManagerType::LeafRange;
179     using LeafIterT = typename LeafRange::Iterator;
180     using ManagerPtr = std::unique_ptr<ManagerType>;
181     using BufferPtr  = std::unique_ptr<double[]>;
182 
183     // disallow copy construction and copy by assignment!
184     LevelSetMeasure(const LevelSetMeasure&);// not implemented
185     LevelSetMeasure& operator=(const LevelSetMeasure&);// not implemented
186 
187     const GridType *mGrid;
188     ManagerPtr      mLeafs;
189     BufferPtr       mBuffer;
190     InterruptT     *mInterrupter;
191     double          mDx, mArea, mVolume, mTotMeanCurvature, mTotGausCurvature;
192     int             mGrainSize;
193     bool            mUpdateArea, mUpdateCurvature;
194 
195     // @brief Return false if the process was interrupted
196     bool checkInterrupter();
197 
198     struct MeasureArea
199     {
MeasureAreaMeasureArea200         MeasureArea(LevelSetMeasure* parent) : mParent(parent), mStencil(*mParent->mGrid)
201         {
202             if (parent->mInterrupter) parent->mInterrupter->start("Measuring area and volume of level set");
203             if (parent->mGrainSize>0) {
204                 tbb::parallel_for(parent->mLeafs->leafRange(parent->mGrainSize), *this);
205             } else {
206                 (*this)(parent->mLeafs->leafRange());
207             }
208             tbb::parallel_invoke([&](){parent->mArea   = parent->reduce(0);},
209                                  [&](){parent->mVolume = parent->reduce(1)/3.0;});
210             parent->mUpdateArea = false;
211             if (parent->mInterrupter) parent->mInterrupter->end();
212         }
MeasureAreaMeasureArea213         MeasureArea(const MeasureArea& other) : mParent(other.mParent), mStencil(*mParent->mGrid) {}
214         void operator()(const LeafRange& range) const;
215         LevelSetMeasure* mParent;
216         mutable math::GradStencil<GridT, false> mStencil;
217     };// MeasureArea
218 
219     struct MeasureCurvatures
220     {
MeasureCurvaturesMeasureCurvatures221         MeasureCurvatures(LevelSetMeasure* parent) : mParent(parent), mStencil(*mParent->mGrid)
222         {
223             if (parent->mInterrupter) parent->mInterrupter->start("Measuring curvatures of level set");
224             if (parent->mGrainSize>0) {
225                 tbb::parallel_for(parent->mLeafs->leafRange(parent->mGrainSize), *this);
226             } else {
227                 (*this)(parent->mLeafs->leafRange());
228             }
229             tbb::parallel_invoke([&](){parent->mTotMeanCurvature = parent->reduce(0);},
230                                  [&](){parent->mTotGausCurvature = parent->reduce(1);});
231             parent->mUpdateCurvature = false;
232             if (parent->mInterrupter) parent->mInterrupter->end();
233         }
MeasureCurvaturesMeasureCurvatures234         MeasureCurvatures(const MeasureCurvatures& other) : mParent(other.mParent), mStencil(*mParent->mGrid) {}
235         void operator()(const LeafRange& range) const;
236         LevelSetMeasure* mParent;
237         mutable math::CurvatureStencil<GridT, false> mStencil;
238     };// MeasureCurvatures
239 
reduce(int offset)240     double reduce(int offset)
241     {
242         double *first = mBuffer.get() + offset*mLeafs->leafCount(), *last = first + mLeafs->leafCount();
243         tbb::parallel_sort(first, last);// mitigates catastrophic cancellation
244         Real sum = 0.0;
245         while(first != last) sum += *first++;
246         return sum;
247     }
248 
249 }; // end of LevelSetMeasure class
250 
251 
252 template<typename GridT, typename InterruptT>
253 inline
LevelSetMeasure(const GridType & grid,InterruptT * interrupt)254 LevelSetMeasure<GridT, InterruptT>::LevelSetMeasure(const GridType& grid, InterruptT* interrupt)
255     : mInterrupter(interrupt)
256     , mGrainSize(1)
257 {
258     this->init(grid);
259 }
260 
261 template<typename GridT, typename InterruptT>
262 inline void
init(const GridType & grid)263 LevelSetMeasure<GridT, InterruptT>::init(const GridType& grid)
264 {
265     if (!grid.hasUniformVoxels()) {
266          OPENVDB_THROW(RuntimeError,
267              "The transform must have uniform scale for the LevelSetMeasure to function");
268     }
269     if (grid.getGridClass() != GRID_LEVEL_SET) {
270         OPENVDB_THROW(RuntimeError,
271             "LevelSetMeasure only supports level sets;"
272             " try setting the grid class to \"level set\"");
273     }
274     if (grid.empty()) {
275         OPENVDB_THROW(RuntimeError,
276             "LevelSetMeasure does not support empty grids;");
277     }
278     mGrid = &grid;
279     mDx = grid.voxelSize()[0];
280     mLeafs  = std::make_unique<ManagerType>(mGrid->tree());
281     mBuffer = std::make_unique<double[]>(2*mLeafs->leafCount());
282     mUpdateArea = mUpdateCurvature = true;
283 }
284 
285 template<typename GridT, typename InterruptT>
286 inline Real
area(bool useWorldUnits)287 LevelSetMeasure<GridT, InterruptT>::area(bool useWorldUnits)
288 {
289     if (mUpdateArea) {MeasureArea m(this);};
290     double area = mArea;
291     if (useWorldUnits) area *= math::Pow2(mDx);
292     return area;
293 }
294 
295 template<typename GridT, typename InterruptT>
296 inline Real
volume(bool useWorldUnits)297 LevelSetMeasure<GridT, InterruptT>::volume(bool useWorldUnits)
298 {
299     if (mUpdateArea) {MeasureArea m(this);};
300     double volume = mVolume;
301     if (useWorldUnits) volume *= math::Pow3(mDx) ;
302     return volume;
303 }
304 
305 template<typename GridT, typename InterruptT>
306 inline Real
totMeanCurvature(bool useWorldUnits)307 LevelSetMeasure<GridT, InterruptT>::totMeanCurvature(bool useWorldUnits)
308 {
309     if (mUpdateCurvature) {MeasureCurvatures m(this);};
310     return mTotMeanCurvature * (useWorldUnits ? mDx : 1);
311 }
312 
313 template<typename GridT, typename InterruptT>
314 inline Real
totGaussianCurvature(bool)315 LevelSetMeasure<GridT, InterruptT>::totGaussianCurvature(bool)
316 {
317     if (mUpdateCurvature) {MeasureCurvatures m(this);};
318     return mTotGausCurvature;
319 }
320 
321 template<typename GridT, typename InterruptT>
322 inline int
eulerCharacteristic()323 LevelSetMeasure<GridT, InterruptT>::eulerCharacteristic()
324 {
325     const Real x = this->totGaussianCurvature(true) / (2.0*math::pi<Real>());
326     return int(math::Round( x ));
327 }
328 
329 ///////////////////////// PRIVATE METHODS //////////////////////
330 
331 template<typename GridT, typename InterruptT>
332 inline bool
checkInterrupter()333 LevelSetMeasure<GridT, InterruptT>::checkInterrupter()
334 {
335     if (util::wasInterrupted(mInterrupter)) {
336         thread::cancelGroupExecution();
337         return false;
338     }
339     return true;
340 }
341 
342 template<typename GridT, typename InterruptT>
343 inline void
344 LevelSetMeasure<GridT, InterruptT>::
operator()345 MeasureArea::operator()(const LeafRange& range) const
346 {
347     using Vec3T = math::Vec3<ValueType>;
348     // computations are performed in index space where dV = 1
349     mParent->checkInterrupter();
350     const Real invDx = 1.0/mParent->mDx;
351     const DiracDelta<Real> DD(1.5);// dirac delta function is 3 voxel units wide
352     const size_t leafCount = mParent->mLeafs->leafCount();
353     for (LeafIterT leafIter=range.begin(); leafIter; ++leafIter) {
354         Real sumA = 0, sumV = 0;//reduce risk of catastrophic cancellation
355         for (VoxelCIterT voxelIter = leafIter->cbeginValueOn(); voxelIter; ++voxelIter) {
356             const Real dd = DD(invDx * (*voxelIter));
357             if (dd > 0.0) {
358                 mStencil.moveTo(voxelIter);
359                 const Coord& p = mStencil.getCenterCoord();// in voxel units
360                 const Vec3T g  = mStencil.gradient();// in world units
361                 sumA += dd*g.length();// \delta(\phi)*|\nabla\phi|
362                 sumV += dd*(g[0]*Real(p[0]) + g[1]*Real(p[1]) + g[2]*Real(p[2]));// \delta(\phi)\vec{x}\cdot\nabla\phi
363             }
364         }
365         double* ptr = mParent->mBuffer.get() + leafIter.pos();
366         *ptr = sumA;
367         ptr += leafCount;
368         *ptr = sumV;
369     }
370 }
371 
372 template<typename GridT, typename InterruptT>
373 inline void
374 LevelSetMeasure<GridT, InterruptT>::
operator()375 MeasureCurvatures::operator()(const LeafRange& range) const
376 {
377     using Vec3T = math::Vec3<ValueType>;
378     // computations are performed in index space where dV = 1
379     mParent->checkInterrupter();
380     const Real dx = mParent->mDx, dx2=dx*dx, invDx = 1.0/dx;
381     const DiracDelta<Real> DD(1.5);// dirac delta function is 3 voxel units wide
382     ValueType mean, gauss;
383     const size_t leafCount = mParent->mLeafs->leafCount();
384     for (LeafIterT leafIter=range.begin(); leafIter; ++leafIter) {
385         Real sumM = 0, sumG = 0;//reduce risk of catastrophic cancellation
386         for (VoxelCIterT voxelIter = leafIter->cbeginValueOn(); voxelIter; ++voxelIter) {
387             const Real dd = DD(invDx * (*voxelIter));
388             if (dd > 0.0) {
389                 mStencil.moveTo(voxelIter);
390                 const Vec3T g  = mStencil.gradient();
391                 const Real dA  = dd*g.length();// \delta(\phi)*\delta(\phi)
392                 mStencil.curvatures(mean, gauss);
393                 sumM += dA*mean*dx;//   \delta(\phi)*\delta(\phi)*MeanCurvature
394                 sumG += dA*gauss*dx2;// \delta(\phi)*\delta(\phi)*GaussCurvature
395             }
396         }
397         double* ptr = mParent->mBuffer.get() + leafIter.pos();
398         *ptr = sumM;
399         ptr += leafCount;
400         *ptr = sumG;
401     }
402 }
403 
404 ////////////////////////////////////////
405 
406 //{
407 /// @cond OPENVDB_DOCS_INTERNAL
408 
409 template<class GridT>
410 inline
411 typename std::enable_if<std::is_floating_point<typename GridT::ValueType>::value, Real>::type
doLevelSetArea(const GridT & grid,bool useWorldUnits)412 doLevelSetArea(const GridT& grid, bool useWorldUnits)
413 {
414     LevelSetMeasure<GridT> m(grid);
415     return m.area(useWorldUnits);
416 }
417 
418 template<class GridT>
419 inline
420 typename std::enable_if<!std::is_floating_point<typename GridT::ValueType>::value, Real>::type
doLevelSetArea(const GridT &,bool)421 doLevelSetArea(const GridT&, bool)
422 {
423     OPENVDB_THROW(TypeError,
424         "level set area is supported only for scalar, floating-point grids");
425 }
426 
427 /// @endcond
428 //}
429 
430 template<class GridT>
431 Real
levelSetArea(const GridT & grid,bool useWorldUnits)432 levelSetArea(const GridT& grid, bool useWorldUnits)
433 {
434     return doLevelSetArea<GridT>(grid, useWorldUnits);
435 }
436 
437 ////////////////////////////////////////
438 
439 //{
440 /// @cond OPENVDB_DOCS_INTERNAL
441 
442 template<class GridT>
443 inline
444 typename std::enable_if<std::is_floating_point<typename GridT::ValueType>::value, Real>::type
doLevelSetVolume(const GridT & grid,bool useWorldUnits)445 doLevelSetVolume(const GridT& grid, bool useWorldUnits)
446 {
447     LevelSetMeasure<GridT> m(grid);
448     return m.volume(useWorldUnits);
449 }
450 
451 template<class GridT>
452 inline
453 typename std::enable_if<!std::is_floating_point<typename GridT::ValueType>::value, Real>::type
doLevelSetVolume(const GridT &,bool)454 doLevelSetVolume(const GridT&, bool)
455 {
456     OPENVDB_THROW(TypeError,
457         "level set volume is supported only for scalar, floating-point grids");
458 }
459 
460 /// @endcond
461 //}
462 
463 template<class GridT>
464 Real
levelSetVolume(const GridT & grid,bool useWorldUnits)465 levelSetVolume(const GridT& grid, bool useWorldUnits)
466 {
467     return doLevelSetVolume<GridT>(grid, useWorldUnits);
468 }
469 
470 ////////////////////////////////////////
471 
472 //{
473 /// @cond OPENVDB_DOCS_INTERNAL
474 
475 template<class GridT>
476 inline
477 typename std::enable_if<std::is_floating_point<typename GridT::ValueType>::value, int>::type
doLevelSetEulerCharacteristic(const GridT & grid)478 doLevelSetEulerCharacteristic(const GridT& grid)
479 {
480     LevelSetMeasure<GridT> m(grid);
481     return m.eulerCharacteristic();
482 }
483 
484 template<class GridT>
485 inline
486 typename std::enable_if<!std::is_floating_point<typename GridT::ValueType>::value, int>::type
doLevelSetEulerCharacteristic(const GridT &)487 doLevelSetEulerCharacteristic(const GridT&)
488 {
489     OPENVDB_THROW(TypeError,
490         "level set euler characteristic is supported only for scalar, floating-point grids");
491 }
492 
493 /// @endcond
494 //}
495 
496 
497 template<class GridT>
498 int
levelSetEulerCharacteristic(const GridT & grid)499 levelSetEulerCharacteristic(const GridT& grid)
500 {
501     return doLevelSetEulerCharacteristic(grid);
502 }
503 
504 ////////////////////////////////////////
505 
506 //{
507 /// @cond OPENVDB_DOCS_INTERNAL
508 
509 template<class GridT>
510 inline
511 typename std::enable_if<std::is_floating_point<typename GridT::ValueType>::value, int>::type
doLevelSetEuler(const GridT & grid)512 doLevelSetEuler(const GridT& grid)
513 {
514     LevelSetMeasure<GridT> m(grid);
515     return m.eulerCharacteristics();
516 
517 }
518 
519 template<class GridT>
520 inline
521 typename std::enable_if<std::is_floating_point<typename GridT::ValueType>::value, int>::type
doLevelSetGenus(const GridT & grid)522 doLevelSetGenus(const GridT& grid)
523 {
524     LevelSetMeasure<GridT> m(grid);
525     return m.genus();
526 }
527 
528 template<class GridT>
529 inline
530 typename std::enable_if<!std::is_floating_point<typename GridT::ValueType>::value, int>::type
doLevelSetGenus(const GridT &)531 doLevelSetGenus(const GridT&)
532 {
533     OPENVDB_THROW(TypeError,
534         "level set genus is supported only for scalar, floating-point grids");
535 }
536 
537 /// @endcond
538 //}
539 
540 template<class GridT>
541 int
levelSetGenus(const GridT & grid)542 levelSetGenus(const GridT& grid)
543 {
544     return doLevelSetGenus(grid);
545 }
546 
547 
548 ////////////////////////////////////////
549 
550 
551 // Explicit Template Instantiation
552 
553 #ifdef OPENVDB_USE_EXPLICIT_INSTANTIATION
554 
555 #ifdef OPENVDB_INSTANTIATE_LEVELSETMEASURE
556 #include <openvdb/util/ExplicitInstantiation.h>
557 #endif
558 
559 #define _FUNCTION(TreeT) \
560     Real levelSetArea(const Grid<TreeT>&, bool)
561 OPENVDB_REAL_TREE_INSTANTIATE(_FUNCTION)
562 #undef _FUNCTION
563 
564 #define _FUNCTION(TreeT) \
565     Real levelSetVolume(const Grid<TreeT>&, bool)
566 OPENVDB_REAL_TREE_INSTANTIATE(_FUNCTION)
567 #undef _FUNCTION
568 
569 #define _FUNCTION(TreeT) \
570     int levelSetEulerCharacteristic(const Grid<TreeT>&)
571 OPENVDB_REAL_TREE_INSTANTIATE(_FUNCTION)
572 #undef _FUNCTION
573 
574 #define _FUNCTION(TreeT) \
575     int levelSetGenus(const Grid<TreeT>&)
576 OPENVDB_REAL_TREE_INSTANTIATE(_FUNCTION)
577 #undef _FUNCTION
578 
579 OPENVDB_INSTANTIATE_CLASS LevelSetMeasure<FloatGrid, util::NullInterrupter>;
580 OPENVDB_INSTANTIATE_CLASS LevelSetMeasure<DoubleGrid, util::NullInterrupter>;
581 
582 #endif // OPENVDB_USE_EXPLICIT_INSTANTIATION
583 
584 
585 } // namespace tools
586 } // namespace OPENVDB_VERSION_NAME
587 } // namespace openvdb
588 
589 #endif // OPENVDB_TOOLS_LEVELSETMEASURE_HAS_BEEN_INCLUDED
590