Files
tono/backend/nodes/grain_edge.py
2026-04-03 23:11:52 -07:00

52 lines
1.7 KiB
Python

"""Grain edge detection — detect grain boundaries using Laplacian edge detection."""
from __future__ import annotations
import numpy as np
from scipy.ndimage import label
from backend.node_registry import register_node
from backend.data_types import DataField
from backend.nodes.helpers import mask_to_bool, bool_to_mask
@register_node(display_name="Grain Edge")
class GrainEdge:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"field": ("DATA_FIELD",),
"mask": ("IMAGE",),
"width": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}),
}
}
OUTPUTS = (
('IMAGE', 'edge_mask'),
)
FUNCTION = "process"
DESCRIPTION = (
"Detect grain boundaries from a binary grain mask. Outputs a mask "
"of pixels at grain edges (where grain meets non-grain). Width "
"controls the boundary thickness in pixels. "
)
def process(self, field: DataField, mask: np.ndarray, width: int) -> tuple:
grain = mask_to_bool(mask)
# Find boundary: grain pixels with at least one non-grain 4-neighbour
padded = np.pad(grain, 1, mode='constant', constant_values=False)
interior = (padded[:-2, 1:-1] & padded[2:, 1:-1] &
padded[1:-1, :-2] & padded[1:-1, 2:])
boundary = grain & ~interior
# Expand boundary by width
if width > 1:
from scipy.ndimage import binary_dilation
struct = np.ones((2 * width - 1, 2 * width - 1), dtype=bool)
boundary = binary_dilation(boundary, structure=struct) & grain
return (bool_to_mask(boundary),)