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/HigraExportLayoutDetail.hpp"
8
9#include <algorithm>
10#include <cmath>
11#include <cstddef>
12#include <cstdint>
13#include <span>
14#include <sstream>
15#include <stdexcept>
16#include <type_traits>
17#include <utility>
18#include <vector>
19
20namespace mmcfilters {
21
22namespace detail::tree_altitude {
23
24template <class Contribution>
25 requires(std::is_arithmetic_v<Contribution> && !std::is_same_v<std::remove_cv_t<Contribution>, bool>)
26[[nodiscard]] std::vector<Contribution> reconstructNodeContributionValues(const MorphologicalTree& tree,
27 std::span<const Contribution> nodeContributions,
28 const char* context) {
29 tree.requireNotEditing(context);
31 nodeContributions.size() == static_cast<std::size_t>(tree.numInternalNodeSlots()),
32 throw std::invalid_argument(std::string(context) + " nodeContributions size must match the internal node slot count."));
33 if constexpr (std::is_floating_point_v<Contribution> && contract::validationsEnabled) {
34 for (Contribution contribution : nodeContributions) {
35 if (!std::isfinite(contribution)) {
36 throw std::invalid_argument(std::string(context) + " requires finite nodeContributions.");
37 }
38 }
39 }
40
41 std::vector<Contribution> accumulated(static_cast<std::size_t>(tree.numInternalNodeSlots()), Contribution{});
42 const NodeId root = tree.root();
43 accumulated[static_cast<std::size_t>(root)] = nodeContributions[static_cast<std::size_t>(root)];
44
45 std::vector<NodeId> pending{root};
46 while (!pending.empty()) {
47 const NodeId nodeId = pending.back();
48 pending.pop_back();
49 for (NodeId childId : tree.children(nodeId)) {
50 accumulated[static_cast<std::size_t>(childId)] =
51 accumulated[static_cast<std::size_t>(nodeId)] + nodeContributions[static_cast<std::size_t>(childId)];
52 pending.push_back(childId);
53 }
54 }
55
56 std::vector<Contribution> pixels(static_cast<std::size_t>(tree.numPixels()), Contribution{});
57 for (PixelId pixel = 0; pixel < tree.numPixels(); ++pixel) {
58 pixels[static_cast<std::size_t>(pixel)] = accumulated[static_cast<std::size_t>(tree.smallestNode(pixel))];
59 }
60 return pixels;
61}
62
63} // namespace detail::tree_altitude
64
73 public:
80 template <AltitudeValue T> static void validateNodeAltitudeBufferShape(const MorphologicalTree& tree, std::span<const T> altitude) {
81 MMCFILTERS_CONTRACT_REQUIRE(altitude.size() == static_cast<std::size_t>(tree.numInternalNodeSlots()),
82 throw std::runtime_error("Altitude buffer size must match the dense internal-node domain."));
83 }
84
92 template <AltitudeValue T> static void validateFiniteAltitudeValue(T altitude, std::size_t index, const char* context) {
93 if constexpr (std::is_floating_point_v<T>) {
94 const long double level = static_cast<long double>(altitude);
95 MMCFILTERS_CONTRACT_REQUIRE(std::isfinite(level), {
96 std::ostringstream oss;
97 oss << context << " requires finite floating-point altitudes; value at index " << index << " is " << level << ".";
98 throw std::invalid_argument(oss.str());
99 });
100 }
101 }
102
109 template <AltitudeValue T> static void validateFiniteAltitudeValues(std::span<const T> altitude, const char* context) {
110 if constexpr (std::is_floating_point_v<T> && contract::validationsEnabled) {
111 for (std::size_t index = 0; index < altitude.size(); ++index) {
112 validateFiniteAltitudeValue(altitude[index], index, context);
113 }
114 }
115 }
116
123 template <AltitudeValue T> static void validateFiniteImageAltitudes(const ImagePtr<T>& image, const char* context) {
124 if constexpr (std::is_floating_point_v<T> && contract::validationsEnabled) {
125 if (!image) {
126 throw std::invalid_argument("Image altitude validation requires a non-null image.");
127 }
128 validateFiniteAltitudeValues(std::span<const T>(image->rawData(), static_cast<std::size_t>(image->getSize())), context);
129 }
130 }
131
139 template <AltitudeValue T> [[nodiscard]] static T nodeAltitude(std::span<const T> altitude, NodeId nodeId) {
140 MMCFILTERS_CONTRACT_REQUIRE(nodeId >= 0 && static_cast<std::size_t>(nodeId) < altitude.size(),
141 throw std::invalid_argument("Altitude access requires a valid internal NodeId."));
142 return altitude[static_cast<std::size_t>(nodeId)];
143 }
144
157 template <AltitudeValue T> [[nodiscard]] static AltitudeDifference<T> nodeResidue(const MorphologicalTree& tree, std::span<const T> altitude, NodeId nodeId) {
158 validateNodeAltitudeBufferShape(tree, altitude);
159 MMCFILTERS_CONTRACT_REQUIRE(tree.isAlive(nodeId), throw std::invalid_argument("Node residue requires a live internal NodeId."));
160 const NodeId parentNodeId = tree.parent(nodeId);
162 return static_cast<AltitudeDifference<T>>(nodeAltitude(altitude, nodeId));
163 }
164 return static_cast<AltitudeDifference<T>>(nodeAltitude(altitude, nodeId)) - static_cast<AltitudeDifference<T>>(nodeAltitude(altitude, parentNodeId));
165 }
166
175 template <AltitudeValue T> [[nodiscard]] static std::uint8_t requireUInt8AltitudeValue(T altitude, NodeId nodeId, const char* context) {
176 const long double level = static_cast<long double>(altitude);
177 if constexpr (std::is_floating_point_v<T>) {
178 if (!std::isfinite(level)) {
179 std::ostringstream oss;
180 oss << context << " requires finite node altitudes in the uint8 domain [0, 255]; node " << nodeId << " has altitude " << level << ".";
181 throw std::invalid_argument(oss.str());
182 }
183 }
184 if (level < 0.0L || level > 255.0L) {
185 std::ostringstream oss;
186 oss << context << " requires node altitudes in the uint8 domain [0, 255]; node " << nodeId << " has altitude " << level << ".";
187 throw std::invalid_argument(oss.str());
188 }
189 return static_cast<std::uint8_t>(altitude);
190 }
191
199 template <AltitudeValue T> static void validateUInt8AltitudeDomain(const MorphologicalTree& tree, std::span<const T> altitude, const char* context) {
200 validateNodeAltitudeBufferShape(tree, altitude);
201 for (NodeId nodeId : tree.aliveNodeIds()) {
203 }
204 }
205
218 template <AltitudeValue T>
219 [[nodiscard]] static ImagePtr<T> reconstructFromNodeAltitudes(const MorphologicalTree& tree, std::span<const T> altitude,
220 const char* context = "TreeAltitudeAlgorithms::reconstructFromNodeAltitudes") {
221 (void)context;
223 validateNodeAltitudeBufferShape(tree, altitude);
225 auto imgBuffer = image->rawData();
226 for (PixelId pixel = 0; pixel < tree.numPixels(); ++pixel) {
227 const NodeId nodeId = tree.smallestNode(pixel);
228 imgBuffer[static_cast<std::size_t>(pixel)] = nodeAltitude(altitude, nodeId);
229 }
230 return image;
231 }
232
245 template <class Contribution>
246 requires(std::is_arithmetic_v<Contribution> && !std::is_same_v<std::remove_cv_t<Contribution>, bool>)
248 const MorphologicalTree& tree, std::span<const Contribution> nodeContributions,
249 const char* context = "TreeAltitudeAlgorithms::reconstructFromNodeContributions") {
250 std::vector<Contribution> pixelValues = detail::tree_altitude::reconstructNodeContributionValues(tree, nodeContributions, context);
252 Contribution* pixels = image->rawData();
253 std::copy(pixelValues.begin(), pixelValues.end(), pixels);
254 return image;
255 }
256
270 template <AltitudeValue T>
271 [[nodiscard]] static std::pair<std::vector<NodeId>, std::vector<T>> exportHigraHierarchy(const MorphologicalTree& tree, std::span<const T> altitude) {
272 tree.requireNotEditing("TreeAltitudeAlgorithms::exportHigraHierarchy");
273 const detail::ExportedHigraLayout layout = detail::computeExportedHigraLayout(tree, altitude);
274 const int numLeaves = layout.numLeaves;
275 const int numVertices = layout.numVertices;
276
277 std::vector<NodeId> parent(static_cast<std::size_t>(numVertices), InvalidNode);
278 std::vector<T> exportedAltitude(static_cast<std::size_t>(numVertices), T{});
279
280 for (NodeId oldNodeId : layout.sortedNodes) {
281 const NodeId newNodeId = layout.nodeToHigra[static_cast<std::size_t>(oldNodeId)];
282 exportedAltitude[static_cast<std::size_t>(newNodeId)] = nodeAltitude(altitude, oldNodeId);
283 }
284
286 const PixelId pixel = layout.properParts[static_cast<std::size_t>(leafIndex)];
287 const NodeId smallestNodeId = tree.smallestNode(pixel);
288 if (smallestNodeId == InvalidNode || !tree.isAlive(smallestNodeId)) {
289 throw std::runtime_error("Each proper part must belong to one alive node when exporting a compact Higra hierarchy.");
290 }
291 parent[static_cast<std::size_t>(leafIndex)] = layout.nodeToHigra[static_cast<std::size_t>(smallestNodeId)];
292 exportedAltitude[static_cast<std::size_t>(leafIndex)] = nodeAltitude(altitude, smallestNodeId);
293 }
294
295 for (NodeId oldNodeId : layout.sortedNodes) {
296 const NodeId newNodeId = layout.nodeToHigra[static_cast<std::size_t>(oldNodeId)];
298 parent[static_cast<std::size_t>(newNodeId)] =
299 oldParentNodeId == oldNodeId ? newNodeId : layout.nodeToHigra[static_cast<std::size_t>(oldParentNodeId)];
300 }
301
302 return {std::move(parent), std::move(exportedAltitude)};
303 }
304
311 template <AltitudeValue T> static void validateMonotoneNodeAltitudes(const MorphologicalTree& tree, std::span<const T> altitude) {
312 validateNodeAltitudeBufferShape(tree, altitude);
313 const NodeAltitudeOrder nodeAltitudeOrder = tree.nodeAltitudeOrder();
314 bool increasingFromRoot = false;
315 switch (nodeAltitudeOrder) {
316 case NodeAltitudeOrder::Increasing:
317 increasingFromRoot = true;
318 break;
319 case NodeAltitudeOrder::Decreasing:
320 increasingFromRoot = false;
321 break;
322 case NodeAltitudeOrder::Unconstrained:
323 return;
324 }
325
326 for (NodeId nodeId : tree.aliveNodeIds()) {
327 if (tree.isRoot(nodeId)) {
328 continue;
329 }
330
331 const NodeId parentNodeId = tree.parent(nodeId);
332 if (parentNodeId == InvalidNode || !tree.isAlive(parentNodeId)) {
333 throw std::runtime_error("Monotonic validation requires every alive non-root node to have an alive parent.");
334 }
335
336 if (increasingFromRoot) {
337 if (nodeAltitude(altitude, parentNodeId) >= nodeAltitude(altitude, nodeId)) {
338 throw std::runtime_error("Hierarchy altitude buffer must be strictly increasing from parent to child.");
339 }
340 } else if (nodeAltitude(altitude, parentNodeId) <= nodeAltitude(altitude, nodeId)) {
341 throw std::runtime_error("Hierarchy altitude buffer must be strictly decreasing from parent to child.");
342 }
343 }
344 }
345};
346
347} // 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:105
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 void validateFiniteAltitudeValues(std::span< const T > altitude, const char *context)
Rejects non-finite floating-point altitudes in a contiguous input range.
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.