MorphologicalAttributeFilters
Public API documentation
Loading...
Searching...
No Matches
ContoursComputedIncrementally.hpp
1#pragma once
2
3/*
4 * Overview
5 * --------
6 * This file implements the arena-based incremental contour computation
7 * described for component trees in:
8 *
9 * D. J. Da Silva et al., "Incremental component tree contour computation",
10 * Pattern Recognition Letters, 2025.
11 *
12 * The paper computes contours in the original image domain by counting exposed
13 * pixel sides; therefore the contour adjacency is always the 4-neighbourhood,
14 * independently from the adjacency used to build the tree. For a tree of
15 * shapes, this implementation applies the same 4-connected side-contour
16 * definition to the node supports projected onto the original image domain.
17 *
18 * The design focuses on memory reuse and locality during the post-order passes
19 * used to extract and aggregate contour pixels.
20 *
21 * 1. PendingPixelLists
22 * A lightweight transient store keeps per-node pixel lists inside one
23 * contiguous buffer. It avoids repeated heap allocation while contour pixels
24 * are inserted, forwarded to ancestors, and removed.
25 *
26 * 2. recycle() and consumeInto()
27 * Removed contour pixels do not normally reappear during the same pass, yet
28 * other nodes still need fresh slots. Without recycling, every `add()` call
29 * would keep extending `entries` and memory usage would scale with the
30 * total number of list operations rather than with the simultaneous peak.
31 * `consumeInto()` drains a list, forwards the values to a temporary
32 * container, and returns the slots to the free list through `recycle()`.
33 *
34 * 3. extractCompactContours()
35 * The incremental traversal first records local contour additions and
36 * removals in transient pixel lists. These lists are compacted into a CSR-like
37 * delta store (`ContourDeltaStore`) before the result is returned. A
38 * temporary list store, `pendingContourRemovals`, stores pixels that must be
39 * removed at some ancestor, typically the lowest common ancestor of two
40 * incomparable nodes.
41 *
42 * 4. Aggregation phase
43 * `ensureSubtreeMaterialized()` performs a second post-order pass on
44 * demand. It accumulates local contour pixels, applies deferred removals,
45 * and removes duplicates using generation-marked scratch storage.
46 * Materialized contours are cached per subtree in contiguous vectors, so
47 * subsequent reads of an already materialised node pay only the iteration
48 * cost. Regular `getContour(node)` reads materialize and cache the requested
49 * subtree when needed, so repeated or broad iteration is incremental.
50 *
51 * See docs/contours.md for the public API, invariants, complexity, memory
52 * notes, and benchmark interpretation.
53 */
54
55#include "../utils/Common.hpp"
56#include "../trees/MorphologicalTree.hpp"
57#include "../trees/WeightedTreeView.hpp"
58#include "../trees/detail/ProperPartEntryNode.hpp"
59#include "../trees/detail/TreeTraversalDetail.hpp"
60#include "../utils/AdjacencyRelation.hpp"
61#include "detail/PendingPixelLists.hpp"
62#include "detail/ContourDeltaStore.hpp"
63
64namespace mmcfilters {
65
70public:
72 static constexpr double ContourSideAdjacencyRadius = 1.0;
73
75 using LocalContourDeltas = detail::ContourDeltaStore;
76
77private:
78 using PendingPixelLists = detail::PendingPixelLists;
79 using ContourDeltaStore = LocalContourDeltas;
80
81 static NodeId contourEntryNode(const MorphologicalTree& tree, int anchorPixel, int samplePixel) {
82 return detail::properPartEntryNode(tree, anchorPixel, samplePixel);
83 }
84
85public:
93 private:
95
97 const MorphologicalTree& tree;
99 std::size_t treeMutationVersion_ = 0;
101 ContourDeltaStore localDeltas_;
103 mutable std::vector<int> cachedContourValues_;
105 mutable std::vector<uint32_t> cachedContourOffset_;
107 mutable std::vector<uint32_t> cachedContourSize_;
109 mutable std::vector<uint8_t> cachedContourReady_;
111 mutable std::vector<uint16_t> pixelMark_;
113 mutable uint16_t markGeneration_ = 1;
114
120 IncrementalContours(const MorphologicalTree& tree, ContourDeltaStore localDeltas, int capacityHint)
121 : tree(tree),
122 treeMutationVersion_(tree.getMutationVersion()),
123 localDeltas_(std::move(localDeltas)),
124 cachedContourOffset_(static_cast<std::size_t>(tree.getNumInternalNodeSlots()), 0),
125 cachedContourSize_(static_cast<std::size_t>(tree.getNumInternalNodeSlots()), 0),
126 cachedContourReady_(static_cast<std::size_t>(tree.getNumInternalNodeSlots()), 0),
127 pixelMark_(tree.getNumRowsOfImage() * tree.getNumColsOfImage(), 0) {
128 if (capacityHint > 0) {
129 cachedContourValues_.reserve(static_cast<size_t>(capacityHint));
130 }
131 }
132
133 public:
139 std::size_t addDeltaValues = 0;
141 std::size_t removeDeltaValues = 0;
143 std::size_t cachedContourValues = 0;
145 std::size_t cachedContourCapacity = 0;
147 std::size_t cachedContourReadyNodes = 0;
149 std::size_t approxAllocatedBytes = 0;
150 };
151
160 public:
162 using iterator = std::vector<int>::const_iterator;
163
167 ContourRange(const IncrementalContours* owner, NodeId node): owner_(owner), node_(node) {}
168
172 iterator begin() const {
173 ensureReadable_();
174 return owner_->cachedContourBegin(node_);
175 }
176
180 iterator end() const {
181 ensureReadable_();
182 return owner_->cachedContourEnd(node_);
183 }
184
188 bool empty() const { return begin() == end(); }
189
190 private:
191 void ensureReadable_() const {
192 owner_->ensureSubtreeMaterialized(node_);
193 }
194
195 const IncrementalContours* owner_ = nullptr;
196 NodeId node_ = InvalidNode;
197 };
198
207 public:
211 class iterator {
212 public:
214 using iterator_category = std::forward_iterator_tag;
215
217 using value_type = std::pair<NodeId, ContourRange>;
218
220 using difference_type = std::ptrdiff_t;
221
223 using pointer = void;
224
227
228 iterator() = default;
229
233 iterator(const IncrementalContours* owner, NodeId node): owner_(owner), node_(node) {
234 settle_();
235 }
236
240 value_type operator*() const { return {node_, ContourRange(owner_, node_)}; }
241
246 ++node_;
247 settle_();
248 return *this;
249 }
250
255 iterator tmp(*this);
256 ++(*this);
257 return tmp;
258 }
259
263 friend bool operator==(const iterator& lhs, const iterator& rhs) {
264 return lhs.node_ == rhs.node_;
265 }
266
270 friend bool operator!=(const iterator& lhs, const iterator& rhs) {
271 return !(lhs == rhs);
272 }
273
274 private:
275 void settle_() {
276 if (!owner_) {
277 node_ = InvalidNode;
278 return;
279 }
280 const NodeId numNodeSlots = owner_->tree.getNumInternalNodeSlots();
281 while (node_ >= 0 && node_ < numNodeSlots &&
282 !owner_->tree.isAlive(node_)) {
283 ++node_;
284 }
285 if (node_ >= numNodeSlots) {
286 node_ = numNodeSlots;
287 }
288 }
289
290 const IncrementalContours* owner_ = nullptr;
291 NodeId node_ = 0;
292 };
293
298 : owner_(owner) {}
299
303 iterator begin() const {
304 owner_->requireStableTree("IncrementalContours::contoursByNode");
305 return iterator(owner_, 0);
306 }
307
311 iterator end() const {
312 owner_->requireStableTree("IncrementalContours::contoursByNode");
313 return iterator(owner_, owner_->tree.getNumInternalNodeSlots());
314 }
315
316 private:
317 const IncrementalContours* owner_ = nullptr;
318 };
319
328 requireStableTree("IncrementalContours::storageStats");
330 stats.addDeltaValues = localDeltas_.addValues.size();
331 stats.removeDeltaValues = localDeltas_.removeValues.size();
332 stats.cachedContourValues = cachedContourValues_.size();
333 stats.cachedContourCapacity = cachedContourValues_.capacity();
334 stats.cachedContourReadyNodes = static_cast<std::size_t>(
335 std::count(cachedContourReady_.begin(), cachedContourReady_.end(), uint8_t{1}));
336 stats.approxAllocatedBytes =
337 localDeltas_.addValues.capacity() * sizeof(int) +
338 localDeltas_.removeValues.capacity() * sizeof(int) +
339 localDeltas_.addSpans.capacity() * sizeof(ContourDeltaStore::Span) +
340 localDeltas_.removeSpans.capacity() * sizeof(ContourDeltaStore::Span) +
341 cachedContourValues_.capacity() * sizeof(int) +
342 cachedContourOffset_.capacity() * sizeof(uint32_t) +
343 cachedContourSize_.capacity() * sizeof(uint32_t) +
344 cachedContourReady_.capacity() * sizeof(uint8_t) +
345 pixelMark_.capacity() * sizeof(uint16_t);
346 return stats;
347 }
348
351 requireStableTree("IncrementalContours::getContour");
352 requireLiveContourNode(node, "IncrementalContours::getContour");
353 return ContourRange(this, node);
354 }
355
358 requireStableTree("IncrementalContours::contoursByNode");
359 return ContoursByNodeRange(this);
360 }
361
368 void materializeAll() const {
369 requireStableTree("IncrementalContours::materializeAll");
370 ensureSubtreeMaterialized(tree.getRoot());
371 }
372
376 [[nodiscard]] bool isMaterialized() const {
377 requireStableTree("IncrementalContours::isMaterialized");
378 for (NodeId node : tree.getAliveNodeIds()) {
379 if (!cachedContourReady_[node]) {
380 return false;
381 }
382 }
383 return true;
384 }
385
390 requireStableTree("IncrementalContours::isContourMaterialized");
391 requireLiveContourNode(node, "IncrementalContours::isContourMaterialized");
392 return static_cast<bool>(cachedContourReady_[node]);
393 }
394
395 private:
396 void requireStableTree(const char* context) const {
397 tree.requireMutationVersion(treeMutationVersion_, context);
398 }
399
403 void requireLiveContourNode(NodeId node, const char* context) const {
404 if (!tree.isAlive(node)) {
405 throw std::invalid_argument(std::string(context) + " requires a live internal NodeId.");
406 }
407 }
408
409 std::vector<int>::const_iterator cachedContourBegin(NodeId node) const {
410 return cachedContourValues_.begin() + static_cast<std::ptrdiff_t>(cachedContourOffset_[node]);
411 }
412
413 std::vector<int>::const_iterator cachedContourEnd(NodeId node) const {
414 return cachedContourBegin(node) + static_cast<std::ptrdiff_t>(cachedContourSize_[node]);
415 }
416
417 void nextMarkGeneration() const {
418 ++markGeneration_;
419 if (markGeneration_ == 0) {
420 std::fill(pixelMark_.begin(), pixelMark_.end(), 0);
421 markGeneration_ = 1;
422 }
423 }
424
425 void addIfUnmarked(std::vector<int>& values, int pixel) const {
426 if (pixel < 0 || pixel >= static_cast<int>(pixelMark_.size())) {
427 return;
428 }
429 if (pixelMark_[pixel] != markGeneration_) {
430 pixelMark_[pixel] = markGeneration_;
431 values.push_back(pixel);
432 }
433 }
434
435 void removeIfMarked(int pixel) const {
436 if (pixel >= 0 && pixel < static_cast<int>(pixelMark_.size())) {
437 pixelMark_[pixel] = 0;
438 }
439 }
440
441 void commitMaterializedContour(NodeId node, const std::vector<int>& values) const {
442 cachedContourOffset_[node] = checkedU32(cachedContourValues_.size(), "cached contour offset");
443 cachedContourSize_[node] = checkedU32(values.size(), "cached contour size");
444 cachedContourValues_.insert(cachedContourValues_.end(), values.begin(), values.end());
445 cachedContourReady_[node] = 1;
446 }
447
448 static uint32_t checkedU32(std::size_t value, const char* context) {
449 if (value > static_cast<std::size_t>(std::numeric_limits<uint32_t>::max())) {
450 throw std::overflow_error(std::string(context) + " exceeds uint32_t.");
451 }
452 return static_cast<uint32_t>(value);
453 }
454
465 void ensureSubtreeMaterialized(NodeId root) const {
466 requireStableTree("IncrementalContours::ensureSubtreeMaterialized");
467 requireLiveContourNode(root, "IncrementalContours::ensureSubtreeMaterialized");
468 if (cachedContourReady_[root]) {
469 return;
470 }
471
472 std::vector<std::pair<NodeId, bool>> stack;
473 stack.emplace_back(root, false);
474 std::vector<int> values;
475
476 while (!stack.empty()) {
477 const auto [node, expanded] = stack.back();
478 stack.pop_back();
479 if (cachedContourReady_[node]) {
480 continue;
481 }
482 if (!expanded) {
483 stack.emplace_back(node, true);
484 for (NodeId child : tree.getChildren(node)) {
485 if (!cachedContourReady_[child]) {
486 stack.emplace_back(child, false);
487 }
488 }
489 continue;
490 }
491
492 values.clear();
493 const auto additions = localDeltas_.additions(node);
494 std::size_t reserveSize = additions.size();
495 for (NodeId child : tree.getChildren(node)) {
496 reserveSize += static_cast<std::size_t>(cachedContourSize_[child]);
497 }
498 values.reserve(reserveSize);
499 nextMarkGeneration();
500
501 for (NodeId child : tree.getChildren(node)) {
502 for (auto it = cachedContourBegin(child); it != cachedContourEnd(child); ++it) {
503 addIfUnmarked(values, *it);
504 }
505 }
506
507 for (int value : additions) {
508 addIfUnmarked(values, value);
509 }
510
511 for (int rem : localDeltas_.removals(node)) {
512 removeIfMarked(rem);
513 }
514
515 std::size_t writeIndex = 0;
516 for (int value : values) {
517 if (pixelMark_[static_cast<std::size_t>(value)] == markGeneration_) {
518 values[writeIndex++] = value;
519 }
520 }
521 values.resize(writeIndex);
522 commitMaterializedContour(node, values);
523 }
524 }
525 };
526
527private:
528 struct ExtractedContourDeltas {
529 ContourDeltaStore deltas;
530 int capacityHint = 0;
531 };
532
533 [[nodiscard]] static ExtractedContourDeltas extractContourDeltasImpl(const MorphologicalTree& tree) {
534 if (tree.getNumRowsOfImage() <= 0 || tree.getNumColsOfImage() <= 0) {
535 throw std::invalid_argument("Contour extraction requires a non-empty image domain.");
536 }
537 if (!tree.isAlive(tree.getRoot())) {
538 throw std::invalid_argument("Contour extraction requires a live tree root.");
539 }
540
541 const int numNodes = tree.getNumInternalNodeSlots();
542 const int totalPixels = tree.getNumRowsOfImage() * tree.getNumColsOfImage();
543
544 const int capacityHint = std::max(totalPixels / 4, 1);
545 // Temporary lists for local deltas before final compaction.
546 PendingPixelLists localContourPixels(numNodes, capacityHint);
547 PendingPixelLists localRemovalPixels(numNodes, capacityHint);
548 // Temporary list carrying pixels to the correct ancestor before removal.
549 PendingPixelLists pendingContourRemovals(numNodes, capacityHint);
550
551 // Auxiliary counter used to detect when a pixel stops being a contour pixel.
552 std::vector<int> ncount(totalPixels, 0);
553 // Reused buffer for pixels that must be removed at this node.
554 std::vector<int> removalBuffer;
555 removalBuffer.reserve(64);
556 AdjacencyRelation adj4(tree.getNumRowsOfImage(), tree.getNumColsOfImage(), ContourSideAdjacencyRadius);
557 detail::traversePostOrder(
558 tree,
559 tree.getRoot(),
560 [](NodeId) -> void {},
561 [](NodeId, NodeId) -> void {},
562 [&](NodeId nodePId) {
563 const NodeId nodeP = nodePId;
564 // Consume and process all pending removals for this node.
565 removalBuffer.clear();
566 pendingContourRemovals.consumeInto(nodeP, removalBuffer);
567 for (int p : removalBuffer) {
568 bool isPixelToBeRemoved = true;
569 for (int r : adj4.getNeighborPixels(p)) {
570 const NodeId entry = contourEntryNode(tree, p, r);
571 if (entry != InvalidNode && tree.isStrictAncestor(entry, nodePId)) {
572 pendingContourRemovals.add(entry, p);
573 isPixelToBeRemoved = false;
574 }
575 }
576 if (!adj4.isBorderDomainImage(p) && isPixelToBeRemoved) {
577 localRemovalPixels.add(nodeP, p);
578 }
579 }
580
581 // Scan proper parts owned by the current node.
582 for (int p : tree.getProperParts(nodePId)) {
583 if (adj4.isBorderDomainImage(p)) {
584 ncount[p]++;
585 }
586
587 for (int q : adj4.getNeighborPixels(p)) {
588 const NodeId nodeQId = tree.getProperPartOwner(q);
589 const NodeId entry = contourEntryNode(tree, p, q);
590 if (entry == InvalidNode) {
591 continue;
592 }
593 if (entry != nodePId) {
594 ncount[p]++;
595 if (entry != nodeQId) {
596 pendingContourRemovals.add(entry, p);
597 }
598 } else if (tree.isStrictAncestor(nodePId, nodeQId)) {
599 ncount[q]--;
600 if (ncount[q] == 0) {
601 localRemovalPixels.add(nodeP, q);
602 }
603 }
604 }
605
606 if (ncount[p] > 0) {
607 localContourPixels.add(nodeP, p);
608 }
609 }
610 });
611
612 return {
613 ContourDeltaStore::fromPendingPixelLists(localContourPixels, localRemovalPixels, totalPixels),
614 capacityHint
615 };
616 }
617
618public:
627 ExtractedContourDeltas extracted = extractContourDeltasImpl(tree);
628 return std::move(extracted.deltas);
629 }
630
631 template<AltitudeValue T>
636 tree.requireTopologyUnchanged("ContoursComputedIncrementally::extractContourDeltas");
637 return extractContourDeltas(tree.topology());
638 }
639
666 ExtractedContourDeltas extracted = extractContourDeltasImpl(tree);
667 return IncrementalContours(tree, std::move(extracted.deltas), extracted.capacityHint);
668 }
669
670 template<AltitudeValue T>
675 tree.requireTopologyUnchanged("ContoursComputedIncrementally::extractCompactContours");
676 return extractCompactContours(tree.topology());
677 }
678};
679
680} // namespace mmcfilters
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:25
iterator begin() const
Returns the first contour pixel, materializing on demand.
ContourRange(const IncrementalContours *owner, NodeId node)
Creates a range for node backed by owner.
std::vector< int >::const_iterator iterator
Immutable iterator over materialized contour pixel indices.
friend bool operator==(const iterator &lhs, const iterator &rhs)
Returns true when both iterators refer to the same node position.
iterator(const IncrementalContours *owner, NodeId node)
Creates an iterator over owner starting at node.
std::forward_iterator_tag iterator_category
Standard iterator category exposed for STL compatibility.
std::pair< NodeId, ContourRange > value_type
Pair of live node id and lazy contour range.
iterator operator++(int)
Advances to the next live node and returns the previous iterator.
friend bool operator!=(const iterator &lhs, const iterator &rhs)
Returns true when iterators refer to different node positions.
ContoursByNodeRange(const IncrementalContours *owner)
Creates a lazy all-node contour range backed by owner.
iterator begin() const
Returns an iterator positioned at the first live node.
Arena-based incremental contour extraction and aggregation for MorphologicalTree.
static LocalContourDeltas extractContourDeltas(const WeightedTreeView< T > &tree)
Extracts compact local contour additions/removals from a weighted view.
static IncrementalContours extractCompactContours(const MorphologicalTree &tree)
Runs incremental contour computation and returns compact contours.
static IncrementalContours extractCompactContours(const WeightedTreeView< T > &tree)
Runs incremental contour computation on a weighted view.
detail::ContourDeltaStore LocalContourDeltas
Compact local contour additions/removals indexed by internal node id.
static constexpr double ContourSideAdjacencyRadius
Radius of the 4-neighbour side-contour adjacency used by the algorithm.
static LocalContourDeltas extractContourDeltas(const MorphologicalTree &tree)
Extracts compact local contour additions/removals without materializing aggregate contours.
Mutable morphological tree built directly on proper parts and dense node ids.
void requireMutationVersion(std::size_t expectedVersion, const char *context) const
Rejects stale read-only views that captured an older mutation version.
int getNumInternalNodeSlots() const
Returns the size of the dense internal-node id domain.
bool isAlive(NodeId nodeId) const
Tests whether a node slot currently represents a live node.
NodeId getRoot() const
Returns the current hierarchy root.
std::size_t getMutationVersion() const noexcept
Returns the monotonic mutation counter used by read-only views.
int getNumColsOfImage() const
Returns the number of image columns in the attached 2D domain.
AliveNodeRange getAliveNodeIds() const
Returns a fail-fast range over all live node ids.
int getNumRowsOfImage() const
Returns the number of image rows in the attached 2D domain.
Owning result for one computed scalar attribute layout and buffer.
Allocation and cache diagnostics for an incremental contour result.
std::size_t approxAllocatedBytes
Approximate bytes reserved by the owned vectors in this result.
std::size_t cachedContourReadyNodes
Number of live-node slots with ready materialized contours.
std::size_t removeDeltaValues
Number of compacted local contour-removal pixels.
std::size_t cachedContourCapacity
Reserved capacity of the materialized contour cache.
std::size_t addDeltaValues
Number of compacted local contour-addition pixels.
std::size_t cachedContourValues
Number of pixels already committed to materialized contour cache.
Incremental contour result stored as compact local deltas.
bool isContourMaterialized(NodeId node) const
Tests whether one live node contour is already materialized.
bool isMaterialized() const
Tests whether every live-node contour has already been materialized.
StorageStats storageStats() const
Returns allocation-oriented diagnostics for benchmarks.
void materializeAll() const
Materializes and caches every live-node contour.