Morphology User Guide¶
mtlearn.morphology is the stable Python entry point for building
morphological trees, computing node attributes, applying deterministic
attribute filters, and inspecting tree topology. This guide is written for
users who want to run those operations directly, without going through the
learnable CFP layer.
The examples assume that the native _mtlearn extension is installed and that
input images are 2D numpy.uint8 arrays.
import numpy as np
from mtlearn import morphology
A Complete First Example¶
Start with a small grayscale image, build a max-tree, compute an area attribute, and apply a deterministic area filter.
image = np.array(
[
[0, 0, 10, 10, 10, 0],
[0, 20, 50, 50, 10, 0],
[0, 20, 50, 50, 10, 0],
[0, 0, 10, 10, 10, 0],
],
dtype=np.uint8,
)
tree = morphology.create_max_tree(image)
area = morphology.compute_single_attribute(
tree,
morphology.AttributeType.AREA,
)
filters = morphology.create_attribute_filter(tree)
filtered = filters.filteringMin(area, threshold=4.0)
print(filtered.shape, filtered.dtype)
filtered is a reconstructed NumPy image. Attribute arrays such as area are
node-indexed arrays: they are not images and should not be reshaped to image
size.
Image Requirements¶
Tree constructors expect a 2D uint8 image. Keep conversion explicit near the boundary of your application.
image = np.asarray(image, dtype=np.uint8)
if image.ndim != 2:
raise ValueError("mtlearn.morphology expects one 2D grayscale image")
If your data starts as a normalized float image, scale it before tree construction.
image_u8 = np.clip(image_float * 255.0, 0, 255).astype(np.uint8)
tree = morphology.create_max_tree(image_u8)
For batched tensors, build one tree per sample and per channel. Tree construction is not a vectorized tensor operation.
Choose a Tree Type¶
The tree type encodes the image structures you want to organize.
Tree type |
Use when |
Constructor |
|---|---|---|
Max-tree |
Bright connected components are foreground. |
|
Min-tree |
Dark connected components are foreground. |
|
Tree of shapes |
Bright and dark structures should be handled symmetrically. |
|
max_tree = morphology.create_max_tree(image)
min_tree = morphology.create_min_tree(image)
tos_tree = morphology.create_tree_of_shapes(
image,
interpolation="self-dual",
)
When tree type comes from a configuration file or command-line argument, use
build_tree.
tree = morphology.build_tree(image, "max-tree")
tree = morphology.build_tree(image, morphology.TreeType.MIN_TREE)
tree = morphology.build_tree(
image,
"tree-of-shapes",
tos_interpolation=morphology.ToSInterpolation.SelfDual,
)
Accepted tree-of-shapes interpolation aliases include "self-dual",
"min4c-max8c", and "min8c-max4c".
Inspect a Tree¶
The tree object exposes image dimensions, live node counts, internal node slot counts, and common topology queries.
print("image:", tree.numRows, "x", tree.numCols)
print("live nodes:", tree.numNodes)
print("node slots:", tree.numInternalNodeSlots)
print("root:", tree.root)
print("leaves:", len(tree.leaf_node_ids))
The backend distinguishes live topology nodes from internal node slots:
numNodescounts currently live topology nodes.numInternalNodeSlotsis the size expected by attribute vectors, boolean criteria, score vectors, and CFP tree tensors.
Use the slot count when allocating node-indexed arrays.
criterion = [False] * tree.numInternalNodeSlots
for node in tree.alive_node_ids:
criterion[node] = tree.isLeaf(node)
This convention matters after pruning or merging, because inactive node slots may remain allocated even when the live topology changes.
Compute Scalar Attributes¶
Scalar attributes are members of morphology.AttributeType.
area = morphology.compute_single_attribute(
tree,
morphology.AttributeType.AREA,
)
gray_height = morphology.compute_single_attribute(
tree,
morphology.AttributeType.GRAY_HEIGHT,
)
Use compute_attributes when you need several attributes at once.
attribute_index, values = morphology.compute_attributes(
tree,
[
morphology.AttributeType.AREA,
morphology.AttributeType.GRAY_HEIGHT,
morphology.AttributeType.COMPACTNESS,
],
)
area_col = attribute_index["AREA"]
compactness_col = attribute_index["COMPACTNESS"]
area = values[:, area_col]
compactness = values[:, compactness_col]
The returned values matrix has one row per node slot and one column per
computed scalar attribute.
Use Attribute Groups¶
Attribute groups are shortcuts for common sets of scalar attributes.
shape_attrs = morphology.expand_attribute_group(
morphology.AttributeGroup.SHAPE,
)
attribute_index, shape_values = morphology.compute_attributes(
tree,
shape_attrs,
)
Groups are useful for exploration, but explicit scalar lists are easier to track in experiments and checkpoints.
attrs_for_experiment = [
morphology.AttributeType.AREA,
morphology.AttributeType.GRAY_HEIGHT,
morphology.AttributeType.COMPACTNESS,
]
Use describe_all_attributes to discover the public attribute set.
for name, description in morphology.describe_all_attributes().items():
print(f"{name}: {description}")
For a grouped list of public attributes and their intended use, see Attribute Catalog.
Tree-of-shapes filters cannot compute every scalar attribute currently exposed by the backend. In particular, attributes that depend on one-sided component tree geometry may be unavailable for trees of shapes.
Find Nodes by Attribute¶
A common workflow is to compute an attribute, restrict to live nodes, and pick a node for inspection.
area = morphology.compute_single_attribute(tree, morphology.AttributeType.AREA)
alive = np.asarray(tree.alive_node_ids, dtype=np.int64)
largest_live_node = alive[np.argmax(area[alive])]
print("largest node:", largest_live_node)
print("area:", area[largest_live_node])
mask = tree.reconstructNode(int(largest_live_node))
The same pattern works for shape attributes, topology attributes, and gray-level attributes.
Deterministic Attribute Filters¶
create_attribute_filter(tree) returns a helper bound to one tree. Its methods
consume node-slot-sized arrays and return reconstructed images.
filters = morphology.create_attribute_filter(tree)
area = morphology.compute_single_attribute(tree, morphology.AttributeType.AREA)
area_opening = filters.filteringMin(area, threshold=16.0)
For boolean-rule filters, build one boolean value per node slot.
criterion = (area >= 16.0).tolist()
direct = filters.filteringDirectRule(criterion)
subtractive = filters.filteringSubtractiveRule(criterion)
Keep this assertion in prototypes; it catches most criterion/attribute shape mistakes.
assert len(criterion) == tree.numInternalNodeSlots
Score-based subtractive filtering expects one float score per node slot.
scores = area.astype(np.float32)
score_image = filters.filteringSubtractiveScoreRule(scores.tolist())
Common Filtering Recipes¶
Remove Small Bright Components¶
Use a max-tree and an area threshold.
tree = morphology.create_max_tree(image)
area = morphology.compute_single_attribute(tree, morphology.AttributeType.AREA)
filters = morphology.create_attribute_filter(tree)
opened = filters.filteringMin(area, threshold=25.0)
Remove Small Dark Components¶
Use a min-tree with the same type of area criterion.
tree = morphology.create_min_tree(image)
area = morphology.compute_single_attribute(tree, morphology.AttributeType.AREA)
filters = morphology.create_attribute_filter(tree)
closed_like = filters.filteringMin(area, threshold=25.0)
Inspect Self-Dual Shapes¶
Use a tree of shapes when you do not want to choose bright or dark polarity.
tree = morphology.create_tree_of_shapes(
image,
interpolation=morphology.ToSInterpolation.SelfDual,
)
attrs = [
morphology.AttributeType.AREA,
morphology.AttributeType.COMPACTNESS,
]
attribute_index, values = morphology.compute_attributes(tree, attrs)
Mutating Trees¶
pruneNode and mergeNodeIntoParent mutate the tree in place. Query
topology-dependent values again after a mutation.
node = int(tree.leaf_node_ids[0])
tree.pruneNode(node)
reconstructed = tree.reconstructionImage()
alive_after_prune = tree.alive_node_ids
Rebuild the tree if you need to preserve the original topology.
original = morphology.create_max_tree(image)
working = morphology.create_max_tree(image)
working.pruneNode(int(working.leaf_node_ids[0]))
Use mutating operations for inspection, deterministic prototypes, and backend
experiments. For learnable preprocessing inside PyTorch models, prefer
ConnectedFilterPreprocessingLayer, which keeps tree construction and
attribute computation outside autograd and learns only node-wise gates.
Common Mistakes¶
Passing RGB or batched arrays to tree constructors. Build one tree from one 2D grayscale image.
Allocating criteria with
numNodesinstead ofnumInternalNodeSlots.Treating attribute arrays as images. Attributes are indexed by node slot.
Reusing topology-dependent node ids after pruning or merging without querying the tree again.
Comparing max-tree and min-tree outputs without accounting for polarity.
Where to Go Next¶
Getting Started gives the shortest end-to-end setup.
Attribute Catalog lists the public scalar attributes and groups.
Connected Filter Preprocessing explains the learnable PyTorch layer.
Custom Attributes and Filter Specs explains how morphology attributes are used in CFP filter specs.
Morphology Facade contains the generated API reference.