mmcfilters
Public API documentation
Loading...
Searching...
No Matches
HierarchicalWatershedSaliency.hpp
1#pragma once
2
3#include "HierarchySaliencyMap.hpp"
4#include "../MorphologicalTreeFactory.hpp"
5#include "../TreeAltitudeAlgorithms.hpp"
6#include "../ValuedMorphologicalTreeView.hpp"
7#include "../detail/HierarchyCapabilityValidation.hpp"
8
9#include <algorithm>
10#include <cmath>
11#include <concepts>
12#include <numeric>
13#include <span>
14#include <stdexcept>
15#include <string>
16#include <utility>
17#include <vector>
18
19namespace mmcfilters {
20
50 private:
52 class DisjointSet {
54 std::vector<NodeId> parent_;
56 std::vector<int> size_;
57
58 public:
64 explicit DisjointSet(int count) : parent_(static_cast<std::size_t>(count)), size_(static_cast<std::size_t>(count), 1) {
65 std::iota(parent_.begin(), parent_.end(), NodeId{0});
66 }
67
74 [[nodiscard]] NodeId find(NodeId id) {
75 NodeId root = id;
76 while (parent_[static_cast<std::size_t>(root)] != root) {
77 root = parent_[static_cast<std::size_t>(root)];
78 }
79 while (parent_[static_cast<std::size_t>(id)] != id) {
80 const NodeId next = parent_[static_cast<std::size_t>(id)];
81 parent_[static_cast<std::size_t>(id)] = root;
82 id = next;
83 }
84 return root;
85 }
86
94 [[nodiscard]] NodeId unite(NodeId lhs, NodeId rhs) {
95 lhs = find(lhs);
96 rhs = find(rhs);
97 if (lhs == rhs) {
98 return lhs;
99 }
100 if (size_[static_cast<std::size_t>(lhs)] < size_[static_cast<std::size_t>(rhs)]) {
101 std::swap(lhs, rhs);
102 }
103 parent_[static_cast<std::size_t>(rhs)] = lhs;
104 size_[static_cast<std::size_t>(lhs)] += size_[static_cast<std::size_t>(rhs)];
105 return lhs;
106 }
107 };
108
110 template <AltitudeValue T> struct OrderedGraphEdge {
112 NodeId source = InvalidNode;
114 NodeId target = InvalidNode;
116 NodeId lca = InvalidNode;
118 T altitude{};
120 bool finestRegionEdge = false;
122 std::size_t order = 0;
123 };
124
126 template <AltitudeValue T, std::floating_point Real> struct PersistenceEdge {
128 OrderedGraphEdge<T> graphEdge;
130 Real persistence = Real{0};
131 };
132
145 template <AltitudeValue T> static bool edgePrecedes(const OrderedGraphEdge<T>& lhs, const OrderedGraphEdge<T>& rhs, NodeAltitudeOrder nodeAltitudeOrder) {
146 if (lhs.finestRegionEdge != rhs.finestRegionEdge) {
147 return lhs.finestRegionEdge;
148 }
149 if (lhs.altitude != rhs.altitude) {
150 if (nodeAltitudeOrder == NodeAltitudeOrder::Increasing) {
151 return rhs.altitude < lhs.altitude;
152 }
153 return lhs.altitude < rhs.altitude;
154 }
155 if (lhs.source != rhs.source) {
156 return lhs.source < rhs.source;
157 }
158 return lhs.target < rhs.target;
159 }
160
174 template <std::floating_point Real>
175 static void validateLeafExtinctions(const MorphologicalTree& tree, std::span<const Real> leafExtinction, const char* context) {
176 if (leafExtinction.size() != static_cast<std::size_t>(tree.numInternalNodeSlots())) {
177 throw std::invalid_argument(std::string(context) + " requires one extinction slot per internal NodeId.");
178 }
179 for (NodeId leaf : tree.leaves()) {
180 const Real value = leafExtinction[static_cast<std::size_t>(leaf)];
181 if (!std::isfinite(value) || value < Real{0}) {
182 throw std::invalid_argument(std::string(context) + " requires finite non-negative leaf extinction values.");
183 }
184 }
185 }
186
197 template <AltitudeValue T>
198 static std::vector<OrderedGraphEdge<T>> collectOrderedGraphEdges(const ValuedMorphologicalTreeView<T>& valuedTree, const RegularGridAdjacency2D& adjacency,
199 const char* context) {
200 const MorphologicalTree& tree = valuedTree.topology();
201 std::vector<OrderedGraphEdge<T>> edges;
202 std::size_t order = 0;
203 for (NodeId source = 0; source < tree.numPixels(); ++source) {
204 const NodeId sourceSmallestNode = tree.smallestNode(source);
205 for (int targetValue : adjacency.getForwardNeighborIndices(source)) {
206 const NodeId target = static_cast<NodeId>(targetValue);
207 const NodeId targetSmallestNode = tree.smallestNode(target);
208 const bool finestRegionEdge = sourceSmallestNode == targetSmallestNode;
210 if (lca == InvalidNode || !tree.isAlive(lca)) {
211 throw std::runtime_error(std::string(context) + " could not find a live LCA for an adjacency edge.");
212 }
213 edges.push_back(OrderedGraphEdge<T>{source, target, lca, valuedTree.nodeAltitude(lca), finestRegionEdge, order++});
214 }
215 }
216 const NodeAltitudeOrder nodeAltitudeOrder = tree.nodeAltitudeOrder();
217 std::stable_sort(edges.begin(), edges.end(), [nodeAltitudeOrder](const auto& lhs, const auto& rhs) { return edgePrecedes(lhs, rhs, nodeAltitudeOrder); });
218 return edges;
219 }
220
231 template <AltitudeValue T>
232 static std::vector<OrderedGraphEdge<T>> selectMinimumSpanningTree(std::vector<OrderedGraphEdge<T>> orderedEdges, int numVertices, const char* context) {
233 DisjointSet components(numVertices);
234 std::vector<OrderedGraphEdge<T>> mst;
235 mst.reserve(static_cast<std::size_t>(std::max(0, numVertices - 1)));
236 for (const OrderedGraphEdge<T>& edge : orderedEdges) {
237 if (components.find(edge.source) == components.find(edge.target)) {
238 continue;
239 }
240 static_cast<void>(components.unite(edge.source, edge.target));
241 mst.push_back(edge);
242 if (mst.size() == static_cast<std::size_t>(numVertices - 1)) {
243 break;
244 }
245 }
246 if (numVertices > 0 && mst.size() != static_cast<std::size_t>(numVertices - 1)) {
247 throw std::invalid_argument(std::string(context) + " requires a connected projection graph.");
248 }
249 return mst;
250 }
251
266 template <AltitudeValue T, std::floating_point Real>
267 static std::vector<PersistenceEdge<T, Real>> assignPersistence(const MorphologicalTree& tree, const std::vector<OrderedGraphEdge<T>>& mst,
268 std::span<const Real> leafExtinction) {
269 const int numVertices = tree.numPixels();
270 std::vector<Real> componentExtinction(static_cast<std::size_t>(numVertices), Real{0});
271 for (NodeId leaf : tree.leaves()) {
272 const auto properParts = tree.properPart(leaf);
273 const auto it = properParts.begin();
274 if (it == properParts.end()) {
275 throw std::invalid_argument("HierarchicalWatershedSaliency requires every component-tree leaf to own a proper part.");
276 }
277 componentExtinction[static_cast<std::size_t>(*it)] = leafExtinction[static_cast<std::size_t>(leaf)];
278 }
279
280 DisjointSet components(numVertices);
281 std::vector<PersistenceEdge<T, Real>> persistenceEdges;
282 persistenceEdges.reserve(mst.size());
283 for (const OrderedGraphEdge<T>& edge : mst) {
284 const NodeId lhsRoot = components.find(edge.source);
285 const NodeId rhsRoot = components.find(edge.target);
286 const Real lhsExtinction = componentExtinction[static_cast<std::size_t>(lhsRoot)];
287 const Real rhsExtinction = componentExtinction[static_cast<std::size_t>(rhsRoot)];
290 componentExtinction[static_cast<std::size_t>(mergedRoot)] = std::max(lhsExtinction, rhsExtinction);
291 }
292 return persistenceEdges;
293 }
294
308 template <AltitudeValue T, std::floating_point Real>
309 static ValuedMorphologicalTree<Real> buildPersistenceDendrogram(std::vector<PersistenceEdge<T, Real>> persistenceEdges, int rows, int columns,
310 const RegularGridAdjacency2D& adjacency) {
311 const int numVertices = rows * columns;
312 std::stable_sort(persistenceEdges.begin(), persistenceEdges.end(), [](const auto& lhs, const auto& rhs) {
313 if (lhs.persistence != rhs.persistence) {
314 return lhs.persistence < rhs.persistence;
315 }
316 return lhs.graphEdge.order < rhs.graphEdge.order;
317 });
318
319 const int numNodes = numVertices == 0 ? 0 : 2 * numVertices - 1;
320 std::vector<NodeId> parent(static_cast<std::size_t>(numNodes), InvalidNode);
321 std::vector<NodeId> smallestNodeMap(static_cast<std::size_t>(numVertices), InvalidNode);
322 std::vector<Real> altitude(static_cast<std::size_t>(numNodes), Real{0});
323 for (PixelId pixel = 0; pixel < numVertices; ++pixel) {
324 parent[static_cast<std::size_t>(pixel)] = pixel;
325 smallestNodeMap[static_cast<std::size_t>(pixel)] = pixel;
326 }
327
328 DisjointSet components(numVertices);
329 std::vector<NodeId> componentNode(static_cast<std::size_t>(numVertices));
330 std::iota(componentNode.begin(), componentNode.end(), NodeId{0});
333 const NodeId lhsRoot = components.find(edge.graphEdge.source);
334 const NodeId rhsRoot = components.find(edge.graphEdge.target);
335 const NodeId lhsNode = componentNode[static_cast<std::size_t>(lhsRoot)];
336 const NodeId rhsNode = componentNode[static_cast<std::size_t>(rhsRoot)];
337 parent[static_cast<std::size_t>(lhsNode)] = nextNode;
338 parent[static_cast<std::size_t>(rhsNode)] = nextNode;
339 parent[static_cast<std::size_t>(nextNode)] = nextNode;
340 altitude[static_cast<std::size_t>(nextNode)] = edge.persistence;
342 componentNode[static_cast<std::size_t>(mergedRoot)] = nextNode;
343 ++nextNode;
344 }
345
346 const NodeId root = numVertices == 1 ? NodeId{0} : nextNode - 1;
348 MorphologicalTreeKind::Generic, NodeAltitudeOrder::Unconstrained, SharedAdjacencyContext{adjacency}};
349 return MorphologicalTreeFactory::createFromNativeTopology(std::span<const NodeId>(parent), std::span<const NodeId>(smallestNodeMap),
350 std::span<const Real>(altitude), root, rows, columns, std::move(semantics));
351 }
352
353 public:
382 template <AltitudeValue T, std::floating_point Real>
384 const RegularGridAdjacency2D& adjacency) {
385 constexpr const char* context = "HierarchicalWatershedSaliency::compute";
386 valuedTree.requireTopologyUnchanged(context);
387 const MorphologicalTree& tree = valuedTree.topology();
388 detail::validateGlobalMonotoneAltitudeOrder(tree, context);
391 validateLeafExtinctions(tree, leafExtinction, context);
393
394 const int numVertices = tree.numPixels();
395 auto graphEdges = collectOrderedGraphEdges(valuedTree, adjacency, context);
396 auto mst = selectMinimumSpanningTree(std::move(graphEdges), numVertices, context);
397 auto persistenceEdges = assignPersistence(tree, mst, leafExtinction);
398 auto dendrogram = buildPersistenceDendrogram(std::move(persistenceEdges), tree.numRows(), tree.numColumns(), adjacency);
399 return HierarchySaliencyMap::computeSaliencyEdgeMap(dendrogram.topology(), adjacency, dendrogram.nodeAltitudeSpan(),
400 HierarchyValuationPolicy::AllowLevelCollapse, HierarchyLevelConvention::EdgeSaliencyValue,
401 HierarchyConnectivityPolicy::AssumeConnected);
402 }
403
417 template <AltitudeValue T, std::floating_point Real>
422};
423
424} // namespace mmcfilters
constexpr NodeId InvalidNode
Sentinel value used to denote an invalid node identifier.
Definition Common.hpp:34
NodeAltitudeOrder
Global ordering constraint of node altitudes along parent-child arcs.
Builds Cousty-style hierarchical-watershed saliency from extinctions.
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 validateHierarchyConnectivity(const MorphologicalTree &tree, const RegularGridAdjacency2D &adjacency, const char *context="HierarchySaliencyMapValidation::validateHierarchyConnectivity")
Validates that every hierarchy support is connected in adjacency.
static EdgeSaliencyMap< int > rankEdgeSaliencyMap(const EdgeSaliencyMap< Value > &edgeMap)
Densely ranks the values already present in an edge saliency map.
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 ValuedMorphologicalTree< T > createFromNativeTopology(std::span< const NodeId > parent, std::span< const NodeId > smallestNodeMap, std::span< const T > nodeAltitudes, NodeId root, MorphologicalTreeSemantics semantics)
Imports a valued connected-subset tree from native buffers.
Mutable connected-subset tree on a finite pixel domain.
int numRows() const
Returns the number of rows in the regular 2D pixel domain.
ProperPartRange properPart(NodeId nodeId) const
Returns a fail-fast range over the pixels in the proper part of nodeId.
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.
NodeAltitudeOrder nodeAltitudeOrder() const noexcept
Returns the global parent-to-child altitude ordering constraint.
int numPixels() const
Returns the cardinality of the pixel domain.
std::vector< NodeId > leaves() const
Returns all live leaf nodes in the current hierarchy.
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.
int numColumns() const
Returns the number of columns in the regular 2D pixel domain.
Immutable regular-grid 2D adjacency with allocation-free traversal.
ForwardNeighborIndexRange getForwardNeighborIndices(int row, int column) const
Returns the directed positive half of the neighbourhood.
static void validateMonotoneNodeAltitudes(const MorphologicalTree &tree, std::span< const T > altitude)
Validates the hierarchy's declared global altitude order.
static void validateFiniteAltitudeValues(std::span< const T > altitude, const char *context)
Rejects non-finite floating-point altitudes in a contiguous input range.
Owning result for one computed scalar attribute layout and buffer.
Immutable scientific interpretation attached to one morphological tree.
Records one adjacency shared by both construction polarities.