74 lines
2.7 KiB
Python
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,)
|