Files
tono/backend/nodes/mark_disconnected.py

74 lines
2.7 KiB
Python

"""Mark disconnected regions — mask topologically isolated surface regions."""
from __future__ import annotations
import numpy as np
from scipy.ndimage import grey_opening, grey_closing
from backend.node_registry import register_node
from backend.data_types import DataField
from backend.nodes.helpers import bool_to_mask, _mask_structure, emit_mask_preview
@register_node(display_name="Mark Disconnected")
class MarkDisconnected:
"""
Detect topologically disconnected (isolated) surface regions using
morphological opening/closing to build a defect-free reference, then
thresholding the residual difference.
"""
_CUSTOM_PREVIEW = True
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"field": ("DATA_FIELD",),
"defect_type": (["positive", "negative", "both"],),
"radius": ("INT", {"default": 5, "min": 1, "max": 100, "step": 1}),
"threshold": ("FLOAT", {"default": 0.1, "min": 0.001, "max": 1.0, "step": 0.001}),
}
}
OUTPUTS = (
('IMAGE', 'mask'),
)
FUNCTION = "process"
DESCRIPTION = (
"Mark topologically disconnected (isolated) surface regions. "
"A morphological opening followed by closing builds a smooth "
"defect-free reference surface; pixels whose deviation from that "
"reference exceeds the sensitivity threshold are flagged. "
"Equivalent to Gwyddion's mark_disconn module."
)
KEYWORDS = ("isolated", "defect", "morphology", "disconn", "outlier")
def process(self, field: DataField, defect_type: str, radius: int, threshold: float) -> tuple:
data = field.data.astype(np.float64)
# Build a disk structuring element for grey-scale morphology.
struct = _mask_structure(radius, "disk")
# Morphological opening then closing produces a defect-free reference.
reference = grey_opening(data, footprint=struct)
reference = grey_closing(reference, footprint=struct)
difference = data - reference
diff_range = difference.max() - difference.min()
# Avoid division-by-zero on perfectly flat surfaces.
if diff_range == 0:
mask = np.zeros(data.shape, dtype=bool)
else:
abs_threshold = threshold * diff_range
if defect_type == "positive":
mask = difference > abs_threshold
elif defect_type == "negative":
mask = difference < -abs_threshold
else: # "both"
mask = np.abs(difference) > abs_threshold
out = bool_to_mask(mask)
emit_mask_preview(field, out)
return (out,)