mmcfilters
Public API documentation
Loading...
Searching...
No Matches
TreeAltitudeAlgorithms.hpp
1#pragma once
2
3#include "../utils/Altitude.hpp"
4#include "../utils/Contract.hpp"
5#include "../utils/Image.hpp"
6#include "MorphologicalTree.hpp"
7#include "detail/HigraHierarchyDetail.hpp"
8#include "detail/TreeKernelAccess.hpp"
9
10#include <algorithm>
11#include <cmath>
12#include <cstddef>
13#include <cstdint>
14#include <span>
15#include <sstream>
16#include <stdexcept>
17#include <string>
18#include <type_traits>
19#include <utility>
20#include <vector>
21
22namespace mmcfilters {
23
24namespace detail::tree_altitude {
25
26template <class Contribution> inline void requireFiniteNodeContribution(Contribution contribution, const char* context) {
27 if constexpr (std::is_floating_point_v<Contribution>) {
28 MMCFILTERS_CONTRACT_REQUIRE(std::isfinite(contribution),
29 throw std::invalid_argument(std::string(context) + " requires finite nodeContributions."));
30 }
31}
32
34template <class Contribution>
35 requires(std::is_arithmetic_v<Contribution> && !std::is_same_v<std::remove_cv_t<Contribution>, bool>)
36[[nodiscard]] std::vector<Contribution> reconstructNodeContributionValuesEstablished(const MorphologicalTree& tree,
37 std::span<const Contribution> nodeContributions) {
38 std::vector<Contribution> accumulated(static_cast<std::size_t>(tree.numInternalNodeSlots()), Contribution{});
39 const NodeId root = tree.root();
40 accumulated[static_cast<std::size_t>(root)] = nodeContributions[static_cast<std::size_t>(root)];
41
42 std::vector<NodeId> pending{root};
43 while (!pending.empty()) {
44 const NodeId nodeId = pending.back();
45 pending.pop_back();
46 for (NodeId childId : TreeKernelAccess::children(tree, nodeId)) {
47 accumulated[static_cast<std::size_t>(childId)] =
48 accumulated[static_cast<std::size_t>(nodeId)] + nodeContributions[static_cast<std::size_t>(childId)];
49 pending.push_back(childId);
50 }
51 }
52
53 std::vector<Contribution> pixels(static_cast<std::size_t>(tree.numPixels()), Contribution{});
54 for (PixelId pixel = 0; pixel < tree.numPixels(); ++pixel) {
55 pixels[static_cast<std::size_t>(pixel)] = accumulated[static_cast<std::size_t>(TreeKernelAccess::smallestNodeMap(tree, pixel))];
56 }
57 return pixels;
58}
59
60template <class Contribution>
61 requires(std::is_arithmetic_v<Contribution> && !std::is_same_v<std::remove_cv_t<Contribution>, bool>)
62[[nodiscard]] std::vector<Contribution> reconstructNodeContributionValues(const MorphologicalTree& tree,
63 std::span<const Contribution> nodeContributions,
64 const char* context) {
65 tree.requireNotEditing(context);
67 nodeContributions.size() == static_cast<std::size_t>(tree.numInternalNodeSlots()),
68 throw std::invalid_argument(std::string(context) + " nodeContributions size must match the internal node slot count."));
69 if constexpr (std::is_floating_point_v<Contribution> && contract::validationsEnabled) {
70 for (Contribution contribution : nodeContributions) {
71 requireFiniteNodeContribution(contribution, context);
72 }
73 }
74 return reconstructNodeContributionValuesEstablished(tree, nodeContributions);
75}
76
77} // namespace detail::tree_altitude
78
87 public:
94 static const char* nodeAltitudeOrderName(NodeAltitudeOrder order) noexcept {
95 switch (order) {
96 case NodeAltitudeOrder::Increasing:
97 return "Increasing";
98 case NodeAltitudeOrder::Decreasing:
99 return "Decreasing";
100 case NodeAltitudeOrder::Unconstrained:
101 return "Unconstrained";
102 }
103 return "UNKNOWN";
104 }
105
112 static bool hasGlobalMonotoneAltitudeOrder(const MorphologicalTree& tree) noexcept { return tree.nodeAltitudeOrder() != NodeAltitudeOrder::Unconstrained; }
113
123 static void requireGlobalMonotoneAltitudeOrder(const MorphologicalTree& tree, const char* context) {
125 throw std::invalid_argument(std::string(context) + " requires a globally monotone altitude order; got " + nodeAltitudeOrderName(tree.nodeAltitudeOrder()) +
126 ".");
127 }
128 }
129
136 template <AltitudeValue T> static void validateNodeAltitudeBufferShape(const MorphologicalTree& tree, std::span<const T> altitude) {
137 MMCFILTERS_CONTRACT_REQUIRE(altitude.size() == static_cast<std::size_t>(tree.numInternalNodeSlots()),
138 throw std::runtime_error("Altitude buffer size must match the dense internal-node domain."));
139 }
140
148 template <AltitudeValue T> static void validateFiniteAltitudeValue(T altitude, std::size_t index, const char* context) {
149 if constexpr (std::is_floating_point_v<T>) {
150 const long double level = static_cast<long double>(altitude);
151 MMCFILTERS_CONTRACT_REQUIRE(std::isfinite(level), {
152 std::ostringstream oss;
153 oss << context << " requires finite floating-point altitudes; value at index " << index << " is " << level << ".";
154 throw std::invalid_argument(oss.str());
155 });
156 }
157 }
158
165 template <AltitudeValue T> static void validateFiniteAltitudeValues(std::span<const T> altitude, const char* context) {
166 if constexpr (std::is_floating_point_v<T> && contract::validationsEnabled) {
167 for (std::size_t index = 0; index < altitude.size(); ++index) {
168 validateFiniteAltitudeValue(altitude[index], index, context);
169 }
170 }
171 }
172
179 template <AltitudeValue T> static void validateFiniteImageAltitudes(const ImagePtr<T>& image, const char* context) {
180 if constexpr (std::is_floating_point_v<T> && contract::validationsEnabled) {
181 if (!image) {
182 throw std::invalid_argument("Image altitude validation requires a non-null image.");
183 }
184 validateFiniteAltitudeValues(std::span<const T>(image->rawData(), static_cast<std::size_t>(image->getSize())), context);
185 }
186 }
187
195 template <AltitudeValue T> [[nodiscard]] static T nodeAltitude(std::span<const T> altitude, NodeId nodeId) {
196 MMCFILTERS_CONTRACT_REQUIRE(nodeId >= 0 && static_cast<std::size_t>(nodeId) < altitude.size(),
197 throw std::invalid_argument("Altitude access requires a valid internal NodeId."));
198 return altitude[static_cast<std::size_t>(nodeId)];
199 }
200
213 template <AltitudeValue T> [[nodiscard]] static AltitudeDifference<T> nodeResidue(const MorphologicalTree& tree, std::span<const T> altitude, NodeId nodeId) {
214 validateNodeAltitudeBufferShape(tree, altitude);
215 MMCFILTERS_CONTRACT_REQUIRE(tree.isAlive(nodeId), throw std::invalid_argument("Node residue requires a live internal NodeId."));
216 const NodeId parentNodeId = tree.parent(nodeId);
217 if (parentNodeId == InvalidNode || parentNodeId == nodeId) {
218 return static_cast<AltitudeDifference<T>>(nodeAltitude(altitude, nodeId));
219 }
220 return static_cast<AltitudeDifference<T>>(nodeAltitude(altitude, nodeId)) - static_cast<AltitudeDifference<T>>(nodeAltitude(altitude, parentNodeId));
221 }
222
231 template <AltitudeValue T> [[nodiscard]] static std::uint8_t requireUInt8AltitudeValue(T altitude, NodeId nodeId, const char* context) {
232 const long double level = static_cast<long double>(altitude);
233 if constexpr (std::is_floating_point_v<T>) {
234 if (!std::isfinite(level)) {
235 std::ostringstream oss;
236 oss << context << " requires finite node altitudes in the uint8 domain [0, 255]; node " << nodeId << " has altitude " << level << ".";
237 throw std::invalid_argument(oss.str());
238 }
239 }
240 if (level < 0.0L || level > 255.0L) {
241 std::ostringstream oss;
242 oss << context << " requires node altitudes in the uint8 domain [0, 255]; node " << nodeId << " has altitude " << level << ".";
243 throw std::invalid_argument(oss.str());
244 }
245 return static_cast<std::uint8_t>(altitude);
246 }
247
255 template <AltitudeValue T> static void validateUInt8AltitudeDomain(const MorphologicalTree& tree, std::span<const T> altitude, const char* context) {
256 validateNodeAltitudeBufferShape(tree, altitude);
257 for (NodeId nodeId : tree.aliveNodeIds()) {
258 (void)requireUInt8AltitudeValue(nodeAltitude(altitude, nodeId), nodeId, context);
259 }
260 }
261
274 template <AltitudeValue T>
275 [[nodiscard]] static ImagePtr<T> reconstructFromNodeAltitudes(const MorphologicalTree& tree, std::span<const T> altitude,
276 const char* context = "TreeAltitudeAlgorithms::reconstructFromNodeAltitudes") {
277 (void)context;
279 validateNodeAltitudeBufferShape(tree, altitude);
281 auto imgBuffer = image->rawData();
282 for (PixelId pixel = 0; pixel < tree.numPixels(); ++pixel) {
283 const NodeId nodeId = tree.smallestNode(pixel);
284 imgBuffer[static_cast<std::size_t>(pixel)] = nodeAltitude(altitude, nodeId);
285 }
286 return image;
287 }
288
301 template <class Contribution>
302 requires(std::is_arithmetic_v<Contribution> && !std::is_same_v<std::remove_cv_t<Contribution>, bool>)
304 const MorphologicalTree& tree, std::span<const Contribution> nodeContributions,
305 const char* context = "TreeAltitudeAlgorithms::reconstructFromNodeContributions") {
306 std::vector<Contribution> pixelValues = detail::tree_altitude::reconstructNodeContributionValues(tree, nodeContributions, context);
308 Contribution* pixels = image->rawData();
309 std::copy(pixelValues.begin(), pixelValues.end(), pixels);
310 return image;
311 }
312
326 template <AltitudeValue T>
327 [[nodiscard]] static std::pair<std::vector<NodeId>, std::vector<T>> exportHigraHierarchy(const MorphologicalTree& tree, std::span<const T> altitude) {
328 tree.requireNotEditing("TreeAltitudeAlgorithms::exportHigraHierarchy");
329 const detail::ExportedHigraLayout layout = detail::computeExportedHigraLayout(tree, altitude);
330 const int numLeaves = layout.numLeaves;
331 const int numVertices = layout.numVertices;
332
333 std::vector<NodeId> parent(static_cast<std::size_t>(numVertices), InvalidNode);
334 std::vector<T> exportedAltitude(static_cast<std::size_t>(numVertices), T{});
335
336 for (NodeId oldNodeId : layout.sortedNodes) {
337 const NodeId newNodeId = layout.nodeToHigra[static_cast<std::size_t>(oldNodeId)];
338 exportedAltitude[static_cast<std::size_t>(newNodeId)] = nodeAltitude(altitude, oldNodeId);
339 }
340
342 const PixelId pixel = layout.properParts[static_cast<std::size_t>(leafIndex)];
343 const NodeId smallestNodeId = tree.smallestNode(pixel);
344 if (smallestNodeId == InvalidNode || !tree.isAlive(smallestNodeId)) {
345 throw std::runtime_error("Each proper part must belong to one alive node when exporting a compact Higra hierarchy.");
346 }
347 parent[static_cast<std::size_t>(leafIndex)] = layout.nodeToHigra[static_cast<std::size_t>(smallestNodeId)];
348 exportedAltitude[static_cast<std::size_t>(leafIndex)] = nodeAltitude(altitude, smallestNodeId);
349 }
350
351 for (NodeId oldNodeId : layout.sortedNodes) {
352 const NodeId newNodeId = layout.nodeToHigra[static_cast<std::size_t>(oldNodeId)];
354 parent[static_cast<std::size_t>(newNodeId)] =
355 oldParentNodeId == oldNodeId ? newNodeId : layout.nodeToHigra[static_cast<std::size_t>(oldParentNodeId)];
356 }
357
358 return {std::move(parent), std::move(exportedAltitude)};
359 }
360
367 template <AltitudeValue T> static void validateMonotoneNodeAltitudes(const MorphologicalTree& tree, std::span<const T> altitude) {
368 validateNodeAltitudeBufferShape(tree, altitude);
369 const NodeAltitudeOrder nodeAltitudeOrder = tree.nodeAltitudeOrder();
370 bool increasingFromRoot = false;
371 switch (nodeAltitudeOrder) {
372 case NodeAltitudeOrder::Increasing:
373 increasingFromRoot = true;
374 break;
375 case NodeAltitudeOrder::Decreasing:
376 increasingFromRoot = false;
377 break;
378 case NodeAltitudeOrder::Unconstrained:
379 return;
380 }
381
382 for (NodeId nodeId : tree.aliveNodeIds()) {
383 if (tree.isRoot(nodeId)) {
384 continue;
385 }
386
387 const NodeId parentNodeId = tree.parent(nodeId);
388 if (parentNodeId == InvalidNode || !tree.isAlive(parentNodeId)) {
389 throw std::runtime_error("Monotonic validation requires every alive non-root node to have an alive parent.");
390 }
391
392 if (increasingFromRoot) {
393 if (nodeAltitude(altitude, parentNodeId) >= nodeAltitude(altitude, nodeId)) {
394 throw std::runtime_error("Hierarchy altitude buffer must be strictly increasing from parent to child.");
395 }
396 } else if (nodeAltitude(altitude, parentNodeId) <= nodeAltitude(altitude, nodeId)) {
397 throw std::runtime_error("Hierarchy altitude buffer must be strictly decreasing from parent to child.");
398 }
399 }
400 }
401};
402
403} // 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
#define MMCFILTERS_CONTRACT_REQUIRE(condition,...)
Evaluates a caller precondition and its failure action only in checked builds.
Definition Contract.hpp:53
NodeAltitudeOrder
Global ordering constraint of node altitudes along parent-child arcs.
static Ptr create(int rows, int columns)
Creates an owned image with uninitialised pixel values.
Definition Image.hpp:89
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.
NodeAltitudeOrder nodeAltitudeOrder() const noexcept
Returns the global parent-to-child altitude ordering constraint.
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.
void requireNotEditing(const char *context) const
Rejects operations that require a committed connected topology.
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.
Pure operations over a topology and an explicit altitude buffer.
static void validateNodeAltitudeBufferShape(const MorphologicalTree &tree, std::span< const T > altitude)
Validates that an altitude buffer covers the dense internal-node domain.
static void validateUInt8AltitudeDomain(const MorphologicalTree &tree, std::span< const T > altitude, const char *context)
Validates all live node altitudes before materialising an ImageUInt8.
static std::uint8_t requireUInt8AltitudeValue(T altitude, NodeId nodeId, const char *context)
Converts one altitude value to uint8_t, rejecting values outside the output domain.
static void validateMonotoneNodeAltitudes(const MorphologicalTree &tree, std::span< const T > altitude)
Validates the hierarchy's declared global altitude order.
static void validateFiniteImageAltitudes(const ImagePtr< T > &image, const char *context)
Rejects non-finite floating-point pixels before using an image as altitude source.
static ImagePtr< Contribution > reconstructFromNodeContributions(const MorphologicalTree &tree, std::span< const Contribution > nodeContributions, const char *context="TreeAltitudeAlgorithms::reconstructFromNodeContributions")
Reconstructs an image by summing node contributions on every root-to-node branch.
static T nodeAltitude(std::span< const T > altitude, NodeId nodeId)
Reads one node altitude from an explicit altitude buffer.
static void validateFiniteAltitudeValue(T altitude, std::size_t index, const char *context)
Rejects non-finite floating-point altitudes while compiling to a no-op for integral types.
static bool hasGlobalMonotoneAltitudeOrder(const MorphologicalTree &tree) noexcept
Tests whether the topology declares a globally monotone altitude order.
static void validateFiniteAltitudeValues(std::span< const T > altitude, const char *context)
Rejects non-finite floating-point altitudes in a contiguous input range.
static void requireGlobalMonotoneAltitudeOrder(const MorphologicalTree &tree, const char *context)
Requires a declared globally monotone altitude order.
static const char * nodeAltitudeOrderName(NodeAltitudeOrder order) noexcept
Returns the name of a declared node-altitude order.
static AltitudeDifference< T > nodeResidue(const MorphologicalTree &tree, std::span< const T > altitude, NodeId nodeId)
Computes the altitude difference between one node and its parent.
static std::pair< std::vector< NodeId >, std::vector< T > > exportHigraHierarchy(const MorphologicalTree &tree, std::span< const T > altitude)
Exports a live rooted topology and explicit altitudes to a compact parent/altitude representation.
static ImagePtr< T > reconstructFromNodeAltitudes(const MorphologicalTree &tree, std::span< const T > altitude, const char *context="TreeAltitudeAlgorithms::reconstructFromNodeAltitudes")
Reconstructs a typed image from topology storage and explicit node altitudes.
Owning result for one computed scalar attribute layout and buffer.