mmcfilters
Public API documentation
Loading...
Searching...
No Matches
ContourTraceComputation.hpp
1#pragma once
2
3#include "ContourTrace.hpp"
4#include "../trees/ValuedMorphologicalTreeView.hpp"
5#include "../trees/detail/CommittedTreeAccess.hpp"
6#include "../trees/detail/MorphologicalTreeConstructionContextQueries.hpp"
7#include "../utils/Image.hpp"
8#include "detail/ContourBoundaryTracer.hpp"
9#include "detail/ContourEdgeDeltaStore.hpp"
10#include "detail/ContourTraceTraversal.hpp"
11
12#include <algorithm>
13#include <array>
14#include <cstddef>
15#include <cstdint>
16#include <iterator>
17#include <memory>
18#include <span>
19#include <stdexcept>
20#include <utility>
21#include <variant>
22#include <vector>
23
24namespace mmcfilters {
25
36 private:
37 using EdgeDeltas = contours::detail::ContourEdgeDeltaStore;
38 using ForegroundConnectivity = contours::detail::ForegroundConnectivity;
39 using BoundaryTracer = contours::detail::ContourBoundaryTracer;
40 using VertexIndex = contours::detail::ContourVertexIndex;
41
43 struct ConstructionData {
44 EdgeDeltas edgeDeltas;
45 std::vector<ForegroundConnectivity> connectivityByNode;
46 };
47
49 struct SharedIndexes {
50 const MorphologicalTree& tree;
51 std::size_t mutationVersion = 0;
52 EdgeDeltas edgeDeltas;
53 std::vector<ForegroundConnectivity> connectivityByNode;
54
56 SharedIndexes(const MorphologicalTree& source, ConstructionData data)
57 : tree(source), mutationVersion(source.getMutationVersion()), edgeDeltas(std::move(data.edgeDeltas)),
58 connectivityByNode(std::move(data.connectivityByNode)) {}
59
61 void requireStableTree() const { tree.requireMutationVersion(mutationVersion, "ContourTraceComputation"); }
62 };
63
65 struct TraversalState {
66 std::shared_ptr<const SharedIndexes> indexes;
67 contours::detail::ContourTraceTraversal traversal;
68 bool hasCurrentTrace = false;
69
71 explicit TraversalState(std::shared_ptr<const SharedIndexes> source)
72 : indexes(std::move(source)), traversal(indexes->tree, indexes->edgeDeltas, indexes->connectivityByNode),
73 hasCurrentTrace(traversal.advance()) {}
74 };
75
76 public:
84 class iterator {
85 public:
87 using iterator_concept = std::input_iterator_tag;
89 using iterator_category = std::input_iterator_tag;
91 using value_type = std::pair<NodeId, ContourTraceView>;
93 using difference_type = std::ptrdiff_t;
94
95 iterator() = default;
96
102 if (!state_ || !state_->hasCurrentTrace) {
103 throw std::out_of_range("Contour trace iterator is exhausted.");
104 }
105 return state_->traversal.current();
106 }
107
113 if (!state_ || !state_->hasCurrentTrace) {
114 throw std::out_of_range("Contour trace iterator is exhausted.");
115 }
116 state_->hasCurrentTrace = state_->traversal.advance();
117 return *this;
118 }
119
121 void operator++(int) { ++*this; }
122
124 friend bool operator==(const iterator& position, std::default_sentinel_t) noexcept {
125 return !position.state_ || !position.state_->hasCurrentTrace;
126 }
127
128 private:
129 friend class ContourTraceComputation;
130
132 explicit iterator(std::shared_ptr<const SharedIndexes> indexes)
133 : state_(std::make_shared<TraversalState>(std::move(indexes))) {}
134
135 std::shared_ptr<TraversalState> state_;
136 };
137
144 [[nodiscard]] static int packEdge(PixelId pixel, ContourSide side) {
145 return contours::detail::packContourEdge(pixel, side);
146 }
147
154 return contours::detail::unpackContourEdge(packedEdge);
155 }
156
162 : indexes_(std::make_shared<SharedIndexes>(tree, prepareConstructionData(tree))) {}
163
168 template <AltitudeValue T>
170 : indexes_(std::make_shared<SharedIndexes>(view.topology(), prepareConstructionData(view))) {}
171
181 indexes_->requireStableTree();
182 if (!indexes_->tree.isAlive(node)) {
183 throw std::invalid_argument("ContourTraceComputation::trace requires a live internal NodeId.");
184 }
185
186 std::vector<int> packedEdges;
187 for (PixelId pixel : indexes_->tree.nodeSupport(node)) {
188 for (ContourSide side : contourSides()) {
189 const PixelId neighbor = adjacentPixel(indexes_->tree, pixel, side);
190 if (neighbor == InvalidPixel || !indexes_->tree.isAncestor(node, indexes_->tree.smallestNode(neighbor))) {
191 packedEdges.push_back(packEdge(pixel, side));
192 }
193 }
194 }
195 if (packedEdges.empty()) {
196 throw std::logic_error("ContourTraceComputation::trace produced an empty edge set for a live node.");
197 }
198
199 std::vector<ContourBoundary> boundaries;
200 BoundaryTracer tracer(indexes_->tree.numRows(), indexes_->tree.numColumns(), VertexIndex::Sparse);
201 tracer.trace(packedEdges, boundaries, indexes_->connectivityByNode[static_cast<std::size_t>(node)]);
202 return ContourTrace(std::move(packedEdges), std::move(boundaries));
203 }
204
210 indexes_->requireStableTree();
211 return iterator(indexes_);
212 }
213
215 [[nodiscard]] std::default_sentinel_t end() const noexcept { return {}; }
216
225 template <typename Consumer> void forEachTrace(Consumer&& consumer) const {
226 for (auto [node, traceView] : *this) {
227 consumer(node, traceView);
228 indexes_->requireStableTree();
229 }
230 }
231
232 private:
234 [[nodiscard]] static constexpr std::array<ContourSide, 4> contourSides() {
235 return {ContourSide::North, ContourSide::West, ContourSide::East, ContourSide::South};
236 }
237
243 [[nodiscard]] static ForegroundConnectivity foregroundConnectivity(const RegularGridAdjacency2D& adjacency) {
244 if (adjacency.is4connectivity()) {
245 return ForegroundConnectivity::Four;
246 }
247 if (adjacency.is8connectivity()) {
248 return ForegroundConnectivity::Eight;
249 }
250 return ForegroundConnectivity::Unknown;
251 }
252
258 [[nodiscard]] static std::array<ForegroundConnectivity, 2> shapeForegroundConnectivities(const MorphologicalTree& tree) {
259 if (const auto* adjacency = detail::constructionAdjacency(tree)) {
260 const auto connectivity = foregroundConnectivity(*adjacency);
261 return {connectivity, connectivity};
262 }
263 if (const auto* adjacencies = detail::complementaryAdjacencies(tree)) {
264 return {foregroundConnectivity(adjacencies->minAdjacency), foregroundConnectivity(adjacencies->maxAdjacency)};
265 }
266 if (const auto* convention = tree.topographicConvention();
267 convention && std::holds_alternative<SelfDualSpanImmersion>(convention->immersion)) {
268 return {ForegroundConnectivity::Four, ForegroundConnectivity::Four};
269 }
270 return {ForegroundConnectivity::Unknown, ForegroundConnectivity::Unknown};
271 }
272
278 [[nodiscard]] static std::vector<ForegroundConnectivity> foregroundConnectivityByNode(const MorphologicalTree& tree) {
279 const auto [lowerShape, upperShape] = shapeForegroundConnectivities(tree);
280 return std::vector<ForegroundConnectivity>(static_cast<std::size_t>(tree.numInternalNodeSlots()),
281 lowerShape == upperShape ? lowerShape : ForegroundConnectivity::Unknown);
282 }
283
289 template <AltitudeValue T>
290 [[nodiscard]] static std::vector<ForegroundConnectivity> foregroundConnectivityByNode(const ValuedMorphologicalTreeView<T>& view) {
291 const MorphologicalTree& tree = view.topology();
292 auto connectivityByNode = foregroundConnectivityByNode(tree);
293 const auto [lowerShape, upperShape] = shapeForegroundConnectivities(tree);
294 if (lowerShape != upperShape) {
295 for (NodeId node : tree.aliveNodeIds()) {
296 if (tree.isRoot(node)) {
297 continue;
298 }
299 const auto altitude = view.nodeAltitude(node);
300 const auto parentAltitude = view.nodeAltitude(tree.parent(node));
301 if (altitude < parentAltitude) {
302 connectivityByNode[static_cast<std::size_t>(node)] = lowerShape;
303 } else if (altitude > parentAltitude) {
304 connectivityByNode[static_cast<std::size_t>(node)] = upperShape;
305 }
306 }
307 }
308 return connectivityByNode;
309 }
310
318 [[nodiscard]] static PixelId adjacentPixel(const MorphologicalTree& tree, PixelId pixel, ContourSide side) {
319 const int rows = tree.numRows();
320 const int columns = tree.numColumns();
321 const auto [row, column] = ImageUtils::to2D(pixel, columns);
322
323 switch (side) {
324 case ContourSide::North:
325 return row == 0 ? InvalidPixel : ImageUtils::to1D(row - 1, column, columns);
326 case ContourSide::West:
327 return column == 0 ? InvalidPixel : ImageUtils::to1D(row, column - 1, columns);
328 case ContourSide::East:
329 return column == columns - 1 ? InvalidPixel : ImageUtils::to1D(row, column + 1, columns);
330 case ContourSide::South:
331 return row == rows - 1 ? InvalidPixel : ImageUtils::to1D(row + 1, column, columns);
332 }
333 return InvalidPixel;
334 }
335
341 [[nodiscard]] static EdgeDeltas prepareEdgeDeltas(const MorphologicalTree& tree) {
342 if (tree.numRows() <= 0 || tree.numColumns() <= 0) {
343 throw std::invalid_argument("Contour tracing requires a non-empty image domain.");
344 }
345 if (!tree.isAlive(tree.root())) {
346 throw std::invalid_argument("Contour tracing requires a live tree root.");
347 }
348
349 const int numNodes = tree.numInternalNodeSlots();
350 std::vector<EdgeDeltas::Event> additions;
351 std::vector<EdgeDeltas::Event> removals;
352 additions.reserve(static_cast<std::size_t>(std::max(tree.numPixels(), 1)));
353 removals.reserve(static_cast<std::size_t>(std::max(tree.numPixels(), 1)));
354 const int rows = tree.numRows();
355 const int columns = tree.numColumns();
356 const std::span<const NodeId> smallestNodes = tree.smallestNodeMap();
357
358 const auto addBorderEdge = [&](PixelId pixel, ContourSide side) {
359 additions.push_back({smallestNodes[static_cast<std::size_t>(pixel)], packEdge(pixel, side)});
360 };
361 for (int column = 0; column < columns; ++column) {
362 addBorderEdge(column, ContourSide::North);
363 addBorderEdge((rows - 1) * columns + column, ContourSide::South);
364 }
365 for (int row = 0; row < rows; ++row) {
366 addBorderEdge(row * columns, ContourSide::West);
367 addBorderEdge(row * columns + columns - 1, ContourSide::East);
368 }
369
370 std::vector<uint8_t> isRightBorder(static_cast<std::size_t>(tree.numPixels()), uint8_t{0});
371 for (int row = 0; row < rows; ++row) {
372 isRightBorder[static_cast<std::size_t>(row * columns + columns - 1)] = uint8_t{1};
373 }
374 const std::size_t numAdjacentQueries = 2 * static_cast<std::size_t>(tree.numPixels());
375 const auto adjacentPixels = [&](std::size_t queryIndex) {
376 const PixelId firstPixel = static_cast<PixelId>(queryIndex >> 1);
377 const bool horizontal = (queryIndex & 1) == 0;
378 if (horizontal) {
379 const PixelId secondPixel = isRightBorder[static_cast<std::size_t>(firstPixel)] ? firstPixel : firstPixel + 1;
380 return std::pair{firstPixel, secondPixel};
381 }
382 const PixelId secondPixel = firstPixel >= tree.numPixels() - columns ? firstPixel : firstPixel + columns;
383 return std::pair{firstPixel, secondPixel};
384 };
385 const auto lcaQuery = [&](std::size_t queryIndex) {
386 const auto [firstPixel, secondPixel] = adjacentPixels(queryIndex);
387 return std::pair{smallestNodes[static_cast<std::size_t>(firstPixel)],
388 smallestNodes[static_cast<std::size_t>(secondPixel)]};
389 };
390 detail::CommittedTreeAccess::forEachLowestCommonAncestor(
391 tree, numAdjacentQueries, lcaQuery,
392 [&](std::size_t queryIndex, NodeId entryNode) {
393 const auto [firstPixel, secondPixel] = adjacentPixels(queryIndex);
394 if (firstPixel == secondPixel) {
395 return;
396 }
397 const bool horizontal = (queryIndex & 1) == 0;
398 const ContourSide firstSide = horizontal ? ContourSide::East : ContourSide::South;
399 const ContourSide secondSide = horizontal ? ContourSide::West : ContourSide::North;
400 const NodeId firstNode = smallestNodes[static_cast<std::size_t>(firstPixel)];
401 const NodeId secondNode = smallestNodes[static_cast<std::size_t>(secondPixel)];
402 if (firstNode != entryNode) {
403 const int packedEdge = packEdge(firstPixel, firstSide);
404 additions.push_back({firstNode, packedEdge});
405 removals.push_back({entryNode, packedEdge});
406 }
407 if (secondNode != entryNode) {
408 const int packedEdge = packEdge(secondPixel, secondSide);
409 additions.push_back({secondNode, packedEdge});
410 removals.push_back({entryNode, packedEdge});
411 }
412 });
413 return EdgeDeltas::groupDistinct(numNodes, additions, removals);
414 }
415
421 [[nodiscard]] static ConstructionData prepareConstructionData(const MorphologicalTree& tree) {
422 tree.requireNotEditing("ContourTraceComputation");
423 return {prepareEdgeDeltas(tree), foregroundConnectivityByNode(tree)};
424 }
425
431 template <AltitudeValue T>
432 [[nodiscard]] static ConstructionData prepareConstructionData(const ValuedMorphologicalTreeView<T>& view) {
433 view.requireTopologyUnchanged("ContourTraceComputation");
434 view.topology().requireNotEditing("ContourTraceComputation");
435 return {prepareEdgeDeltas(view.topology()), foregroundConnectivityByNode(view)};
436 }
437
438 std::shared_ptr<const SharedIndexes> indexes_;
439};
440
441} // 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 PixelId InvalidPixel
Sentinel value used to denote an invalid pixel identifier.
Definition Common.hpp:43
Single-pass iterator yielding a node and borrowed ordered trace.
std::pair< NodeId, ContourTraceView > value_type
Node identifier and borrowed ordered trace yielded by dereference.
std::input_iterator_tag iterator_concept
C++20 iterator concept for a single-pass traversal.
std::input_iterator_tag iterator_category
Iterator category used by standard algorithms.
value_type operator*() const
Borrows the current node and ordered trace.
void operator++(int)
Advances without retaining the preceding borrowed trace.
std::ptrdiff_t difference_type
Signed iterator-distance type.
friend bool operator==(const iterator &position, std::default_sentinel_t) noexcept
Tests exhaustion against the traversal sentinel.
iterator & operator++()
Advances to the next node trace.
Incremental ordered contour traces on the image domain.
ContourTraceComputation(const MorphologicalTree &tree)
Prepares compact edge changes for a stable tree with a 2D domain.
std::default_sentinel_t end() const noexcept
Returns the exhaustion sentinel shared by all traversals.
static ContourEdge unpackEdge(int packedEdge)
Unpacks one compact contour edge identifier.
ContourTraceComputation(const ValuedMorphologicalTreeView< T > &view)
Prepares traces using the current valued view's shape connectivity.
ContourTrace trace(NodeId node) const
Computes an independently owned ordered trace for one live node.
void forEachTrace(Consumer &&consumer) const
Calls consumer(node, trace) once for every live node.
static int packEdge(PixelId pixel, ContourSide side)
Packs one pixel-side edge into a compact integer identifier.
iterator begin() const
Starts an independent incremental post-order traversal.
Independently owned ordered contour trace for one tree node.
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.
void requireMutationVersion(std::size_t expectedVersion, const char *context) const
Rejects stale read-only views that captured an older mutation version.
std::size_t getMutationVersion() const noexcept
Returns the monotonic mutation counter used by read-only views.
Owning result for one computed scalar attribute layout and buffer.
One contour edge represented by its support pixel and side.