52 lines
1.7 KiB
Python
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),)
|