MorphologicalAttributeFilters
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
132enum class HierarchyConnectivityPolicy {
133 AssumeConnected,
134 ValidateConnected,
135};
136
151 private:
160 template <class Value>
161 static void validateValuationValue(const Value& value, NodeId nodeId, const char* context, HierarchyValuationRangePolicy rangePolicy) {
162 if constexpr (std::is_floating_point_v<Value>) {
163 if (!std::isfinite(value)) {
164 std::ostringstream oss;
165 oss << context << " requires finite valuation values; node " << nodeId << " has value " << value << ".";
166 throw std::invalid_argument(oss.str());
167 }
168 }
169 if (rangePolicy == HierarchyValuationRangePolicy::RequireNonNegative && value < Value{}) {
170 std::ostringstream oss;
171 oss << context << " requires non-negative valuation values; node " << nodeId << " has value " << value << ".";
172 throw std::invalid_argument(oss.str());
173 }
174 }
175
176 public:
194 const char* context = "HierarchySaliencyMapValidation::validateHierarchyConnectivity") {
195 detail::requireCommittedRootedHierarchy(tree, context);
196 const int rows = tree.numRows();
197 const int columns = tree.numColumns();
198 const int numPixels = tree.numPixels();
199 if (rows <= 0 || columns <= 0 || numPixels <= 0 || adjacency.getNumRows() != rows || adjacency.getNumColumns() != columns ||
200 static_cast<std::size_t>(numPixels) != static_cast<std::size_t>(rows) * static_cast<std::size_t>(columns)) {
201 throw std::invalid_argument(std::string(context) + " requires one graph vertex per 2D proper part and matching adjacency dimensions.");
202 }
203
204 struct DomainDisjointSet {
205 std::vector<NodeId> parent;
206 std::vector<int> size;
207
208 explicit DomainDisjointSet(int count) : parent(static_cast<std::size_t>(count)), size(static_cast<std::size_t>(count), 1) {
209 for (NodeId id = 0; id < count; ++id) {
210 parent[static_cast<std::size_t>(id)] = id;
211 }
212 }
213
214 NodeId find(NodeId id) {
215 NodeId root = id;
216 while (parent[static_cast<std::size_t>(root)] != root) {
217 root = parent[static_cast<std::size_t>(root)];
218 }
219 while (parent[static_cast<std::size_t>(id)] != id) {
220 const NodeId next = parent[static_cast<std::size_t>(id)];
221 parent[static_cast<std::size_t>(id)] = root;
222 id = next;
223 }
224 return root;
225 }
226
227 void unite(NodeId lhs, NodeId rhs) {
228 lhs = find(lhs);
229 rhs = find(rhs);
230 if (lhs == rhs) {
231 return;
232 }
233 if (size[static_cast<std::size_t>(lhs)] < size[static_cast<std::size_t>(rhs)]) {
234 std::swap(lhs, rhs);
235 }
236 parent[static_cast<std::size_t>(rhs)] = lhs;
237 size[static_cast<std::size_t>(lhs)] += size[static_cast<std::size_t>(rhs)];
238 }
239 };
240
241 using DomainEdge = std::pair<NodeId, NodeId>;
242 std::vector<std::vector<DomainEdge>> edgesByLca(static_cast<std::size_t>(tree.numInternalNodeSlots()));
243 for (NodeId source = 0; source < numPixels; ++source) {
244 const NodeId sourceSmallestNode = tree.smallestNode(source);
245 if (!tree.isAlive(sourceSmallestNode)) {
246 throw std::invalid_argument(std::string(context) + " found a pixel without a live smallest node.");
247 }
248 for (int targetValue : adjacency.getForwardNeighborIndices(source)) {
249 const NodeId target = static_cast<NodeId>(targetValue);
250 const NodeId targetSmallestNode = tree.smallestNode(target);
251 if (!tree.isAlive(targetSmallestNode)) {
252 throw std::invalid_argument(std::string(context) + " found a neighbour pixel without a live smallest node.");
253 }
255 if (lca == InvalidNode || !tree.isAlive(lca)) {
256 throw std::invalid_argument(std::string(context) + " could not assign an adjacency edge to a live hierarchy node.");
257 }
258 edgesByLca[static_cast<std::size_t>(lca)].emplace_back(source, target);
259 }
260 }
261
262 DomainDisjointSet components(numPixels);
263 std::vector<NodeId> supportRepresentative(static_cast<std::size_t>(tree.numInternalNodeSlots()), InvalidNode);
264 for (NodeId nodeId : tree.postOrder()) {
265 for (const DomainEdge& edge : edgesByLca[static_cast<std::size_t>(nodeId)]) {
267 }
268
269 NodeId representative = InvalidNode;
271 if (representative == InvalidNode) {
272 representative = candidate;
273 return;
274 }
275 if (components.find(representative) != components.find(candidate)) {
276 std::ostringstream oss;
277 oss << context << " requires every hierarchy region to be connected in the projection graph; node " << nodeId
278 << " has a disconnected support.";
279 throw std::invalid_argument(oss.str());
280 }
281 };
282
283 for (PixelId pixel : tree.properPart(nodeId)) {
285 }
286 for (NodeId childId : tree.children(nodeId)) {
287 const NodeId childRepresentative = supportRepresentative[static_cast<std::size_t>(childId)];
289 throw std::invalid_argument(std::string(context) + " found a live child without proper-part support.");
290 }
292 }
293 if (representative == InvalidNode) {
294 throw std::invalid_argument(std::string(context) + " found a live node without proper-part support.");
295 }
296 supportRepresentative[static_cast<std::size_t>(nodeId)] = components.find(representative);
297 }
298 }
299
321 template <class Value>
322 static void validateHierarchyValuation(const MorphologicalTree& tree, std::span<const Value> valuation,
323 HierarchyValuationPolicy policy = HierarchyValuationPolicy::AllowLevelCollapse,
324 HierarchyValuationRangePolicy rangePolicy = HierarchyValuationRangePolicy::AllowAnyFinite,
325 const char* context = "HierarchySaliencyMapValidation::validateHierarchyValuation") {
326 detail::requireCommittedRootedHierarchy(tree, context);
327 if (valuation.size() != static_cast<std::size_t>(tree.numInternalNodeSlots())) {
328 std::ostringstream oss;
329 oss << context << " requires one valuation value per dense internal node slot; expected " << tree.numInternalNodeSlots() << " values but got "
330 << valuation.size() << ".";
331 throw std::invalid_argument(oss.str());
332 }
333
334 for (NodeId nodeId : tree.aliveNodeIds()) {
335 validateValuationValue(valuation[static_cast<std::size_t>(nodeId)], nodeId, context, rangePolicy);
336 }
337
338 for (NodeId parentId : tree.aliveNodeIds()) {
339 const Value& parentValue = valuation[static_cast<std::size_t>(parentId)];
340 for (NodeId childId : tree.children(parentId)) {
341 const Value& childValue = valuation[static_cast<std::size_t>(childId)];
342 const bool validOrder = policy == HierarchyValuationPolicy::RequireStrictHierarchy ? childValue < parentValue : !(parentValue < childValue);
343 if (!validOrder) {
344 std::ostringstream oss;
345 oss << context << " requires "
346 << (policy == HierarchyValuationPolicy::RequireStrictHierarchy ? "valuation(parent) > valuation(child)"
347 : "valuation(parent) >= valuation(child)")
348 << "; parent node " << parentId << " has value " << parentValue << " and child node " << childId << " has value " << childValue << ".";
349 throw std::invalid_argument(oss.str());
350 }
351 }
352 }
353 }
354
370 template <class Value>
371 [[nodiscard]] static std::vector<int> rankHierarchyValuation(const MorphologicalTree& tree, std::span<const Value> valuation,
372 HierarchyValuationPolicy policy = HierarchyValuationPolicy::AllowLevelCollapse) {
373 validateHierarchyValuation(tree, valuation, policy, HierarchyValuationRangePolicy::AllowAnyFinite,
374 "HierarchySaliencyMapValidation::rankHierarchyValuation");
375
376 std::vector<Value> uniqueValues;
377 uniqueValues.reserve(static_cast<std::size_t>(tree.numInternalNodeSlots()));
378 for (NodeId nodeId : tree.aliveNodeIds()) {
379 uniqueValues.push_back(valuation[static_cast<std::size_t>(nodeId)]);
380 }
381
382 std::sort(uniqueValues.begin(), uniqueValues.end());
383 uniqueValues.erase(std::unique(uniqueValues.begin(), uniqueValues.end()), uniqueValues.end());
384
385 std::vector<int> ranks(static_cast<std::size_t>(tree.numInternalNodeSlots()), 0);
386 for (NodeId nodeId : tree.aliveNodeIds()) {
387 const Value& value = valuation[static_cast<std::size_t>(nodeId)];
388 const auto it = std::lower_bound(uniqueValues.begin(), uniqueValues.end(), value);
389 ranks[static_cast<std::size_t>(nodeId)] = static_cast<int>(std::distance(uniqueValues.begin(), it));
390 }
391 return ranks;
392 }
393
416 template <class Value>
417 [[nodiscard]] static std::vector<double>
418 computeNormalizedScores(const MorphologicalTree& tree, std::span<const Value> valuation,
419 HierarchyValuationPolicy policy = HierarchyValuationPolicy::AllowLevelCollapse,
420 HierarchyValuationRangePolicy rangePolicy = HierarchyValuationRangePolicy::AllowAnyFinite) {
421 using BareValue = std::remove_cv_t<Value>;
422 static_assert(std::is_arithmetic_v<BareValue> && !std::is_same_v<BareValue, bool>,
423 "HierarchySaliencyMapValidation::computeNormalizedScores requires a numeric non-bool valuation type.");
424 validateHierarchyValuation(tree, valuation, policy, rangePolicy, "HierarchySaliencyMapValidation::computeNormalizedScores");
425
426 bool initialized = false;
429 for (NodeId nodeId : tree.aliveNodeIds()) {
430 const BareValue value = valuation[static_cast<std::size_t>(nodeId)];
431 if (!initialized) {
432 minValue = value;
433 maxValue = value;
434 initialized = true;
435 } else {
436 minValue = std::min(minValue, value);
437 maxValue = std::max(maxValue, value);
438 }
439 }
440
441 std::vector<double> scores(static_cast<std::size_t>(tree.numInternalNodeSlots()), 0.0);
442 if (maxValue == minValue) {
443 return scores;
444 }
445
446 for (NodeId nodeId : tree.aliveNodeIds()) {
447 const BareValue value = valuation[static_cast<std::size_t>(nodeId)];
448 long double normalized = 0.0L;
449
450 if constexpr (std::is_integral_v<BareValue>) {
451 using UnsignedValue = std::make_unsigned_t<BareValue>;
452 const UnsignedValue offset = static_cast<UnsignedValue>(value) - static_cast<UnsignedValue>(minValue);
453 const UnsignedValue range = static_cast<UnsignedValue>(maxValue) - static_cast<UnsignedValue>(minValue);
454 normalized = static_cast<long double>(offset) / static_cast<long double>(range);
455 } else {
456 const long double low = static_cast<long double>(minValue);
457 const long double high = static_cast<long double>(maxValue);
458 const long double current = static_cast<long double>(value);
459
460 if (low < 0.0L && high > 0.0L) {
461 const long double scale = std::max(-low, high);
462 const long double scaledLow = low / scale;
463 const long double scaledHigh = high / scale;
465 } else {
466 normalized = (current - low) / (high - low);
467 }
468 }
469
470 scores[static_cast<std::size_t>(nodeId)] = static_cast<double>(std::clamp(normalized, 0.0L, 1.0L));
471 }
472 return scores;
473 }
474
487 template <AltitudeValue T> [[nodiscard]] static std::vector<double> computeNormalizedScores(const ValuedMorphologicalTree<T>& tree) {
488 const MorphologicalTree& topology = tree.topology();
489 detail::requireCommittedRootedHierarchy(topology, "HierarchySaliencyMapValidation::computeNormalizedScores");
490 const NodeAltitudeOrder nodeAltitudeOrder = topology.nodeAltitudeOrder();
491 if (nodeAltitudeOrder == NodeAltitudeOrder::Unconstrained) {
492 throw std::invalid_argument("HierarchySaliencyMapValidation::computeNormalizedScores requires a globally monotone altitude order.");
493 }
494 // Use at least double precision while retaining long double when it is
495 // the input type. An unconditional cast to double can collapse distinct
496 // long-double hierarchy levels and change the induced hierarchy.
497 using OrientedAltitude = std::common_type_t<T, double>;
498 std::vector<OrientedAltitude> orientedAltitude(static_cast<std::size_t>(topology.numInternalNodeSlots()), OrientedAltitude{});
499 for (NodeId nodeId : topology.aliveNodeIds()) {
500 const OrientedAltitude altitude = static_cast<OrientedAltitude>(tree.nodeAltitude(nodeId));
501 orientedAltitude[static_cast<std::size_t>(nodeId)] = nodeAltitudeOrder == NodeAltitudeOrder::Increasing ? -altitude : altitude;
502 }
503 return computeNormalizedScores(topology, std::span<const OrientedAltitude>(orientedAltitude), HierarchyValuationPolicy::AllowLevelCollapse);
504 }
505};
506
507} // 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 inclusion-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.