MorphologicalAttributeFilters
Public API documentation
Loading...
Searching...
No Matches
HierarchySaliencyMapProjection.hpp
1#pragma once
2
3#include "HierarchySaliencyMap.hpp"
4#include "../../utils/Image.hpp"
5#include "../detail/MorphologicalTreeConstructionContextQueries.hpp"
6
7#include <cstddef>
8#include <cstdint>
9#include <numeric>
10#include <span>
11#include <stdexcept>
12#include <string>
13#include <vector>
14
15namespace mmcfilters {
16
25 int numRows = 0;
27 int numColumns = 0;
29 double adjacencyRadius = 0.0;
31 std::vector<NodeId> sources;
33 std::vector<NodeId> targets;
34
40 [[nodiscard]] std::size_t size() const noexcept { return sources.size(); }
41
47 [[nodiscard]] bool empty() const noexcept { return sources.empty(); }
48};
49
53enum class EdgeToPixelReducer {
55 Max,
57 Mean
58};
59
69 int numRows = 0;
71 int numColumns = 0;
73 double adjacencyRadius = 0.0;
75 std::vector<NodeId> sources;
77 std::vector<NodeId> targets;
79 std::vector<NodeId> nodes;
80
86 [[nodiscard]] std::size_t size() const noexcept { return nodes.size(); }
87
93 [[nodiscard]] bool empty() const noexcept { return nodes.empty(); }
94};
95
108 int numRows = 0;
110 int numColumns = 0;
114 double adjacencyRadius = 0.0;
116 std::vector<std::size_t> offsets;
118 std::vector<NodeId> sources;
120 std::vector<NodeId> targets;
121
127 [[nodiscard]] std::size_t size() const noexcept { return sources.size(); }
128
134 [[nodiscard]] bool empty() const noexcept { return sources.empty(); }
135
142 [[nodiscard]] std::size_t nodeBegin(NodeId nodeId) const { return offsets.at(static_cast<std::size_t>(nodeId)); }
143
150 [[nodiscard]] std::size_t nodeEnd(NodeId nodeId) const { return offsets.at(static_cast<std::size_t>(nodeId) + 1); }
151
158 [[nodiscard]] std::size_t nodeSize(NodeId nodeId) const { return nodeEnd(nodeId) - nodeBegin(nodeId); }
159};
160
169 private:
176 template <class Value> static void validateEdgeMap(const EdgeSaliencyMap<Value>& edgeMap, const char* context) {
177 if (edgeMap.sources.size() != edgeMap.targets.size() || edgeMap.sources.size() != edgeMap.values.size()) {
178 throw std::invalid_argument(std::string(context) + " requires sources, targets, and values to have the same length.");
179 }
180 if (edgeMap.numRows <= 0 || edgeMap.numColumns <= 0) {
181 throw std::invalid_argument(std::string(context) + " requires a non-empty image domain.");
182 }
183 }
184
191 static void validateIncrementalContours(const IncrementalNodeContourMap& contours, const char* context) {
192 if (contours.numRows <= 0 || contours.numColumns <= 0) {
193 throw std::invalid_argument(std::string(context) + " requires a non-empty image domain.");
194 }
195 if (contours.numNodeSlots < 0) {
196 throw std::invalid_argument(std::string(context) + " requires a non-negative node-slot count.");
197 }
198 if (contours.sources.size() != contours.targets.size()) {
199 throw std::invalid_argument(std::string(context) + " requires sources and targets to have the same length.");
200 }
201 if (contours.offsets.size() != static_cast<std::size_t>(contours.numNodeSlots) + 1) {
202 throw std::invalid_argument(std::string(context) + " requires offsets to have numNodeSlots + 1 entries.");
203 }
204 if (contours.offsets.empty() || contours.offsets.front() != 0 || contours.offsets.back() != contours.sources.size()) {
205 throw std::invalid_argument(std::string(context) + " requires offsets to span exactly the contour edge arrays.");
206 }
207 for (std::size_t i = 1; i < contours.offsets.size(); ++i) {
208 if (contours.offsets[i] < contours.offsets[i - 1] || contours.offsets[i] > contours.sources.size()) {
209 throw std::invalid_argument(std::string(context) + " requires monotonically increasing offsets inside the edge-array bounds.");
210 }
211 }
212 }
213
220 template <class Value> static void validateEdgeEndpoints(const EdgeSaliencyMap<Value>& edgeMap, const char* context) {
221 const auto pixelCount = static_cast<std::size_t>(edgeMap.numRows) * static_cast<std::size_t>(edgeMap.numColumns);
222 for (std::size_t i = 0; i < edgeMap.size(); ++i) {
223 const NodeId source = edgeMap.sources[i];
224 const NodeId target = edgeMap.targets[i];
225 if (source < 0 || static_cast<std::size_t>(source) >= pixelCount || target < 0 || static_cast<std::size_t>(target) >= pixelCount) {
226 throw std::invalid_argument(std::string(context) + " requires sources and targets inside the image domain.");
227 }
228 }
229 }
230
238 static void validateTreeAndAdjacency(const MorphologicalTree& tree, const RegularGridAdjacency2D& adjacency, const char* context) {
239 if (tree.root() == InvalidNode) {
240 throw std::invalid_argument(std::string(context) + " requires a non-empty rooted tree.");
241 }
242 if (tree.numRows() <= 0 || tree.numColumns() <= 0 || tree.numPixels() <= 0) {
243 throw std::invalid_argument(std::string(context) + " requires a non-empty image/pixel domain.");
244 }
245 if (adjacency.getNumRows() != tree.numRows() || adjacency.getNumColumns() != tree.numColumns()) {
246 throw std::invalid_argument(std::string(context) + " adjacency domain must match the tree image domain.");
247 }
248 }
249
257 static IncrementalNodeContourMap groupNodeContourEdges(NodeContourEdgeMap&& flat, int numNodeSlots) {
259 contours.numRows = flat.numRows;
260 contours.numColumns = flat.numColumns;
261 contours.numNodeSlots = numNodeSlots;
262 contours.adjacencyRadius = flat.adjacencyRadius;
263 contours.offsets.assign(static_cast<std::size_t>(contours.numNodeSlots) + 1, 0);
264
265 for (NodeId nodeId : flat.nodes) {
266 if (nodeId < 0 || nodeId >= contours.numNodeSlots) {
267 throw std::runtime_error("HierarchySaliencyMapProjection::computeIncrementalNodeContours found an LCA outside the dense node-id domain.");
268 }
269 ++contours.offsets[static_cast<std::size_t>(nodeId) + 1];
270 }
271 std::partial_sum(contours.offsets.begin(), contours.offsets.end(), contours.offsets.begin());
272
273 contours.sources.resize(flat.size());
274 contours.targets.resize(flat.size());
275 std::vector<std::size_t> cursor = contours.offsets;
276 for (std::size_t i = 0; i < flat.size(); ++i) {
277 const NodeId nodeId = flat.nodes[i];
278 const std::size_t dst = cursor[static_cast<std::size_t>(nodeId)]++;
279 contours.sources[dst] = flat.sources[i];
280 contours.targets[dst] = flat.targets[i];
281 }
282
283 return contours;
284 }
285
286 public:
300 template <class Value>
301 [[nodiscard]] static ImagePtr<double> edgeMapToPixelImage(const EdgeSaliencyMap<Value>& edgeMap, EdgeToPixelReducer reducer = EdgeToPixelReducer::Max) {
302 constexpr const char* context = "HierarchySaliencyMapProjection::edgeMapToPixelImage";
303 validateEdgeMap(edgeMap, context);
304 validateEdgeEndpoints(edgeMap, context);
305
306 ImagePtr<double> image = Image<double>::create(edgeMap.numRows, edgeMap.numColumns, 0.0);
307 double* pixels = image->rawData();
308 const auto pixelCount = static_cast<std::size_t>(edgeMap.numRows) * static_cast<std::size_t>(edgeMap.numColumns);
309
310 switch (reducer) {
311 case EdgeToPixelReducer::Max: {
312 std::vector<uint8_t> initialized(pixelCount, false);
313 for (std::size_t i = 0; i < edgeMap.size(); ++i) {
314 const double value = static_cast<double>(edgeMap.values[i]);
315 const auto source = static_cast<std::size_t>(edgeMap.sources[i]);
316 const auto target = static_cast<std::size_t>(edgeMap.targets[i]);
317 if (!initialized[source] || pixels[source] < value) {
318 pixels[source] = value;
319 initialized[source] = true;
320 }
321 if (!initialized[target] || pixels[target] < value) {
322 pixels[target] = value;
323 initialized[target] = true;
324 }
325 }
326 return image;
327 }
328 case EdgeToPixelReducer::Mean: {
329 std::vector<std::size_t> count(pixelCount, 0);
330 for (std::size_t i = 0; i < edgeMap.size(); ++i) {
331 const double value = static_cast<double>(edgeMap.values[i]);
332 const auto source = static_cast<std::size_t>(edgeMap.sources[i]);
333 const auto target = static_cast<std::size_t>(edgeMap.targets[i]);
334 pixels[source] += value;
335 pixels[target] += value;
336 ++count[source];
337 ++count[target];
338 }
339 for (std::size_t i = 0; i < pixelCount; ++i) {
340 if (count[i] != 0) {
341 pixels[i] /= static_cast<double>(count[i]);
342 }
343 }
344 return image;
345 }
346 default:
347 throw std::invalid_argument(std::string(context) + " received an unknown edge-to-pixel reducer.");
348 }
349 }
350
362 template <class Value, class Threshold> [[nodiscard]] static EdgeContourMap thresholdCut(const EdgeSaliencyMap<Value>& edgeMap, Threshold threshold) {
363 constexpr const char* context = "HierarchySaliencyMapProjection::thresholdCut";
364 validateEdgeMap(edgeMap, context);
365
367 contours.numRows = edgeMap.numRows;
368 contours.numColumns = edgeMap.numColumns;
369 contours.adjacencyRadius = edgeMap.adjacencyRadius;
370 contours.sources.reserve(edgeMap.size());
371 contours.targets.reserve(edgeMap.size());
372
373 for (std::size_t i = 0; i < edgeMap.size(); ++i) {
374 if (edgeMap.values[i] >= threshold) {
375 contours.sources.push_back(edgeMap.sources[i]);
376 contours.targets.push_back(edgeMap.targets[i]);
377 }
378 }
379 return contours;
380 }
381
394 constexpr const char* context = "HierarchySaliencyMapProjection::nodeContourEdges";
395 validateTreeAndAdjacency(tree, adjacency, context);
396
398 contours.numRows = tree.numRows();
399 contours.numColumns = tree.numColumns();
400 contours.adjacencyRadius = adjacency.getRadius();
401
402 const int numPixels = tree.numPixels();
403 for (NodeId source = 0; source < numPixels; ++source) {
404 const NodeId sourceSmallestNode = tree.smallestNode(source);
405 if (!tree.isAlive(sourceSmallestNode)) {
406 throw std::runtime_error(std::string(context) + " found a pixel without a live smallest node.");
407 }
408
409 for (int target : adjacency.getForwardNeighborIndices(source)) {
410 const NodeId targetSmallestNode = tree.smallestNode(target);
411 if (!tree.isAlive(targetSmallestNode)) {
412 throw std::runtime_error(std::string(context) + " found a neighbour pixel without a live smallest node.");
413 }
415 continue;
416 }
417
419 if (lca == InvalidNode || !tree.isAlive(lca)) {
420 throw std::runtime_error(std::string(context) + " could not find a live LCA for an adjacency edge.");
421 }
422
423 contours.sources.push_back(source);
424 contours.targets.push_back(target);
425 contours.nodes.push_back(lca);
426 }
427 }
428 return contours;
429 }
430
438 const RegularGridAdjacency2D* adjacency = ::mmcfilters::detail::constructionAdjacency(tree);
439 if (adjacency == nullptr) {
440 throw std::invalid_argument(
441 "HierarchySaliencyMapProjection::nodeContourEdges requires an attached adjacency relation; pass an explicit adjacency relation instead.");
442 }
443 return nodeContourEdges(tree, *adjacency);
444 }
445
453 template <AltitudeValue T>
455 return nodeContourEdges(tree.topology(), adjacency);
456 }
457
464 template <AltitudeValue T> [[nodiscard]] static NodeContourEdgeMap nodeContourEdges(const ValuedMorphologicalTree<T>& tree) {
465 return nodeContourEdges(tree.topology());
466 }
467
480 return groupNodeContourEdges(nodeContourEdges(tree, adjacency), tree.numInternalNodeSlots());
481 }
482
490 return groupNodeContourEdges(nodeContourEdges(tree), tree.numInternalNodeSlots());
491 }
492
500 template <AltitudeValue T>
502 const RegularGridAdjacency2D& adjacency) {
503 return computeIncrementalNodeContours(tree.topology(), adjacency);
504 }
505
513 return computeIncrementalNodeContours(tree.topology());
514 }
515
529 template <class Value>
531 constexpr const char* context = "HierarchySaliencyMapProjection::projectNodeValuation";
532 validateIncrementalContours(contours, context);
533 if (nodeValuation.size() != static_cast<std::size_t>(contours.numNodeSlots)) {
534 throw std::invalid_argument(std::string(context) + " requires one valuation value per dense node slot.");
535 }
536
538 edgeMap.numRows = contours.numRows;
539 edgeMap.numColumns = contours.numColumns;
540 edgeMap.adjacencyRadius = contours.adjacencyRadius;
541 edgeMap.sources.reserve(contours.size());
542 edgeMap.targets.reserve(contours.size());
543 edgeMap.values.reserve(contours.size());
544
545 for (NodeId nodeId = 0; nodeId < contours.numNodeSlots; ++nodeId) {
546 const Value value = nodeValuation[static_cast<std::size_t>(nodeId)];
547 for (std::size_t i = contours.offsets[static_cast<std::size_t>(nodeId)]; i < contours.offsets[static_cast<std::size_t>(nodeId) + 1]; ++i) {
548 edgeMap.sources.push_back(contours.sources[i]);
549 edgeMap.targets.push_back(contours.targets[i]);
550 edgeMap.values.push_back(value);
551 }
552 }
553 return edgeMap;
554 }
555
570 template <class Value, class Threshold>
573 constexpr const char* context = "HierarchySaliencyMapProjection::thresholdByNodeValuation";
574 validateIncrementalContours(contours, context);
575 if (nodeValuation.size() != static_cast<std::size_t>(contours.numNodeSlots)) {
576 throw std::invalid_argument(std::string(context) + " requires one valuation value per dense node slot.");
577 }
578
580 cut.numRows = contours.numRows;
581 cut.numColumns = contours.numColumns;
582 cut.adjacencyRadius = contours.adjacencyRadius;
583 cut.sources.reserve(contours.size());
584 cut.targets.reserve(contours.size());
585
586 for (NodeId nodeId = 0; nodeId < contours.numNodeSlots; ++nodeId) {
587 if (nodeValuation[static_cast<std::size_t>(nodeId)] >= threshold) {
588 for (std::size_t i = contours.offsets[static_cast<std::size_t>(nodeId)]; i < contours.offsets[static_cast<std::size_t>(nodeId) + 1]; ++i) {
589 cut.sources.push_back(contours.sources[i]);
590 cut.targets.push_back(contours.targets[i]);
591 }
592 }
593 }
594 return cut;
595 }
596};
597
598} // 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:34
Derived projections and contour materializations of saliency maps.
static ImagePtr< double > edgeMapToPixelImage(const EdgeSaliencyMap< Value > &edgeMap, EdgeToPixelReducer reducer=EdgeToPixelReducer::Max)
Rasterizes an edge-indexed map into a pixel image for display.
static IncrementalNodeContourMap computeIncrementalNodeContours(const ValuedMorphologicalTree< T > &tree, const RegularGridAdjacency2D &adjacency)
Computes valued-tree incremental contours with explicit adjacency.
static IncrementalNodeContourMap computeIncrementalNodeContours(const MorphologicalTree &tree)
Computes per-node incremental contour edges using stored adjacency.
static EdgeContourMap thresholdCut(const EdgeSaliencyMap< Value > &edgeMap, Threshold threshold)
Thresholds an edge saliency map into an edge contour set.
static EdgeContourMap thresholdByNodeValuation(const IncrementalNodeContourMap &contours, std::span< const Value > nodeValuation, Threshold threshold)
Thresholds incremental contour edges by dense node valuation.
static IncrementalNodeContourMap computeIncrementalNodeContours(const MorphologicalTree &tree, const RegularGridAdjacency2D &adjacency)
Computes per-node incremental contour edges with explicit adjacency.
static EdgeSaliencyMap< Value > projectNodeValuation(const IncrementalNodeContourMap &contours, std::span< const Value > nodeValuation)
Projects dense node valuation onto transition contour edges.
static NodeContourEdgeMap nodeContourEdges(const ValuedMorphologicalTree< T > &tree)
Projects valued-tree stored-adjacency edges onto hierarchy nodes.
static IncrementalNodeContourMap computeIncrementalNodeContours(const ValuedMorphologicalTree< T > &tree)
Computes valued-tree incremental contours using stored adjacency.
static NodeContourEdgeMap nodeContourEdges(const ValuedMorphologicalTree< T > &tree, const RegularGridAdjacency2D &adjacency)
Projects valued-tree adjacency edges onto their hierarchy node.
static NodeContourEdgeMap nodeContourEdges(const MorphologicalTree &tree, const RegularGridAdjacency2D &adjacency)
Projects transition adjacency edges onto their hierarchy node.
static NodeContourEdgeMap nodeContourEdges(const MorphologicalTree &tree)
Projects stored-adjacency transition edges onto hierarchy nodes.
static Ptr create(int rows, int columns)
Creates an owned image with uninitialised pixel values.
Definition Image.hpp:105
Mutable connected-subset tree on a finite pixel domain.
int numRows() const
Returns the number of rows in the regular 2D pixel domain.
int numInternalNodeSlots() 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.
int numPixels() const
Returns the cardinality of the pixel domain.
NodeId smallestNode(PixelId pixel) const
Returns the inclusion-smallest node containing pixel.
NodeId lowestCommonAncestor(NodeId u, NodeId v) const
Returns the lowest common ancestor of u and v.
int numColumns() const
Returns the number of columns in the regular 2D pixel domain.
NodeId root() const
Returns the current hierarchy root.
Immutable regular-grid 2D adjacency with allocation-free traversal.
double getRadius() const noexcept
Returns the configured or bounding Euclidean radius.
ForwardNeighborIndexRange getForwardNeighborIndices(int row, int column) const
Returns the directed positive half of the neighbourhood.
int getNumColumns() const noexcept
Returns the number of columns in the attached grid domain.
int getNumRows() const noexcept
Returns the number of rows in the attached grid domain.
Owning result for one computed scalar attribute layout and buffer.
std::vector< Real > & values() noexcept
Returns the mutable flat attribute buffer.
Edge-indexed contour set induced by thresholding a saliency map.
std::vector< NodeId > sources
Source proper-part id of each selected edge.
int numColumns
Number of columns in the proper-part grid.
int numRows
Number of rows in the proper-part grid.
std::vector< NodeId > targets
Target proper-part id of each selected edge.
std::size_t size() const noexcept
Returns the number of selected edges.
bool empty() const noexcept
Returns whether no contour edge is selected.
double adjacencyRadius
Radius of the adjacency used to enumerate the edges.
Per-node incremental contour edges induced by a hierarchy.
std::vector< NodeId > sources
Source pixel ids grouped by boundary-owning node.
int numNodeSlots
Number of slots in the dense node-id domain.
int numRows
Number of rows in the proper-part grid.
std::size_t size() const noexcept
Returns the total number of stored contour edges.
std::vector< NodeId > targets
Target pixel ids parallel to sources.
std::size_t nodeEnd(NodeId nodeId) const
Returns the exclusive edge offset for nodeId.
std::vector< std::size_t > offsets
CSR-style boundaries of the edge slice owned by each node.
std::size_t nodeBegin(NodeId nodeId) const
Returns the inclusive edge offset for nodeId.
double adjacencyRadius
Radius of the adjacency used to enumerate the edges.
std::size_t nodeSize(NodeId nodeId) const
Returns the number of contour edges owned by nodeId.
bool empty() const noexcept
Returns whether no contour edge is stored.
int numColumns
Number of columns in the proper-part grid.
Edge-indexed contour set projected onto hierarchy nodes.
double adjacencyRadius
Radius of the adjacency used to enumerate the edges.
std::vector< NodeId > sources
Source proper-part id of each contour edge.
std::size_t size() const noexcept
Returns the number of projected contour edges.
int numColumns
Number of columns in the proper-part grid.
std::vector< NodeId > targets
Target proper-part id of each contour edge.
std::vector< NodeId > nodes
Boundary-owning hierarchy node parallel to each edge.
bool empty() const noexcept
Returns whether the projection has no contour edges.
int numRows
Number of rows in the proper-part grid.