MorphologicalAttributeFilters
Public API documentation
Loading...
Searching...
No Matches
TreeOfShapesProducer.hpp
1#pragma once
2
3#include "../utils/Image.hpp"
4#include "../utils/Common.hpp"
6#include "detail/NativeHierarchyValidationDetail.hpp"
7
8#include <algorithm>
9#include <array>
10#include <concepts>
11#include <cstdint>
12#include <deque>
13#include <limits>
14#include <stdexcept>
15#include <tuple>
16#include <vector>
17
18namespace mmcfilters {
19
20namespace detail {
22enum class TopographicImmersionMode { SelfDualSpan, Min4Max8, Min8Max4 };
23} // namespace detail
24
25inline constexpr int TopographicInterpolationScale = 2;
26inline constexpr int TopographicInterpolationPadding = 1;
27inline constexpr int ToSUInt8Depth = 8;
28inline constexpr int ToSSelfDualDepth = 9;
29
30using ToSGrayLevel = uint16_t;
31using ToSFloodDepth = uint32_t;
32
42
50 static value_type encode(ToSGrayLevel constructionLevel, detail::TopographicImmersionMode immersionMode) noexcept {
51 if (immersionMode == detail::TopographicImmersionMode::SelfDualSpan) {
52 return constructionLevel;
53 }
54 return static_cast<value_type>(TopographicInterpolationScale * static_cast<unsigned int>(constructionLevel));
55 }
56};
57
68 std::vector<NodeId> parent;
70 std::vector<NodeId> smallestNodeMap;
72 std::vector<ToSGrayLevel> nodeAltitudes;
76 int numRows = 0;
78 int numColumns = 0;
80 detail::NativeTopologyProof topologyProof;
81
89 [[nodiscard]] detail::ValidatedNativeHierarchy<ToSGrayLevel> takeValidatedHierarchy(MorphologicalTreeSemantics semantics) && {
90 return detail::makeValidatedNativeHierarchy<ToSGrayLevel>(std::move(parent), std::move(smallestNodeMap), std::move(nodeAltitudes), root,
91 GridDomain2D{numRows, numColumns}, std::move(semantics), std::move(topologyProof));
92 }
93};
94
95/************************ Tree of Shapes support ************************/
96
98namespace detail {
99
100/*
101 * Adaptive adjacency backend used by the tree-of-shapes construction.
102 *
103 * Diagonal links are activated on demand so that the interpolated grid can
104 * emulate the required 4/8-connectivity behaviour during the union-find pass.
105 * Each pixel may carry four diagonal flags: SW, NE, SE, and NW.
106 */
107enum class DiagonalConnection : uint8_t { None = 0, Sw = 1 << 0, Ne = 1 << 1, Se = 1 << 2, Nw = 1 << 3 };
108
109// Helper operators for diagonal-connection flags.
117inline DiagonalConnection operator|(DiagonalConnection a, DiagonalConnection b) {
118 return static_cast<DiagonalConnection>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
119}
120
128inline DiagonalConnection& operator|=(DiagonalConnection& a, DiagonalConnection b) {
129 a = a | b;
130 return a;
131}
132
140inline bool operator&(DiagonalConnection a, DiagonalConnection b) { return static_cast<uint8_t>(a) & static_cast<uint8_t>(b); }
141
145class AdjacencyUC {
146 private:
148 int numRows;
150 int numColumns;
152 std::vector<uint8_t> dconnFlags; // 4-connect. + diag. connect.
153 // N, W, S, E, SW, NE, SE, NW
155 const std::vector<int> offsetRows = {-1, 0, 1, 0, 1, -1, 1, -1};
157 const std::vector<int> offsetColumns = {0, -1, 0, 1, -1, 1, 1, -1};
159 bool enableDiagonalConnection;
161 const std::vector<DiagonalConnection> requiredDiagonal = {DiagonalConnection::Sw, DiagonalConnection::Ne, DiagonalConnection::Se, DiagonalConnection::Nw};
162
163 public:
171 AdjacencyUC(int rows, int columns, bool enableDiagonalConnection) : numRows(rows), numColumns(columns), enableDiagonalConnection(enableDiagonalConnection) {
172 if (enableDiagonalConnection)
173 dconnFlags.resize(rows * columns, 0);
174 }
175
179 ~AdjacencyUC() {}
180
188 void setDiagonalConnection(int row, int column, DiagonalConnection conn) { dconnFlags[ImageUtils::to1D(row, column, numColumns)] |= static_cast<uint8_t>(conn); }
189
196 void setDiagonalConnection(PixelId pixel, DiagonalConnection conn) { dconnFlags[pixel] |= static_cast<uint8_t>(conn); }
197
206 bool hasConnection(int row, int column, DiagonalConnection conn) const { return dconnFlags[ImageUtils::to1D(row, column, numColumns)] & static_cast<uint8_t>(conn); }
207
215 uint8_t getConnections(int row, int column) const { return dconnFlags[ImageUtils::to1D(row, column, numColumns)]; }
216
220 class NeighborIterator {
221 private:
223 AdjacencyUC& instance;
225 int row;
227 int column;
229 std::size_t id;
230
234 void advanceToValid() {
235 while (id < instance.offsetRows.size()) {
236 int r = row + instance.offsetRows[id];
237 int c = column + instance.offsetColumns[id];
238 if (r >= 0 && c >= 0 && r < instance.numRows && c < instance.numColumns) {
239 if (id < 4 || (instance.enableDiagonalConnection && instance.dconnFlags[ImageUtils::to1D(row, column, instance.numColumns)] &
240 static_cast<uint8_t>(instance.requiredDiagonal[id - 4]))) {
241 return;
242 }
243 }
244 ++id;
245 }
246 }
247
248 public:
257 NeighborIterator(AdjacencyUC& adj, int row, int column, int id) : instance(adj), row(row), column(column), id(id) { advanceToValid(); }
258
264 PixelId operator*() const {
265 int dr = instance.offsetRows[id];
266 int dc = instance.offsetColumns[id];
267 return ImageUtils::to1D(row + dr, column + dc, instance.numColumns);
268 }
269
275 NeighborIterator& operator++() {
276 ++id;
277 advanceToValid();
278 return *this;
279 }
280
287 bool operator==(const NeighborIterator& other) const { return id == other.id; }
288
295 bool operator!=(const NeighborIterator& other) const { return !(*this == other); }
296 };
297
301 class NeighborRange {
302 private:
304 AdjacencyUC& instance;
306 int row;
308 int column;
309
310 public:
318 NeighborRange(AdjacencyUC& instance, int row, int column) : instance(instance), row(row), column(column) {}
319
325 NeighborIterator begin() { return NeighborIterator(instance, row, column, 0); }
331 NeighborIterator end() { return NeighborIterator(instance, row, column, 8); }
332 };
333
340 NeighborRange getNeighborIndices(PixelId pixel) {
341 auto [row, column] = ImageUtils::to2D(pixel, numColumns);
342 return NeighborRange(*this, row, column);
343 }
344
352 NeighborRange getNeighborIndices(int row, int column) { return NeighborRange(*this, row, column); }
353};
354
358class PriorityQueueToS {
359 private:
361 std::vector<std::deque<PixelId>> buckets;
363 int currentPriority;
365 int numElements;
367 int maxPriorityLevels;
368
369 public:
375 PriorityQueueToS(int depthOfImage = ToSUInt8Depth) : currentPriority(0), numElements(0), maxPriorityLevels(1 << depthOfImage) {
376 buckets.resize(maxPriorityLevels);
377 }
378
385 void initial(PixelId element, int priority) {
386 currentPriority = priority;
387 buckets[priority].push_back(element);
388 numElements++;
389 }
395 int getCurrentPriority() { return currentPriority; }
401 bool isEmpty() { return numElements == 0; }
402
410 void priorityPush(PixelId element, int lower, int upper) {
411 int priority;
412 if (lower > currentPriority) {
413 priority = lower;
414 } else if (upper < currentPriority) {
415 priority = upper;
416 } else {
417 priority = currentPriority;
418 }
419 numElements++;
420 buckets[priority].push_back(element);
421 }
422
428 PixelId priorityPop() {
429 if (buckets[currentPriority].empty()) {
430 int nextPriority = -1;
431 for (int distance = 1; distance < maxPriorityLevels; ++distance) {
432 const int lowerPriority = currentPriority - distance;
433 if (lowerPriority >= 0 && !buckets[lowerPriority].empty()) {
434 nextPriority = lowerPriority;
435 break;
436 }
437
438 const int upperPriority = currentPriority + distance;
439 if (upperPriority < maxPriorityLevels && !buckets[upperPriority].empty()) {
440 nextPriority = upperPriority;
441 break;
442 }
443 }
444
445 if (nextPriority == -1) {
446 throw std::runtime_error("PriorityQueueToS is empty.");
447 }
448 currentPriority = nextPriority;
449 }
450
451 const PixelId element = buckets[currentPriority].front();
452 buckets[currentPriority].pop_front();
453
454 numElements--;
455 return element;
456 }
457};
458
459} // namespace detail
460
462
472 private:
474 using AdjacencyUC = detail::AdjacencyUC;
476 using DiagonalConnection = detail::DiagonalConnection;
478 using PriorityQueueToS = detail::PriorityQueueToS;
479
481 struct FloodResult {
483 std::vector<ToSFloodDepth> treeLevel;
485 std::vector<ToSGrayLevel> grayLevel;
487 std::vector<PixelId> order;
489 AdjacencyUC adjacency;
490 };
491
493 struct CanonicalFloodTree {
495 std::vector<ToSFloodDepth> treeLevel;
497 std::vector<ToSGrayLevel> grayLevel;
499 std::vector<PixelId> order;
501 std::vector<PixelId> parent;
502 };
503
505 TopographicConvention convention_;
507 detail::TopographicImmersionMode immersionMode_ = detail::TopographicImmersionMode::SelfDualSpan;
508
515 static detail::TopographicImmersionMode validateConvention(const TopographicConvention& convention) {
516 if (convention.infinityPixel < 0) {
517 throw std::invalid_argument("Tree-of-shapes infinity pixel must be non-negative.");
518 }
519 if (std::holds_alternative<SelfDualSpanImmersion>(convention.immersion)) {
520 return detail::TopographicImmersionMode::SelfDualSpan;
521 }
522 const auto& adjacencies = std::get<ComplementaryGridImmersion>(convention.immersion).complementaryAdjacencies;
523 if (adjacencies.minAdjacency.is4connectivity() && adjacencies.maxAdjacency.is8connectivity()) {
524 return detail::TopographicImmersionMode::Min4Max8;
525 }
526 if (adjacencies.minAdjacency.is8connectivity() && adjacencies.maxAdjacency.is4connectivity()) {
527 return detail::TopographicImmersionMode::Min8Max4;
528 }
529 throw std::invalid_argument("Complementary-grid immersion requires minimum/maximum adjacencies in the canonical 4/8 or 8/4 pairing.");
530 }
531
533 void validateConventionForImage(const ImageUInt8Ptr& image) const {
534 if (!image) {
535 throw std::invalid_argument("TreeOfShapesProducer requires a non-null image.");
536 }
537 if (const auto* immersion = std::get_if<ComplementaryGridImmersion>(&convention_.immersion)) {
538 const auto& adjacencies = immersion->complementaryAdjacencies;
539 const int rows = image->getNumRows();
540 const int columns = image->getNumColumns();
541 if (adjacencies.minAdjacency.getNumRows() != rows || adjacencies.minAdjacency.getNumColumns() != columns ||
542 adjacencies.maxAdjacency.getNumRows() != rows || adjacencies.maxAdjacency.getNumColumns() != columns) {
543 throw std::invalid_argument("Tree-of-shapes complementary adjacencies must match the source image domain.");
544 }
545 }
546 static_cast<void>(validatedInfinityPixel(interpolatedNumRows(image->getNumRows()), interpolatedNumColumns(image->getNumColumns())));
547 }
548
554 inline bool usesConnectivityMap() const noexcept {
555 return immersionMode_ != detail::TopographicImmersionMode::SelfDualSpan;
556 }
557
563 inline bool usesHighDiagonalAtSaddle() const noexcept { return immersionMode_ == detail::TopographicImmersionMode::Min4Max8; }
564
570 inline bool usesExteriorPadding() const noexcept { return convention_.domainExtension == TopographicDomainExtension::ExteriorRing; }
571
577 inline int interpolationOffset() const noexcept { return usesExteriorPadding() ? TopographicInterpolationPadding : 0; }
578
585 inline int checkedInterpolatedExtent(int extent) const {
586 if (extent <= 0) {
587 throw std::invalid_argument("TreeOfShapesProducer requires positive image dimensions.");
588 }
589 const std::int64_t interpolated = TopographicInterpolationScale * static_cast<std::int64_t>(extent) + (usesExteriorPadding() ? 1 : -1);
590 if (interpolated <= 0 || interpolated > std::numeric_limits<int>::max()) {
591 throw std::overflow_error("Tree-of-shapes interpolated dimension exceeds int range.");
592 }
593 return static_cast<int>(interpolated);
594 }
595
602 inline int interpolatedNumRows(int numRows) const { return checkedInterpolatedExtent(numRows); }
603
610 inline int interpolatedNumColumns(int numColumns) const { return checkedInterpolatedExtent(numColumns); }
611
619 inline int checkedDomainSize(int numRows, int numColumns) const {
620 if (numRows <= 0 || numColumns <= 0 || numRows > std::numeric_limits<int>::max() / numColumns) {
621 throw std::overflow_error("Tree-of-shapes domain size exceeds int range.");
622 }
623 return numRows * numColumns;
624 }
625
632 inline int originalPointRow(int row) const noexcept { return TopographicInterpolationScale * row + interpolationOffset(); }
633
640 inline int originalPointColumn(int column) const noexcept { return TopographicInterpolationScale * column + interpolationOffset(); }
641
648 inline ToSGrayLevel scaledOriginalLevel(uint8_t value) const noexcept {
649 return static_cast<ToSGrayLevel>(TopographicInterpolationScale * static_cast<int>(value));
650 }
651
659 inline PixelId validatedInfinityPixel(int interpNumRows, int interpNumColumns) const {
660 const std::int64_t activeSize = static_cast<std::int64_t>(interpNumRows) * static_cast<std::int64_t>(interpNumColumns);
661 if (convention_.infinityPixel < 0 || static_cast<std::int64_t>(convention_.infinityPixel) >= activeSize) {
662 throw std::invalid_argument("Tree-of-shapes infinity pixel must belong to the active topographic domain.");
663 }
664 return convention_.infinityPixel;
665 }
666
674 inline void setDiagonal0Connection(AdjacencyUC& adj, int row, int column) const {
675 adj.setDiagonalConnection(row, column - 1, DiagonalConnection::Se);
676 adj.setDiagonalConnection(row + 1, column, DiagonalConnection::Nw);
677
678 adj.setDiagonalConnection(row - 1, column - 1, DiagonalConnection::Se);
679 adj.setDiagonalConnection(row, column, DiagonalConnection::Se | DiagonalConnection::Nw);
680 adj.setDiagonalConnection(row + 1, column + 1, DiagonalConnection::Nw);
681
682 adj.setDiagonalConnection(row - 1, column, DiagonalConnection::Se);
683 adj.setDiagonalConnection(row, column + 1, DiagonalConnection::Nw);
684 }
685
693 inline void setDiagonal1Connection(AdjacencyUC& adj, int row, int column) const {
694 adj.setDiagonalConnection(row, column - 1, DiagonalConnection::Ne);
695 adj.setDiagonalConnection(row - 1, column, DiagonalConnection::Sw);
696
697 adj.setDiagonalConnection(row - 1, column + 1, DiagonalConnection::Sw);
698 adj.setDiagonalConnection(row, column, DiagonalConnection::Sw | DiagonalConnection::Ne);
699 adj.setDiagonalConnection(row + 1, column - 1, DiagonalConnection::Ne);
700
701 adj.setDiagonalConnection(row + 1, column, DiagonalConnection::Ne);
702 adj.setDiagonalConnection(row, column + 1, DiagonalConnection::Sw);
703 }
704
705 std::tuple<std::vector<ToSGrayLevel>, std::vector<ToSGrayLevel>, AdjacencyUC>
717 cropExteriorInterpolation(std::vector<ToSGrayLevel> paddedMin, std::vector<ToSGrayLevel> paddedMax, AdjacencyUC paddedAdjacency, int paddedRows,
718 int paddedColumns, bool adaptiveDiagonal) const {
719 if (paddedRows < 3 || paddedColumns < 3) {
720 throw std::logic_error("Tree-of-shapes padded immersion cannot be cropped.");
721 }
722 const int rows = paddedRows - 2;
723 const int columns = paddedColumns - 2;
724 const int size = checkedDomainSize(rows, columns);
725 std::vector<ToSGrayLevel> croppedMin(static_cast<std::size_t>(size));
726 std::vector<ToSGrayLevel> croppedMax(static_cast<std::size_t>(size));
727 AdjacencyUC croppedAdjacency(rows, columns, adaptiveDiagonal);
728
729 for (int row = 0; row < rows; ++row) {
730 for (int column = 0; column < columns; ++column) {
731 const PixelId source = ImageUtils::to1D(row + 1, column + 1, paddedColumns);
732 const PixelId target = ImageUtils::to1D(row, column, columns);
733 croppedMin[static_cast<std::size_t>(target)] = paddedMin[static_cast<std::size_t>(source)];
734 croppedMax[static_cast<std::size_t>(target)] = paddedMax[static_cast<std::size_t>(source)];
735 if (adaptiveDiagonal) {
736 const std::uint8_t connections = paddedAdjacency.getConnections(row + 1, column + 1);
737 if (connections != 0) {
738 croppedAdjacency.setDiagonalConnection(target, static_cast<DiagonalConnection>(connections));
739 }
740 }
741 }
742 }
743 return {std::move(croppedMin), std::move(croppedMax), std::move(croppedAdjacency)};
744 }
745
746 public:
753 : convention_(std::move(convention)), immersionMode_(validateConvention(convention_)) {}
754
760 [[nodiscard]] const TopographicConvention& convention() const noexcept { return convention_; }
761
766
768
774 std::tuple<std::vector<ToSGrayLevel>, std::vector<ToSGrayLevel>, AdjacencyUC> interpolateImage(const ImageUInt8Ptr& imgPtr) const {
775 // Implements the self-dual span-based immersion used by Boutry's PhD
776 // thesis: ISpan(u) followed by front propagation from a median-valued
777 // outer boundary. Internal levels are stored in Z/2 by scaling values
778 // by 2, so odd integers represent half gray levels.
779 if (!imgPtr) {
780 throw std::invalid_argument("TreeOfShapesProducer requires a non-null image.");
781 }
782 if (!usesExteriorPadding()) {
784 paddedConvention.domainExtension = TopographicDomainExtension::ExteriorRing;
785 paddedConvention.infinityPixel = 0;
787 auto [paddedMin, paddedMax, paddedAdjacency] = paddedProducer.interpolateImage(imgPtr);
788 const int paddedRows = paddedProducer.interpolatedNumRows(imgPtr->getNumRows());
789 const int paddedColumns = paddedProducer.interpolatedNumColumns(imgPtr->getNumColumns());
790 return cropExteriorInterpolation(std::move(paddedMin), std::move(paddedMax), std::move(paddedAdjacency), paddedRows, paddedColumns, false);
791 }
792 auto img = imgPtr->rawData();
793 int numRows = imgPtr->getNumRows();
794 int numColumns = imgPtr->getNumColumns();
795 if (numRows <= 0 || numColumns <= 0) {
796 throw std::invalid_argument("TreeOfShapesProducer requires a non-empty image.");
797 }
798 if (numRows == 1 && numColumns == 1) {
799 const int interpNumRows = interpolatedNumRows(numRows);
800 const int interpNumColumns = interpolatedNumColumns(numColumns);
801 const int interpSize = checkedDomainSize(interpNumRows, interpNumColumns);
802 std::vector<ToSGrayLevel> interpolationMin(static_cast<std::size_t>(interpSize), scaledOriginalLevel(img[0]));
803 std::vector<ToSGrayLevel> interpolationMax(static_cast<std::size_t>(interpSize), scaledOriginalLevel(img[0]));
804 AdjacencyUC adj(interpNumRows, interpNumColumns, false);
805 return std::make_tuple(std::move(interpolationMin), std::move(interpolationMax), std::move(adj));
806 }
807
808 constexpr int adjCircleColumn[] = {-1, +1, -1, +1};
809 constexpr int adjCircleRow[] = {-1, -1, +1, +1};
810
811 constexpr int adjRetHorColumn[] = {0, 0};
812 constexpr int adjRetHorRow[] = {-1, +1};
813
814 constexpr int adjRetVerColumn[] = {+1, -1};
815 constexpr int adjRetVerRow[] = {0, 0};
816
817 int interpNumColumns = interpolatedNumColumns(numColumns);
818 int interpNumRows = interpolatedNumRows(numRows);
819 int size = checkedDomainSize(interpNumRows, interpNumColumns);
820
821 // Allocate interpolation result buffers for minimum and maximum levels.
822 std::vector<ToSGrayLevel> interpolationMin(size);
823 std::vector<ToSGrayLevel> interpolationMax(size);
824
825 // Collect each boundary pixel exactly once. The closed-form perimeter
826 // size 2 * (rows + columns) - 4 is valid only when both dimensions are at
827 // least two and previously left zero-filled entries for thin images.
828 std::array<int, 256> boundaryHistogram{};
829 int numBoundary = 0;
830
831 PixelId pT;
832
833 for (PixelId pixel = 0; pixel < numColumns * numRows; ++pixel) {
834 auto [row, column] = ImageUtils::to2D(pixel, numColumns);
835
836 // Check whether the pixel lies on the image border.
837 if (row == 0 || row == numRows - 1 || column == 0 || column == numColumns - 1) {
838 ++boundaryHistogram[static_cast<std::size_t>(img[pixel])];
839 ++numBoundary;
840 }
841
842 // Compute the interpolated-image index.
843 pT = ImageUtils::to1D(originalPointRow(row), originalPointColumn(column), interpNumColumns);
844
845 // Assign interpolation values.
846 interpolationMin[pT] = interpolationMax[pT] = scaledOriginalLevel(img[pixel]);
847 }
848
849 auto boundaryValueAtRank = [&](int rank) {
850 int cumulative = 0;
851 for (int value = 0; value < 256; ++value) {
852 cumulative += boundaryHistogram[static_cast<std::size_t>(value)];
853 if (rank < cumulative) {
854 return value;
855 }
856 }
857 throw std::runtime_error("Tree-of-shapes boundary histogram is inconsistent.");
858 };
859 int median;
860 if (numBoundary % 2 == 0) {
862 } else {
863 median = TopographicInterpolationScale * boundaryValueAtRank(numBoundary / 2);
864 }
865 // std::cout << "Interpolation (Median): " << median << std::endl;
866
867 PixelId qT;
868 int qColumn, qRow, min, max;
869 const int* adjColumn = nullptr;
870 const int* adjRow = nullptr;
871 int adjSize;
872 AdjacencyUC adj(interpNumRows, interpNumColumns, false);
873
874 for (int row = 0; row < interpNumRows; row++) {
875 for (int column = 0; column < interpNumColumns; column++) {
876 if (column % 2 == 1 && row % 2 == 1)
877 continue;
878 pT = ImageUtils::to1D(row, column, interpNumColumns);
879 if (column == 0 || column == interpNumColumns - 1 || row == 0 || row == interpNumRows - 1) {
880 max = median;
881 min = median;
882 } else {
883 if (column % 2 == 0 && row % 2 == 0) {
886 adjSize = 4;
887 } else if (column % 2 == 0 && row % 2 == 1) {
890 adjSize = 2;
891 } else if (column % 2 == 1 && row % 2 == 0) {
894 adjSize = 2;
895 }
896
897 min = std::numeric_limits<int>::max();
898 max = std::numeric_limits<int>::min();
899 for (int i = 0; i < adjSize; i++) {
900 qRow = row + adjRow[i];
901 qColumn = column + adjColumn[i];
902
903 if (qRow >= 0 && qColumn >= 0 && qRow < interpNumRows && qColumn < interpNumColumns) {
905
906 if (interpolationMax[qT] > max) {
908 }
909 if (interpolationMin[qT] < min) {
911 }
912 } else {
913 if (median > max) {
914 max = median;
915 }
916 if (median < min) {
917 min = median;
918 }
919 }
920 }
921 }
922 interpolationMin[pT] = static_cast<ToSGrayLevel>(min);
923 interpolationMax[pT] = static_cast<ToSGrayLevel>(max);
924 }
925 }
926 return std::make_tuple(std::move(interpolationMin), std::move(interpolationMax), std::move(adj));
927 }
928
935 std::tuple<std::vector<ToSGrayLevel>, std::vector<ToSGrayLevel>, AdjacencyUC> interpolateImage4c8c(const ImageUInt8Ptr& imgPtr) const {
936 // Implements the optimized 2D immersion/connectivity-map rules from
937 // Carlinet, Crozet, and Geraud, "The Tree of Shapes Turned into a
938 // Max-Tree: A Simple and Efficient Linear Algorithm", ICIP 2018.
939 if (!imgPtr) {
940 throw std::invalid_argument("TreeOfShapesProducer requires a non-null image.");
941 }
942 if (!usesExteriorPadding()) {
943 TopographicConvention paddedConvention = convention_;
944 paddedConvention.domainExtension = TopographicDomainExtension::ExteriorRing;
945 paddedConvention.infinityPixel = 0;
946 TreeOfShapesProducer paddedProducer(std::move(paddedConvention));
947 auto [paddedMin, paddedMax, paddedAdjacency] = paddedProducer.interpolateImage4c8c(imgPtr);
948 const int paddedRows = paddedProducer.interpolatedNumRows(imgPtr->getNumRows());
949 const int paddedColumns = paddedProducer.interpolatedNumColumns(imgPtr->getNumColumns());
950 return cropExteriorInterpolation(std::move(paddedMin), std::move(paddedMax), std::move(paddedAdjacency), paddedRows, paddedColumns, true);
951 }
952 auto img = imgPtr->rawData();
953 int numRows = imgPtr->getNumRows();
954 int numColumns = imgPtr->getNumColumns();
955 if (numRows <= 0 || numColumns <= 0) {
956 throw std::invalid_argument("TreeOfShapesProducer requires a non-empty image.");
957 }
958
959 int interpNumColumns = interpolatedNumColumns(numColumns);
960 int interpNumRows = interpolatedNumRows(numRows);
961 int size = checkedDomainSize(interpNumRows, interpNumColumns);
962 AdjacencyUC adj(interpNumRows, interpNumColumns, true);
963
964 // Allocate interpolation result buffers for minimum and maximum levels.
965 std::vector<ToSGrayLevel> interpolationMin(size);
966 std::vector<ToSGrayLevel> interpolationMax(size);
967
968 PixelId pT;
969 // Compute interval from 2-faces.
970 for (PixelId pixel = 0; pixel < numColumns * numRows; ++pixel) {
971 auto [row, column] = ImageUtils::to2D(pixel, numColumns);
972
973 // Compute the interpolated-image index.
974 pT = ImageUtils::to1D(originalPointRow(row), originalPointColumn(column), interpNumColumns);
975
976 // Assign interpolation values.
977 interpolationMin[pT] = interpolationMax[pT] = img[pixel];
978 }
979
980 auto getValue = [&](int row, int column) -> int {
981 int origRow = (row - 1) / 2;
982 int originalColumn = (column - 1) / 2;
983 return img[ImageUtils::to1D(origRow, originalColumn, numColumns)];
984 };
985
986 // Borders.
987 for (int row = 0; row < interpNumRows; row++) {
988 int column;
989 if (row % 2 == 1) { // horizontal e vertical
990 column = 0;
991 int v1 = getValue(row, column + 1);
992 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
993 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
994
995 column = interpNumColumns - 1;
996 v1 = getValue(row, column - 1);
997 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
998 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
999 } else { // circulos
1000 if (row == 0) {
1001 column = 0;
1002 int v1 = getValue(row + 1, column + 1);
1003 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1004 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1005
1006 column = interpNumColumns - 1;
1007 v1 = getValue(row + 1, column - 1);
1008 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1009 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1010
1011 } else if (row == interpNumRows - 1) {
1012 column = 0;
1013 int v1 = getValue(row - 1, 1);
1014 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1015 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1016
1017 column = interpNumColumns - 1;
1018 v1 = getValue(row - 1, column - 1);
1019 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1020 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1021 } else {
1022 column = 0;
1023 int v1 = getValue(row - 1, column + 1);
1024 int v2 = getValue(row + 1, column + 1);
1025 interpolationMin[ImageUtils::to1D(row, 0, interpNumColumns)] = std::min(v1, v2);
1026 interpolationMax[ImageUtils::to1D(row, 0, interpNumColumns)] = std::max(v1, v2);
1027
1028 column = interpNumColumns - 1;
1029 v1 = getValue(row - 1, column - 1);
1030 v2 = getValue(row + 1, column - 1);
1031 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = std::min(v1, v2);
1032 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = std::max(v1, v2);
1033 }
1034 }
1035 }
1036
1037 for (int column = 1; column < interpNumColumns - 1; column++) {
1038 int row;
1039 if (column % 2 == 1) { // horizontal e vertical
1040 row = 0;
1041 int v1 = getValue(row + 1, column);
1042 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1043 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1044
1045 row = interpNumRows - 1;
1046 v1 = getValue(row - 1, column);
1047 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1048 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1049 } else { // circulos
1050 row = 0;
1051 int v1 = getValue(row + 1, column - 1);
1052 int v2 = getValue(row + 1, column + 1);
1053 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = std::min(v1, v2);
1054 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = std::max(v1, v2);
1055
1056 row = interpNumRows - 1;
1057 v1 = getValue(row - 1, column - 1);
1058 v2 = getValue(row - 1, column + 1);
1059 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = std::min(v1, v2);
1060 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = std::max(v1, v2);
1061 }
1062 }
1063
1064 // Compute interval from 1-faces
1065 for (int row = 1; row < interpNumRows - 1; row++) {
1066 for (int column = 1; column < interpNumColumns - 1; column++) {
1067 if (row % 2 == 1 && column % 2 == 1)
1068 continue; // Already defined.
1069
1070 pT = ImageUtils::to1D(row, column, interpNumColumns);
1071 if (column % 2 == 0 && row % 2 == 1) {
1072 int v1 = getValue(row, column + 1);
1073 int v2 = getValue(row, column - 1);
1074 interpolationMin[pT] = std::min(v1, v2);
1075 interpolationMax[pT] = std::max(v1, v2);
1076 } else if (column % 2 == 1 && row % 2 == 0) {
1077 int v1 = getValue(row + 1, column);
1078 int v2 = getValue(row - 1, column);
1079 interpolationMin[pT] = std::min(v1, v2);
1080 interpolationMax[pT] = std::max(v1, v2);
1081 }
1082 }
1083 }
1084 // Compute interval from 0-faces
1085 for (int row = 1; row < interpNumRows - 1; row++) {
1086 for (int column = 1; column < interpNumColumns - 1; column++) {
1087 if (row % 2 == 1 && column % 2 == 1)
1088 continue; // Already defined.
1089 pT = ImageUtils::to1D(row, column, interpNumColumns);
1090 if (row % 2 == 0 && column % 2 == 0) {
1091 // | v0 | v1 |
1092 // | v2 | v3 |
1093 int v0 = getValue(row - 1, column - 1);
1094 int v1 = getValue(row + 1, column - 1);
1095 int v2 = getValue(row - 1, column + 1);
1096 int v3 = getValue(row + 1, column + 1);
1097
1098 const int min_v0v3 = std::min(v0, v3);
1099 const int max_v0v3 = std::max(v0, v3);
1100 const int min_v1v2 = std::min(v1, v2);
1101 const int max_v1v2 = std::max(v1, v2);
1102 const bool diagonal0IsHigh = min_v0v3 > max_v1v2;
1103 const bool diagonal1IsHigh = min_v1v2 > max_v0v3;
1104
1105 if (diagonal0IsHigh || diagonal1IsHigh) {
1106 const bool chooseDiagonal0 = usesHighDiagonalAtSaddle() ? diagonal0IsHigh : !diagonal0IsHigh;
1107 if (chooseDiagonal0) {
1108 setDiagonal0Connection(adj, row, column);
1109 interpolationMin[pT] = min_v0v3;
1110 interpolationMax[pT] = max_v0v3;
1111 } else {
1112 setDiagonal1Connection(adj, row, column);
1113 interpolationMin[pT] = min_v1v2;
1114 interpolationMax[pT] = max_v1v2;
1115 }
1116 } else {
1117 // Non-critical configuration.
1118 interpolationMin[pT] = std::min({v0, v1, v2, v3});
1119 interpolationMax[pT] = std::max({v0, v1, v2, v3});
1120 }
1121 }
1122 }
1123 }
1124 return std::make_tuple(std::move(interpolationMin), std::move(interpolationMax), std::move(adj));
1125 }
1126
1127 private:
1134 FloodResult floodImage(const ImageUInt8Ptr& imgPtr) const {
1135 if (!imgPtr) {
1136 throw std::invalid_argument("TreeOfShapesProducer requires a non-null image.");
1137 }
1138
1139 int numRows = imgPtr->getNumRows();
1140 int numColumns = imgPtr->getNumColumns();
1141
1142 int interpNumColumns = interpolatedNumColumns(numColumns);
1143 int interpNumRows = interpolatedNumRows(numRows);
1144 int size = checkedDomainSize(interpNumRows, interpNumColumns);
1145 const bool connectivityMap = usesConnectivityMap();
1146 auto [interpolationMin, interpolationMax, adj] = connectivityMap ? interpolateImage4c8c(imgPtr) : interpolateImage(imgPtr);
1147
1148 std::vector<uint8_t> dejavu(size, 0);
1149 std::vector<PixelId> imgR(size); // Ordered pixels.
1150 std::vector<ToSFloodDepth> imgU(size); // Monotone max-tree levels.
1151 std::vector<ToSGrayLevel> grayLevel(size); // Actual propagation levels.
1152
1153 PriorityQueueToS queue(connectivityMap ? ToSUInt8Depth : ToSSelfDualDepth); // Priority queue.
1154 const PixelId infinityPixel = validatedInfinityPixel(interpNumRows, interpNumColumns);
1155 int priorityQueueOld = interpolationMin[infinityPixel];
1156 queue.initial(infinityPixel, priorityQueueOld);
1157 dejavu[infinityPixel] = true;
1158
1159 int order = 0;
1160 ToSFloodDepth depth = 0;
1161 while (!queue.isEmpty()) {
1162 const PixelId pixel = queue.priorityPop(); // Pop the element with highest priority.
1163 int priorityQueue = queue.getCurrentPriority(); // Current priority.
1164 if (connectivityMap) {
1165 if (priorityQueue != priorityQueueOld)
1166 depth++;
1167 imgU[pixel] = depth;
1168 } else {
1169 imgU[pixel] = static_cast<ToSFloodDepth>(priorityQueue);
1170 }
1171 grayLevel[pixel] = static_cast<ToSGrayLevel>(priorityQueue);
1172
1173 // Store h in the correct output order.
1174 imgR[order++] = pixel;
1175
1176 // Adjacencies.
1177 for (PixelId neighbor : adj.getNeighborIndices(pixel)) {
1178 if (!dejavu[neighbor]) {
1179 queue.priorityPush(neighbor, interpolationMin[neighbor], interpolationMax[neighbor]);
1180 dejavu[neighbor] = true; // Mark as processed.
1181 }
1182 }
1183 priorityQueueOld = priorityQueue;
1184 }
1185 if (order != size) {
1186 throw std::runtime_error("Tree-of-shapes propagation did not visit the complete interpolated domain.");
1187 }
1188 return FloodResult{std::move(imgU), std::move(grayLevel), std::move(imgR), std::move(adj)};
1189 }
1190
1197 CanonicalFloodTree buildCanonicalFloodTree(const ImageUInt8Ptr& imgPtr) const {
1198 FloodResult flood = floodImage(imgPtr);
1199 const int numPixelsInterp = static_cast<int>(flood.treeLevel.size());
1200
1201 std::vector<PixelId> zPar(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1202 std::vector<PixelId> parentInterpolate(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1203 auto findRoot = [&](PixelId pStar) {
1204 while (zPar[pStar] != pStar) {
1205 zPar[pStar] = zPar[zPar[pStar]];
1206 pStar = zPar[pStar];
1207 }
1208 return pStar;
1209 };
1210
1211 for (int i = numPixelsInterp - 1; i >= 0; --i) {
1212 const PixelId pStar = flood.order[static_cast<size_t>(i)];
1213 parentInterpolate[pStar] = pStar;
1214 zPar[pStar] = pStar;
1215 for (PixelId qStar : flood.adjacency.getNeighborIndices(pStar)) {
1216 if (zPar[qStar] != InvalidPixel) {
1217 const PixelId rStar = findRoot(qStar);
1218 if (pStar != rStar) {
1219 parentInterpolate[rStar] = pStar;
1220 zPar[rStar] = pStar;
1221 }
1222 }
1223 }
1224 }
1225
1226 auto sameLevel = [&](PixelId aStar, PixelId bStar) { return flood.treeLevel[aStar] == flood.treeLevel[bStar]; };
1227 for (PixelId pStar : flood.order) {
1228 const PixelId qStar = parentInterpolate[pStar];
1229 if (sameLevel(parentInterpolate[qStar], qStar)) {
1230 parentInterpolate[pStar] = parentInterpolate[qStar];
1231 }
1232 }
1233
1234 return CanonicalFloodTree{std::move(flood.treeLevel), std::move(flood.grayLevel), std::move(flood.order), std::move(parentInterpolate)};
1235 }
1236
1237 public:
1244 std::tuple<std::vector<ToSFloodDepth>, std::vector<PixelId>, AdjacencyUC> sort(const ImageUInt8Ptr& imgPtr) const {
1245 FloodResult flood = floodImage(imgPtr);
1246 return std::make_tuple(std::move(flood.treeLevel), std::move(flood.order), std::move(flood.adjacency));
1247 }
1248
1249 // Tests whether an interpolated pixel corresponds to an original pixel.
1257 inline bool isOriginal1D(PixelId pixel, int interpNumColumns) const {
1258 const int row = pixel / interpNumColumns;
1259 const int column = pixel - row * interpNumColumns;
1260 const int offset = interpolationOffset();
1261 return (row & 1) == offset && (column & 1) == offset;
1262 }
1263
1264 // Maps an interpolated pixel to the original image domain.
1273 inline PixelId toOriginal1D(PixelId pStar, int interNumColumns, int numColumns) const {
1274 int r = pStar / interNumColumns;
1275 int c = pStar - r * interNumColumns; // Avoid the modulo operator.
1276 const int offset = interpolationOffset();
1277 return ((r - offset) >> 1) * numColumns + ((c - offset) >> 1);
1278 }
1280
1281 private:
1294 [[nodiscard]] TreeOfShapesBuildResult buildTreeOfShapes(const ImageUInt8Ptr& imgPtr) const {
1295 if (!imgPtr) {
1296 throw std::invalid_argument("TreeOfShapesProducer requires a non-null image.");
1297 }
1298 const int numRows = imgPtr->getNumRows();
1299 const int numColumns = imgPtr->getNumColumns();
1300 const int numPixels = checkedDomainSize(numRows, numColumns);
1301 const int interpNumColumns = interpolatedNumColumns(numColumns);
1302
1303 CanonicalFloodTree canonical = buildCanonicalFloodTree(imgPtr);
1304 const int numPixelsInterp = static_cast<int>(canonical.parent.size());
1305 auto sameLevel = [&](PixelId aStar, PixelId bStar) {
1306 return canonical.treeLevel[static_cast<size_t>(aStar)] == canonical.treeLevel[static_cast<size_t>(bStar)];
1307 };
1308 auto repOf = [&](PixelId pStar) {
1309 const PixelId parentStar = canonical.parent[static_cast<size_t>(pStar)];
1310 return (parentStar == pStar || sameLevel(parentStar, pStar)) ? parentStar : pStar;
1311 };
1312
1313 std::vector<PixelId> canonicalReps;
1314 canonicalReps.reserve(static_cast<size_t>(numPixels));
1315 for (PixelId pStar : canonical.order) {
1316 const PixelId parentStar = canonical.parent[static_cast<size_t>(pStar)];
1317 if (parentStar == pStar || !sameLevel(parentStar, pStar)) {
1318 canonicalReps.push_back(pStar);
1319 }
1320 }
1321 if (canonicalReps.empty()) {
1322 throw std::runtime_error("Tree-of-shapes construction produced no canonical interpolated node.");
1323 }
1324
1325 const PixelId rawRoot = canonicalReps.front();
1326 std::vector<PixelId> rawParent(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1327 std::vector<PixelId> firstRawChild(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1328 std::vector<PixelId> nextRawSibling(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1329 for (PixelId repStar : canonicalReps) {
1330 if (repStar == rawRoot) {
1331 rawParent[static_cast<size_t>(repStar)] = repStar;
1332 continue;
1333 }
1334 const PixelId parentRep = repOf(canonical.parent[static_cast<size_t>(repStar)]);
1335 if (parentRep < 0 || parentRep >= numPixelsInterp || rawParent[static_cast<size_t>(parentRep)] == InvalidPixel) {
1336 throw std::runtime_error("Tree-of-shapes canonical node has no canonical parent.");
1337 }
1338 rawParent[static_cast<size_t>(repStar)] = parentRep;
1339 nextRawSibling[static_cast<size_t>(repStar)] = firstRawChild[static_cast<size_t>(parentRep)];
1340 firstRawChild[static_cast<size_t>(parentRep)] = repStar;
1341 }
1342
1343 std::vector<int> directCount(static_cast<size_t>(numPixelsInterp), 0);
1344 std::vector<PixelId> representativePixelByElement(static_cast<size_t>(numPixels), InvalidPixel);
1345 for (PixelId pStar = 0; pStar < numPixelsInterp; ++pStar) {
1346 if (!isOriginal1D(pStar, interpNumColumns)) {
1347 continue;
1348 }
1349 const PixelId elementId = toOriginal1D(pStar, interpNumColumns, numColumns);
1350 if (elementId < 0 || elementId >= numPixels) {
1351 throw std::runtime_error("Tree-of-shapes projection produced an element outside the source domain.");
1352 }
1353 const PixelId representativePixel = repOf(pStar);
1354 representativePixelByElement[static_cast<size_t>(elementId)] = representativePixel;
1355 ++directCount[static_cast<size_t>(representativePixel)];
1356 }
1357 if (std::find(representativePixelByElement.begin(), representativePixelByElement.end(), InvalidPixel) != representativePixelByElement.end()) {
1358 throw std::runtime_error("Tree-of-shapes projection did not assign every source-domain element.");
1359 }
1360
1361 // The canonical depth, parent, and full interpolated order are no
1362 // longer needed after the raw hierarchy and source-domain smallest nodes have
1363 // been extracted. Release them before allocating the projection buffer.
1364 std::vector<ToSFloodDepth>().swap(canonical.treeLevel);
1365 std::vector<PixelId>().swap(canonical.parent);
1366 std::vector<PixelId>().swap(canonical.order);
1367
1368 // Bottom-up support projection. projectedRep[r] is the canonical
1369 // representative of the distinct projected support rooted at r.
1370 std::vector<PixelId> projectedRep(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1371 for (auto it = canonicalReps.rbegin(); it != canonicalReps.rend(); ++it) {
1372 const PixelId repStar = *it;
1373 int numProjectedChildren = 0;
1374 PixelId onlyProjectedChild = InvalidPixel;
1375 for (PixelId childStar = firstRawChild[static_cast<size_t>(repStar)]; childStar != InvalidPixel;
1376 childStar = nextRawSibling[static_cast<size_t>(childStar)]) {
1377 const PixelId childProjection = projectedRep[static_cast<size_t>(childStar)];
1378 if (childProjection != InvalidPixel) {
1379 ++numProjectedChildren;
1380 onlyProjectedChild = childProjection;
1381 }
1382 }
1383
1384 if (directCount[static_cast<size_t>(repStar)] > 0 || numProjectedChildren >= 2) {
1385 projectedRep[static_cast<size_t>(repStar)] = repStar;
1386 } else if (numProjectedChildren == 1) {
1387 projectedRep[static_cast<size_t>(repStar)] = onlyProjectedChild;
1388 }
1389 }
1390
1391 const PixelId projectedRootRep = projectedRep[static_cast<size_t>(rawRoot)];
1392 if (projectedRootRep == InvalidPixel || projectedRep[static_cast<size_t>(projectedRootRep)] != projectedRootRep) {
1393 throw std::runtime_error("Tree-of-shapes projection produced an empty hierarchy.");
1394 }
1395
1396 std::vector<PixelId>().swap(firstRawChild);
1397 std::vector<PixelId>().swap(nextRawSibling);
1398 std::vector<int>().swap(directCount);
1399
1400 std::vector<NodeId> nodeIdByRep(static_cast<size_t>(numPixelsInterp), InvalidNode);
1401 int numNodes = 0;
1402 for (PixelId repStar : canonicalReps) {
1403 if (projectedRep[static_cast<size_t>(repStar)] == repStar) {
1404 nodeIdByRep[static_cast<size_t>(repStar)] = numNodes++;
1405 }
1406 }
1407
1408 TreeOfShapesBuildResult result;
1409 result.parent.assign(static_cast<size_t>(numNodes), InvalidNode);
1410 result.smallestNodeMap.assign(static_cast<size_t>(numPixels), InvalidNode);
1411 result.nodeAltitudes.assign(static_cast<size_t>(numNodes), ToSGrayLevel{});
1412 result.root = nodeIdByRep[static_cast<size_t>(projectedRootRep)];
1413 result.numRows = numRows;
1414 result.numColumns = numColumns;
1415 detail::TopologicalNativeHierarchyRecorder proofRecorder(static_cast<std::size_t>(numNodes), static_cast<std::size_t>(numPixels), result.root);
1416
1417 for (PixelId repStar : canonicalReps) {
1418 const NodeId nodeId = nodeIdByRep[static_cast<size_t>(repStar)];
1419 if (nodeId == InvalidNode) {
1420 continue;
1421 }
1422
1423 if (repStar == projectedRootRep) {
1424 result.parent[static_cast<size_t>(nodeId)] = nodeId;
1425 } else {
1426 PixelId ancestorRep = rawParent[static_cast<size_t>(repStar)];
1427 while (ancestorRep != InvalidPixel && nodeIdByRep[static_cast<size_t>(ancestorRep)] == InvalidNode) {
1428 if (ancestorRep == rawParent[static_cast<size_t>(ancestorRep)]) {
1429 ancestorRep = InvalidPixel;
1430 break;
1431 }
1432 ancestorRep = rawParent[static_cast<size_t>(ancestorRep)];
1433 }
1434 if (ancestorRep == InvalidPixel) {
1435 throw std::runtime_error("Tree-of-shapes retained node has no retained parent.");
1436 }
1437 result.parent[static_cast<size_t>(nodeId)] = nodeIdByRep[static_cast<size_t>(ancestorRep)];
1438 }
1439 proofRecorder.recordSupportedNode(nodeId, result.parent[static_cast<std::size_t>(nodeId)]);
1440
1441 result.nodeAltitudes[static_cast<size_t>(nodeId)] =
1442 ToSExactDoubledAltitudeEncoding::encode(canonical.grayLevel[static_cast<size_t>(repStar)], immersionMode_);
1443 }
1444
1445 for (PixelId elementId = 0; elementId < numPixels; ++elementId) {
1446 const PixelId representativePixel = representativePixelByElement[static_cast<size_t>(elementId)];
1447 const NodeId smallestNodeId = nodeIdByRep[static_cast<size_t>(representativePixel)];
1448 if (smallestNodeId == InvalidNode) {
1449 throw std::runtime_error("Tree-of-shapes proper part belongs to a contracted node.");
1450 }
1451 result.smallestNodeMap[static_cast<size_t>(elementId)] = smallestNodeId;
1452 proofRecorder.recordProperPart(elementId, smallestNodeId);
1453 }
1454 for (NodeId nodeId = 0; nodeId < numNodes; ++nodeId) {
1455 const NodeId parentNodeId = result.parent[static_cast<std::size_t>(nodeId)];
1456 if (nodeId != result.root &&
1457 result.nodeAltitudes[static_cast<std::size_t>(nodeId)] == result.nodeAltitudes[static_cast<std::size_t>(parentNodeId)]) {
1458 throw std::runtime_error("Tree-of-shapes construction produced an equal-altitude parent-child edge.");
1459 }
1460 }
1461 result.topologyProof = std::move(proofRecorder).finish();
1462 return result;
1463 }
1464
1465 public:
1477 validateConventionForImage(imgPtr);
1478 return buildTreeOfShapes(imgPtr);
1479 }
1480};
1481
1482} // namespace mmcfilters
int PixelId
Pixel identifier type used by source and active construction domains.
Definition Common.hpp:26
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
constexpr PixelId InvalidPixel
Sentinel value used to denote an invalid pixel identifier.
Definition Common.hpp:43
std::shared_ptr< ImageUInt8 > ImageUInt8Ptr
Shared pointer to an 8-bit unsigned image.
Definition Image.hpp:276
Immutable scientific metadata attached to a morphological tree.
static std::pair< int, int > to2D(PixelId index, int numColumns) noexcept
Converts a row-major linear index to (row, column).
Definition Image.hpp:312
static PixelId to1D(int row, int column, int numColumns) noexcept
Converts (row, column) to a row-major linear index.
Definition Image.hpp:303
Builds trees of shapes (ToS) using a union-find construction.
TreeOfShapesBuildResult build(const ImageUInt8Ptr &imgPtr) const
Builds the hierarchy with exact doubled source-level altitudes.
const TopographicConvention & convention() const noexcept
Returns the immutable topographic convention selected for this producer.
TreeOfShapesProducer(TopographicConvention convention={})
Creates a tree-of-shapes builder.
~TreeOfShapesProducer()=default
Destroys TreeOfShapesProducer.
Owning result for one computed scalar attribute layout and buffer.
Shape metadata optionally attached to the pixel domain.
Immutable scientific interpretation attached to one morphological tree.
Exact encoding in doubled source-level units.
static value_type encode(ToSGrayLevel constructionLevel, detail::TopographicImmersionMode immersionMode) noexcept
Converts one transient construction level to exact doubled units.
Complete discrete convention retained by a tree-of-shapes result.
TreeOfShapesImmersion immersion
Selected topographic immersion.
PixelId infinityPixel
Declared infinity pixel in the active topographic domain.
TopographicDomainExtension domainExtension
Active-domain extension convention.
Native representation produced by a Tree-of-Shapes producer.
int numRows
Number of rows in the original pixel domain.
detail::NativeTopologyProof topologyProof
Move-only proof that the producer established the topology invariants.
NodeId root
Root node id in the produced internal-node domain.
int numColumns
Number of columns in the original pixel domain.
std::vector< NodeId > smallestNodeMap
Direct owning node for every original-domain proper part.
std::vector< ToSGrayLevel > nodeAltitudes
Encoded altitude for every produced internal node.
std::vector< NodeId > parent
Parent id for every produced internal node.
detail::ValidatedNativeHierarchy< ToSGrayLevel > takeValidatedHierarchy(MorphologicalTreeSemantics semantics) &&
Transfers the producer-owned buffers together with their generic topology proof.