MorphologicalAttributeFilters
Public API documentation
Loading...
Searching...
No Matches
ContourTraceComputation.hpp
1#pragma once
2
3#include "../localAttributes/FiniteWindowLocalAttributeComputer.hpp"
4#include "../trees/MorphologicalTree.hpp"
5#include "../trees/ValuedMorphologicalTreeView.hpp"
6#include "../trees/detail/TreeTraversalDetail.hpp"
7#include "../utils/Common.hpp"
8#include "../utils/Image.hpp"
9#include "detail/ContourTraceDeltaStore.hpp"
10#include "detail/PendingPixelLists.hpp"
11
12#include <algorithm>
13#include <array>
14#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
15#include <chrono>
16#endif
17#include <cstddef>
18#include <cstdint>
19#include <iterator>
20#include <limits>
21#include <stdexcept>
22#include <string>
23#include <utility>
24#include <vector>
25
26namespace mmcfilters {
27
31enum class ContourTraceSide : uint8_t { North = 0, West = 1, East = 2, South = 3 };
32
36enum class ContourLoopKind : uint8_t { External, Internal };
37
45 ContourTraceSide side = ContourTraceSide::North;
46
48 friend bool operator==(const ContourTraceEdge&, const ContourTraceEdge&) = default;
49};
50
56 ContourLoopKind kind = ContourLoopKind::External;
62 int signedArea2 = 0;
63};
64
76 private:
78 using LocalTraceDeltas = detail::ContourTraceDeltaStore;
79
80 public:
88 [[nodiscard]] static int packEdge(PixelId pixel, ContourTraceSide side) { return (4 * pixel) + static_cast<int>(side); }
89
96 [[nodiscard]] static ContourTraceEdge unpackEdge(int packedEdge) {
97 if (packedEdge < 0) {
98 return {};
99 }
100 const int side = packedEdge & 3;
101 return ContourTraceEdge{packedEdge / 4, static_cast<ContourTraceSide>(side)};
102 }
103
108 private:
109 friend class ContourTraceComputation;
110
112 enum class Direction : uint8_t { North = 0, East = 1, South = 2, West = 3 };
113
115 enum class TraceAdjacencyMode : uint8_t { Dense, Sparse };
116
118 struct DirectedEdge {
120 int packedEdge = -1;
122 int startVertex = -1;
124 int endVertex = -1;
126 Direction direction = Direction::North;
128 int signedArea2 = 0;
129
133 DirectedEdge() = default;
134
144 DirectedEdge(int packedEdge, int startVertex, int endVertex, Direction direction, int signedArea2)
145 : packedEdge(packedEdge), startVertex(startVertex), endVertex(endVertex), direction(direction), signedArea2(signedArea2) {}
146 };
147
148#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
149 public:
150 struct TraceProfileStats {
151 std::size_t nodesTraced = 0;
152 std::size_t edgesTraced = 0;
153 std::size_t loopsTraced = 0;
154 std::size_t outgoingVertices = 0;
155 std::size_t singleOutgoingVertices = 0;
156 std::size_t multiOutgoingVertices = 0;
157 std::size_t maxOutgoingDegree = 0;
158 std::size_t closedLoopStops = 0;
159 std::size_t missingOutgoingStops = 0;
160 std::size_t singleSuccessorSteps = 0;
161 std::size_t singleSuccessorVisitedStops = 0;
162 std::size_t ambiguousSuccessorSteps = 0;
163 std::size_t ambiguousSuccessorDeadEnds = 0;
164 std::size_t successorCandidateScans = 0;
165 std::size_t successorVisitedSkips = 0;
166 std::size_t successorUnvisitedCandidates = 0;
167 std::int64_t profileCountersNs = 0;
168 std::int64_t buildAdjacencyNs = 0;
169 std::int64_t walkLoopsNs = 0;
170 std::int64_t resetOutgoingNs = 0;
171 std::int64_t commitEdgesNs = 0;
172 std::int64_t commitLoopsNs = 0;
173 std::int64_t releaseScratchNs = 0;
174 };
175
176 private:
177 mutable TraceProfileStats* activeTraceProfile_ = nullptr;
178#endif
179
181 const MorphologicalTree& tree;
183 std::size_t treeMutationVersion_ = 0;
185 mutable LocalTraceDeltas localDeltas_;
186
188 mutable std::vector<int> cachedEdgeValues_;
190 mutable std::vector<uint32_t> cachedEdgeOffset_;
192 mutable std::vector<uint32_t> cachedEdgeSize_;
194 mutable std::vector<uint8_t> cachedEdgeReady_;
196 mutable std::size_t cachedEdgeReadyCount_ = 0;
197
199 mutable std::vector<ContourTraceLoop> cachedLoopInfos_;
201 mutable std::vector<uint32_t> cachedLoopInfoOffset_;
203 mutable std::vector<uint32_t> cachedLoopInfoSize_;
205 mutable std::vector<uint8_t> cachedLoopReady_;
207 mutable std::size_t cachedLoopReadyCount_ = 0;
209 mutable std::size_t cachedLoopEdgeCount_ = 0;
210
212 mutable std::vector<uint16_t> edgeMark_;
214 mutable uint16_t markGeneration_ = 1;
216 mutable bool edgeMaterializationScratchReleased_ = false;
217
219 mutable std::vector<DirectedEdge> traceDirectedEdges_;
221 mutable std::vector<int> traceOutgoingHead_;
223 mutable std::vector<int> traceOutgoingNext_;
225 mutable std::vector<int> traceTouchedVertices_;
227 mutable std::vector<int> traceSparseVertexKeys_;
229 mutable std::vector<int> traceSparseOutgoingHead_;
231 mutable std::vector<uint32_t> traceSparseSlotGeneration_;
233 mutable std::vector<int> traceSparseTouchedSlots_;
235 mutable uint32_t traceSparseGeneration_ = 1;
237 mutable std::size_t nodeLocalLoopTraceCount_ = 0;
239 mutable std::vector<uint32_t> traceVisitedGeneration_;
241 mutable uint32_t traceVisitGeneration_ = 1;
243 mutable std::vector<int> traceNodeLoopEdges_;
245 mutable std::vector<ContourTraceLoop> traceNodeLoops_;
247 mutable bool traceScratchReleased_ = false;
248
256 IncrementalContourTraces(const MorphologicalTree& tree, LocalTraceDeltas localDeltas, int capacityHint)
257 : tree(tree), treeMutationVersion_(tree.getMutationVersion()), localDeltas_(std::move(localDeltas)),
258 cachedEdgeOffset_(static_cast<std::size_t>(tree.numInternalNodeSlots()), 0),
259 cachedEdgeSize_(static_cast<std::size_t>(tree.numInternalNodeSlots()), 0),
260 cachedEdgeReady_(static_cast<std::size_t>(tree.numInternalNodeSlots()), 0),
261 cachedLoopInfoOffset_(static_cast<std::size_t>(tree.numInternalNodeSlots()), 0),
262 cachedLoopInfoSize_(static_cast<std::size_t>(tree.numInternalNodeSlots()), 0),
263 cachedLoopReady_(static_cast<std::size_t>(tree.numInternalNodeSlots()), 0),
264 edgeMark_(static_cast<std::size_t>(4 * tree.numRows() * tree.numColumns()), 0) {
265 if (capacityHint > 0) {
266 cachedEdgeValues_.reserve(static_cast<std::size_t>(capacityHint));
267 }
268 }
269
270 public:
274 class EdgeRange {
275 public:
279 class iterator {
280 public:
282 using iterator_category = std::forward_iterator_tag;
286 using difference_type = std::ptrdiff_t;
288 using pointer = void;
291
295 iterator() = default;
296
303 iterator(const std::vector<int>* values, std::size_t index) : values_(values), index_(index) {}
304
310 value_type operator*() const { return ContourTraceComputation::unpackEdge((*values_)[index_]); }
311
318 ++index_;
319 return *this;
320 }
321
328 iterator tmp(*this);
329 ++(*this);
330 return tmp;
331 }
332
334 friend bool operator==(const iterator& lhs, const iterator& rhs) { return lhs.values_ == rhs.values_ && lhs.index_ == rhs.index_; }
335
337 friend bool operator!=(const iterator& lhs, const iterator& rhs) { return !(lhs == rhs); }
338
339 private:
341 const std::vector<int>* values_ = nullptr;
343 std::size_t index_ = 0;
344 };
345
349 EdgeRange() = default;
350
358 EdgeRange(const std::vector<int>* values, std::size_t offset, std::size_t size) : values_(values), offset_(offset), size_(size) {}
359
365 iterator begin() const { return iterator(values_, offset_); }
366
372 iterator end() const { return iterator(values_, offset_ + size_); }
373
379 [[nodiscard]] bool empty() const noexcept { return size_ == 0; }
380
386 [[nodiscard]] std::size_t size() const noexcept { return size_; }
387
388 private:
390 const std::vector<int>* values_ = nullptr;
392 std::size_t offset_ = 0;
394 std::size_t size_ = 0;
395 };
396
397#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
401 struct StorageStats {
402 std::size_t addDeltaValues = 0;
403 std::size_t removeDeltaValues = 0;
404 std::size_t cachedEdgeValues = 0;
405 std::size_t cachedLoopEdges = 0;
406 std::size_t cachedLoops = 0;
407 std::size_t cachedEdgeReadyNodes = 0;
408 std::size_t cachedLoopReadyNodes = 0;
409 std::size_t traceDenseOutgoingSlots = 0;
410 std::size_t traceSparseOutgoingSlots = 0;
411 std::size_t approxAllocatedBytes = 0;
412 };
413
414 [[nodiscard]] StorageStats storageStats() const {
415 requireStableTree("IncrementalContourTraces::storageStats");
416 StorageStats stats;
417 stats.addDeltaValues = localDeltas_.addValues.size();
418 stats.removeDeltaValues = localDeltas_.removeValues.size();
419 stats.cachedEdgeValues = cachedEdgeValues_.size();
420 stats.cachedLoopEdges = cachedLoopEdgeCount_;
421 stats.cachedLoops = cachedLoopInfos_.size();
422 stats.cachedEdgeReadyNodes = cachedEdgeReadyCount_;
423 stats.cachedLoopReadyNodes = cachedLoopReadyCount_;
424 stats.traceDenseOutgoingSlots = traceOutgoingHead_.capacity();
425 stats.traceSparseOutgoingSlots = traceSparseVertexKeys_.capacity();
426 stats.approxAllocatedBytes =
427 localDeltas_.addValues.capacity() * sizeof(int) + localDeltas_.removeValues.capacity() * sizeof(int) +
428 localDeltas_.addSpans.capacity() * sizeof(LocalTraceDeltas::Span) + localDeltas_.removeSpans.capacity() * sizeof(LocalTraceDeltas::Span) +
429 cachedEdgeValues_.capacity() * sizeof(int) + cachedEdgeOffset_.capacity() * sizeof(uint32_t) + cachedEdgeSize_.capacity() * sizeof(uint32_t) +
430 cachedEdgeReady_.capacity() * sizeof(uint8_t) + cachedLoopInfos_.capacity() * sizeof(ContourTraceLoop) +
431 cachedLoopInfoOffset_.capacity() * sizeof(uint32_t) + cachedLoopInfoSize_.capacity() * sizeof(uint32_t) +
432 cachedLoopReady_.capacity() * sizeof(uint8_t) + edgeMark_.capacity() * sizeof(uint16_t) +
433 traceDirectedEdges_.capacity() * sizeof(DirectedEdge) + traceOutgoingHead_.capacity() * sizeof(int) +
434 traceOutgoingNext_.capacity() * sizeof(int) + traceTouchedVertices_.capacity() * sizeof(int) + traceSparseVertexKeys_.capacity() * sizeof(int) +
435 traceSparseOutgoingHead_.capacity() * sizeof(int) + traceSparseSlotGeneration_.capacity() * sizeof(uint32_t) +
436 traceSparseTouchedSlots_.capacity() * sizeof(int) + traceVisitedGeneration_.capacity() * sizeof(uint32_t) +
437 traceNodeLoopEdges_.capacity() * sizeof(int) + traceNodeLoops_.capacity() * sizeof(ContourTraceLoop);
438 return stats;
439 }
440#endif
441
449 requireStableTree("IncrementalContourTraces::getEdges");
450 requireLiveTraceNode(node, "IncrementalContourTraces::getEdges");
451 ensureEdgesMaterialized(node);
452 return EdgeRange(&cachedEdgeValues_, cachedEdgeOffset_[static_cast<std::size_t>(node)], cachedEdgeSize_[static_cast<std::size_t>(node)]);
453 }
454
464 [[nodiscard]] std::vector<ContourTraceLoop> getLoops(NodeId node) const {
465 requireStableTree("IncrementalContourTraces::getLoops");
466 requireLiveTraceNode(node, "IncrementalContourTraces::getLoops");
467 ensureNodeLoopsMaterialized(node);
468 const auto offset = static_cast<std::size_t>(cachedLoopInfoOffset_[static_cast<std::size_t>(node)]);
469 const auto size = static_cast<std::size_t>(cachedLoopInfoSize_[static_cast<std::size_t>(node)]);
470 if (size == 0) {
471 return {};
472 }
473 return std::vector<ContourTraceLoop>(cachedLoopInfos_.begin() + static_cast<std::ptrdiff_t>(offset),
474 cachedLoopInfos_.begin() + static_cast<std::ptrdiff_t>(offset + size));
475 }
476
484 requireStableTree("IncrementalContourTraces::getLoopEdges");
485 const std::size_t offset = loop.edgeOffset;
486 const std::size_t size = loop.edgeCount;
487 if (offset > cachedEdgeValues_.size() || size > cachedEdgeValues_.size() - offset) {
488 throw std::invalid_argument("ContourTraceLoop does not belong to this contour-trace result.");
489 }
490 return EdgeRange(&cachedEdgeValues_, offset, size);
491 }
492
496 void materializeAll() const {
497 requireStableTree("IncrementalContourTraces::materializeAll");
498 ensureLoopsMaterialized(tree.root());
499 }
500
506 [[nodiscard]] bool isMaterialized() const {
507 requireStableTree("IncrementalContourTraces::isMaterialized");
508 for (NodeId node : tree.aliveNodeIds()) {
509 if (!cachedLoopReady_[static_cast<std::size_t>(node)]) {
510 return false;
511 }
512 }
513 return true;
514 }
515
522 [[nodiscard]] bool isEdgeMaterialized(NodeId node) const {
523 requireStableTree("IncrementalContourTraces::isEdgeMaterialized");
524 requireLiveTraceNode(node, "IncrementalContourTraces::isEdgeMaterialized");
525 return static_cast<bool>(cachedEdgeReady_[static_cast<std::size_t>(node)]);
526 }
527
534 [[nodiscard]] bool isNodeTraced(NodeId node) const {
535 requireStableTree("IncrementalContourTraces::isNodeTraced");
536 requireLiveTraceNode(node, "IncrementalContourTraces::isNodeTraced");
537 return static_cast<bool>(cachedLoopReady_[static_cast<std::size_t>(node)]);
538 }
539
540#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
542 requireStableTree("IncrementalContourTraces::profileMaterializeAllLoops");
546 try {
547 ensureLoopsMaterialized(tree.root());
548 } catch (...) {
550 throw;
551 }
552 activeTraceProfile_ = previousProfile;
553 return stats;
554 }
555#endif
556
557 private:
563 void requireStableTree(const char* context) const { tree.requireMutationVersion(treeMutationVersion_, context); }
564
571 void requireLiveTraceNode(NodeId node, const char* context) const {
572 if (!tree.isAlive(node)) {
573 throw std::invalid_argument(std::string(context) + " requires a live internal NodeId.");
574 }
575 }
576
584 static uint32_t checkedU32(std::size_t value, const char* context) {
585 if (value > static_cast<std::size_t>(std::numeric_limits<uint32_t>::max())) {
586 throw std::overflow_error(std::string(context) + " exceeds uint32_t.");
587 }
588 return static_cast<uint32_t>(value);
589 }
590
596 template <class T> static void releaseVector(std::vector<T>& values) { std::vector<T>().swap(values); }
597
598#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
599 using TraceProfileClock = std::chrono::steady_clock;
600
601 [[nodiscard]] static TraceProfileClock::time_point traceProfileNow() { return TraceProfileClock::now(); }
602
603 static std::int64_t traceProfileElapsedNs(TraceProfileClock::time_point start, TraceProfileClock::time_point end) {
604 return std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
605 }
606#endif
607
613 [[nodiscard]] bool allEdgesMaterialized() const { return cachedEdgeReadyCount_ == static_cast<std::size_t>(tree.numNodes()); }
614
620 [[nodiscard]] bool allLoopsMaterialized() const { return cachedLoopReadyCount_ == static_cast<std::size_t>(tree.numNodes()); }
621
625 void releaseEdgeMaterializationScratchIfComplete() const {
626 if (edgeMaterializationScratchReleased_ || !allEdgesMaterialized()) {
627 return;
628 }
629
630 localDeltas_ = LocalTraceDeltas{};
631 releaseVector(edgeMark_);
632 markGeneration_ = 1;
633 edgeMaterializationScratchReleased_ = true;
634 }
635
639 void releaseSparseTraceOutgoingScratch() const {
640 releaseVector(traceSparseVertexKeys_);
641 releaseVector(traceSparseOutgoingHead_);
642 releaseVector(traceSparseSlotGeneration_);
643 releaseVector(traceSparseTouchedSlots_);
644 traceSparseGeneration_ = 1;
645 }
646
650 void releaseTraceScratchIfComplete() const {
651 if (traceScratchReleased_ || !allLoopsMaterialized()) {
652 return;
653 }
654
655 resetTraceOutgoingHeads();
656 releaseVector(traceDirectedEdges_);
657 releaseVector(traceOutgoingHead_);
658 releaseVector(traceOutgoingNext_);
659 releaseVector(traceTouchedVertices_);
660 releaseSparseTraceOutgoingScratch();
661 releaseVector(traceVisitedGeneration_);
662 releaseVector(traceNodeLoopEdges_);
663 releaseVector(traceNodeLoops_);
664 nodeLocalLoopTraceCount_ = 0;
665 traceVisitGeneration_ = 1;
666 traceScratchReleased_ = true;
667 }
668
672 void nextMarkGeneration() const {
673 ++markGeneration_;
674 if (markGeneration_ == 0) {
675 std::fill(edgeMark_.begin(), edgeMark_.end(), 0);
676 markGeneration_ = 1;
677 }
678 }
679
686 void addIfUnmarked(std::vector<int>& values, int packedEdge) const {
687 if (packedEdge < 0 || packedEdge >= static_cast<int>(edgeMark_.size())) {
688 return;
689 }
690 if (edgeMark_[static_cast<std::size_t>(packedEdge)] != markGeneration_) {
691 edgeMark_[static_cast<std::size_t>(packedEdge)] = markGeneration_;
692 values.push_back(packedEdge);
693 }
694 }
695
701 void removeIfMarked(int packedEdge) const {
702 if (packedEdge >= 0 && packedEdge < static_cast<int>(edgeMark_.size())) {
703 edgeMark_[static_cast<std::size_t>(packedEdge)] = 0;
704 }
705 }
706
713 std::vector<int>::const_iterator cachedEdgeBegin(NodeId node) const {
714 return cachedEdgeValues_.begin() + static_cast<std::ptrdiff_t>(cachedEdgeOffset_[static_cast<std::size_t>(node)]);
715 }
716
723 std::vector<int>::const_iterator cachedEdgeEnd(NodeId node) const {
724 return cachedEdgeBegin(node) + static_cast<std::ptrdiff_t>(cachedEdgeSize_[static_cast<std::size_t>(node)]);
725 }
726
733 void commitMaterializedEdges(NodeId node, const std::vector<int>& values) const {
734 cachedEdgeOffset_[static_cast<std::size_t>(node)] = checkedU32(cachedEdgeValues_.size(), "cached trace edge offset");
735 cachedEdgeSize_[static_cast<std::size_t>(node)] = checkedU32(values.size(), "cached trace edge size");
736 cachedEdgeValues_.insert(cachedEdgeValues_.end(), values.begin(), values.end());
737 const auto index = static_cast<std::size_t>(node);
738 if (!cachedEdgeReady_[index]) {
739 cachedEdgeReady_[index] = 1;
740 ++cachedEdgeReadyCount_;
741 }
742 }
743
749 void ensureEdgesMaterialized(NodeId root) const {
750 requireStableTree("IncrementalContourTraces::ensureEdgesMaterialized");
751 requireLiveTraceNode(root, "IncrementalContourTraces::ensureEdgesMaterialized");
752 if (cachedEdgeReady_[static_cast<std::size_t>(root)]) {
753 return;
754 }
755
756 std::vector<std::pair<NodeId, bool>> stack;
757 stack.emplace_back(root, false);
758 std::vector<int> values;
759
760 while (!stack.empty()) {
761 const auto [node, expanded] = stack.back();
762 stack.pop_back();
763 if (cachedEdgeReady_[static_cast<std::size_t>(node)]) {
764 continue;
765 }
766 if (!expanded) {
767 stack.emplace_back(node, true);
768 for (NodeId child : tree.children(node)) {
769 if (!cachedEdgeReady_[static_cast<std::size_t>(child)]) {
770 stack.emplace_back(child, false);
771 }
772 }
773 continue;
774 }
775
776 values.clear();
777 const auto additions = localDeltas_.additions(node);
778 std::size_t reserveSize = additions.size();
779 for (NodeId child : tree.children(node)) {
780 reserveSize += static_cast<std::size_t>(cachedEdgeSize_[static_cast<std::size_t>(child)]);
781 }
782 values.reserve(reserveSize);
783 nextMarkGeneration();
784
785 for (NodeId child : tree.children(node)) {
786 for (auto it = cachedEdgeBegin(child); it != cachedEdgeEnd(child); ++it) {
787 addIfUnmarked(values, *it);
788 }
789 }
790
791 for (int packedEdge : additions) {
792 addIfUnmarked(values, packedEdge);
793 }
794
795 for (int packedEdge : localDeltas_.removals(node)) {
796 removeIfMarked(packedEdge);
797 }
798
799 std::size_t writeIndex = 0;
800 for (int packedEdge : values) {
801 if (edgeMark_[static_cast<std::size_t>(packedEdge)] == markGeneration_) {
802 values[writeIndex++] = packedEdge;
803 }
804 }
805 values.resize(writeIndex);
806 commitMaterializedEdges(node, values);
807 }
808 releaseEdgeMaterializationScratchIfComplete();
809 }
810
819 static int vertexId(int row, int column, int numVertexColumns) { return (row * numVertexColumns) + column; }
820
822 struct OrientedGeometry {
824 int startVertex = -1;
826 int endVertex = -1;
828 Direction direction = Direction::North;
830 int signedArea2 = 0;
831 };
832
839 OrientedGeometry orientedGeometry(int packedEdge) const {
840 const ContourTraceEdge edge = ContourTraceComputation::unpackEdge(packedEdge);
841 const int columns = tree.numColumns();
842 const int numVertexColumns = columns + 1;
843 const auto [row, column] = ImageUtils::to2D(edge.pixel, columns);
844
845 switch (edge.side) {
846 case ContourTraceSide::North:
847 return {vertexId(row, column, numVertexColumns), vertexId(row, column + 1, numVertexColumns), Direction::East, -row};
848 case ContourTraceSide::East:
849 return {vertexId(row, column + 1, numVertexColumns), vertexId(row + 1, column + 1, numVertexColumns), Direction::South, column + 1};
850 case ContourTraceSide::South:
851 return {vertexId(row + 1, column + 1, numVertexColumns), vertexId(row + 1, column, numVertexColumns), Direction::West, row + 1};
852 case ContourTraceSide::West:
853 return {vertexId(row + 1, column, numVertexColumns), vertexId(row, column, numVertexColumns), Direction::North, -column};
854 }
855 throw std::runtime_error("Invalid contour trace side.");
856 }
857
865 static int turnPriority(Direction incoming, Direction outgoing) {
866 const int turn = (static_cast<int>(outgoing) - static_cast<int>(incoming) + 4) & 3;
867 static constexpr std::array<int, 4> priorities{
868 1, // straight
869 0, // right
870 3, // back
871 2 // left
872 };
873 return priorities[static_cast<std::size_t>(turn)];
874 }
875
887 static int chooseNextEdge(const DirectedEdge& current, int outgoingHead, const std::vector<DirectedEdge>& directedEdges,
888 const std::vector<int>& outgoingNext, const std::vector<uint32_t>& visitedGeneration, uint32_t visitGeneration
889#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
890 ,
891 std::size_t& candidateScans, std::size_t& visitedSkips, std::size_t& unvisitedCandidates
892#endif
893 ) {
894 int best = -1;
895 int bestPriority = std::numeric_limits<int>::max();
896 for (int candidate = outgoingHead; candidate != -1; candidate = outgoingNext[static_cast<std::size_t>(candidate)]) {
897#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
898 ++candidateScans;
899#endif
900 if (visitedGeneration[static_cast<std::size_t>(candidate)] == visitGeneration) {
901#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
902 ++visitedSkips;
903#endif
904 continue;
905 }
906#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
907 ++unvisitedCandidates;
908#endif
909 const int priority = turnPriority(current.direction, directedEdges[static_cast<std::size_t>(candidate)].direction);
910 if (priority < bestPriority || (priority == bestPriority && directedEdges[static_cast<std::size_t>(candidate)].packedEdge <
911 directedEdges[static_cast<std::size_t>(best)].packedEdge)) {
912 best = candidate;
913 bestPriority = priority;
914 }
915 }
916 return best;
917 }
918
922 void resetTraceOutgoingHeads() const {
923 for (int vertex : traceTouchedVertices_) {
924 const auto vertexIndex = static_cast<std::size_t>(vertex);
925 traceOutgoingHead_[vertexIndex] = -1;
926 }
927 traceTouchedVertices_.clear();
928 }
929
935 [[nodiscard]] std::size_t imageVertexCount() const {
936 return static_cast<std::size_t>((tree.numRows() + 1) * (tree.numColumns() + 1));
937 }
938
942 void ensureTraceOutgoingHeadStorage() const {
943 if (!traceOutgoingHead_.empty()) {
944 return;
945 }
946 traceOutgoingHead_.assign(imageVertexCount(), -1);
947 }
948
955 static std::size_t nextPowerOfTwoAtLeast(std::size_t value) {
956 std::size_t result = 1;
957 while (result < value) {
958 result <<= 1;
959 }
960 return result;
961 }
962
969 static std::size_t sparseOutgoingTableSize(std::size_t edgeCount) {
970 const std::size_t target = std::max<std::size_t>(2, (2 * edgeCount) + 1);
971 return nextPowerOfTwoAtLeast(target);
972 }
973
979 static constexpr std::size_t sparseNodeLocalTraceLimit() {
980 // Keep sparse adjacency for interactive point queries, then switch
981 // to dense storage before random all-node access pays hash overhead.
982 return 8;
983 }
984
992 [[nodiscard]] bool shouldUseSparseTraceAdjacency(std::size_t edgeCount, bool streamLoopInfosDirectly) const {
993 if (streamLoopInfosDirectly || !traceOutgoingHead_.empty()) {
994 return false;
995 }
996 if (nodeLocalLoopTraceCount_ >= sparseNodeLocalTraceLimit()) {
997 return false;
998 }
999 const std::size_t sparseBytes = sparseOutgoingTableSize(edgeCount) * ((2 * sizeof(int)) + sizeof(uint32_t));
1000 const std::size_t denseBytes = imageVertexCount() * sizeof(int);
1001 return sparseBytes < denseBytes;
1002 }
1003
1009 void prepareSparseTraceOutgoingHeads(std::size_t edgeCount) const {
1010 const std::size_t tableSize = sparseOutgoingTableSize(edgeCount);
1011 if (traceSparseVertexKeys_.size() != tableSize) {
1012 traceSparseVertexKeys_.assign(tableSize, 0);
1013 traceSparseOutgoingHead_.assign(tableSize, -1);
1014 traceSparseSlotGeneration_.assign(tableSize, 0);
1015 }
1016 traceSparseTouchedSlots_.clear();
1017
1018 ++traceSparseGeneration_;
1019 if (traceSparseGeneration_ == 0) {
1020 std::fill(traceSparseSlotGeneration_.begin(), traceSparseSlotGeneration_.end(), 0);
1021 traceSparseGeneration_ = 1;
1022 }
1023 }
1024
1031 [[nodiscard]] std::size_t sparseTraceSlot(int vertex) const {
1032 const auto hash = static_cast<uint32_t>(vertex) * uint32_t{2654435761u};
1033 return static_cast<std::size_t>(hash) & (traceSparseVertexKeys_.size() - 1);
1034 }
1035
1042 [[nodiscard]] int sparseTraceOutgoingHead(int vertex) const {
1043 std::size_t slot = sparseTraceSlot(vertex);
1044 while (traceSparseSlotGeneration_[slot] == traceSparseGeneration_) {
1045 if (traceSparseVertexKeys_[slot] == vertex) {
1046 return traceSparseOutgoingHead_[slot];
1047 }
1048 slot = (slot + 1) & (traceSparseVertexKeys_.size() - 1);
1049 }
1050 return -1;
1051 }
1052
1059 [[nodiscard]] std::size_t findOrInsertSparseTraceVertex(int vertex) const {
1060 std::size_t slot = sparseTraceSlot(vertex);
1061 while (traceSparseSlotGeneration_[slot] == traceSparseGeneration_) {
1062 if (traceSparseVertexKeys_[slot] == vertex) {
1063 return slot;
1064 }
1065 slot = (slot + 1) & (traceSparseVertexKeys_.size() - 1);
1066 }
1067
1068 traceSparseSlotGeneration_[slot] = traceSparseGeneration_;
1069 traceSparseVertexKeys_[slot] = vertex;
1070 traceSparseOutgoingHead_[slot] = -1;
1071 traceSparseTouchedSlots_.push_back(static_cast<int>(slot));
1072 return slot;
1073 }
1074
1081 template <TraceAdjacencyMode Mode> [[nodiscard]] int traceOutgoingHead(int vertex) const {
1082 if constexpr (Mode == TraceAdjacencyMode::Sparse) {
1083 return sparseTraceOutgoingHead(vertex);
1084 } else {
1085 return traceOutgoingHead_[static_cast<std::size_t>(vertex)];
1086 }
1087 }
1088
1094 template <TraceAdjacencyMode Mode> void prepareTraceAdjacency(std::size_t edgeCount) const {
1095 if constexpr (Mode == TraceAdjacencyMode::Sparse) {
1096 prepareSparseTraceOutgoingHeads(edgeCount);
1097 } else {
1098 if (!traceSparseVertexKeys_.empty()) {
1099 releaseSparseTraceOutgoingScratch();
1100 }
1101 ensureTraceOutgoingHeadStorage();
1102 resetTraceOutgoingHeads();
1103 }
1104 }
1105
1109 template <TraceAdjacencyMode Mode> void resetTraceAdjacency() const {
1110 if constexpr (Mode == TraceAdjacencyMode::Dense) {
1111 resetTraceOutgoingHeads();
1112 }
1113 }
1114
1121 template <TraceAdjacencyMode Mode> void pushTraceOutgoingEdge(int startVertex, int edgeIndex) const {
1122 if constexpr (Mode == TraceAdjacencyMode::Sparse) {
1123 const std::size_t slot = findOrInsertSparseTraceVertex(startVertex);
1124 traceOutgoingNext_.push_back(traceSparseOutgoingHead_[slot]);
1125 traceSparseOutgoingHead_[slot] = edgeIndex;
1126 } else {
1127 const auto vertexIndex = static_cast<std::size_t>(startVertex);
1128 if (traceOutgoingHead_[vertexIndex] == -1) {
1129 traceTouchedVertices_.push_back(startVertex);
1130 }
1131 traceOutgoingNext_.push_back(traceOutgoingHead_[vertexIndex]);
1132 traceOutgoingHead_[vertexIndex] = edgeIndex;
1133 }
1134 }
1135
1141 void nextTraceVisitGeneration(std::size_t edgeCount) const {
1142 if (traceVisitedGeneration_.size() < edgeCount) {
1143 traceVisitedGeneration_.resize(edgeCount, 0);
1144 }
1145 ++traceVisitGeneration_;
1146 if (traceVisitGeneration_ == 0) {
1147 std::fill(traceVisitedGeneration_.begin(), traceVisitedGeneration_.end(), 0);
1148 traceVisitGeneration_ = 1;
1149 }
1150 }
1151
1158 [[nodiscard]] bool isTraceVisited(int edgeIndex) const { return traceVisitedGeneration_[static_cast<std::size_t>(edgeIndex)] == traceVisitGeneration_; }
1159
1165 void markTraceVisited(int edgeIndex) const { traceVisitedGeneration_[static_cast<std::size_t>(edgeIndex)] = traceVisitGeneration_; }
1166
1172 void reserveLoopInfoCapacityForGlobalTrace(NodeId root) const {
1173 std::size_t pendingNonEmptyNodes = 0;
1174 std::size_t pendingEdgeCount = 0;
1175 for (NodeId node : tree.subtreeNodes(root)) {
1176 if (cachedLoopReady_[static_cast<std::size_t>(node)]) {
1177 continue;
1178 }
1179 const auto edgeCount = static_cast<std::size_t>(cachedEdgeSize_[static_cast<std::size_t>(node)]);
1180 if (edgeCount == 0) {
1181 continue;
1182 }
1183 ++pendingNonEmptyNodes;
1184 pendingEdgeCount += edgeCount;
1185 }
1186 if (pendingNonEmptyNodes == 0) {
1187 return;
1188 }
1189
1190 const std::size_t edgeBasedEstimate = std::max<std::size_t>(1, pendingEdgeCount / 16);
1191 const std::size_t additionalCapacity = std::max(pendingNonEmptyNodes, edgeBasedEstimate);
1192 const std::size_t targetCapacity = cachedLoopInfos_.size() + additionalCapacity;
1193 if (cachedLoopInfos_.capacity() < targetCapacity) {
1194 cachedLoopInfos_.reserve(targetCapacity);
1195 }
1196 }
1197
1204 bool canReuseEdgeSegmentForLoops(NodeId node) const {
1205 if (tree.isRoot(node)) {
1206 return true;
1207 }
1208 const NodeId parent = tree.parent(node);
1209 return parent == InvalidNode || parent == node || cachedEdgeReady_[static_cast<std::size_t>(parent)] != 0;
1210 }
1211
1217 void ensureNodeLoopsMaterialized(NodeId node) const {
1218 requireStableTree("IncrementalContourTraces::ensureNodeLoopsMaterialized");
1219 requireLiveTraceNode(node, "IncrementalContourTraces::ensureNodeLoopsMaterialized");
1220 if (cachedLoopReady_[static_cast<std::size_t>(node)]) {
1221 return;
1222 }
1223 ensureEdgesMaterialized(node);
1224 traceNodeLoops(node, false);
1225 }
1226
1232 void ensureLoopsMaterialized(NodeId root) const {
1233 requireStableTree("IncrementalContourTraces::ensureLoopsMaterialized");
1234 requireLiveTraceNode(root, "IncrementalContourTraces::ensureLoopsMaterialized");
1235 ensureEdgesMaterialized(root);
1236 reserveLoopInfoCapacityForGlobalTrace(root);
1237
1238 std::vector<std::pair<NodeId, bool>> stack;
1239 stack.emplace_back(root, false);
1240
1241 while (!stack.empty()) {
1242 const auto [node, expanded] = stack.back();
1243 stack.pop_back();
1244 if (!expanded) {
1245 stack.emplace_back(node, true);
1246 for (NodeId child : tree.children(node)) {
1247 if (!cachedLoopReady_[static_cast<std::size_t>(child)]) {
1248 stack.emplace_back(child, false);
1249 }
1250 }
1251 continue;
1252 }
1253 if (!cachedLoopReady_[static_cast<std::size_t>(node)]) {
1254 traceNodeLoops(node, true);
1255 }
1256 }
1257 }
1258
1265 void traceNodeLoops(NodeId node, bool streamLoopInfosDirectly) const {
1266 const std::size_t edgeCount = static_cast<std::size_t>(cachedEdgeSize_[static_cast<std::size_t>(node)]);
1267 const bool useSparseAdjacency = shouldUseSparseTraceAdjacency(edgeCount, streamLoopInfosDirectly);
1268 if (!streamLoopInfosDirectly) {
1269 ++nodeLocalLoopTraceCount_;
1270 }
1271 if (useSparseAdjacency) {
1272 traceNodeLoopsWithAdjacency<TraceAdjacencyMode::Sparse>(node, streamLoopInfosDirectly, edgeCount);
1273 } else {
1274 traceNodeLoopsWithAdjacency<TraceAdjacencyMode::Dense>(node, streamLoopInfosDirectly, edgeCount);
1275 }
1276 }
1277
1285 template <TraceAdjacencyMode Mode> void traceNodeLoopsWithAdjacency(NodeId node, bool streamLoopInfosDirectly, std::size_t edgeCount) const {
1286 prepareTraceAdjacency<Mode>(edgeCount);
1287#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1288 const auto buildAdjacencyStart = traceProfileNow();
1289 std::size_t nodeOutgoingVertices = 0;
1290 std::size_t nodeSingleOutgoingVertices = 0;
1291 std::size_t nodeMultiOutgoingVertices = 0;
1292 std::size_t nodeMaxOutgoingDegree = 0;
1293 std::size_t nodeClosedLoopStops = 0;
1294 std::size_t nodeMissingOutgoingStops = 0;
1295 std::size_t nodeSingleSuccessorSteps = 0;
1296 std::size_t nodeSingleSuccessorVisitedStops = 0;
1297 std::size_t nodeAmbiguousSuccessorSteps = 0;
1298 std::size_t nodeAmbiguousSuccessorDeadEnds = 0;
1299 std::size_t nodeSuccessorCandidateScans = 0;
1300 std::size_t nodeSuccessorVisitedSkips = 0;
1301 std::size_t nodeSuccessorUnvisitedCandidates = 0;
1302#endif
1303 traceDirectedEdges_.clear();
1304 traceDirectedEdges_.reserve(edgeCount);
1305 traceOutgoingNext_.clear();
1306 traceOutgoingNext_.reserve(edgeCount);
1307
1308 for (auto it = cachedEdgeBegin(node); it != cachedEdgeEnd(node); ++it) {
1309 const OrientedGeometry geometry = orientedGeometry(*it);
1310 const int edgeIndex = static_cast<int>(traceDirectedEdges_.size());
1311 traceDirectedEdges_.emplace_back(*it, geometry.startVertex, geometry.endVertex, geometry.direction, geometry.signedArea2);
1312 pushTraceOutgoingEdge<Mode>(geometry.startVertex, edgeIndex);
1313 }
1314#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1315 const auto buildAdjacencyEnd = traceProfileNow();
1316 const auto profileCountersStart = traceProfileNow();
1317 if constexpr (Mode == TraceAdjacencyMode::Dense) {
1318 for (int vertex : traceTouchedVertices_) {
1319 std::size_t degree = 0;
1320 for (int candidate = traceOutgoingHead_[static_cast<std::size_t>(vertex)]; candidate != -1;
1321 candidate = traceOutgoingNext_[static_cast<std::size_t>(candidate)]) {
1322 ++degree;
1323 }
1324 if (degree == 0) {
1325 continue;
1326 }
1327 ++nodeOutgoingVertices;
1328 if (degree == 1) {
1329 ++nodeSingleOutgoingVertices;
1330 } else {
1331 ++nodeMultiOutgoingVertices;
1332 }
1333 nodeMaxOutgoingDegree = std::max(nodeMaxOutgoingDegree, degree);
1334 }
1335 } else {
1336 for (int touchedSlot : traceSparseTouchedSlots_) {
1337 std::size_t degree = 0;
1338 const auto slot = static_cast<std::size_t>(touchedSlot);
1339 for (int candidate = traceSparseOutgoingHead_[slot]; candidate != -1; candidate = traceOutgoingNext_[static_cast<std::size_t>(candidate)]) {
1340 ++degree;
1341 }
1342 if (degree == 0) {
1343 continue;
1344 }
1345 ++nodeOutgoingVertices;
1346 if (degree == 1) {
1347 ++nodeSingleOutgoingVertices;
1348 } else {
1349 ++nodeMultiOutgoingVertices;
1350 }
1351 nodeMaxOutgoingDegree = std::max(nodeMaxOutgoingDegree, degree);
1352 }
1353 }
1354 const auto profileCountersEnd = traceProfileNow();
1355#endif
1356
1357 nextTraceVisitGeneration(traceDirectedEdges_.size());
1358 traceNodeLoopEdges_.clear();
1359 traceNodeLoopEdges_.reserve(traceDirectedEdges_.size());
1360 traceNodeLoops_.clear();
1361 if (!streamLoopInfosDirectly) {
1362 traceNodeLoops_.reserve((traceDirectedEdges_.size() / 4) + 1);
1363 }
1364
1365 std::size_t globalEdgeOffset = static_cast<std::size_t>(cachedEdgeOffset_[static_cast<std::size_t>(node)]);
1366 const bool reuseEdgeSegment = canReuseEdgeSegmentForLoops(node);
1367 if (!reuseEdgeSegment) {
1368 globalEdgeOffset = cachedEdgeValues_.size();
1369 }
1370 const std::size_t loopInfoOffset = cachedLoopInfos_.size();
1371
1372#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1373 const auto walkLoopsStart = traceProfileNow();
1374#endif
1375 try {
1376 for (int startEdge = 0; startEdge < static_cast<int>(traceDirectedEdges_.size()); ++startEdge) {
1377 if (isTraceVisited(startEdge)) {
1378 continue;
1379 }
1380
1381 const int loopStartVertex = traceDirectedEdges_[static_cast<std::size_t>(startEdge)].startVertex;
1382 const std::size_t loopEdgeOffset = traceNodeLoopEdges_.size();
1383 int signedArea2 = 0;
1384 int current = startEdge;
1385
1386 // Successor selection only returns unvisited edges, so the
1387 // hot loop does not need a second visited check here.
1388 while (current != -1) {
1389 const DirectedEdge& edge = traceDirectedEdges_[static_cast<std::size_t>(current)];
1390 markTraceVisited(current);
1391 traceNodeLoopEdges_.push_back(edge.packedEdge);
1392 signedArea2 += edge.signedArea2;
1393
1394 if (edge.endVertex == loopStartVertex) {
1395#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1396 ++nodeClosedLoopStops;
1397#endif
1398 break;
1399 }
1400
1401 const int outgoingHead = traceOutgoingHead<Mode>(edge.endVertex);
1402 if (outgoingHead == -1) {
1403#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1404 ++nodeMissingOutgoingStops;
1405#endif
1406 break;
1407 }
1408 if (traceOutgoingNext_[static_cast<std::size_t>(outgoingHead)] == -1) {
1409#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1410 ++nodeSingleSuccessorSteps;
1411#endif
1412 if (isTraceVisited(outgoingHead)) {
1413#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1414 ++nodeSingleSuccessorVisitedStops;
1415#endif
1416 current = -1;
1417 } else {
1418 current = outgoingHead;
1419 }
1420 } else {
1421#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1422 ++nodeAmbiguousSuccessorSteps;
1423#endif
1424 current = chooseNextEdge(edge, outgoingHead, traceDirectedEdges_, traceOutgoingNext_, traceVisitedGeneration_, traceVisitGeneration_
1425#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1426 ,
1427 nodeSuccessorCandidateScans, nodeSuccessorVisitedSkips, nodeSuccessorUnvisitedCandidates
1428#endif
1429 );
1430#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1431 if (current == -1) {
1432 ++nodeAmbiguousSuccessorDeadEnds;
1433 }
1434#endif
1435 }
1436 }
1437
1438 const std::size_t loopEdgeCount = traceNodeLoopEdges_.size() - loopEdgeOffset;
1439 if (loopEdgeCount == 0) {
1440 continue;
1441 }
1442
1443 const ContourLoopKind kind = signedArea2 >= 0 ? ContourLoopKind::External : ContourLoopKind::Internal;
1444 if (streamLoopInfosDirectly) {
1445 cachedLoopInfos_.push_back(ContourTraceLoop{kind, checkedU32(globalEdgeOffset + loopEdgeOffset, "global loop edge offset"),
1446 checkedU32(loopEdgeCount, "loop edge count"), signedArea2});
1447 } else {
1448 traceNodeLoops_.push_back(ContourTraceLoop{kind, checkedU32(loopEdgeOffset, "local loop edge offset"),
1449 checkedU32(loopEdgeCount, "loop edge count"), signedArea2});
1450 }
1451 }
1452 } catch (...) {
1453 if (streamLoopInfosDirectly) {
1454 cachedLoopInfos_.resize(loopInfoOffset);
1455 }
1456 resetTraceAdjacency<Mode>();
1457 throw;
1458 }
1459#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1460 const auto walkLoopsEnd = traceProfileNow();
1461 const auto resetOutgoingStart = traceProfileNow();
1462#endif
1463 resetTraceAdjacency<Mode>();
1464#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1465 const auto resetOutgoingEnd = traceProfileNow();
1466#endif
1467
1468 if (traceNodeLoopEdges_.size() != edgeCount) {
1469 if (streamLoopInfosDirectly) {
1470 cachedLoopInfos_.resize(loopInfoOffset);
1471 }
1472 throw std::runtime_error("Contour trace loop traversal did not cover every materialized edge.");
1473 }
1474#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1475 const auto commitEdgesStart = traceProfileNow();
1476#endif
1477 if (reuseEdgeSegment) {
1478 std::copy(traceNodeLoopEdges_.begin(), traceNodeLoopEdges_.end(), cachedEdgeValues_.begin() + static_cast<std::ptrdiff_t>(globalEdgeOffset));
1479 } else {
1480 cachedEdgeValues_.insert(cachedEdgeValues_.end(), traceNodeLoopEdges_.begin(), traceNodeLoopEdges_.end());
1481 }
1482#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1483 const auto commitEdgesEnd = traceProfileNow();
1484 const auto commitLoopsStart = traceProfileNow();
1485#endif
1486 cachedLoopEdgeCount_ += traceNodeLoopEdges_.size();
1487
1488 if (!streamLoopInfosDirectly) {
1489 for (ContourTraceLoop& loop : traceNodeLoops_) {
1490 loop.edgeOffset = checkedU32(globalEdgeOffset + loop.edgeOffset, "global loop edge offset");
1491 }
1492 cachedLoopInfos_.insert(cachedLoopInfos_.end(), traceNodeLoops_.begin(), traceNodeLoops_.end());
1493 }
1494
1495 cachedLoopInfoOffset_[static_cast<std::size_t>(node)] = checkedU32(loopInfoOffset, "loop info offset");
1496 cachedLoopInfoSize_[static_cast<std::size_t>(node)] = checkedU32(cachedLoopInfos_.size() - loopInfoOffset, "loop info size");
1497 const auto index = static_cast<std::size_t>(node);
1498 if (!cachedLoopReady_[index]) {
1499 cachedLoopReady_[index] = 1;
1500 ++cachedLoopReadyCount_;
1501 }
1502#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1503 const auto commitLoopsEnd = traceProfileNow();
1504 const auto releaseScratchStart = traceProfileNow();
1505#endif
1506 releaseTraceScratchIfComplete();
1507#ifdef MMCFILTERS_ENABLE_CONTOUR_TRACE_PROFILE
1508 const auto releaseScratchEnd = traceProfileNow();
1509 if (activeTraceProfile_ != nullptr) {
1510 activeTraceProfile_->nodesTraced += 1;
1511 activeTraceProfile_->edgesTraced += edgeCount;
1512 activeTraceProfile_->loopsTraced += cachedLoopInfos_.size() - loopInfoOffset;
1513 activeTraceProfile_->outgoingVertices += nodeOutgoingVertices;
1514 activeTraceProfile_->singleOutgoingVertices += nodeSingleOutgoingVertices;
1515 activeTraceProfile_->multiOutgoingVertices += nodeMultiOutgoingVertices;
1516 activeTraceProfile_->maxOutgoingDegree = std::max(activeTraceProfile_->maxOutgoingDegree, nodeMaxOutgoingDegree);
1517 activeTraceProfile_->closedLoopStops += nodeClosedLoopStops;
1518 activeTraceProfile_->missingOutgoingStops += nodeMissingOutgoingStops;
1519 activeTraceProfile_->singleSuccessorSteps += nodeSingleSuccessorSteps;
1520 activeTraceProfile_->singleSuccessorVisitedStops += nodeSingleSuccessorVisitedStops;
1521 activeTraceProfile_->ambiguousSuccessorSteps += nodeAmbiguousSuccessorSteps;
1522 activeTraceProfile_->ambiguousSuccessorDeadEnds += nodeAmbiguousSuccessorDeadEnds;
1523 activeTraceProfile_->successorCandidateScans += nodeSuccessorCandidateScans;
1524 activeTraceProfile_->successorVisitedSkips += nodeSuccessorVisitedSkips;
1525 activeTraceProfile_->successorUnvisitedCandidates += nodeSuccessorUnvisitedCandidates;
1526 activeTraceProfile_->profileCountersNs += traceProfileElapsedNs(profileCountersStart, profileCountersEnd);
1527 activeTraceProfile_->buildAdjacencyNs += traceProfileElapsedNs(buildAdjacencyStart, buildAdjacencyEnd);
1528 activeTraceProfile_->walkLoopsNs += traceProfileElapsedNs(walkLoopsStart, walkLoopsEnd);
1529 activeTraceProfile_->resetOutgoingNs += traceProfileElapsedNs(resetOutgoingStart, resetOutgoingEnd);
1530 activeTraceProfile_->commitEdgesNs += traceProfileElapsedNs(commitEdgesStart, commitEdgesEnd);
1531 activeTraceProfile_->commitLoopsNs += traceProfileElapsedNs(commitLoopsStart, commitLoopsEnd);
1532 activeTraceProfile_->releaseScratchNs += traceProfileElapsedNs(releaseScratchStart, releaseScratchEnd);
1533 }
1534#endif
1535 }
1536 };
1537
1538 private:
1540 using PendingEdgeLists = detail::PendingPixelLists;
1541
1543 struct ExtractedTraceDeltas {
1545 LocalTraceDeltas deltas;
1547 int capacityHint = 0;
1548 };
1549
1558 static PixelId neighborPixel(const MorphologicalTree& tree, PixelId pixel, ContourTraceSide side) {
1559 const int rows = tree.numRows();
1560 const int columns = tree.numColumns();
1561 const auto [row, column] = ImageUtils::to2D(pixel, columns);
1562
1563 switch (side) {
1564 case ContourTraceSide::North:
1565 return row == 0 ? -1 : ImageUtils::to1D(row - 1, column, columns);
1566 case ContourTraceSide::West:
1567 return column == 0 ? -1 : ImageUtils::to1D(row, column - 1, columns);
1568 case ContourTraceSide::East:
1569 return column == columns - 1 ? -1 : ImageUtils::to1D(row, column + 1, columns);
1570 case ContourTraceSide::South:
1571 return row == rows - 1 ? -1 : ImageUtils::to1D(row + 1, column, columns);
1572 }
1573 return -1;
1574 }
1575
1582 [[nodiscard]] static ExtractedTraceDeltas extractTraceDeltasImpl(const MorphologicalTree& tree) {
1583 if (tree.numRows() <= 0 || tree.numColumns() <= 0) {
1584 throw std::invalid_argument("Contour trace extraction requires a non-empty image domain.");
1585 }
1586 if (!tree.isAlive(tree.root())) {
1587 throw std::invalid_argument("Contour trace extraction requires a live tree root.");
1588 }
1589
1590 const int numNodes = tree.numInternalNodeSlots();
1591 const int totalPixels = tree.numRows() * tree.numColumns();
1592 const int totalPackedEdges = 4 * totalPixels;
1593 const int capacityHint = std::max(totalPixels, 1);
1594
1595 PendingEdgeLists localEdgeAdditions(numNodes, capacityHint);
1596 PendingEdgeLists localEdgeRemovals(numNodes, capacityHint);
1597
1598 static constexpr std::array<ContourTraceSide, 4> sides{ContourTraceSide::North, ContourTraceSide::West, ContourTraceSide::East,
1599 ContourTraceSide::South};
1600
1601 detail::traversePostOrder(
1602 tree, tree.root(), [](NodeId) -> void {}, [](NodeId, NodeId) -> void {},
1603 [&](NodeId nodeId) {
1604 for (PixelId pixel : tree.properPart(nodeId)) {
1605 for (ContourTraceSide side : sides) {
1606 const int packedEdge = packEdge(pixel, side);
1607 const PixelId neighbor = neighborPixel(tree, pixel, side);
1608 if (neighbor < 0) {
1609 localEdgeAdditions.add(nodeId, packedEdge);
1610 continue;
1611 }
1612
1613 const NodeId entry = local_attributes::detail::kernel::anchoredEntry(tree, pixel, neighbor);
1614 if (entry == InvalidNode || entry == nodeId) {
1615 continue;
1616 }
1617
1618 localEdgeAdditions.add(nodeId, packedEdge);
1619 localEdgeRemovals.add(entry, packedEdge);
1620 }
1621 }
1622 });
1623
1624 return {LocalTraceDeltas::fromPendingEdgeLists(localEdgeAdditions, localEdgeRemovals, totalPackedEdges), capacityHint};
1625 }
1626
1627 public:
1635 ExtractedTraceDeltas extracted = extractTraceDeltasImpl(tree);
1636 return IncrementalContourTraces(tree, std::move(extracted.deltas), extracted.capacityHint);
1637 }
1638
1639 template <AltitudeValue T>
1647 tree.requireTopologyUnchanged("ContourTraceComputation::extract");
1648 return extract(tree.topology());
1649 }
1650};
1651
1652} // 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
Mode
Available compile-time policies for defensive API-boundary validation.
Definition Contract.hpp:32
value_type operator*() const
Returns the unpacked edge at the current position.
friend bool operator==(const iterator &lhs, const iterator &rhs)
Compares the backing buffer and position.
iterator operator++(int)
Advances and returns the previous iterator position.
std::forward_iterator_tag iterator_category
Standard category for a multi-pass forward iterator.
iterator(const std::vector< int > *values, std::size_t index)
Creates an iterator over a packed-edge buffer position.
friend bool operator!=(const iterator &lhs, const iterator &rhs)
Returns true when two iterator positions differ.
EdgeRange(const std::vector< int > *values, std::size_t offset, std::size_t size)
Creates a view over size packed edges starting at offset.
bool empty() const noexcept
Returns whether the range contains no edges.
std::size_t size() const noexcept
Returns the number of edges in the range.
Incremental side-level contour extraction and boundary-loop tracing.
static int packEdge(PixelId pixel, ContourTraceSide side)
Packs one pixel-side edge into a compact integer id.
static ContourTraceEdge unpackEdge(int packedEdge)
Unpacks one compact edge id.
static IncrementalContourTraces extract(const MorphologicalTree &tree)
Runs incremental side-level contour extraction and returns lazy traces.
static IncrementalContourTraces extract(const ValuedMorphologicalTreeView< T > &tree)
Runs incremental side-level contour extraction on a valued-tree view.
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
Mutable connected-subset tree on a finite pixel domain.
int numRows() const
Returns the number of rows in the regular 2D pixel domain.
int numNodes() const noexcept
Returns the number of currently live nodes.
int numInternalNodeSlots() const
Returns the size of the dense internal-node id domain.
void requireMutationVersion(std::size_t expectedVersion, const char *context) const
Rejects stale read-only views that captured an older mutation version.
bool isAlive(NodeId nodeId) const
Tests whether a node slot currently represents a live node.
bool isRoot(NodeId nodeId) const
Tests whether nodeId is the current root.
AliveNodeRange aliveNodeIds() const
Returns a fail-fast range over all live node ids.
std::size_t getMutationVersion() const noexcept
Returns the monotonic mutation counter used by read-only views.
int numColumns() const
Returns the number of columns in the regular 2D pixel domain.
NodeId parent(NodeId nodeId) const
Returns the direct parent of nodeId.
NodeId root() const
Returns the current hierarchy root.
Owning result for one computed scalar attribute layout and buffer.
EdgeRange getEdges(NodeId node) const
Returns unordered materialized boundary edges for one node.
bool isMaterialized() const
Returns whether loop traces are materialized for every live node.
bool isNodeTraced(NodeId node) const
Returns whether ordered loops are materialized for node.
EdgeRange getLoopEdges(const ContourTraceLoop &loop) const
Returns the ordered edges belonging to one loop.
std::vector< ContourTraceLoop > getLoops(NodeId node) const
Returns an owning copy of the loop metadata for one node.
bool isEdgeMaterialized(NodeId node) const
Returns whether packed boundary edges are materialized for node.
void materializeAll() const
Materializes and traces every live node.
One unpacked boundary edge attached to a support pixel.
ContourTraceSide side
Side of the support pixel occupied by the boundary edge.
PixelId pixel
Row-major support-pixel index incident to the boundary edge.
friend bool operator==(const ContourTraceEdge &, const ContourTraceEdge &)=default
Compares the support pixel and side.
Metadata for one ordered boundary loop.
int signedArea2
Doubled signed area under the trace-orientation convention.
uint32_t edgeCount
Number of consecutive edges in the loop.
uint32_t edgeOffset
First edge in the shared ordered-edge buffer.
ContourLoopKind kind
Whether this loop is an external boundary or an internal hole.