diff --git a/backend/data_types.py b/backend/data_types.py index c8746c0..c33c837 100644 --- a/backend/data_types.py +++ b/backend/data_types.py @@ -2,12 +2,12 @@ Core data types for tono. DataField mirrors Gwyddion's GwyDataField structure: - xres, yres – pixel dimensions - xreal, yreal – physical dimensions in metres - xoff, yoff – position offset in metres - si_unit_xy – lateral unit string (e.g. "m", "nm") - si_unit_z – height/value unit string (e.g. "m", "V", "A") - domain – "spatial" or "frequency" (set by FFT nodes) + xres, yres - pixel dimensions + xreal, yreal - physical dimensions in metres + xoff, yoff - position offset in metres + si_unit_xy - lateral unit string (e.g. "m", "nm") + si_unit_z - height/value unit string (e.g. "m", "V", "A") + domain - "spatial" or "frequency" (set by FFT nodes) """ from __future__ import annotations diff --git a/backend/importers/__init__.py b/backend/importers/__init__.py index bfc7a40..ad93875 100644 --- a/backend/importers/__init__.py +++ b/backend/importers/__init__.py @@ -2,10 +2,10 @@ File importer registry. Each module in this package exposes: - extensions frozenset[str] – lower-case extensions it handles - calibrated bool – True when physical dimensions are known - load(path) → list[DataField] – load all channels - channel_names(path) → list[str] – channel name strings (same order as load) + extensions frozenset[str] - lower-case extensions it handles + calibrated bool - True when physical dimensions are known + load(path) → list[DataField] - load all channels + channel_names(path) → list[str] - channel name strings (same order as load) Usage:: diff --git a/backend/importers/ergo_hdf5.py b/backend/importers/ergo_hdf5.py index a920bd0..78677ae 100644 --- a/backend/importers/ergo_hdf5.py +++ b/backend/importers/ergo_hdf5.py @@ -5,19 +5,19 @@ Asylum Research instruments store scan metadata in a sidecar group rather than as dataset attributes. This importer reads physical dimensions from: Image/DataSetInfo/Global/Channels//ImageDims - DimScaling – (2,2) array: [[Y_start, Y_end], [X_start, X_end]] + DimScaling - (2,2) array: [[Y_start, Y_end], [X_start, X_end]] absolute physical coordinate ranges in DimUnits - DimExtents – pixel counts [yres, xres] (stored in a child group, not used for sizing) - DimUnits – lateral unit strings [Y_unit, X_unit] - DataUnits – Z unit string + DimExtents - pixel counts [yres, xres] (stored in a child group, not used for sizing) + DimUnits - lateral unit strings [Y_unit, X_unit] + DataUnits - Z unit string If the sidecar group is absent (generic HDF5), standard dataset attributes are used as a fallback: - xreal / yreal – physical scan size in metres (fallback: 1e-6) - xoff / yoff – position offset in metres (fallback: 0) - si_unit_xy – lateral unit string (fallback: "m") - si_unit_z – value unit string (fallback: "m") + xreal / yreal - physical scan size in metres (fallback: 1e-6) + xoff / yoff - position offset in metres (fallback: 0) + si_unit_xy - lateral unit string (fallback: "m") + si_unit_z - value unit string (fallback: "m") Requires: pip install h5py diff --git a/backend/importers/hdf5.py b/backend/importers/hdf5.py index b670745..7285f59 100644 --- a/backend/importers/hdf5.py +++ b/backend/importers/hdf5.py @@ -4,10 +4,10 @@ Generic HDF5 importer (.h5, .hdf5, .he5). Each 2-D dataset found in the file is returned as a DataField. Physical dimensions are read from standard dataset attributes if present: - xreal / yreal – physical scan size in metres (fallback: 1e-6) - xoff / yoff – position offset in metres (fallback: 0) - si_unit_xy – lateral unit string (fallback: "m") - si_unit_z – value unit string (fallback: "m") + xreal / yreal - physical scan size in metres (fallback: 1e-6) + xoff / yoff - position offset in metres (fallback: 0) + si_unit_xy - lateral unit string (fallback: "m") + si_unit_z - value unit string (fallback: "m") For Asylum Research / Ergo format files (which store scan metadata in a sidecar group rather than as dataset attributes), use the ergo_hdf5 importer. diff --git a/backend/nodes/acf_1d.py b/backend/nodes/acf_1d.py index bc9f70d..710717b 100644 --- a/backend/nodes/acf_1d.py +++ b/backend/nodes/acf_1d.py @@ -44,6 +44,8 @@ class ACF1D: "The measurement table reports the dominant period from the first positive peak." ) + KEYWORDS = ("autocorrelation", "correlation", "period") + def process(self, profile: LineData, level: str) -> tuple: z = np.asarray(profile, dtype=np.float64) if level == "mean": diff --git a/backend/nodes/acf_2d.py b/backend/nodes/acf_2d.py index 92cd191..dae8cfd 100644 --- a/backend/nodes/acf_2d.py +++ b/backend/nodes/acf_2d.py @@ -27,6 +27,8 @@ class ACF2D: "and uses the default half-range extents from acf2d." ) + KEYWORDS = ("autocorrelation", "correlation") + def process(self, field: DataField, level: str) -> tuple: data = preprocess_spectral_data(field, level=level, windowing="none") return (acf_field_from_data(field, data),) diff --git a/backend/nodes/affine_correction.py b/backend/nodes/affine_correction.py index d155ab6..c078b24 100644 --- a/backend/nodes/affine_correction.py +++ b/backend/nodes/affine_correction.py @@ -35,6 +35,8 @@ class AffineCorrection: "The transform is applied about the centre of the field. " ) + KEYWORDS = ("shear", "scale", "distortion", "warp") + def process( self, field: DataField, diff --git a/backend/nodes/annotations.py b/backend/nodes/annotations.py index ebb4b2d..57b6a75 100644 --- a/backend/nodes/annotations.py +++ b/backend/nodes/annotations.py @@ -50,6 +50,8 @@ class Annotations: "or annotate an IMAGE that carries viewport metadata from View3D." ) + KEYWORDS = ("scale bar", "legend", "colorbar", "publication") + def render( self, input, diff --git a/backend/nodes/calibration.py b/backend/nodes/calibration.py index cb2486a..55ec5f3 100644 --- a/backend/nodes/calibration.py +++ b/backend/nodes/calibration.py @@ -84,6 +84,8 @@ class Calibration: "Equivalent to Gwyddion's calibrate functionality." ) + KEYWORDS = ("units", "rescale", "dimensions") + def process( self, field: DataField, diff --git a/backend/nodes/colormap.py b/backend/nodes/colormap.py index f2ae45a..5a41682 100644 --- a/backend/nodes/colormap.py +++ b/backend/nodes/colormap.py @@ -33,6 +33,8 @@ class ColorMap: "and any number of intermediate stops." ) + KEYWORDS = ("colour", "palette", "lut", "gradient") + def build(self, mode: str, preset: str, stops: str | None = None, stops_json: str | None = None) -> tuple: if mode == "preset": return ({"mode": "preset", "preset": normalize_colormap_spec(preset)},) diff --git a/backend/nodes/crop_resize.py b/backend/nodes/crop_resize.py index fd7f539..f951191 100644 --- a/backend/nodes/crop_resize.py +++ b/backend/nodes/crop_resize.py @@ -37,6 +37,8 @@ class CropResizeField: "resizing preserves the cropped physical size." ) + KEYWORDS = ("resize", "rescale", "trim", "bilinear", "bicubic", "nearest") + def process( self, field: DataField, diff --git a/backend/nodes/cross_correlate.py b/backend/nodes/cross_correlate.py index 316cfbd..20d97ad 100644 --- a/backend/nodes/cross_correlate.py +++ b/backend/nodes/cross_correlate.py @@ -30,6 +30,8 @@ class CrossCorrelate: "alignment." ) + KEYWORDS = ("xcorr", "alignment", "registration", "drift", "match") + def process( self, field_a: DataField, diff --git a/backend/nodes/cross_section.py b/backend/nodes/cross_section.py index ce5c128..8ceeb7c 100644 --- a/backend/nodes/cross_section.py +++ b/backend/nodes/cross_section.py @@ -38,6 +38,8 @@ class CrossSection: "Drag the markers on the image to set the line endpoints. " ) + KEYWORDS = ("profile", "line profile", "extract", "slice") + def process( self, field: DataField, x1: float, y1: float, x2: float, y2: float, diff --git a/backend/nodes/cursors.py b/backend/nodes/cursors.py index c335562..1a34e3a 100644 --- a/backend/nodes/cursors.py +++ b/backend/nodes/cursors.py @@ -40,6 +40,8 @@ class Cursors: "On fields it reports x/y/z at both markers plus dx/dy/dz." ) + KEYWORDS = ("marker", "distance", "measure", "delta", "ruler") + def process( self, line, x1: float, y1: float, x2: float, y2: float, coord_pair=None, diff --git a/backend/nodes/curvature.py b/backend/nodes/curvature.py index 44ff3f3..04d385f 100644 --- a/backend/nodes/curvature.py +++ b/backend/nodes/curvature.py @@ -291,6 +291,8 @@ class Curvature: "also returns the two corresponding height profiles." ) + KEYWORDS = ("radius", "principal", "quadratic", "bow") + def process( self, field: DataField, diff --git a/backend/nodes/deconvolution.py b/backend/nodes/deconvolution.py index d61a890..c7b5d6a 100644 --- a/backend/nodes/deconvolution.py +++ b/backend/nodes/deconvolution.py @@ -34,6 +34,8 @@ class Deconvolution: "Richardson-Lucy is iterative and preserves positivity. " ) + KEYWORDS = ("wiener", "richardson lucy", "deblur", "restoration", "psf") + def process( self, field: DataField, diff --git a/backend/nodes/displacement_field.py b/backend/nodes/displacement_field.py index 9cbe329..6f49a41 100644 --- a/backend/nodes/displacement_field.py +++ b/backend/nodes/displacement_field.py @@ -39,9 +39,10 @@ class DisplacementField: "Distort an image using synthetic displacement fields. " "Supports 1D Gaussian (row-correlated), 2D Gaussian (fully correlated), " "and tear (random horizontal tear lines) distortion modes. " - "Equivalent to Gwyddion's displfield.c module." ) + KEYWORDS = ("distortion", "warp", "tear") + def process( self, field: DataField, diff --git a/backend/nodes/distribution_coercion.py b/backend/nodes/distribution_coercion.py index bcf3250..dc3d193 100644 --- a/backend/nodes/distribution_coercion.py +++ b/backend/nodes/distribution_coercion.py @@ -67,6 +67,8 @@ class DistributionCoercion: "Equivalent to Gwyddion's coerce.c module." ) + KEYWORDS = ("coerce", "histogram matching", "equalize", "uniform", "gaussian", "quantize") + def process(self, field: DataField, distribution: str, n_levels: int, processing: str) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/drift_correction.py b/backend/nodes/drift_correction.py index 258e58a..edb5b38 100644 --- a/backend/nodes/drift_correction.py +++ b/backend/nodes/drift_correction.py @@ -60,6 +60,8 @@ class DriftCorrection: "the drift offset, then shifts lines to correct. " ) + KEYWORDS = ("thermal", "piezo", "alignment", "shift", "row") + def process(self, field: DataField, reference: str, direction: str) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/dwt_anisotropy.py b/backend/nodes/dwt_anisotropy.py index 5671dae..2d92b63 100644 --- a/backend/nodes/dwt_anisotropy.py +++ b/backend/nodes/dwt_anisotropy.py @@ -163,9 +163,11 @@ class DWTAnisotropy: "Quantify surface anisotropy using a multi-level 2-D Haar wavelet decomposition. " "At each level, horizontal (HL) and vertical (LH) detail energies are compared to " "produce an X/Y energy ratio. Ratio > 1 indicates more horizontal features; " - "ratio < 1 indicates more vertical features. Equivalent to Gwyddion's dwtanisotropy.c." + "ratio < 1 indicates more vertical features." ) + KEYWORDS = ("wavelet", "haar", "directional", "texture") + def process( self, field: DataField, diff --git a/backend/nodes/entropy.py b/backend/nodes/entropy.py index 23b8c4e..5354d55 100644 --- a/backend/nodes/entropy.py +++ b/backend/nodes/entropy.py @@ -30,6 +30,8 @@ class Entropy: "H = -\u03a3 p\u00b7ln(p)." ) + KEYWORDS = ("shannon", "information", "disorder") + def process(self, field: DataField, mode: str, n_bins: int) -> tuple: n_bins = max(16, int(n_bins)) data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/extend_pad.py b/backend/nodes/extend_pad.py index 8062630..d9e6fc6 100644 --- a/backend/nodes/extend_pad.py +++ b/backend/nodes/extend_pad.py @@ -34,6 +34,8 @@ class ExtendPad: "Mirror and periodic modes avoid edge discontinuities for FFT. " ) + KEYWORDS = ("border", "margin", "mirror", "periodic") + def process(self, field: DataField, top: int, bottom: int, left: int, right: int, method: str) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/facet_analysis.py b/backend/nodes/facet_analysis.py index 5298754..2ccc062 100644 --- a/backend/nodes/facet_analysis.py +++ b/backend/nodes/facet_analysis.py @@ -32,6 +32,8 @@ class FacetAnalysis: "Intensity represents how much surface area faces each orientation. " ) + KEYWORDS = ("orientation", "stereographic", "azimuth", "inclination", "slope", "crystal") + def process(self, field: DataField, n_bins: int, kernel_size: int) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/feature_detection.py b/backend/nodes/feature_detection.py index 1d539e5..04fe2b9 100644 --- a/backend/nodes/feature_detection.py +++ b/backend/nodes/feature_detection.py @@ -40,6 +40,8 @@ class FeatureDetection: "Outputs a feature map and a table of detected feature locations. " ) + KEYWORDS = ("canny", "harris", "corner", "edge", "interest point", "roi") + def process( self, field: DataField, diff --git a/backend/nodes/fft_1d.py b/backend/nodes/fft_1d.py index a634122..677a017 100644 --- a/backend/nodes/fft_1d.py +++ b/backend/nodes/fft_1d.py @@ -30,6 +30,8 @@ class FFT1D: "Returns the FFT spectrum of the line, and identifies peaks." ) + KEYWORDS = ("fourier", "frequency", "spectrum", "period") + def process( self, profile, ) -> tuple: diff --git a/backend/nodes/fft_2d.py b/backend/nodes/fft_2d.py index eab168e..75b0182 100644 --- a/backend/nodes/fft_2d.py +++ b/backend/nodes/fft_2d.py @@ -34,6 +34,8 @@ class FFT2D: "Outputs log magnitude, magnitude, phase, and PSDF as separate channels. " ) + KEYWORDS = ("fourier", "frequency", "spectrum", "psdf", "magnitude", "phase") + def process(self, field: DataField, windowing: str, level: str) -> tuple: data = preprocess_spectral_data(field, level=level, windowing=windowing) F = np.fft.fftshift(np.fft.fft2(data)) diff --git a/backend/nodes/fft_2d_inverse.py b/backend/nodes/fft_2d_inverse.py index 88c7d4f..9ce4c7e 100644 --- a/backend/nodes/fft_2d_inverse.py +++ b/backend/nodes/fft_2d_inverse.py @@ -29,6 +29,8 @@ class FFT2DInverse: "or PSDF/phase) from the 2D FFT node. If phase is omitted, zero phase is assumed." ) + KEYWORDS = ("fourier", "ifft", "reconstruct") + def process(self, spectrum: DataField, representation: str, phase: DataField | None = None) -> tuple: if spectrum.domain != "frequency": raise ValueError("Inverse 2D FFT requires a frequency-domain DATA_FIELD input.") diff --git a/backend/nodes/field_arithmetic.py b/backend/nodes/field_arithmetic.py index f96cdd5..a894a19 100644 --- a/backend/nodes/field_arithmetic.py +++ b/backend/nodes/field_arithmetic.py @@ -29,6 +29,8 @@ class FieldArithmetic: "hypot computes sqrt(a² + b²) per pixel. " ) + KEYWORDS = ("math", "add", "subtract", "multiply", "divide", "hypot") + def process(self, field_a: DataField, field_b: DataField, operation: str) -> tuple: if field_a.data.shape != field_b.data.shape: raise ValueError( diff --git a/backend/nodes/filter_custom.py b/backend/nodes/filter_custom.py index 3fc1907..f96babd 100644 --- a/backend/nodes/filter_custom.py +++ b/backend/nodes/filter_custom.py @@ -99,6 +99,8 @@ class CustomConvolution: "Example sharpen: '0 -1 0 / -1 5 -1 / 0 -1 0' (use newlines, not slashes). " ) + KEYWORDS = ("kernel", "sharpen", "custom filter", "convolution") + def process( self, field: DataField, diff --git a/backend/nodes/filter_fft.py b/backend/nodes/filter_fft.py index 3c88900..6a29798 100644 --- a/backend/nodes/filter_fft.py +++ b/backend/nodes/filter_fft.py @@ -46,6 +46,8 @@ class FFTFilter: "with a Butterworth roll-off. Cutoffs are fractions of the Nyquist frequency." ) + KEYWORDS = ("butterworth", "lowpass", "highpass", "bandpass", "notch", "fourier") + def process(self, input, filter_type: str, cutoff: float, cutoff_high: float, order: int) -> tuple: if isinstance(input, DataField): diff --git a/backend/nodes/filter_gaussian.py b/backend/nodes/filter_gaussian.py index a2aef8e..cd1ee1f 100644 --- a/backend/nodes/filter_gaussian.py +++ b/backend/nodes/filter_gaussian.py @@ -20,7 +20,9 @@ class GaussianFilter: FUNCTION = "process" DESCRIPTION = "Apply a Gaussian blur." - + + KEYWORDS = ("blur", "smooth", "lowpass") + def process(self, field: DataField, sigma: float) -> tuple: from scipy.ndimage import gaussian_filter data = gaussian_filter(field.data, sigma=float(sigma)) diff --git a/backend/nodes/filter_kuwahara.py b/backend/nodes/filter_kuwahara.py index 08a0a48..99a3624 100644 --- a/backend/nodes/filter_kuwahara.py +++ b/backend/nodes/filter_kuwahara.py @@ -75,10 +75,12 @@ class KuwaharaFilter: DESCRIPTION = ( """ Edge-preserving smoothing using Kuwahara's minimum-variance quadrant method. - "Unlike Gaussian blur, sharp boundaries are preserved. + "Unlike Gaussian blur, sharp boundaries are preserved. """ ) + KEYWORDS = ("edge preserving", "smooth", "denoise") + def process(self, field: DataField, iterations: int) -> tuple: data = np.asarray(field.data, dtype=np.float64) iterations = max(1, int(iterations)) diff --git a/backend/nodes/filter_median.py b/backend/nodes/filter_median.py index 18650fc..e25bb40 100644 --- a/backend/nodes/filter_median.py +++ b/backend/nodes/filter_median.py @@ -21,6 +21,8 @@ class MedianFilter: DESCRIPTION = "Apply a median filter." + KEYWORDS = ("denoise", "despeckle", "salt pepper") + def process(self, field: DataField, size: int) -> tuple: from scipy.ndimage import median_filter size = max(1, int(size)) diff --git a/backend/nodes/filter_rank.py b/backend/nodes/filter_rank.py index 4643088..4d86c10 100644 --- a/backend/nodes/filter_rank.py +++ b/backend/nodes/filter_rank.py @@ -34,6 +34,8 @@ class RankFilter: "median the 50th percentile. Custom percentile allows any rank. " ) + KEYWORDS = ("erosion", "dilation", "percentile", "morphology", "minimum", "maximum") + def process(self, field: DataField, operation: str, radius: int, percentile: float) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/fix_zero.py b/backend/nodes/fix_zero.py index 7b6e219..58b0ab9 100644 --- a/backend/nodes/fix_zero.py +++ b/backend/nodes/fix_zero.py @@ -24,6 +24,8 @@ class FixZero: "Shift data so that the minimum (or mean/median) is zero. " ) + KEYWORDS = ("offset", "subtract", "baseline", "datum") + def process(self, field: DataField, method: str) -> tuple: data = field.data.copy() if method == "min": diff --git a/backend/nodes/flatten_base.py b/backend/nodes/flatten_base.py index 24781b2..dd51094 100644 --- a/backend/nodes/flatten_base.py +++ b/backend/nodes/flatten_base.py @@ -33,6 +33,8 @@ class FlattenBase: "this ignores tall features that would bias the fit. " ) + KEYWORDS = ("level", "background", "substrate", "tilt") + def process(self, field: DataField, threshold_percentile: float, poly_degree: int) -> tuple: data = np.asarray(field.data, dtype=np.float64) yres, xres = data.shape diff --git a/backend/nodes/flip.py b/backend/nodes/flip.py index 6657bac..701d2ec 100644 --- a/backend/nodes/flip.py +++ b/backend/nodes/flip.py @@ -29,6 +29,8 @@ class FlipField: "Physical extents are preserved, and stored markup overlays are mirrored with the data." ) + KEYWORDS = ("mirror", "reflect", "invert") + def process(self, field: DataField, axis: str) -> tuple: axis_name = str(axis).strip().lower() if axis_name == "x": diff --git a/backend/nodes/folder.py b/backend/nodes/folder.py index 61687fe..6a62c9d 100644 --- a/backend/nodes/folder.py +++ b/backend/nodes/folder.py @@ -20,9 +20,10 @@ class Folder: DESCRIPTION = ( "Pick a folder and output its directory path plus one file socket per compatible image, array, or SPM file inside it. " - "Supported files include common images, .npy/.npz arrays, and .gwy/.sxm/.ibw scans." ) + KEYWORDS = ("directory", "batch", "load", "open") + def list_files(self, folder: str) -> tuple: entries = list_folder_paths(folder) if not entries: diff --git a/backend/nodes/fractal_dimension.py b/backend/nodes/fractal_dimension.py index b27eedb..21423f5 100644 --- a/backend/nodes/fractal_dimension.py +++ b/backend/nodes/fractal_dimension.py @@ -310,6 +310,8 @@ class FractalDimension: "power-spectrum, or HHCF methods. The in-node graph shows the log-log curve and lets you drag the fit range." ) + KEYWORDS = ("partitioning", "cube counting", "triangulation", "hhcf", "box counting", "self similar") + def process( self, field, diff --git a/backend/nodes/fractal_interpolation.py b/backend/nodes/fractal_interpolation.py index 776b98d..3c5d5f0 100644 --- a/backend/nodes/fractal_interpolation.py +++ b/backend/nodes/fractal_interpolation.py @@ -32,6 +32,8 @@ class FractalInterpolation: "infill that preserves texture. Better than Laplace for rough surfaces. " ) + KEYWORDS = ("inpaint", "fill", "hole", "infill") + def process(self, field: DataField, mask: np.ndarray, iterations: int) -> tuple: data = np.asarray(field.data, dtype=np.float64).copy() hole = mask_to_bool(mask) diff --git a/backend/nodes/freq_split.py b/backend/nodes/freq_split.py index 3aaa4cf..88222d8 100644 --- a/backend/nodes/freq_split.py +++ b/backend/nodes/freq_split.py @@ -31,6 +31,8 @@ class FrequencySplit: "frequency (0.5 = no filtering, 0.001 = very aggressive). " ) + KEYWORDS = ("lowpass", "highpass", "decompose", "background", "detail") + def process(self, field: DataField, cutoff: float) -> tuple: data = np.asarray(field.data, dtype=np.float64) yres, xres = data.shape diff --git a/backend/nodes/gradient.py b/backend/nodes/gradient.py index 0baa9e2..dace331 100644 --- a/backend/nodes/gradient.py +++ b/backend/nodes/gradient.py @@ -29,6 +29,8 @@ class Gradient: "'azimuth' gives the local slope direction in radians via atan2(gy, gx). " ) + KEYWORDS = ("sobel", "slope", "derivative", "azimuth") + def process(self, field: DataField, component: str) -> tuple: from backend.nodes.surface_common import physical_sobel_gradient, slope_unit diff --git a/backend/nodes/grain_analysis.py b/backend/nodes/grain_analysis.py index 8078149..8bec287 100644 --- a/backend/nodes/grain_analysis.py +++ b/backend/nodes/grain_analysis.py @@ -27,6 +27,8 @@ class GrainAnalysis: "statistics: area, equivalent diameter, mean/max height, bounding box. " ) + KEYWORDS = ("particle", "blob", "label", "connected components", "area") + def process(self, field: DataField, mask: np.ndarray, min_size: int) -> tuple: from scipy.ndimage import label diff --git a/backend/nodes/grain_cross.py b/backend/nodes/grain_cross.py index 439e3d8..9ab2876 100644 --- a/backend/nodes/grain_cross.py +++ b/backend/nodes/grain_cross.py @@ -38,6 +38,8 @@ class GrainCross: "plus Pearson correlation coefficient. " ) + KEYWORDS = ("pearson", "scatter", "correlate", "property") + def process(self, field_a: DataField, field_b: DataField, mask: np.ndarray, property_a: str, property_b: str, min_size: int) -> tuple: data_a = np.asarray(field_a.data, dtype=np.float64) diff --git a/backend/nodes/grain_distance_transform.py b/backend/nodes/grain_distance_transform.py index 7b596b4..5531166 100644 --- a/backend/nodes/grain_distance_transform.py +++ b/backend/nodes/grain_distance_transform.py @@ -115,6 +115,8 @@ class GrainDistanceTransform: "image-boundary handling matching mask_edt." ) + KEYWORDS = ("edt", "euclidean", "chessboard", "cityblock", "manhattan") + def process( self, field: DataField, diff --git a/backend/nodes/grain_distributions.py b/backend/nodes/grain_distributions.py index 577c2ba..0d2ade0 100644 --- a/backend/nodes/grain_distributions.py +++ b/backend/nodes/grain_distributions.py @@ -36,6 +36,8 @@ class GrainDistributions: "max height, volume, and boundary length. " ) + KEYWORDS = ("histogram", "particle", "diameter", "area", "volume") + def process(self, field: DataField, mask: np.ndarray, property: str, n_bins: int, min_size: int) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/grain_edge.py b/backend/nodes/grain_edge.py index 2352c7c..d402cb1 100644 --- a/backend/nodes/grain_edge.py +++ b/backend/nodes/grain_edge.py @@ -33,6 +33,8 @@ class GrainEdge: "controls the boundary thickness in pixels. " ) + KEYWORDS = ("boundary", "outline", "perimeter", "contour") + def process(self, field: DataField, mask: np.ndarray, width: int) -> tuple: grain = mask_to_bool(mask) diff --git a/backend/nodes/grain_filter.py b/backend/nodes/grain_filter.py index e0a1f98..4bcb3a0 100644 --- a/backend/nodes/grain_filter.py +++ b/backend/nodes/grain_filter.py @@ -31,6 +31,8 @@ class GrainFilter: "'remove_border': discard any grain that touches the image edge. " ) + KEYWORDS = ("particle", "size filter", "despeckle", "area") + def process( self, mask: np.ndarray, diff --git a/backend/nodes/grain_mark.py b/backend/nodes/grain_mark.py index cea6965..2230f35 100644 --- a/backend/nodes/grain_mark.py +++ b/backend/nodes/grain_mark.py @@ -32,10 +32,12 @@ class GrainMark: DESCRIPTION = ( "Mark grains by thresholding height, slope magnitude, or curvature. " - "Thresholds are relative (0–1) to the data range. Small regions below " + "Thresholds are relative (0-1) to the data range. Small regions below " "min_size pixels are removed. Use inverted to mark valleys instead of peaks. " ) + KEYWORDS = ("threshold", "segment", "peak", "particle") + def process(self, field: DataField, criterion: str, threshold_low: float, threshold_high: float, min_size: int, inverted: bool) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/grain_summary.py b/backend/nodes/grain_summary.py index 7437b38..b148ee0 100644 --- a/backend/nodes/grain_summary.py +++ b/backend/nodes/grain_summary.py @@ -32,6 +32,8 @@ class GrainSummary: "coverage fraction, mean/median area, total volume, and height statistics. " ) + KEYWORDS = ("particle", "count", "density", "coverage", "statistics") + def process(self, field: DataField, mask: np.ndarray, min_size: int) -> tuple: data = np.asarray(field.data, dtype=np.float64) grain_mask = mask_to_bool(mask) diff --git a/backend/nodes/grain_visualization.py b/backend/nodes/grain_visualization.py index f2e7c6d..59e3f13 100644 --- a/backend/nodes/grain_visualization.py +++ b/backend/nodes/grain_visualization.py @@ -181,6 +181,8 @@ class GrainVisualization: "Equivalent to Gwyddion's grain selection visualization (grain_makesel)." ) + KEYWORDS = ("ellipse", "disc", "bounding box", "centroid", "inscribed", "label") + def process(self, field: DataField, mask: np.ndarray, style: str, fill: bool) -> tuple: mask_bool = mask_to_bool(mask) labels, n_grains = label(mask_bool.astype(np.int32)) diff --git a/backend/nodes/histogram.py b/backend/nodes/histogram.py index cccf320..f06cd6d 100644 --- a/backend/nodes/histogram.py +++ b/backend/nodes/histogram.py @@ -34,6 +34,8 @@ class Histogram: "Outputs marker measurements while showing the histogram interactively in-node. " ) + KEYWORDS = ("distribution", "height distribution", "dh") + def process( self, field: DataField, diff --git a/backend/nodes/hough_transform.py b/backend/nodes/hough_transform.py index 519a1fe..1aa2661 100644 --- a/backend/nodes/hough_transform.py +++ b/backend/nodes/hough_transform.py @@ -39,6 +39,8 @@ class HoughTransform: "For circles: centre coordinates and radius. " ) + KEYWORDS = ("line detection", "circle detection", "shape detection") + def process( self, field: DataField, diff --git a/backend/nodes/image.py b/backend/nodes/image.py index 68439b7..784ae76 100644 --- a/backend/nodes/image.py +++ b/backend/nodes/image.py @@ -31,11 +31,13 @@ class Image: DESCRIPTION = ( "Load any supported file. " - "SPM formats (.gwy, .sxm, .ibw) and HDF5 (.h5, .hdf5) provide calibrated dimensions; " + "SPM and HDF5 provide calibrated dimensions; " "each channel gets its own output. " - "Images (.png, .tiff, .jpg) and arrays (.npy, .npz) are loaded as uncalibrated fields." + "Images and arrays are loaded as uncalibrated fields." ) + KEYWORDS = ("load", "open", "file", "import", "gwy", "sxm", "ibw", "png", "tiff", "npy", "hdf5") + def load(self, filename: str = "", colormap: str = "viridis", colormap_map=None, path: str | None = None): selected_path = str(path).strip() if path is not None else str(filename).strip() if not selected_path: diff --git a/backend/nodes/image_demo.py b/backend/nodes/image_demo.py index 9710810..f897d2a 100644 --- a/backend/nodes/image_demo.py +++ b/backend/nodes/image_demo.py @@ -26,6 +26,8 @@ class ImageDemo: DESCRIPTION = "Load a bundled demo file so you can try the app without providing your own data." + KEYWORDS = ("example", "sample", "test") + def load(self, name: str = "", colormap: str = "viridis", colormap_map=None): from backend.nodes.image import Image loader = Image() diff --git a/backend/nodes/image_stitch.py b/backend/nodes/image_stitch.py index daabb4d..9515ffa 100644 --- a/backend/nodes/image_stitch.py +++ b/backend/nodes/image_stitch.py @@ -55,6 +55,8 @@ class ImageStitch: "'auto' uses cross-correlation to determine the best placement. " ) + KEYWORDS = ("mosaic", "merge", "combine", "panorama", "blend") + def process(self, field_a: DataField, field_b: DataField, direction: str, blend: str) -> tuple: a = np.asarray(field_a.data, dtype=np.float64) b = np.asarray(field_b.data, dtype=np.float64) diff --git a/backend/nodes/immerse_detail.py b/backend/nodes/immerse_detail.py index 6dc761d..af77af1 100644 --- a/backend/nodes/immerse_detail.py +++ b/backend/nodes/immerse_detail.py @@ -30,6 +30,8 @@ class ImmerseDetail: "image using cross-correlation to find the best position. " ) + KEYWORDS = ("inset", "zoom", "merge", "overlay") + def process(self, overview: DataField, detail: DataField, blend: str) -> tuple: ov = np.asarray(overview.data, dtype=np.float64) dt = np.asarray(detail.data, dtype=np.float64) diff --git a/backend/nodes/laplace_interpolation.py b/backend/nodes/laplace_interpolation.py index 188fbf8..db127b9 100644 --- a/backend/nodes/laplace_interpolation.py +++ b/backend/nodes/laplace_interpolation.py @@ -32,6 +32,8 @@ class LaplaceInterpolation: "Produces a smooth, harmonic interpolation without overshooting. " ) + KEYWORDS = ("inpaint", "fill", "hole", "infill", "harmonic") + def process(self, field: DataField, mask: np.ndarray, iterations: int) -> tuple: data = np.asarray(field.data, dtype=np.float64).copy() hole = mask_to_bool(mask) diff --git a/backend/nodes/lateral_force_sim.py b/backend/nodes/lateral_force_sim.py index dfeb8f1..28d354c 100644 --- a/backend/nodes/lateral_force_sim.py +++ b/backend/nodes/lateral_force_sim.py @@ -43,6 +43,8 @@ class LateralForceSim: "the sample surface." ) + KEYWORDS = ("lfm", "friction", "ffm", "tribology") + def process( self, field: DataField, diff --git a/backend/nodes/lattice_measurement.py b/backend/nodes/lattice_measurement.py index ae9a86c..0ca90ff 100644 --- a/backend/nodes/lattice_measurement.py +++ b/backend/nodes/lattice_measurement.py @@ -67,6 +67,8 @@ class LatticeMeasurement: "and reports lattice vectors (spacing and angle). " ) + KEYWORDS = ("periodic", "crystal", "unit cell", "spacing", "atomic") + def process(self, field: DataField, method: str) -> tuple: data = np.asarray(field.data, dtype=np.float64) data = data - data.mean() diff --git a/backend/nodes/level_facet.py b/backend/nodes/level_facet.py index 366e78d..2bafcfd 100644 --- a/backend/nodes/level_facet.py +++ b/backend/nodes/level_facet.py @@ -125,6 +125,8 @@ class FacetLevelField: "selection and expects topographic data with compatible XY and Z units." ) + KEYWORDS = ("flatten", "tilt", "plane") + def process( self, field: DataField, diff --git a/backend/nodes/level_grains.py b/backend/nodes/level_grains.py index 7dffb91..bb38282 100644 --- a/backend/nodes/level_grains.py +++ b/backend/nodes/level_grains.py @@ -34,6 +34,8 @@ class LevelGrains: "Useful for consistent grain height comparisons. " ) + KEYWORDS = ("align", "baseline", "flatten", "particle") + def process(self, field: DataField, mask: np.ndarray, reference: str) -> tuple: data = np.asarray(field.data, dtype=np.float64).copy() grain_mask = mask_to_bool(mask) diff --git a/backend/nodes/level_plane.py b/backend/nodes/level_plane.py index ad49083..09d96de 100644 --- a/backend/nodes/level_plane.py +++ b/backend/nodes/level_plane.py @@ -55,6 +55,8 @@ class PlaneLevelField: "for flattening around features, similar to masked plane fitting workflows in Gwyddion." ) + KEYWORDS = ("flatten", "tilt", "background") + def process( self, field: DataField, diff --git a/backend/nodes/level_poly.py b/backend/nodes/level_poly.py index 302c6a8..def47f8 100644 --- a/backend/nodes/level_poly.py +++ b/backend/nodes/level_poly.py @@ -26,6 +26,8 @@ class PolyLevelField: "Fit and subtract a polynomial background of given degree in x and y. " ) + KEYWORDS = ("flatten", "background", "bow", "curvature") + def process(self, field: DataField, degree_x: int, degree_y: int) -> tuple: data = field.data.copy() yres, xres = data.shape diff --git a/backend/nodes/line_correction.py b/backend/nodes/line_correction.py index 0464c10..843cee6 100644 --- a/backend/nodes/line_correction.py +++ b/backend/nodes/line_correction.py @@ -296,6 +296,8 @@ class LineCorrection: "and the step-line correction path from Gwyddion's linecorrect/linematch modules." ) + KEYWORDS = ("row", "linematch", "linecorrect", "destripe", "scanline", "align") + def process( self, field: DataField, diff --git a/backend/nodes/local_contrast.py b/backend/nodes/local_contrast.py index c1772b8..f4eda8b 100644 --- a/backend/nodes/local_contrast.py +++ b/backend/nodes/local_contrast.py @@ -28,6 +28,8 @@ class LocalContrast: "Reveals fine surface features that are hidden by global contrast range. " ) + KEYWORDS = ("clahe", "enhance", "adaptive", "dynamic range") + def process(self, field: DataField, kernel_size: int, weight: float) -> tuple: from scipy.ndimage import minimum_filter, maximum_filter @@ -50,7 +52,7 @@ class LocalContrast: safe_range = np.where(local_range > eps, local_range, 1.0) global_span = global_max - global_min if global_span <= eps: - # Uniform field – nothing to enhance. + # Uniform field - nothing to enhance. return (field.replace(data=data.copy()),) enhancement_factor = global_span / safe_range diff --git a/backend/nodes/logistic_classification.py b/backend/nodes/logistic_classification.py index 9bfef9b..f9fc182 100644 --- a/backend/nodes/logistic_classification.py +++ b/backend/nodes/logistic_classification.py @@ -172,6 +172,8 @@ class LogisticClassification: "generates pseudo-labels automatically." ) + KEYWORDS = ("machine learning", "regression", "segment", "ml", "neural") + def process( self, field: DataField, diff --git a/backend/nodes/mark_disconnected.py b/backend/nodes/mark_disconnected.py index fd4e3d7..57e7c85 100644 --- a/backend/nodes/mark_disconnected.py +++ b/backend/nodes/mark_disconnected.py @@ -41,6 +41,8 @@ class MarkDisconnected: "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) diff --git a/backend/nodes/markup.py b/backend/nodes/markup.py index cf45c6b..67a3a36 100644 --- a/backend/nodes/markup.py +++ b/backend/nodes/markup.py @@ -43,6 +43,8 @@ class Markup: "or rasterize markup directly onto an IMAGE." ) + KEYWORDS = ("annotate", "arrow", "rectangle", "circle", "line", "draw", "overlay") + def process( self, input, diff --git a/backend/nodes/mask_draw.py b/backend/nodes/mask_draw.py index 210651e..bdf7925 100644 --- a/backend/nodes/mask_draw.py +++ b/backend/nodes/mask_draw.py @@ -33,6 +33,8 @@ class DrawMask: "and invert flips the final binary output." ) + KEYWORDS = ("paint", "brush", "roi", "annotate", "manual") + def process(self, field: DataField, pen_size: int, invert: bool, mask_paths: str) -> tuple: strokes = _parse_mask_strokes(mask_paths) mask = _rasterize_mask(field.xres, field.yres, strokes, pen_size) diff --git a/backend/nodes/mask_morphology.py b/backend/nodes/mask_morphology.py index 45df80a..b8e27a8 100644 --- a/backend/nodes/mask_morphology.py +++ b/backend/nodes/mask_morphology.py @@ -38,6 +38,8 @@ class MaskMorphology: "close (dilate then erode) fills small holes. " ) + KEYWORDS = ("dilate", "erode", "open", "close", "binary") + def process(self, mask: np.ndarray, operation: str, radius: int, shape: str, field: DataField | None = None) -> tuple: from scipy.ndimage import binary_closing, binary_dilation, binary_erosion, binary_opening diff --git a/backend/nodes/mask_noisify.py b/backend/nodes/mask_noisify.py index 1e7287a..52c7e93 100644 --- a/backend/nodes/mask_noisify.py +++ b/backend/nodes/mask_noisify.py @@ -41,6 +41,8 @@ class MaskNoisify: "Use a fixed seed for reproducible results." ) + KEYWORDS = ("random", "perturb", "boundary", "roughen", "jitter") + def process(self, mask: np.ndarray, density: float, direction: str, boundaries_only: bool, seed: int, field: DataField | None = None) -> tuple: diff --git a/backend/nodes/mask_operations.py b/backend/nodes/mask_operations.py index d018af8..8765453 100644 --- a/backend/nodes/mask_operations.py +++ b/backend/nodes/mask_operations.py @@ -48,6 +48,8 @@ class MaskOperations: "XNOR, directional subtraction, implication, pass-through, and constant true/false outputs." ) + KEYWORDS = ("boolean", "and", "or", "xor", "logic", "union", "intersect", "subtract") + def process( self, mask_a: np.ndarray, diff --git a/backend/nodes/mask_shift.py b/backend/nodes/mask_shift.py index a190eeb..da55c38 100644 --- a/backend/nodes/mask_shift.py +++ b/backend/nodes/mask_shift.py @@ -40,6 +40,8 @@ class MaskShift: "wrap (periodic roll), or mirror (reflected padding)." ) + KEYWORDS = ("translate", "offset", "move", "roll") + def process(self, mask: np.ndarray, shift_x: int, shift_y: int, border_mode: str, field: DataField | None = None) -> tuple: binary = mask_to_bool(mask) diff --git a/backend/nodes/mask_threshold.py b/backend/nodes/mask_threshold.py index dfcb8b0..ef717b0 100644 --- a/backend/nodes/mask_threshold.py +++ b/backend/nodes/mask_threshold.py @@ -32,6 +32,8 @@ class ThresholdMask: "Otsu automatically finds the optimal threshold. " ) + KEYWORDS = ("otsu", "binarize", "segment", "cutoff", "level") + def process(self, field: DataField, method: str, threshold: float, direction: str) -> tuple: data = field.data diff --git a/backend/nodes/median_background.py b/backend/nodes/median_background.py index 5ac7680..e97ff55 100644 --- a/backend/nodes/median_background.py +++ b/backend/nodes/median_background.py @@ -33,6 +33,8 @@ class MedianBackground: "for surfaces with sparse tall features. " ) + KEYWORDS = ("rolling ball", "flatten", "level", "subtract", "baseline") + def process(self, field: DataField, radius: int, output: str) -> tuple: data = np.asarray(field.data, dtype=np.float64) size = 2 * radius + 1 diff --git a/backend/nodes/mfm_analysis.py b/backend/nodes/mfm_analysis.py index 44fa1fa..0401861 100644 --- a/backend/nodes/mfm_analysis.py +++ b/backend/nodes/mfm_analysis.py @@ -37,6 +37,8 @@ class MFMAnalysis: "magnetisation estimates the z-component of sample magnetisation. " ) + KEYWORDS = ("magnetic", "force gradient", "stray field", "phase", "charge density", "magnetisation") + def process(self, field: DataField, operation: str, lift_height: float) -> tuple: data = np.asarray(field.data, dtype=np.float64) yres, xres = data.shape diff --git a/backend/nodes/mfm_current.py b/backend/nodes/mfm_current.py index 18deab2..3c0cc51 100644 --- a/backend/nodes/mfm_current.py +++ b/backend/nodes/mfm_current.py @@ -49,6 +49,8 @@ class MFMCurrentSimulation: "gradient dHz/dz." ) + KEYWORDS = ("magnetic", "biot savart", "wire", "strip", "force", "dipole", "tip") + def process( self, field: DataField, diff --git a/backend/nodes/mfm_domains.py b/backend/nodes/mfm_domains.py index 4b97297..d86941e 100644 --- a/backend/nodes/mfm_domains.py +++ b/backend/nodes/mfm_domains.py @@ -49,6 +49,8 @@ class MFMDomainGeneration: "FFT-based transfer functions, suitable for MFM simulation and testing. " ) + KEYWORDS = ("magnetic", "stripe", "stray field", "synthetic", "simulation") + def process( self, field: DataField, diff --git a/backend/nodes/multi_profile.py b/backend/nodes/multi_profile.py index 0a67046..77a6763 100644 --- a/backend/nodes/multi_profile.py +++ b/backend/nodes/multi_profile.py @@ -33,6 +33,8 @@ class MultipleProfiles: "profile, mean averages both, difference subtracts b from a. " ) + KEYWORDS = ("line profile", "compare", "overlay", "cross section") + def process(self, field_a: DataField, field_b: DataField, row: int, direction: str, mode: str) -> tuple: a = np.asarray(field_a.data, dtype=np.float64) diff --git a/backend/nodes/mutual_crop.py b/backend/nodes/mutual_crop.py index 02a877f..e8afc93 100644 --- a/backend/nodes/mutual_crop.py +++ b/backend/nodes/mutual_crop.py @@ -31,6 +31,8 @@ class MutualCrop: "different times or with slight position offsets. " ) + KEYWORDS = ("align", "overlap", "registration", "cross correlation", "match") + def process(self, field_a: DataField, field_b: DataField) -> tuple: a = np.asarray(field_a.data, dtype=np.float64) b = np.asarray(field_b.data, dtype=np.float64) diff --git a/backend/nodes/neural_classification.py b/backend/nodes/neural_classification.py index 24acc1e..a4f05c6 100644 --- a/backend/nodes/neural_classification.py +++ b/backend/nodes/neural_classification.py @@ -126,6 +126,8 @@ class NeuralClassification: "Equivalent in purpose to Gwyddion's neural.c classifier." ) + KEYWORDS = ("machine learning", "ml", "segment", "nn", "feedforward", "classifier") + def process( self, field: DataField, diff --git a/backend/nodes/note.py b/backend/nodes/note.py index f2a5444..5aa14b3 100644 --- a/backend/nodes/note.py +++ b/backend/nodes/note.py @@ -32,8 +32,8 @@ def _parse_ibw_note(note_bytes: bytes) -> list[dict]: return rows -@register_node(display_name="Note") -class Note: +@register_node(display_name="Igor Note") +class IgorNote: @classmethod def INPUT_TYPES(cls): return { @@ -55,6 +55,8 @@ class Note: "as a table of key/value pairs." ) + KEYWORDS = ("ibw", "metadata", "header", "parameters", "igor") + def load(self, filename: str = "", path: str | None = None) -> tuple: selected = str(path).strip() if path is not None else str(filename).strip() if not selected: diff --git a/backend/nodes/outlier_mask.py b/backend/nodes/outlier_mask.py index 6c6db9f..2230c6b 100644 --- a/backend/nodes/outlier_mask.py +++ b/backend/nodes/outlier_mask.py @@ -32,6 +32,8 @@ class OutlierMask: "low outliers, or both. Quick way to identify noise spikes and defects. " ) + KEYWORDS = ("sigma", "zscore", "spikes", "defect", "anomaly", "despeckle") + def process(self, field: DataField, sigma_threshold: float, mode: str) -> tuple: data = np.asarray(field.data, dtype=np.float64) mean = data.mean() diff --git a/backend/nodes/perspective_correction.py b/backend/nodes/perspective_correction.py index 38e4788..f9e8eae 100644 --- a/backend/nodes/perspective_correction.py +++ b/backend/nodes/perspective_correction.py @@ -39,6 +39,8 @@ class PerspectiveCorrection: "a rectangle." ) + KEYWORDS = ("keystone", "homography", "projective", "warp", "quadrilateral", "distortion") + def process(self, field: DataField, top_left_x: float, top_left_y: float, top_right_x: float, top_right_y: float, diff --git a/backend/nodes/pfm_analysis.py b/backend/nodes/pfm_analysis.py index 0de2d1d..0e86d24 100644 --- a/backend/nodes/pfm_analysis.py +++ b/backend/nodes/pfm_analysis.py @@ -41,6 +41,8 @@ class PFMAnalysis: "inclination angle (zero in 2D mode)." ) + KEYWORDS = ("piezoresponse", "polarization", "ferroelectric", "vpfm", "lpfm", "domain") + def process( self, vpfm_amplitude: DataField, diff --git a/backend/nodes/pixel_binning.py b/backend/nodes/pixel_binning.py index 318d49d..247af64 100644 --- a/backend/nodes/pixel_binning.py +++ b/backend/nodes/pixel_binning.py @@ -31,6 +31,8 @@ class PixelBinning: "resampling. Pixels that don't fill a complete block are trimmed. " ) + KEYWORDS = ("downsample", "block", "reduce", "coarsen", "average") + def process(self, field: DataField, bin_size: int, method: str) -> tuple: data = np.asarray(field.data, dtype=np.float64) yres, xres = data.shape diff --git a/backend/nodes/pixel_classification.py b/backend/nodes/pixel_classification.py index f69d36d..382fcc8 100644 --- a/backend/nodes/pixel_classification.py +++ b/backend/nodes/pixel_classification.py @@ -178,9 +178,10 @@ class PixelClassification: "Classify pixels into discrete classes based on height, slope, and/or curvature. " "Single-feature modes use threshold-based classification (Otsu, equal range, or quantile). " "Multi-feature modes (height_slope, all) use k-means clustering. " - "Equivalent to Gwyddion's classify.c module." ) + KEYWORDS = ("kmeans", "cluster", "otsu", "segment", "quantile", "slope", "curvature") + def process(self, field: DataField, n_classes: int, feature: str, method: str) -> tuple: data = np.asarray(field.data, dtype=np.float64) maps = _feature_maps(data, feature) diff --git a/backend/nodes/poly_distort.py b/backend/nodes/poly_distort.py index 058f7fa..a2fe58f 100644 --- a/backend/nodes/poly_distort.py +++ b/backend/nodes/poly_distort.py @@ -36,6 +36,8 @@ class PolynomialDistortion: "k2 (quadratic), k3 (cubic) are applied independently to x and y axes. " ) + KEYWORDS = ("warp", "scanner", "nonlinear", "cubic", "quadratic", "barrel") + def process(self, field: DataField, k1_x: float, k1_y: float, k2_x: float, k2_y: float, diff --git a/backend/nodes/presentation_ops.py b/backend/nodes/presentation_ops.py index 07af9d3..edbbf63 100644 --- a/backend/nodes/presentation_ops.py +++ b/backend/nodes/presentation_ops.py @@ -42,9 +42,10 @@ class PresentationOps: "extract_presentation normalises the field to [0, 1]. " "attach replaces the field data with an overlay (resampled if needed). " "blend linearly mixes the field and overlay by a configurable factor. " - "Equivalent to Gwyddion's presentationops.c module." ) + KEYWORDS = ("logscale", "normalize", "blend", "attach", "overlay") + def process(self, field: DataField, operation: str, blend_factor: float, overlay: DataField | None = None) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/preview_image.py b/backend/nodes/preview_image.py index d9c096b..6a95167 100644 --- a/backend/nodes/preview_image.py +++ b/backend/nodes/preview_image.py @@ -36,6 +36,8 @@ class PreviewImage: OUTPUT_NODE = True DESCRIPTION = "Display an IMAGE or DATA_FIELD as a coloured thumbnail." + KEYWORDS = ("display", "thumbnail", "show", "view", "render") + def preview( self, colormap: str, diff --git a/backend/nodes/print_table.py b/backend/nodes/print_table.py index 217f366..d3240df 100644 --- a/backend/nodes/print_table.py +++ b/backend/nodes/print_table.py @@ -19,7 +19,9 @@ class PrintTable: FUNCTION = "print_table" OUTPUT_NODE = True - DESCRIPTION = "Send a measurement or record table to the browser as a WebSocket message for display." + DESCRIPTION = "Show a measurement or record table." + + KEYWORDS = ("display", "show", "report", "view") def print_table(self, table: list) -> tuple: emit_table(table) diff --git a/backend/nodes/psdf.py b/backend/nodes/psdf.py index 6b2927c..77d7205 100644 --- a/backend/nodes/psdf.py +++ b/backend/nodes/psdf.py @@ -27,6 +27,8 @@ class PSDF: "window RMS compensation and centered zero frequency." ) + KEYWORDS = ("power spectrum", "fourier", "frequency", "roughness", "spectral density") + def process(self, field: DataField, windowing: str, level: str) -> tuple: data = preprocess_spectral_data(field, level=level, windowing=windowing) return (psdf_field_from_data(field, data),) diff --git a/backend/nodes/psdf_log_polar.py b/backend/nodes/psdf_log_polar.py index 10a8ce4..21ed608 100644 --- a/backend/nodes/psdf_log_polar.py +++ b/backend/nodes/psdf_log_polar.py @@ -27,10 +27,12 @@ class LogPolarPSDF: DESCRIPTION = ( "Compute the power spectral density function in log-polar coordinates. " - "The x-axis is the azimuthal angle (0–360°) and y-axis is log(frequency). " + "The x-axis is the azimuthal angle (0-360°) and y-axis is log(frequency). " "Better than Cartesian PSDF for anisotropy analysis. " ) + KEYWORDS = ("power spectrum", "azimuthal", "anisotropy", "directional", "fourier") + def process(self, field: DataField, n_phi: int, n_r: int) -> tuple: data = np.asarray(field.data, dtype=np.float64) yres, xres = data.shape diff --git a/backend/nodes/psf_estimation.py b/backend/nodes/psf_estimation.py index 48f9e6e..43e0028 100644 --- a/backend/nodes/psf_estimation.py +++ b/backend/nodes/psf_estimation.py @@ -33,9 +33,10 @@ class PSFEstimation: "and an ideal (sharp) reference. The PSF can then be used with the " "Deconvolution node to restore other images. Three methods are available: " "pseudo-Wiener deconvolution, regularised least-squares, and Gaussian fit. " - "Equivalent to Gwyddion's psf.c / psf-fit.c modules." ) + KEYWORDS = ("point spread function", "deconvolution", "wiener", "gaussian", "blur", "kernel") + # ------------------------------------------------------------------ # helpers # ------------------------------------------------------------------ diff --git a/backend/nodes/radial_profile.py b/backend/nodes/radial_profile.py index 8aad709..f3cc812 100644 --- a/backend/nodes/radial_profile.py +++ b/backend/nodes/radial_profile.py @@ -30,6 +30,8 @@ class RadialProfile: "Output x-axis is radius in physical xy units. " ) + KEYWORDS = ("azimuthal average", "ring average", "circular", "isotropic") + def process(self, field: DataField, cx: float, cy: float, n_bins: int) -> tuple: yres, xres = field.data.shape diff --git a/backend/nodes/relate_fields.py b/backend/nodes/relate_fields.py index ec8aa57..4658fd5 100644 --- a/backend/nodes/relate_fields.py +++ b/backend/nodes/relate_fields.py @@ -33,6 +33,8 @@ class RelateFields: "parameters with R² goodness-of-fit. " ) + KEYWORDS = ("fit", "regression", "correlate", "polynomial", "power", "logarithmic") + def process(self, field_a: DataField, field_b: DataField, function: str) -> tuple: a = np.asarray(field_a.data, dtype=np.float64).ravel() diff --git a/backend/nodes/resample.py b/backend/nodes/resample.py index 37d94cf..fe8ef9c 100644 --- a/backend/nodes/resample.py +++ b/backend/nodes/resample.py @@ -29,6 +29,8 @@ class Resample: "Physical size (xreal, yreal) is unchanged; pixel size dx/dy scales accordingly. " ) + KEYWORDS = ("resize", "rescale", "interpolate", "bilinear", "bicubic", "nearest", "zoom") + _ORDERS = {"nearest": 0, "linear": 1, "cubic": 3} def process(self, field: DataField, width: int, height: int, interpolation: str) -> tuple: diff --git a/backend/nodes/rotate.py b/backend/nodes/rotate.py index e6e676c..7eb52a9 100644 --- a/backend/nodes/rotate.py +++ b/backend/nodes/rotate.py @@ -28,6 +28,8 @@ class RotateField: "Optionally expand the canvas to keep the full rotated field while preserving the field center." ) + KEYWORDS = ("turn", "angle", "spin", "orient") + def process( self, field: DataField, diff --git a/backend/nodes/save.py b/backend/nodes/save.py index b5b4dcc..2b64119 100644 --- a/backend/nodes/save.py +++ b/backend/nodes/save.py @@ -74,6 +74,8 @@ class Save: "Save a single graph value to disk. Supports fields, images, lines, tables, scalars, and 3D meshes." ) + KEYWORDS = ("export", "write", "download", "png", "tiff", "csv", "json", "npz", "obj", "stl") + def save( self, filename: str, diff --git a/backend/nodes/save_layers.py b/backend/nodes/save_layers.py index 0ce918d..e16eb6d 100644 --- a/backend/nodes/save_layers.py +++ b/backend/nodes/save_layers.py @@ -63,6 +63,8 @@ class SaveImage: "Click Save to write (does not auto-run)." ) + KEYWORDS = ("export", "write", "multipage", "stack", "tiff", "npz", "channels") + def save( self, filename: str, diff --git a/backend/nodes/scan_line_reorder.py b/backend/nodes/scan_line_reorder.py index 5d7aa6d..a5d868f 100644 --- a/backend/nodes/scan_line_reorder.py +++ b/backend/nodes/scan_line_reorder.py @@ -31,6 +31,8 @@ class ScanLineReorder: "odd or even rows and stretches to fill. flip_vertical reverses row order. " ) + KEYWORDS = ("meander", "serpentine", "interlace", "trace retrace", "reverse") + def process(self, field: DataField, operation: str) -> tuple: data = np.asarray(field.data, dtype=np.float64).copy() yres, xres = data.shape diff --git a/backend/nodes/scar_removal.py b/backend/nodes/scar_removal.py index 4896da8..3017c5d 100644 --- a/backend/nodes/scar_removal.py +++ b/backend/nodes/scar_removal.py @@ -197,6 +197,8 @@ class ScarRemoval: "then interpolate over the detected mask with a Laplace-style inpaint." ) + KEYWORDS = ("stripe", "streak", "glitch", "artifact", "inpaint", "destripe") + def process( self, field: DataField, diff --git a/backend/nodes/sem_simulation.py b/backend/nodes/sem_simulation.py index 552d21a..3367465 100644 --- a/backend/nodes/sem_simulation.py +++ b/backend/nodes/sem_simulation.py @@ -43,6 +43,8 @@ class SEMSimulation: "edge-enhanced contrast similar to real SEM images." ) + KEYWORDS = ("electron", "secondary electron", "synthetic", "render", "shading", "slope") + def process(self, field: DataField, method: str, sigma: float, n_samples: int) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/shade.py b/backend/nodes/shade.py index 4591ceb..5f186e8 100644 --- a/backend/nodes/shade.py +++ b/backend/nodes/shade.py @@ -34,6 +34,8 @@ class Shade: "Blend mixes original data (0) with shaded relief (1). " ) + KEYWORDS = ("hillshade", "relief", "lighting", "lambertian", "render", "illumination") + def process(self, field: DataField, azimuth: float, elevation: float, blend: float) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/shape_fitting.py b/backend/nodes/shape_fitting.py index ac67c16..6475e00 100644 --- a/backend/nodes/shape_fitting.py +++ b/backend/nodes/shape_fitting.py @@ -81,6 +81,8 @@ class ShapeFitting: "of curvature, centre position, etc. " ) + KEYWORDS = ("sphere", "paraboloid", "cylinder", "fit", "primitive", "geometry", "residual") + def process(self, field: DataField, shape: str, output: str) -> tuple: data = np.asarray(field.data, dtype=np.float64) yres, xres = data.shape diff --git a/backend/nodes/slope_distribution.py b/backend/nodes/slope_distribution.py index 7226c1a..0412183 100644 --- a/backend/nodes/slope_distribution.py +++ b/backend/nodes/slope_distribution.py @@ -25,11 +25,13 @@ class SlopeDistribution: DESCRIPTION = ( "Compute the angular slope distribution of a DATA_FIELD surface. " - "'theta' is the inclination angle (0–max°), probability density (1/deg); " - "'phi' is the azimuthal slope direction (0–360°), weighted by slope² (z/xy)²; " + "'theta' is the inclination angle (0-max°), probability density (1/deg); " + "'phi' is the azimuthal slope direction (0-360°), weighted by slope² (z/xy)²; " "'gradient' is the gradient magnitude distribution, probability density (1/(z/xy)). " ) + KEYWORDS = ("angle", "inclination", "azimuth", "theta", "phi", "facet", "histogram") + def process(self, field: DataField, distribution: str, n_bins: int) -> tuple: from backend.nodes.surface_common import physical_sobel_gradient, slope_unit as _slope_unit diff --git a/backend/nodes/smm_analysis.py b/backend/nodes/smm_analysis.py index f107eb1..a9840d1 100644 --- a/backend/nodes/smm_analysis.py +++ b/backend/nodes/smm_analysis.py @@ -56,6 +56,8 @@ class SMMAnalysis: "then extracts tip-sample capacitance and real impedance maps." ) + KEYWORDS = ("microwave", "s11", "capacitance", "impedance", "vna", "calibration") + def process( self, s11_amplitude: DataField, diff --git a/backend/nodes/spot_removal.py b/backend/nodes/spot_removal.py index d6d4e8f..4d5de28 100644 --- a/backend/nodes/spot_removal.py +++ b/backend/nodes/spot_removal.py @@ -32,6 +32,8 @@ class SpotRemoval: "for smooth inpainting." ) + KEYWORDS = ("defect", "hot pixel", "dropout", "inpaint", "fill", "despeckle", "artifact") + def process( self, field: DataField, diff --git a/backend/nodes/statistics.py b/backend/nodes/statistics.py index a571099..a8ed8b6 100644 --- a/backend/nodes/statistics.py +++ b/backend/nodes/statistics.py @@ -24,6 +24,8 @@ class Statistics: "and skewness." ) + KEYWORDS = ("mean", "rms", "min", "max", "skewness", "kurtosis", "median", "roughness") + def process(self, field: DataField) -> tuple: d = field.data mean = float(d.mean()) diff --git a/backend/nodes/stats.py b/backend/nodes/stats.py index db6df85..1fead30 100644 --- a/backend/nodes/stats.py +++ b/backend/nodes/stats.py @@ -56,6 +56,8 @@ class Stats: "The available operations adapt to the connected input type." ) + KEYWORDS = ("mean", "sum", "min", "max", "median", "scalar", "reduce") + def process(self, input, operation: str, column: str = "value") -> tuple: source_type, values, resolved_column = self._resolve_input_values(input, column) diff --git a/backend/nodes/straighten_path.py b/backend/nodes/straighten_path.py index bb263f1..33ed766 100644 --- a/backend/nodes/straighten_path.py +++ b/backend/nodes/straighten_path.py @@ -30,11 +30,13 @@ class StraightenPath: DESCRIPTION = ( "Extract a cross-section along an arbitrary curved path defined by " - "control points. Points are given as fractional coordinates (0–1). " + "control points. Points are given as fractional coordinates (0-1). " "The path is interpolated with cubic splines, and data is sampled " "along it with configurable thickness. " ) + KEYWORDS = ("unbend", "unroll", "spline", "curved profile", "extract path") + def process(self, field: DataField, points_x: str, points_y: str, thickness: int, n_samples: int) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/super_resolution.py b/backend/nodes/super_resolution.py index 715bdb6..591f75c 100644 --- a/backend/nodes/super_resolution.py +++ b/backend/nodes/super_resolution.py @@ -75,6 +75,8 @@ class SuperResolution: "is provided the image is upsampled using cubic interpolation." ) + KEYWORDS = ("upscale", "upsample", "multiframe", "stack", "subpixel", "enhance") + def process( self, field1: DataField, diff --git a/backend/nodes/synthetic_surface.py b/backend/nodes/synthetic_surface.py index 8619142..675928c 100644 --- a/backend/nodes/synthetic_surface.py +++ b/backend/nodes/synthetic_surface.py @@ -552,9 +552,10 @@ class SyntheticSurface: "Generate synthetic test surfaces for development, calibration, and " "algorithm testing. 28 patterns covering noise, geometry, growth " "simulations, phase separation, reaction-diffusion, and tiling. " - "Equivalent to Gwyddion's *_synth.c modules." ) + KEYWORDS = ("generate", "fbm", "fractal", "noise", "simulation", "test", "dla", "voronoi", "turing", "spinodal", "pattern") + def process( self, pattern: str, diff --git a/backend/nodes/template_match.py b/backend/nodes/template_match.py index 4ec77aa..a50b796 100644 --- a/backend/nodes/template_match.py +++ b/backend/nodes/template_match.py @@ -34,6 +34,8 @@ class TemplateMatch: "above the threshold." ) + KEYWORDS = ("find", "locate", "pattern", "cross correlation", "ncc", "detect") + def process( self, image: DataField, diff --git a/backend/nodes/terrace_fit.py b/backend/nodes/terrace_fit.py index 8b25ac4..49ea156 100644 --- a/backend/nodes/terrace_fit.py +++ b/backend/nodes/terrace_fit.py @@ -35,6 +35,8 @@ class TerraceFit: "Set n_terraces=0 for automatic detection via histogram clustering. " ) + KEYWORDS = ("step height", "atomic step", "flatten", "crystal", "semiconductor", "monolayer") + def process(self, field: DataField, n_terraces: int, broadening: float, poly_degree: int, output: str) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/tilt.py b/backend/nodes/tilt.py index 5a11560..281d1ed 100644 --- a/backend/nodes/tilt.py +++ b/backend/nodes/tilt.py @@ -32,6 +32,8 @@ class Tilt: "Use 'subtract' mode to remove a known tilt. " ) + KEYWORDS = ("slope", "plane", "level", "bow", "ramp") + def process(self, field: DataField, slope_x: float, slope_y: float, mode: str) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/tip_blind_estimate.py b/backend/nodes/tip_blind_estimate.py index 19ceca2..4dc14fd 100644 --- a/backend/nodes/tip_blind_estimate.py +++ b/backend/nodes/tip_blind_estimate.py @@ -92,7 +92,7 @@ def _quantize_tip(tip_data: np.ndarray, tip_max: float, step: float) -> np.ndarr Matches Gwyddion's i_datafield_to_field(tip, maxzero=TRUE, min=tip_min, step). With tip_min = 0 (our tips are always shifted so min = 0) this simplifies to: - itip[y,x] = floor( (tip[y,x] − tip_max) / step ) + itip[y,x] = floor( (tip[y,x] - tip_max) / step ) """ return np.floor((tip_data - tip_max) / step).astype(np.int32) @@ -155,7 +155,7 @@ def _estimate_point_interior( The tip "touches" the image at reference pixel (jxp+yc-jd, ixp+xc-id). For the tip to be inside the surface (physically valid), the image - height at that reference pixel must be ≥ image_p − tip0[jd, id]. + height at that reference pixel must be ≥ image_p - tip0[jd, id]. Pixels that pass this check are called "good pixels" — they are the tip positions that are geometrically consistent with the apex sitting at @@ -384,14 +384,14 @@ def _certainty_map_fast( For tip pixel (ti, tj) and its reflected counterpart tip_flip[ti, tj]: - image[i, j] − tip_flip[ti, tj] − rsurf[sy, sx] ≈ 0 + image[i, j] - tip_flip[ti, tj] - rsurf[sy, sx] ≈ 0 means that placing the tip so that tip pixel (ti, tj) touches the surface at (sy, sx) exactly accounts for the measured height at image pixel (i, j). The reflected tip is used because dilation/erosion use the tip flipped 180°. Coordinates: - sy = ti + i − ryc, sx = tj + j − rxc - where ryc = tyres−1−yc and rxc = txres−1−xc are the reflected-apex + sy = ti + i - ryc, sx = tj + j - rxc + where ryc = tyres-1-yc and rxc = txres-1-xc are the reflected-apex offsets. ── Why "exactly one" contact? ─────────────────────────────────────────── @@ -494,6 +494,8 @@ class BlindTipEstimate: "Certainty map marks surface pixels where the tip was in unambiguous single contact. " ) + KEYWORDS = ("villarrubia", "morphology", "cantilever", "apex", "dilation", "reconstruct") + def process( self, field: DataField, diff --git a/backend/nodes/tip_deconvolution.py b/backend/nodes/tip_deconvolution.py index 6e4d2e3..39a82db 100644 --- a/backend/nodes/tip_deconvolution.py +++ b/backend/nodes/tip_deconvolution.py @@ -26,16 +26,18 @@ class TipDeconvolution: DESCRIPTION = ( "Reconstruct the true surface from a tip-broadened measured image. " "Uses morphological grey erosion (Villarrubia algorithm): " - " mytip = flip(tip) − max(flip(tip)) [max shifted to 0] " - " surface[y,x] = min_{dy,dx}[image[y+dy, x+dx] − mytip[dy,dx]] " + " mytip = flip(tip) - max(flip(tip)) [max shifted to 0] " + " surface[y,x] = min_{dy,dx}[image[y+dy, x+dx] - mytip[dy,dx]] " "Connect the tip output from a TipModel node. " "The tip pixel size must match the image pixel size. " ) + KEYWORDS = ("erosion", "morphology", "villarrubia", "surface reconstruction", "apex", "deblur") + def process(self, field: DataField, tip: DataField) -> tuple: # Gwyddion gwy_tip_erosion: - # mytip = flip(tip) − max(flip(tip)) (values ≤ 0, apex = 0) - # result[y,x] = min_{ty,tx}[surface[y+ty, x+tx] − mytip[ty,tx]] + # mytip = flip(tip) - max(flip(tip)) (values ≤ 0, apex = 0) + # result[y,x] = min_{ty,tx}[surface[y+ty, x+tx] - mytip[ty,tx]] tip_flipped = np.flipud(np.fliplr(tip.data)) mytip = tip_flipped - tip_flipped.max() # shift so max = 0 diff --git a/backend/nodes/tip_model.py b/backend/nodes/tip_model.py index 9d78d62..c7f5642 100644 --- a/backend/nodes/tip_model.py +++ b/backend/nodes/tip_model.py @@ -38,6 +38,8 @@ class TipModel: "sphere — ball-on-stick (sphere cap only). " ) + KEYWORDS = ("apex", "parabola", "cone", "sphere", "synthetic", "probe", "cantilever") + def process( self, field: DataField, @@ -60,7 +62,7 @@ class TipModel: if shape == "parabola": # Gwyddion parabola(): a = 1/(2R), z0 = 2a·x_half² - # z[y,x] = z0 − a·r² → min at corners is exactly 0 + # z[y,x] = z0 - a·r² → min at corners is exactly 0 a = 0.5 / radius x_half = ci * pixel_size # half-width in physical units z0 = 2.0 * a * x_half**2 @@ -69,11 +71,11 @@ class TipModel: elif shape == "cone": # Gwyddion cone(): - # angle = half-angle from horizontal = π/2 − half_angle_from_axis + # angle = half-angle from horizontal = π/2 - half_angle_from_axis # z0 = R/sin(angle) # r_cross² = (R·cos(angle))² - # inner (r² < r_cross²): z = sqrt(R² − r²) ← spherical cap - # outer: z = z0 − r/tan(angle) + # inner (r² < r_cross²): z = sqrt(R² - r²) ← spherical cap + # outer: z = z0 - r/tan(angle) angle = np.radians(90.0 - half_angle) # slope from horizontal z0 = radius / np.sin(angle) br2 = (radius * np.cos(angle))**2 diff --git a/backend/nodes/tip_shape_estimate.py b/backend/nodes/tip_shape_estimate.py index 8bc0586..dabca0f 100644 --- a/backend/nodes/tip_shape_estimate.py +++ b/backend/nodes/tip_shape_estimate.py @@ -38,9 +38,10 @@ class TipShapeEstimate: "The 2D tip is built by revolving the 1D radial profile (axial " "symmetry assumption). Output parameters include estimated tip " "radius of curvature at the apex and half-cone angle. " - "Equivalent to Gwyddion's tipshape.c analysis. " ) + KEYWORDS = ("calibration", "villarrubia", "tipshape", "radius", "apex", "half angle", "edge", "sphere", "cylinder", "dilation") + def process( self, field: DataField, diff --git a/backend/nodes/trimmed_mean.py b/backend/nodes/trimmed_mean.py index bbdbe1e..73398d1 100644 --- a/backend/nodes/trimmed_mean.py +++ b/backend/nodes/trimmed_mean.py @@ -32,6 +32,8 @@ class TrimmedMean: "trim_fraction=0.5 approaches the median. " ) + KEYWORDS = ("robust", "outlier", "percentile", "smoothing", "denoise", "alpha trimmed") + def process(self, field: DataField, radius: int, trim_fraction: float) -> tuple: data = np.asarray(field.data, dtype=np.float64) yres, xres = data.shape diff --git a/backend/nodes/view_3d.py b/backend/nodes/view_3d.py index cebb7bf..ab949d8 100644 --- a/backend/nodes/view_3d.py +++ b/backend/nodes/view_3d.py @@ -148,6 +148,8 @@ class View3D: "Drag to rotate, middle-drag to pan, and right-drag or scroll to zoom. z_scale exaggerates height." ) + KEYWORDS = ("surface", "mesh", "render", "perspective", "height map", "visualize", "opengl") + def render( self, field: DataField, colormap: str, z_scale: float, resolution: int, make_solid: bool = False, diff --git a/backend/nodes/watershed_segmentation.py b/backend/nodes/watershed_segmentation.py index 1fad463..04c3cf7 100644 --- a/backend/nodes/watershed_segmentation.py +++ b/backend/nodes/watershed_segmentation.py @@ -232,6 +232,8 @@ class WatershedSegmentation: "and optional union/intersection with an existing mask." ) + KEYWORDS = ("grain", "basin", "flood fill", "gwyddion", "peak", "valley", "hill", "marker") + def process( self, field: DataField, diff --git a/backend/nodes/wavelet_denoise.py b/backend/nodes/wavelet_denoise.py index 320642a..161bf13 100644 --- a/backend/nodes/wavelet_denoise.py +++ b/backend/nodes/wavelet_denoise.py @@ -36,6 +36,8 @@ class WaveletDenoise: "per sub-band; VisuShrink uses a global threshold." ) + KEYWORDS = ("daubechies", "symlet", "coiflet", "biorthogonal", "dwt", "threshold", "bayesshrink", "visushrink", "smooth") + def process( self, field: DataField, diff --git a/backend/nodes/wrap_value.py b/backend/nodes/wrap_value.py index 87b7ebe..a5de4d3 100644 --- a/backend/nodes/wrap_value.py +++ b/backend/nodes/wrap_value.py @@ -32,6 +32,8 @@ class WrapValue: "Preset ranges for degrees and radians, or specify a custom range. " ) + KEYWORDS = ("phase", "angle", "modulo", "periodic", "degree", "radian", "unwrap", "rewrap") + def process(self, field: DataField, range: str, custom_min: float, custom_max: float) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/nodes/zero_crossing.py b/backend/nodes/zero_crossing.py index ddd50a0..5c21089 100644 --- a/backend/nodes/zero_crossing.py +++ b/backend/nodes/zero_crossing.py @@ -32,6 +32,8 @@ class ZeroCrossing: "out weak edges (relative to the LoG range). " ) + KEYWORDS = ("edge", "laplacian of gaussian", "log", "marr hildreth", "contour", "boundary") + def process(self, field: DataField, sigma: float, threshold: float) -> tuple: data = np.asarray(field.data, dtype=np.float64) diff --git a/backend/runtime_paths.py b/backend/runtime_paths.py index fa7b4cf..1f1f32c 100644 --- a/backend/runtime_paths.py +++ b/backend/runtime_paths.py @@ -64,8 +64,8 @@ def plugins_enabled(*, native: bool) -> bool: Default behaviour: enabled on native/desktop builds, disabled for web. Override with the TONO_PLUGINS environment variable: - TONO_PLUGINS=1 – force on (useful for testing plugins via main.py) - TONO_PLUGINS=0 – force off (disable even on native builds) + TONO_PLUGINS=1 - force on (useful for testing plugins via main.py) + TONO_PLUGINS=0 - force off (disable even on native builds) """ env = os.getenv("TONO_PLUGINS", "").strip().lower() if env in ("1", "true", "yes"): diff --git a/docs/nodes/3D View.md b/docs/nodes/3D View.md index a37f942..de486c8 100644 --- a/docs/nodes/3D View.md +++ b/docs/nodes/3D View.md @@ -22,8 +22,8 @@ Interactive 3D surface view of a DATA_FIELD. Use the mesh input for geometry and | Name | Type | Default | Description | |------|------|---------|-------------| | colormap | dropdown | auto | Colormap preset applied to the surface; hidden when colormap_map is connected | -| z_scale | FLOAT | 1.0 | Height exaggeration factor (range 0.1–10.0) | -| resolution | INT | 128 | Downsampling resolution for mesh generation (32–512) | +| z_scale | FLOAT | 1.0 | Height exaggeration factor (range 0.1-10.0) | +| resolution | INT | 128 | Downsampling resolution for mesh generation (32-512) | | make_solid | BOOLEAN | False | When enabled adds a flat base and side walls to close the mesh | ## Notes diff --git a/docs/nodes/ACF 2D.md b/docs/nodes/ACF 2D.md index bd3b392..60ff599 100644 --- a/docs/nodes/ACF 2D.md +++ b/docs/nodes/ACF 2D.md @@ -22,5 +22,5 @@ Compute the two-dimensional autocorrelation function with Gwyddion-style mean or ## Notes -- Output is not normalized to [−1, 1]; peak value equals the field variance. +- Output is not normalized to [-1, 1]; peak value equals the field variance. - Plane levelling assumes a linear trend; strongly curved surfaces may not detrend correctly. diff --git a/docs/nodes/Angle Measure.md b/docs/nodes/Angle Measure.md index 5bbf681..41749a1 100644 --- a/docs/nodes/Angle Measure.md +++ b/docs/nodes/Angle Measure.md @@ -20,7 +20,7 @@ Measure the included angle between two draggable line segments over a DATA_FIELD | Name | Type | Default | Description | |------|------|---------|-------------| | color | STRING (color picker) | #ff9800 | Overlay color for the angle arms and arc | -| stroke_width | FLOAT | 1.35 | Line thickness in display pixels (0.35–6.0) | +| stroke_width | FLOAT | 1.35 | Line thickness in display pixels (0.35-6.0) | ## Notes diff --git a/docs/nodes/Annotations.md b/docs/nodes/Annotations.md index e11c560..e19d19c 100644 --- a/docs/nodes/Annotations.md +++ b/docs/nodes/Annotations.md @@ -23,7 +23,7 @@ Attach optional publication-style annotations (scale bar, color-map legend) to a | colormap | dropdown | auto | Colormap for the legend; hidden when colormap_map is connected | | show_scale_bar | BOOLEAN | True | Render a physical scale bar | | show_color_map | BOOLEAN | True | Render a color-map legend with min/mid/max values | -| text_size | FLOAT | 14.0 | Font size in points for annotation labels (6–96) | +| text_size | FLOAT | 14.0 | Font size in points for annotation labels (6-96) | ## Notes diff --git a/docs/nodes/Blind Tip Estimate.md b/docs/nodes/Blind Tip Estimate.md index 39df15a..e000da3 100644 --- a/docs/nodes/Blind Tip Estimate.md +++ b/docs/nodes/Blind Tip Estimate.md @@ -19,7 +19,7 @@ Blind tip estimation from a measured SPM image using the Villarrubia algorithm. | Name | Type | Default | Description | |------|------|---------|-------------| -| n_pixels | INT | 33 | Tip grid size in pixels (odd values only, 3–129) | +| n_pixels | INT | 33 | Tip grid size in pixels (odd values only, 3-129) | | threshold | FLOAT | 0.0 | Noise floor in metres; increase if the estimated tip is unrealistically sharp | | method | dropdown | partial | partial: uses local maxima only (faster, needs sharp isolated features); full: uses all points above morphological opening (slower, more robust) | | use_edges | BOOLEAN | False | When enabled, also uses image edge pixels as refinement candidates | diff --git a/docs/nodes/Colormap Adjust.md b/docs/nodes/Colormap Adjust.md index 2d3a66b..c6ef539 100644 --- a/docs/nodes/Colormap Adjust.md +++ b/docs/nodes/Colormap Adjust.md @@ -18,8 +18,8 @@ Adjust how a DATA_FIELD maps into its colormap without changing the underlying d | Name | Type | Default | Description | |------|------|---------|-------------| -| offset | FLOAT | 0.0 | Shift the colormap center in normalized units (−1 to 1) | -| scale | FLOAT | 1.0 | Zoom the colormap range (0.05–4.0); values below 1 stretch contrast | +| offset | FLOAT | 0.0 | Shift the colormap center in normalized units (-1 to 1) | +| scale | FLOAT | 1.0 | Zoom the colormap range (0.05-4.0); values below 1 stretch contrast | | auto | BUTTON | — | Reset offset to 0 and scale to 1 (full data range) | ## Notes diff --git a/docs/nodes/Cross-Correlate.md b/docs/nodes/Cross-Correlate.md index 02fa1a1..e07b2af 100644 --- a/docs/nodes/Cross-Correlate.md +++ b/docs/nodes/Cross-Correlate.md @@ -19,8 +19,8 @@ Compute 2D cross-correlation between two fields. The correlation peak indicates | Name | Type | Default | Description | |------|------|---------|-------------| -| mode | dropdown | same | Output size: full (Na+Nb−1), same (same as field_a), or valid (overlapping region only) | -| normalize | BOOLEAN | True | Normalize the result to [−1, 1] by dividing by the product of RMS values | +| mode | dropdown | same | Output size: full (Na+Nb-1), same (same as field_a), or valid (overlapping region only) | +| normalize | BOOLEAN | True | Normalize the result to [-1, 1] by dividing by the product of RMS values | ## Notes diff --git a/docs/nodes/Deconvolution.md b/docs/nodes/Deconvolution.md index d6e8922..51f4472 100644 --- a/docs/nodes/Deconvolution.md +++ b/docs/nodes/Deconvolution.md @@ -19,13 +19,13 @@ Restore an image via regularised deconvolution. Assumes the image was blurred by | Name | Type | Default | Description | |------|------|---------|-------------| | method | dropdown | wiener | Deconvolution method: wiener or richardson_lucy | -| sigma | FLOAT | 2.0 | Gaussian PSF sigma in pixels (0.1–50.0) | -| regularisation | FLOAT | 0.01 | Regularisation parameter for Wiener filter (1e-6–1.0) | -| iterations | INT | 10 | Number of iterations (Richardson-Lucy only, 1–200) | +| sigma | FLOAT | 2.0 | Gaussian PSF sigma in pixels (0.1-50.0) | +| regularisation | FLOAT | 0.01 | Regularisation parameter for Wiener filter (1e-6-1.0) | +| iterations | INT | 10 | Number of iterations (Richardson-Lucy only, 1-200) | ## Notes - **Wiener**: Fast, single-pass frequency-domain filter. The regularisation parameter controls the noise/sharpness tradeoff — smaller values sharpen more but amplify noise. - **Richardson-Lucy**: Iterative method that preserves positivity. More iterations = sharper result but risk of ringing artifacts. -- The PSF sigma should match the actual blur in the image. If unknown, start with sigma=1–3 and adjust. +- The PSF sigma should match the actual blur in the image. If unknown, start with sigma=1-3 and adjust. - For tip-shape deconvolution (non-Gaussian PSF), use Tip Deconvolution instead. diff --git a/docs/nodes/Distribution Coercion.md b/docs/nodes/Distribution Coercion.md index fd73239..2619e71 100644 --- a/docs/nodes/Distribution Coercion.md +++ b/docs/nodes/Distribution Coercion.md @@ -19,7 +19,7 @@ Transform pixel values so their distribution matches a target shape (uniform, Ga | Name | Type | Default | Description | |------|------|---------|-------------| | distribution | dropdown | uniform | Target distribution shape: uniform, gaussian, or levels | -| n_levels | INT | 4 | Number of discrete output levels (2–1000); visible only for levels mode | +| n_levels | INT | 4 | Number of discrete output levels (2-1000); visible only for levels mode | | processing | dropdown | field | Processing scope: field (entire array at once) or rows (line-by-line) | ## Notes diff --git a/docs/nodes/Draw Mask.md b/docs/nodes/Draw Mask.md index da72c37..a8a54c3 100644 --- a/docs/nodes/Draw Mask.md +++ b/docs/nodes/Draw Mask.md @@ -18,7 +18,7 @@ Paint a binary mask directly over an image preview. Pen size controls newly draw | Name | Type | Default | Description | |------|------|---------|-------------| -| pen_size | INT | 12 | Brush diameter in pixels for newly drawn strokes (1–128) | +| pen_size | INT | 12 | Brush diameter in pixels for newly drawn strokes (1-128) | | invert | BOOLEAN | False | When enabled, swaps painted and unpainted regions | | clear_mask | BUTTON | — | Clears all painted strokes | diff --git a/docs/nodes/Edge Detect.md b/docs/nodes/Edge Detect.md index baba948..012e8f3 100644 --- a/docs/nodes/Edge Detect.md +++ b/docs/nodes/Edge Detect.md @@ -19,7 +19,7 @@ Detect edges using Sobel, Prewitt, Laplacian, or Laplacian-of-Gaussian (LoG) ope | Name | Type | Default | Description | |------|------|---------|-------------| | method | dropdown | sobel | Edge detection operator: sobel, prewitt, laplacian, or log (Laplacian of Gaussian) | -| sigma | FLOAT | 1.0 | Gaussian smoothing sigma used only for the LoG operator (0.1–10.0) | +| sigma | FLOAT | 1.0 | Gaussian smoothing sigma used only for the LoG operator (0.1-10.0) | ## Notes diff --git a/docs/nodes/Entropy.md b/docs/nodes/Entropy.md index b5894f5..9e2a2cb 100644 --- a/docs/nodes/Entropy.md +++ b/docs/nodes/Entropy.md @@ -1,6 +1,6 @@ # Entropy -Compute the Shannon entropy of the height or slope distribution. H = −Σ p·ln(p). Equivalent to Gwyddion entropy.c. +Compute the Shannon entropy of the height or slope distribution. H = -Σ p·ln(p). Equivalent to Gwyddion entropy.c. ## Inputs @@ -20,7 +20,7 @@ Compute the Shannon entropy of the height or slope distribution. H = −Σ p·ln | Name | Type | Default | Description | |------|------|---------|-------------| | mode | dropdown | height values | Compute entropy of height values or slope magnitude | -| n_bins | INT | 256 | Number of histogram bins for probability estimation (16–1024) | +| n_bins | INT | 256 | Number of histogram bins for probability estimation (16-1024) | ## Notes diff --git a/docs/nodes/Extend Pad.md b/docs/nodes/Extend Pad.md index f186125..35732e9 100644 --- a/docs/nodes/Extend Pad.md +++ b/docs/nodes/Extend Pad.md @@ -18,10 +18,10 @@ Add configurable borders around a DATA_FIELD using various padding methods. Usef | Name | Type | Default | Description | |------|------|---------|-------------| -| left | INT | 0 | Number of pixels to add on the left edge (0–1024) | -| right | INT | 0 | Number of pixels to add on the right edge (0–1024) | -| top | INT | 0 | Number of pixels to add on the top edge (0–1024) | -| bottom | INT | 0 | Number of pixels to add on the bottom edge (0–1024) | +| left | INT | 0 | Number of pixels to add on the left edge (0-1024) | +| right | INT | 0 | Number of pixels to add on the right edge (0-1024) | +| top | INT | 0 | Number of pixels to add on the top edge (0-1024) | +| bottom | INT | 0 | Number of pixels to add on the bottom edge (0-1024) | | method | dropdown | mirror | Padding method: mean (fill with field mean), edge (replicate border pixels), mirror (reflect across edge), periodic (tile the field), or zero (fill with zeros) | ## Notes diff --git a/docs/nodes/FFT Filter.md b/docs/nodes/FFT Filter.md index 8b100a9..29e9089 100644 --- a/docs/nodes/FFT Filter.md +++ b/docs/nodes/FFT Filter.md @@ -19,9 +19,9 @@ Frequency-domain filtering of a line profile or 2D data field using a Butterwort | Name | Type | Default | Description | |------|------|---------|-------------| | filter_type | dropdown | lowpass | Filter mode: lowpass, highpass, bandpass, or notch (band-reject) | -| cutoff | FLOAT | 0.1 | Lower cutoff frequency as a fraction of Nyquist (0.001–1.0) | -| cutoff_high | FLOAT | 0.4 | Upper cutoff for bandpass/notch modes (0.001–1.0) | -| order | INT | 2 | Butterworth filter order; higher values give steeper roll-off (1–10) | +| cutoff | FLOAT | 0.1 | Lower cutoff frequency as a fraction of Nyquist (0.001-1.0) | +| cutoff_high | FLOAT | 0.4 | Upper cutoff for bandpass/notch modes (0.001-1.0) | +| order | INT | 2 | Butterworth filter order; higher values give steeper roll-off (1-10) | ## Notes diff --git a/docs/nodes/Facet Analysis.md b/docs/nodes/Facet Analysis.md index 1cc6102..943fefe 100644 --- a/docs/nodes/Facet Analysis.md +++ b/docs/nodes/Facet Analysis.md @@ -18,12 +18,12 @@ Compute the facet orientation distribution of a surface. Outputs a 2D histogram | Name | Type | Default | Description | |------|------|---------|-------------| -| n_bins | INT | 180 | Number of azimuthal bins; theta bins = n_bins/4 (30–720) | -| kernel_size | INT | 3 | Sobel gradient kernel size in pixels (3–9, odd) | +| n_bins | INT | 180 | Number of azimuthal bins; theta bins = n_bins/4 (30-720) | +| kernel_size | INT | 3 | Sobel gradient kernel size in pixels (3-9, odd) | ## Notes - The output is a normalised probability density — it sums to 1.0. -- X-axis: azimuthal angle phi (0–360°). Y-axis: inclination theta (0° = flat, max = steepest facet). +- X-axis: azimuthal angle phi (0-360°). Y-axis: inclination theta (0° = flat, max = steepest facet). - A flat surface produces a single bright spot near theta=0. A surface with distinct facets shows multiple peaks. - Larger kernel_size smooths the gradient estimate, reducing noise sensitivity. diff --git a/docs/nodes/Feature Detection.md b/docs/nodes/Feature Detection.md index 9257b1a..4b6b77e 100644 --- a/docs/nodes/Feature Detection.md +++ b/docs/nodes/Feature Detection.md @@ -20,11 +20,11 @@ Detect edges or corners in a surface using Canny edge detection or Harris corner | Name | Type | Default | Description | |------|------|---------|-------------| | method | dropdown | canny | Detection method: canny (edges) or harris (corners) | -| sigma | FLOAT | 1.0 | Gaussian smoothing sigma in pixels (0.1–20.0) | -| low_threshold | FLOAT | 0.1 | Canny low hysteresis threshold (0–1) | -| high_threshold | FLOAT | 0.2 | Canny high hysteresis threshold (0–1) | -| harris_k | FLOAT | 0.05 | Harris detector sensitivity parameter (0.01–0.5) | -| min_distance | INT | 5 | Minimum distance between detected corners in pixels (1–100) | +| sigma | FLOAT | 1.0 | Gaussian smoothing sigma in pixels (0.1-20.0) | +| low_threshold | FLOAT | 0.1 | Canny low hysteresis threshold (0-1) | +| high_threshold | FLOAT | 0.2 | Canny high hysteresis threshold (0-1) | +| harris_k | FLOAT | 0.05 | Harris detector sensitivity parameter (0.01-0.5) | +| min_distance | INT | 5 | Minimum distance between detected corners in pixels (1-100) | ## Notes diff --git a/docs/nodes/Flatten Base.md b/docs/nodes/Flatten Base.md index 7361bb6..75ec06b 100644 --- a/docs/nodes/Flatten Base.md +++ b/docs/nodes/Flatten Base.md @@ -18,11 +18,11 @@ Level the flat base of a surface that has raised features (particles, grains). U | Name | Type | Default | Description | |------|------|---------|-------------| -| threshold_percentile | FLOAT | 30.0 | Height percentile below which pixels are considered base (5–80) | -| poly_degree | INT | 2 | Polynomial degree for base fit: 0 = constant, 1 = plane, 2 = quadratic (0–5) | +| threshold_percentile | FLOAT | 30.0 | Height percentile below which pixels are considered base (5-80) | +| poly_degree | INT | 2 | Polynomial degree for base fit: 0 = constant, 1 = plane, 2 = quadratic (0-5) | ## Notes -- Set the threshold percentile so that it includes most of the base but excludes the features. For sparse particles on a flat substrate, 20–40% typically works well. -- poly_degree=1 is equivalent to plane leveling on the base only. Use 2–3 for curved substrates. +- Set the threshold percentile so that it includes most of the base but excludes the features. For sparse particles on a flat substrate, 20-40% typically works well. +- poly_degree=1 is equivalent to plane leveling on the base only. Use 2-3 for curved substrates. - If the features dominate the surface (>50% coverage), this node may not give good results — consider Median Background instead. diff --git a/docs/nodes/Fractal Interpolation.md b/docs/nodes/Fractal Interpolation.md index d5fb907..d9e6cdc 100644 --- a/docs/nodes/Fractal Interpolation.md +++ b/docs/nodes/Fractal Interpolation.md @@ -19,7 +19,7 @@ Fill masked regions using fractal interpolation. Matches the spectral characteri | Name | Type | Default | Description | |------|------|---------|-------------| -| iterations | INT | 200 | Number of boundary relaxation iterations (10–5000) | +| iterations | INT | 200 | Number of boundary relaxation iterations (10-5000) | ## Notes diff --git a/docs/nodes/Frequency Split.md b/docs/nodes/Frequency Split.md index 2912206..843e255 100644 --- a/docs/nodes/Frequency Split.md +++ b/docs/nodes/Frequency Split.md @@ -19,7 +19,7 @@ Separate a DATA_FIELD into low-frequency and high-frequency components using an | Name | Type | Default | Description | |------|------|---------|-------------| -| cutoff | FLOAT | 0.1 | Cutoff frequency as a fraction of Nyquist (0.001–0.5) | +| cutoff | FLOAT | 0.1 | Cutoff frequency as a fraction of Nyquist (0.001-0.5) | ## Notes diff --git a/docs/nodes/Gaussian Filter.md b/docs/nodes/Gaussian Filter.md index c7aa1b4..8707bb2 100644 --- a/docs/nodes/Gaussian Filter.md +++ b/docs/nodes/Gaussian Filter.md @@ -18,7 +18,7 @@ Apply a Gaussian blur to a DATA_FIELD. Equivalent to gwy_data_field_filter_gauss | Name | Type | Default | Description | |------|------|---------|-------------| -| sigma | FLOAT | 1.0 | Standard deviation of the Gaussian kernel in pixels (0.01–50.0) | +| sigma | FLOAT | 1.0 | Standard deviation of the Gaussian kernel in pixels (0.01-50.0) | ## Notes diff --git a/docs/nodes/Grain Analysis.md b/docs/nodes/Grain Analysis.md index 646b562..14f8f39 100644 --- a/docs/nodes/Grain Analysis.md +++ b/docs/nodes/Grain Analysis.md @@ -19,7 +19,7 @@ Label connected grain regions in a binary mask and compute per-grain statistics: | Name | Type | Default | Description | |------|------|---------|-------------| -| min_size | INT | 10 | Minimum grain area in pixels; smaller connected regions are ignored (1–100000) | +| min_size | INT | 10 | Minimum grain area in pixels; smaller connected regions are ignored (1-100000) | ## Notes diff --git a/docs/nodes/Grain Cross.md b/docs/nodes/Grain Cross.md index cea9ee0..510ff5c 100644 --- a/docs/nodes/Grain Cross.md +++ b/docs/nodes/Grain Cross.md @@ -22,7 +22,7 @@ Correlate grain properties between two fields using a shared grain mask. Reports |------|------|---------|-------------| | property_a | dropdown | mean_height | Property to compute from field_a: area, mean_height, max_height, or volume | | property_b | dropdown | max_height | Property to compute from field_b: area, mean_height, max_height, or volume | -| min_size | INT | 10 | Minimum grain area in pixels; smaller grains are excluded (1–100000) | +| min_size | INT | 10 | Minimum grain area in pixels; smaller grains are excluded (1-100000) | ## Notes diff --git a/docs/nodes/Grain Distributions.md b/docs/nodes/Grain Distributions.md index ca9c3e7..ae6689d 100644 --- a/docs/nodes/Grain Distributions.md +++ b/docs/nodes/Grain Distributions.md @@ -20,8 +20,8 @@ Compute a histogram distribution of a grain property from a labeled mask. Suppor | Name | Type | Default | Description | |------|------|---------|-------------| | property | dropdown | area | Grain property to plot: area, equiv_diameter, mean_height, max_height, volume, boundary_length | -| n_bins | INT | 30 | Number of histogram bins (5–200) | -| min_size | INT | 10 | Minimum grain size in pixels to include (1–100000) | +| n_bins | INT | 30 | Number of histogram bins (5-200) | +| min_size | INT | 10 | Minimum grain size in pixels to include (1-100000) | ## Notes diff --git a/docs/nodes/Grain Edge.md b/docs/nodes/Grain Edge.md index c064e92..c2b0839 100644 --- a/docs/nodes/Grain Edge.md +++ b/docs/nodes/Grain Edge.md @@ -19,7 +19,7 @@ Detect grain boundaries from a binary grain mask. Outputs a mask of pixels at gr | Name | Type | Default | Description | |------|------|---------|-------------| -| width | INT | 1 | Boundary thickness in pixels; values greater than 1 dilate the edge outward (1–10) | +| width | INT | 1 | Boundary thickness in pixels; values greater than 1 dilate the edge outward (1-10) | ## Notes diff --git a/docs/nodes/Grain Mark.md b/docs/nodes/Grain Mark.md index a103da1..5088b42 100644 --- a/docs/nodes/Grain Mark.md +++ b/docs/nodes/Grain Mark.md @@ -1,6 +1,6 @@ # Grain Mark -Mark grains by thresholding height, slope magnitude, or curvature. Thresholds are relative (0–1) to the data range. Small regions below min_size pixels are removed. Equivalent to Gwyddion's grain_mark.c module. +Mark grains by thresholding height, slope magnitude, or curvature. Thresholds are relative (0-1) to the data range. Small regions below min_size pixels are removed. Equivalent to Gwyddion's grain_mark.c module. ## Inputs @@ -19,9 +19,9 @@ Mark grains by thresholding height, slope magnitude, or curvature. Thresholds ar | Name | Type | Default | Description | |------|------|---------|-------------| | criterion | dropdown | height | What to threshold: height, slope, or curvature | -| threshold_low | FLOAT | 0.3 | Lower bound of the normalized threshold range (0–1) | -| threshold_high | FLOAT | 1.0 | Upper bound of the normalized threshold range (0–1) | -| min_size | INT | 10 | Minimum grain size in pixels; smaller regions are removed (1–100000) | +| threshold_low | FLOAT | 0.3 | Lower bound of the normalized threshold range (0-1) | +| threshold_high | FLOAT | 1.0 | Upper bound of the normalized threshold range (0-1) | +| min_size | INT | 10 | Minimum grain size in pixels; smaller regions are removed (1-100000) | | inverted | BOOLEAN | False | Invert the mask to mark valleys instead of peaks | ## Notes diff --git a/docs/nodes/Grain Summary.md b/docs/nodes/Grain Summary.md index 6bf8b77..6e12af8 100644 --- a/docs/nodes/Grain Summary.md +++ b/docs/nodes/Grain Summary.md @@ -19,7 +19,7 @@ Compute aggregate statistics for all grains in a mask: count, density, coverage | Name | Type | Default | Description | |------|------|---------|-------------| -| min_size | INT | 10 | Minimum grain size in pixels to include (1–100000) | +| min_size | INT | 10 | Minimum grain size in pixels to include (1-100000) | ## Notes diff --git a/docs/nodes/Histogram.md b/docs/nodes/Histogram.md index 46ceeae..7cd7992 100644 --- a/docs/nodes/Histogram.md +++ b/docs/nodes/Histogram.md @@ -19,7 +19,7 @@ Compute the height distribution histogram (DH). Use log scale to reveal small pe | Name | Type | Default | Description | |------|------|---------|-------------| -| n_bins | INT | 256 | Number of histogram bins (10–1000) | +| n_bins | INT | 256 | Number of histogram bins (10-1000) | | y_scale | dropdown | linear | Y-axis scale: linear or log | ## Notes diff --git a/docs/nodes/Hough Transform.md b/docs/nodes/Hough Transform.md index 90cc688..865bfe6 100644 --- a/docs/nodes/Hough Transform.md +++ b/docs/nodes/Hough Transform.md @@ -20,8 +20,8 @@ Detect lines or circles in images using the Hough transform. Returns an accumula | Name | Type | Default | Description | |------|------|---------|-------------| | mode | dropdown | lines | Detection mode: lines or circles | -| n_peaks | INT | 3 | Number of strongest features to report (1–50) | -| threshold | FLOAT | 1.0 | Minimum accumulator value relative to peak (0.1–10.0) | +| n_peaks | INT | 3 | Number of strongest features to report (1-50) | +| threshold | FLOAT | 1.0 | Minimum accumulator value relative to peak (0.1-10.0) | | min_radius | INT | 10 | Minimum circle radius in pixels (circles mode only) | | max_radius | INT | 30 | Maximum circle radius in pixels (circles mode only) | diff --git a/docs/nodes/Kuwahara Filter.md b/docs/nodes/Kuwahara Filter.md index 6a9ba17..bb5c544 100644 --- a/docs/nodes/Kuwahara Filter.md +++ b/docs/nodes/Kuwahara Filter.md @@ -18,7 +18,7 @@ Edge-preserving smoothing using Kuwahara's minimum-variance quadrant method. Unl | Name | Type | Default | Description | |------|------|---------|-------------| -| iterations | INT | 1 | Number of times the 5×5 Kuwahara pass is applied (1–20) | +| iterations | INT | 1 | Number of times the 5×5 Kuwahara pass is applied (1-20) | ## Notes diff --git a/docs/nodes/Laplace Interpolation.md b/docs/nodes/Laplace Interpolation.md index 241d3c4..d68d9d5 100644 --- a/docs/nodes/Laplace Interpolation.md +++ b/docs/nodes/Laplace Interpolation.md @@ -19,11 +19,11 @@ Fill masked (missing) regions by solving the Laplace equation with Dirichlet bou | Name | Type | Default | Description | |------|------|---------|-------------| -| iterations | INT | 500 | Number of Jacobi relaxation iterations; more iterations = smoother result (10–10000) | +| iterations | INT | 500 | Number of Jacobi relaxation iterations; more iterations = smoother result (10-10000) | ## Notes - Laplace interpolation produces the smoothest possible fill — it minimizes the integral of the squared gradient within the masked region. -- For small holes (<50 px diameter), 200–500 iterations is usually sufficient. Larger holes may need 1000+. +- For small holes (<50 px diameter), 200-500 iterations is usually sufficient. Larger holes may need 1000+. - Use a Draw Mask or Threshold Mask node to create the mask input. - For surfaces with texture, consider Fractal Interpolation instead, which preserves surface roughness characteristics. diff --git a/docs/nodes/Lateral Force Simulation.md b/docs/nodes/Lateral Force Simulation.md index 708e901..47c24da 100644 --- a/docs/nodes/Lateral Force Simulation.md +++ b/docs/nodes/Lateral Force Simulation.md @@ -20,14 +20,14 @@ Simulate lateral (friction) force signals from topography data, modeling how the | Name | Type | Default | Description | |------|------|---------|-------------| | direction | dropdown | forward | Scan direction to compute: forward, reverse, or both. When set to forward or reverse, both outputs carry the same single-direction result | -| friction_coefficient | FLOAT | 0.3 | Coulomb friction coefficient between tip and sample (0.0–10.0) | -| adhesion | FLOAT | 1e-9 | Tip-sample adhesion force in Newtons (0.0–1e-6) | -| load | FLOAT | 10e-9 | Applied normal load on the cantilever in Newtons (1e-12–1e-6) | +| friction_coefficient | FLOAT | 0.3 | Coulomb friction coefficient between tip and sample (0.0-10.0) | +| adhesion | FLOAT | 1e-9 | Tip-sample adhesion force in Newtons (0.0-1e-6) | +| load | FLOAT | 10e-9 | Applied normal load on the cantilever in Newtons (1e-12-1e-6) | ## Notes - The lateral force is computed from a contact-mechanics model where the measured torsion signal depends on the local surface tilt angle. The x-gradient of the topography gives the slope, and the resulting lateral force combines the gravitational component along the slope with the friction force (proportional to the normal component of load plus adhesion): F_lateral = (F_load sin(theta) + mu (F_load cos(theta) + F_adhesion)) / (cos(theta) - mu sin(theta)). - Forward and reverse scans produce different lateral force signals because friction opposes the scan direction. The forward scan (+x) adds the friction contribution to the slope component, while the reverse scan (-x) subtracts it, producing the characteristic "friction loop" seen in LFM experiments. -- Typical friction coefficients for common AFM sample materials: mica ~0.1–0.3, silicon ~0.2–0.5, polymers ~0.3–0.8, metals ~0.3–0.6. Use lower values for atomically smooth or lubricated surfaces. +- Typical friction coefficients for common AFM sample materials: mica ~0.1-0.3, silicon ~0.2-0.5, polymers ~0.3-0.8, metals ~0.3-0.6. Use lower values for atomically smooth or lubricated surfaces. - Output values represent the lateral force on the cantilever tip in Newtons. To convert to photodetector voltage, divide by the lateral sensitivity of the optical lever system. - This node is the equivalent of Gwyddion's `latsim.c` lateral force simulation and uses the same contact-mechanics formulation for topography-induced friction artifacts. diff --git a/docs/nodes/Lattice Measurement.md b/docs/nodes/Lattice Measurement.md index a4eda50..9f933aa 100644 --- a/docs/nodes/Lattice Measurement.md +++ b/docs/nodes/Lattice Measurement.md @@ -26,4 +26,4 @@ Detect and measure periodic lattice structures from ACF or FFT peak positions. R - ACF method finds the strongest off-center peaks in the 2D autocorrelation. Works well for real-space periodic structures. - FFT method finds peaks in the power spectrum. Better for weak periodicity or noisy data. - Reports up to two lattice vectors (a, b), their magnitudes, and the angle between them. -- For best results, the field should contain at least 3–4 complete periods in each direction. +- For best results, the field should contain at least 3-4 complete periods in each direction. diff --git a/docs/nodes/Line Correction.md b/docs/nodes/Line Correction.md index ee713e2..b8da00d 100644 --- a/docs/nodes/Line Correction.md +++ b/docs/nodes/Line Correction.md @@ -24,8 +24,8 @@ Correct scan-line mismatches using Gwyddion-derived row alignment methods. Suppo | method | dropdown | median | Alignment method: median, median_diff, trimmed_mean, trimmed_diff, polynomial, or step | | direction | dropdown | horizontal | Direction of scan lines to correct: horizontal or vertical | | masking | dropdown | ignore | How to use the mask: ignore, include (correct using masked rows only), or exclude | -| trim_fraction | FLOAT | 0.05 | Fraction of extreme values to trim; visible only for trimmed_mean and trimmed_diff methods (0–0.5) | -| polynomial_degree | INT | 1 | Polynomial degree for the polynomial method (0–5); visible only for polynomial method | +| trim_fraction | FLOAT | 0.05 | Fraction of extreme values to trim; visible only for trimmed_mean and trimmed_diff methods (0-0.5) | +| polynomial_degree | INT | 1 | Polynomial degree for the polynomial method (0-5); visible only for polynomial method | ## Notes diff --git a/docs/nodes/Local Contrast.md b/docs/nodes/Local Contrast.md index 210f53f..1192b07 100644 --- a/docs/nodes/Local Contrast.md +++ b/docs/nodes/Local Contrast.md @@ -18,8 +18,8 @@ Expand the local dynamic range at each pixel to reveal fine surface features tha | Name | Type | Default | Description | |------|------|---------|-------------| -| kernel_size | INT | 10 | Size of the local neighbourhood window in pixels (2–100) | -| weight | FLOAT | 0.5 | Blend weight between original and full-contrast output (0 = original, 1 = full local contrast; 0–1) | +| kernel_size | INT | 10 | Size of the local neighbourhood window in pixels (2-100) | +| weight | FLOAT | 0.5 | Blend weight between original and full-contrast output (0 = original, 1 = full local contrast; 0-1) | ## Notes diff --git a/docs/nodes/Log-Polar PSDF.md b/docs/nodes/Log-Polar PSDF.md index 3c3a48f..82e51a7 100644 --- a/docs/nodes/Log-Polar PSDF.md +++ b/docs/nodes/Log-Polar PSDF.md @@ -18,8 +18,8 @@ Compute the power spectral density function in log-polar coordinates. The x-axis | Name | Type | Default | Description | |------|------|---------|-------------| -| n_phi | INT | 180 | Number of azimuthal angle bins (36–720) | -| n_r | INT | 100 | Number of radial (log-frequency) bins (20–500) | +| n_phi | INT | 180 | Number of azimuthal angle bins (36-720) | +| n_r | INT | 100 | Number of radial (log-frequency) bins (20-500) | ## Notes diff --git a/docs/nodes/Logistic Classification.md b/docs/nodes/Logistic Classification.md index ff6d840..224fa46 100644 --- a/docs/nodes/Logistic Classification.md +++ b/docs/nodes/Logistic Classification.md @@ -21,12 +21,12 @@ Classify surface features using logistic regression on engineered height-derived | Name | Type | Default | Description | |------|------|---------|-------------| | use_gaussians | BOOLEAN | True | Include Gaussian blur features at multiple scales | -| n_gaussians | INT | 4 | Number of Gaussian scales (1–10). Only shown when use_gaussians is True | +| n_gaussians | INT | 4 | Number of Gaussian scales (1-10). Only shown when use_gaussians is True | | use_sobel | BOOLEAN | True | Include Sobel gradient features (horizontal and vertical) | | use_laplacian | BOOLEAN | True | Include Laplacian (sum of second differences) feature | -| regularization | FLOAT | 1.0 | L2 regularization strength lambda (0.0–10.0) | -| max_iter | INT | 500 | Maximum gradient descent iterations (10–5000) | -| seed | INT | 42 | Random seed for reproducibility (0–999999) | +| regularization | FLOAT | 1.0 | L2 regularization strength lambda (0.0-10.0) | +| max_iter | INT | 500 | Maximum gradient descent iterations (10-5000) | +| seed | INT | 42 | Random seed for reproducibility (0-999999) | ## Notes diff --git a/docs/nodes/Markup.md b/docs/nodes/Markup.md index caba27e..dfbb08c 100644 --- a/docs/nodes/Markup.md +++ b/docs/nodes/Markup.md @@ -20,7 +20,7 @@ Draw simple vector shapes (lines, rectangles, circles, arrows) over a DATA_FIELD |------|------|---------|-------------| | shape | dropdown | arrow | Shape type to draw next: line, rectangle, circle, or arrow | | stroke_color | STRING (color picker) | #ff0000 | Color for newly drawn shapes | -| stroke_width | INT | 3 | Line thickness in display pixels for newly drawn shapes (1–64) | +| stroke_width | INT | 3 | Line thickness in display pixels for newly drawn shapes (1-64) | | clear_shapes | BUTTON | — | Remove all drawn shapes | ## Notes diff --git a/docs/nodes/Mask Morphology.md b/docs/nodes/Mask Morphology.md index d357337..af08b26 100644 --- a/docs/nodes/Mask Morphology.md +++ b/docs/nodes/Mask Morphology.md @@ -20,7 +20,7 @@ Apply morphological operations to a binary mask. Dilate expands regions, erode s | Name | Type | Default | Description | |------|------|---------|-------------| | operation | dropdown | dilate | Morphological operation: dilate, erode, open, or close | -| radius | INT | 1 | Structuring element radius in pixels (1–50) | +| radius | INT | 1 | Structuring element radius in pixels (1-50) | | shape | dropdown | disk | Structuring element shape: disk or square | ## Notes diff --git a/docs/nodes/Median Background.md b/docs/nodes/Median Background.md index 3df7ff7..876e1a7 100644 --- a/docs/nodes/Median Background.md +++ b/docs/nodes/Median Background.md @@ -18,7 +18,7 @@ Extract background using a local median filter and subtract it. The radius contr | Name | Type | Default | Description | |------|------|---------|-------------| -| radius | INT | 20 | Half-size of the median filter window in pixels; the full window is (2×radius+1)² (2–500) | +| radius | INT | 20 | Half-size of the median filter window in pixels; the full window is (2×radius+1)² (2-500) | | output | dropdown | subtracted | Output mode: subtracted (original minus background) or background (extracted background) | ## Notes diff --git a/docs/nodes/Median Filter.md b/docs/nodes/Median Filter.md index f85929c..306b822 100644 --- a/docs/nodes/Median Filter.md +++ b/docs/nodes/Median Filter.md @@ -18,7 +18,7 @@ Apply a median filter to a DATA_FIELD. Equivalent to gwy_data_field_filter_media | Name | Type | Default | Description | |------|------|---------|-------------| -| size | INT | 3 | Kernel size (side length) in pixels; odd values only (1–21) | +| size | INT | 3 | Kernel size (side length) in pixels; odd values only (1-21) | ## Notes diff --git a/docs/nodes/Multiple Profiles.md b/docs/nodes/Multiple Profiles.md index cd668f7..84a905e 100644 --- a/docs/nodes/Multiple Profiles.md +++ b/docs/nodes/Multiple Profiles.md @@ -19,7 +19,7 @@ Extract and compare line profiles from two fields along a chosen row or column. | Name | Type | Default | Description | |------|------|---------|-------------| -| row | INT | -1 | Row (horizontal) or column (vertical) index to extract; -1 uses the centre row/column (-1–10000) | +| row | INT | -1 | Row (horizontal) or column (vertical) index to extract; -1 uses the centre row/column (-1-10000) | | direction | dropdown | horizontal | Profile direction: horizontal (extract a row) or vertical (extract a column) | | mode | dropdown | overlay | Combination mode: overlay (field_a profile only), mean (average of both), or difference (field_a minus field_b) | diff --git a/docs/nodes/Outlier Mask.md b/docs/nodes/Outlier Mask.md index b989e60..02a7c48 100644 --- a/docs/nodes/Outlier Mask.md +++ b/docs/nodes/Outlier Mask.md @@ -18,7 +18,7 @@ Create a mask marking pixels that deviate more than N standard deviations from t | Name | Type | Default | Description | |------|------|---------|-------------| -| sigma_threshold | FLOAT | 3.0 | Number of standard deviations beyond which a pixel is an outlier (1.0–10.0) | +| sigma_threshold | FLOAT | 3.0 | Number of standard deviations beyond which a pixel is an outlier (1.0-10.0) | | mode | dropdown | both | Which outliers to flag: both (high and low), high only, or low only | ## Notes diff --git a/docs/nodes/Perspective Correction.md b/docs/nodes/Perspective Correction.md index facf1af..b734e47 100644 --- a/docs/nodes/Perspective Correction.md +++ b/docs/nodes/Perspective Correction.md @@ -18,14 +18,14 @@ Fix perspective distortion in a DATA_FIELD via a projective (homography) transfo | Name | Type | Default | Description | |------|------|---------|-------------| -| top_left_x | FLOAT | 0.0 | Horizontal offset of the top-left corner as a fraction of image width (-0.5–0.5) | -| top_left_y | FLOAT | 0.0 | Vertical offset of the top-left corner as a fraction of image height (-0.5–0.5) | -| top_right_x | FLOAT | 0.0 | Horizontal offset of the top-right corner as a fraction of image width (-0.5–0.5) | -| top_right_y | FLOAT | 0.0 | Vertical offset of the top-right corner as a fraction of image height (-0.5–0.5) | -| bottom_left_x | FLOAT | 0.0 | Horizontal offset of the bottom-left corner as a fraction of image width (-0.5–0.5) | -| bottom_left_y | FLOAT | 0.0 | Vertical offset of the bottom-left corner as a fraction of image height (-0.5–0.5) | -| bottom_right_x | FLOAT | 0.0 | Horizontal offset of the bottom-right corner as a fraction of image width (-0.5–0.5) | -| bottom_right_y | FLOAT | 0.0 | Vertical offset of the bottom-right corner as a fraction of image height (-0.5–0.5) | +| top_left_x | FLOAT | 0.0 | Horizontal offset of the top-left corner as a fraction of image width (-0.5-0.5) | +| top_left_y | FLOAT | 0.0 | Vertical offset of the top-left corner as a fraction of image height (-0.5-0.5) | +| top_right_x | FLOAT | 0.0 | Horizontal offset of the top-right corner as a fraction of image width (-0.5-0.5) | +| top_right_y | FLOAT | 0.0 | Vertical offset of the top-right corner as a fraction of image height (-0.5-0.5) | +| bottom_left_x | FLOAT | 0.0 | Horizontal offset of the bottom-left corner as a fraction of image width (-0.5-0.5) | +| bottom_left_y | FLOAT | 0.0 | Vertical offset of the bottom-left corner as a fraction of image height (-0.5-0.5) | +| bottom_right_x | FLOAT | 0.0 | Horizontal offset of the bottom-right corner as a fraction of image width (-0.5-0.5) | +| bottom_right_y | FLOAT | 0.0 | Vertical offset of the bottom-right corner as a fraction of image height (-0.5-0.5) | ## Notes diff --git a/docs/nodes/Pixel Binning.md b/docs/nodes/Pixel Binning.md index 62a25c6..7c629c7 100644 --- a/docs/nodes/Pixel Binning.md +++ b/docs/nodes/Pixel Binning.md @@ -18,7 +18,7 @@ Downsample a DATA_FIELD by grouping pixels into NxN blocks and reducing each blo | Name | Type | Default | Description | |------|------|---------|-------------| -| bin_size | INT | 2 | Side length of the square binning block in pixels (2–64) | +| bin_size | INT | 2 | Side length of the square binning block in pixels (2-64) | | method | dropdown | mean | Reduction method per block: mean (average), sum (total), or median (middle value) | ## Notes diff --git a/docs/nodes/Pixel Classification.md b/docs/nodes/Pixel Classification.md index 466d6ee..fad0624 100644 --- a/docs/nodes/Pixel Classification.md +++ b/docs/nodes/Pixel Classification.md @@ -19,7 +19,7 @@ Classify pixels into discrete classes based on height, slope, and/or curvature u | Name | Type | Default | Description | |------|------|---------|-------------| -| n_classes | INT | 3 | Number of output classes (2–10) | +| n_classes | INT | 3 | Number of output classes (2-10) | | feature | dropdown | height | Feature used for classification: height, slope, curvature, height_slope, or all | | method | dropdown | otsu | Thresholding method: otsu, equal_range, or quantile | diff --git a/docs/nodes/Polynomial Distortion.md b/docs/nodes/Polynomial Distortion.md index e153046..47dd47b 100644 --- a/docs/nodes/Polynomial Distortion.md +++ b/docs/nodes/Polynomial Distortion.md @@ -18,16 +18,16 @@ Correct nonlinear scanner distortions by applying polynomial coordinate warping | Name | Type | Default | Description | |------|------|---------|-------------| -| k1_x | FLOAT | 0.0 | Linear distortion coefficient for the x axis (-1.0–1.0) | -| k2_x | FLOAT | 0.0 | Quadratic distortion coefficient for the x axis (-1.0–1.0) | -| k3_x | FLOAT | 0.0 | Cubic distortion coefficient for the x axis (-1.0–1.0) | -| k1_y | FLOAT | 0.0 | Linear distortion coefficient for the y axis (-1.0–1.0) | -| k2_y | FLOAT | 0.0 | Quadratic distortion coefficient for the y axis (-1.0–1.0) | -| k3_y | FLOAT | 0.0 | Cubic distortion coefficient for the y axis (-1.0–1.0) | +| k1_x | FLOAT | 0.0 | Linear distortion coefficient for the x axis (-1.0-1.0) | +| k2_x | FLOAT | 0.0 | Quadratic distortion coefficient for the x axis (-1.0-1.0) | +| k3_x | FLOAT | 0.0 | Cubic distortion coefficient for the x axis (-1.0-1.0) | +| k1_y | FLOAT | 0.0 | Linear distortion coefficient for the y axis (-1.0-1.0) | +| k2_y | FLOAT | 0.0 | Quadratic distortion coefficient for the y axis (-1.0-1.0) | +| k3_y | FLOAT | 0.0 | Cubic distortion coefficient for the y axis (-1.0-1.0) | ## Notes - k1 controls linear stretching/compression, k2 controls barrel/pincushion-like quadratic distortion, and k3 controls cubic (S-shaped) distortion. - Coefficients for x and y are independent, allowing correction of anisotropic scanner nonlinearities. -- Small values (0.01–0.1) are typical for real scanner corrections; large values produce extreme warping. +- Small values (0.01-0.1) are typical for real scanner corrections; large values produce extreme warping. - Set all coefficients to 0.0 to pass the field through unchanged. diff --git a/docs/nodes/Polynomial Level.md b/docs/nodes/Polynomial Level.md index 9b95298..04a0d3d 100644 --- a/docs/nodes/Polynomial Level.md +++ b/docs/nodes/Polynomial Level.md @@ -19,8 +19,8 @@ Fit and subtract a polynomial background of given degree in x and y. Equivalent | Name | Type | Default | Description | |------|------|---------|-------------| -| degree_x | INT | 2 | Polynomial degree in the x direction (0–5) | -| degree_y | INT | 2 | Polynomial degree in the y direction (0–5) | +| degree_x | INT | 2 | Polynomial degree in the x direction (0-5) | +| degree_y | INT | 2 | Polynomial degree in the y direction (0-5) | ## Notes diff --git a/docs/nodes/Radial Profile.md b/docs/nodes/Radial Profile.md index 87ec4f8..8af77da 100644 --- a/docs/nodes/Radial Profile.md +++ b/docs/nodes/Radial Profile.md @@ -20,7 +20,7 @@ Compute the azimuthally averaged radial profile from a centre point. The output |------|------|---------|-------------| | cx | FLOAT | 0.5 | Centre x position as a fraction of field width (0 = left, 1 = right) | | cy | FLOAT | 0.5 | Centre y position as a fraction of field height (0 = top, 1 = bottom) | -| n_bins | INT | 128 | Number of radial bins (4–4096) | +| n_bins | INT | 128 | Number of radial bins (4-4096) | ## Notes diff --git a/docs/nodes/Rank Filter.md b/docs/nodes/Rank Filter.md index c6fb9ba..b26c055 100644 --- a/docs/nodes/Rank Filter.md +++ b/docs/nodes/Rank Filter.md @@ -18,9 +18,9 @@ Apply a general rank-order (morphological) filter to a DATA_FIELD. Selects the k | Name | Type | Default | Description | |------|------|---------|-------------| -| radius | INT | 3 | Radius of the circular filter window in pixels (1–50) | +| radius | INT | 3 | Radius of the circular filter window in pixels (1-50) | | operation | dropdown | median | Filter operation: erosion (local minimum), dilation (local maximum), median (50th percentile), or percentile (custom rank) | -| percentile | FLOAT | 50.0 | Custom percentile rank, used only when operation is percentile (0.0–100.0) | +| percentile | FLOAT | 50.0 | Custom percentile rank, used only when operation is percentile (0.0-100.0) | ## Notes diff --git a/docs/nodes/Resample.md b/docs/nodes/Resample.md index 47361dc..0272b1f 100644 --- a/docs/nodes/Resample.md +++ b/docs/nodes/Resample.md @@ -18,8 +18,8 @@ Resample a DATA_FIELD to a new pixel resolution while preserving physical dimens | Name | Type | Default | Description | |------|------|---------|-------------| -| width | INT | 256 | Output pixel width (2–16384) | -| height | INT | 256 | Output pixel height (2–16384) | +| width | INT | 256 | Output pixel width (2-16384) | +| height | INT | 256 | Output pixel height (2-16384) | | interpolation | dropdown | linear | Interpolation method: linear, cubic, or nearest | ## Notes diff --git a/docs/nodes/Rotate.md b/docs/nodes/Rotate.md index 46599f3..56e4cfe 100644 --- a/docs/nodes/Rotate.md +++ b/docs/nodes/Rotate.md @@ -18,7 +18,7 @@ Rotate a DATA_FIELD counterclockwise by an angle in degrees. Optionally expand t | Name | Type | Default | Description | |------|------|---------|-------------| -| angle | FLOAT | 90.0 | Rotation angle in degrees, counterclockwise (−360 to 360) | +| angle | FLOAT | 90.0 | Rotation angle in degrees, counterclockwise (-360 to 360) | | interpolation | dropdown | bilinear | Interpolation method for resampling: bilinear, nearest, or bicubic | | expand_canvas | BOOLEAN | True | When True, canvas is expanded to contain the full rotated image; when False, canvas is clipped to original size | diff --git a/docs/nodes/Scar Removal.md b/docs/nodes/Scar Removal.md index d995b9d..b2a47f7 100644 --- a/docs/nodes/Scar Removal.md +++ b/docs/nodes/Scar Removal.md @@ -20,10 +20,10 @@ Detect and remove horizontal scan scars using Gwyddion-derived scar marking thre | Name | Type | Default | Description | |------|------|---------|-------------| | scar_type | dropdown | both | Which scar polarity to detect: both, positive (bright), or negative (dark) | -| threshold_high | FLOAT | 0.666 | High threshold relative to local RMS for strong scar detection (0–2) | -| threshold_low | FLOAT | 0.25 | Low threshold for extending already-detected scars (0–2) | -| min_length | INT | 16 | Minimum horizontal run length in pixels to classify as a scar (1–4096) | -| max_width | INT | 4 | Maximum vertical width in pixels for a scar candidate (1–32) | +| threshold_high | FLOAT | 0.666 | High threshold relative to local RMS for strong scar detection (0-2) | +| threshold_low | FLOAT | 0.25 | Low threshold for extending already-detected scars (0-2) | +| min_length | INT | 16 | Minimum horizontal run length in pixels to classify as a scar (1-4096) | +| max_width | INT | 4 | Maximum vertical width in pixels for a scar candidate (1-32) | ## Notes diff --git a/docs/nodes/Shade.md b/docs/nodes/Shade.md index c867304..61d5c7c 100644 --- a/docs/nodes/Shade.md +++ b/docs/nodes/Shade.md @@ -18,9 +18,9 @@ Render a DATA_FIELD as a directional hillshade image using Lambertian reflectanc | Name | Type | Default | Description | |------|------|---------|-------------| -| azimuth | FLOAT | 0.0 | Light direction in degrees: 0 = north, 90 = east, 180 = south, 270 = west (0–360) | -| elevation | FLOAT | 45.0 | Light elevation angle above the horizon in degrees (0–90) | -| blend | FLOAT | 0.5 | Blend factor between original data and shading: 0.0 = original data only, 1.0 = shading only (0.0–1.0) | +| azimuth | FLOAT | 0.0 | Light direction in degrees: 0 = north, 90 = east, 180 = south, 270 = west (0-360) | +| elevation | FLOAT | 45.0 | Light elevation angle above the horizon in degrees (0-90) | +| blend | FLOAT | 0.5 | Blend factor between original data and shading: 0.0 = original data only, 1.0 = shading only (0.0-1.0) | ## Notes diff --git a/docs/nodes/Slope Distribution.md b/docs/nodes/Slope Distribution.md index f0db138..ef382d3 100644 --- a/docs/nodes/Slope Distribution.md +++ b/docs/nodes/Slope Distribution.md @@ -18,8 +18,8 @@ Compute the angular slope distribution of a DATA_FIELD surface. Equivalent to Gw | Name | Type | Default | Description | |------|------|---------|-------------| -| distribution | dropdown | theta | Distribution type: theta (inclination angle, probability density in 1/deg), phi (azimuthal direction, weighted by slope², 0–360°), or gradient (slope magnitude, probability density in 1/(z/xy)) | -| n_bins | INT | 90 | Number of histogram bins (10–1000) | +| distribution | dropdown | theta | Distribution type: theta (inclination angle, probability density in 1/deg), phi (azimuthal direction, weighted by slope², 0-360°), or gradient (slope magnitude, probability density in 1/(z/xy)) | +| n_bins | INT | 90 | Number of histogram bins (10-1000) | ## Notes diff --git a/docs/nodes/Spot Removal.md b/docs/nodes/Spot Removal.md index 8a004ec..4d8b6cb 100644 --- a/docs/nodes/Spot Removal.md +++ b/docs/nodes/Spot Removal.md @@ -20,7 +20,7 @@ Fill defect pixels (hot pixels, dropouts, scan artifacts) by interpolation. The | Name | Type | Default | Description | |------|------|---------|-------------| | method | dropdown | laplace | Inpainting method: laplace (smooth Laplace equation solution), mean (local mean), or zero | -| max_iter | INT | 100 | Maximum number of iterations for the Laplace solver (1–2000) | +| max_iter | INT | 100 | Maximum number of iterations for the Laplace solver (1-2000) | ## Notes diff --git a/docs/nodes/Straighten Path.md b/docs/nodes/Straighten Path.md index 2afc385..1449b35 100644 --- a/docs/nodes/Straighten Path.md +++ b/docs/nodes/Straighten Path.md @@ -18,10 +18,10 @@ Extract a cross-section along an arbitrary curved path defined by control points | Name | Type | Default | Description | |------|------|---------|-------------| -| points_x | STRING | "0.25, 0.5, 0.75" | Comma-separated fractional x-coordinates of control points (0.0–1.0) | -| points_y | STRING | "0.5, 0.3, 0.5" | Comma-separated fractional y-coordinates of control points (0.0–1.0) | -| thickness | INT | 1 | Width of the sampled strip perpendicular to the path, in pixels (1–100) | -| n_samples | INT | 256 | Number of sample points along the path (10–2048) | +| points_x | STRING | "0.25, 0.5, 0.75" | Comma-separated fractional x-coordinates of control points (0.0-1.0) | +| points_y | STRING | "0.5, 0.3, 0.5" | Comma-separated fractional y-coordinates of control points (0.0-1.0) | +| thickness | INT | 1 | Width of the sampled strip perpendicular to the path, in pixels (1-100) | +| n_samples | INT | 256 | Number of sample points along the path (10-2048) | ## Notes diff --git a/docs/nodes/Template Match.md b/docs/nodes/Template Match.md index f6c782c..2d246aa 100644 --- a/docs/nodes/Template Match.md +++ b/docs/nodes/Template Match.md @@ -20,7 +20,7 @@ Find a template pattern within a larger data field using normalised cross-correl | Name | Type | Default | Description | |------|------|---------|-------------| -| threshold | FLOAT | 0.8 | Minimum correlation score to mark as a detection (0.0–1.0) | +| threshold | FLOAT | 0.8 | Minimum correlation score to mark as a detection (0.0-1.0) | ## Notes diff --git a/docs/nodes/Terrace Fit.md b/docs/nodes/Terrace Fit.md index 10f65c6..3b249ca 100644 --- a/docs/nodes/Terrace Fit.md +++ b/docs/nodes/Terrace Fit.md @@ -19,9 +19,9 @@ Segment a surface into flat terraces separated by atomic steps, fit a polynomial | Name | Type | Default | Description | |------|------|---------|-------------| -| n_terraces | INT | 0 | Number of terraces to fit; 0 = auto-detect from histogram peaks (0–50) | -| broadening | FLOAT | 1.0 | Smoothing factor for terrace detection; larger values merge noisy pixels (0.1–20.0) | -| poly_degree | INT | 0 | Polynomial degree per terrace: 0 = constant (flat), 1 = linear, 2 = quadratic, 3 = cubic (0–3) | +| n_terraces | INT | 0 | Number of terraces to fit; 0 = auto-detect from histogram peaks (0-50) | +| broadening | FLOAT | 1.0 | Smoothing factor for terrace detection; larger values merge noisy pixels (0.1-20.0) | +| poly_degree | INT | 0 | Polynomial degree per terrace: 0 = constant (flat), 1 = linear, 2 = quadratic, 3 = cubic (0-3) | | output | dropdown | residual | Output mode: residual (original minus fit), fitted (fit surface), or labels (terrace assignment map) | ## Notes diff --git a/docs/nodes/Threshold Mask.md b/docs/nodes/Threshold Mask.md index 36a7599..0d79bbb 100644 --- a/docs/nodes/Threshold Mask.md +++ b/docs/nodes/Threshold Mask.md @@ -19,8 +19,8 @@ Create a binary mask by thresholding data. Otsu automatically finds the optimal | Name | Type | Default | Description | |------|------|---------|-------------| -| method | dropdown | absolute | Thresholding method: absolute (raw data value), relative (fraction of min–max range), or otsu (automatic Otsu threshold) | -| threshold | FLOAT | 0.0 | Threshold value; for absolute: raw z value; for relative: fraction 0–1; ignored for otsu (socket-only input) | +| method | dropdown | absolute | Thresholding method: absolute (raw data value), relative (fraction of min-max range), or otsu (automatic Otsu threshold) | +| threshold | FLOAT | 0.0 | Threshold value; for absolute: raw z value; for relative: fraction 0-1; ignored for otsu (socket-only input) | | direction | dropdown | above | Which pixels to select: above or below the threshold | ## Notes diff --git a/docs/nodes/Tip Model.md b/docs/nodes/Tip Model.md index 2bfcf4b..b3c911f 100644 --- a/docs/nodes/Tip Model.md +++ b/docs/nodes/Tip Model.md @@ -20,8 +20,8 @@ Generate a synthetic AFM tip model DATA_FIELD. The input field sets the pixel si |------|------|---------|-------------| | shape | dropdown | parabola | Tip geometry: parabola (paraboloid with apex radius R), cone (sphere-capped cone), or sphere (ball-on-stick) | | radius | FLOAT | 10 nm | Apex radius of curvature in metres (1e-10 to 1e-6) | -| half_angle | FLOAT | 20.0 | Half-cone angle from the tip axis in degrees for the cone shape (1–89°) | -| n_pixels | INT | 65 | Side length of the square tip grid in pixels (odd values only, 3–511) | +| half_angle | FLOAT | 20.0 | Half-cone angle from the tip axis in degrees for the cone shape (1-89°) | +| n_pixels | INT | 65 | Side length of the square tip grid in pixels (odd values only, 3-511) | ## Notes diff --git a/docs/nodes/Trimmed Mean.md b/docs/nodes/Trimmed Mean.md index 744b16f..a6846b6 100644 --- a/docs/nodes/Trimmed Mean.md +++ b/docs/nodes/Trimmed Mean.md @@ -18,12 +18,12 @@ Apply a local trimmed-mean filter to a DATA_FIELD. Within each circular window, | Name | Type | Default | Description | |------|------|---------|-------------| -| radius | INT | 3 | Radius of the circular filter window in pixels (1–50) | -| trim_fraction | FLOAT | 0.1 | Fraction of values to exclude from each end of the sorted window (0.0–0.49) | +| radius | INT | 3 | Radius of the circular filter window in pixels (1-50) | +| trim_fraction | FLOAT | 0.1 | Fraction of values to exclude from each end of the sorted window (0.0-0.49) | ## Notes - A trim_fraction of 0.0 gives a plain local mean (no trimming). A trim_fraction approaching 0.5 converges to the local median. -- Typical values of 0.05–0.2 effectively suppress outlier spikes while preserving smooth features better than a median filter. +- Typical values of 0.05-0.2 effectively suppress outlier spikes while preserving smooth features better than a median filter. - The filter uses a circular (disc-shaped) kernel; the actual window diameter is 2*radius + 1 pixels. - More computationally expensive than a Gaussian filter due to the sorting step within each window. diff --git a/docs/nodes/Watershed Segmentation.md b/docs/nodes/Watershed Segmentation.md index 843889a..fc4726b 100644 --- a/docs/nodes/Watershed Segmentation.md +++ b/docs/nodes/Watershed Segmentation.md @@ -20,11 +20,11 @@ Segment a height field into grains using the two-stage Gwyddion watershed workfl | Name | Type | Default | Description | |------|------|---------|-------------| | invert_height | BOOLEAN | False | When True, detects valleys instead of hills (inverts the height field) | -| locate_steps | INT | 10 | Number of drop steps in the seed location stage (1–200) | -| locate_threshold | INT | 10 | Minimum drop threshold for seed acceptance (0–100000) | -| locate_drop_size | FLOAT | 0.1 | Relative drop size for seed location stage (0.0001–1.0) | -| watershed_steps | INT | 20 | Number of steps in the watershed growth stage (1–2000) | -| watershed_drop_size | FLOAT | 0.1 | Relative drop size for watershed growth stage (0.0001–1.0) | +| locate_steps | INT | 10 | Number of drop steps in the seed location stage (1-200) | +| locate_threshold | INT | 10 | Minimum drop threshold for seed acceptance (0-100000) | +| locate_drop_size | FLOAT | 0.1 | Relative drop size for seed location stage (0.0001-1.0) | +| watershed_steps | INT | 20 | Number of steps in the watershed growth stage (1-2000) | +| watershed_drop_size | FLOAT | 0.1 | Relative drop size for watershed growth stage (0.0001-1.0) | | combine_mode | dropdown | replace | How to combine with an existing mask: replace (ignore existing), union (OR), or intersection (AND) | ## Notes diff --git a/docs/nodes/Wavelet Denoise.md b/docs/nodes/Wavelet Denoise.md index e0ee58c..36f8e7c 100644 --- a/docs/nodes/Wavelet Denoise.md +++ b/docs/nodes/Wavelet Denoise.md @@ -20,7 +20,7 @@ Denoise a DATA_FIELD using wavelet coefficient thresholding. BayesShrink adapts |------|------|---------|-------------| | wavelet | dropdown | db4 | Wavelet family: db1 (Haar), db2, db4, db8, sym4, coif1, or bior1.3 | | method | dropdown | BayesShrink | Threshold estimation method: BayesShrink (per sub-band adaptive) or VisuShrink (global universal) | -| sigma | FLOAT | 0.0 | Noise level estimate in data units; 0 = automatic estimation (0–1.0) | +| sigma | FLOAT | 0.0 | Noise level estimate in data units; 0 = automatic estimation (0-1.0) | | mode | dropdown | soft | Thresholding mode: soft (smooth shrinkage) or hard (zero below threshold) | ## Notes diff --git a/docs/nodes/Zero Crossing.md b/docs/nodes/Zero Crossing.md index 3b00669..b698f1c 100644 --- a/docs/nodes/Zero Crossing.md +++ b/docs/nodes/Zero Crossing.md @@ -18,8 +18,8 @@ Detect edges by finding zero crossings of the Laplacian of Gaussian (LoG). Sigma | Name | Type | Default | Description | |------|------|---------|-------------| -| sigma | FLOAT | 2.0 | Gaussian smoothing scale for the LoG operator (0.5–20.0) | -| threshold | FLOAT | 0.0 | Minimum edge strength as a fraction of the maximum LoG contrast; filters weak edges (0.0–1.0) | +| sigma | FLOAT | 2.0 | Gaussian smoothing scale for the LoG operator (0.5-20.0) | +| threshold | FLOAT | 0.0 | Minimum edge strength as a fraction of the maximum LoG contrast; filters weak edges (0.0-1.0) | ## Notes diff --git a/scripts/generate_demo_particles.py b/scripts/generate_demo_particles.py index 7d8899e..6688d4b 100644 --- a/scripts/generate_demo_particles.py +++ b/scripts/generate_demo_particles.py @@ -44,8 +44,8 @@ for cx_f, cy_f, r_nm, h_nm in fixed: for _ in range(15): cx = RNG.uniform(20, N - 20) cy = RNG.uniform(20, N - 20) - radius = RNG.uniform(30, 180) * 1e-9 # 30–180 nm - height = RNG.uniform(8, 60) * 1e-9 # 8–60 nm + radius = RNG.uniform(30, 180) * 1e-9 # 30-180 nm + height = RNG.uniform(8, 60) * 1e-9 # 8-60 nm particles.append((cx, cy, radius, height)) # --- Render height map --- diff --git a/tests/node_tests/tip_deconvolution.py b/tests/node_tests/tip_deconvolution.py index 89d6093..3c7e397 100644 --- a/tests/node_tests/tip_deconvolution.py +++ b/tests/node_tests/tip_deconvolution.py @@ -97,7 +97,7 @@ def test_deconv_sharpens_broadened_image(): # Simulate measured image via tip dilation (Gwyddion gwy_tip_dilation): # dilation_tip = tip - max(tip) (max shifted to 0, values ≤ 0) - # measured[y,x] = max_{ty,tx}[surface[y−ty, x−tx] + dilation_tip[ty,tx]] + # measured[y,x] = max_{ty,tx}[surface[y-ty, x-tx] + dilation_tip[ty,tx]] dilation_struct = tip_data - tip_data.max() measured_data = grey_dilation(data, structure=dilation_struct) measured = make_field(data=measured_data) diff --git a/tests/test_grains.py b/tests/test_grains.py index 96ef98d..4339fcf 100644 --- a/tests/test_grains.py +++ b/tests/test_grains.py @@ -411,7 +411,7 @@ def test_pipeline_demo_image(): assert grain["max_height"] >= grain["mean_height"] assert grain["mean_height"] > 0 - # Physical size sanity: equivalent diameters should be in the nm–µm range + # Physical size sanity: equivalent diameters should be in the nm-µm range diams_nm = [g["equiv_diam_m"] * 1e9 for g in table] print(f" Diameters: min={min(diams_nm):.0f} nm, max={max(diams_nm):.0f} nm") assert all(1 < d < 2000 for d in diams_nm), \