mmcfilters
Public API documentation
Loading...
Searching...
No Matches
HierarchySaliencyMapValidation.hpp
1#pragma once
2
3#include "../../utils/Altitude.hpp"
4#include "../../utils/Common.hpp"
5#include "../../utils/RegularGridAdjacency2D.hpp"
6#include "../MorphologicalTree.hpp"
7#include "../ValuedMorphologicalTree.hpp"
8
9#include <algorithm>
10#include <cmath>
11#include <cstddef>
12#include <cstdint>
13#include <iterator>
14#include <sstream>
15#include <span>
16#include <stdexcept>
17#include <string>
18#include <type_traits>
19#include <utility>
20#include <vector>
21
22namespace mmcfilters {
23
24namespace detail {
25
37inline void requireCommittedRootedHierarchy(const MorphologicalTree& tree, const char* context) {
38 if (tree.isEditing()) {
39 throw std::invalid_argument(std::string(context) + " requires a committed tree; an edit session is still open.");
40 }
41 if (tree.root() == InvalidNode || !tree.isAlive(tree.root())) {
42 throw std::invalid_argument(std::string(context) + " requires a non-empty connected rooted tree.");
43 }
44 if (tree.parent(tree.root()) != tree.root()) {
45 throw std::invalid_argument(std::string(context) + " requires the root to point to itself.");
46 }
47
48 const std::size_t slotCount = static_cast<std::size_t>(tree.numInternalNodeSlots());
49 std::vector<std::uint8_t> visited(slotCount, 0);
50 std::vector<NodeId> stack{tree.root()};
51 visited[static_cast<std::size_t>(tree.root())] = 1;
52 std::size_t visitedCount = 0;
53 std::size_t traversedEdges = 0;
54
55 while (!stack.empty()) {
56 const NodeId nodeId = stack.back();
57 stack.pop_back();
58 ++visitedCount;
59
60 for (NodeId childId : tree.children(nodeId)) {
61 ++traversedEdges;
62 if (traversedEdges >= slotCount || !tree.isAlive(childId) || tree.parent(childId) != nodeId) {
63 throw std::invalid_argument(std::string(context) + " requires consistent parent-child relations.");
64 }
65 const std::size_t childIndex = static_cast<std::size_t>(childId);
66 if (visited[childIndex] != 0) {
67 throw std::invalid_argument(std::string(context) + " requires an acyclic rooted tree.");
68 }
69 visited[childIndex] = 1;
70 stack.push_back(childId);
71 }
72 }
73
74 std::size_t aliveCount = 0;
75 for (NodeId nodeId : tree.aliveNodeIds()) {
76 ++aliveCount;
77 if (visited[static_cast<std::size_t>(nodeId)] == 0) {
78 throw std::invalid_argument(std::string(context) + " requires every live node to be connected to the root.");
79 }
80 }
81 if (visitedCount != aliveCount) {
82 throw std::invalid_argument(std::string(context) + " requires one connected rooted tree.");
83 }
84}
85
86} // namespace detail
87
104enum class HierarchyValuationPolicy {
105 AllowLevelCollapse,
106 RequireStrictHierarchy,
107};
108
118enum class HierarchyValuationRangePolicy {
119 AllowAnyFinite,
120 RequireNonNegative,
121};
122
136enum class HierarchyConnectivityPolicy {
137 AssumeConnected,
138 ValidateConnected,
139};
140
155 private:
164 template <class Value>
165 static void validateValuationValue(const Value& value, NodeId nodeId, const char* context, HierarchyValuationRangePolicy rangePolicy) {
166 if constexpr (std::is_floating_point_v<Value>) {
167 if (!std::isfinite(value)) {
168 std::ostringstream oss;
169 oss << context << " requires finite valuation values; node " << nodeId << " has value " << value << ".";
170 throw std::invalid_argument(oss.str());
171 }
172 }
173 if (rangePolicy == HierarchyValuationRangePolicy::RequireNonNegative && value < Value{}) {
174 std::ostringstream oss;
175 oss << context << " requires non-negative valuation values; node " << nodeId << " has value " << value << ".";
176 throw std::invalid_argument(oss.str());
177 }
178 }
179
180 public:
198 const char* context = "HierarchySaliencyMapValidation::validateHierarchyConnectivity") {
199 detail::requireCommittedRootedHierarchy(tree, context);
200 const int rows = tree.numRows();
201 const int columns = tree.numColumns();
202 const int numPixels = tree.numPixels();
203 if (rows <= 0 || columns <= 0 || numPixels <= 0 || adjacency.getNumRows() != rows || adjacency.getNumColumns() != columns ||
204 static_cast<std::size_t>(numPixels) != static_cast<std::size_t>(rows) * static_cast<std::size_t>(columns)) {
205 throw std::invalid_argument(std::string(context) + " requires one graph vertex per 2D proper part and matching adjacency dimensions.");
206 }
207
208 struct DomainDisjointSet {
209 std::vector<NodeId> parent;
210 std::vector<int> size;
211
212 explicit DomainDisjointSet(int count) : parent(static_cast<std::size_t>(count)), size(static_cast<std::size_t>(count), 1) {
213 for (NodeId id = 0; id < count; ++id) {
214 parent[static_cast<std::size_t>(id)] = id;
215 }
216 }
217
218 NodeId find(NodeId id) {
219 NodeId root = id;
220 while (parent[static_cast<std::size_t>(root)] != root) {
221 root = parent[static_cast<std::size_t>(root)];
222 }
223 while (parent[static_cast<std::size_t>(id)] != id) {
224 const NodeId next = parent[static_cast<std::size_t>(id)];
225 parent[static_cast<std::size_t>(id)] = root;
226 id = next;
227 }
228 return root;
229 }
230
231 void unite(NodeId lhs, NodeId rhs) {
232 lhs = find(lhs);
233 rhs = find(rhs);
234 if (lhs == rhs) {
235 return;
236 }
237 if (size[static_cast<std::size_t>(lhs)] < size[static_cast<std::size_t>(rhs)]) {
238 std::swap(lhs, rhs);
239 }
240 parent[static_cast<std::size_t>(rhs)] = lhs;
241 size[static_cast<std::size_t>(lhs)] += size[static_cast<std::size_t>(rhs)];
242 }
243 };
244
245 using DomainEdge = std::pair<NodeId, NodeId>;
246 std::vector<std::vector<DomainEdge>> edgesByLca(static_cast<std::size_t>(tree.numInternalNodeSlots()));
247 for (NodeId source = 0; source < numPixels; ++source) {
248 const NodeId sourceSmallestNode = tree.smallestNode(source);
249 if (!tree.isAlive(sourceSmallestNode)) {
250 throw std::invalid_argument(std::string(context) + " found a pixel without a live smallest node.");
251 }
252 for (int targetValue : adjacency.getForwardNeighborIndices(source)) {
253 const NodeId target = static_cast<NodeId>(targetValue);
254 const NodeId targetSmallestNode = tree.smallestNode(target);
255 if (!tree.isAlive(targetSmallestNode)) {
256 throw std::invalid_argument(std::string(context) + " found a neighbour pixel without a live smallest node.");
257 }
259 if (lca == InvalidNode || !tree.isAlive(lca)) {
260 throw std::invalid_argument(std::string(context) + " could not assign an adjacency edge to a live hierarchy node.");
261 }
262 edgesByLca[static_cast<std::size_t>(lca)].emplace_back(source, target);
263 }
264 }
265
266 DomainDisjointSet components(numPixels);
267 std::vector<NodeId> supportRepresentative(static_cast<std::size_t>(tree.numInternalNodeSlots()), InvalidNode);
268 for (NodeId nodeId : tree.postOrder()) {
269 for (const DomainEdge& edge : edgesByLca[static_cast<std::size_t>(nodeId)]) {
271 }
272
273 NodeId representative = InvalidNode;
275 if (representative == InvalidNode) {
276 representative = candidate;
277 return;
278 }
279 if (components.find(representative) != components.find(candidate)) {
280 std::ostringstream oss;
281 oss << context << " requires every hierarchy region to be connected in the projection graph; node " << nodeId
282 << " has a disconnected support.";
283 throw std::invalid_argument(oss.str());
284 }
285 };
286
287 for (PixelId pixel : tree.properPart(nodeId)) {
289 }
290 for (NodeId childId : tree.children(nodeId)) {
291 const NodeId childRepresentative = supportRepresentative[static_cast<std::size_t>(childId)];
293 throw std::invalid_argument(std::string(context) + " found a live child without proper-part support.");
294 }
296 }
297 if (representative == InvalidNode) {
298 throw std::invalid_argument(std::string(context) + " found a live node without proper-part support.");
299 }
300 supportRepresentative[static_cast<std::size_t>(nodeId)] = components.find(representative);
301 }
302 }
303
325 template <class Value>
326 static void validateHierarchyValuation(const MorphologicalTree& tree, std::span<const Value> valuation,
327 HierarchyValuationPolicy policy = HierarchyValuationPolicy::AllowLevelCollapse,
328 HierarchyValuationRangePolicy rangePolicy = HierarchyValuationRangePolicy::AllowAnyFinite,
329 const char* context = "HierarchySaliencyMapValidation::validateHierarchyValuation") {
330 detail::requireCommittedRootedHierarchy(tree, context);
331 if (valuation.size() != static_cast<std::size_t>(tree.numInternalNodeSlots())) {
332 std::ostringstream oss;
333 oss << context << " requires one valuation value per dense internal node slot; expected " << tree.numInternalNodeSlots() << " values but got "
334 << valuation.size() << ".";
335 throw std::invalid_argument(oss.str());
336 }
337
338 for (NodeId nodeId : tree.aliveNodeIds()) {
339 validateValuationValue(valuation[static_cast<std::size_t>(nodeId)], nodeId, context, rangePolicy);
340 }
341
342 for (NodeId parentId : tree.aliveNodeIds()) {
343 const Value& parentValue = valuation[static_cast<std::size_t>(parentId)];
344 for (NodeId childId : tree.children(parentId)) {
345 const Value& childValue = valuation[static_cast<std::size_t>(childId)];
346 const bool validOrder = policy == HierarchyValuationPolicy::RequireStrictHierarchy ? childValue < parentValue : !(parentValue < childValue);
347 if (!validOrder) {
348 std::ostringstream oss;
349 oss << context << " requires "
350 << (policy == HierarchyValuationPolicy::RequireStrictHierarchy ? "valuation(parent) > valuation(child)"
351 : "valuation(parent) >= valuation(child)")
352 << "; parent node " << parentId << " has value " << parentValue << " and child node " << childId << " has value " << childValue << ".";
353 throw std::invalid_argument(oss.str());
354 }
355 }
356 }
357 }
358
374 template <class Value>
375 [[nodiscard]] static std::vector<int> rankHierarchyValuation(const MorphologicalTree& tree, std::span<const Value> valuation,
376 HierarchyValuationPolicy policy = HierarchyValuationPolicy::AllowLevelCollapse) {
377 validateHierarchyValuation(tree, valuation, policy, HierarchyValuationRangePolicy::AllowAnyFinite,
378 "HierarchySaliencyMapValidation::rankHierarchyValuation");
379
380 std::vector<Value> uniqueValues;
381 uniqueValues.reserve(static_cast<std::size_t>(tree.numInternalNodeSlots()));
382 for (NodeId nodeId : tree.aliveNodeIds()) {
383 uniqueValues.push_back(valuation[static_cast<std::size_t>(nodeId)]);
384 }
385
386 std::sort(uniqueValues.begin(), uniqueValues.end());
387 uniqueValues.erase(std::unique(uniqueValues.begin(), uniqueValues.end()), uniqueValues.end());
388
389 std::vector<int> ranks(static_cast<std::size_t>(tree.numInternalNodeSlots()), 0);
390 for (NodeId nodeId : tree.aliveNodeIds()) {
391 const Value& value = valuation[static_cast<std::size_t>(nodeId)];
392 const auto it = std::lower_bound(uniqueValues.begin(), uniqueValues.end(), value);
393 ranks[static_cast<std::size_t>(nodeId)] = static_cast<int>(std::distance(uniqueValues.begin(), it));
394 }
395 return ranks;
396 }
397
420 template <class Value>
421 [[nodiscard]] static std::vector<double>
422 computeNormalizedScores(const MorphologicalTree& tree, std::span<const Value> valuation,
423 HierarchyValuationPolicy policy = HierarchyValuationPolicy::AllowLevelCollapse,
424 HierarchyValuationRangePolicy rangePolicy = HierarchyValuationRangePolicy::AllowAnyFinite) {
425 using BareValue = std::remove_cv_t<Value>;
426 static_assert(std::is_arithmetic_v<BareValue> && !std::is_same_v<BareValue, bool>,
427 "HierarchySaliencyMapValidation::computeNormalizedScores requires a numeric non-bool valuation type.");
428 validateHierarchyValuation(tree, valuation, policy, rangePolicy, "HierarchySaliencyMapValidation::computeNormalizedScores");
429
430 bool initialized = false;
433 for (NodeId nodeId : tree.aliveNodeIds()) {
434 const BareValue value = valuation[static_cast<std::size_t>(nodeId)];
435 if (!initialized) {
436 minValue = value;
437 maxValue = value;
438 initialized = true;
439 } else {
440 minValue = std::min(minValue, value);
441 maxValue = std::max(maxValue, value);
442 }
443 }
444
445 std::vector<double> scores(static_cast<std::size_t>(tree.numInternalNodeSlots()), 0.0);
446 if (maxValue == minValue) {
447 return scores;
448 }
449
450 for (NodeId nodeId : tree.aliveNodeIds()) {
451 const BareValue value = valuation[static_cast<std::size_t>(nodeId)];
452 long double normalized = 0.0L;
453
454 if constexpr (std::is_integral_v<BareValue>) {
455 using UnsignedValue = std::make_unsigned_t<BareValue>;
456 const UnsignedValue offset = static_cast<UnsignedValue>(value) - static_cast<UnsignedValue>(minValue);
457 const UnsignedValue range = static_cast<UnsignedValue>(maxValue) - static_cast<UnsignedValue>(minValue);
458 normalized = static_cast<long double>(offset) / static_cast<long double>(range);
459 } else {
460 const long double low = static_cast<long double>(minValue);
461 const long double high = static_cast<long double>(maxValue);
462 const long double current = static_cast<long double>(value);
463
464 if (low < 0.0L && high > 0.0L) {
465 const long double scale = std::max(-low, high);
466 const long double scaledLow = low / scale;
467 const long double scaledHigh = high / scale;
469 } else {
470 normalized = (current - low) / (high - low);
471 }
472 }
473
474 scores[static_cast<std::size_t>(nodeId)] = static_cast<double>(std::clamp(normalized, 0.0L, 1.0L));
475 }
476 return scores;
477 }
478
491 template <AltitudeValue T> [[nodiscard]] static std::vector<double> computeNormalizedScores(const ValuedMorphologicalTree<T>& tree) {
492 const MorphologicalTree& topology = tree.topology();
493 detail::requireCommittedRootedHierarchy(topology, "HierarchySaliencyMapValidation::computeNormalizedScores");
494 const NodeAltitudeOrder nodeAltitudeOrder = topology.nodeAltitudeOrder();
495 if (nodeAltitudeOrder == NodeAltitudeOrder::Unconstrained) {
496 throw std::invalid_argument("HierarchySaliencyMapValidation::computeNormalizedScores requires a globally monotone altitude order.");
497 }
498 // Use at least double precision while retaining long double when it is
499 // the input type. An unconditional cast to double can collapse distinct
500 // long-double hierarchy levels and change the induced hierarchy.
501 using OrientedAltitude = std::common_type_t<T, double>;
502 std::vector<OrientedAltitude> orientedAltitude(static_cast<std::size_t>(topology.numInternalNodeSlots()), OrientedAltitude{});
503 for (NodeId nodeId : topology.aliveNodeIds()) {
504 const OrientedAltitude altitude = static_cast<OrientedAltitude>(tree.nodeAltitude(nodeId));
505 orientedAltitude[static_cast<std::size_t>(nodeId)] = nodeAltitudeOrder == NodeAltitudeOrder::Increasing ? -altitude : altitude;
506 }
507 return computeNormalizedScores(topology, std::span<const OrientedAltitude>(orientedAltitude), HierarchyValuationPolicy::AllowLevelCollapse);
508 }
509};
510
511} // 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
NodeAltitudeOrder
Global ordering constraint of node altitudes along parent-child arcs.
Validates and transforms hierarchy valuations used by saliency maps.
static std::vector< double > computeNormalizedScores(const MorphologicalTree &tree, std::span< const Value > valuation, HierarchyValuationPolicy policy=HierarchyValuationPolicy::AllowLevelCollapse, HierarchyValuationRangePolicy rangePolicy=HierarchyValuationRangePolicy::AllowAnyFinite)
Normalizes a compatible hierarchy valuation to [0, 1].
static void validateHierarchyConnectivity(const MorphologicalTree &tree, const RegularGridAdjacency2D &adjacency, const char *context="HierarchySaliencyMapValidation::validateHierarchyConnectivity")
Validates that every hierarchy support is connected in adjacency.
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 std::vector< double > computeNormalizedScores(const ValuedMorphologicalTree< T > &tree)
Computes a dense normalized altitude score buffer in [0, 1].
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.
PostOrderNodeRange postOrder() const
Returns a post-order traversal range rooted at the connected root.
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.
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.
Immutable regular-grid 2D adjacency with allocation-free traversal.
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.
Owning result for one computed scalar attribute layout and buffer.
std::vector< Real > second
Flat per-node attribute buffer indexed through first.
AttributeNames first
Layout used to interpret second; kept public for tuple-like access.