Files
tono/backend/nodes/feature_detection.py

101 lines
3.5 KiB
Python

"""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. "
)
KEYWORDS = ("canny", "harris", "corner", "edge", "interest point", "roi")
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)