"""Feature detection — Canny edge and Harris corner detection.""" from __future__ import annotations import numpy as np from skimage.feature import canny, corner_harris, corner_peaks from backend.node_registry import register_node from backend.data_types import DataField, RecordTable @register_node(display_name="Feature Detection") class FeatureDetection: @classmethod def INPUT_TYPES(cls): return { "required": { "field": ("DATA_FIELD",), "method": (["canny", "harris"], {"default": "canny"}), "sigma": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 20.0, "step": 0.1}), }, "optional": { "low_threshold": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}), "high_threshold": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01}), "harris_k": ("FLOAT", {"default": 0.05, "min": 0.01, "max": 0.5, "step": 0.01}), "min_distance": ("INT", {"default": 5, "min": 1, "max": 100}), } } OUTPUTS = ( ('DATA_FIELD', 'result'), ('RECORD_TABLE', 'features'), ) FUNCTION = "process" DESCRIPTION = ( "Detect edges or corners in a surface. " "Canny: multi-stage edge detector with hysteresis thresholding. " "Harris: corner/interest point detector based on structure tensor. " "Outputs a feature map and a table of detected feature locations. " "Equivalent to Gwyddion's edge/corner detection in filters.c." ) def process( self, field: DataField, method: str, sigma: float, low_threshold: float = 0.1, high_threshold: float = 0.2, harris_k: float = 0.05, min_distance: int = 5, ) -> tuple: data = np.asarray(field.data, dtype=np.float64) # Normalise to [0, 1] dmin, dmax = data.min(), data.max() if dmax > dmin: norm = (data - dmin) / (dmax - dmin) else: norm = np.zeros_like(data) records: RecordTable = RecordTable() if method == "canny": edges = canny( norm, sigma=sigma, low_threshold=low_threshold, high_threshold=high_threshold, ) result = edges.astype(np.float64) n_edge_pixels = int(edges.sum()) edge_fraction = n_edge_pixels / data.size records.append({"quantity": "Edge pixels", "value": str(n_edge_pixels), "unit": ""}) records.append({"quantity": "Edge fraction", "value": f"{edge_fraction:.4f}", "unit": ""}) elif method == "harris": response = corner_harris(norm, sigma=sigma, k=harris_k) result = response.astype(np.float64) corners = corner_peaks( response, min_distance=min_distance, threshold_rel=0.1, ) records.append({"quantity": "Corners detected", "value": str(len(corners)), "unit": ""}) for i, (row, col) in enumerate(corners[:20]): x_phys = col * field.dx y_phys = row * field.dy records.append({ "quantity": f"Corner {i + 1}", "value": f"({x_phys:.4g}, {y_phys:.4g})", "unit": field.si_unit_xy, }) else: raise ValueError(f"Unknown method: {method!r}") return (field.replace(data=result, si_unit_z=""), records)