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/CommittedTreeAccess.hpp"
8#include "../detail/MorphologicalTreeConstructionContextQueries.hpp"
9
10#include <algorithm>
11#include <bit>
12#include <cmath>
13#include <concepts>
14#include <cstddef>
15#include <cstdint>
16#include <limits>
17#include <sstream>
18#include <span>
19#include <stdexcept>
20#include <string>
21#include <utility>
22#include <vector>
23
24namespace mmcfilters {
25
29enum class ShapeSpaceExtremaPolarity { Minima, Maxima };
30
34template <std::floating_point Real> struct ShapeSpaceExtremum {
38 Real birthLevel{};
40 Real deathLevel{};
42 Real extinction{};
43};
44
48template <std::floating_point Real> struct ShapeSpaceExtinctionResult {
50 std::vector<ShapeSpaceExtremum<Real>> extrema;
52 std::vector<Real> nodeScores;
53};
54
58template <std::floating_point Real> struct ShapeSpaceSaliencyResult {
60 std::vector<ShapeSpaceExtremum<Real>> extrema;
62 std::vector<Real> nodeScores;
65};
66
88 private:
95 static void validatePolarity(ShapeSpaceExtremaPolarity polarity, const char* context) {
96 switch (polarity) {
97 case ShapeSpaceExtremaPolarity::Minima:
98 case ShapeSpaceExtremaPolarity::Maxima:
99 return;
100 }
101 throw std::invalid_argument(std::string(context) + " received an unknown extrema polarity.");
102 }
103
110 static void validateRootedTree(const MorphologicalTree& tree, const char* context) { detail::requireCommittedRootedHierarchy(tree, context); }
111
121 template <std::floating_point Real>
122 static void validateNodeBuffer(const MorphologicalTree& tree, std::span<const Real> values, bool requireNonNegative, const char* valueName,
123 const char* context) {
124 const std::size_t expected = static_cast<std::size_t>(tree.numInternalNodeSlots());
125 if (values.size() != expected) {
126 std::ostringstream oss;
127 oss << context << " requires one " << valueName << " value per dense internal node slot; expected " << expected << " values but got "
128 << values.size() << ".";
129 throw std::invalid_argument(oss.str());
130 }
131
132 for (NodeId nodeId : tree.aliveNodeIds()) {
133 const Real value = values[static_cast<std::size_t>(nodeId)];
134 if (!std::isfinite(value)) {
135 std::ostringstream oss;
136 oss << context << " requires finite " << valueName << " values; node " << nodeId << " is non-finite.";
137 throw std::invalid_argument(oss.str());
138 }
139 if (requireNonNegative && value < Real{0}) {
140 std::ostringstream oss;
141 oss << context << " requires non-negative " << valueName << " values; node " << nodeId << " has value " << value << ".";
142 throw std::invalid_argument(oss.str());
143 }
144 }
145 }
146
154 static void validateTreeAndAdjacency(const MorphologicalTree& tree, const RegularGridAdjacency2D& adjacency, const char* context) {
155 validateRootedTree(tree, context);
156
157 const int rows = tree.numRows();
158 const int columns = tree.numColumns();
159 const int numPixels = tree.numPixels();
160 if (rows <= 0 || columns <= 0 || numPixels <= 0) {
161 throw std::invalid_argument(std::string(context) + " requires a non-empty image/pixel domain.");
162 }
163
164 const long long pixelCount = static_cast<long long>(rows) * static_cast<long long>(columns);
165 if (pixelCount != static_cast<long long>(numPixels)) {
166 std::ostringstream oss;
167 oss << context << " requires the tree pixel domain to match the image grid; got " << numPixels << " pixels for a " << rows << "x" << columns
168 << " image domain.";
169 throw std::invalid_argument(oss.str());
170 }
171
172 if (adjacency.getNumRows() != rows || adjacency.getNumColumns() != columns) {
173 std::ostringstream oss;
174 oss << context << " adjacency domain must match the tree image domain; got " << adjacency.getNumRows() << "x" << adjacency.getNumColumns()
175 << " for a tree domain of " << rows << "x" << columns << ".";
176 throw std::invalid_argument(oss.str());
177 }
178 }
179
187 static const RegularGridAdjacency2D& requireStoredAdjacency(const MorphologicalTree& tree, const char* context) {
188 const RegularGridAdjacency2D* adjacency = ::mmcfilters::detail::constructionAdjacency(tree);
189 if (adjacency == nullptr) {
190 throw std::invalid_argument(std::string(context) + " requires an attached adjacency relation; pass an explicit adjacency relation instead.");
191 }
192 return *adjacency;
193 }
194
205 template <std::floating_point Real>
206 static Real checkedExtinction(Real birthLevel, Real deathLevel, ShapeSpaceExtremaPolarity polarity, NodeId representative, const char* context) {
207 const long double birth = static_cast<long double>(birthLevel);
208 const long double death = static_cast<long double>(deathLevel);
209 const long double difference = polarity == ShapeSpaceExtremaPolarity::Minima ? death - birth : birth - death;
210
211 if (difference < 0.0L) {
212 std::ostringstream oss;
213 oss << context << " produced an invalid extinction interval for representative node " << representative << ".";
214 throw std::invalid_argument(oss.str());
215 }
216
217 const long double maximum = static_cast<long double>(std::numeric_limits<Real>::max());
218 if (!std::isfinite(difference) || difference > maximum) {
219 std::ostringstream oss;
220 oss << context << " extinction for representative node " << representative << " is not representable by the requested floating-point type.";
221 throw std::overflow_error(oss.str());
222 }
223
224 const Real result = static_cast<Real>(difference);
225 if (!std::isfinite(result)) {
226 std::ostringstream oss;
227 oss << context << " extinction for representative node " << representative << " overflowed the requested floating-point type.";
228 throw std::overflow_error(oss.str());
229 }
230 return result;
231 }
232
233 public:
247 template <std::floating_point Real>
248 [[nodiscard]] static ShapeSpaceExtinctionResult<Real> computeExtinctionValues(const MorphologicalTree& tree, std::span<const Real> attribute,
249 ShapeSpaceExtremaPolarity polarity) {
250 constexpr const char* context = "ShapeSpaceSaliency::computeExtinctionValues";
251 validatePolarity(polarity, context);
252 validateRootedTree(tree, context);
253 validateNodeBuffer(tree, attribute, false, "attribute", context);
254
255 std::vector<NodeId> nodes;
256 nodes.reserve(static_cast<std::size_t>(tree.numInternalNodeSlots()));
257 for (NodeId nodeId : tree.aliveNodeIds()) {
258 nodes.push_back(nodeId);
259 }
260 if (nodes.empty()) {
261 throw std::invalid_argument(std::string(context) + " requires at least one live node.");
262 }
263
264 const int numSlots = tree.numInternalNodeSlots();
265 const std::size_t slotCount = static_cast<std::size_t>(numSlots);
266
267 std::vector<int> depth(slotCount, -1);
268 std::vector<NodeId> stack;
269 stack.push_back(tree.root());
270 depth[static_cast<std::size_t>(tree.root())] = 0;
271 while (!stack.empty()) {
272 const NodeId nodeId = stack.back();
273 stack.pop_back();
274 for (NodeId childId : tree.children(nodeId)) {
275 depth[static_cast<std::size_t>(childId)] = depth[static_cast<std::size_t>(nodeId)] + 1;
276 stack.push_back(childId);
277 }
278 }
279
280 const std::span<const PixelId> smallestSupportPixelByNode = detail::CommittedTreeAccess::smallestNodeSupportPixels(tree);
281 const std::span<const std::int32_t> supportCardinalityByNode = detail::CommittedTreeAccess::nodeSupportCardinalities(tree);
283 if (lhs == rhs) {
284 return false;
285 }
286 const std::size_t lhsIndex = static_cast<std::size_t>(lhs);
287 const std::size_t rhsIndex = static_cast<std::size_t>(rhs);
290 }
293 }
294 if (depth[lhsIndex] != depth[rhsIndex]) {
295 return depth[lhsIndex] < depth[rhsIndex];
296 }
297 throw std::logic_error(std::string(context) +
298 " cannot distinguish two live nodes by spatial support and hierarchy depth.");
299 };
300
301 std::sort(nodes.begin(), nodes.end(), [&](NodeId lhs, NodeId rhs) {
302 const Real lhsLevel = attribute[static_cast<std::size_t>(lhs)];
303 const Real rhsLevel = attribute[static_cast<std::size_t>(rhs)];
304 if (lhsLevel == rhsLevel) {
305 return canonicalShapeSpaceNodePrecedes(lhs, rhs);
306 }
307 if (polarity == ShapeSpaceExtremaPolarity::Minima) {
308 return lhsLevel < rhsLevel;
309 }
310 return rhsLevel < lhsLevel;
311 });
312
313 Real globalMinimum = attribute[static_cast<std::size_t>(nodes.front())];
315 for (NodeId nodeId : nodes) {
316 const Real level = attribute[static_cast<std::size_t>(nodeId)];
317 globalMinimum = std::min(globalMinimum, level);
318 globalMaximum = std::max(globalMaximum, level);
319 }
320
321 std::vector<NodeId> componentParent(slotCount, InvalidNode);
322 std::vector<int> componentSize(slotCount, 0);
323 std::vector<int> survivor(slotCount, -1);
324 std::vector<std::uint8_t> active(slotCount, 0);
325 std::vector<NodeId> plateauRepresentative(slotCount, InvalidNode);
326 std::vector<std::vector<NodeId>> priorComponents(slotCount);
327
328 auto findComponent = [&](NodeId nodeId) {
329 NodeId root = nodeId;
330 while (componentParent[static_cast<std::size_t>(root)] != root) {
331 root = componentParent[static_cast<std::size_t>(root)];
332 }
333 while (componentParent[static_cast<std::size_t>(nodeId)] != nodeId) {
334 const NodeId next = componentParent[static_cast<std::size_t>(nodeId)];
335 componentParent[static_cast<std::size_t>(nodeId)] = root;
336 nodeId = next;
337 }
338 return root;
339 };
340
341 auto joinComponents = [&](NodeId lhs, NodeId rhs) {
342 lhs = findComponent(lhs);
343 rhs = findComponent(rhs);
344 if (lhs == rhs) {
345 return lhs;
346 }
347 const int lhsSize = componentSize[static_cast<std::size_t>(lhs)];
348 const int rhsSize = componentSize[static_cast<std::size_t>(rhs)];
349 if (lhsSize < rhsSize || (lhsSize == rhsSize && canonicalShapeSpaceNodePrecedes(rhs, lhs))) {
350 std::swap(lhs, rhs);
351 }
352 componentParent[static_cast<std::size_t>(rhs)] = lhs;
353 componentSize[static_cast<std::size_t>(lhs)] += componentSize[static_cast<std::size_t>(rhs)];
354 return lhs;
355 };
356
357 auto forEachShapeNeighbor = [&](NodeId nodeId, auto&& visitor) {
358 if (!tree.isRoot(nodeId)) {
359 visitor(tree.parent(nodeId));
360 }
361 for (NodeId childId : tree.children(nodeId)) {
362 visitor(childId);
363 }
364 };
365
366 std::vector<ShapeSpaceExtremum<Real>> extrema;
367 std::vector<std::uint8_t> finalized;
368
369 auto finishExtremum = [&](int extremumIndex, Real deathLevel) {
370 if (extremumIndex < 0 || static_cast<std::size_t>(extremumIndex) >= extrema.size() || finalized[static_cast<std::size_t>(extremumIndex)] != 0) {
371 throw std::runtime_error(std::string(context) + " encountered inconsistent component-extremum state.");
372 }
373 ShapeSpaceExtremum<Real>& extremum = extrema[static_cast<std::size_t>(extremumIndex)];
374 extremum.deathLevel = deathLevel;
375 extremum.extinction = checkedExtinction(extremum.birthLevel, deathLevel, polarity, extremum.representative, context);
376 finalized[static_cast<std::size_t>(extremumIndex)] = 1;
377 };
378
379 auto isStronger = [&](int lhsIndex, int rhsIndex) {
380 const auto& lhs = extrema[static_cast<std::size_t>(lhsIndex)];
381 const auto& rhs = extrema[static_cast<std::size_t>(rhsIndex)];
382 if (lhs.birthLevel != rhs.birthLevel) {
383 if (polarity == ShapeSpaceExtremaPolarity::Minima) {
384 return lhs.birthLevel < rhs.birthLevel;
385 }
386 return rhs.birthLevel < lhs.birthLevel;
387 }
388 return canonicalShapeSpaceNodePrecedes(lhs.representative, rhs.representative);
389 };
390
391 std::size_t batchBegin = 0;
392 while (batchBegin < nodes.size()) {
393 const Real level = attribute[static_cast<std::size_t>(nodes[batchBegin])];
394 std::size_t batchEnd = batchBegin + 1;
395 while (batchEnd < nodes.size() && attribute[static_cast<std::size_t>(nodes[batchEnd])] == level) {
396 ++batchEnd;
397 }
398
399 for (std::size_t i = batchBegin; i < batchEnd; ++i) {
400 const NodeId nodeId = nodes[i];
401 const std::size_t index = static_cast<std::size_t>(nodeId);
402 active[index] = 1;
403 componentParent[index] = nodeId;
404 componentSize[index] = 1;
405 survivor[index] = -1;
406 }
407
408 for (std::size_t i = batchBegin; i < batchEnd; ++i) {
409 const NodeId nodeId = nodes[i];
410 forEachShapeNeighbor(nodeId, [&](NodeId neighborId) {
411 if (active[static_cast<std::size_t>(neighborId)] != 0 && attribute[static_cast<std::size_t>(neighborId)] == level) {
412 static_cast<void>(joinComponents(nodeId, neighborId));
413 }
414 });
415 }
416
417 std::vector<NodeId> plateauRoots;
418 plateauRoots.reserve(batchEnd - batchBegin);
419 for (std::size_t i = batchBegin; i < batchEnd; ++i) {
420 const NodeId nodeId = nodes[i];
421 const NodeId plateauRoot = findComponent(nodeId);
422 plateauRoots.push_back(plateauRoot);
423
424 NodeId& representative = plateauRepresentative[static_cast<std::size_t>(plateauRoot)];
425 if (representative == InvalidNode || depth[static_cast<std::size_t>(nodeId)] < depth[static_cast<std::size_t>(representative)] ||
426 (depth[static_cast<std::size_t>(nodeId)] == depth[static_cast<std::size_t>(representative)] &&
427 canonicalShapeSpaceNodePrecedes(nodeId, representative))) {
428 representative = nodeId;
429 }
430
431 forEachShapeNeighbor(nodeId, [&](NodeId neighborId) {
432 if (active[static_cast<std::size_t>(neighborId)] != 0 && attribute[static_cast<std::size_t>(neighborId)] != level) {
433 priorComponents[static_cast<std::size_t>(plateauRoot)].push_back(findComponent(neighborId));
434 }
435 });
436 }
437
438 std::sort(plateauRoots.begin(), plateauRoots.end(), canonicalShapeSpaceNodePrecedes);
439 plateauRoots.erase(std::unique(plateauRoots.begin(), plateauRoots.end()), plateauRoots.end());
440
441 for (NodeId plateauRoot : plateauRoots) {
442 auto& adjacentComponents = priorComponents[static_cast<std::size_t>(plateauRoot)];
443 for (NodeId& component : adjacentComponents) {
444 component = findComponent(component);
445 }
446 std::sort(adjacentComponents.begin(), adjacentComponents.end(), canonicalShapeSpaceNodePrecedes);
447 adjacentComponents.erase(std::unique(adjacentComponents.begin(), adjacentComponents.end()), adjacentComponents.end());
448
449 int winningExtremum = -1;
450 if (adjacentComponents.empty()) {
451 const NodeId representative = plateauRepresentative[static_cast<std::size_t>(plateauRoot)];
452 winningExtremum = static_cast<int>(extrema.size());
453 extrema.push_back(ShapeSpaceExtremum<Real>{representative, level, level, Real{0}});
454 finalized.push_back(0);
455 } else {
456 for (NodeId component : adjacentComponents) {
457 const int candidate = survivor[static_cast<std::size_t>(component)];
458 if (candidate < 0) {
459 throw std::runtime_error(std::string(context) + " found an active level component without a surviving extremum.");
460 }
461 if (winningExtremum < 0 || isStronger(candidate, winningExtremum)) {
462 winningExtremum = candidate;
463 }
464 }
465
466 for (NodeId component : adjacentComponents) {
467 const int candidate = survivor[static_cast<std::size_t>(component)];
468 if (candidate != winningExtremum) {
469 finishExtremum(candidate, level);
470 }
471 }
472 }
473
474 NodeId combinedRoot = plateauRoot;
475 for (NodeId component : adjacentComponents) {
476 combinedRoot = joinComponents(combinedRoot, component);
477 }
478 survivor[static_cast<std::size_t>(combinedRoot)] = winningExtremum;
479
480 plateauRepresentative[static_cast<std::size_t>(plateauRoot)] = InvalidNode;
481 adjacentComponents.clear();
482 }
483
484 batchBegin = batchEnd;
485 }
486
487 const NodeId finalComponent = findComponent(nodes.front());
488 for (NodeId nodeId : nodes) {
489 if (findComponent(nodeId) != finalComponent) {
490 throw std::runtime_error(std::string(context) + " did not produce one connected final level component.");
491 }
492 }
493
494 const int dominantExtremum = survivor[static_cast<std::size_t>(finalComponent)];
495 if (dominantExtremum < 0) {
496 throw std::runtime_error(std::string(context) + " did not retain a dominant extremum.");
497 }
498 finishExtremum(dominantExtremum, polarity == ShapeSpaceExtremaPolarity::Minima ? globalMaximum : globalMinimum);
499
500 for (std::uint8_t isFinalized : finalized) {
501 if (isFinalized == 0) {
502 throw std::runtime_error(std::string(context) + " left an extremum without a death level.");
503 }
504 }
505
506 std::sort(extrema.begin(), extrema.end(),
507 [&](const auto& lhs, const auto& rhs) { return canonicalShapeSpaceNodePrecedes(lhs.representative, rhs.representative); });
508
509 ShapeSpaceExtinctionResult<Real> result;
510 result.extrema = std::move(extrema);
511 result.nodeScores.assign(slotCount, Real{0});
512 for (const ShapeSpaceExtremum<Real>& extremum : result.extrema) {
513 result.nodeScores[static_cast<std::size_t>(extremum.representative)] = extremum.extinction;
514 }
515 return result;
516 }
517
530 template <std::floating_point Real>
531 [[nodiscard]] static EdgeSaliencyMap<Real> projectContourScores(const MorphologicalTree& tree, std::span<const Real> nodeScores,
532 const RegularGridAdjacency2D& adjacency) {
533 constexpr const char* context = "ShapeSpaceSaliency::projectContourScores";
534 validateTreeAndAdjacency(tree, adjacency, context);
535 validateNodeBuffer(tree, nodeScores, true, "node-score", context);
536
537 EdgeSaliencyMap<Real> edgeMap;
538 edgeMap.numRows = tree.numRows();
539 edgeMap.numColumns = tree.numColumns();
540 edgeMap.adjacencyRadius = adjacency.getRadius();
541
542 const std::size_t slotCount = static_cast<std::size_t>(tree.numInternalNodeSlots());
543 const std::size_t liftingLevelCount = std::max<std::size_t>(1, static_cast<std::size_t>(std::bit_width(slotCount - 1)));
544
545 std::vector<int> depth(slotCount, -1);
546 std::vector<std::vector<NodeId>> ancestor(liftingLevelCount, std::vector<NodeId>(slotCount, InvalidNode));
547 std::vector<std::vector<Real>> pathMaximum(liftingLevelCount, std::vector<Real>(slotCount, Real{0}));
548
549 std::vector<NodeId> stack{tree.root()};
550 depth[static_cast<std::size_t>(tree.root())] = 0;
551 while (!stack.empty()) {
552 const NodeId nodeId = stack.back();
553 stack.pop_back();
554 const std::size_t nodeIndex = static_cast<std::size_t>(nodeId);
555 ancestor[0][nodeIndex] = tree.parent(nodeId);
556 pathMaximum[0][nodeIndex] = nodeScores[nodeIndex];
557
558 for (NodeId childId : tree.children(nodeId)) {
559 depth[static_cast<std::size_t>(childId)] = depth[nodeIndex] + 1;
560 stack.push_back(childId);
561 }
562 }
563
564 for (std::size_t level = 1; level < liftingLevelCount; ++level) {
565 for (NodeId nodeId : tree.aliveNodeIds()) {
566 const std::size_t nodeIndex = static_cast<std::size_t>(nodeId);
567 const NodeId middle = ancestor[level - 1][nodeIndex];
568 const std::size_t middleIndex = static_cast<std::size_t>(middle);
571 }
572 }
573
574 auto accumulateBranch = [&](NodeId smallestNode, NodeId lca, Real& edgeScore) {
575 const int smallestNodeDepth = depth[static_cast<std::size_t>(smallestNode)];
576 const int lcaDepth = depth[static_cast<std::size_t>(lca)];
578 throw std::runtime_error(std::string(context) + " found an LCA below an edge-endpoint smallestNode.");
579 }
580
581 NodeId current = smallestNode;
582 std::size_t remaining = static_cast<std::size_t>(smallestNodeDepth - lcaDepth);
583 std::size_t level = 0;
584 while (remaining != 0) {
585 if ((remaining & std::size_t{1}) != 0) {
586 const std::size_t currentIndex = static_cast<std::size_t>(current);
589 }
590 remaining >>= 1;
591 ++level;
592 }
593 if (current != lca) {
594 throw std::runtime_error(std::string(context) + " encountered a branch that does not reach its LCA.");
595 }
596 };
597
598 const int numPixels = tree.numPixels();
599 for (NodeId source = 0; source < numPixels; ++source) {
600 const NodeId sourceSmallestNode = tree.smallestNode(source);
601 if (!tree.isAlive(sourceSmallestNode)) {
602 throw std::runtime_error(std::string(context) + " found a pixel without a live smallest node.");
603 }
604
605 for (int targetValue : adjacency.getForwardNeighborIndices(source)) {
606 const NodeId target = static_cast<NodeId>(targetValue);
607 if (target < 0 || target >= numPixels) {
608 throw std::runtime_error(std::string(context) + " adjacency produced an endpoint outside the pixel domain.");
609 }
610
611 const NodeId targetSmallestNode = tree.smallestNode(target);
612 if (!tree.isAlive(targetSmallestNode)) {
613 throw std::runtime_error(std::string(context) + " found a neighbour pixel without a live smallest node.");
614 }
615
617 if (lca == InvalidNode || !tree.isAlive(lca)) {
618 throw std::runtime_error(std::string(context) + " could not find a live LCA for an adjacency edge.");
619 }
620
621 Real edgeScore{0};
624
625 edgeMap.sources.push_back(source);
626 edgeMap.targets.push_back(target);
627 edgeMap.values.push_back(edgeScore);
628 }
629 }
630
631 return edgeMap;
632 }
633
641 template <std::floating_point Real>
642 [[nodiscard]] static EdgeSaliencyMap<Real> projectContourScores(const MorphologicalTree& tree, std::span<const Real> nodeScores) {
643 return projectContourScores(tree, nodeScores, requireStoredAdjacency(tree, "ShapeSpaceSaliency::projectContourScores"));
644 }
645
655 template <std::floating_point Real>
656 [[nodiscard]] static ShapeSpaceSaliencyResult<Real> compute(const MorphologicalTree& tree, std::span<const Real> attribute,
657 ShapeSpaceExtremaPolarity polarity, const RegularGridAdjacency2D& adjacency) {
659 ShapeSpaceExtinctionResult<Real> extinction = computeExtinctionValues(tree, attribute, polarity);
660 result.extrema = std::move(extinction.extrema);
661 result.nodeScores = std::move(extinction.nodeScores);
662 result.edgeMap = projectContourScores(tree, std::span<const Real>(result.nodeScores), adjacency);
663 return result;
664 }
665
674 template <std::floating_point Real>
675 [[nodiscard]] static ShapeSpaceSaliencyResult<Real> compute(const MorphologicalTree& tree, std::span<const Real> attribute,
676 ShapeSpaceExtremaPolarity polarity) {
677 return compute(tree, attribute, polarity, requireStoredAdjacency(tree, "ShapeSpaceSaliency::compute"));
678 }
679};
680
681} // 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.
bool isAlive(NodeId nodeId) const
Tests whether a node slot currently represents a live node.
int numPixels() const
Returns the cardinality of the pixel domain.
bool isRoot(NodeId nodeId) const
Tests whether nodeId is the current root.
AliveNodeRange aliveNodeIds() const
Returns a fail-fast range over all live node ids.
NodeId smallestNode(PixelId pixel) const
Returns the smallest node containing pixel.
NodeId lowestCommonAncestor(NodeId u, NodeId v) const
Returns the lowest common ancestor of u and v.
ChildrenRange children(NodeId nodeId) const
Returns a fail-fast range over the direct children of nodeId.
int numColumns() const
Returns the number of columns in the regular 2D pixel domain.
NodeId parent(NodeId nodeId) const
Returns the direct parent of nodeId.
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.
ForwardNeighborIndexRange getForwardNeighborIndices(int row, int column) const
Returns the directed positive half of the neighbourhood.
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.