MorphologicalAttributeFilters
Public API documentation
Loading...
Searching...
No Matches
FiniteWindowLocalAttributeComputer.hpp
1#pragma once
2
3#include "../trees/MorphologicalTree.hpp"
4#include "../trees/detail/CommittedTreeAccess.hpp"
5#include "../trees/detail/TreeTraversalDetail.hpp"
6#include "../utils/Common.hpp"
7#include "../utils/Contract.hpp"
8
9#include <algorithm>
10#include <array>
11#include <concepts>
12#include <cstddef>
13#include <cstdint>
14#include <initializer_list>
15#include <limits>
16#include <optional>
17#include <set>
18#include <span>
19#include <stdexcept>
20#include <string>
21#include <utility>
22#include <vector>
23
24namespace mmcfilters::local_attributes {
25
26namespace detail {
27struct BinaryVisibilityStateAccess;
28}
29
32 int rowOffset = 0;
33 int columnOffset = 0;
34
36 friend bool operator==(const WindowOffset& lhs, const WindowOffset& rhs) = default;
37};
38
49 public:
51 static constexpr std::size_t maxNumOffsets = 32;
52
54 explicit ObservationWindow(std::vector<WindowOffset> offsets) : offsets_(std::move(offsets)) { validate(); }
55
57 ObservationWindow(std::initializer_list<WindowOffset> offsets) : ObservationWindow(std::vector<WindowOffset>(offsets)) {}
58
60 template <std::size_t N>
61 explicit ObservationWindow(const std::array<WindowOffset, N>& offsets) : ObservationWindow(std::vector<WindowOffset>(offsets.begin(), offsets.end())) {}
62
64 [[nodiscard]] std::size_t size() const noexcept { return offsets_.size(); }
66 [[nodiscard]] const WindowOffset& operator[](std::size_t index) const noexcept { return offsets_[index]; }
68 [[nodiscard]] std::span<const WindowOffset> offsets() const noexcept { return offsets_; }
70 [[nodiscard]] auto begin() const noexcept { return offsets_.begin(); }
72 [[nodiscard]] auto end() const noexcept { return offsets_.end(); }
73
74 private:
75 std::vector<WindowOffset> offsets_;
76
78 void validate() const {
79 if (offsets_.empty()) {
80 throw std::invalid_argument("ObservationWindow requires at least one offset.");
81 }
82 if (offsets_.size() > maxNumOffsets) {
83 throw std::invalid_argument("ObservationWindow supports at most 32 offsets.");
84 }
85
86 std::size_t numZeroOffsets = 0;
87 for (std::size_t i = 0; i < offsets_.size(); ++i) {
88 if (offsets_[i] == WindowOffset{}) {
89 ++numZeroOffsets;
90 }
91 for (std::size_t j = 0; j < i; ++j) {
92 if (offsets_[i] == offsets_[j]) {
93 throw std::invalid_argument("ObservationWindow does not permit duplicate offsets.");
94 }
95 }
96 }
97 if (numZeroOffsets != 1) {
98 throw std::invalid_argument("ObservationWindow requires the zero offset exactly once.");
99 }
100 }
101};
102
105 public:
107 BinaryVisibilityState(std::uint32_t bits, std::size_t coordinateCount) : bits_(bits), coordinateCount_(coordinateCount) {
108 if (coordinateCount_ == 0) {
109 throw std::invalid_argument("BinaryVisibilityState requires at least one coordinate.");
110 }
111 if (coordinateCount_ > ObservationWindow::maxNumOffsets) {
112 throw std::invalid_argument("BinaryVisibilityState supports at most 32 coordinates.");
113 }
114 const std::uint32_t validMask = coordinateCount_ == 32 ? std::numeric_limits<std::uint32_t>::max()
115 : (coordinateCount_ == 0 ? 0 : (std::uint32_t{1} << coordinateCount_) - std::uint32_t{1});
116 if ((bits_ & ~validMask) != 0) {
117 throw std::invalid_argument("BinaryVisibilityState contains bits outside its coordinate domain.");
118 }
119 }
120
122 [[nodiscard]] std::uint32_t bits() const noexcept { return bits_; }
124 [[nodiscard]] std::size_t coordinateCount() const noexcept { return coordinateCount_; }
125
127 [[nodiscard]] bool isVisible(std::size_t coordinate) const {
128 if (coordinate >= coordinateCount_) {
129 throw std::out_of_range("BinaryVisibilityState coordinate is outside the observation window.");
130 }
131 return (bits_ & (std::uint32_t{1} << coordinate)) != 0;
132 }
133
135 [[nodiscard]] BinaryVisibilityState withEnteringOffsets(std::uint32_t enteringOffsetMask) const {
136 return BinaryVisibilityState(bits_ | enteringOffsetMask, coordinateCount_);
137 }
138
140 friend bool operator==(const BinaryVisibilityState& lhs, const BinaryVisibilityState& rhs) = default;
141
142 private:
143 struct UncheckedConstructionTag {};
144
150 BinaryVisibilityState(UncheckedConstructionTag, std::uint32_t bits, std::size_t coordinateCount) noexcept
151 : bits_(bits), coordinateCount_(coordinateCount) {}
152
153 std::uint32_t bits_ = 0;
154 std::size_t coordinateCount_ = 0;
155
156 friend struct detail::BinaryVisibilityStateAccess;
157};
158
162 std::uint32_t enteringOffsetMask = 0;
163
165 friend bool operator==(const AnchoredEntryMask& lhs, const AnchoredEntryMask& rhs) = default;
166};
167
170 public:
172 AnchoredEntryMap(PixelId anchorPixel, std::vector<std::optional<NodeId>> entries) : anchorPixel_(anchorPixel), entries_(std::move(entries)) {}
173
175 [[nodiscard]] PixelId anchorPixel() const noexcept { return anchorPixel_; }
177 [[nodiscard]] std::size_t size() const noexcept { return entries_.size(); }
179 [[nodiscard]] const std::optional<NodeId>& operator[](std::size_t coordinate) const noexcept { return entries_[coordinate]; }
181 [[nodiscard]] std::span<const std::optional<NodeId>> entries() const noexcept { return entries_; }
182
183 private:
184 PixelId anchorPixel_ = InvalidPixel;
185 std::vector<std::optional<NodeId>> entries_;
186};
187
188using AnchorBranch = std::vector<NodeId>;
189using AnchoredEntrySet = std::set<NodeId>;
190using OrderedAnchoredEntries = std::vector<AnchoredEntryMask>;
191
201
206template <class Value> struct LocalAttributeIncrement {
209};
210
215template <class Value> struct NodeAttribute {
218};
219
229template <class Rule>
230concept LocalRule = requires(const Rule& rule, BinaryVisibilityState state, typename Rule::Value& target, const typename Rule::Value& source) {
231 typename Rule::Value;
232 { rule.additiveIdentity() } -> std::same_as<typename Rule::Value>;
233 { rule.evaluateLocalRule(state) } -> std::same_as<typename Rule::Value>;
234 { rule.addAssign(target, source) } -> std::same_as<void>;
235 { rule.subtractAssign(target, source) } -> std::same_as<void>;
236};
237
238namespace detail {
239
247 [[nodiscard]] static BinaryVisibilityState zero(std::size_t coordinateCount) noexcept {
248 return BinaryVisibilityState(BinaryVisibilityState::UncheckedConstructionTag{}, 0, coordinateCount);
249 }
250
256 static void addEnteringOffsets(BinaryVisibilityState& state, std::uint32_t enteringOffsetMask) noexcept {
257 state.bits_ |= enteringOffsetMask;
258 }
259};
260
261inline void validateAnchoredEntryMap(const MorphologicalTree& tree, const AnchoredEntryMap& entryMap) {
262 if (entryMap.size() == 0 || entryMap.size() > ObservationWindow::maxNumOffsets) {
263 throw std::invalid_argument("AnchoredEntryMap size must match a valid observation-window state domain.");
264 }
265 if (entryMap.anchorPixel() < 0 || entryMap.anchorPixel() >= tree.numPixels()) {
266 throw std::invalid_argument("AnchoredEntryMap requires a valid anchor pixel.");
267 }
268 const NodeId anchorSmallestNode = tree.smallestNode(entryMap.anchorPixel());
269 if (!tree.isAlive(anchorSmallestNode)) {
270 throw std::invalid_argument("AnchoredEntryMap requires an anchor pixel with a live smallest node.");
271 }
272 for (const std::optional<NodeId>& entry : entryMap.entries()) {
273 if (entry.has_value() && (!tree.isAlive(*entry) || !tree.isAncestor(*entry, anchorSmallestNode))) {
274 throw std::invalid_argument("Every anchored entry must be a live node on the anchor branch.");
275 }
276 }
277}
278
279inline void validateFiniteWindowLocalAttributeInput(const MorphologicalTree& tree) {
280 const GridDomain2D& domain = tree.requireGridDomain2D("FiniteWindowLocalAttributeComputer");
281 if (domain.rows <= 0 || domain.columns <= 0) {
282 throw std::invalid_argument("FiniteWindowLocalAttributeComputer requires a non-empty 2D image domain.");
283 }
284 if (!tree.isAlive(tree.root())) {
285 throw std::invalid_argument("FiniteWindowLocalAttributeComputer requires a live tree root.");
286 }
287}
288
289template <class Value>
290inline void validateLocalAttributeIncrements(const MorphologicalTree& tree, std::span<const LocalAttributeIncrement<Value>> localAttributeIncrements) {
291 if (localAttributeIncrements.size() != static_cast<std::size_t>(tree.numInternalNodeSlots())) {
292 throw std::invalid_argument("Local-attribute increments must cover every dense internal node slot.");
293 }
294 for (std::size_t slot = 0; slot < localAttributeIncrements.size(); ++slot) {
295 if (localAttributeIncrements[slot].node != static_cast<NodeId>(slot)) {
296 throw std::invalid_argument("Local-attribute increments must be ordered by dense node slot.");
297 }
298 }
299}
300
301namespace kernel {
302
303inline NodeId anchoredEntryFromSmallestNodes(const MorphologicalTree& tree, NodeId anchorSmallestNode, NodeId sampleSmallestNode) {
304 if (::mmcfilters::detail::CommittedTreeAccess::isAncestor(tree, anchorSmallestNode, sampleSmallestNode)) {
305 return anchorSmallestNode;
306 }
307 if (::mmcfilters::detail::CommittedTreeAccess::isAncestor(tree, sampleSmallestNode, anchorSmallestNode)) {
308 return sampleSmallestNode;
309 }
310 return ::mmcfilters::detail::CommittedTreeAccess::lowestCommonAncestor(tree, anchorSmallestNode, sampleSmallestNode);
311}
312
313inline NodeId anchoredEntry(const MorphologicalTree& tree, PixelId anchorPixel, PixelId samplePixel) {
314 const NodeId anchorSmallestNode = ::mmcfilters::detail::CommittedTreeAccess::smallestNodeMap(tree, anchorPixel);
315 const NodeId sampleSmallestNode = ::mmcfilters::detail::CommittedTreeAccess::smallestNodeMap(tree, samplePixel);
316 return anchoredEntryFromSmallestNodes(tree, anchorSmallestNode, sampleSmallestNode);
317}
318
319inline NodeId anchoredEntry(const MorphologicalTree& tree, PixelId anchorPixel, WindowOffset offset) {
320 const GridDomain2D& domain = ::mmcfilters::detail::CommittedTreeAccess::gridDomain2D(tree);
321 const int anchorRow = anchorPixel / domain.columns;
322 const int anchorColumn = anchorPixel % domain.columns;
323 const int sampleRow = anchorRow + offset.rowOffset;
324 const int sampleColumn = anchorColumn + offset.columnOffset;
325 if (sampleRow < 0 || sampleRow >= domain.rows || sampleColumn < 0 || sampleColumn >= domain.columns) {
326 return InvalidNode;
327 }
328 return anchoredEntry(tree, anchorPixel, static_cast<PixelId>(sampleRow * domain.columns + sampleColumn));
329}
330
331inline AnchoredEntryMap anchoredEntryMap(const MorphologicalTree& tree, PixelId anchorPixel, const ObservationWindow& observationWindow) {
332 std::vector<std::optional<NodeId>> entries;
333 entries.reserve(observationWindow.size());
334 for (WindowOffset windowOffset : observationWindow) {
335 const NodeId entry = anchoredEntry(tree, anchorPixel, windowOffset);
336 entries.push_back(entry == InvalidNode ? std::nullopt : std::optional<NodeId>{entry});
337 }
338 return AnchoredEntryMap(anchorPixel, std::move(entries));
339}
340
341inline OrderedAnchoredEntries orderedAnchoredEntries(const MorphologicalTree& tree, const AnchoredEntryMap& entryMap) {
342 OrderedAnchoredEntries entries;
343 entries.reserve(entryMap.size());
344 for (std::size_t coordinate = 0; coordinate < entryMap.size(); ++coordinate) {
345 if (entryMap[coordinate].has_value()) {
346 entries.push_back({*entryMap[coordinate], std::uint32_t{1} << coordinate});
347 }
348 }
349
350 std::sort(entries.begin(), entries.end(), [&](const AnchoredEntryMask& lhs, const AnchoredEntryMask& rhs) {
351 if (lhs.node == rhs.node) {
352 return false;
353 }
354 return ::mmcfilters::detail::CommittedTreeAccess::isAncestor(tree, rhs.node, lhs.node);
355 });
356
357 OrderedAnchoredEntries grouped;
358 grouped.reserve(entries.size());
359 for (const AnchoredEntryMask& entry : entries) {
360 if (!grouped.empty() && grouped.back().node == entry.node) {
361 grouped.back().enteringOffsetMask |= entry.enteringOffsetMask;
362 } else {
363 grouped.push_back(entry);
364 }
365 }
366 return grouped;
367}
368
369using AnchoredEntryScratch = std::array<AnchoredEntryMask, ObservationWindow::maxNumOffsets>;
370
380inline std::size_t fillOrderedAnchoredEntries(const MorphologicalTree& tree, const GridDomain2D& domain, PixelId anchorPixel,
381 const ObservationWindow& observationWindow, AnchoredEntryScratch& scratch) {
382 const int anchorRow = anchorPixel / domain.columns;
383 const int anchorColumn = anchorPixel % domain.columns;
384 const NodeId anchorSmallestNode = ::mmcfilters::detail::CommittedTreeAccess::smallestNodeMap(tree, anchorPixel);
385
386 std::size_t numEntries = 0;
387 for (std::size_t coordinate = 0; coordinate < observationWindow.size(); ++coordinate) {
388 const WindowOffset offset = observationWindow[coordinate];
389 const int sampleRow = anchorRow + offset.rowOffset;
390 const int sampleColumn = anchorColumn + offset.columnOffset;
391 if (sampleRow < 0 || sampleRow >= domain.rows || sampleColumn < 0 || sampleColumn >= domain.columns) {
392 continue;
393 }
394 const PixelId samplePixel = static_cast<PixelId>(sampleRow * domain.columns + sampleColumn);
395 const NodeId sampleSmallestNode = ::mmcfilters::detail::CommittedTreeAccess::smallestNodeMap(tree, samplePixel);
396 const NodeId entry = anchoredEntryFromSmallestNodes(tree, anchorSmallestNode, sampleSmallestNode);
397 scratch[numEntries++] = {entry, std::uint32_t{1} << coordinate};
398 }
399
400 std::sort(scratch.begin(), scratch.begin() + static_cast<std::ptrdiff_t>(numEntries), [&](const AnchoredEntryMask& lhs, const AnchoredEntryMask& rhs) {
401 if (lhs.node == rhs.node) {
402 return false;
403 }
404 return ::mmcfilters::detail::CommittedTreeAccess::isAncestor(tree, rhs.node, lhs.node);
405 });
406
407 std::size_t numGroupedEntries = 0;
408 for (std::size_t index = 0; index < numEntries; ++index) {
409 const AnchoredEntryMask entry = scratch[index];
410 if (numGroupedEntries > 0 && scratch[numGroupedEntries - 1].node == entry.node) {
411 scratch[numGroupedEntries - 1].enteringOffsetMask |= entry.enteringOffsetMask;
412 } else {
413 scratch[numGroupedEntries++] = entry;
414 }
415 }
416 return numGroupedEntries;
417}
418
419template <LocalRule Rule, class Consumer>
420inline void visitEventDeltas(std::span<const AnchoredEntryMask> entries, std::size_t coordinateCount, const Rule& localRule, Consumer&& consumer) {
421 using Value = typename Rule::Value;
422 BinaryVisibilityState visibilityState = BinaryVisibilityStateAccess::zero(coordinateCount);
423 Value previousRuleValue = localRule.additiveIdentity();
424 bool hasPreviousRuleValue = false;
425 for (const AnchoredEntryMask& entry : entries) {
426 BinaryVisibilityStateAccess::addEnteringOffsets(visibilityState, entry.enteringOffsetMask);
427 Value currentRuleValue = localRule.evaluateLocalRule(visibilityState);
428 Value eventDelta = currentRuleValue;
429 if (hasPreviousRuleValue) {
430 localRule.subtractAssign(eventDelta, previousRuleValue);
431 }
432 consumer(entry.node, std::move(eventDelta));
433 previousRuleValue = std::move(currentRuleValue);
434 hasPreviousRuleValue = true;
435 }
436}
437
438template <LocalRule Rule>
439inline std::vector<EventDelta<typename Rule::Value>> computeEventDeltas(const MorphologicalTree& tree, PixelId anchorPixel,
440 const ObservationWindow& observationWindow, const Rule& localRule) {
441 using Value = typename Rule::Value;
442 const GridDomain2D& domain = ::mmcfilters::detail::CommittedTreeAccess::gridDomain2D(tree);
443 AnchoredEntryScratch entryScratch;
444 const std::size_t numEntries = fillOrderedAnchoredEntries(tree, domain, anchorPixel, observationWindow, entryScratch);
445 const std::span<const AnchoredEntryMask> entries(entryScratch.data(), numEntries);
446
447 std::vector<EventDelta<Value>> eventDeltas;
448 eventDeltas.reserve(entries.size());
449 visitEventDeltas(entries, observationWindow.size(), localRule,
450 [&](NodeId entry, Value&& eventDelta) { eventDeltas.push_back({anchorPixel, entry, std::move(eventDelta)}); });
451 return eventDeltas;
452}
453
454template <LocalRule Rule>
455inline void accumulateLocalAttributeIncrementValues(const MorphologicalTree& tree, const ObservationWindow& observationWindow, const Rule& localRule,
456 std::span<typename Rule::Value> localAttributeIncrementValues) {
457 using Value = typename Rule::Value;
458 const GridDomain2D& domain = ::mmcfilters::detail::CommittedTreeAccess::gridDomain2D(tree);
459 const int totalPixels = domain.rows * domain.columns;
460 AnchoredEntryScratch entryScratch;
461 for (PixelId anchorPixel = 0; anchorPixel < totalPixels; ++anchorPixel) {
462 const std::size_t numEntries = fillOrderedAnchoredEntries(tree, domain, anchorPixel, observationWindow, entryScratch);
463 const std::span<const AnchoredEntryMask> entries(entryScratch.data(), numEntries);
464 visitEventDeltas(entries, observationWindow.size(), localRule, [&](NodeId entry, Value&& eventDelta) {
465 localRule.addAssign(localAttributeIncrementValues[static_cast<std::size_t>(entry)], eventDelta);
466 });
467 }
468}
469
470template <LocalRule Rule>
471inline std::vector<typename Rule::Value> computeLocalAttributeIncrementValues(const MorphologicalTree& tree, const ObservationWindow& observationWindow,
472 const Rule& localRule) {
473 using Value = typename Rule::Value;
474 std::vector<Value> localAttributeIncrementValues;
475 localAttributeIncrementValues.reserve(static_cast<std::size_t>(tree.numInternalNodeSlots()));
476 for (NodeId node = 0; node < tree.numInternalNodeSlots(); ++node) {
477 localAttributeIncrementValues.push_back(localRule.additiveIdentity());
478 }
479 accumulateLocalAttributeIncrementValues(tree, observationWindow, localRule, localAttributeIncrementValues);
480 return localAttributeIncrementValues;
481}
482
483template <LocalRule Rule>
484inline std::vector<LocalAttributeIncrement<typename Rule::Value>>
485computeLocalAttributeIncrements(const MorphologicalTree& tree, const ObservationWindow& observationWindow, const Rule& localRule) {
486 using Value = typename Rule::Value;
487 std::vector<Value> localAttributeIncrementValues = computeLocalAttributeIncrementValues(tree, observationWindow, localRule);
488 std::vector<LocalAttributeIncrement<Value>> localAttributeIncrements;
489 localAttributeIncrements.reserve(localAttributeIncrementValues.size());
490 for (NodeId node = 0; node < tree.numInternalNodeSlots(); ++node) {
491 localAttributeIncrements.push_back({node, std::move(localAttributeIncrementValues[static_cast<std::size_t>(node)])});
492 }
493 return localAttributeIncrements;
494}
495
496template <LocalRule Rule>
497inline void aggregateLocalAttributeIncrementValues(const MorphologicalTree& tree, std::span<typename Rule::Value> localAttributeIncrementValues,
498 const Rule& localRule) {
499 ::mmcfilters::detail::kernel::traversePostOrder(
500 tree, tree.root(), [](NodeId) {},
501 [&](NodeId parent, NodeId child) {
502 localRule.addAssign(localAttributeIncrementValues[static_cast<std::size_t>(parent)],
503 localAttributeIncrementValues[static_cast<std::size_t>(child)]);
504 },
505 [](NodeId) {});
506}
507
508template <LocalRule Rule>
509inline std::vector<NodeAttribute<typename Rule::Value>>
510aggregateLocalAttributeIncrements(const MorphologicalTree& tree, std::span<const LocalAttributeIncrement<typename Rule::Value>> localAttributeIncrements,
511 const Rule& localRule) {
512 using Value = typename Rule::Value;
513 std::vector<NodeAttribute<Value>> nodeAttributes;
514 nodeAttributes.reserve(localAttributeIncrements.size());
515 for (const LocalAttributeIncrement<Value>& localAttributeIncrement : localAttributeIncrements) {
516 nodeAttributes.push_back({localAttributeIncrement.node, localAttributeIncrement.value});
517 }
518
519 ::mmcfilters::detail::kernel::traversePostOrder(
520 tree, tree.root(), [](NodeId) {},
521 [&](NodeId parent, NodeId child) {
522 localRule.addAssign(nodeAttributes[static_cast<std::size_t>(parent)].value, nodeAttributes[static_cast<std::size_t>(child)].value);
523 },
524 [](NodeId) {});
525 return nodeAttributes;
526}
527
528template <LocalRule Rule>
529inline std::vector<NodeAttribute<typename Rule::Value>> computeFiniteWindowLocalAttribute(const MorphologicalTree& tree,
530 const ObservationWindow& observationWindow, const Rule& localRule) {
531 using Value = typename Rule::Value;
532 std::vector<Value> localAttributeIncrementValues = computeLocalAttributeIncrementValues(tree, observationWindow, localRule);
533 aggregateLocalAttributeIncrementValues(tree, localAttributeIncrementValues, localRule);
534
535 std::vector<NodeAttribute<Value>> nodeAttributes;
536 nodeAttributes.reserve(localAttributeIncrementValues.size());
537 for (NodeId node = 0; node < tree.numInternalNodeSlots(); ++node) {
538 nodeAttributes.push_back({node, std::move(localAttributeIncrementValues[static_cast<std::size_t>(node)])});
539 }
540 return nodeAttributes;
541}
542
543} // namespace kernel
544} // namespace detail
545
548 public:
551 [[nodiscard]] static std::optional<NodeId> anchoredEntry(const MorphologicalTree& tree, PixelId anchorPixel, PixelId samplePixel) {
553 return std::nullopt;
554 }
555 const NodeId anchorSmallestNode = tree.smallestNode(anchorPixel);
558 return std::nullopt;
559 }
560 const NodeId entry = detail::kernel::anchoredEntry(tree, anchorPixel, samplePixel);
561 return entry == InvalidNode ? std::nullopt : std::optional<NodeId>{entry};
562 }
563
566 [[nodiscard]] static std::optional<NodeId> anchoredEntry(const MorphologicalTree& tree, PixelId anchorPixel, WindowOffset windowOffset) {
567 const GridDomain2D& domain = tree.requireGridDomain2D("FiniteWindowLocalAttributeComputer::anchoredEntry");
568 if (anchorPixel < 0 || anchorPixel >= domain.rows * domain.columns) {
569 return std::nullopt;
570 }
571 const NodeId entry = detail::kernel::anchoredEntry(tree, anchorPixel, windowOffset);
572 return entry == InvalidNode ? std::nullopt : std::optional<NodeId>{entry};
573 }
574
577 [[nodiscard]] static AnchorBranch anchorBranch(const MorphologicalTree& tree, PixelId anchorPixel) {
579 return {};
580 }
581 NodeId node = tree.smallestNode(anchorPixel);
582 if (!tree.isAlive(node)) {
583 return {};
584 }
585
586 AnchorBranch branch;
587 while (true) {
588 branch.push_back(node);
589 if (node == tree.root()) {
590 break;
591 }
592 node = tree.parent(node);
593 }
594 return branch;
595 }
596
600 detail::validateFiniteWindowLocalAttributeInput(tree);
602 return AnchoredEntryMap(anchorPixel, std::vector<std::optional<NodeId>>(observationWindow.size()));
603 }
604 return detail::kernel::anchoredEntryMap(tree, anchorPixel, observationWindow);
605 }
606
609 [[nodiscard]] static AnchoredEntrySet anchoredEntrySet(const MorphologicalTree& tree, PixelId anchorPixel, const ObservationWindow& observationWindow) {
610 AnchoredEntrySet entries;
611 const AnchoredEntryMap entryMap = anchoredEntryMap(tree, anchorPixel, observationWindow);
612 for (const std::optional<NodeId>& entry : entryMap.entries()) {
613 if (entry.has_value()) {
614 entries.insert(*entry);
615 }
616 }
617 return entries;
618 }
619
622 [[nodiscard]] static OrderedAnchoredEntries orderedAnchoredEntries(const MorphologicalTree& tree, PixelId anchorPixel,
624 const AnchoredEntryMap entryMap = anchoredEntryMap(tree, anchorPixel, observationWindow);
625 return detail::kernel::orderedAnchoredEntries(tree, entryMap);
626 }
627
630 [[nodiscard]] static OrderedAnchoredEntries orderAnchoredEntriesByInclusion(const MorphologicalTree& tree, const AnchoredEntryMap& entryMap) {
631 detail::validateAnchoredEntryMap(tree, entryMap);
632 return detail::kernel::orderedAnchoredEntries(tree, entryMap);
633 }
634
638 std::uint32_t stateBits = 0;
639 if (entryMap.size() == 0) {
640 throw std::invalid_argument("Binary visibility state requires a non-empty anchored-entry map.");
641 }
642 detail::validateAnchoredEntryMap(tree, entryMap);
643 if (!tree.isAlive(node)) {
644 throw std::invalid_argument("Binary visibility state requires a live node.");
645 }
646 if (entryMap.anchorPixel() < 0 || entryMap.anchorPixel() >= tree.numPixels()) {
647 throw std::invalid_argument("Binary visibility state requires a valid anchor pixel.");
648 }
649 const NodeId anchorSmallestNode = tree.smallestNode(entryMap.anchorPixel());
650 if (!tree.isAlive(anchorSmallestNode) || !tree.isAncestor(node, anchorSmallestNode)) {
651 throw std::invalid_argument("Binary visibility state is defined only on the anchor branch.");
652 }
653 for (std::size_t coordinate = 0; coordinate < entryMap.size(); ++coordinate) {
654 if (entryMap[coordinate].has_value() && tree.isAncestor(node, *entryMap[coordinate])) {
655 stateBits |= std::uint32_t{1} << coordinate;
656 }
657 }
659 }
660
664 template <LocalRule Rule>
665 [[nodiscard]] static std::vector<EventDelta<typename Rule::Value>> computeEventDeltas(const MorphologicalTree& tree, PixelId anchorPixel,
667 detail::validateFiniteWindowLocalAttributeInput(tree);
669 throw std::invalid_argument("Event-delta computation requires a valid anchor pixel.");
670 }
671 return detail::kernel::computeEventDeltas(tree, anchorPixel, observationWindow, localRule);
672 }
673
677 template <LocalRule Rule>
678 [[nodiscard]] static std::vector<LocalAttributeIncrement<typename Rule::Value>>
680 MMCFILTERS_CONTRACT_CHECKED_ONLY(detail::validateFiniteWindowLocalAttributeInput(tree));
681 return detail::kernel::computeLocalAttributeIncrements(tree, observationWindow, localRule);
682 }
683
687 template <LocalRule Rule>
688 [[nodiscard]] static std::vector<NodeAttribute<typename Rule::Value>>
690 const Rule& localRule) {
691 detail::validateLocalAttributeIncrements(tree, localAttributeIncrements);
692 return detail::kernel::aggregateLocalAttributeIncrements(tree, localAttributeIncrements, localRule);
693 }
694
698 template <LocalRule Rule>
699 [[nodiscard]] static std::vector<NodeAttribute<typename Rule::Value>> compute(const MorphologicalTree& tree, const ObservationWindow& observationWindow,
700 const Rule& localRule) {
701 MMCFILTERS_CONTRACT_CHECKED_ONLY(detail::validateFiniteWindowLocalAttributeInput(tree));
702 return detail::kernel::computeFiniteWindowLocalAttribute(tree, observationWindow, localRule);
703 }
704};
705
706} // namespace mmcfilters::local_attributes
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
#define MMCFILTERS_CONTRACT_CHECKED_ONLY(...)
Executes validation statements only when defensive checks are enabled.
Definition Contract.hpp:67
Mutable connected-subset tree on a finite pixel domain.
bool isAlive(NodeId nodeId) const
Tests whether a node slot currently represents a live node.
int numPixels() const
Returns the cardinality of the pixel domain.
NodeId smallestNode(PixelId pixel) const
Returns the inclusion-smallest node containing pixel.
bool isAncestor(NodeId u, NodeId v) const
Tests whether u is an ancestor of v.
const GridDomain2D & requireGridDomain2D(const char *context) const
Returns the regular 2D domain or rejects a geometry-dependent call.
NodeId parent(NodeId nodeId) const
Returns the direct parent of nodeId.
NodeId root() const
Returns the current hierarchy root.
Window-coordinate map whose missing entries represent out-of-domain samples.
std::span< const std::optional< NodeId > > entries() const noexcept
Returns all optional entries in coordinate order.
const std::optional< NodeId > & operator[](std::size_t coordinate) const noexcept
Returns one optional entry.
std::size_t size() const noexcept
Returns the coordinate count.
PixelId anchorPixel() const noexcept
Returns the anchor pixel.
AnchoredEntryMap(PixelId anchorPixel, std::vector< std::optional< NodeId > > entries)
Creates a coordinate-preserving map.
Binary sample-visibility vector encoded in observation-window order.
BinaryVisibilityState withEnteringOffsets(std::uint32_t enteringOffsetMask) const
Adds all coordinates entering at one node.
bool isVisible(std::size_t coordinate) const
Tests one visibility coordinate.
std::uint32_t bits() const noexcept
Returns the packed visibility bits.
friend bool operator==(const BinaryVisibilityState &lhs, const BinaryVisibilityState &rhs)=default
Compares state bits and coordinate domains.
std::size_t coordinateCount() const noexcept
Returns the state-coordinate count.
BinaryVisibilityState(std::uint32_t bits, std::size_t coordinateCount)
Creates a state over a fixed coordinate domain.
Generic finite-window local-attribute computation over a connected-subset tree model.
static AnchoredEntrySet anchoredEntrySet(const MorphologicalTree &tree, PixelId anchorPixel, const ObservationWindow &observationWindow)
Materializes the distinct anchored entries.
static std::vector< EventDelta< typename Rule::Value > > computeEventDeltas(const MorphologicalTree &tree, PixelId anchorPixel, const ObservationWindow &observationWindow, const Rule &localRule)
Computes the rule changes for one anchor.
static OrderedAnchoredEntries orderedAnchoredEntries(const MorphologicalTree &tree, PixelId anchorPixel, const ObservationWindow &observationWindow)
Groups and orders entries from the smallest node toward the root.
static std::vector< NodeAttribute< typename Rule::Value > > aggregateLocalAttributeIncrements(const MorphologicalTree &tree, std::span< const LocalAttributeIncrement< typename Rule::Value > > localAttributeIncrements, const Rule &localRule)
Aggregates node increments from children into parents.
static AnchoredEntryMap anchoredEntryMap(const MorphologicalTree &tree, PixelId anchorPixel, const ObservationWindow &observationWindow)
Materializes anchored entries in window-coordinate order.
static std::optional< NodeId > anchoredEntry(const MorphologicalTree &tree, PixelId anchorPixel, WindowOffset windowOffset)
Locates one translated sample on the anchor branch.
static std::optional< NodeId > anchoredEntry(const MorphologicalTree &tree, PixelId anchorPixel, PixelId samplePixel)
Locates a valid absolute sample on the anchor branch.
static std::vector< LocalAttributeIncrement< typename Rule::Value > > computeLocalAttributeIncrements(const MorphologicalTree &tree, const ObservationWindow &observationWindow, const Rule &localRule)
Sums all anchor-specific event deltas into dense node increments.
static AnchorBranch anchorBranch(const MorphologicalTree &tree, PixelId anchorPixel)
Materializes the branch from the smallest node to the root.
static OrderedAnchoredEntries orderAnchoredEntriesByInclusion(const MorphologicalTree &tree, const AnchoredEntryMap &entryMap)
Groups and orders an existing entry map by increasing inclusion.
static BinaryVisibilityState binaryVisibilityState(const MorphologicalTree &tree, const AnchoredEntryMap &entryMap, NodeId node)
Evaluates visibility at one node of the anchor branch.
static std::vector< NodeAttribute< typename Rule::Value > > compute(const MorphologicalTree &tree, const ObservationWindow &observationWindow, const Rule &localRule)
Computes the final node attribute induced by a finite window and additive rule.
Indexed finite set of translated-sample offsets.
ObservationWindow(std::vector< WindowOffset > offsets)
Creates and validates an ordered window.
const WindowOffset & operator[](std::size_t index) const noexcept
Returns one offset in semantic coordinate order.
std::span< const WindowOffset > offsets() const noexcept
Returns all offsets in semantic coordinate order.
ObservationWindow(const std::array< WindowOffset, N > &offsets)
Creates and validates an ordered fixed-size window.
auto end() const noexcept
Ends ordered offset iteration.
ObservationWindow(std::initializer_list< WindowOffset > offsets)
Creates and validates an ordered window.
std::size_t size() const noexcept
Returns the number of state coordinates.
static constexpr std::size_t maxNumOffsets
Maximum coordinate count supported by the visibility mask.
auto begin() const noexcept
Starts ordered offset iteration.
Compile-time contract for a rule valued in an additive Abelian group.
Owning result for one computed scalar attribute layout and buffer.
Shape metadata optionally attached to the pixel domain.
int columns
Number of grid columns.
int rows
Number of grid rows.
One inclusion node and the observation coordinates that enter there.
NodeId node
Anchored entry shared by the grouped coordinates.
friend bool operator==(const AnchoredEntryMask &lhs, const AnchoredEntryMask &rhs)=default
Compares entry node and coordinate mask.
std::uint32_t enteringOffsetMask
Coordinates that first become visible at node.
One anchor-specific local-rule change attached to an anchored entry.
NodeId anchoredEntry
Entry node to which the difference is attached.
PixelId anchorPixel
Anchor whose visibility transition produced the difference.
Value value
Signed local-rule difference at the entry.
Sum of all finite-window event deltas attached to one node.
Value value
Pre-aggregation contribution introduced at the node.
Final value of one finite-window node attribute after bottom-up aggregation.
NodeId node
Dense node slot represented by this record.
Relative row-column offset of one observation-window sample.
friend bool operator==(const WindowOffset &lhs, const WindowOffset &rhs)=default
Compares both coordinate displacements.
Trusted state operations used only after observation-window validation.
static BinaryVisibilityState zero(std::size_t coordinateCount) noexcept
Creates the zero state for an already validated coordinate domain.
static void addEnteringOffsets(BinaryVisibilityState &state, std::uint32_t enteringOffsetMask) noexcept
Activates a mask produced from coordinates of the validated observation window.