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 "../detail/TreeKernelAccess.hpp"
8#include "../../utils/CommittedGridAccess.hpp"
9#include "../ValuedMorphologicalTree.hpp"
10
11#include <algorithm>
12#include <cmath>
13#include <cstddef>
14#include <cstdint>
15#include <iterator>
16#include <sstream>
17#include <span>
18#include <stdexcept>
19#include <string>
20#include <type_traits>
21#include <utility>
22#include <vector>
23
24namespace mmcfilters {
25
26namespace detail {
27
38inline void requireCommittedRootedHierarchy(const MorphologicalTree& tree, const char* context) {
39 if (tree.isEditing()) {
40 throw std::invalid_argument(std::string(context) + " requires a committed tree; an edit session is still open.");
41 }
42 if (tree.root() == InvalidNode || !tree.isAlive(tree.root())) {
43 throw std::invalid_argument(std::string(context) + " requires a non-empty connected rooted tree.");
44 }
45}
46
47} // namespace detail
48
65enum class HierarchyValuationPolicy {
66 AllowLevelCollapse,
67 RequireStrictHierarchy,
68};
69
79enum class HierarchyValuationRangePolicy {
80 AllowAnyFinite,
81 RequireNonNegative,
82};
83
97enum class HierarchyConnectivityPolicy {
98 AssumeConnected,
99 ValidateConnected,
100};
101
116 friend class HierarchySaliencyMap;
117
118 private:
127 template <class Value>
128 static void validateValuationValue(const Value& value, NodeId nodeId, const char* context, HierarchyValuationRangePolicy rangePolicy) {
129 if constexpr (std::is_floating_point_v<Value>) {
130 if (!std::isfinite(value)) {
131 std::ostringstream oss;
132 oss << context << " requires finite valuation values; node " << nodeId << " has value " << value << ".";
133 throw std::invalid_argument(oss.str());
134 }
135 }
136 if (rangePolicy == HierarchyValuationRangePolicy::RequireNonNegative && value < Value{}) {
137 std::ostringstream oss;
138 oss << context << " requires non-negative valuation values; node " << nodeId << " has value " << value << ".";
139 throw std::invalid_argument(oss.str());
140 }
141 }
142
143 public:
161 const char* context = "HierarchySaliencyMapValidation::validateHierarchyConnectivity") {
162 detail::requireCommittedRootedHierarchy(tree, context);
163 const int rows = tree.numRows();
164 const int columns = tree.numColumns();
165 if (!tree.hasGridDomain2D() || adjacency.getNumRows() != rows || adjacency.getNumColumns() != columns) {
166 throw std::invalid_argument(std::string(context) + " requires one graph vertex per 2D proper part and matching adjacency dimensions.");
167 }
168
169 validateHierarchyConnectivityEstablished(tree, adjacency, context);
170 }
171
193 template <class Value>
194 static void validateHierarchyValuation(const MorphologicalTree& tree, std::span<const Value> valuation,
195 HierarchyValuationPolicy policy = HierarchyValuationPolicy::AllowLevelCollapse,
196 HierarchyValuationRangePolicy rangePolicy = HierarchyValuationRangePolicy::AllowAnyFinite,
197 const char* context = "HierarchySaliencyMapValidation::validateHierarchyValuation") {
198 detail::requireCommittedRootedHierarchy(tree, context);
199 validateHierarchyValuationEstablished(tree, valuation, policy, rangePolicy, context);
200 }
201
217 template <class Value>
218 [[nodiscard]] static std::vector<int> rankHierarchyValuation(const MorphologicalTree& tree, std::span<const Value> valuation,
219 HierarchyValuationPolicy policy = HierarchyValuationPolicy::AllowLevelCollapse) {
220 validateHierarchyValuation(tree, valuation, policy, HierarchyValuationRangePolicy::AllowAnyFinite,
221 "HierarchySaliencyMapValidation::rankHierarchyValuation");
222
223 std::vector<Value> uniqueValues;
224 uniqueValues.reserve(static_cast<std::size_t>(tree.numInternalNodeSlots()));
225 for (NodeId nodeId : tree.aliveNodeIds()) {
226 uniqueValues.push_back(valuation[static_cast<std::size_t>(nodeId)]);
227 }
228
229 std::sort(uniqueValues.begin(), uniqueValues.end());
230 uniqueValues.erase(std::unique(uniqueValues.begin(), uniqueValues.end()), uniqueValues.end());
231
232 std::vector<int> ranks(static_cast<std::size_t>(tree.numInternalNodeSlots()), 0);
233 for (NodeId nodeId : tree.aliveNodeIds()) {
234 const Value& value = valuation[static_cast<std::size_t>(nodeId)];
235 const auto it = std::lower_bound(uniqueValues.begin(), uniqueValues.end(), value);
236 ranks[static_cast<std::size_t>(nodeId)] = static_cast<int>(std::distance(uniqueValues.begin(), it));
237 }
238 return ranks;
239 }
240
263 template <class Value>
264 [[nodiscard]] static std::vector<double>
265 computeNormalizedScores(const MorphologicalTree& tree, std::span<const Value> valuation,
266 HierarchyValuationPolicy policy = HierarchyValuationPolicy::AllowLevelCollapse,
267 HierarchyValuationRangePolicy rangePolicy = HierarchyValuationRangePolicy::AllowAnyFinite) {
268 using BareValue = std::remove_cv_t<Value>;
269 static_assert(std::is_arithmetic_v<BareValue> && !std::is_same_v<BareValue, bool>,
270 "HierarchySaliencyMapValidation::computeNormalizedScores requires a numeric non-bool valuation type.");
271 validateHierarchyValuation(tree, valuation, policy, rangePolicy, "HierarchySaliencyMapValidation::computeNormalizedScores");
272
273 return normalizeHierarchyValuation(tree, valuation);
274 }
275
288 template <AltitudeValue T> [[nodiscard]] static std::vector<double> computeNormalizedScores(const ValuedMorphologicalTree<T>& tree) {
289 const MorphologicalTree& topology = tree.topology();
290 detail::requireCommittedRootedHierarchy(topology, "HierarchySaliencyMapValidation::computeNormalizedScores");
291 const NodeAltitudeOrder nodeAltitudeOrder = topology.nodeAltitudeOrder();
292 if (nodeAltitudeOrder == NodeAltitudeOrder::Unconstrained) {
293 throw std::invalid_argument("HierarchySaliencyMapValidation::computeNormalizedScores requires a globally monotone altitude order.");
294 }
295 // Use at least double precision while retaining long double when it is
296 // the input type. An unconditional cast to double can collapse distinct
297 // long-double hierarchy levels and change the induced hierarchy.
298 using OrientedAltitude = std::common_type_t<T, double>;
299 std::vector<OrientedAltitude> orientedAltitude(static_cast<std::size_t>(topology.numInternalNodeSlots()), OrientedAltitude{});
300 for (NodeId nodeId : topology.aliveNodeIds()) {
301 const OrientedAltitude altitude = static_cast<OrientedAltitude>(tree.nodeAltitudeSpan()[static_cast<std::size_t>(nodeId)]);
302 orientedAltitude[static_cast<std::size_t>(nodeId)] = nodeAltitudeOrder == NodeAltitudeOrder::Increasing ? -altitude : altitude;
303 }
304 return normalizeHierarchyValuation(topology, std::span<const OrientedAltitude>(orientedAltitude));
305 }
306
307 private:
314 static void validateHierarchyConnectivityEstablished(const MorphologicalTree& tree, const RegularGridAdjacency2D& adjacency, const char* context) {
315 const int numPixels = tree.numPixels();
316 struct DomainDisjointSet {
317 std::vector<NodeId> parent;
318 std::vector<int> size;
319
320 explicit DomainDisjointSet(int count) : parent(static_cast<std::size_t>(count)), size(static_cast<std::size_t>(count), 1) {
321 for (NodeId id = 0; id < count; ++id) {
322 parent[static_cast<std::size_t>(id)] = id;
323 }
324 }
325
326 NodeId find(NodeId id) {
327 NodeId root = id;
328 while (parent[static_cast<std::size_t>(root)] != root) {
329 root = parent[static_cast<std::size_t>(root)];
330 }
331 while (parent[static_cast<std::size_t>(id)] != id) {
332 const NodeId next = parent[static_cast<std::size_t>(id)];
333 parent[static_cast<std::size_t>(id)] = root;
334 id = next;
335 }
336 return root;
337 }
338
339 void unite(NodeId lhs, NodeId rhs) {
340 lhs = find(lhs);
341 rhs = find(rhs);
342 if (lhs == rhs) {
343 return;
344 }
345 if (size[static_cast<std::size_t>(lhs)] < size[static_cast<std::size_t>(rhs)]) {
346 std::swap(lhs, rhs);
347 }
348 parent[static_cast<std::size_t>(rhs)] = lhs;
349 size[static_cast<std::size_t>(lhs)] += size[static_cast<std::size_t>(rhs)];
350 }
351 };
352
353 using DomainEdge = std::pair<NodeId, NodeId>;
354 std::vector<std::vector<DomainEdge>> edgesByLca(static_cast<std::size_t>(tree.numInternalNodeSlots()));
355 for (NodeId source = 0; source < numPixels; ++source) {
356 const NodeId sourceSmallestNode = detail::TreeKernelAccess::smallestNodeMap(tree, source);
357 for (int targetValue : detail::CommittedGridAccess::forwardNeighbors(adjacency, source)) {
358 const NodeId target = static_cast<NodeId>(targetValue);
359 const NodeId targetSmallestNode = detail::TreeKernelAccess::smallestNodeMap(tree, target);
360 const NodeId lca = sourceSmallestNode == targetSmallestNode ? sourceSmallestNode : detail::TreeKernelAccess::lowestCommonAncestor(tree, sourceSmallestNode, targetSmallestNode);
361 edgesByLca[static_cast<std::size_t>(lca)].emplace_back(source, target);
362 }
363 }
364
365 DomainDisjointSet components(numPixels);
366 std::vector<NodeId> supportRepresentative(static_cast<std::size_t>(tree.numInternalNodeSlots()), InvalidNode);
367 for (NodeId nodeId : tree.postOrder()) {
368 for (const DomainEdge& edge : edgesByLca[static_cast<std::size_t>(nodeId)]) {
369 components.unite(edge.first, edge.second);
370 }
371
372 NodeId representative = InvalidNode;
373 auto acceptRepresentative = [&](NodeId candidate) {
374 if (representative == InvalidNode) {
375 representative = candidate;
376 return;
377 }
378 if (components.find(representative) != components.find(candidate)) {
379 std::ostringstream oss;
380 oss << context << " requires every hierarchy region to be connected in the projection graph; node " << nodeId
381 << " has a disconnected support.";
382 throw std::invalid_argument(oss.str());
383 }
384 };
385
386 for (PixelId pixel : detail::TreeKernelAccess::properParts(tree, nodeId)) {
387 acceptRepresentative(pixel);
388 }
389 for (NodeId childId : detail::TreeKernelAccess::children(tree, nodeId)) {
390 const NodeId childRepresentative = supportRepresentative[static_cast<std::size_t>(childId)];
391 acceptRepresentative(childRepresentative);
392 }
393 supportRepresentative[static_cast<std::size_t>(nodeId)] = components.find(representative);
394 }
395 }
396
406 template <class Value>
407 static void validateHierarchyValuationEstablished(const MorphologicalTree& tree, std::span<const Value> valuation,
408 HierarchyValuationPolicy policy, HierarchyValuationRangePolicy rangePolicy, const char* context) {
409 if (valuation.size() != static_cast<std::size_t>(tree.numInternalNodeSlots())) {
410 std::ostringstream oss;
411 oss << context << " requires one valuation value per dense internal node slot; expected " << tree.numInternalNodeSlots() << " values but got "
412 << valuation.size() << ".";
413 throw std::invalid_argument(oss.str());
414 }
415
416 for (NodeId nodeId : tree.aliveNodeIds()) {
417 validateValuationValue(valuation[static_cast<std::size_t>(nodeId)], nodeId, context, rangePolicy);
418 }
419
420 for (NodeId parentId : tree.aliveNodeIds()) {
421 const Value& parentValue = valuation[static_cast<std::size_t>(parentId)];
422 for (NodeId childId : detail::TreeKernelAccess::children(tree, parentId)) {
423 const Value& childValue = valuation[static_cast<std::size_t>(childId)];
424 const bool validOrder = policy == HierarchyValuationPolicy::RequireStrictHierarchy ? childValue < parentValue : !(parentValue < childValue);
425 if (!validOrder) {
426 std::ostringstream oss;
427 oss << context << " requires "
428 << (policy == HierarchyValuationPolicy::RequireStrictHierarchy ? "valuation(parent) > valuation(child)"
429 : "valuation(parent) >= valuation(child)")
430 << "; parent node " << parentId << " has value " << parentValue << " and child node " << childId << " has value " << childValue << ".";
431 throw std::invalid_argument(oss.str());
432 }
433 }
434 }
435 }
436
444 template <class Value>
445 [[nodiscard]] static std::vector<double> normalizeHierarchyValuation(const MorphologicalTree& tree, std::span<const Value> valuation) {
446 using BareValue = std::remove_cv_t<Value>;
447 bool initialized = false;
448 BareValue minValue{};
449 BareValue maxValue{};
450 for (NodeId nodeId : tree.aliveNodeIds()) {
451 const BareValue value = valuation[static_cast<std::size_t>(nodeId)];
452 if (!initialized) {
453 minValue = value;
454 maxValue = value;
455 initialized = true;
456 } else {
457 minValue = std::min(minValue, value);
458 maxValue = std::max(maxValue, value);
459 }
460 }
461
462 std::vector<double> scores(static_cast<std::size_t>(tree.numInternalNodeSlots()), 0.0);
463 if (maxValue == minValue) {
464 return scores;
465 }
466
467 for (NodeId nodeId : tree.aliveNodeIds()) {
468 const BareValue value = valuation[static_cast<std::size_t>(nodeId)];
469 long double normalized = 0.0L;
470
471 if constexpr (std::is_integral_v<BareValue>) {
472 using UnsignedValue = std::make_unsigned_t<BareValue>;
473 const UnsignedValue offset = static_cast<UnsignedValue>(value) - static_cast<UnsignedValue>(minValue);
474 const UnsignedValue range = static_cast<UnsignedValue>(maxValue) - static_cast<UnsignedValue>(minValue);
475 normalized = static_cast<long double>(offset) / static_cast<long double>(range);
476 } else {
477 const long double low = static_cast<long double>(minValue);
478 const long double high = static_cast<long double>(maxValue);
479 const long double current = static_cast<long double>(value);
480
481 if (low < 0.0L && high > 0.0L) {
482 const long double scale = std::max(-low, high);
483 const long double scaledLow = low / scale;
484 const long double scaledHigh = high / scale;
485 normalized = (current / scale - scaledLow) / (scaledHigh - scaledLow);
486 } else {
487 normalized = (current - low) / (high - low);
488 }
489 }
490
491 scores[static_cast<std::size_t>(nodeId)] = static_cast<double>(std::clamp(normalized, 0.0L, 1.0L));
492 }
493 return scores;
494 }
495};
496
497} // 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].
Projects a morphological tree hierarchy onto an image adjacency graph.
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 hasGridDomain2D() const noexcept
Tests whether pixel ids have an attached row/column layout.
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.
int numColumns() const
Returns the number of columns in the regular 2D pixel domain.
Immutable regular-grid 2D adjacency with allocation-free traversal.
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.