"""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,)