mmcfilters
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
70 using value_type = std::uint8_t;
71
79 static value_type encode(ToSGrayLevel constructionLevel, detail::TopographicImmersionMode immersionMode) {
80 unsigned int sourceLevel = constructionLevel;
81 if (immersionMode == detail::TopographicImmersionMode::SelfDualSpan) {
82 if (constructionLevel % TopographicInterpolationScale != 0) {
83 throw std::logic_error("Tree-of-shapes 8-bit altitudes cannot represent a self-dual half level.");
84 }
85 sourceLevel = constructionLevel / TopographicInterpolationScale;
86 }
87 if (sourceLevel > std::numeric_limits<value_type>::max()) {
88 throw std::logic_error("Tree-of-shapes construction level exceeds the 8-bit source level set.");
89 }
90 return static_cast<value_type>(sourceLevel);
91 }
92};
93
99template <class Altitude> struct ToSAltitudeEncodingFor;
100
102template <> struct ToSAltitudeEncodingFor<std::uint8_t> {
103 using type = ToSUInt8AltitudeEncoding;
104 static constexpr TopographicAltitudeEncoding declaration = TopographicAltitudeEncoding::UInt8;
105};
106
110 static constexpr TopographicAltitudeEncoding declaration = TopographicAltitudeEncoding::ExactDoubled;
111};
112
124template <class Altitude> struct TreeOfShapesBuildResult {
126 std::vector<NodeId> parent;
128 std::vector<NodeId> smallestNodeMap;
130 std::vector<Altitude> nodeAltitudes;
134 int numRows = 0;
136 int numColumns = 0;
138 detail::NativeTopologyProof topologyProof;
139
147 [[nodiscard]] detail::ValidatedNativeHierarchy<Altitude> takeValidatedHierarchy(MorphologicalTreeSemantics semantics) && {
148 return detail::makeValidatedNativeHierarchy<Altitude>(std::move(parent), std::move(smallestNodeMap), std::move(nodeAltitudes), root,
149 GridDomain2D{numRows, numColumns}, std::move(semantics), std::move(topologyProof));
150 }
151};
152
153/************************ Tree of Shapes support ************************/
154
156namespace detail {
157
158/*
159 * Adaptive adjacency backend used by the tree-of-shapes construction.
160 *
161 * Diagonal links are activated on demand so that the interpolated grid can
162 * emulate the required 4/8-connectivity behaviour during the union-find pass.
163 * Each pixel may carry four diagonal flags: SW, NE, SE, and NW.
164 */
165enum class DiagonalConnection : uint8_t { None = 0, Sw = 1 << 0, Ne = 1 << 1, Se = 1 << 2, Nw = 1 << 3 };
166
167// Helper operators for diagonal-connection flags.
175inline DiagonalConnection operator|(DiagonalConnection a, DiagonalConnection b) {
176 return static_cast<DiagonalConnection>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
177}
178
186inline DiagonalConnection& operator|=(DiagonalConnection& a, DiagonalConnection b) {
187 a = a | b;
188 return a;
189}
190
198inline bool operator&(DiagonalConnection a, DiagonalConnection b) { return static_cast<uint8_t>(a) & static_cast<uint8_t>(b); }
199
203class AdjacencyUC {
204 private:
206 int numRows;
208 int numColumns;
210 std::vector<uint8_t> dconnFlags; // 4-connect. + diag. connect.
211 // N, W, S, E, SW, NE, SE, NW
213 const std::vector<int> offsetRows = {-1, 0, 1, 0, 1, -1, 1, -1};
215 const std::vector<int> offsetColumns = {0, -1, 0, 1, -1, 1, 1, -1};
217 bool enableDiagonalConnection;
219 const std::vector<DiagonalConnection> requiredDiagonal = {DiagonalConnection::Sw, DiagonalConnection::Ne, DiagonalConnection::Se, DiagonalConnection::Nw};
220
221 public:
229 AdjacencyUC(int rows, int columns, bool enableDiagonalConnection) : numRows(rows), numColumns(columns), enableDiagonalConnection(enableDiagonalConnection) {
230 if (enableDiagonalConnection)
231 dconnFlags.resize(rows * columns, 0);
232 }
233
237 ~AdjacencyUC() {}
238
246 void setDiagonalConnection(int row, int column, DiagonalConnection conn) { dconnFlags[ImageUtils::to1D(row, column, numColumns)] |= static_cast<uint8_t>(conn); }
247
254 void setDiagonalConnection(PixelId pixel, DiagonalConnection conn) { dconnFlags[pixel] |= static_cast<uint8_t>(conn); }
255
264 bool hasConnection(int row, int column, DiagonalConnection conn) const { return dconnFlags[ImageUtils::to1D(row, column, numColumns)] & static_cast<uint8_t>(conn); }
265
273 uint8_t getConnections(int row, int column) const { return dconnFlags[ImageUtils::to1D(row, column, numColumns)]; }
274
278 class NeighborIterator {
279 private:
281 AdjacencyUC& instance;
283 int row;
285 int column;
287 std::size_t id;
288
292 void advanceToValid() {
293 while (id < instance.offsetRows.size()) {
294 int r = row + instance.offsetRows[id];
295 int c = column + instance.offsetColumns[id];
296 if (r >= 0 && c >= 0 && r < instance.numRows && c < instance.numColumns) {
297 if (id < 4 || (instance.enableDiagonalConnection && instance.dconnFlags[ImageUtils::to1D(row, column, instance.numColumns)] &
298 static_cast<uint8_t>(instance.requiredDiagonal[id - 4]))) {
299 return;
300 }
301 }
302 ++id;
303 }
304 }
305
306 public:
315 NeighborIterator(AdjacencyUC& adj, int row, int column, int id) : instance(adj), row(row), column(column), id(id) { advanceToValid(); }
316
322 PixelId operator*() const {
323 int dr = instance.offsetRows[id];
324 int dc = instance.offsetColumns[id];
325 return ImageUtils::to1D(row + dr, column + dc, instance.numColumns);
326 }
327
333 NeighborIterator& operator++() {
334 ++id;
335 advanceToValid();
336 return *this;
337 }
338
345 bool operator==(const NeighborIterator& other) const { return id == other.id; }
346
353 bool operator!=(const NeighborIterator& other) const { return !(*this == other); }
354 };
355
359 class NeighborRange {
360 private:
362 AdjacencyUC& instance;
364 int row;
366 int column;
367
368 public:
376 NeighborRange(AdjacencyUC& instance, int row, int column) : instance(instance), row(row), column(column) {}
377
383 NeighborIterator begin() { return NeighborIterator(instance, row, column, 0); }
389 NeighborIterator end() { return NeighborIterator(instance, row, column, 8); }
390 };
391
398 NeighborRange getNeighborIndices(PixelId pixel) {
399 auto [row, column] = ImageUtils::to2D(pixel, numColumns);
400 return NeighborRange(*this, row, column);
401 }
402
410 NeighborRange getNeighborIndices(int row, int column) { return NeighborRange(*this, row, column); }
411};
412
416class PriorityQueueToS {
417 private:
419 std::vector<std::deque<PixelId>> buckets;
421 int currentPriority;
423 int numElements;
425 int maxPriorityLevels;
426
427 public:
433 PriorityQueueToS(int depthOfImage = ToSUInt8Depth) : currentPriority(0), numElements(0), maxPriorityLevels(1 << depthOfImage) {
434 buckets.resize(maxPriorityLevels);
435 }
436
443 void initial(PixelId element, int priority) {
444 currentPriority = priority;
445 buckets[priority].push_back(element);
446 numElements++;
447 }
453 int getCurrentPriority() { return currentPriority; }
459 bool isEmpty() { return numElements == 0; }
460
468 void priorityPush(PixelId element, int lower, int upper) {
469 int priority;
470 if (lower > currentPriority) {
471 priority = lower;
472 } else if (upper < currentPriority) {
473 priority = upper;
474 } else {
475 priority = currentPriority;
476 }
477 numElements++;
478 buckets[priority].push_back(element);
479 }
480
486 PixelId priorityPop() {
487 if (buckets[currentPriority].empty()) {
488 int nextPriority = -1;
489 for (int distance = 1; distance < maxPriorityLevels; ++distance) {
490 const int lowerPriority = currentPriority - distance;
491 if (lowerPriority >= 0 && !buckets[lowerPriority].empty()) {
492 nextPriority = lowerPriority;
493 break;
494 }
495
496 const int upperPriority = currentPriority + distance;
497 if (upperPriority < maxPriorityLevels && !buckets[upperPriority].empty()) {
498 nextPriority = upperPriority;
499 break;
500 }
501 }
502
503 if (nextPriority == -1) {
504 throw std::runtime_error("PriorityQueueToS is empty.");
505 }
506 currentPriority = nextPriority;
507 }
508
509 const PixelId element = buckets[currentPriority].front();
510 buckets[currentPriority].pop_front();
511
512 numElements--;
513 return element;
514 }
515};
516
517} // namespace detail
518
520
530 private:
532 using AdjacencyUC = detail::AdjacencyUC;
534 using DiagonalConnection = detail::DiagonalConnection;
536 using PriorityQueueToS = detail::PriorityQueueToS;
537
539 struct FloodResult {
541 std::vector<ToSFloodDepth> treeLevel;
543 std::vector<ToSGrayLevel> grayLevel;
545 std::vector<PixelId> order;
547 AdjacencyUC adjacency;
548 };
549
551 struct CanonicalFloodTree {
553 std::vector<ToSFloodDepth> treeLevel;
555 std::vector<ToSGrayLevel> grayLevel;
557 std::vector<PixelId> order;
559 std::vector<PixelId> parent;
560 };
561
563 TopographicConvention convention_;
565 detail::TopographicImmersionMode immersionMode_ = detail::TopographicImmersionMode::SelfDualSpan;
566
573 static detail::TopographicImmersionMode validateConvention(const TopographicConvention& convention) {
574 if (convention.infinityPixel < 0) {
575 throw std::invalid_argument("Tree-of-shapes infinity pixel must be non-negative.");
576 }
577 if (std::holds_alternative<SelfDualSpanImmersion>(convention.immersion)) {
578 // Only the exterior ring carries the boundary reference level, which
579 // is the mean of the two central boundary values on an even boundary
580 // and therefore the single source of half levels. Without that ring
581 // the reference level is cropped away and never read by an interior
582 // cell, so every construction level stays on the source lattice.
583 if (convention.altitudeEncoding == TopographicAltitudeEncoding::UInt8 &&
584 convention.domainExtension == TopographicDomainExtension::ExteriorRing) {
585 throw std::invalid_argument("Self-dual span immersion over an exterior ring cannot publish 8-bit altitudes because its boundary reference "
586 "level may fall on a half level; declare TopographicAltitudeEncoding::ExactDoubled or "
587 "TopographicDomainExtension::None.");
588 }
589 return detail::TopographicImmersionMode::SelfDualSpan;
590 }
591 if (const auto* canonical = std::get_if<CanonicalComplementaryGridImmersion>(&convention.immersion)) {
592 return canonical->pairing == ComplementaryPairing::Min4Max8 ? detail::TopographicImmersionMode::Min4Max8
593 : detail::TopographicImmersionMode::Min8Max4;
594 }
595 const auto& adjacencies = std::get<ComplementaryGridImmersion>(convention.immersion).complementaryAdjacencies;
596 if (adjacencies.minAdjacency.is4connectivity() && adjacencies.maxAdjacency.is8connectivity()) {
597 return detail::TopographicImmersionMode::Min4Max8;
598 }
599 if (adjacencies.minAdjacency.is8connectivity() && adjacencies.maxAdjacency.is4connectivity()) {
600 return detail::TopographicImmersionMode::Min8Max4;
601 }
602 throw std::invalid_argument("Complementary-grid immersion requires minimum/maximum adjacencies in the canonical 4/8 or 8/4 pairing.");
603 }
604
606 void validateConventionForImage(const ImageUInt8Ptr& image) const {
607 if (!image) {
608 throw std::invalid_argument("TreeOfShapesProducer requires a non-null image.");
609 }
610 if (const auto* immersion = std::get_if<ComplementaryGridImmersion>(&convention_.immersion)) {
611 const auto& adjacencies = immersion->complementaryAdjacencies;
612 const int rows = image->getNumRows();
613 const int columns = image->getNumColumns();
614 if (adjacencies.minAdjacency.getNumRows() != rows || adjacencies.minAdjacency.getNumColumns() != columns ||
615 adjacencies.maxAdjacency.getNumRows() != rows || adjacencies.maxAdjacency.getNumColumns() != columns) {
616 throw std::invalid_argument("Tree-of-shapes complementary adjacencies must match the source image domain.");
617 }
618 }
619 static_cast<void>(validatedInfinityPixel(interpolatedNumRows(image->getNumRows()), interpolatedNumColumns(image->getNumColumns())));
620 }
621
627 inline bool usesConnectivityMap() const noexcept {
628 return immersionMode_ != detail::TopographicImmersionMode::SelfDualSpan;
629 }
630
636 inline bool usesHighDiagonalAtSaddle() const noexcept { return immersionMode_ == detail::TopographicImmersionMode::Min4Max8; }
637
643 inline bool usesExteriorPadding() const noexcept { return convention_.domainExtension == TopographicDomainExtension::ExteriorRing; }
644
650 inline int interpolationOffset() const noexcept { return usesExteriorPadding() ? TopographicInterpolationPadding : 0; }
651
658 inline int checkedInterpolatedExtent(int extent) const {
659 if (extent <= 0) {
660 throw std::invalid_argument("TreeOfShapesProducer requires positive image dimensions.");
661 }
662 const std::int64_t interpolated = TopographicInterpolationScale * static_cast<std::int64_t>(extent) + (usesExteriorPadding() ? 1 : -1);
663 if (interpolated <= 0 || interpolated > std::numeric_limits<int>::max()) {
664 throw std::overflow_error("Tree-of-shapes interpolated dimension exceeds int range.");
665 }
666 return static_cast<int>(interpolated);
667 }
668
675 inline int interpolatedNumRows(int numRows) const { return checkedInterpolatedExtent(numRows); }
676
683 inline int interpolatedNumColumns(int numColumns) const { return checkedInterpolatedExtent(numColumns); }
684
692 inline int checkedDomainSize(int numRows, int numColumns) const {
693 if (numRows <= 0 || numColumns <= 0 || numRows > std::numeric_limits<int>::max() / numColumns) {
694 throw std::overflow_error("Tree-of-shapes domain size exceeds int range.");
695 }
696 return numRows * numColumns;
697 }
698
705 inline int originalPointRow(int row) const noexcept { return TopographicInterpolationScale * row + interpolationOffset(); }
706
713 inline int originalPointColumn(int column) const noexcept { return TopographicInterpolationScale * column + interpolationOffset(); }
714
721 inline ToSGrayLevel scaledOriginalLevel(uint8_t value) const noexcept {
722 return static_cast<ToSGrayLevel>(TopographicInterpolationScale * static_cast<int>(value));
723 }
724
732 inline PixelId validatedInfinityPixel(int interpNumRows, int interpNumColumns) const {
733 const std::int64_t activeSize = static_cast<std::int64_t>(interpNumRows) * static_cast<std::int64_t>(interpNumColumns);
734 if (convention_.infinityPixel < 0 || static_cast<std::int64_t>(convention_.infinityPixel) >= activeSize) {
735 throw std::invalid_argument("Tree-of-shapes infinity pixel must belong to the active topographic domain.");
736 }
737 return convention_.infinityPixel;
738 }
739
747 inline void setDiagonal0Connection(AdjacencyUC& adj, int row, int column) const {
748 adj.setDiagonalConnection(row, column - 1, DiagonalConnection::Se);
749 adj.setDiagonalConnection(row + 1, column, DiagonalConnection::Nw);
750
751 adj.setDiagonalConnection(row - 1, column - 1, DiagonalConnection::Se);
752 adj.setDiagonalConnection(row, column, DiagonalConnection::Se | DiagonalConnection::Nw);
753 adj.setDiagonalConnection(row + 1, column + 1, DiagonalConnection::Nw);
754
755 adj.setDiagonalConnection(row - 1, column, DiagonalConnection::Se);
756 adj.setDiagonalConnection(row, column + 1, DiagonalConnection::Nw);
757 }
758
766 inline void setDiagonal1Connection(AdjacencyUC& adj, int row, int column) const {
767 adj.setDiagonalConnection(row, column - 1, DiagonalConnection::Ne);
768 adj.setDiagonalConnection(row - 1, column, DiagonalConnection::Sw);
769
770 adj.setDiagonalConnection(row - 1, column + 1, DiagonalConnection::Sw);
771 adj.setDiagonalConnection(row, column, DiagonalConnection::Sw | DiagonalConnection::Ne);
772 adj.setDiagonalConnection(row + 1, column - 1, DiagonalConnection::Ne);
773
774 adj.setDiagonalConnection(row + 1, column, DiagonalConnection::Ne);
775 adj.setDiagonalConnection(row, column + 1, DiagonalConnection::Sw);
776 }
777
778 std::tuple<std::vector<ToSGrayLevel>, std::vector<ToSGrayLevel>, AdjacencyUC>
790 cropExteriorInterpolation(std::vector<ToSGrayLevel> paddedMin, std::vector<ToSGrayLevel> paddedMax, AdjacencyUC paddedAdjacency, int paddedRows,
791 int paddedColumns, bool adaptiveDiagonal) const {
792 if (paddedRows < 3 || paddedColumns < 3) {
793 throw std::logic_error("Tree-of-shapes padded immersion cannot be cropped.");
794 }
795 const int rows = paddedRows - 2;
796 const int columns = paddedColumns - 2;
797 const int size = checkedDomainSize(rows, columns);
798 std::vector<ToSGrayLevel> croppedMin(static_cast<std::size_t>(size));
799 std::vector<ToSGrayLevel> croppedMax(static_cast<std::size_t>(size));
800 AdjacencyUC croppedAdjacency(rows, columns, adaptiveDiagonal);
801
802 for (int row = 0; row < rows; ++row) {
803 for (int column = 0; column < columns; ++column) {
804 const PixelId source = ImageUtils::to1D(row + 1, column + 1, paddedColumns);
805 const PixelId target = ImageUtils::to1D(row, column, columns);
806 croppedMin[static_cast<std::size_t>(target)] = paddedMin[static_cast<std::size_t>(source)];
807 croppedMax[static_cast<std::size_t>(target)] = paddedMax[static_cast<std::size_t>(source)];
808 if (adaptiveDiagonal) {
809 const std::uint8_t connections = paddedAdjacency.getConnections(row + 1, column + 1);
810 if (connections != 0) {
811 croppedAdjacency.setDiagonalConnection(target, static_cast<DiagonalConnection>(connections));
812 }
813 }
814 }
815 }
816 return {std::move(croppedMin), std::move(croppedMax), std::move(croppedAdjacency)};
817 }
818
819 public:
826 : convention_(std::move(convention)), immersionMode_(validateConvention(convention_)) {}
827
833 [[nodiscard]] const TopographicConvention& convention() const noexcept { return convention_; }
834
839
841
847 std::tuple<std::vector<ToSGrayLevel>, std::vector<ToSGrayLevel>, AdjacencyUC> interpolateImage(const ImageUInt8Ptr& imgPtr) const {
848 // Implements the self-dual span-based immersion used by Boutry's PhD
849 // thesis: ISpan(u) followed by front propagation from a median-valued
850 // outer boundary. Internal levels are stored in Z/2 by scaling values
851 // by 2, so odd integers represent half gray levels.
852 if (!imgPtr) {
853 throw std::invalid_argument("TreeOfShapesProducer requires a non-null image.");
854 }
855 if (!usesExteriorPadding()) {
857 paddedConvention.domainExtension = TopographicDomainExtension::ExteriorRing;
858 paddedConvention.infinityPixel = 0;
859 // The padded producer only supplies interpolation buffers, which are
860 // altitude-encoding independent. Declaring the doubled encoding keeps
861 // it constructible for a convention whose own encoding is 8-bit.
862 paddedConvention.altitudeEncoding = TopographicAltitudeEncoding::ExactDoubled;
864 auto [paddedMin, paddedMax, paddedAdjacency] = paddedProducer.interpolateImage(imgPtr);
865 const int paddedRows = paddedProducer.interpolatedNumRows(imgPtr->getNumRows());
866 const int paddedColumns = paddedProducer.interpolatedNumColumns(imgPtr->getNumColumns());
867 return cropExteriorInterpolation(std::move(paddedMin), std::move(paddedMax), std::move(paddedAdjacency), paddedRows, paddedColumns, false);
868 }
869 auto img = imgPtr->rawData();
870 int numRows = imgPtr->getNumRows();
871 int numColumns = imgPtr->getNumColumns();
872 if (numRows <= 0 || numColumns <= 0) {
873 throw std::invalid_argument("TreeOfShapesProducer requires a non-empty image.");
874 }
875 if (numRows == 1 && numColumns == 1) {
876 const int interpNumRows = interpolatedNumRows(numRows);
877 const int interpNumColumns = interpolatedNumColumns(numColumns);
878 const int interpSize = checkedDomainSize(interpNumRows, interpNumColumns);
879 std::vector<ToSGrayLevel> interpolationMin(static_cast<std::size_t>(interpSize), scaledOriginalLevel(img[0]));
880 std::vector<ToSGrayLevel> interpolationMax(static_cast<std::size_t>(interpSize), scaledOriginalLevel(img[0]));
881 AdjacencyUC adj(interpNumRows, interpNumColumns, false);
882 return std::make_tuple(std::move(interpolationMin), std::move(interpolationMax), std::move(adj));
883 }
884
885 constexpr int adjCircleColumn[] = {-1, +1, -1, +1};
886 constexpr int adjCircleRow[] = {-1, -1, +1, +1};
887
888 constexpr int adjRetHorColumn[] = {0, 0};
889 constexpr int adjRetHorRow[] = {-1, +1};
890
891 constexpr int adjRetVerColumn[] = {+1, -1};
892 constexpr int adjRetVerRow[] = {0, 0};
893
894 int interpNumColumns = interpolatedNumColumns(numColumns);
895 int interpNumRows = interpolatedNumRows(numRows);
896 int size = checkedDomainSize(interpNumRows, interpNumColumns);
897
898 // Allocate interpolation result buffers for minimum and maximum levels.
899 std::vector<ToSGrayLevel> interpolationMin(size);
900 std::vector<ToSGrayLevel> interpolationMax(size);
901
902 // Collect each boundary pixel exactly once. The closed-form perimeter
903 // size 2 * (rows + columns) - 4 is valid only when both dimensions are at
904 // least two and previously left zero-filled entries for thin images.
905 std::array<int, 256> boundaryHistogram{};
906 int numBoundary = 0;
907
908 PixelId pT;
909
910 for (PixelId pixel = 0; pixel < numColumns * numRows; ++pixel) {
911 auto [row, column] = ImageUtils::to2D(pixel, numColumns);
912
913 // Check whether the pixel lies on the image border.
914 if (row == 0 || row == numRows - 1 || column == 0 || column == numColumns - 1) {
915 ++boundaryHistogram[static_cast<std::size_t>(img[pixel])];
916 ++numBoundary;
917 }
918
919 // Compute the interpolated-image index.
920 pT = ImageUtils::to1D(originalPointRow(row), originalPointColumn(column), interpNumColumns);
921
922 // Assign interpolation values.
923 interpolationMin[pT] = interpolationMax[pT] = scaledOriginalLevel(img[pixel]);
924 }
925
926 auto boundaryValueAtRank = [&](int rank) {
927 int cumulative = 0;
928 for (int value = 0; value < 256; ++value) {
929 cumulative += boundaryHistogram[static_cast<std::size_t>(value)];
930 if (rank < cumulative) {
931 return value;
932 }
933 }
934 throw std::runtime_error("Tree-of-shapes boundary histogram is inconsistent.");
935 };
936 int median;
937 if (numBoundary % 2 == 0) {
939 } else {
940 median = TopographicInterpolationScale * boundaryValueAtRank(numBoundary / 2);
941 }
942 // std::cout << "Interpolation (Median): " << median << std::endl;
943
944 PixelId qT;
945 int qColumn, qRow, min, max;
946 const int* adjColumn = nullptr;
947 const int* adjRow = nullptr;
948 int adjSize;
949 AdjacencyUC adj(interpNumRows, interpNumColumns, false);
950
951 for (int row = 0; row < interpNumRows; row++) {
952 for (int column = 0; column < interpNumColumns; column++) {
953 if (column % 2 == 1 && row % 2 == 1)
954 continue;
955 pT = ImageUtils::to1D(row, column, interpNumColumns);
956 if (column == 0 || column == interpNumColumns - 1 || row == 0 || row == interpNumRows - 1) {
957 max = median;
958 min = median;
959 } else {
960 if (column % 2 == 0 && row % 2 == 0) {
963 adjSize = 4;
964 } else if (column % 2 == 0 && row % 2 == 1) {
967 adjSize = 2;
968 } else if (column % 2 == 1 && row % 2 == 0) {
971 adjSize = 2;
972 }
973
974 min = std::numeric_limits<int>::max();
975 max = std::numeric_limits<int>::min();
976 for (int i = 0; i < adjSize; i++) {
977 qRow = row + adjRow[i];
978 qColumn = column + adjColumn[i];
979
980 if (qRow >= 0 && qColumn >= 0 && qRow < interpNumRows && qColumn < interpNumColumns) {
982
983 if (interpolationMax[qT] > max) {
985 }
986 if (interpolationMin[qT] < min) {
988 }
989 } else {
990 if (median > max) {
991 max = median;
992 }
993 if (median < min) {
994 min = median;
995 }
996 }
997 }
998 }
999 interpolationMin[pT] = static_cast<ToSGrayLevel>(min);
1000 interpolationMax[pT] = static_cast<ToSGrayLevel>(max);
1001 }
1002 }
1003 return std::make_tuple(std::move(interpolationMin), std::move(interpolationMax), std::move(adj));
1004 }
1005
1012 std::tuple<std::vector<ToSGrayLevel>, std::vector<ToSGrayLevel>, AdjacencyUC> interpolateImage4c8c(const ImageUInt8Ptr& imgPtr) const {
1013 // Implements the optimized 2D immersion/connectivity-map rules from
1014 // Carlinet, Crozet, and Geraud, "The Tree of Shapes Turned into a
1015 // Max-Tree: A Simple and Efficient Linear Algorithm", ICIP 2018.
1016 if (!imgPtr) {
1017 throw std::invalid_argument("TreeOfShapesProducer requires a non-null image.");
1018 }
1019 if (!usesExteriorPadding()) {
1020 TopographicConvention paddedConvention = convention_;
1021 paddedConvention.domainExtension = TopographicDomainExtension::ExteriorRing;
1022 paddedConvention.infinityPixel = 0;
1023 // The padded producer only supplies interpolation buffers, which are
1024 // altitude-encoding independent. Declaring the doubled encoding keeps
1025 // it constructible for a convention whose own encoding is 8-bit.
1026 paddedConvention.altitudeEncoding = TopographicAltitudeEncoding::ExactDoubled;
1027 TreeOfShapesProducer paddedProducer(std::move(paddedConvention));
1028 auto [paddedMin, paddedMax, paddedAdjacency] = paddedProducer.interpolateImage4c8c(imgPtr);
1029 const int paddedRows = paddedProducer.interpolatedNumRows(imgPtr->getNumRows());
1030 const int paddedColumns = paddedProducer.interpolatedNumColumns(imgPtr->getNumColumns());
1031 return cropExteriorInterpolation(std::move(paddedMin), std::move(paddedMax), std::move(paddedAdjacency), paddedRows, paddedColumns, true);
1032 }
1033 auto img = imgPtr->rawData();
1034 int numRows = imgPtr->getNumRows();
1035 int numColumns = imgPtr->getNumColumns();
1036 if (numRows <= 0 || numColumns <= 0) {
1037 throw std::invalid_argument("TreeOfShapesProducer requires a non-empty image.");
1038 }
1039
1040 int interpNumColumns = interpolatedNumColumns(numColumns);
1041 int interpNumRows = interpolatedNumRows(numRows);
1042 int size = checkedDomainSize(interpNumRows, interpNumColumns);
1043 AdjacencyUC adj(interpNumRows, interpNumColumns, true);
1044
1045 // Allocate interpolation result buffers for minimum and maximum levels.
1046 std::vector<ToSGrayLevel> interpolationMin(size);
1047 std::vector<ToSGrayLevel> interpolationMax(size);
1048
1049 PixelId pT;
1050 // Compute interval from 2-faces.
1051 for (PixelId pixel = 0; pixel < numColumns * numRows; ++pixel) {
1052 auto [row, column] = ImageUtils::to2D(pixel, numColumns);
1053
1054 // Compute the interpolated-image index.
1055 pT = ImageUtils::to1D(originalPointRow(row), originalPointColumn(column), interpNumColumns);
1056
1057 // Assign interpolation values.
1058 interpolationMin[pT] = interpolationMax[pT] = img[pixel];
1059 }
1060
1061 auto getValue = [&](int row, int column) -> int {
1062 int origRow = (row - 1) / 2;
1063 int originalColumn = (column - 1) / 2;
1064 return img[ImageUtils::to1D(origRow, originalColumn, numColumns)];
1065 };
1066
1067 // Borders.
1068 for (int row = 0; row < interpNumRows; row++) {
1069 int column;
1070 if (row % 2 == 1) { // horizontal e vertical
1071 column = 0;
1072 int v1 = getValue(row, column + 1);
1073 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1074 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1075
1076 column = interpNumColumns - 1;
1077 v1 = getValue(row, column - 1);
1078 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1079 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1080 } else { // circulos
1081 if (row == 0) {
1082 column = 0;
1083 int v1 = getValue(row + 1, column + 1);
1084 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1085 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1086
1087 column = interpNumColumns - 1;
1088 v1 = getValue(row + 1, column - 1);
1089 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1090 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1091
1092 } else if (row == interpNumRows - 1) {
1093 column = 0;
1094 int v1 = getValue(row - 1, 1);
1095 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1096 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1097
1098 column = interpNumColumns - 1;
1099 v1 = getValue(row - 1, column - 1);
1100 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1101 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1102 } else {
1103 column = 0;
1104 int v1 = getValue(row - 1, column + 1);
1105 int v2 = getValue(row + 1, column + 1);
1106 interpolationMin[ImageUtils::to1D(row, 0, interpNumColumns)] = std::min(v1, v2);
1107 interpolationMax[ImageUtils::to1D(row, 0, interpNumColumns)] = std::max(v1, v2);
1108
1109 column = interpNumColumns - 1;
1110 v1 = getValue(row - 1, column - 1);
1111 v2 = getValue(row + 1, column - 1);
1112 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = std::min(v1, v2);
1113 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = std::max(v1, v2);
1114 }
1115 }
1116 }
1117
1118 for (int column = 1; column < interpNumColumns - 1; column++) {
1119 int row;
1120 if (column % 2 == 1) { // horizontal e vertical
1121 row = 0;
1122 int v1 = getValue(row + 1, column);
1123 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1124 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1125
1126 row = interpNumRows - 1;
1127 v1 = getValue(row - 1, column);
1128 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1129 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = v1;
1130 } else { // circulos
1131 row = 0;
1132 int v1 = getValue(row + 1, column - 1);
1133 int v2 = getValue(row + 1, column + 1);
1134 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = std::min(v1, v2);
1135 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = std::max(v1, v2);
1136
1137 row = interpNumRows - 1;
1138 v1 = getValue(row - 1, column - 1);
1139 v2 = getValue(row - 1, column + 1);
1140 interpolationMin[ImageUtils::to1D(row, column, interpNumColumns)] = std::min(v1, v2);
1141 interpolationMax[ImageUtils::to1D(row, column, interpNumColumns)] = std::max(v1, v2);
1142 }
1143 }
1144
1145 // Compute interval from 1-faces
1146 for (int row = 1; row < interpNumRows - 1; row++) {
1147 for (int column = 1; column < interpNumColumns - 1; column++) {
1148 if (row % 2 == 1 && column % 2 == 1)
1149 continue; // Already defined.
1150
1151 pT = ImageUtils::to1D(row, column, interpNumColumns);
1152 if (column % 2 == 0 && row % 2 == 1) {
1153 int v1 = getValue(row, column + 1);
1154 int v2 = getValue(row, column - 1);
1155 interpolationMin[pT] = std::min(v1, v2);
1156 interpolationMax[pT] = std::max(v1, v2);
1157 } else if (column % 2 == 1 && row % 2 == 0) {
1158 int v1 = getValue(row + 1, column);
1159 int v2 = getValue(row - 1, column);
1160 interpolationMin[pT] = std::min(v1, v2);
1161 interpolationMax[pT] = std::max(v1, v2);
1162 }
1163 }
1164 }
1165 // Compute interval from 0-faces
1166 for (int row = 1; row < interpNumRows - 1; row++) {
1167 for (int column = 1; column < interpNumColumns - 1; column++) {
1168 if (row % 2 == 1 && column % 2 == 1)
1169 continue; // Already defined.
1170 pT = ImageUtils::to1D(row, column, interpNumColumns);
1171 if (row % 2 == 0 && column % 2 == 0) {
1172 // | v0 | v1 |
1173 // | v2 | v3 |
1174 int v0 = getValue(row - 1, column - 1);
1175 int v1 = getValue(row + 1, column - 1);
1176 int v2 = getValue(row - 1, column + 1);
1177 int v3 = getValue(row + 1, column + 1);
1178
1179 const int min_v0v3 = std::min(v0, v3);
1180 const int max_v0v3 = std::max(v0, v3);
1181 const int min_v1v2 = std::min(v1, v2);
1182 const int max_v1v2 = std::max(v1, v2);
1183 const bool diagonal0IsHigh = min_v0v3 > max_v1v2;
1184 const bool diagonal1IsHigh = min_v1v2 > max_v0v3;
1185
1186 if (diagonal0IsHigh || diagonal1IsHigh) {
1187 const bool chooseDiagonal0 = usesHighDiagonalAtSaddle() ? diagonal0IsHigh : !diagonal0IsHigh;
1188 if (chooseDiagonal0) {
1189 setDiagonal0Connection(adj, row, column);
1190 interpolationMin[pT] = min_v0v3;
1191 interpolationMax[pT] = max_v0v3;
1192 } else {
1193 setDiagonal1Connection(adj, row, column);
1194 interpolationMin[pT] = min_v1v2;
1195 interpolationMax[pT] = max_v1v2;
1196 }
1197 } else {
1198 // Non-critical configuration.
1199 interpolationMin[pT] = std::min({v0, v1, v2, v3});
1200 interpolationMax[pT] = std::max({v0, v1, v2, v3});
1201 }
1202 }
1203 }
1204 }
1205 return std::make_tuple(std::move(interpolationMin), std::move(interpolationMax), std::move(adj));
1206 }
1207
1208 private:
1215 FloodResult floodImage(const ImageUInt8Ptr& imgPtr) const {
1216 if (!imgPtr) {
1217 throw std::invalid_argument("TreeOfShapesProducer requires a non-null image.");
1218 }
1219
1220 int numRows = imgPtr->getNumRows();
1221 int numColumns = imgPtr->getNumColumns();
1222
1223 int interpNumColumns = interpolatedNumColumns(numColumns);
1224 int interpNumRows = interpolatedNumRows(numRows);
1225 int size = checkedDomainSize(interpNumRows, interpNumColumns);
1226 const bool connectivityMap = usesConnectivityMap();
1227 auto [interpolationMin, interpolationMax, adj] = connectivityMap ? interpolateImage4c8c(imgPtr) : interpolateImage(imgPtr);
1228
1229 std::vector<uint8_t> dejavu(size, 0);
1230 std::vector<PixelId> imgR(size); // Ordered pixels.
1231 std::vector<ToSFloodDepth> imgU(size); // Monotone max-tree levels.
1232 std::vector<ToSGrayLevel> grayLevel(size); // Actual propagation levels.
1233
1234 PriorityQueueToS queue(connectivityMap ? ToSUInt8Depth : ToSSelfDualDepth); // Priority queue.
1235 const PixelId infinityPixel = validatedInfinityPixel(interpNumRows, interpNumColumns);
1236 int priorityQueueOld = interpolationMin[infinityPixel];
1237 queue.initial(infinityPixel, priorityQueueOld);
1238 dejavu[infinityPixel] = true;
1239
1240 int order = 0;
1241 ToSFloodDepth depth = 0;
1242 while (!queue.isEmpty()) {
1243 const PixelId pixel = queue.priorityPop(); // Pop the element with highest priority.
1244 int priorityQueue = queue.getCurrentPriority(); // Current priority.
1245 if (connectivityMap) {
1246 if (priorityQueue != priorityQueueOld)
1247 depth++;
1248 imgU[pixel] = depth;
1249 } else {
1250 imgU[pixel] = static_cast<ToSFloodDepth>(priorityQueue);
1251 }
1252 grayLevel[pixel] = static_cast<ToSGrayLevel>(priorityQueue);
1253
1254 // Store h in the correct output order.
1255 imgR[order++] = pixel;
1256
1257 // Adjacencies.
1258 for (PixelId neighbor : adj.getNeighborIndices(pixel)) {
1259 if (!dejavu[neighbor]) {
1260 queue.priorityPush(neighbor, interpolationMin[neighbor], interpolationMax[neighbor]);
1261 dejavu[neighbor] = true; // Mark as processed.
1262 }
1263 }
1264 priorityQueueOld = priorityQueue;
1265 }
1266 if (order != size) {
1267 throw std::runtime_error("Tree-of-shapes propagation did not visit the complete interpolated domain.");
1268 }
1269 return FloodResult{std::move(imgU), std::move(grayLevel), std::move(imgR), std::move(adj)};
1270 }
1271
1278 CanonicalFloodTree buildCanonicalFloodTree(const ImageUInt8Ptr& imgPtr) const {
1279 FloodResult flood = floodImage(imgPtr);
1280 const int numPixelsInterp = static_cast<int>(flood.treeLevel.size());
1281
1282 std::vector<PixelId> zPar(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1283 std::vector<PixelId> parentInterpolate(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1284 auto findRoot = [&](PixelId pStar) {
1285 while (zPar[pStar] != pStar) {
1286 zPar[pStar] = zPar[zPar[pStar]];
1287 pStar = zPar[pStar];
1288 }
1289 return pStar;
1290 };
1291
1292 for (int i = numPixelsInterp - 1; i >= 0; --i) {
1293 const PixelId pStar = flood.order[static_cast<size_t>(i)];
1294 parentInterpolate[pStar] = pStar;
1295 zPar[pStar] = pStar;
1296 for (PixelId qStar : flood.adjacency.getNeighborIndices(pStar)) {
1297 if (zPar[qStar] != InvalidPixel) {
1298 const PixelId rStar = findRoot(qStar);
1299 if (pStar != rStar) {
1300 parentInterpolate[rStar] = pStar;
1301 zPar[rStar] = pStar;
1302 }
1303 }
1304 }
1305 }
1306
1307 auto sameLevel = [&](PixelId aStar, PixelId bStar) { return flood.treeLevel[aStar] == flood.treeLevel[bStar]; };
1308 for (PixelId pStar : flood.order) {
1309 const PixelId qStar = parentInterpolate[pStar];
1310 if (sameLevel(parentInterpolate[qStar], qStar)) {
1311 parentInterpolate[pStar] = parentInterpolate[qStar];
1312 }
1313 }
1314
1315 return CanonicalFloodTree{std::move(flood.treeLevel), std::move(flood.grayLevel), std::move(flood.order), std::move(parentInterpolate)};
1316 }
1317
1318 public:
1325 std::tuple<std::vector<ToSFloodDepth>, std::vector<PixelId>, AdjacencyUC> sort(const ImageUInt8Ptr& imgPtr) const {
1326 FloodResult flood = floodImage(imgPtr);
1327 return std::make_tuple(std::move(flood.treeLevel), std::move(flood.order), std::move(flood.adjacency));
1328 }
1329
1330 // Tests whether an interpolated pixel corresponds to an original pixel.
1338 inline bool isOriginal1D(PixelId pixel, int interpNumColumns) const {
1339 const int row = pixel / interpNumColumns;
1340 const int column = pixel - row * interpNumColumns;
1341 const int offset = interpolationOffset();
1342 return (row & 1) == offset && (column & 1) == offset;
1343 }
1344
1345 // Maps an interpolated pixel to the original image domain.
1354 inline PixelId toOriginal1D(PixelId pStar, int interNumColumns, int numColumns) const {
1355 int r = pStar / interNumColumns;
1356 int c = pStar - r * interNumColumns; // Avoid the modulo operator.
1357 const int offset = interpolationOffset();
1358 return ((r - offset) >> 1) * numColumns + ((c - offset) >> 1);
1359 }
1361
1362 private:
1376 template <class Altitude> [[nodiscard]] TreeOfShapesBuildResult<Altitude> buildTreeOfShapes(const ImageUInt8Ptr& imgPtr) const {
1377 if (!imgPtr) {
1378 throw std::invalid_argument("TreeOfShapesProducer requires a non-null image.");
1379 }
1380 const int numRows = imgPtr->getNumRows();
1381 const int numColumns = imgPtr->getNumColumns();
1382 const int numPixels = checkedDomainSize(numRows, numColumns);
1383 const int interpNumColumns = interpolatedNumColumns(numColumns);
1384
1385 CanonicalFloodTree canonical = buildCanonicalFloodTree(imgPtr);
1386 const int numPixelsInterp = static_cast<int>(canonical.parent.size());
1387 auto sameLevel = [&](PixelId aStar, PixelId bStar) {
1388 return canonical.treeLevel[static_cast<size_t>(aStar)] == canonical.treeLevel[static_cast<size_t>(bStar)];
1389 };
1390 auto repOf = [&](PixelId pStar) {
1391 const PixelId parentStar = canonical.parent[static_cast<size_t>(pStar)];
1392 return (parentStar == pStar || sameLevel(parentStar, pStar)) ? parentStar : pStar;
1393 };
1394
1395 std::vector<PixelId> canonicalReps;
1396 canonicalReps.reserve(static_cast<size_t>(numPixels));
1397 for (PixelId pStar : canonical.order) {
1398 const PixelId parentStar = canonical.parent[static_cast<size_t>(pStar)];
1399 if (parentStar == pStar || !sameLevel(parentStar, pStar)) {
1400 canonicalReps.push_back(pStar);
1401 }
1402 }
1403 if (canonicalReps.empty()) {
1404 throw std::runtime_error("Tree-of-shapes construction produced no canonical interpolated node.");
1405 }
1406
1407 const PixelId rawRoot = canonicalReps.front();
1408 std::vector<PixelId> rawParent(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1409 std::vector<PixelId> firstRawChild(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1410 std::vector<PixelId> nextRawSibling(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1411 for (PixelId repStar : canonicalReps) {
1412 if (repStar == rawRoot) {
1413 rawParent[static_cast<size_t>(repStar)] = repStar;
1414 continue;
1415 }
1416 const PixelId parentRep = repOf(canonical.parent[static_cast<size_t>(repStar)]);
1417 if (parentRep < 0 || parentRep >= numPixelsInterp || rawParent[static_cast<size_t>(parentRep)] == InvalidPixel) {
1418 throw std::runtime_error("Tree-of-shapes canonical node has no canonical parent.");
1419 }
1420 rawParent[static_cast<size_t>(repStar)] = parentRep;
1421 nextRawSibling[static_cast<size_t>(repStar)] = firstRawChild[static_cast<size_t>(parentRep)];
1422 firstRawChild[static_cast<size_t>(parentRep)] = repStar;
1423 }
1424
1425 std::vector<int> directCount(static_cast<size_t>(numPixelsInterp), 0);
1426 std::vector<PixelId> representativePixelByElement(static_cast<size_t>(numPixels), InvalidPixel);
1427 for (PixelId pStar = 0; pStar < numPixelsInterp; ++pStar) {
1428 if (!isOriginal1D(pStar, interpNumColumns)) {
1429 continue;
1430 }
1431 const PixelId elementId = toOriginal1D(pStar, interpNumColumns, numColumns);
1432 if (elementId < 0 || elementId >= numPixels) {
1433 throw std::runtime_error("Tree-of-shapes projection produced an element outside the source domain.");
1434 }
1435 const PixelId representativePixel = repOf(pStar);
1436 representativePixelByElement[static_cast<size_t>(elementId)] = representativePixel;
1437 ++directCount[static_cast<size_t>(representativePixel)];
1438 }
1439 if (std::find(representativePixelByElement.begin(), representativePixelByElement.end(), InvalidPixel) != representativePixelByElement.end()) {
1440 throw std::runtime_error("Tree-of-shapes projection did not assign every source-domain element.");
1441 }
1442
1443 // The canonical depth, parent, and full interpolated order are no
1444 // longer needed after the raw hierarchy and source-domain smallest nodes have
1445 // been extracted. Release them before allocating the projection buffer.
1446 std::vector<ToSFloodDepth>().swap(canonical.treeLevel);
1447 std::vector<PixelId>().swap(canonical.parent);
1448 std::vector<PixelId>().swap(canonical.order);
1449
1450 // Bottom-up support projection. projectedRep[r] is the canonical
1451 // representative of the distinct projected support rooted at r.
1452 std::vector<PixelId> projectedRep(static_cast<size_t>(numPixelsInterp), InvalidPixel);
1453 for (auto it = canonicalReps.rbegin(); it != canonicalReps.rend(); ++it) {
1454 const PixelId repStar = *it;
1455 int numProjectedChildren = 0;
1456 PixelId onlyProjectedChild = InvalidPixel;
1457 for (PixelId childStar = firstRawChild[static_cast<size_t>(repStar)]; childStar != InvalidPixel;
1458 childStar = nextRawSibling[static_cast<size_t>(childStar)]) {
1459 const PixelId childProjection = projectedRep[static_cast<size_t>(childStar)];
1460 if (childProjection != InvalidPixel) {
1461 ++numProjectedChildren;
1462 onlyProjectedChild = childProjection;
1463 }
1464 }
1465
1466 if (directCount[static_cast<size_t>(repStar)] > 0 || numProjectedChildren >= 2) {
1467 projectedRep[static_cast<size_t>(repStar)] = repStar;
1468 } else if (numProjectedChildren == 1) {
1469 projectedRep[static_cast<size_t>(repStar)] = onlyProjectedChild;
1470 }
1471 }
1472
1473 const PixelId projectedRootRep = projectedRep[static_cast<size_t>(rawRoot)];
1474 if (projectedRootRep == InvalidPixel || projectedRep[static_cast<size_t>(projectedRootRep)] != projectedRootRep) {
1475 throw std::runtime_error("Tree-of-shapes projection produced an empty hierarchy.");
1476 }
1477
1478 std::vector<PixelId>().swap(firstRawChild);
1479 std::vector<PixelId>().swap(nextRawSibling);
1480 std::vector<int>().swap(directCount);
1481
1482 std::vector<NodeId> nodeIdByRep(static_cast<size_t>(numPixelsInterp), InvalidNode);
1483 int numNodes = 0;
1484 for (PixelId repStar : canonicalReps) {
1485 if (projectedRep[static_cast<size_t>(repStar)] == repStar) {
1486 nodeIdByRep[static_cast<size_t>(repStar)] = numNodes++;
1487 }
1488 }
1489
1490 TreeOfShapesBuildResult<Altitude> result;
1491 result.parent.assign(static_cast<size_t>(numNodes), InvalidNode);
1492 result.smallestNodeMap.assign(static_cast<size_t>(numPixels), InvalidNode);
1493 result.nodeAltitudes.assign(static_cast<size_t>(numNodes), Altitude{});
1494 result.root = nodeIdByRep[static_cast<size_t>(projectedRootRep)];
1495 result.numRows = numRows;
1496 result.numColumns = numColumns;
1497 detail::TopologicalNativeHierarchyRecorder proofRecorder(static_cast<std::size_t>(numNodes), static_cast<std::size_t>(numPixels), result.root);
1498
1499 for (PixelId repStar : canonicalReps) {
1500 const NodeId nodeId = nodeIdByRep[static_cast<size_t>(repStar)];
1501 if (nodeId == InvalidNode) {
1502 continue;
1503 }
1504
1505 if (repStar == projectedRootRep) {
1506 result.parent[static_cast<size_t>(nodeId)] = nodeId;
1507 } else {
1508 PixelId ancestorRep = rawParent[static_cast<size_t>(repStar)];
1509 while (ancestorRep != InvalidPixel && nodeIdByRep[static_cast<size_t>(ancestorRep)] == InvalidNode) {
1510 if (ancestorRep == rawParent[static_cast<size_t>(ancestorRep)]) {
1511 ancestorRep = InvalidPixel;
1512 break;
1513 }
1514 ancestorRep = rawParent[static_cast<size_t>(ancestorRep)];
1515 }
1516 if (ancestorRep == InvalidPixel) {
1517 throw std::runtime_error("Tree-of-shapes retained node has no retained parent.");
1518 }
1519 result.parent[static_cast<size_t>(nodeId)] = nodeIdByRep[static_cast<size_t>(ancestorRep)];
1520 }
1521 proofRecorder.recordSupportedNode(nodeId, result.parent[static_cast<std::size_t>(nodeId)]);
1522
1523 result.nodeAltitudes[static_cast<size_t>(nodeId)] =
1524 ToSAltitudeEncodingFor<Altitude>::type::encode(canonical.grayLevel[static_cast<size_t>(repStar)], immersionMode_);
1525 }
1526
1527 for (PixelId elementId = 0; elementId < numPixels; ++elementId) {
1528 const PixelId representativePixel = representativePixelByElement[static_cast<size_t>(elementId)];
1529 const NodeId smallestNodeId = nodeIdByRep[static_cast<size_t>(representativePixel)];
1530 if (smallestNodeId == InvalidNode) {
1531 throw std::runtime_error("Tree-of-shapes proper part belongs to a contracted node.");
1532 }
1533 result.smallestNodeMap[static_cast<size_t>(elementId)] = smallestNodeId;
1534 proofRecorder.recordProperPart(elementId, smallestNodeId);
1535 }
1536 for (NodeId nodeId = 0; nodeId < numNodes; ++nodeId) {
1537 const NodeId parentNodeId = result.parent[static_cast<std::size_t>(nodeId)];
1538 if (nodeId != result.root &&
1539 result.nodeAltitudes[static_cast<std::size_t>(nodeId)] == result.nodeAltitudes[static_cast<std::size_t>(parentNodeId)]) {
1540 throw std::runtime_error("Tree-of-shapes construction produced an equal-altitude parent-child edge.");
1541 }
1542 }
1543 result.topologyProof = std::move(proofRecorder).finish();
1544 return result;
1545 }
1546
1547 public:
1564 template <class Altitude = std::uint8_t> [[nodiscard]] TreeOfShapesBuildResult<Altitude> build(const ImageUInt8Ptr& imgPtr) const {
1566 throw std::invalid_argument("Tree-of-shapes altitude type does not match the altitude encoding declared by the topographic convention.");
1567 }
1568 validateConventionForImage(imgPtr);
1570 }
1571};
1572
1573} // 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.
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: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.
const TopographicConvention & convention() const noexcept
Returns the immutable topographic convention selected for this producer.
TreeOfShapesProducer(TopographicConvention convention={})
Creates a tree-of-shapes builder.
TreeOfShapesBuildResult< Altitude > build(const ImageUInt8Ptr &imgPtr) const
Builds the hierarchy in the altitude scale declared by the convention.
~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.
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.
Native representation produced by a Tree-of-Shapes producer.
NodeId root
Root node id in the produced internal-node domain.
detail::NativeTopologyProof topologyProof
Move-only proof that the producer established the topology invariants.
std::vector< Altitude > nodeAltitudes
Encoded altitude for every produced internal node.
int numColumns
Number of columns in the original pixel domain.
int numRows
Number of rows in the original pixel domain.
detail::ValidatedNativeHierarchy< Altitude > takeValidatedHierarchy(MorphologicalTreeSemantics semantics) &&
Transfers the producer-owned buffers together with their generic topology proof.
std::vector< NodeId > parent
Parent id for every produced internal node.
std::vector< NodeId > smallestNodeMap
Direct owning node for every original-domain proper part.