|
MorphologicalAttributeFilters
Public API documentation
|
ContoursComputedIncrementally stores compact per-node contour deltas during extraction and materializes final contours lazily when callers iterate them.
This document covers the public C++/Python API, benchmark interpretation, and internal design of mmcfilters/contours/ContoursComputedIncrementally.hpp and its helper storage types under mmcfilters/contours/detail/.
For the underlying tree support and proper-part ownership model, see Morphological Trees.
getContour(nodeId) returns a cache-aware range. The first iteration materializes the requested subtree as needed; later iterations over already materialized nodes only scan cached contiguous values.
materializeAll() is an explicit prefetch for workloads that will revisit many contours repeatedly. It is not required for ordinary iteration.
The Python API uses the same contour access names: getContour(node_id) for one node and contoursByNode() for all live nodes.
Build examples and run:
The benchmark reports Component Tree and Tree of Shapes timings for extraction, single-root access, full ordered iteration, full random-order iteration, and explicit materializeAll() prefetch plus iteration.
The contour computation has two separate responsibilities:
The implementation avoids keeping two public access models. All contour reads go through getContour(node) or contoursByNode(), and both use the same incremental materialization path. materializeAll() is only an explicit prefetch for workloads that know they will visit many or all contours.
ContoursComputedIncrementally.hpp Public API, extraction traversal, lazy materialization, cache state, and diagnostics.detail/PendingPixelLists.hpp Transient per-node linked lists backed by one reusable contiguous buffer.detail/ContourDeltaStore.hpp Persistent compact delta store with CSR-like spans for additions and removals.The detail/ headers are implementation details. They should not be treated as stable public API.
After extractCompactContours(tree), each node has two local delta lists:
These deltas are stored in ContourDeltaStore:
The final materialized contour cache is stored separately:
This keeps extraction compact and lets broad iteration become incremental: materializing one node also materializes any missing descendants in that subtree.
extractCompactContours(tree) performs one post-order traversal of the tree.
For each processed node:
detail::properPartEntryNode(...) and forwards removals to that ancestor when the event still belongs above the current node.PendingPixelLists.ContourDeltaStore.The contour side adjacency is intentionally fixed to radius 1.0, independently from the adjacency used to build the morphological tree. This follows the side-contour definition where exposed pixel sides are counted in the original image domain.
getContour(node) returns a ContourRange. The range materializes its node on first begin()/end() use.
Materialization is handled by ensureSubtreeMaterialized(root):
cachedContourValues_.contoursByNode() is only an iterator over live nodes. It does not implement a second contour model; each yielded contour range still uses getContour's materialization mechanism.
materializeAll() calls the same path from the root. It is a prefetch command, not a different representation.
Both delta compaction and contour materialization remove duplicates with a generation-marked pixel array:
The generation counter is uint16_t to reduce memory. When it wraps to zero, the whole mark buffer is cleared and the generation restarts from one. This makes the common reset path O(1) and keeps the rare wraparound path correct.
The mark buffer is scratch state. It is not part of the materialized result.
The implementation relies on these invariants:
localDeltas_ is immutable after extraction.cachedContourReady_[node] == 1 means cachedContourOffset_[node] and cachedContourSize_[node] identify a valid slice in cachedContourValues_.getContour(node) and isContourMaterialized(node) only accept live internal node ids.PendingPixelLists is transient. It is consumed or compacted before the returned IncrementalContours object is created.ContourDeltaStore stores compacted per-node deltas; duplicates inside one node span are removed during compaction.Let:
P be the number of image pixels;N be the number of internal node slots;N_live be the number of live internal nodes;C(S) be the total number of cached contour values committed while materializing a subtree S;M(S) be the number of not-yet-materialized live nodes visited while materializing a subtree S.The implementation has transient pending-removal lists and compact local add/remove delta lists, but these internal counts are generated by the fixed 4-neighbour side-contour stencil. For this implementation they are bounded by a constant factor of the number of pixel-side events, and therefore by O(P). The formulas below fold those terms into P.
Tree-query preprocessing is accounted separately because it belongs to MorphologicalTree caches and may already have been paid by another consumer:
After these caches are valid, owner lookup, ancestry checks, and LCA queries are constant time for this algorithm's entry-node evaluations.
Extraction with valid tree-query caches:
The 4-neighbour contour adjacency is fixed, so the direct neighbour scan over owned proper parts is O(P). Pending-removal propagation and final delta compaction add only linear fixed-stencil work. Therefore the cold-cache extraction bound is:
First materialization of a subtree:
This is a public upper bound that avoids exposing the internal local-delta count. A tighter output-sensitive reading replaces P by the number of compact local delta values read in the newly materialized part of S.
Calling getContour(node) for an already materialized node is constant time. Iterating the returned range costs:
Cold materializeAll():
The cost can be high for trees where many large contours are materialized, because the cache stores each node contour as its own contiguous slice. Since each cached contour is a subset of the image domain, C(S) is bounded by M(S) * P, and C(all) is bounded by N_live * P, but the output-sensitive form is usually more informative.
Before materialization, dominant storage is:
After broad materialization, dominant storage is usually:
For large images, changing pixel ids from int to uint16_t is not generally valid because image-domain indices exceed 65535. uint32_t would have the same size as int on the supported platforms, so it does not materially reduce the main cache. The current memory reduction comes mainly from using uint16_t for the scratch mark buffer.
examples/contour_benchmark.cpp reports these workloads:
extract only [extractCompactContours] Measures extraction and delta compaction without materializing final contours.extract + root via getContour(root) Measures extraction plus materializing the root subtree through normal access.extract + iterate all via getContour(node) Iterates every live node in tree order through getContour.extract + iterate all via contoursByNode() Uses the all-node range wrapper. It should be close to tree-order getContour.extract + iterate all via getContour(node) random order Measures broad access when cache reuse is less aligned with tree order.extract + materializeAll() + iterate via getContour(node) Separates prefetch/materialization from later iteration.When the workload really needs all contours, materializeAll() is the clearest way to express that intent. When the workload may stop early or visit only a subtree, plain getContour(node) keeps the computation incremental.
Prefer preserving the single public access model:
Avoid reintroducing separate "on-demand" APIs unless they expose a genuinely different semantic contract. The current implementation already computes on demand and caches incrementally.
If memory becomes the dominant problem, benchmark before changing storage. The largest post-materialization cost is usually the repeated materialized contour cache, not the delta store. Delta compression such as varint encoding may reduce memory, but it would add decoding cost and complicate random access; it should be guarded by benchmarks on Component Tree and Tree of Shapes.
NodeId domains.CONTOUR_* descriptors.