mmcfilters
Public API documentation
Loading...
Searching...
No Matches
ShapeSpaceSaliency.hpp
1#pragma once
2
3#include "HierarchySaliencyMap.hpp"
4#include "../../utils/RegularGridAdjacency2D.hpp"
5#include "../../utils/Common.hpp"
6#include "../MorphologicalTree.hpp"
7#include "../detail/TreeKernelAccess.hpp"
8
9#include <algorithm>
10#include <bit>
11#include <cmath>
12#include <concepts>
13#include <cstddef>
14#include <cstdint>
15#include <limits>
16#include <sstream>
17#include <span>
18#include <stdexcept>
19#include <string>
20#include <utility>
21#include <vector>
22
23namespace mmcfilters {
24
28enum class ShapeSpaceExtremaPolarity { Minima, Maxima };
29
33template <std::floating_point Real> struct ShapeSpaceExtremum {
37 Real birthLevel{};
39 Real deathLevel{};
41 Real extinction{};
42};
43
47template <std::floating_point Real> struct ShapeSpaceExtinctionResult {
49 std::vector<ShapeSpaceExtremum<Real>> extrema;
51 std::vector<Real> nodeScores;
52};
53
57template <std::floating_point Real> struct ShapeSpaceSaliencyResult {
59 std::vector<ShapeSpaceExtremum<Real>> extrema;
61 std::vector<Real> nodeScores;
64};
65
87 private:
94 static void validatePolarity(ShapeSpaceExtremaPolarity polarity, const char* context) {
95 switch (polarity) {
96 case ShapeSpaceExtremaPolarity::Minima:
97 case ShapeSpaceExtremaPolarity::Maxima:
98 return;
99 }
100 throw std::invalid_argument(std::string(context) + " received an unknown extrema polarity.");
101 }
102
109 static void validateRootedTree(const MorphologicalTree& tree, const char* context) { detail::requireCommittedRootedHierarchy(tree, context); }
110
120 template <std::floating_point Real>
121 static void validateNodeBuffer(const MorphologicalTree& tree, std::span<const Real> values, bool requireNonNegative, const char* valueName,
122 const char* context) {
123 const std::size_t expected = static_cast<std::size_t>(tree.numInternalNodeSlots());
124 if (values.size() != expected) {
125 std::ostringstream oss;
126 oss << context << " requires one " << valueName << " value per dense internal node slot; expected " << expected << " values but got "
127 << values.size() << ".";
128 throw std::invalid_argument(oss.str());
129 }
130
131 for (NodeId nodeId : tree.aliveNodeIds()) {
132 const Real value = values[static_cast<std::size_t>(nodeId)];
133 if (!std::isfinite(value)) {
134 std::ostringstream oss;
135 oss << context << " requires finite " << valueName << " values; node " << nodeId << " is non-finite.";
136 throw std::invalid_argument(oss.str());
137 }
138 if (requireNonNegative && value < Real{0}) {
139 std::ostringstream oss;
140 oss << context << " requires non-negative " << valueName << " values; node " << nodeId << " has value " << value << ".";
141 throw std::invalid_argument(oss.str());
142 }
143 }
144 }
145
153 static void validateTreeAndAdjacency(const MorphologicalTree& tree, const RegularGridAdjacency2D& adjacency, const char* context) {
154 validateRootedTree(tree, context);
155
156 const int rows = tree.numRows();
157 const int columns = tree.numColumns();
158 const int numPixels = tree.numPixels();
159 if (rows <= 0 || columns <= 0 || numPixels <= 0) {
160 throw std::invalid_argument(std::string(context) + " requires a non-empty image/pixel domain.");
161 }
162
163 if (adjacency.getNumRows() != rows || adjacency.getNumColumns() != columns) {
164 std::ostringstream oss;
165 oss << context << " adjacency domain must match the tree image domain; got " << adjacency.getNumRows() << "x" << adjacency.getNumColumns()
166 << " for a tree domain of " << rows << "x" << columns << ".";
167 throw std::invalid_argument(oss.str());
168 }
169 }
170
178 static const RegularGridAdjacency2D& requireStoredAdjacency(const MorphologicalTree& tree, const char* context) {
179 const RegularGridAdjacency2D* adjacency = tree.constructionAdjacency();
180 if (adjacency == nullptr) {
181 throw std::invalid_argument(std::string(context) + " requires an attached adjacency relation; pass an explicit adjacency relation instead.");
182 }
183 return *adjacency;
184 }
185
196 template <std::floating_point Real>
197 static Real checkedExtinction(Real birthLevel, Real deathLevel, ShapeSpaceExtremaPolarity polarity, NodeId representative, const char* context) {
198 const long double birth = static_cast<long double>(birthLevel);
199 const long double death = static_cast<long double>(deathLevel);
200 const long double difference = polarity == ShapeSpaceExtremaPolarity::Minima ? death - birth : birth - death;
201
202 if (difference < 0.0L) {
203 std::ostringstream oss;
204 oss << context << " produced an invalid extinction interval for representative node " << representative << ".";
205 throw std::invalid_argument(oss.str());
206 }
207
208 const long double maximum = static_cast<long double>(std::numeric_limits<Real>::max());
209 if (!std::isfinite(difference) || difference > maximum) {
210 std::ostringstream oss;
211 oss << context << " extinction for representative node " << representative << " is not representable by the requested floating-point type.";
212 throw std::overflow_error(oss.str());
213 }
214
215 const Real result = static_cast<Real>(difference);
216 if (!std::isfinite(result)) {
217 std::ostringstream oss;
218 oss << context << " extinction for representative node " << representative << " overflowed the requested floating-point type.";
219 throw std::overflow_error(oss.str());
220 }
221 return result;
222 }
223
224 public:
238 template <std::floating_point Real>
239 [[nodiscard]] static ShapeSpaceExtinctionResult<Real> computeExtinctionValues(const MorphologicalTree& tree, std::span<const Real> attribute,
240 ShapeSpaceExtremaPolarity polarity) {
241 constexpr const char* context = "ShapeSpaceSaliency::computeExtinctionValues";
242 validatePolarity(polarity, context);
243 validateRootedTree(tree, context);
244 validateNodeBuffer(tree, attribute, false, "attribute", context);
245
246 std::vector<NodeId> nodes;
247 nodes.reserve(static_cast<std::size_t>(tree.numInternalNodeSlots()));
248 for (NodeId nodeId : tree.aliveNodeIds()) {
249 nodes.push_back(nodeId);
250 }
251
252 const int numSlots = tree.numInternalNodeSlots();
253 const std::size_t slotCount = static_cast<std::size_t>(numSlots);
254
255 std::vector<int> depth(slotCount, -1);
256 std::vector<NodeId> stack;
257 stack.push_back(tree.root());
258 depth[static_cast<std::size_t>(tree.root())] = 0;
259 while (!stack.empty()) {
260 const NodeId nodeId = stack.back();
261 stack.pop_back();
262 for (NodeId childId : detail::TreeKernelAccess::children(tree, nodeId)) {
263 depth[static_cast<std::size_t>(childId)] = depth[static_cast<std::size_t>(nodeId)] + 1;
264 stack.push_back(childId);
265 }
266 }
267
268 const std::span<const PixelId> smallestSupportPixelByNode = detail::TreeKernelAccess::smallestNodeSupportPixels(tree);
269 const std::span<const std::int32_t> supportCardinalityByNode = detail::TreeKernelAccess::nodeSupportCardinalities(tree);
271 if (lhs == rhs) {
272 return false;
273 }
274 const std::size_t lhsIndex = static_cast<std::size_t>(lhs);
275 const std::size_t rhsIndex = static_cast<std::size_t>(rhs);
278 }
281 }
282 if (depth[lhsIndex] != depth[rhsIndex]) {
283 return depth[lhsIndex] < depth[rhsIndex];
284 }
285 throw std::logic_error(std::string(context) +
286 " cannot distinguish two live nodes by spatial support and hierarchy depth.");
287 };
288
289 std::sort(nodes.begin(), nodes.end(), [&](NodeId lhs, NodeId rhs) {
290 const Real lhsLevel = attribute[static_cast<std::size_t>(lhs)];
291 const Real rhsLevel = attribute[static_cast<std::size_t>(rhs)];
292 if (lhsLevel == rhsLevel) {
293 return canonicalShapeSpaceNodePrecedes(lhs, rhs);
294 }
295 if (polarity == ShapeSpaceExtremaPolarity::Minima) {
296 return lhsLevel < rhsLevel;
297 }
298 return rhsLevel < lhsLevel;
299 });
300
301 Real globalMinimum = attribute[static_cast<std::size_t>(nodes.front())];
303 for (NodeId nodeId : nodes) {
304 const Real level = attribute[static_cast<std::size_t>(nodeId)];
305 globalMinimum = std::min(globalMinimum, level);
306 globalMaximum = std::max(globalMaximum, level);
307 }
308
309 std::vector<NodeId> componentParent(slotCount, InvalidNode);
310 std::vector<int> componentSize(slotCount, 0);
311 std::vector<int> survivor(slotCount, -1);
312 std::vector<std::uint8_t> active(slotCount, 0);
313 std::vector<NodeId> plateauRepresentative(slotCount, InvalidNode);
314 std::vector<std::vector<NodeId>> priorComponents(slotCount);
315
316 auto findComponent = [&](NodeId nodeId) {
317 NodeId root = nodeId;
318 while (componentParent[static_cast<std::size_t>(root)] != root) {
319 root = componentParent[static_cast<std::size_t>(root)];
320 }
321 while (componentParent[static_cast<std::size_t>(nodeId)] != nodeId) {
322 const NodeId next = componentParent[static_cast<std::size_t>(nodeId)];
323 componentParent[static_cast<std::size_t>(nodeId)] = root;
324 nodeId = next;
325 }
326 return root;
327 };
328
329 auto joinComponents = [&](NodeId lhs, NodeId rhs) {
330 lhs = findComponent(lhs);
331 rhs = findComponent(rhs);
332 if (lhs == rhs) {
333 return lhs;
334 }
335 const int lhsSize = componentSize[static_cast<std::size_t>(lhs)];
336 const int rhsSize = componentSize[static_cast<std::size_t>(rhs)];
337 if (lhsSize < rhsSize || (lhsSize == rhsSize && canonicalShapeSpaceNodePrecedes(rhs, lhs))) {
338 std::swap(lhs, rhs);
339 }
340 componentParent[static_cast<std::size_t>(rhs)] = lhs;
341 componentSize[static_cast<std::size_t>(lhs)] += componentSize[static_cast<std::size_t>(rhs)];
342 return lhs;
343 };
344
345 auto forEachShapeNeighbor = [&](NodeId nodeId, auto&& visitor) {
346 if (!tree.isRoot(nodeId)) {
347 visitor(detail::TreeKernelAccess::nodeParent(tree, nodeId));
348 }
349 for (NodeId childId : detail::TreeKernelAccess::children(tree, nodeId)) {
350 visitor(childId);
351 }
352 };
353
354 std::vector<ShapeSpaceExtremum<Real>> extrema;
355 std::vector<std::uint8_t> finalized;
356
357 auto finishExtremum = [&](int extremumIndex, Real deathLevel) {
358 if (extremumIndex < 0 || static_cast<std::size_t>(extremumIndex) >= extrema.size() || finalized[static_cast<std::size_t>(extremumIndex)] != 0) {
359 throw std::runtime_error(std::string(context) + " encountered inconsistent component-extremum state.");
360 }
361 ShapeSpaceExtremum<Real>& extremum = extrema[static_cast<std::size_t>(extremumIndex)];
362 extremum.deathLevel = deathLevel;
363 extremum.extinction = checkedExtinction(extremum.birthLevel, deathLevel, polarity, extremum.representative, context);
364 finalized[static_cast<std::size_t>(extremumIndex)] = 1;
365 };
366
367 auto isStronger = [&](int lhsIndex, int rhsIndex) {
368 const auto& lhs = extrema[static_cast<std::size_t>(lhsIndex)];
369 const auto& rhs = extrema[static_cast<std::size_t>(rhsIndex)];
370 if (lhs.birthLevel != rhs.birthLevel) {
371 if (polarity == ShapeSpaceExtremaPolarity::Minima) {
372 return lhs.birthLevel < rhs.birthLevel;
373 }
374 return rhs.birthLevel < lhs.birthLevel;
375 }
376 return canonicalShapeSpaceNodePrecedes(lhs.representative, rhs.representative);
377 };
378
379 std::size_t batchBegin = 0;
380 while (batchBegin < nodes.size()) {
381 const Real level = attribute[static_cast<std::size_t>(nodes[batchBegin])];
382 std::size_t batchEnd = batchBegin + 1;
383 while (batchEnd < nodes.size() && attribute[static_cast<std::size_t>(nodes[batchEnd])] == level) {
384 ++batchEnd;
385 }
386
387 for (std::size_t i = batchBegin; i < batchEnd; ++i) {
388 const NodeId nodeId = nodes[i];
389 const std::size_t index = static_cast<std::size_t>(nodeId);
390 active[index] = 1;
391 componentParent[index] = nodeId;
392 componentSize[index] = 1;
393 survivor[index] = -1;
394 }
395
396 for (std::size_t i = batchBegin; i < batchEnd; ++i) {
397 const NodeId nodeId = nodes[i];
398 forEachShapeNeighbor(nodeId, [&](NodeId neighborId) {
399 if (active[static_cast<std::size_t>(neighborId)] != 0 && attribute[static_cast<std::size_t>(neighborId)] == level) {
400 static_cast<void>(joinComponents(nodeId, neighborId));
401 }
402 });
403 }
404
405 std::vector<NodeId> plateauRoots;
406 plateauRoots.reserve(batchEnd - batchBegin);
407 for (std::size_t i = batchBegin; i < batchEnd; ++i) {
408 const NodeId nodeId = nodes[i];
409 const NodeId plateauRoot = findComponent(nodeId);
410 plateauRoots.push_back(plateauRoot);
411
412 NodeId& representative = plateauRepresentative[static_cast<std::size_t>(plateauRoot)];
413 if (representative == InvalidNode || depth[static_cast<std::size_t>(nodeId)] < depth[static_cast<std::size_t>(representative)] ||
414 (depth[static_cast<std::size_t>(nodeId)] == depth[static_cast<std::size_t>(representative)] &&
415 canonicalShapeSpaceNodePrecedes(nodeId, representative))) {
416 representative = nodeId;
417 }
418
419 forEachShapeNeighbor(nodeId, [&](NodeId neighborId) {
420 if (active[static_cast<std::size_t>(neighborId)] != 0 && attribute[static_cast<std::size_t>(neighborId)] != level) {
421 priorComponents[static_cast<std::size_t>(plateauRoot)].push_back(findComponent(neighborId));
422 }
423 });
424 }
425
426 std::sort(plateauRoots.begin(), plateauRoots.end(), canonicalShapeSpaceNodePrecedes);
427 plateauRoots.erase(std::unique(plateauRoots.begin(), plateauRoots.end()), plateauRoots.end());
428
429 for (NodeId plateauRoot : plateauRoots) {
430 auto& adjacentComponents = priorComponents[static_cast<std::size_t>(plateauRoot)];
431 for (NodeId& component : adjacentComponents) {
432 component = findComponent(component);
433 }
434 std::sort(adjacentComponents.begin(), adjacentComponents.end(), canonicalShapeSpaceNodePrecedes);
435 adjacentComponents.erase(std::unique(adjacentComponents.begin(), adjacentComponents.end()), adjacentComponents.end());
436
437 int winningExtremum = -1;
438 if (adjacentComponents.empty()) {
439 const NodeId representative = plateauRepresentative[static_cast<std::size_t>(plateauRoot)];
440 winningExtremum = static_cast<int>(extrema.size());
441 extrema.push_back(ShapeSpaceExtremum<Real>{representative, level, level, Real{0}});
442 finalized.push_back(0);
443 } else {
444 for (NodeId component : adjacentComponents) {
445 const int candidate = survivor[static_cast<std::size_t>(component)];
446 if (candidate < 0) {
447 throw std::runtime_error(std::string(context) + " found an active level component without a surviving extremum.");
448 }
449 if (winningExtremum < 0 || isStronger(candidate, winningExtremum)) {
450 winningExtremum = candidate;
451 }
452 }
453
454 for (NodeId component : adjacentComponents) {
455 const int candidate = survivor[static_cast<std::size_t>(component)];
456 if (candidate != winningExtremum) {
457 finishExtremum(candidate, level);
458 }
459 }
460 }
461
462 NodeId combinedRoot = plateauRoot;
463 for (NodeId component : adjacentComponents) {
464 combinedRoot = joinComponents(combinedRoot, component);
465 }
466 survivor[static_cast<std::size_t>(combinedRoot)] = winningExtremum;
467
468 plateauRepresentative[static_cast<std::size_t>(plateauRoot)] = InvalidNode;
469 adjacentComponents.clear();
470 }
471
472 batchBegin = batchEnd;
473 }
474
475 const NodeId finalComponent = findComponent(nodes.front());
476 for (NodeId nodeId : nodes) {
477 if (findComponent(nodeId) != finalComponent) {
478 throw std::runtime_error(std::string(context) + " did not produce one connected final level component.");
479 }
480 }
481
482 const int dominantExtremum = survivor[static_cast<std::size_t>(finalComponent)];
483 if (dominantExtremum < 0) {
484 throw std::runtime_error(std::string(context) + " did not retain a dominant extremum.");
485 }
486 finishExtremum(dominantExtremum, polarity == ShapeSpaceExtremaPolarity::Minima ? globalMaximum : globalMinimum);
487
488 for (std::uint8_t isFinalized : finalized) {
489 if (isFinalized == 0) {
490 throw std::runtime_error(std::string(context) + " left an extremum without a death level.");
491 }
492 }
493
494 std::sort(extrema.begin(), extrema.end(),
495 [&](const auto& lhs, const auto& rhs) { return canonicalShapeSpaceNodePrecedes(lhs.representative, rhs.representative); });
496
497 ShapeSpaceExtinctionResult<Real> result;
498 result.extrema = std::move(extrema);
499 result.nodeScores.assign(slotCount, Real{0});
500 for (const ShapeSpaceExtremum<Real>& extremum : result.extrema) {
501 result.nodeScores[static_cast<std::size_t>(extremum.representative)] = extremum.extinction;
502 }
503 return result;
504 }
505
518 template <std::floating_point Real>
519 [[nodiscard]] static EdgeSaliencyMap<Real> projectContourScores(const MorphologicalTree& tree, std::span<const Real> nodeScores,
520 const RegularGridAdjacency2D& adjacency) {
521 constexpr const char* context = "ShapeSpaceSaliency::projectContourScores";
522 validateTreeAndAdjacency(tree, adjacency, context);
523 validateNodeBuffer(tree, nodeScores, true, "node-score", context);
524
525 return projectContourScoresEstablished(tree, nodeScores, adjacency);
526 }
527
535 template <std::floating_point Real>
536 [[nodiscard]] static EdgeSaliencyMap<Real> projectContourScores(const MorphologicalTree& tree, std::span<const Real> nodeScores) {
537 return projectContourScores(tree, nodeScores, requireStoredAdjacency(tree, "ShapeSpaceSaliency::projectContourScores"));
538 }
539
549 template <std::floating_point Real>
550 [[nodiscard]] static ShapeSpaceSaliencyResult<Real> compute(const MorphologicalTree& tree, std::span<const Real> attribute,
551 ShapeSpaceExtremaPolarity polarity, const RegularGridAdjacency2D& adjacency) {
553 ShapeSpaceExtinctionResult<Real> extinction = computeExtinctionValues(tree, attribute, polarity);
554 result.extrema = std::move(extinction.extrema);
555 result.nodeScores = std::move(extinction.nodeScores);
556 validateTreeAndAdjacency(tree, adjacency, "ShapeSpaceSaliency::compute");
557 result.edgeMap = projectContourScoresEstablished(tree, std::span<const Real>(result.nodeScores), adjacency);
558 return result;
559 }
560
569 template <std::floating_point Real>
570 [[nodiscard]] static ShapeSpaceSaliencyResult<Real> compute(const MorphologicalTree& tree, std::span<const Real> attribute,
571 ShapeSpaceExtremaPolarity polarity) {
572 return compute(tree, attribute, polarity, requireStoredAdjacency(tree, "ShapeSpaceSaliency::compute"));
573 }
574
575 private:
584 template <std::floating_point Real>
585 [[nodiscard]] static EdgeSaliencyMap<Real> projectContourScoresEstablished(const MorphologicalTree& tree,
586 std::span<const Real> nodeScores, const RegularGridAdjacency2D& adjacency) {
587 EdgeSaliencyMap<Real> edgeMap;
588 edgeMap.numRows = tree.numRows();
589 edgeMap.numColumns = tree.numColumns();
590 edgeMap.adjacencyRadius = adjacency.getRadius();
591
592 const std::size_t slotCount = static_cast<std::size_t>(tree.numInternalNodeSlots());
593 const std::size_t liftingLevelCount = std::max<std::size_t>(1, static_cast<std::size_t>(std::bit_width(slotCount - 1)));
594
595 std::vector<int> depth(slotCount, -1);
596 std::vector<std::vector<NodeId>> ancestor(liftingLevelCount, std::vector<NodeId>(slotCount, InvalidNode));
597 std::vector<std::vector<Real>> pathMaximum(liftingLevelCount, std::vector<Real>(slotCount, Real{0}));
598
599 std::vector<NodeId> stack{tree.root()};
600 depth[static_cast<std::size_t>(tree.root())] = 0;
601 while (!stack.empty()) {
602 const NodeId nodeId = stack.back();
603 stack.pop_back();
604 const std::size_t nodeIndex = static_cast<std::size_t>(nodeId);
605 ancestor[0][nodeIndex] = detail::TreeKernelAccess::nodeParent(tree, nodeId);
606 pathMaximum[0][nodeIndex] = nodeScores[nodeIndex];
607
608 for (NodeId childId : detail::TreeKernelAccess::children(tree, nodeId)) {
609 depth[static_cast<std::size_t>(childId)] = depth[nodeIndex] + 1;
610 stack.push_back(childId);
611 }
612 }
613
614 for (std::size_t level = 1; level < liftingLevelCount; ++level) {
615 for (NodeId nodeId : tree.aliveNodeIds()) {
616 const std::size_t nodeIndex = static_cast<std::size_t>(nodeId);
617 const NodeId middle = ancestor[level - 1][nodeIndex];
618 const std::size_t middleIndex = static_cast<std::size_t>(middle);
619 ancestor[level][nodeIndex] = ancestor[level - 1][middleIndex];
620 pathMaximum[level][nodeIndex] = std::max(pathMaximum[level - 1][nodeIndex], pathMaximum[level - 1][middleIndex]);
621 }
622 }
623
624 auto accumulateBranch = [&](NodeId smallestNode, NodeId lca, Real& edgeScore) {
625 const int smallestNodeDepth = depth[static_cast<std::size_t>(smallestNode)];
626 const int lcaDepth = depth[static_cast<std::size_t>(lca)];
627 NodeId current = smallestNode;
628 std::size_t remaining = static_cast<std::size_t>(smallestNodeDepth - lcaDepth);
629 std::size_t level = 0;
630 while (remaining != 0) {
631 if ((remaining & std::size_t{1}) != 0) {
632 const std::size_t currentIndex = static_cast<std::size_t>(current);
633 edgeScore = std::max(edgeScore, pathMaximum[level][currentIndex]);
634 current = ancestor[level][currentIndex];
635 }
636 remaining >>= 1;
637 ++level;
638 }
639 };
640
641 const int numPixels = tree.numPixels();
642 for (NodeId source = 0; source < numPixels; ++source) {
643 const NodeId sourceSmallestNode = detail::TreeKernelAccess::smallestNodeMap(tree, source);
644
645 for (int targetValue : detail::CommittedGridAccess::forwardNeighbors(adjacency, source)) {
646 const NodeId target = static_cast<NodeId>(targetValue);
647
648 const NodeId targetSmallestNode = detail::TreeKernelAccess::smallestNodeMap(tree, target);
649
650 const NodeId lca = detail::TreeKernelAccess::lowestCommonAncestor(tree, sourceSmallestNode, targetSmallestNode);
651
652 Real edgeScore{0};
653 accumulateBranch(sourceSmallestNode, lca, edgeScore);
654 accumulateBranch(targetSmallestNode, lca, edgeScore);
655
656 edgeMap.sources.push_back(source);
657 edgeMap.targets.push_back(target);
658 edgeMap.values.push_back(edgeScore);
659 }
660 }
661
662 return edgeMap;
663 }
664
665};
666
667} // namespace mmcfilters
int NodeId
Node identifier type used throughout the project.
Definition Common.hpp:17
constexpr NodeId InvalidNode
Sentinel value used to denote an invalid node identifier.
Definition Common.hpp:34
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.
int numPixels() const
Returns the cardinality of the pixel domain.
bool isRoot(NodeId nodeId) const
Tests whether nodeId is the current root.
const RegularGridAdjacency2D * constructionAdjacency() const noexcept
Returns the adjacency retained by a shared construction context.
AliveNodeRange aliveNodeIds() const
Returns a fail-fast range over all live node ids.
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.
double getRadius() const noexcept
Returns the configured or bounding Euclidean radius.
int getNumColumns() const noexcept
Returns the number of columns in the attached grid domain.
int getNumRows() const noexcept
Returns the number of rows in the attached grid domain.
Computes Xu-style extinction values in the shape space of a tree.
static ShapeSpaceSaliencyResult< Real > compute(const MorphologicalTree &tree, std::span< const Real > attribute, ShapeSpaceExtremaPolarity polarity, const RegularGridAdjacency2D &adjacency)
Computes extinction values, sparse representative scores, and contours.
static ShapeSpaceExtinctionResult< Real > computeExtinctionValues(const MorphologicalTree &tree, std::span< const Real > attribute, ShapeSpaceExtremaPolarity polarity)
Computes regional extrema and finite extinction values.
static ShapeSpaceSaliencyResult< Real > compute(const MorphologicalTree &tree, std::span< const Real > attribute, ShapeSpaceExtremaPolarity polarity)
Computes the complete result using the adjacency stored by the tree.
static EdgeSaliencyMap< Real > projectContourScores(const MorphologicalTree &tree, std::span< const Real > nodeScores, const RegularGridAdjacency2D &adjacency)
Projects sparse node scores onto every image-domain adjacency edge.
static EdgeSaliencyMap< Real > projectContourScores(const MorphologicalTree &tree, std::span< const Real > nodeScores)
Projects node scores using the adjacency stored by the tree.
Owning result for one computed scalar attribute layout and buffer.
std::vector< Real > & values() noexcept
Returns the mutable flat attribute buffer.
Regional extrema and their sparse dense-domain node scores.
std::vector< ShapeSpaceExtremum< Real > > extrema
Regional extrema ordered by the computation.
std::vector< Real > nodeScores
Sparse extinction score stored in the dense node-id domain.
One regional extremum and its extinction interval in attribute space.
NodeId representative
Stable representative node of the regional extremum.
Real birthLevel
Attribute level at which the extremum appears.
Real deathLevel
Attribute level at which the extremum is absorbed.
Real extinction
Absolute difference between death and birth levels.
Extrema, their sparse node scores, and the projected contour map.
std::vector< Real > nodeScores
Sparse extinction score stored in the dense node-id domain.
std::vector< ShapeSpaceExtremum< Real > > extrema
Regional extrema ordered by the computation.
EdgeSaliencyMap< Real > edgeMap
Image-adjacency edge map obtained from the node scores.