3#include "ConnectedSubsetTreeLocalizer.hpp"
4#include "LocalEventModel.hpp"
5#include "../trees/MorphologicalTree.hpp"
6#include "../trees/detail/CommittedTreeAccess.hpp"
7#include "../utils/Contract.hpp"
19namespace mmcfilters::local_attributes {
32 state.bits_ |= enteringOffsetMask;
38 throw std::invalid_argument(
"AnchoredEntryMap size must match a valid observation-window state domain.");
41 throw std::invalid_argument(
"AnchoredEntryMap requires a valid anchor pixel.");
44 if (!tree.
isAlive(anchorSmallestNode)) {
45 throw std::invalid_argument(
"AnchoredEntryMap requires an anchor pixel with a live smallest node.");
47 for (
const std::optional<NodeId>& entry : entryMap.entries()) {
48 if (entry.has_value() && (!tree.
isAlive(*entry) || !tree.
isAncestor(*entry, anchorSmallestNode))) {
49 throw std::invalid_argument(
"Every anchored entry must be a live node on the anchor branch.");
54inline void validateFiniteWindowLocalAttributeInput(
const MorphologicalTree& tree) {
55 const GridDomain2D& domain = tree.requireGridDomain2D(
"FiniteWindowLocalAttributeComputer");
56 if (domain.rows <= 0 || domain.columns <= 0) {
57 throw std::invalid_argument(
"FiniteWindowLocalAttributeComputer requires a non-empty 2D image domain.");
59 if (!tree.isAlive(tree.root())) {
60 throw std::invalid_argument(
"FiniteWindowLocalAttributeComputer requires a live tree root.");
65inline void validateLocalAttributeIncrements(
const MorphologicalTree& tree, std::span<
const LocalAttributeIncrement<Value>> increments) {
66 if (increments.size() !=
static_cast<std::size_t
>(tree.numInternalNodeSlots())) {
67 throw std::invalid_argument(
"Local-attribute increments must cover every dense internal node slot.");
69 for (std::size_t slot = 0; slot < increments.size(); ++slot) {
70 if (increments[slot].node !=
static_cast<NodeId>(slot)) {
71 throw std::invalid_argument(
"Local-attribute increments must be ordered by dense node slot.");
78inline AnchoredEntryMap anchoredEntryMap(
const MorphologicalTree& tree, PixelId anchorPixel,
const ObservationWindow& window) {
79 std::vector<std::optional<NodeId>> entries;
80 entries.reserve(window.size());
81 for (WindowOffset offset : window) {
82 const NodeId entry = anchoredEntry(tree, anchorPixel, offset);
83 entries.push_back(entry == InvalidNode ? std::nullopt : std::optional<
NodeId>{entry});
85 return AnchoredEntryMap(anchorPixel, std::move(entries));
88inline OrderedAnchoredEntries orderedAnchoredEntries(
const MorphologicalTree& tree,
const AnchoredEntryMap& entryMap) {
89 OrderedAnchoredEntries entries;
90 entries.reserve(entryMap.size());
91 for (std::size_t coordinate = 0; coordinate < entryMap.size(); ++coordinate) {
92 if (entryMap[coordinate].has_value()) {
93 entries.push_back({*entryMap[coordinate], std::uint32_t{1} << coordinate});
97 std::sort(entries.begin(), entries.end(), [&](
const AnchoredEntryMask& lhs,
const AnchoredEntryMask& rhs) {
98 if (lhs.node == rhs.node) {
101 return ::mmcfilters::detail::CommittedTreeAccess::isAncestor(tree, rhs.node, lhs.node);
104 OrderedAnchoredEntries grouped;
105 grouped.reserve(entries.size());
106 for (
const AnchoredEntryMask& entry : entries) {
107 if (!grouped.empty() && grouped.back().node == entry.node) {
108 grouped.back().enteringOffsetMask |= entry.enteringOffsetMask;
110 grouped.push_back(entry);
116using AnchoredEntryScratch = std::array<AnchoredEntryMask, ObservationWindow::maxNumOffsets>;
118inline std::size_t fillOrderedAnchoredEntries(
const MorphologicalTree& tree,
const GridDomain2D& domain, PixelId anchorPixel,
119 const ObservationWindow& window, AnchoredEntryScratch& scratch) {
120 const int anchorRow = anchorPixel / domain.columns;
121 const int anchorColumn = anchorPixel % domain.columns;
122 const NodeId anchorSmallestNode = ::mmcfilters::detail::CommittedTreeAccess::smallestNodeMap(tree, anchorPixel);
124 std::size_t numEntries = 0;
125 for (std::size_t coordinate = 0; coordinate < window.size(); ++coordinate) {
126 const WindowOffset offset = window[coordinate];
127 const int sampleRow = anchorRow + offset.rowOffset;
128 const int sampleColumn = anchorColumn + offset.columnOffset;
129 if (sampleRow < 0 || sampleRow >= domain.rows || sampleColumn < 0 || sampleColumn >= domain.columns) {
132 const PixelId samplePixel =
static_cast<PixelId>(sampleRow * domain.columns + sampleColumn);
133 const NodeId sampleSmallestNode = ::mmcfilters::detail::CommittedTreeAccess::smallestNodeMap(tree, samplePixel);
134 scratch[numEntries++] = {connectedSubsetJoin(tree, anchorSmallestNode, sampleSmallestNode), std::uint32_t{1} << coordinate};
137 std::sort(scratch.begin(), scratch.begin() +
static_cast<std::ptrdiff_t
>(numEntries), [&](
const AnchoredEntryMask& lhs,
const AnchoredEntryMask& rhs) {
138 if (lhs.node == rhs.node) {
141 return ::mmcfilters::detail::CommittedTreeAccess::isAncestor(tree, rhs.node, lhs.node);
144 std::size_t numGroupedEntries = 0;
145 for (std::size_t index = 0; index < numEntries; ++index) {
146 const AnchoredEntryMask entry = scratch[index];
147 if (numGroupedEntries > 0 && scratch[numGroupedEntries - 1].node == entry.node) {
148 scratch[numGroupedEntries - 1].enteringOffsetMask |= entry.enteringOffsetMask;
150 scratch[numGroupedEntries++] = entry;
153 return numGroupedEntries;
156template <LocalDecision Decision,
class Algebra,
class Consumer>
157 requires EventAlgebra<Algebra, typename Decision::Value>
158inline void visitEventDeltas(std::span<const AnchoredEntryMask> entries, std::size_t coordinateCount,
const Decision& decision,
const Algebra& algebra,
159 Consumer&& consumer) {
160 using Value =
typename Decision::Value;
161 BinaryVisibilityState state = BinaryVisibilityStateAccess::zero(coordinateCount);
162 Value previousValue = algebra.additiveIdentity();
163 bool hasPreviousValue =
false;
164 for (
const AnchoredEntryMask& entry : entries) {
165 BinaryVisibilityStateAccess::addEnteringOffsets(state, entry.enteringOffsetMask);
166 Value currentValue = decision.evaluateLocalDecision(state);
167 Value delta = currentValue;
168 if (hasPreviousValue) {
169 algebra.subtractAssign(delta, previousValue);
171 consumer(entry.node, std::move(delta));
172 previousValue = std::move(currentValue);
173 hasPreviousValue =
true;
177template <LocalDecision Decision,
class Algebra>
178 requires EventAlgebra<Algebra, typename Decision::Value>
179inline std::vector<EventDelta<typename Decision::Value>> computeEventDeltas(
const MorphologicalTree& tree, PixelId anchorPixel,
180 const ObservationWindow& window,
const Decision& decision,
181 const Algebra& algebra) {
182 using Value =
typename Decision::Value;
183 const GridDomain2D& domain = ::mmcfilters::detail::CommittedTreeAccess::gridDomain2D(tree);
184 AnchoredEntryScratch scratch;
185 const std::size_t numEntries = fillOrderedAnchoredEntries(tree, domain, anchorPixel, window, scratch);
186 const std::span<const AnchoredEntryMask> entries(scratch.data(), numEntries);
188 std::vector<EventDelta<Value>> deltas;
189 deltas.reserve(entries.size());
190 visitEventDeltas(entries, window.size(), decision, algebra,
191 [&](NodeId entry, Value&& delta) { deltas.push_back({anchorPixel, entry, std::move(delta)}); });
195template <LocalDecision Decision,
class Algebra>
196 requires EventAlgebra<Algebra, typename Decision::Value>
197inline void accumulateLocalAttributeIncrementValues(
const MorphologicalTree& tree,
const ObservationWindow& window,
const Decision& decision,
198 const Algebra& algebra, std::span<typename Decision::Value> increments) {
199 using Value =
typename Decision::Value;
200 const GridDomain2D& domain = ::mmcfilters::detail::CommittedTreeAccess::gridDomain2D(tree);
201 const int totalPixels = domain.rows * domain.columns;
202 AnchoredEntryScratch scratch;
203 for (PixelId anchorPixel = 0; anchorPixel < totalPixels; ++anchorPixel) {
204 const std::size_t numEntries = fillOrderedAnchoredEntries(tree, domain, anchorPixel, window, scratch);
205 const std::span<const AnchoredEntryMask> entries(scratch.data(), numEntries);
206 visitEventDeltas(entries, window.size(), decision, algebra,
207 [&](NodeId entry, Value&& delta) { algebra.addAssign(increments[static_cast<std::size_t>(entry)], delta); });
211template <LocalDecision Decision,
class Algebra>
212 requires EventAlgebra<Algebra, typename Decision::Value>
213inline std::vector<typename Decision::Value> computeLocalAttributeIncrementValues(
const MorphologicalTree& tree,
const ObservationWindow& window,
214 const Decision& decision,
const Algebra& algebra) {
215 using Value =
typename Decision::Value;
216 std::vector<Value> increments;
217 increments.reserve(
static_cast<std::size_t
>(tree.numInternalNodeSlots()));
218 for (NodeId node = 0; node < tree.numInternalNodeSlots(); ++node) {
219 increments.push_back(algebra.additiveIdentity());
221 accumulateLocalAttributeIncrementValues(tree, window, decision, algebra, increments);
225template <LocalDecision Decision,
class Algebra>
226 requires EventAlgebra<Algebra, typename Decision::Value>
227inline std::vector<LocalAttributeIncrement<typename Decision::Value>> computeLocalAttributeIncrements(
const MorphologicalTree& tree,
228 const ObservationWindow& window,
229 const Decision& decision,
230 const Algebra& algebra) {
231 using Value =
typename Decision::Value;
232 std::vector<Value> values = computeLocalAttributeIncrementValues(tree, window, decision, algebra);
233 std::vector<LocalAttributeIncrement<Value>> increments;
234 increments.reserve(values.size());
235 for (NodeId node = 0; node < tree.numInternalNodeSlots(); ++node) {
236 increments.push_back({node, std::move(values[
static_cast<std::size_t
>(node)])});
258 template <LocalDecision Decision,
class Algebra>
263 detail::validateFiniteWindowLocalAttributeInput(tree);
265 throw std::invalid_argument(
"Event-delta computation requires a valid anchor pixel.");
280 template <LocalDecision Decision,
class Algebra>
282 [[
nodiscard]]
static std::vector<LocalAttributeIncrement<typename Decision::Value>>
int PixelId
Pixel identifier type used by source and active construction domains.
int NodeId
Node identifier type used throughout the project.
#define MMCFILTERS_CONTRACT_CHECKED_ONLY(...)
Executes validation statements only when defensive checks are enabled.
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 smallest node containing pixel.
bool isAncestor(NodeId u, NodeId v) const
Tests whether u is an ancestor of v.
Window-coordinate map whose missing entries represent out-of-domain samples.
PixelId anchorPixel() const noexcept
Returns the anchor pixel.
Binary sample-visibility vector encoded in observation-window order.
Compiles pure finite-window decisions into hierarchy-attached event deltas.
static std::vector< LocalAttributeIncrement< typename Decision::Value > > computeLocalAttributeIncrements(const MorphologicalTree &tree, const ObservationWindow &window, const Decision &decision, const Algebra &algebra)
Compiles every anchor into dense node-local increments.
static std::vector< EventDelta< typename Decision::Value > > computeEventDeltas(const MorphologicalTree &tree, PixelId anchorPixel, const ObservationWindow &window, const Decision &decision, const Algebra &algebra)
Compiles the state changes of one anchor into sparse node events.
Indexed finite set of translated-sample offsets.
static constexpr std::size_t maxNumOffsets
Maximum number of offsets representable by one visibility bit mask.
Additive event algebra kept separate from the local decision.
Owning result for one computed scalar attribute layout and buffer.
Trusted state operations used only after observation-window validation.
static BinaryVisibilityState zero(std::size_t coordinateCount) noexcept
Builds the trusted all-hidden state.
static void addEnteringOffsets(BinaryVisibilityState &state, std::uint32_t enteringOffsetMask) noexcept
Marks coordinates visible without revalidation.