MorphologicalAttributeFilters
Public API documentation
Loading...
Searching...
No Matches
WeightedMorphologicalTree.hpp
1#pragma once
2
3#include "MorphologicalTree.hpp"
4#include "TreeAltitudeAlgorithms.hpp"
5#include "TreeEditor.hpp"
6#include "WeightedTreeView.hpp"
7
8#include <span>
9#include <stdexcept>
10#include <utility>
11#include <vector>
12
13namespace mmcfilters {
14
15template<AltitudeValue T>
16class WeightedTreeEditor;
17
18template<AltitudeValue T>
19class WeightedMorphologicalTree;
20
21
22namespace detail {
23
32enum class AltitudeDomain {
36 InternalNodeSlots,
37
41 HigraNodeIds
42};
43
51template<AltitudeValue T>
52struct AltitudeInput {
53 std::span<const T> values;
54 AltitudeDomain domain;
55};
56
57} // namespace detail
58
72template<AltitudeValue T>
74 friend class WeightedTreeEditor<T>;
75
76public:
79
81 using altitude_buffer = std::vector<T>;
82
83private:
85 altitude_buffer altitude_;
86
93 void assignAltitudeFromDirectProperParts(const ImagePtr<T>& img) {
94 altitude_.assign(static_cast<size_t>(tree_.getNumInternalNodeSlots()), T{});
95 for (NodeId nodeId : tree_.getAliveNodeIds()) {
96 const auto properParts = tree_.getProperParts(nodeId);
97 auto it = properParts.begin();
98 if (it == properParts.end()) {
99 throw std::runtime_error("Cannot infer node altitude from a topology node without direct proper parts.");
100 }
101 altitude_[static_cast<size_t>(nodeId)] = static_cast<T>((*img)[*it]);
102 }
103 }
104
108 void assignInternalAltitude(std::span<const T> altitudeValues) {
109 if (altitudeValues.size() != static_cast<size_t>(tree_.getNumInternalNodeSlots())) {
110 throw std::invalid_argument("Internal altitude buffer size must match the dense internal-node domain.");
111 }
112 TreeAltitudeAlgorithms::validateFiniteAltitudeValues(altitudeValues, "WeightedMorphologicalTree internal altitude input");
113 altitude_.assign(altitudeValues.begin(), altitudeValues.end());
114 }
115
119 void importAltitudeFromHigra(std::span<const T> higraAltitude) {
120 if (static_cast<size_t>(tree_.getNumHigraNodes()) != higraAltitude.size()) {
121 throw std::invalid_argument("Higra altitude buffer size must match the preserved imported Higra hierarchy.");
122 }
123 TreeAltitudeAlgorithms::validateFiniteAltitudeValues(higraAltitude, "WeightedMorphologicalTree Higra altitude input");
124 altitude_.assign(static_cast<size_t>(tree_.getNumInternalNodeSlots()), T{});
125 for (NodeId slotId = 0; slotId < tree_.getNumInternalNodeSlots(); ++slotId) {
126 const NodeId higraNodeId = tree_.getHigraNodeId(slotId);
127 altitude_[static_cast<size_t>(slotId)] = static_cast<T>(higraAltitude[static_cast<size_t>(higraNodeId)]);
128 }
129 }
130
134 static bool skipsMonotoneValidation(const MorphologicalTree& tree) noexcept {
135 const MorphologicalTreeKind treeType = tree.getTreeType();
136 return treeType == MorphologicalTreeKind::TREE_OF_SHAPES || treeType == MorphologicalTreeKind::SELF_DUAL_RESIDUAL_TREE;
137 }
138
145 void validateLocalMonotoneAltitudeUpdate(NodeId nodeId, T value) const {
146 if (skipsMonotoneValidation(tree_)) {
147 return;
148 }
149
150 const bool increasingTowardLeaves = tree_.getTreeType() == MorphologicalTreeKind::MAX_TREE;
151 if (!tree_.isRoot(nodeId)) {
152 const NodeId parentNodeId = tree_.getNodeParent(nodeId);
153 if (parentNodeId == InvalidNode || !tree_.isAlive(parentNodeId)) {
154 throw std::runtime_error("Monotone altitude update requires an alive non-root node to have an alive parent.");
155 }
156 const T parentAltitude = altitude_[static_cast<size_t>(parentNodeId)];
157 if (increasingTowardLeaves && parentAltitude > value) {
158 throw std::runtime_error("Max-tree altitude update must keep altitude non-decreasing from parent to child.");
159 }
160 if (!increasingTowardLeaves && parentAltitude < value) {
161 throw std::runtime_error("Min-tree altitude update must keep altitude non-increasing from parent to child.");
162 }
163 }
164
165 for (NodeId childId : tree_.getChildren(nodeId)) {
166 const T childAltitude = altitude_[static_cast<size_t>(childId)];
167 if (increasingTowardLeaves && value > childAltitude) {
168 throw std::runtime_error("Max-tree altitude update must keep altitude non-decreasing from parent to child.");
169 }
170 if (!increasingTowardLeaves && value < childAltitude) {
171 throw std::runtime_error("Min-tree altitude update must keep altitude non-increasing from parent to child.");
172 }
173 }
174 }
175
176private:
177 WeightedMorphologicalTree() = delete;
178
179public:
180 WeightedMorphologicalTree(const WeightedMorphologicalTree&) = delete;
181 WeightedMorphologicalTree& operator=(const WeightedMorphologicalTree&) = delete;
182
185
188
201 if (!img) {
202 throw std::invalid_argument("WeightedMorphologicalTree construction requires a non-null image.");
203 }
204 if (img->getNumRows() <= 0 || img->getNumCols() <= 0 || img->getSize() <= 0) {
205 throw std::invalid_argument("WeightedMorphologicalTree construction requires a non-empty 2D image.");
206 }
207
208 if (img->getNumRows() != tree_.getNumRowsOfImage() || img->getNumCols() != tree_.getNumColsOfImage() || img->getSize() != tree_.getNumTotalProperParts()) {
209 throw std::invalid_argument("WeightedMorphologicalTree construction requires an image domain matching the topology.");
210 }
211
212 assignAltitudeFromDirectProperParts(img);
214 }
215
226 WeightedMorphologicalTree(detail::MorphologicalTreeConstructionTag, MorphologicalTree&& topology, detail::AltitudeInput<T> altitude) : tree_(std::move(topology)) {
227 switch (altitude.domain) {
228 case detail::AltitudeDomain::InternalNodeSlots:
229 assignInternalAltitude(altitude.values);
230 break;
231 case detail::AltitudeDomain::HigraNodeIds:
232 importAltitudeFromHigra(altitude.values);
233 break;
234 }
236 }
237
244
253 return tree_;
254 }
255
260 return altitude_;
261 }
262
267 return std::span<const T>(altitude_);
268 }
269
274 return WeightedTreeView<T>(tree_, altitudeSpan());
275 }
276
284 tree_.requireNotEditing("WeightedMorphologicalTree::setAltitudeBuffer");
285 if (altitudeBuffer.size() != static_cast<size_t>(tree_.getNumInternalNodeSlots())) {
286 throw std::runtime_error("Altitude buffer size must match the dense internal-node domain.");
287 }
288 TreeAltitudeAlgorithms::validateFiniteAltitudeValues(std::span<const T>(altitudeBuffer), "WeightedMorphologicalTree::setAltitudeBuffer");
290 altitude_ = std::move(altitudeBuffer);
291 }
292
301 tree_.requireNotEditing("WeightedMorphologicalTree::setAltitudeBufferUnchecked");
302 if (altitudeBuffer.size() != static_cast<size_t>(tree_.getNumInternalNodeSlots())) {
303 throw std::runtime_error("Altitude buffer size must match the dense internal-node domain.");
304 }
305 TreeAltitudeAlgorithms::validateFiniteAltitudeValues(std::span<const T>(altitudeBuffer), "WeightedMorphologicalTree::setAltitudeBufferUnchecked");
306 altitude_ = std::move(altitudeBuffer);
307 }
308
315
320 if (!tree_.isAlive(nodeId) || static_cast<size_t>(nodeId) >= altitude_.size()) {
321 throw std::invalid_argument("WeightedMorphologicalTree::getAltitude requires a live internal NodeId.");
322 }
323 return altitude_[static_cast<size_t>(nodeId)];
324 }
325
330 tree_.requireNotEditing("WeightedMorphologicalTree::setAltitude");
331 if (!tree_.isAlive(nodeId) || static_cast<size_t>(nodeId) >= altitude_.size()) {
332 throw std::invalid_argument("WeightedMorphologicalTree::setAltitude requires a live internal NodeId.");
333 }
334 TreeAltitudeAlgorithms::validateFiniteAltitudeValue(value, static_cast<std::size_t>(nodeId), "WeightedMorphologicalTree::setAltitude");
335 validateLocalMonotoneAltitudeUpdate(nodeId, value);
336 altitude_[static_cast<size_t>(nodeId)] = value;
337 }
338
347 tree_.requireNotEditing("WeightedMorphologicalTree::setAltitudeUnchecked");
348 if (!tree_.isAlive(nodeId) || static_cast<size_t>(nodeId) >= altitude_.size()) {
349 throw std::invalid_argument("WeightedMorphologicalTree::setAltitudeUnchecked requires a live internal NodeId.");
350 }
351 TreeAltitudeAlgorithms::validateFiniteAltitudeValue(value, static_cast<std::size_t>(nodeId), "WeightedMorphologicalTree::setAltitudeUnchecked");
352 altitude_[static_cast<size_t>(nodeId)] = value;
353 }
354
362 tree_.requireNotEditing("WeightedMorphologicalTree::pruneNode");
363 tree_.pruneNode(nodeId);
364 }
365
373 tree_.requireNotEditing("WeightedMorphologicalTree::mergeNodeIntoParent");
375 }
376
383
388 return TreeAltitudeAlgorithms::reconstructImage(tree_, altitudeSpan(), "WeightedMorphologicalTree::reconstructImage");
389 }
390
399 [[nodiscard]] std::pair<std::vector<NodeId>, std::vector<T>> exportHigraHierarchy() const {
401 }
402
406 [[nodiscard("Discarding the editor leaves the weighted tree edit session open")]] WeightedTreeEditor<T> edit() {
407 return WeightedTreeEditor<T>(*this);
408 }
409};
410
418template<AltitudeValue T>
420 friend class WeightedMorphologicalTree<T>;
421
422private:
424 TreeEditor editor_;
425
429 explicit WeightedTreeEditor(WeightedMorphologicalTree<T>& weighted) : weighted_(weighted), editor_(weighted.tree_.edit()) {}
430
431public:
432
439 [[nodiscard("Discarding a detached node id makes the staged edit impossible to complete safely")]] NodeId createDetachedNode(T altitude = T{}) {
440 TreeAltitudeAlgorithms::validateFiniteAltitudeValue(altitude, 0, "WeightedTreeEditor::createDetachedNode");
441 const std::size_t requiredAltitudeSize = static_cast<std::size_t>(weighted_.tree_.getNumInternalNodeSlots()) + (weighted_.tree_.getNumFreeNodeSlots() == 0 ? 1u : 0u);
442 weighted_.altitude_.reserve(requiredAltitudeSize);
443 const NodeId nodeId = editor_.createDetachedNode();
444 weighted_.altitude_.resize(static_cast<size_t>(weighted_.tree_.getNumInternalNodeSlots()), T{});
445 weighted_.altitude_[static_cast<size_t>(nodeId)] = altitude;
446 return nodeId;
447 }
448
456 void setNodeAltitude(NodeId nodeId, T altitude) {
457 if (!weighted_.tree_.isAlive(nodeId)) {
458 throw std::invalid_argument("WeightedTreeEditor::setNodeAltitude requires a live node.");
459 }
460 TreeAltitudeAlgorithms::validateFiniteAltitudeValue(altitude, static_cast<std::size_t>(nodeId), "WeightedTreeEditor::setNodeAltitude");
461 weighted_.altitude_[static_cast<size_t>(nodeId)] = altitude;
462 }
463
468 editor_.detach(nodeId);
469 }
470
477
484
491
498
505
512
517 editor_.releaseNode(nodeId);
518 }
519
524 editor_.setRoot(nodeId);
525 }
526
531 editor_.pruneNode(nodeId);
532 }
533
540
545 return editor_.hasDetachedAliveNodes();
546 }
547
554 return editor_.validate();
555 }
556
560 [[nodiscard("Inspect the validation result or use commit() for exception-based failure handling")]] TreeValidationResult validateAndCommit() noexcept {
562 if (!result.ok) {
563 return result;
564 }
565 try {
566 weighted_.validateMonotoneAltitude();
567 } catch (const std::exception& ex) {
568 return {false, ex.what()};
569 } catch (...) {
570 return {false, "WeightedTreeEditor monotone-altitude validation failed with an unknown error."};
571 }
572 editor_.commitUnchecked();
573 return result;
574 }
575
579 void commit() {
581 if (!result.ok) {
582 throw std::runtime_error(result.message);
583 }
584 }
585
590 editor_.commitUnchecked();
591 }
592};
593
594} // namespace mmcfilters
int NodeId
Node identifier type used throughout the project.
Definition Common.hpp:17
std::shared_ptr< Image< PixelType > > ImagePtr
Shared pointer alias for an image with arbitrary pixel type.
Definition Image.hpp:253
Mutable morphological tree built directly on proper parts and dense node ids.
int getNumTotalProperParts() const
Returns the size of the proper-part domain.
int getNumInternalNodeSlots() 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.
NodeId getHigraNodeId(NodeId nodeId) const noexcept
Returns the preserved imported Higra node id for one live tree node.
void pruneNode(NodeId nodeId)
Prunes the subtree of nodeId, moving all its support to the parent.
bool isRoot(NodeId nodeId) const
Tests whether nodeId is the current root.
int getNumHigraNodes() const
Returns the size of the preserved imported Higra node-id domain.
ProperPartsRange getProperParts(NodeId nodeId) const
Returns a fail-fast range over the direct proper parts of nodeId.
MorphologicalTreeKind getTreeType() const noexcept
Returns the current tree type.
int getNumColsOfImage() const
Returns the number of image columns in the attached 2D domain.
void requireNotEditing(const char *context) const
Rejects operations that require a committed connected topology.
AliveNodeRange getAliveNodeIds() const
Returns a fail-fast range over all live node ids.
NodeId getNodeParent(NodeId nodeId) const
Returns the direct parent of nodeId.
int getNumRowsOfImage() const
Returns the number of image rows in the attached 2D domain.
void mergeNodeIntoParent(NodeId nodeId)
Merges nodeId into its parent and releases the emptied slot.
static void validateAltitudeBufferShape(const MorphologicalTree &tree, std::span< const T > altitude)
Validates that an altitude buffer covers the dense internal-node domain.
static void validateMonotoneAltitude(const MorphologicalTree &tree, std::span< const T > altitude)
Validates altitude monotonicity for max-trees and min-trees.
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 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 AltitudeDiff< T > getNodeResidue(const MorphologicalTree &tree, std::span< const T > altitude, NodeId nodeId)
Computes the altitude difference between one node and its parent.
static ImagePtr< T > reconstructImage(const MorphologicalTree &tree, std::span< const T > altitude, const char *context="TreeAltitudeAlgorithms::reconstructImage")
Reconstructs a typed image from topology ownership and explicit node altitudes.
Thin edit-session facade for multi-step topology updates.
TreeValidationResult validate() const noexcept
Runs strong validation without closing the session.
void detach(NodeId nodeId)
Detaches one non-root node from the connected rooted component.
bool hasDetachedAliveNodes() const noexcept
Returns whether the staged edit still has detached alive nodes.
void mergeNodeIntoParent(NodeId nodeId)
Applies the committed-safe parent merge inside the staged edit.
void reparent(NodeId nodeId, NodeId newParentId)
Reparents one live non-root node under another live node.
void attach(NodeId parentId, NodeId detachedNodeId)
Attaches one detached node back under the connected rooted tree.
NodeId createDetachedNode()
Creates a live detached node in the topological hierarchy.
void setRoot(NodeId nodeId)
Promotes nodeId to become the connected root.
void removeChild(NodeId parentNodeId, NodeId childId, bool releaseNodeFlag)
Detaches a direct child from its parent and optionally releases an empty detached slot.
void moveProperParts(NodeId targetNodeId, NodeId sourceNodeId)
Transfers every direct proper part from sourceNodeId to targetNodeId.
void releaseNode(NodeId nodeId)
Releases an empty detached non-root node slot.
void moveProperPart(NodeId targetNodeId, NodeId sourceNodeId, NodeId properPartId)
Transfers one direct proper part from sourceNodeId to targetNodeId.
void moveChildren(NodeId parentId, NodeId sourceId)
Transfers every direct child of sourceId under parentId.
void commitUnchecked() noexcept
Finalizes a staged edit without running the linear validation.
void pruneNode(NodeId nodeId)
Applies the committed-safe subtree prune inside the staged edit.
Wrapper pairing MorphologicalTree topology with an external altitude buffer.
void setAltitudeBuffer(altitude_buffer altitudeBuffer)
Replaces the owned altitude buffer after full validation.
AltitudeSpan< T > altitudeSpan() const noexcept
Returns a read-only span over the dense altitude buffer.
ImagePtr< T > reconstructionImage() const
Reconstructs an image by assigning each proper part its owner altitude.
void mergeNodeIntoParent(NodeId nodeId)
Merges one node into its parent through the owned topology.
void setAltitudeBufferUnchecked(altitude_buffer altitudeBuffer)
Replaces the owned altitude buffer without checking tree-order monotonicity.
std::vector< T > altitude_buffer
Dense altitude buffer indexed by internal NodeId.
std::pair< std::vector< NodeId >, std::vector< T > > exportHigraHierarchy() const
Exports the current live rooted tree to a new compact Higra parent/altitude representation.
WeightedMorphologicalTree(detail::MorphologicalTreeConstructionTag, MorphologicalTree &&topology, detail::AltitudeInput< T > altitude)
Creates a weighted tree from an explicit altitude input view.
void pruneNode(NodeId nodeId)
Prunes a complete subtree through the owned topology.
void validateMonotoneAltitude() const
Validates the current altitude buffer against the topology order.
void validateAltitudeBufferShape() const
Checks that the altitude buffer covers the dense internal-node domain.
const altitude_buffer & getAltitudeBuffer() const noexcept
Returns the dense altitude buffer indexed by internal NodeId.
WeightedTreeView< T > asView() const
Creates a non-owning weighted view over this owner.
WeightedMorphologicalTree(WeightedMorphologicalTree &&) noexcept=default
Transfers topology and altitude ownership from another weighted tree.
void setAltitude(NodeId nodeId, T value)
Updates one live node altitude with local monotonicity validation.
const MorphologicalTree & topology() const noexcept
Returns read-only access to the owned topology.
WeightedTreeEditor< T > edit()
Opens the only public entrypoint for staged weighted edits.
void setAltitudeUnchecked(NodeId nodeId, T value)
Updates one node altitude without checking tree-order monotonicity.
T getAltitude(NodeId nodeId) const
Returns one live node altitude from the dense buffer.
AltitudeDiff< T > getNodeResidue(NodeId nodeId) const
Returns the altitude difference between a node and its parent.
Edit-session facade for WeightedMorphologicalTree.
TreeValidationResult validateAndCommit() noexcept
Validates topology, validates altitude order, then closes the edit.
void moveProperPart(NodeId targetNodeId, NodeId sourceNodeId, NodeId properPartId)
Moves one direct proper part between nodes.
void commit()
Exception-based wrapper around validateAndCommit().
NodeId createDetachedNode(T altitude=T{})
Creates a detached topology node and initializes its altitude.
void setRoot(NodeId nodeId)
Promotes one node to the topology root.
void reparent(NodeId nodeId, NodeId newParentId)
Reparents one node through the structural editor.
void removeChild(NodeId parentNodeId, NodeId childId, bool releaseNodeFlag)
Detaches a direct child and optionally releases an empty node slot.
void commitUnchecked() noexcept
Closes the weighted edit without topology or altitude validation.
void moveChildren(NodeId parentId, NodeId sourceId)
Moves all direct children from sourceId under parentId.
void setNodeAltitude(NodeId nodeId, T altitude)
Sets a live node altitude during a staged topology edit.
void pruneNode(NodeId nodeId)
Applies the topology prune helper inside the weighted edit session.
TreeValidationResult validate() const noexcept
Validates only the staged topology.
void mergeNodeIntoParent(NodeId nodeId)
Applies the topology merge helper inside the weighted edit session.
void moveProperParts(NodeId targetNodeId, NodeId sourceNodeId)
Moves all direct proper parts between nodes.
void detach(NodeId nodeId)
Detaches one non-root node through the structural editor.
bool hasDetachedAliveNodes() const noexcept
Returns whether the structural edit still has detached live nodes.
void releaseNode(NodeId nodeId)
Releases an empty detached node slot.
void attach(NodeId parentId, NodeId detachedNodeId)
Attaches one detached node through the structural editor.
Owning result for one computed scalar attribute layout and buffer.
Non-throwing validation result returned by edit-session checks.