mmcfilters
Public API documentation
Loading...
Searching...
No Matches
TreeOfShapesBuilder.hpp
1#pragma once
2
3#include "../utils/Image.hpp"
4#include "../utils/Common.hpp"
6#include "NativeHierarchyOwned.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 <utility>
17#include <vector>
18
19namespace mmcfilters {
20
21namespace detail {
23enum class TopographicImmersionMode { SelfDualSpan, Min4Max8, Min8Max4 };
24} // namespace detail
25
26inline constexpr int TopographicInterpolationScale = 2;
27inline constexpr int TopographicInterpolationPadding = 1;
28inline constexpr int ToSUInt8Depth = 8;
29inline constexpr int ToSSelfDualDepth = 9;
30
31using ToSGrayLevel = uint16_t;
32using ToSFloodDepth = uint32_t;
33
43
51 static value_type encode(ToSGrayLevel constructionLevel, detail::TopographicImmersionMode immersionMode) noexcept {
52 if (immersionMode == detail::TopographicImmersionMode::SelfDualSpan) {
53 return constructionLevel;
54 }
55 return static_cast<value_type>(TopographicInterpolationScale * static_cast<unsigned int>(constructionLevel));
56 }
57};
58
71 using value_type = std::uint8_t;
72
80 static value_type encode(ToSGrayLevel constructionLevel, detail::TopographicImmersionMode immersionMode) {
81 unsigned int sourceLevel = constructionLevel;
82 if (immersionMode == detail::TopographicImmersionMode::SelfDualSpan) {
83 if (constructionLevel % TopographicInterpolationScale != 0) {
84 throw std::logic_error("Tree-of-shapes 8-bit altitudes cannot represent a self-dual half level.");
85 }
86 sourceLevel = constructionLevel / TopographicInterpolationScale;
87 }
88 if (sourceLevel > std::numeric_limits<value_type>::max()) {
89 throw std::logic_error("Tree-of-shapes construction level exceeds the 8-bit source level set.");
90 }
91 return static_cast<value_type>(sourceLevel);
92 }
93};
94
100template <class Altitude> struct ToSAltitudeEncodingFor;
101
103template <> struct ToSAltitudeEncodingFor<std::uint8_t> {
104 using type = ToSUInt8AltitudeEncoding;
105 static constexpr TopographicAltitudeEncoding declaration = TopographicAltitudeEncoding::UInt8;
106};
107
111 static constexpr TopographicAltitudeEncoding declaration = TopographicAltitudeEncoding::ExactDoubled;
112};
113
114/************************ Tree of Shapes support ************************/
115
117namespace detail {
118
119/*
120 * Adaptive adjacency backend used by the tree-of-shapes construction.
121 *
122 * Diagonal links are activated on demand so that the interpolated grid can
123 * emulate the required 4/8-connectivity behaviour during the union-find pass.
124 * Each pixel may carry four diagonal flags: SW, NE, SE, and NW.
125 */
126enum class DiagonalConnection : uint8_t { None = 0, Sw = 1 << 0, Ne = 1 << 1, Se = 1 << 2, Nw = 1 << 3 };
127
128// Helper operators for diagonal-connection flags.
136inline DiagonalConnection operator|(DiagonalConnection a, DiagonalConnection b) {
137 return static_cast<DiagonalConnection>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
138}
139
147inline DiagonalConnection& operator|=(DiagonalConnection& a, DiagonalConnection b) {
148 a = a | b;
149 return a;
150}
151
159inline bool operator&(DiagonalConnection a, DiagonalConnection b) { return static_cast<uint8_t>(a) & static_cast<uint8_t>(b); }
160
167class AdjacencyUC {
168 private:
170 int numRows;
172 int numColumns;
174 std::vector<uint8_t> dconnFlags; // 4-connect. + diag. connect.
175 // N, W, S, E, SW, NE, SE, NW
177 const std::vector<int> offsetRows = {-1, 0, 1, 0, 1, -1, 1, -1};
179 const std::vector<int> offsetColumns = {0, -1, 0, 1, -1, 1, 1, -1};
181 bool enableDiagonalConnection;
183 const std::vector<DiagonalConnection> requiredDiagonal = {DiagonalConnection::Sw, DiagonalConnection::Ne, DiagonalConnection::Se, DiagonalConnection::Nw};
184
185 public:
193 AdjacencyUC(int rows, int columns, bool enableDiagonalConnection) : numRows(rows), numColumns(columns), enableDiagonalConnection(enableDiagonalConnection) {
194 if (enableDiagonalConnection)
195 dconnFlags.resize(rows * columns, 0);
196 }
197
201 ~AdjacencyUC() {}
202
210 void setDiagonalConnection(int row, int column, DiagonalConnection conn) { dconnFlags[ImageUtils::to1D(row, column, numColumns)] |= static_cast<uint8_t>(conn); }
211
218 void setDiagonalConnection(PixelId pixel, DiagonalConnection conn) { dconnFlags[pixel] |= static_cast<uint8_t>(conn); }
219
228 bool hasConnection(int row, int column, DiagonalConnection conn) const { return dconnFlags[ImageUtils::to1D(row, column, numColumns)] & static_cast<uint8_t>(conn); }
229
237 uint8_t getConnections(int row, int column) const { return dconnFlags[ImageUtils::to1D(row, column, numColumns)]; }
238
242 class NeighborIterator {
243 private:
245 AdjacencyUC& instance;
247 int row;
249 int column;
251 std::size_t id;
252
256 void advanceToValid() {
257 while (id < instance.offsetRows.size()) {
258 int r = row + instance.offsetRows[id];
259 int c = column + instance.offsetColumns[id];
260 if (r >= 0 && c >= 0 && r < instance.numRows && c < instance.numColumns) {
261 if (id < 4 || (instance.enableDiagonalConnection && instance.dconnFlags[ImageUtils::to1D(row, column, instance.numColumns)] &
262 static_cast<uint8_t>(instance.requiredDiagonal[id - 4]))) {
263 return;
264 }
265 }
266 ++id;
267 }
268 }
269
270 public:
279 NeighborIterator(AdjacencyUC& adj, int row, int column, int id) : instance(adj), row(row), column(column), id(id) { advanceToValid(); }
280
286 PixelId operator*() const {
287 int dr = instance.offsetRows[id];
288 int dc = instance.offsetColumns[id];
289 return ImageUtils::to1D(row + dr, column + dc, instance.numColumns);
290 }
291
297 NeighborIterator& operator++() {
298 ++id;
299 advanceToValid();
300 return *this;
301 }
302
309 bool operator==(const NeighborIterator& other) const { return id == other.id; }
310
317 bool operator!=(const NeighborIterator& other) const { return !(*this == other); }
318 };
319
323 class NeighborRange {
324 private:
326 AdjacencyUC& instance;
328 int row;
330 int column;
331
332 public:
340 NeighborRange(AdjacencyUC& instance, int row, int column) : instance(instance), row(row), column(column) {}
341
347 NeighborIterator begin() { return NeighborIterator(instance, row, column, 0); }
353 NeighborIterator end() { return NeighborIterator(instance, row, column, 8); }
354 };
355
362 NeighborRange getNeighborIndices(PixelId pixel) {
363 auto [row, column] = ImageUtils::to2D(pixel, numColumns);
364 return NeighborRange(*this, row, column);
365 }
366
374 NeighborRange getNeighborIndices(int row, int column) { return NeighborRange(*this, row, column); }
375};
376
386class PriorityQueueToS {
387 private:
389 std::vector<std::deque<PixelId>> buckets;
391 int currentPriority;
393 int numElements;
395 int maxPriorityLevels;
396
397 public:
403 PriorityQueueToS(int depthOfImage = ToSUInt8Depth) : currentPriority(0), numElements(0), maxPriorityLevels(1 << depthOfImage) {
404 buckets.resize(maxPriorityLevels);
405 }
406
413 void initial(PixelId element, int priority) {
414 currentPriority = priority;
415 buckets[priority].push_back(element);
416 numElements++;
417 }
423 int getCurrentPriority() { return currentPriority; }
429 bool isEmpty() { return numElements == 0; }
430
438 void priorityPush(PixelId element, int lower, int upper) {
439 int priority;
440 if (lower > currentPriority) {
441 priority = lower;
442 } else if (upper < currentPriority) {
443 priority = upper;
444 } else {
445 priority = currentPriority;
446 }
447 numElements++;
448 buckets[priority].push_back(element);
449 }
450
456 PixelId priorityPop() {
457 if (buckets[currentPriority].empty()) {
458 int nextPriority = -1;
459 for (int distance = 1; distance < maxPriorityLevels; ++distance) {
460 const int lowerPriority = currentPriority - distance;
461 if (lowerPriority >= 0 && !buckets[lowerPriority].empty()) {
462 nextPriority = lowerPriority;
463 break;
464 }
465
466 const int upperPriority = currentPriority + distance;
467 if (upperPriority < maxPriorityLevels && !buckets[upperPriority].empty()) {
468 nextPriority = upperPriority;
469 break;
470 }
471 }
472
473 if (nextPriority == -1) {
474 throw std::runtime_error("PriorityQueueToS is empty.");
475 }
476 currentPriority = nextPriority;
477 }
478
479 const PixelId element = buckets[currentPriority].front();
480 buckets[currentPriority].pop_front();
481
482 numElements--;
483 return element;
484 }
485};
486
487} // namespace detail
488
490
523 private:
525 using AdjacencyUC = detail::AdjacencyUC;
527 using DiagonalConnection = detail::DiagonalConnection;
529 using PriorityQueueToS = detail::PriorityQueueToS;
530
532 struct FloodResult {
534 std::vector<ToSFloodDepth> treeLevel;
536 std::vector<ToSGrayLevel> grayLevel;
538 std::vector<PixelId> order;
540 AdjacencyUC adjacency;
541 };
542
544 struct CanonicalFloodTree {
546 std::vector<ToSFloodDepth> treeLevel;
548 std::vector<ToSGrayLevel> grayLevel;
550 std::vector<PixelId> order;
552 std::vector<PixelId> parent;
553 };
554
556 TopographicConvention convention_;
558 detail::TopographicImmersionMode immersionMode_ = detail::TopographicImmersionMode::SelfDualSpan;
559
566 static detail::TopographicImmersionMode validateConvention(const TopographicConvention& convention) {
567 if (convention.infinityPixel < 0) {
568 throw std::invalid_argument("Tree-of-shapes infinity pixel must be non-negative.");
569 }
570 if (std::holds_alternative<SelfDualSpanImmersion>(convention.immersion)) {
571 // Only the exterior ring carries the boundary reference level, which
572 // is the mean of the two central boundary values on an even boundary
573 // and therefore the single source of half levels. Without that ring
574 // the reference level is cropped away and never read by an interior
575 // cell, so every construction level stays on the source lattice.
576 if (convention.altitudeEncoding == TopographicAltitudeEncoding::UInt8 &&
577 convention.domainExtension == TopographicDomainExtension::ExteriorRing) {
578 throw std::invalid_argument("Self-dual span immersion over an exterior ring cannot publish 8-bit altitudes because its boundary reference "
579 "level may fall on a half level; declare TopographicAltitudeEncoding::ExactDoubled or "
580 "TopographicDomainExtension::None.");
581 }
582 return detail::TopographicImmersionMode::SelfDualSpan;
583 }
584 if (const auto* canonical = std::get_if<CanonicalComplementaryGridImmersion>(&convention.immersion)) {
585 return canonical->pairing == ComplementaryPairing::Min4Max8 ? detail::TopographicImmersionMode::Min4Max8
586 : detail::TopographicImmersionMode::Min8Max4;
587 }
588 const auto& adjacencies = std::get<ComplementaryGridImmersion>(convention.immersion).complementaryAdjacencies;
589 if (adjacencies.minAdjacency.is4connectivity() && adjacencies.maxAdjacency.is8connectivity()) {
590 return detail::TopographicImmersionMode::Min4Max8;
591 }
592 if (adjacencies.minAdjacency.is8connectivity() && adjacencies.maxAdjacency.is4connectivity()) {
593 return detail::TopographicImmersionMode::Min8Max4;
594 }
595 throw std::invalid_argument("Complementary-grid immersion requires minimum/maximum adjacencies in the canonical 4/8 or 8/4 pairing.");
596 }
597
599 void validateConventionForImage(const ImageUInt8Ptr& image) const {
600 if (!image) {
601 throw std::invalid_argument("TreeOfShapesBuilder requires a non-null image.");
602 }
603 if (const auto* immersion = std::get_if<ComplementaryGridImmersion>(&convention_.immersion)) {
604 const auto& adjacencies = immersion->complementaryAdjacencies;
605 const int rows = image->getNumRows();
606 const int columns = image->getNumColumns();
607 if (adjacencies.minAdjacency.getNumRows() != rows || adjacencies.minAdjacency.getNumColumns() != columns ||
608 adjacencies.maxAdjacency.getNumRows() != rows || adjacencies.maxAdjacency.getNumColumns() != columns) {
609 throw std::invalid_argument("Tree-of-shapes complementary adjacencies must match the source image domain.");
610 }
611 }
612 static_cast<void>(validatedInfinityPixel(interpolatedNumRows(image->getNumRows()), interpolatedNumColumns(image->getNumColumns())));
613 }
614
621 void resolveTopographicImmersion(GridDomain2D gridDomain) {
622 const auto* canonical = std::get_if<CanonicalComplementaryGridImmersion>(&convention_.immersion);
623 if (canonical == nullptr) {
624 return;
625 }
626 constexpr double connectivity4Radius = 1.0;
627 constexpr double connectivity8Radius = 1.5;
628 const bool min4Max8 = canonical->pairing == ComplementaryPairing::Min4Max8;
632 }
633
639 inline bool usesConnectivityMap() const noexcept {
640 return immersionMode_ != detail::TopographicImmersionMode::SelfDualSpan;
641 }
642
648 inline bool usesHighDiagonalAtSaddle() const noexcept { return immersionMode_ == detail::TopographicImmersionMode::Min4Max8; }
649
655 inline bool usesExteriorPadding() const noexcept { return convention_.domainExtension == TopographicDomainExtension::ExteriorRing; }
656
662 inline int interpolationOffset() const noexcept { return usesExteriorPadding() ? TopographicInterpolationPadding : 0; }
663
670 inline int checkedInterpolatedExtent(int extent) const {
671 if (extent <= 0) {
672 throw std::invalid_argument("TreeOfShapesBuilder requires positive image dimensions.");
673 }
674 const std::int64_t interpolated = TopographicInterpolationScale * static_cast<std::int64_t>(extent) + (usesExteriorPadding() ? 1 : -1);
675 if (interpolated <= 0 || interpolated > std::numeric_limits<int>::max()) {
676 throw std::overflow_error("Tree-of-shapes interpolated dimension exceeds int range.");
677 }
678 return static_cast<int>(interpolated);
679 }
680
687 inline int interpolatedNumRows(int numRows) const { return checkedInterpolatedExtent(numRows); }
688
695 inline int interpolatedNumColumns(int numColumns) const { return checkedInterpolatedExtent(numColumns); }
696
704 inline int checkedDomainSize(int numRows, int numColumns) const {
705 if (numRows <= 0 || numColumns <= 0 || numRows > std::numeric_limits<int>::max() / numColumns) {
706 throw std::overflow_error("Tree-of-shapes domain size exceeds int range.");
707 }
708 return numRows * numColumns;
709 }
710
717 inline int originalPointRow(int row) const noexcept { return TopographicInterpolationScale * row + interpolationOffset(); }
718
725 inline int originalPointColumn(int column) const noexcept { return TopographicInterpolationScale * column + interpolationOffset(); }
726
733 inline ToSGrayLevel scaledOriginalLevel(uint8_t value) const noexcept {
734 return static_cast<ToSGrayLevel>(TopographicInterpolationScale * static_cast<int>(value));
735 }
736
744 inline PixelId validatedInfinityPixel(int interpNumRows, int interpNumColumns) const {
745 const std::int64_t activeSize = static_cast<std::int64_t>(interpNumRows) * static_cast<std::int64_t>(interpNumColumns);
746 if (convention_.infinityPixel < 0 || static_cast<std::int64_t>(convention_.infinityPixel) >= activeSize) {
747 throw std::invalid_argument("Tree-of-shapes infinity pixel must belong to the active topographic domain.");
748 }
749 return convention_.infinityPixel;
750 }
751
759 inline void setDiagonal0Connection(AdjacencyUC& adj, int row, int column) const {
760 adj.setDiagonalConnection(row, column - 1, DiagonalConnection::Se);
761 adj.setDiagonalConnection(row + 1, column, DiagonalConnection::Nw);
762
763 adj.setDiagonalConnection(row - 1, column - 1, DiagonalConnection::Se);
764 adj.setDiagonalConnection(row, column, DiagonalConnection::Se | DiagonalConnection::Nw);
765 adj.setDiagonalConnection(row + 1, column + 1, DiagonalConnection::Nw);
766
767 adj.setDiagonalConnection(row - 1, column, DiagonalConnection::Se);
768 adj.setDiagonalConnection(row, column + 1, DiagonalConnection::Nw);
769 }
770
778 inline void setDiagonal1Connection(AdjacencyUC& adj, int row, int column) const {
779 adj.setDiagonalConnection(row, column - 1, DiagonalConnection::Ne);
780 adj.setDiagonalConnection(row - 1, column, DiagonalConnection::Sw);
781
782 adj.setDiagonalConnection(row - 1, column + 1, DiagonalConnection::Sw);
783 adj.setDiagonalConnection(row, column, DiagonalConnection::Sw | DiagonalConnection::Ne);
784 adj.setDiagonalConnection(row + 1, column - 1, DiagonalConnection::Ne);
785
786 adj.setDiagonalConnection(row + 1, column, DiagonalConnection::Ne);
787 adj.setDiagonalConnection(row, column + 1, DiagonalConnection::Sw);
788 }
789
790 std::tuple<std::vector<ToSGrayLevel>, std::vector<ToSGrayLevel>, AdjacencyUC>
802 cropExteriorInterpolation(std::vector<ToSGrayLevel> paddedMin, std::vector<ToSGrayLevel> paddedMax, AdjacencyUC paddedAdjacency, int paddedRows,
803 int paddedColumns, bool adaptiveDiagonal) const {
804 if (paddedRows < 3 || paddedColumns < 3) {
805 throw std::logic_error("Tree-of-shapes padded immersion cannot be cropped.");
806 }
807 const int rows = paddedRows - 2;
808 const int columns = paddedColumns - 2;
809 const int size = checkedDomainSize(rows, columns);
810 std::vector<ToSGrayLevel> croppedMin(static_cast<std::size_t>(size));
811 std::vector<ToSGrayLevel> croppedMax(static_cast<std::size_t>(size));
812 AdjacencyUC croppedAdjacency(rows, columns, adaptiveDiagonal);
813
814 for (int row = 0; row < rows; ++row) {
815 for (int column = 0; column < columns; ++column) {
816 const PixelId source = ImageUtils::to1D(row + 1, column + 1, paddedColumns);
817 const PixelId target = ImageUtils::to1D(row, column, columns);
818 croppedMin[static_cast<std::size_t>(target)] = paddedMin[static_cast<std::size_t>(source)];
819 croppedMax[static_cast<std::size_t>(target)] = paddedMax[static_cast<std::size_t>(source)];
820 if (adaptiveDiagonal) {
821 const std::uint8_t connections = paddedAdjacency.getConnections(row + 1, column + 1);
822 if (connections != 0) {
823 croppedAdjacency.setDiagonalConnection(target, static_cast<DiagonalConnection>(connections));
824 }
825 }
826 }
827 }
828 return {std::move(croppedMin), std::move(croppedMax), std::move(croppedAdjacency)};
829 }
830
831 public:
838 : convention_(std::move(convention)), immersionMode_(validateConvention(convention_)) {}
839
845 [[nodiscard]] const TopographicConvention& convention() const noexcept { return convention_; }
846
851
853
867 std::tuple<std::vector<ToSGrayLevel>, std::vector<ToSGrayLevel>, AdjacencyUC> interpolateImage(const ImageUInt8Ptr& imgPtr) const {
868 if (!imgPtr) {
869 throw std::invalid_argument("TreeOfShapesBuilder requires a non-null image.");
870 }
871 if (!usesExteriorPadding()) {
873 paddedConvention.domainExtension = TopographicDomainExtension::ExteriorRing;
874 paddedConvention.infinityPixel = 0;
875 // The padded producer only supplies interpolation buffers, which are
876 // altitude-encoding independent. Declaring the doubled encoding keeps
877 // it constructible for a convention whose own encoding is 8-bit.
878 paddedConvention.altitudeEncoding = TopographicAltitudeEncoding::ExactDoubled;
880 auto [paddedMin, paddedMax, paddedAdjacency] = paddedProducer.interpolateImage(imgPtr);
881 const int paddedRows = paddedProducer.interpolatedNumRows(imgPtr->getNumRows());
882 const int paddedColumns = paddedProducer.interpolatedNumColumns(imgPtr->getNumColumns());
883 return cropExteriorInterpolation(std::move(paddedMin), std::move(paddedMax), std::move(paddedAdjacency), paddedRows, paddedColumns, false);
884 }
885 auto img = imgPtr->rawData();
886 int numRows = imgPtr->getNumRows();
887 int numColumns = imgPtr->getNumColumns();
888 if (numRows <= 0 || numColumns <= 0) {
889 throw std::invalid_argument("TreeOfShapesBuilder requires a non-empty image.");
890 }
891 if (numRows == 1 && numColumns == 1) {
892 const int interpNumRows = interpolatedNumRows(numRows);
893 const int interpNumColumns = interpolatedNumColumns(numColumns);
894 const int interpSize = checkedDomainSize(interpNumRows, interpNumColumns);
895 std::vector<ToSGrayLevel> interpolationMin(static_cast<std::size_t>(interpSize), scaledOriginalLevel(img[0]));
896 std::vector<ToSGrayLevel> interpolationMax(static_cast<std::size_t>(interpSize), scaledOriginalLevel(img[0]));
897 AdjacencyUC adj(interpNumRows, interpNumColumns, false);
898 return std::make_tuple(std::move(interpolationMin), std::move(interpolationMax), std::move(adj));
899 }
900
901 constexpr int adjCircleColumn[] = {-1, +1, -1, +1};
902 constexpr int adjCircleRow[] = {-1, -1, +1, +1};
903
904 constexpr int adjRetHorColumn[] = {0, 0};
905 constexpr int adjRetHorRow[] = {-1, +1};
906
907 constexpr int adjRetVerColumn[] = {+1, -1};
908 constexpr int adjRetVerRow[] = {0, 0};
909
910 int interpNumColumns = interpolatedNumColumns(numColumns);
911 int interpNumRows = interpolatedNumRows(numRows);
912 int size = checkedDomainSize(interpNumRows, interpNumColumns);
913
914 // Allocate interpolation result buffers for minimum and maximum levels.
915 std::vector<ToSGrayLevel> interpolationMin(size);
916 std::vector<ToSGrayLevel> interpolationMax(size);
917
918 // Collect each boundary pixel exactly once. The closed-form perimeter
919 // size 2 * (rows + columns) - 4 is valid only when both dimensions are at
920 // least two and previously left zero-filled entries for thin images.
921 std::array<int, 256> boundaryHistogram{};
922 int numBoundary = 0;
923
924 PixelId pT;
925
926 for (PixelId pixel = 0; pixel < numColumns * numRows; ++pixel) {
927 auto [row, column] = ImageUtils::to2D(pixel, numColumns);
928
929 // Check whether the pixel lies on the image border.
930 if (row == 0 || row == numRows - 1 || column == 0 || column == numColumns - 1) {
931 ++boundaryHistogram[static_cast<std::size_t>(img[pixel])];
932 ++numBoundary;
933 }
934
935 // Compute the interpolated-image index.
936 pT = ImageUtils::to1D(originalPointRow(row), originalPointColumn(column), interpNumColumns);
937
938 // Assign interpolation values.
939 interpolationMin[pT] = interpolationMax[pT] = scaledOriginalLevel(img[pixel]);
940 }
941
942 auto boundaryValueAtRank = [&](int rank) {
943 int cumulative = 0;
944 for (int value = 0; value < 256; ++value) {
945 cumulative += boundaryHistogram[static_cast<std::size_t>(value)];
946 if (rank < cumulative) {
947 return value;
948 }
949 }
950 throw std::runtime_error("Tree-of-shapes boundary histogram is inconsistent.");
951 };
952 int median;
953 if (numBoundary % 2 == 0) {
955 } else {
956 median = TopographicInterpolationScale * boundaryValueAtRank(numBoundary / 2);
957 }
958 // std::cout << "Interpolation (Median): " << median << std::endl;
959
960 PixelId qT;
961 int qColumn, qRow, min, max;
962 const int* adjColumn = nullptr;
963 const int* adjRow = nullptr;
964 int adjSize;
965 AdjacencyUC adj(interpNumRows, interpNumColumns, false);
966
967 for (int row = 0; row < interpNumRows; row++) {
968 for (int column = 0; column < interpNumColumns; column++) {
969 if (column % 2 == 1 && row % 2 == 1)
970 continue;
971 pT = ImageUtils::to1D(row, column, interpNumColumns);
972 if (column == 0 || column == interpNumColumns - 1 || row == 0 || row == interpNumRows - 1) {
973 max = median;
974 min = median;
975 } else {
976 if (column % 2 == 0 && row % 2 == 0) {
979 adjSize = 4;
980 } else if (column % 2 == 0 && row % 2 == 1) {
983 adjSize = 2;
984 } else if (column % 2 == 1 && row % 2 == 0) {
987 adjSize = 2;
988 }
989
990 min = std::numeric_limits<int>::max();
991 max = std::numeric_limits<int>::min();
992 for (int i = 0; i < adjSize; i++) {
993 qRow = row + adjRow[i];
994 qColumn = column + adjColumn[i];
995
996 if (qRow >= 0 && qColumn >= 0 && qRow < interpNumRows && qColumn < interpNumColumns) {
998
999 if (interpolationMax[qT] > max) {
1001 }
1002 if (interpolationMin[qT] < min) {
1004 }
1005 } else {
1006 if (median > max) {
1007 max = median;
1008 }
1009 if (median < min) {
1010 min = median;
1011 }
1012 }
1013 }
1014 }
1015 interpolationMin[pT] = static_cast<ToSGrayLevel>(min);
1016 interpolationMax[pT] = static_cast<ToSGrayLevel>(max);
1017 }
1018 }
1019 return std::make_tuple(std::move(interpolationMin), std::move(interpolationMax), std::move(adj));
1020 }
1021
1034 std::tuple<std::vector<ToSGrayLevel>, std::vector<ToSGrayLevel>, AdjacencyUC> interpolateImage4c8c(const ImageUInt8Ptr& imgPtr) const {
1035 if (!imgPtr) {
1036 throw std::invalid_argument("TreeOfShapesBuilder requires a non-null image.");
1037 }
1038 if (!usesExteriorPadding()) {
1039 TopographicConvention paddedConvention = convention_;
1040 paddedConvention.domainExtension = TopographicDomainExtension::ExteriorRing;
1041 paddedConvention.infinityPixel = 0;
1042 // The padded producer only supplies interpolation buffers, which are
1043 // altitude-encoding independent. Declaring the doubled encoding keeps
1044 // it constructible for a convention whose own encoding is 8-bit.
1045 paddedConvention.altitudeEncoding = TopographicAltitudeEncoding::ExactDoubled;
1046 TreeOfShapesBuilder paddedProducer(std::move(paddedConvention));
1047 auto [paddedMin, paddedMax, paddedAdjacency] = paddedProducer.interpolateImage4c8c(imgPtr);
1048 const int paddedRows = paddedProducer.interpolatedNumRows(imgPtr->getNumRows());
1049 const int paddedColumns = paddedProducer.interpolatedNumColumns(imgPtr->getNumColumns());
1050 return cropExteriorInterpolation(std::move(paddedMin), std::move(paddedMax), std::move(paddedAdjacency), paddedRows, paddedColumns, true);
1051 }
1052 auto img = imgPtr->rawData();
1053 int numRows = imgPtr->getNumRows();
1054 int numColumns = imgPtr->getNumColumns();
1055 if (numRows <= 0 || numColumns <= 0) {
1056 throw std::invalid_argument("TreeOfShapesBuilder requires a non-empty image.");
1057 }
1058
1059 int interpNumColumns = interpolatedNumColumns(numColumns);
1060 int interpNumRows = interpolatedNumRows(numRows);
1061 int size = checkedDomainSize(interpNumRows, interpNumColumns);
1062 AdjacencyUC adj(interpNumRows, interpNumColumns, true);
1063
1064 // Allocate interpolation result buffers for minimum and maximum levels.
1065 std::vector<ToSGrayLevel> interpolationMin(size);
1066 std::vector<ToSGrayLevel> interpolationMax(size);
1067
1068 PixelId pT;
1069 // Compute interval from 2-faces.
1070 for (PixelId pixel = 0; pixel < numColumns * numRows; ++pixel) {
1071 auto [row, column] = ImageUtils::to2D(pixel, numColumns);
1072
1073 // Compute the interpolated-image index.
1074 pT = ImageUtils::to1D(originalPointRow(row), originalPointColumn(column), interpNumColumns);
1075
1076 // Assign interpolation values.
1077 interpolationMin[pT] = interpolationMax[pT] = img[pixel];
1078 }
1079
1080 auto getValue = [&](int row, int column) -> int {
1081 int origRow = (row - 1) / 2;
1082 int originalColumn = (column - 1) / 2;
1083 return img[ImageUtils::to1D(origRow, originalColumn, numColumns)];
1084 };
1085
1086 // Borders.
1087 for (int row = 0; row < interpNumRows; row++) {
1088 int column;
1089 if (row % 2 == 1) { // horizontal e vertical
1090 column = 0;
1091 int v1 = getValue(row, column + 1);
1092 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1093 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1094
1095 column = interpNumColumns - 1;
1096 v1 = getValue(row, column - 1);
1097 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1098 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1099 } else { // circulos
1100 if (row == 0) {
1101 column = 0;
1102 int v1 = getValue(row + 1, column + 1);
1103 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1104 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1105
1106 column = interpNumColumns - 1;
1107 v1 = getValue(row + 1, column - 1);
1108 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1109 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1110
1111 } else if (row == interpNumRows - 1) {
1112 column = 0;
1113 int v1 = getValue(row - 1, 1);
1114 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1115 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1116
1117 column = interpNumColumns - 1;
1118 v1 = getValue(row - 1, column - 1);
1119 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1120 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1121 } else {
1122 column = 0;
1123 int v1 = getValue(row - 1, column + 1);
1124 int v2 = getValue(row + 1, column + 1);
1125 interpolationMin[ImageUtils::to1D(row, 0, interpNumColumns)] = std::min(v1, v2);
1126 interpolationMax[ImageUtils::to1D(row, 0, interpNumColumns)] = std::max(v1, v2);
1127
1128 column = interpNumColumns - 1;
1129 v1 = getValue(row - 1, column - 1);
1130 v2 = getValue(row + 1, column - 1);
1131 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = std::min(v1, v2);
1132 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = std::max(v1, v2);
1133 }
1134 }
1135 }
1136
1137 for (int column = 1; column < interpNumColumns - 1; column++) {
1138 int row;
1139 if (column % 2 == 1) { // horizontal e vertical
1140 row = 0;
1141 int v1 = getValue(row + 1, column);
1142 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1143 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1144
1145 row = interpNumRows - 1;
1146 v1 = getValue(row - 1, column);
1147 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1148 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1149 } else { // circulos
1150 row = 0;
1151 int v1 = getValue(row + 1, column - 1);
1152 int v2 = getValue(row + 1, column + 1);
1153 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = std::min(v1, v2);
1154 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = std::max(v1, v2);
1155
1156 row = interpNumRows - 1;
1157 v1 = getValue(row - 1, column - 1);
1158 v2 = getValue(row - 1, column + 1);
1159 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = std::min(v1, v2);
1160 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = std::max(v1, v2);
1161 }
1162 }
1163
1164 // Compute interval from 1-faces
1165 for (int row = 1; row < interpNumRows - 1; row++) {
1166 for (int column = 1; column < interpNumColumns - 1; column++) {
1167 if (row % 2 == 1 && column % 2 == 1)
1168 continue; // Already defined.
1169
1170 pT = ImageUtils::to1D(row, column, interpNumColumns);
1171 if (column % 2 == 0 && row % 2 == 1) {
1172 int v1 = getValue(row, column + 1);
1173 int v2 = getValue(row, column - 1);
1174 interpolationMin[pT] = std::min(v1, v2);
1175 interpolationMax[pT] = std::max(v1, v2);
1176 } else if (column % 2 == 1 && row % 2 == 0) {
1177 int v1 = getValue(row + 1, column);
1178 int v2 = getValue(row - 1, column);
1179 interpolationMin[pT] = std::min(v1, v2);
1180 interpolationMax[pT] = std::max(v1, v2);
1181 }
1182 }
1183 }
1184 // Compute interval from 0-faces
1185 for (int row = 1; row < interpNumRows - 1; row++) {
1186 for (int column = 1; column < interpNumColumns - 1; column++) {
1187 if (row % 2 == 1 && column % 2 == 1)
1188 continue; // Already defined.
1189 pT = ImageUtils::to1D(row, column, interpNumColumns);
1190 if (row % 2 == 0 && column % 2 == 0) {
1191 // | v0 | v1 |
1192 // | v2 | v3 |
1193 int v0 = getValue(row - 1, column - 1);
1194 int v1 = getValue(row + 1, column - 1);
1195 int v2 = getValue(row - 1, column + 1);
1196 int v3 = getValue(row + 1, column + 1);
1197
1198 const int min_v0v3 = std::min(v0, v3);
1199 const int max_v0v3 = std::max(v0, v3);
1200 const int min_v1v2 = std::min(v1, v2);
1201 const int max_v1v2 = std::max(v1, v2);
1202 const bool diagonal0IsHigh = min_v0v3 > max_v1v2;
1203 const bool diagonal1IsHigh = min_v1v2 > max_v0v3;
1204
1205 if (diagonal0IsHigh || diagonal1IsHigh) {
1206 const bool chooseDiagonal0 = usesHighDiagonalAtSaddle() ? diagonal0IsHigh : !diagonal0IsHigh;
1207 if (chooseDiagonal0) {
1208 setDiagonal0Connection(adj, row, column);
1209 interpolationMin[pT] = min_v0v3;
1210 interpolationMax[pT] = max_v0v3;
1211 } else {
1212 setDiagonal1Connection(adj, row, column);
1213 interpolationMin[pT] = min_v1v2;
1214 interpolationMax[pT] = max_v1v2;
1215 }
1216 } else {
1217 // Non-critical configuration.
1218 interpolationMin[pT] = std::min({v0, v1, v2, v3});
1219 interpolationMax[pT] = std::max({v0, v1, v2, v3});
1220 }
1221 }
1222 }
1223 }
1224 return std::make_tuple(std::move(interpolationMin), std::move(interpolationMax), std::move(adj));
1225 }
1226
1227 private:
1241 FloodResult floodImage(const ImageUInt8Ptr& imgPtr) const {
1242 if (!imgPtr) {
1243 throw std::invalid_argument("TreeOfShapesBuilder requires a non-null image.");
1244 }
1245
1246 int numRows = imgPtr->getNumRows();
1247 int numColumns = imgPtr->getNumColumns();
1248
1249 int interpNumColumns = interpolatedNumColumns(numColumns);
1250 int interpNumRows = interpolatedNumRows(numRows);
1251 int size = checkedDomainSize(interpNumRows, interpNumColumns);
1252 const bool connectivityMap = usesConnectivityMap();
1253 auto [interpolationMin, interpolationMax, adj] = connectivityMap ? interpolateImage4c8c(imgPtr) : interpolateImage(imgPtr);
1254
1255 std::vector<uint8_t> dejavu(size, 0);
1256 std::vector<PixelId> imgR(size); // Ordered pixels.
1257 std::vector<ToSFloodDepth> imgU(size); // Flood values or order-map levels.
1258 std::vector<ToSGrayLevel> grayLevel(size); // Actual propagation levels.
1259
1260 PriorityQueueToS queue(connectivityMap ? ToSUInt8Depth : ToSSelfDualDepth); // Priority queue.
1261 const PixelId infinityPixel = validatedInfinityPixel(interpNumRows, interpNumColumns);
1262 int priorityQueueOld = interpolationMin[infinityPixel];
1263 queue.initial(infinityPixel, priorityQueueOld);
1264 dejavu[infinityPixel] = true;
1265
1266 int order = 0;
1267 ToSFloodDepth depth = 0;
1268 while (!queue.isEmpty()) {
1269 const PixelId pixel = queue.priorityPop(); // Current or nearest queued level.
1270 int priorityQueue = queue.getCurrentPriority(); // Current priority.
1271 if (connectivityMap) {
1272 if (priorityQueue != priorityQueueOld)
1273 depth++;
1274 imgU[pixel] = depth;
1275 } else {
1276 imgU[pixel] = static_cast<ToSFloodDepth>(priorityQueue);
1277 }
1278 grayLevel[pixel] = static_cast<ToSGrayLevel>(priorityQueue);
1279
1280 // Store h in the correct output order.
1281 imgR[order++] = pixel;
1282
1283 // Adjacencies.
1284 for (PixelId neighbor : adj.getNeighborIndices(pixel)) {
1285 if (!dejavu[neighbor]) {
1286 queue.priorityPush(neighbor, interpolationMin[neighbor], interpolationMax[neighbor]);
1287 dejavu[neighbor] = true; // Mark as processed.
1288 }
1289 }
1290 priorityQueueOld = priorityQueue;
1291 }
1292 if (order != size) {
1293 throw std::runtime_error("Tree-of-shapes propagation did not visit the complete interpolated domain.");
1294 }
1295 return FloodResult{std::move(imgU), std::move(grayLevel), std::move(imgR), std::move(adj)};
1296 }
1297
1309 CanonicalFloodTree buildCanonicalFloodTree(const ImageUInt8Ptr& imgPtr) const {
1310 FloodResult flood = floodImage(imgPtr);
1311 const int numPixelsInterp = static_cast<int>(flood.treeLevel.size());
1312
1313 std::vector<PixelId> zPar(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1314 std::vector<PixelId> parentInterpolate(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1315 auto findRoot = [&](PixelId pStar) {
1316 while (zPar[pStar] != pStar) {
1317 zPar[pStar] = zPar[zPar[pStar]];
1318 pStar = zPar[pStar];
1319 }
1320 return pStar;
1321 };
1322
1323 for (int i = numPixelsInterp - 1; i >= 0; --i) {
1324 const PixelId pStar = flood.order[static_cast<size_t>(i)];
1325 parentInterpolate[pStar] = pStar;
1326 zPar[pStar] = pStar;
1327 for (PixelId qStar : flood.adjacency.getNeighborIndices(pStar)) {
1328 if (zPar[qStar] != InvalidPixel) {
1329 const PixelId rStar = findRoot(qStar);
1330 if (pStar != rStar) {
1331 parentInterpolate[rStar] = pStar;
1332 zPar[rStar] = pStar;
1333 }
1334 }
1335 }
1336 }
1337
1338 auto sameLevel = [&](PixelId aStar, PixelId bStar) { return flood.treeLevel[aStar] == flood.treeLevel[bStar]; };
1339 for (PixelId pStar : flood.order) {
1340 const PixelId qStar = parentInterpolate[pStar];
1341 if (sameLevel(parentInterpolate[qStar], qStar)) {
1342 parentInterpolate[pStar] = parentInterpolate[qStar];
1343 }
1344 }
1345
1346 return CanonicalFloodTree{std::move(flood.treeLevel), std::move(flood.grayLevel), std::move(flood.order), std::move(parentInterpolate)};
1347 }
1348
1349 public:
1356 std::tuple<std::vector<ToSFloodDepth>, std::vector<PixelId>, AdjacencyUC> sort(const ImageUInt8Ptr& imgPtr) const {
1357 FloodResult flood = floodImage(imgPtr);
1358 return std::make_tuple(std::move(flood.treeLevel), std::move(flood.order), std::move(flood.adjacency));
1359 }
1360
1361 // Tests whether an interpolated pixel corresponds to an original pixel.
1369 inline bool isOriginal1D(PixelId pixel, int interpNumColumns) const {
1370 const int row = pixel / interpNumColumns;
1371 const int column = pixel - row * interpNumColumns;
1372 const int offset = interpolationOffset();
1373 return (row & 1) == offset && (column & 1) == offset;
1374 }
1375
1376 // Maps an interpolated pixel to the original image domain.
1385 inline PixelId toOriginal1D(PixelId pStar, int interNumColumns, int numColumns) const {
1386 int r = pStar / interNumColumns;
1387 int c = pStar - r * interNumColumns; // Avoid the modulo operator.
1388 const int offset = interpolationOffset();
1389 return ((r - offset) >> 1) * numColumns + ((c - offset) >> 1);
1390 }
1392
1393 private:
1414 template <class Altitude>
1415 [[nodiscard]] NativeHierarchyOwned<Altitude> computeIntrinsicAttributes(CanonicalFloodTree canonical, GridDomain2D gridDomain) {
1416 const int numRows = gridDomain.rows;
1417 const int numColumns = gridDomain.columns;
1418 const int numPixels = checkedDomainSize(numRows, numColumns);
1419 const int interpNumColumns = interpolatedNumColumns(numColumns);
1420
1421 const int numPixelsInterp = static_cast<int>(canonical.parent.size());
1422 auto sameLevel = [&](PixelId aStar, PixelId bStar) {
1423 return canonical.treeLevel[static_cast<size_t>(aStar)] == canonical.treeLevel[static_cast<size_t>(bStar)];
1424 };
1425 auto repOf = [&](PixelId pStar) {
1426 const PixelId parentStar = canonical.parent[static_cast<size_t>(pStar)];
1427 return (parentStar == pStar || sameLevel(parentStar, pStar)) ? parentStar : pStar;
1428 };
1429
1430 std::vector<PixelId> canonicalReps;
1431 canonicalReps.reserve(static_cast<size_t>(numPixels));
1432 for (PixelId pStar : canonical.order) {
1433 const PixelId parentStar = canonical.parent[static_cast<size_t>(pStar)];
1434 if (parentStar == pStar || !sameLevel(parentStar, pStar)) {
1435 canonicalReps.push_back(pStar);
1436 }
1437 }
1438 if (canonicalReps.empty()) {
1439 throw std::runtime_error("Tree-of-shapes construction produced no canonical interpolated node.");
1440 }
1441
1442 const PixelId rawRoot = canonicalReps.front();
1443 std::vector<PixelId> rawParent(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1444 std::vector<PixelId> firstRawChild(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1445 std::vector<PixelId> nextRawSibling(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1446 for (PixelId repStar : canonicalReps) {
1447 if (repStar == rawRoot) {
1448 rawParent[static_cast<size_t>(repStar)] = repStar;
1449 continue;
1450 }
1451 const PixelId parentRep = repOf(canonical.parent[static_cast<size_t>(repStar)]);
1452 if (parentRep < 0 || parentRep >= numPixelsInterp || rawParent[static_cast<size_t>(parentRep)] == InvalidPixel) {
1453 throw std::runtime_error("Tree-of-shapes canonical node has no canonical parent.");
1454 }
1455 rawParent[static_cast<size_t>(repStar)] = parentRep;
1456 nextRawSibling[static_cast<size_t>(repStar)] = firstRawChild[static_cast<size_t>(parentRep)];
1457 firstRawChild[static_cast<size_t>(parentRep)] = repStar;
1458 }
1459
1460 std::vector<int> directCount(static_cast<size_t>(numPixelsInterp), 0);
1461 std::vector<PixelId> representativePixelByElement(static_cast<size_t>(numPixels), InvalidPixel);
1462 for (PixelId pStar = 0; pStar < numPixelsInterp; ++pStar) {
1463 if (!isOriginal1D(pStar, interpNumColumns)) {
1464 continue;
1465 }
1466 const PixelId elementId = toOriginal1D(pStar, interpNumColumns, numColumns);
1467 if (elementId < 0 || elementId >= numPixels) {
1468 throw std::runtime_error("Tree-of-shapes projection produced an element outside the source domain.");
1469 }
1470 const PixelId representativePixel = repOf(pStar);
1471 representativePixelByElement[static_cast<size_t>(elementId)] = representativePixel;
1472 ++directCount[static_cast<size_t>(representativePixel)];
1473 }
1474 if (std::find(representativePixelByElement.begin(), representativePixelByElement.end(), InvalidPixel) != representativePixelByElement.end()) {
1475 throw std::runtime_error("Tree-of-shapes projection did not assign every source-domain element.");
1476 }
1477
1478 // The canonical depth, parent, and full interpolated order are no
1479 // longer needed after the raw hierarchy and source-domain smallest nodes have
1480 // been extracted. Release them before allocating the projection buffer.
1481 std::vector<ToSFloodDepth>().swap(canonical.treeLevel);
1482 std::vector<PixelId>().swap(canonical.parent);
1483 std::vector<PixelId>().swap(canonical.order);
1484
1485 // Bottom-up support projection. projectedRep[r] is the canonical
1486 // representative of the distinct projected support rooted at r.
1487 std::vector<PixelId> projectedRep(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1488 for (auto it = canonicalReps.rbegin(); it != canonicalReps.rend(); ++it) {
1489 const PixelId repStar = *it;
1490 int numProjectedChildren = 0;
1491 PixelId onlyProjectedChild = InvalidPixel;
1492 for (PixelId childStar = firstRawChild[static_cast<size_t>(repStar)]; childStar != InvalidPixel;
1493 childStar = nextRawSibling[static_cast<size_t>(childStar)]) {
1494 const PixelId childProjection = projectedRep[static_cast<size_t>(childStar)];
1495 if (childProjection != InvalidPixel) {
1496 ++numProjectedChildren;
1497 onlyProjectedChild = childProjection;
1498 }
1499 }
1500
1501 if (directCount[static_cast<size_t>(repStar)] > 0 || numProjectedChildren >= 2) {
1502 projectedRep[static_cast<size_t>(repStar)] = repStar;
1503 } else if (numProjectedChildren == 1) {
1504 projectedRep[static_cast<size_t>(repStar)] = onlyProjectedChild;
1505 }
1506 }
1507
1508 const PixelId projectedRootRep = projectedRep[static_cast<size_t>(rawRoot)];
1509 if (projectedRootRep == InvalidPixel || projectedRep[static_cast<size_t>(projectedRootRep)] != projectedRootRep) {
1510 throw std::runtime_error("Tree-of-shapes projection produced an empty hierarchy.");
1511 }
1512
1513 std::vector<PixelId>().swap(firstRawChild);
1514 std::vector<PixelId>().swap(nextRawSibling);
1515 std::vector<int>().swap(directCount);
1516
1517 std::vector<NodeId> nodeIdByRep(static_cast<size_t>(numPixelsInterp), InvalidNode);
1518 int numNodes = 0;
1519 for (PixelId repStar : canonicalReps) {
1520 if (projectedRep[static_cast<size_t>(repStar)] == repStar) {
1521 nodeIdByRep[static_cast<size_t>(repStar)] = numNodes++;
1522 }
1523 }
1524
1525 std::vector<NodeId> nodeParent(static_cast<size_t>(numNodes), InvalidNode);
1526 std::vector<NodeId> smallestNode(static_cast<size_t>(numPixels), InvalidNode);
1527 std::vector<Altitude> nodeValuation(static_cast<size_t>(numNodes));
1528 const NodeId root = nodeIdByRep[static_cast<size_t>(projectedRootRep)];
1529
1530 for (PixelId repStar : canonicalReps) {
1531 const NodeId nodeId = nodeIdByRep[static_cast<size_t>(repStar)];
1532 if (nodeId == InvalidNode) {
1533 continue;
1534 }
1535
1536 if (repStar == projectedRootRep) {
1537 nodeParent[static_cast<size_t>(nodeId)] = nodeId;
1538 } else {
1539 PixelId ancestorRep = rawParent[static_cast<size_t>(repStar)];
1540 while (ancestorRep != InvalidPixel && nodeIdByRep[static_cast<size_t>(ancestorRep)] == InvalidNode) {
1541 if (ancestorRep == rawParent[static_cast<size_t>(ancestorRep)]) {
1542 ancestorRep = InvalidPixel;
1543 break;
1544 }
1545 ancestorRep = rawParent[static_cast<size_t>(ancestorRep)];
1546 }
1547 if (ancestorRep == InvalidPixel) {
1548 throw std::runtime_error("Tree-of-shapes retained node has no retained parent.");
1549 }
1550 nodeParent[static_cast<size_t>(nodeId)] = nodeIdByRep[static_cast<size_t>(ancestorRep)];
1551 }
1552
1553 nodeValuation[static_cast<size_t>(nodeId)] =
1554 ToSAltitudeEncodingFor<Altitude>::type::encode(canonical.grayLevel[static_cast<size_t>(repStar)], immersionMode_);
1555 }
1556
1557 for (PixelId elementId = 0; elementId < numPixels; ++elementId) {
1558 const PixelId representativePixel = representativePixelByElement[static_cast<size_t>(elementId)];
1559 const NodeId smallestNodeId = nodeIdByRep[static_cast<size_t>(representativePixel)];
1560 if (smallestNodeId == InvalidNode) {
1561 throw std::runtime_error("Tree-of-shapes proper part belongs to a contracted node.");
1562 }
1563 smallestNode[static_cast<size_t>(elementId)] = smallestNodeId;
1564 }
1565 for (NodeId nodeId = 0; nodeId < numNodes; ++nodeId) {
1566 const NodeId parentNodeId = nodeParent[static_cast<std::size_t>(nodeId)];
1567 if (nodeId != root && nodeValuation[static_cast<std::size_t>(nodeId)] == nodeValuation[static_cast<std::size_t>(parentNodeId)]) {
1568 throw std::runtime_error("Tree-of-shapes construction produced an equal-altitude parent-child edge.");
1569 }
1570 }
1571 return makeNativeHierarchyOwned<Altitude>(
1572 std::move(nodeParent), std::move(smallestNode), std::move(nodeValuation), root, gridDomain,
1573 makeMorphologicalTreeSemantics(MorphologicalTreeKind::TreeOfShapes, std::move(convention_)));
1574 }
1575
1576 public:
1596 template <class Altitude = std::uint8_t> [[nodiscard]] NativeHierarchyOwned<Altitude> build(const ImageUInt8Ptr& imgPtr) && {
1598 throw std::invalid_argument("Tree-of-shapes altitude type does not match the altitude encoding declared by the topographic convention.");
1599 }
1600 validateConventionForImage(imgPtr);
1601 const GridDomain2D gridDomain{imgPtr->getNumRows(), imgPtr->getNumColumns()};
1602 resolveTopographicImmersion(gridDomain);
1605 }
1606};
1607
1608} // 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:260
Immutable scientific metadata attached to a morphological tree.
MorphologicalTreeSemantics makeMorphologicalTreeSemantics(MorphologicalTreeKind kind, MorphologicalTreeConstructionContext constructionContext=NoConstructionContext{})
Constructs semantics using the conventional altitude order of kind.
TopographicAltitudeEncoding
Scale in which tree-of-shapes node altitudes are published.
static std::pair< int, int > to2D(PixelId index, int numColumns) noexcept
Converts a row-major linear index to (row, column).
Definition Image.hpp:296
static PixelId to1D(int row, int column, int numColumns) noexcept
Converts (row, column) to a row-major linear index.
Definition Image.hpp:287
Immutable regular-grid 2D adjacency with allocation-free traversal.
Builds trees of shapes (ToS) using a union-find construction.
NativeHierarchyOwned< Altitude > build(const ImageUInt8Ptr &imgPtr) &&
Builds the native hierarchy in the declared altitude scale, consuming the producer.
TreeOfShapesBuilder(TopographicConvention convention={})
Creates a tree-of-shapes builder.
const TopographicConvention & convention() const noexcept
Returns the immutable topographic convention selected for this producer.
~TreeOfShapesBuilder()=default
Destroys TreeOfShapesBuilder.
Ordered pair of complementary minimum and maximum adjacencies.
Selects the optimized complementary-grid immersion.
Owning result for one computed scalar attribute layout and buffer.
Shape metadata optionally attached to the pixel domain.
Selects the altitude encoding policy declared by a topographic convention.
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.
Exact encoding in 8-bit source-level units.
static value_type encode(ToSGrayLevel constructionLevel, detail::TopographicImmersionMode immersionMode)
Converts one transient construction level to 8-bit source units.
std::uint8_t value_type
Scalar type storing source gray levels.
Complete discrete convention retained by a tree-of-shapes result.
TreeOfShapesImmersion immersion
Selected topographic immersion.
TopographicAltitudeEncoding altitudeEncoding
Published node-altitude scale.
PixelId infinityPixel
Declared infinity pixel in the active topographic domain.
TopographicDomainExtension domainExtension
Active-domain extension convention.