mmcfilters
Public API documentation
Loading...
Searching...
No Matches
ExtinctionValues.hpp
1#pragma once
2
3#include "../trees/TreeAltitudeAlgorithms.hpp"
4#include "../trees/ValuedMorphologicalTree.hpp"
5#include "../trees/ValuedMorphologicalTreeView.hpp"
6#include "../trees/detail/TreeKernelAccess.hpp"
7#include "../trees/saliency/HierarchySaliencyMapValidation.hpp"
8#include "../trees/saliency/HierarchySaliencyMap.hpp"
9#include "../trees/saliency/HierarchicalWatershedSaliency.hpp"
10#include "../utils/Image.hpp"
11#include "../utils/Common.hpp"
12#include "../utils/Contract.hpp"
13#include "../contours/ContourComputation.hpp"
14
15#include <algorithm>
16#include <cmath>
17#include <concepts>
18#include <limits>
19#include <memory>
20#include <span>
21#include <stack>
22#include <stdexcept>
23#include <string>
24#include <utility>
25#include <vector>
26
27namespace mmcfilters {
28
52
60template <std::floating_point Real = float> class ExtinctionSelectionPolicy {
62 enum class Mode { TopK, MinimumExtinction };
63
65 Mode mode_ = Mode::TopK;
67 int extremaToKeep_ = 0;
69 Real threshold_ = Real{0};
70
71 public:
81 policy.mode_ = Mode::TopK;
82 policy.extremaToKeep_ = extremaToKeep;
83 return policy;
84 }
85
95 policy.mode_ = Mode::MinimumExtinction;
96 policy.threshold_ = threshold;
97 return policy;
98 }
99
105 [[nodiscard]] bool selectsTopK() const noexcept { return mode_ == Mode::TopK; }
106
113 [[nodiscard]] int extremaToKeep() const noexcept { return extremaToKeep_; }
114
121 [[nodiscard]] Real minimumExtinction() const noexcept { return threshold_; }
122};
123
127enum class ExtinctionContourScorePolicy {
129 RankScore,
131 ExtinctionValue
132};
133
172template <AltitudeValue T, std::floating_point Real = float> class ExtinctionValues {
173 protected:
175
177
179 std::vector<RegionalExtremaNode<Real>> regionalExtremaNodes_;
181 std::vector<Real> extinctionValueAttribute_;
185 const ValuedMorphologicalTree<T>* valuedTree_ = nullptr;
187 const MorphologicalTree& tree;
189 std::size_t treeMutationVersion_ = 0;
190
192 struct SelectedExtremum {
194 const RegionalExtremaNode<Real>* record = nullptr;
196 int rankScore = 0;
197 };
198
209 [[nodiscard]] AltitudeView view() const { return valuedTree_ != nullptr ? valuedTree_->asView() : view_; }
210
220 void requireStableTree(const char* context) const { tree.requireMutationVersion(treeMutationVersion_, context); }
221
229 static void requireAttributePointer(const Real* attr, const char* context) {
231 throw std::invalid_argument(std::string(context) + " requires a non-null attribute buffer."));
232 }
233
244 static const Real* requireAttributeBuffer(const MorphologicalTree& tree, const std::vector<Real>& attr, const char* context) {
245 MMCFILTERS_CONTRACT_REQUIRE(attr.size() == static_cast<std::size_t>(tree.numInternalNodeSlots()),
246 throw std::invalid_argument(std::string(context) + " attribute size must match the internal node slot count."));
247 return attr.data();
248 }
249
260 static T altitudeOf(const AltitudeView& view, NodeId nodeId) noexcept {
261 return view.nodeAltitudes()[static_cast<std::size_t>(nodeId)];
262 }
263
275 std::vector<NodeId> collectComponentTreeExtrema() const { return this->tree.leaves(); }
276
286 static Real dominantExtremumSentinel() noexcept { return std::numeric_limits<Real>::max(); }
287
295 static void requireNonNegativeExtremaToKeep(int extremaToKeep, const char* context) {
296 MMCFILTERS_CONTRACT_REQUIRE(extremaToKeep >= 0,
297 throw std::invalid_argument(std::string(context) + " requires a non-negative extremaToKeep value."));
298 }
299
307 static void requireFiniteThreshold(Real threshold, const char* context) {
309 throw std::invalid_argument(std::string(context) + " requires a finite extinction threshold."));
310 }
311
324 if (policy.selectsTopK()) {
326 } else {
327 requireFiniteThreshold(policy.minimumExtinction(), context);
328 }
329 }
330
343 static void requireFormalExtinctionValue(Real value, NodeId nodeId, const char* context) {
344 if (!std::isfinite(value)) {
345 throw std::invalid_argument(std::string(context) + " requires finite extinction values.");
346 }
347 if (value < Real{0}) {
348 throw std::invalid_argument(std::string(context) + " requires non-negative extinction values.");
349 }
350 (void)nodeId;
351 }
352
365 extinctionValueAttribute_.assign(static_cast<std::size_t>(tree.numInternalNodeSlots()), Real{0});
367 Real& localValue = extinctionValueAttribute_[static_cast<std::size_t>(record.leaf)];
368 if (localValue < record.extinction) {
369 localValue = record.extinction;
370 }
371 }
372
373 for (NodeId nodeId : tree.postOrder()) {
374 Real& nodeValue = extinctionValueAttribute_[static_cast<std::size_t>(nodeId)];
375 for (NodeId childId : detail::TreeKernelAccess::children(tree, nodeId)) {
376 const Real childValue = extinctionValueAttribute_[static_cast<std::size_t>(childId)];
377 if (nodeValue < childValue) {
379 }
380 }
381 }
382 }
383
394 void validateExtinctionValueAttribute(const char* context) const {
397 }
399 HierarchyValuationPolicy::AllowLevelCollapse,
400 HierarchyValuationRangePolicy::RequireNonNegative, context);
401 }
402
417 [[nodiscard]] std::vector<SelectedExtremum> selectExtrema(const ExtinctionSelectionPolicy<Real>& policy, const char* context) const {
419
420 std::vector<const RegionalExtremaNode<Real>*> records;
421 if (policy.selectsTopK()) {
422 const int extremaToSelect = std::min(policy.extremaToKeep(), static_cast<int>(regionalExtremaNodes_.size()));
423 records.reserve(static_cast<std::size_t>(extremaToSelect));
424 for (int i = 0; i < extremaToSelect; ++i) {
425 records.push_back(&regionalExtremaNodes_[static_cast<std::size_t>(i)]);
426 }
427 } else {
428 records.reserve(regionalExtremaNodes_.size());
430 if (record.extinction >= policy.minimumExtinction()) {
431 records.push_back(&record);
432 }
433 }
434 }
435
436 std::vector<SelectedExtremum> selected;
437 selected.reserve(records.size());
438 const int selectedCount = static_cast<int>(records.size());
439 for (int i = 0; i < selectedCount; ++i) {
440 selected.push_back(SelectedExtremum{records[static_cast<std::size_t>(i)], selectedCount - i});
441 }
442 return selected;
443 }
444
459 requireStableTree(context);
460 const AltitudeView altitudeView = view();
461 for (NodeId nodeId : tree.postOrder()) {
462 if (!tree.isRoot(nodeId) && extremumPreservationMask[static_cast<std::size_t>(nodeId)]) {
463 extremumPreservationMask[static_cast<std::size_t>(detail::TreeKernelAccess::nodeParent(tree, nodeId))] = true;
464 }
465 }
466
468 auto imgOutput = imgOutputPtr->rawData();
469 std::stack<NodeId> stack;
470 stack.push(tree.root());
471 while (!stack.empty()) {
472 const NodeId nodeId = stack.top();
473 stack.pop();
474 const T level = altitudeOf(altitudeView, nodeId);
475 for (PixelId pixel : detail::TreeKernelAccess::properParts(tree, nodeId)) {
476 imgOutput[pixel] = level;
477 }
478 for (NodeId childNodeId : detail::TreeKernelAccess::children(tree, nodeId)) {
479 if (extremumPreservationMask[static_cast<std::size_t>(childNodeId)]) {
480 stack.push(childNodeId);
481 } else {
482 for (NodeId subtreeNodeId : detail::TreeKernelAccess::subtree(tree, childNodeId)) {
483 for (PixelId pixel : detail::TreeKernelAccess::properParts(tree, subtreeNodeId)) {
484 imgOutput[pixel] = level;
485 }
486 }
487 }
488 }
489 }
490 return imgOutputPtr;
491 }
492
503 [[nodiscard]] ImagePtr<T> filteringFromSelectedExtrema(const std::vector<SelectedExtremum>& selected, const char* context) const {
504 std::vector<uint8_t> extremumPreservationMask(tree.numInternalNodeSlots(), false);
505 for (const SelectedExtremum& item : selected) {
506 extremumPreservationMask[static_cast<std::size_t>(item.record->leaf)] = true;
507 }
509 }
510
532 void initialize(const Real* attr) {
533 requireAttributePointer(attr, "ExtinctionValues");
534 std::vector<NodeId> leaves = collectComponentTreeExtrema();
535 regionalExtremaNodes_.reserve(leaves.size());
536 std::vector<uint8_t> visited(this->tree.numInternalNodeSlots(), false);
537 for (NodeId leafNodeId : leaves) {
538 Real extinction = dominantExtremumSentinel();
540 NodeId parentNodeId = detail::TreeKernelAccess::nodeParent(this->tree, cutoffNodeId);
541 bool flag = true;
542 while (flag && !this->tree.isRoot(cutoffNodeId)) {
543 if (detail::TreeKernelAccess::numChildren(this->tree, parentNodeId) > 1) {
544 for (NodeId sonNodeId : detail::TreeKernelAccess::children(this->tree, parentNodeId)) {
545 if (flag) {
547 flag = false;
548 } else if (sonNodeId != cutoffNodeId && attr[sonNodeId] > attr[cutoffNodeId]) {
549 flag = false;
550 }
551 visited[sonNodeId] = true;
552 }
553 }
554 }
555 if (flag) {
557 parentNodeId = detail::TreeKernelAccess::nodeParent(this->tree, cutoffNodeId);
558 }
559 }
560 if (!this->tree.isRoot(cutoffNodeId)) {
561 extinction = attr[cutoffNodeId];
562 }
563 regionalExtremaNodes_.emplace_back(leafNodeId, cutoffNodeId, extinction);
564 }
565
566 std::sort(regionalExtremaNodes_.begin(), regionalExtremaNodes_.end(), [](const auto& a, const auto& b) {
567 if (a.extinction != b.extinction) {
568 return a.extinction > b.extinction;
569 }
570 if (a.cutoffNode != b.cutoffNode) {
571 return a.cutoffNode < b.cutoffNode;
572 }
573 return a.leaf < b.leaf;
574 });
576 }
578
579 public:
581 using value_type = Real;
582
593 ExtinctionValues(const AltitudeView& view, const std::shared_ptr<Real[]>& attr) : ExtinctionValues(view, attr.get()) {}
594
605 ExtinctionValues(const AltitudeView& view, const std::vector<Real>& attr)
606 : ExtinctionValues(view, requireAttributeBuffer(view.topology(), attr, "ExtinctionValues")) {}
607
617 ExtinctionValues(const AltitudeView& view, const Real* attr) : view_(view), tree(view_.topology()), treeMutationVersion_(tree.getMutationVersion()) {
618 view_.requireTopologyUnchanged("ExtinctionValues");
620 initialize(attr);
621 }
622
634 ExtinctionValues(const ValuedMorphologicalTree<T>& valuedTree, const std::shared_ptr<Real[]>& attr) : ExtinctionValues(valuedTree.asView(), attr.get()) {
635 valuedTree_ = &valuedTree;
636 }
637
650 valuedTree_ = &valuedTree;
651 }
652
664
681 constexpr const char* context = "ExtinctionValues::contourMap";
682 requireStableTree(context);
683 const std::vector<SelectedExtremum> selected = selectExtrema(selection, context);
684
685 std::vector<uint8_t> keep(tree.numInternalNodeSlots(), false);
686 std::vector<Real> scoreByNode(tree.numInternalNodeSlots(), Real{0});
687 std::vector<NodeId> keptNodes;
688 for (const SelectedExtremum& item : selected) {
689 const NodeId cutoffNode = item.record->cutoffNode;
690 Real score = Real{0};
691 switch (scorePolicy) {
692 case ExtinctionContourScorePolicy::RankScore:
693 score = static_cast<Real>(item.rankScore);
694 break;
695 case ExtinctionContourScorePolicy::ExtinctionValue:
696 score = item.record->extinction;
697 break;
698 default:
699 throw std::invalid_argument(std::string(context) + " received an unknown extinction contour score policy.");
700 }
701 if (!keep[static_cast<std::size_t>(cutoffNode)]) {
702 keptNodes.push_back(cutoffNode);
703 keep[static_cast<std::size_t>(cutoffNode)] = true;
704 scoreByNode[static_cast<std::size_t>(cutoffNode)] = score;
705 } else if (scoreByNode[static_cast<std::size_t>(cutoffNode)] < score) {
706 scoreByNode[static_cast<std::size_t>(cutoffNode)] = score;
707 }
708 }
709
710 ImagePtr<Real> imgOutputPtr = Image<Real>::create(tree.numRows(), tree.numColumns(), Real{0});
711 auto contourOutput = imgOutputPtr->rawData();
712
713 if (keptNodes.empty()) {
714 return imgOutputPtr;
715 }
716 // Preserve the selected-node overwrite priority independently of the
717 // contour traversal's post-order (shared boundary pixels can overlap).
718 std::vector<std::size_t> priorityByNode(tree.numInternalNodeSlots(), 0);
719 for (std::size_t selectionIndex = 0; selectionIndex < keptNodes.size(); ++selectionIndex) {
720 priorityByNode[static_cast<std::size_t>(keptNodes[selectionIndex])] = selectionIndex + 1;
721 }
722 std::vector<std::size_t> pixelPriority(tree.numPixels(), 0);
723 ContourComputation(tree).forEachContour([&](NodeId node, std::span<const PixelId> pixels) {
724 const auto selectionPriority = priorityByNode[static_cast<std::size_t>(node)];
725 if (selectionPriority == 0) {
726 return;
727 }
728 for (PixelId pixel : pixels) {
729 if (selectionPriority > pixelPriority[static_cast<std::size_t>(pixel)]) {
730 contourOutput[pixel] = scoreByNode[static_cast<std::size_t>(node)];
731 pixelPriority[static_cast<std::size_t>(pixel)] = selectionPriority;
732 }
733 }
734 });
735
736 return imgOutputPtr;
737 }
738
763 [[nodiscard]] const std::vector<Real>& getExtinctionValueAttribute() const {
764 constexpr const char* context = "ExtinctionValues::getExtinctionValueAttribute";
765 requireStableTree(context);
768 }
769
785 const std::vector<Real>& valuation = getExtinctionValueAttribute();
786 return HierarchySaliencyMapValidation::rankHierarchyValuation(tree, std::span<const Real>(valuation), HierarchyValuationPolicy::AllowLevelCollapse);
787 }
788
814 const std::vector<Real>& valuation = getExtinctionValueAttribute();
815 return HierarchicalWatershedSaliency::compute(view(), std::span<const Real>(valuation), adjacency);
816 }
817
831 return computeFormalSaliencyEdgeMap(HierarchySaliencyMap::requireProjectionAdjacency(tree, "ExtinctionValues::computeFormalSaliencyEdgeMap"));
832 }
833
848 const std::vector<Real>& valuation = getExtinctionValueAttribute();
849 return HierarchicalWatershedSaliency::computeRanked(view(), std::span<const Real>(valuation), adjacency);
850 }
851
865 return computeRankedFormalSaliencyEdgeMap(
866 HierarchySaliencyMap::requireProjectionAdjacency(tree, "ExtinctionValues::computeRankedFormalSaliencyEdgeMap"));
867 }
868
884 const std::vector<Real>& valuation = getExtinctionValueAttribute();
885 return HierarchySaliencyMap::computeSaliencyEdgeMap(tree, adjacency, std::span<const Real>(valuation), HierarchyValuationPolicy::AllowLevelCollapse);
886 }
887
897 return computeMonotoneExtinctionProjection(
898 HierarchySaliencyMap::requireProjectionAdjacency(tree, "ExtinctionValues::computeMonotoneExtinctionProjection"));
899 }
900
911 const std::vector<Real>& valuation = getExtinctionValueAttribute();
912 return HierarchySaliencyMap::computeCanonicalRankedSaliencyEdgeMap(tree, adjacency, std::span<const Real>(valuation),
913 HierarchyValuationPolicy::AllowLevelCollapse);
914 }
915
925 return computeRankedMonotoneExtinctionProjection(
926 HierarchySaliencyMap::requireProjectionAdjacency(tree, "ExtinctionValues::computeRankedMonotoneExtinctionProjection"));
927 }
928
939 constexpr const char* context = "ExtinctionValues::filtering";
940 const std::vector<SelectedExtremum> selected = selectExtrema(selection, context);
942 }
943
950 [[nodiscard]] const std::vector<RegionalExtremaNode<Real>>& getRegionalExtrema() const {
951 requireStableTree("ExtinctionValues::getRegionalExtrema");
953 }
954};
955
956} // namespace mmcfilters
#define MMCFILTERS_CONTRACT_REQUIRE(condition,...)
Evaluates a caller precondition and its failure action only in checked builds.
Definition Contract.hpp:53
Incremental foreground A4 contours on the image domain.
void forEachContour(Consumer &&consumer) const
Calls consumer(node, pixels) once for every live node.
Explicit extinction-extrema selection policy.
int extremaToKeep() const noexcept
Maximum number of ranked extrema retained by a top-k policy.
bool selectsTopK() const noexcept
Returns true when the policy selects the first ranked extrema.
static ExtinctionSelectionPolicy byThreshold(Real threshold) noexcept
Select every extremum whose extinction value is at least threshold.
static ExtinctionSelectionPolicy byTopK(int extremaToKeep) noexcept
Select the strongest extrema by decreasing extinction ranking.
Real minimumExtinction() const noexcept
Minimum accepted extinction value for a threshold policy.
Computes and stores extinction values for regional extrema.
ExtinctionValues(const AltitudeView &view, const std::shared_ptr< Real[]> &attr)
Computes extinction values from a valued-tree view and shared attribute buffer.
EdgeSaliencyMap< Real > computeMonotoneExtinctionProjection(const RegularGridAdjacency2D &adjacency) const
Projects the max-descendant extinction attribute directly by LCA.
ExtinctionValues(const ValuedMorphologicalTree< T > &valuedTree, const std::vector< Real > &attr)
Computes extinction values from a valued tree and vector attribute buffer.
ExtinctionValues(const ValuedMorphologicalTree< T > &valuedTree, const std::shared_ptr< Real[]> &attr)
Computes extinction values from a valued tree and shared attribute buffer.
const std::vector< RegionalExtremaNode< Real > > & getRegionalExtrema() const
Returns regional-extremum records sorted by decreasing extinction.
EdgeSaliencyMap< int > computeRankedMonotoneExtinctionProjection(const RegularGridAdjacency2D &adjacency) const
Computes canonical effective-edge ranks for the monotone projection.
ImagePtr< T > filtering(const ExtinctionSelectionPolicy< Real > &selection) const
Reconstructs an image from selected regional extrema.
EdgeSaliencyMap< Real > computeFormalSaliencyEdgeMap(const RegularGridAdjacency2D &adjacency) const
Computes the formal hierarchical-watershed extinction saliency map.
EdgeSaliencyMap< int > computeRankedFormalSaliencyEdgeMap(const RegularGridAdjacency2D &adjacency) const
Computes a ranked formal extinction saliency edge map.
Real value_type
Scalar type used for input attributes and extinction values.
ExtinctionValues(const ValuedMorphologicalTree< T > &valuedTree, const Real *attr)
Computes extinction values from a valued tree and raw attribute buffer.
EdgeSaliencyMap< int > computeRankedFormalSaliencyEdgeMap() const
Computes a ranked formal extinction saliency edge map using stored adjacency.
std::vector< int > computeRankedExtinctionValueAttribute() const
Builds a dense integer extinction attribute from extinction levels.
ExtinctionValues(const AltitudeView &view, const Real *attr)
Computes extinction values from a valued-tree view and raw attribute buffer.
EdgeSaliencyMap< int > computeRankedMonotoneExtinctionProjection() const
Stored-adjacency overload of computeRankedMonotoneExtinctionProjection.
EdgeSaliencyMap< Real > computeFormalSaliencyEdgeMap() const
Computes the formal extinction saliency edge map using stored adjacency.
ExtinctionValues(const AltitudeView &view, const std::vector< Real > &attr)
Computes extinction values from a valued-tree view and vector attribute buffer.
ImagePtr< Real > contourMap(const ExtinctionSelectionPolicy< Real > &selection, ExtinctionContourScorePolicy scorePolicy) const
Builds a contour-valued image from selected extinction events.
const std::vector< Real > & getExtinctionValueAttribute() const
Returns extinction values extended from extrema to every hierarchy node.
EdgeSaliencyMap< Real > computeMonotoneExtinctionProjection() const
Stored-adjacency overload of computeMonotoneExtinctionProjection.
static EdgeSaliencyMap< Real > compute(const ValuedMorphologicalTreeView< T > &valuedTree, std::span< const Real > leafExtinction, const RegularGridAdjacency2D &adjacency)
Computes the full-graph extinction hierarchical-watershed saliency.
static EdgeSaliencyMap< int > computeRanked(const ValuedMorphologicalTreeView< T > &valuedTree, std::span< const Real > leafExtinction, const RegularGridAdjacency2D &adjacency)
Computes the canonical dense rank scale of compute.
static void validateHierarchyValuation(const MorphologicalTree &tree, std::span< const Value > valuation, HierarchyValuationPolicy policy=HierarchyValuationPolicy::AllowLevelCollapse, HierarchyValuationRangePolicy rangePolicy=HierarchyValuationRangePolicy::AllowAnyFinite, const char *context="HierarchySaliencyMapValidation::validateHierarchyValuation")
Validates that a node-indexed valuation is compatible with a hierarchy.
static std::vector< int > rankHierarchyValuation(const MorphologicalTree &tree, std::span< const Value > valuation, HierarchyValuationPolicy policy=HierarchyValuationPolicy::AllowLevelCollapse)
Converts a compatible valuation to dense non-negative integer levels.
static EdgeSaliencyMap< int > computeCanonicalRankedSaliencyEdgeMap(const MorphologicalTree &tree, const RegularGridAdjacency2D &adjacency, std::span< const Value > valuation, HierarchyValuationPolicy policy=HierarchyValuationPolicy::AllowLevelCollapse, HierarchyConnectivityPolicy connectivityPolicy=HierarchyConnectivityPolicy::ValidateConnected)
Computes the canonical dense integer saliency scale of a valuation.
static RegularGridAdjacency2D requireProjectionAdjacency(const MorphologicalTree &tree, const char *context="HierarchySaliencyMap::requireProjectionAdjacency")
Returns the unambiguous adjacency stored by a hierarchy.
static EdgeSaliencyMap< Value > computeSaliencyEdgeMap(const MorphologicalTree &tree, const RegularGridAdjacency2D &adjacency, std::span< const Value > valuation, HierarchyValuationPolicy policy=HierarchyValuationPolicy::AllowLevelCollapse, HierarchyLevelConvention levelConvention=HierarchyLevelConvention::EdgeSaliencyValue, HierarchyConnectivityPolicy connectivityPolicy=HierarchyConnectivityPolicy::ValidateConnected)
Computes the formal edge-indexed saliency map induced by a valuation.
static Ptr create(int rows, int columns)
Creates an owned image with uninitialised pixel values.
Definition Image.hpp:89
Mutable connected-subset tree on a finite pixel domain.
int numRows() const
Returns the number of rows in the regular 2D pixel domain.
int numInternalNodeSlots() const
Returns the size of the dense internal-node id domain.
PostOrderNodeRange postOrder() const
Returns a post-order traversal range rooted at the connected root.
void requireMutationVersion(std::size_t expectedVersion, const char *context) const
Rejects stale read-only views that captured an older mutation version.
bool isRoot(NodeId nodeId) const
Tests whether nodeId is the current root.
std::vector< NodeId > leaves() const
Returns all live leaf nodes in the current hierarchy.
int numColumns() const
Returns the number of columns in the regular 2D pixel domain.
NodeId root() const
Returns the current hierarchy root.
Immutable regular-grid 2D adjacency with allocation-free traversal.
static void requireGlobalMonotoneAltitudeOrder(const MorphologicalTree &tree, const char *context)
Requires a declared globally monotone altitude order.
Owning result for one computed scalar attribute layout and buffer.
Record describing one regional extremum and its extinction value.
RegionalExtremaNode(NodeId leaf, NodeId cutoffNode, Real extinction)
Builds one extinction-value record.
Real extinction
Attribute value at cutoffNode, or numeric_limits<Real>::max() for the dominant extremum that survives...
NodeId cutoffNode
Highest node retained before the extremum merges with a stronger branch.
NodeId leaf
Leaf node that represents the regional extremum in a max-tree/min-tree.