
Project status: research library for dynamic morphological-tree experiments.
This package focuses on a proper-part tree model for image-domain partial partitions, tree editing, typed altitude contracts, incremental attributes, contours, and project-specific morphology research. It is not intended to be a general-purpose replacement for Higra.
Scope
This repository is a research implementation for morphological-tree workflows that need direct topology ownership, staged edits, typed altitude buffers, and project-specific attribute machinery. Its central model is a tree over image-domain partial partitions: image pixels are explicit proper parts, and internal morphological nodes own those proper parts directly. The implemented models share the MorphologicalTree topology abstraction: rooted inclusion topology, dense internal NodeId values, and explicit proper-part ownership.
Current functionality includes:
- max-tree, min-tree, tree-of-shapes, and self-dual residual tree construction;
- import/export of Higra-style
(parent, altitude) hierarchies;
- dynamic topology edits through safe mutators and staged editors;
- gray-level, shape, boundary, topology, and max-distance attributes;
- attribute filters, extinction values, and Ultimate Attribute Opening;
- a C++20 header-oriented core plus a pybind11 Python package.
Higra remains the better fit for stable, general-purpose hierarchical image-analysis workflows. Use this project when the experiment needs mutable tree topology, direct owner-state access, or the local attribute/filter machinery exposed here. See docs/attribute-catalog.md for the public descriptor catalog and docs/higra-interoperability.md for import, export, and attribute-projection contracts.
Python currently follows the canonical 8-bit contract: factory inputs must be C-contiguous np.uint8 arrays and external altitude inputs must stay in [0, 255]. C++ supports typed max/min and SDRT construction through Image<T>, typed Higra imports through createFromHigraParent<T>, and read-only WeightedTreeView<T> altitude spans. Tree of Shapes construction is currently uint8_t.
Installation
From PyPI:
From source:
cmake -S . -B build -DMMCFILTERS_BUILD_PYTHON=ON
cmake --build build
Installed C++ package:
find_package(mmcfilters CONFIG REQUIRED)
target_link_libraries(my_target PRIVATE mmcfilters::core)
To enable the regression suite or examples:
cmake -S . -B build \
-DMMCFILTERS_BUILD_PYTHON=ON \
-DMMCFILTERS_BUILD_TESTS=ON \
-DMMCFILTERS_BUILD_EXAMPLES=ON
cmake --build build
ctest --test-dir build --output-on-failure
Quick Python example
import numpy as np
import mmcfilters
img = np.array(
[
[3, 3, 2, 2],
[3, 4, 4, 2],
[1, 4, 5, 2],
[1, 1, 5, 0],
],
dtype=np.uint8,
)
img = np.ascontiguousarray(img, dtype=np.uint8)
adjacency_radius = 1.5
img,
radius=adjacency_radius,
)
root_node_id = weighted_tree.getRoot()
root_children = weighted_tree.getChildren(root_node_id)
root_direct_proper_parts = weighted_tree.getProperParts(root_node_id)
pixel_index = 10
pixel_component_id = weighted_tree.getProperPartOwner(pixel_index)
pixel_component_pixels = list(weighted_tree.getConnectedComponent(pixel_component_id))
pixel_component_mask = weighted_tree.reconstructNode(pixel_component_id)
topology_names, topology_by_node = mmcfilters.Attribute.computeTopologyAttributes(
weighted_tree,
[mmcfilters.Attribute.AREA, mmcfilters.Attribute.BOX_HEIGHT],
)
area_by_node = topology_by_node[:, topology_names["AREA"]]
box_height_by_node = topology_by_node[:, topology_names["BOX_HEIGHT"]]
max_dist_by_node = mmcfilters.Attribute.computeSingleAttribute(
weighted_tree,
mmcfilters.Attribute.MAX_DIST,
)
reconstructed_image = weighted_tree.reconstructionImage()
uao.execute(img.shape[0])
max_contrast_image = uao.getMaxContrastImage()
associated_image = uao.getAssociatedImage()
higra_parent, higra_altitude = weighted_tree.exportHigraHierarchy()
max_dist_by_higra = weighted_tree.project_node_values_to_exported_higra(
max_dist_by_node,
mmcfilters.Attribute.MAX_DIST,
)
higra_parent,
higra_altitude,
img.shape[0],
img.shape[1],
mmcfilters.MorphologicalTreeKind.MAX_TREE,
radius=adjacency_radius,
)
static WeightedMorphologicalTree< T > createMaxTree(ImagePtr< T > img, double radius=1.5)
Builds a typed weighted max-tree from an image.
static WeightedMorphologicalTree< T > createFromHigraParent(std::span< const NodeId > higraParent, std::span< const T > higraAltitude, int rows, int cols, MorphologicalTreeKind kind, std::optional< AdjacencyRelation > adjacency=std::nullopt)
Imports a static Higra parent/altitude hierarchy.
Computes an Ultimate Attribute Opening by accumulating maximal contrasts.
Repository guide
Use this map to find the right entry point quickly:
API guides:
Contributor design notes:
Documentation
The Documentation workflow validates the public and internal Doxygen targets. On pushes to main, it publishes only the public HTML output to GitHub Pages: wonderalexandre.github.io/MorphologicalAttributeFilters. The internal HTML output remains available as a workflow artifact.
Release process
Releases are automated by GitHub Actions. For a production release:
- Make sure the
CI and Package workflows are green on main.
- Create and push a semantic version tag, for example
v1.0.1.
- The
Release workflow validates that the tag matches the resolved package version, builds the source distribution and platform wheels, validates the package metadata, and attaches the artifacts to a GitHub Release.
The release wheel matrix targets Python 3.9 through 3.14 on:
- Linux manylinux x86_64;
- Windows x86_64;
- macOS arm64;
- macOS Intel x86_64.
PyPI publication is intentionally manual. Download the release artifacts from the GitHub Release or from the Release workflow run, then upload them with:
python -m pip install --upgrade twine
python -m twine upload dist/*
Manual runs of the Release workflow also build downloadable artifacts without creating a GitHub Release.