add keywords for all nodes

This commit is contained in:
2026-04-04 14:58:56 -07:00
parent 69f1d1bebd
commit a0d3b22f18
195 changed files with 437 additions and 198 deletions

View File

@@ -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

View File

@@ -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::

View File

@@ -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/<channel>/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

View File

@@ -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.

View File

@@ -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":

View File

@@ -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),)

View File

@@ -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,

View File

@@ -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,

View File

@@ -84,6 +84,8 @@ class Calibration:
"Equivalent to Gwyddion's calibrate functionality."
)
KEYWORDS = ("units", "rescale", "dimensions")
def process(
self,
field: DataField,

View File

@@ -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)},)

View File

@@ -37,6 +37,8 @@ class CropResizeField:
"resizing preserves the cropped physical size."
)
KEYWORDS = ("resize", "rescale", "trim", "bilinear", "bicubic", "nearest")
def process(
self,
field: DataField,

View File

@@ -30,6 +30,8 @@ class CrossCorrelate:
"alignment."
)
KEYWORDS = ("xcorr", "alignment", "registration", "drift", "match")
def process(
self,
field_a: DataField,

View File

@@ -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,

View File

@@ -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,

View File

@@ -291,6 +291,8 @@ class Curvature:
"also returns the two corresponding height profiles."
)
KEYWORDS = ("radius", "principal", "quadratic", "bow")
def process(
self,
field: DataField,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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:

View File

@@ -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))

View File

@@ -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.")

View File

@@ -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(

View File

@@ -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,

View File

@@ -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):

View File

@@ -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))

View File

@@ -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))

View File

@@ -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))

View File

@@ -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)

View File

@@ -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":

View File

@@ -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

View File

@@ -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":

View File

@@ -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:

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -115,6 +115,8 @@ class GrainDistanceTransform:
"image-boundary handling matching mask_edt."
)
KEYWORDS = ("edt", "euclidean", "chessboard", "cityblock", "manhattan")
def process(
self,
field: DataField,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -32,10 +32,12 @@ class GrainMark:
DESCRIPTION = (
"Mark grains by thresholding height, slope magnitude, or curvature. "
"Thresholds are relative (01) 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)

View File

@@ -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)

View File

@@ -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))

View File

@@ -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,

View File

@@ -39,6 +39,8 @@ class HoughTransform:
"For circles: centre coordinates and radius. "
)
KEYWORDS = ("line detection", "circle detection", "shape detection")
def process(
self,
field: DataField,

View File

@@ -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:

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -43,6 +43,8 @@ class LateralForceSim:
"the sample surface."
)
KEYWORDS = ("lfm", "friction", "ffm", "tribology")
def process(
self,
field: DataField,

View File

@@ -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()

View File

@@ -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,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -172,6 +172,8 @@ class LogisticClassification:
"generates pseudo-labels automatically."
)
KEYWORDS = ("machine learning", "regression", "segment", "ml", "neural")
def process(
self,
field: DataField,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -49,6 +49,8 @@ class MFMCurrentSimulation:
"gradient dHz/dz."
)
KEYWORDS = ("magnetic", "biot savart", "wire", "strip", "force", "dipole", "tip")
def process(
self,
field: DataField,

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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:

View File

@@ -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()

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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),)

View File

@@ -27,10 +27,12 @@ class LogPolarPSDF:
DESCRIPTION = (
"Compute the power spectral density function in log-polar coordinates. "
"The x-axis is the azimuthal angle (0360°) 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

View File

@@ -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
# ------------------------------------------------------------------

View File

@@ -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

View File

@@ -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()

View File

@@ -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:

View File

@@ -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,

View File

@@ -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,

Some files were not shown because too many files have changed in this diff Show More