clean up node naming
This commit is contained in:
@@ -585,13 +585,16 @@ class ExecutionEngine:
|
|||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
fallback_image = f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"
|
fallback_image = f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"
|
||||||
|
|
||||||
return {
|
result_dict = {
|
||||||
"kind": "line_plot",
|
"kind": "line_plot",
|
||||||
"line": y.tolist(),
|
"line": y.tolist(),
|
||||||
"x_axis": x.tolist(),
|
"x_axis": x.tolist(),
|
||||||
"interactive": False,
|
"interactive": False,
|
||||||
"fallback_image": fallback_image,
|
"fallback_image": fallback_image,
|
||||||
}
|
}
|
||||||
|
if y_meta is not None and y_meta.x_unit:
|
||||||
|
result_dict["x_unit"] = y_meta.x_unit
|
||||||
|
return result_dict
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +1,60 @@
|
|||||||
# Import all node modules to trigger @register_node decorators.
|
# Import all node modules to trigger @register_node decorators.
|
||||||
from backend.nodes import (
|
from backend.nodes import (
|
||||||
# IO
|
# IO
|
||||||
|
colormap,
|
||||||
|
crop_resize,
|
||||||
|
fft_2d_invert,
|
||||||
|
filter_fft_1d,
|
||||||
|
filter_fft_2d,
|
||||||
|
filter_gaussian,
|
||||||
|
filter_median,
|
||||||
|
flip,
|
||||||
|
font,
|
||||||
image,
|
image,
|
||||||
ibw_note,
|
|
||||||
image_demo,
|
image_demo,
|
||||||
folder,
|
folder,
|
||||||
coordinate,
|
coordinate,
|
||||||
coordinate_pair,
|
coordinate_pair,
|
||||||
|
level_facet,
|
||||||
|
level_plane,
|
||||||
|
level_poly,
|
||||||
|
mask_draw,
|
||||||
|
mask_threshold,
|
||||||
|
note,
|
||||||
number,
|
number,
|
||||||
range_slider,
|
range_slider,
|
||||||
|
rotate,
|
||||||
save,
|
save,
|
||||||
save_image,
|
|
||||||
# Filters
|
|
||||||
gaussian_filter,
|
|
||||||
median_filter,
|
|
||||||
edge_detect,
|
edge_detect,
|
||||||
fft_filter_1d,
|
|
||||||
fft_filter_2d,
|
|
||||||
# Modify
|
|
||||||
colormap_adjust,
|
colormap_adjust,
|
||||||
crop_resize_field,
|
|
||||||
rotate_field,
|
|
||||||
flip_field,
|
|
||||||
# Level
|
|
||||||
plane_level_field,
|
|
||||||
facet_level_field,
|
|
||||||
poly_level_field,
|
|
||||||
fix_zero,
|
fix_zero,
|
||||||
line_correction,
|
line_correction,
|
||||||
# Mask
|
# Mask
|
||||||
draw_mask,
|
|
||||||
threshold_mask,
|
|
||||||
mask_morphology,
|
mask_morphology,
|
||||||
mask_invert,
|
mask_invert,
|
||||||
mask_operations,
|
mask_operations,
|
||||||
grain_distance_transform,
|
grain_distance_transform,
|
||||||
|
save_layers,
|
||||||
# Correction
|
# Correction
|
||||||
scar_removal,
|
scar_removal,
|
||||||
# Display
|
# Display
|
||||||
color_map,
|
|
||||||
font_node,
|
|
||||||
annotations,
|
annotations,
|
||||||
angle_measure,
|
angle_measure,
|
||||||
markup,
|
markup,
|
||||||
preview_image,
|
preview_image,
|
||||||
|
statistics,
|
||||||
view_3d,
|
view_3d,
|
||||||
print_table,
|
print_table,
|
||||||
value_display,
|
value_display,
|
||||||
# Analysis
|
# Analysis
|
||||||
curvature,
|
curvature,
|
||||||
fractal_dimension,
|
fractal_dimension,
|
||||||
statistics_node,
|
|
||||||
histogram,
|
histogram,
|
||||||
acf_2d,
|
acf_2d,
|
||||||
acf_1d,
|
acf_1d,
|
||||||
cursors,
|
cursors,
|
||||||
fft_2d,
|
fft_2d,
|
||||||
psdf,
|
psdf,
|
||||||
inverse_fft_2d,
|
|
||||||
cross_section,
|
cross_section,
|
||||||
stats,
|
stats,
|
||||||
watershed_segmentation,
|
watershed_segmentation,
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class Cursors:
|
|||||||
"section_title": "Cursors",
|
"section_title": "Cursors",
|
||||||
"line": y.tolist(),
|
"line": y.tolist(),
|
||||||
"x_axis": x.tolist(),
|
"x_axis": x.tolist(),
|
||||||
|
"x_unit": x_unit,
|
||||||
"x1": x1,
|
"x1": x1,
|
||||||
"x2": x2,
|
"x2": x2,
|
||||||
"y1": float(y1),
|
"y1": float(y1),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class FFT1D:
|
|||||||
|
|
||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
("LINE", "frequency_plot"),
|
("LINE", "frequency_plot"),
|
||||||
('RECORD_TABLE', 'measurement'),
|
('RECORD_TABLE', 'max'),
|
||||||
)
|
)
|
||||||
|
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from backend.nodes.spectral_common import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_node(display_name="2D FFT")
|
@register_node(display_name="FFT 2D")
|
||||||
class FFT2D:
|
class FFT2D:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from backend.data_types import LineData
|
|||||||
from backend.nodes.helpers import _cached_1d_transfer
|
from backend.nodes.helpers import _cached_1d_transfer
|
||||||
|
|
||||||
|
|
||||||
@register_node(display_name="1D FFT Filter")
|
@register_node(display_name="FFT Filter 1D")
|
||||||
class FFTFilter1D:
|
class FFTFilter1D:
|
||||||
"""Bandpass / lowpass / highpass / notch filtering of 1-D line profiles.
|
"""Bandpass / lowpass / highpass / notch filtering of 1-D line profiles.
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ from backend.data_types import DataField
|
|||||||
from backend.nodes.helpers import _cached_2d_transfer
|
from backend.nodes.helpers import _cached_2d_transfer
|
||||||
|
|
||||||
|
|
||||||
@register_node(display_name="2D FFT Filter")
|
@register_node(display_name="FFT Filter 2D")
|
||||||
class FFTFilter2D:
|
class FFTFilter2D:
|
||||||
"""Frequency-domain filtering of 2-D data fields (images).
|
"""Frequency-domain filtering of 2-D data fields (images).
|
||||||
|
|
||||||
@@ -80,6 +80,7 @@ class Histogram:
|
|||||||
"section_title": "Histogram",
|
"section_title": "Histogram",
|
||||||
"line": counts.tolist(),
|
"line": counts.tolist(),
|
||||||
"x_axis": bin_centers.astype(np.float64).tolist(),
|
"x_axis": bin_centers.astype(np.float64).tolist(),
|
||||||
|
"x_unit": field.si_unit_z,
|
||||||
"x1": float(np.clip(x1, 0.0, 1.0)),
|
"x1": float(np.clip(x1, 0.0, 1.0)),
|
||||||
"x2": float(np.clip(x2, 0.0, 1.0)),
|
"x2": float(np.clip(x2, 0.0, 1.0)),
|
||||||
"y1": float(y1),
|
"y1": float(y1),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from backend.node_registry import register_node
|
from backend.node_registry import register_node
|
||||||
from backend.execution_context import emit_preview
|
from backend.execution_context import emit_preview, emit_overlay
|
||||||
from backend.data_types import DataField, encode_preview
|
from backend.data_types import DataField, encode_preview, RecordTable
|
||||||
from backend.nodes.helpers import _mask_overlay
|
from backend.nodes.helpers import _mask_overlay
|
||||||
|
|
||||||
|
|
||||||
@@ -15,14 +15,15 @@ class ThresholdMask:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"field": ("DATA_FIELD",),
|
"field": ("DATA_FIELD",),
|
||||||
"method": (["otsu", "absolute", "relative"],),
|
"method": (["absolute", "relative", "otsu"],),
|
||||||
"threshold": ("FLOAT", {"default": 0.0, "min": -1e9, "max": 1e9, "step": 0.001}),
|
"threshold": ("FLOAT", {"default": 0.0, "min": -1e9, "max": 1e9, "step": 0.001, "socket_only": True}),
|
||||||
"direction": (["above", "below"],),
|
"direction": (["above", "below"],),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OUTPUTS = (
|
OUTPUTS = (
|
||||||
('IMAGE', 'mask'),
|
('IMAGE', 'mask'),
|
||||||
|
('RECORD_TABLE', 'threshold'),
|
||||||
)
|
)
|
||||||
FUNCTION = "process"
|
FUNCTION = "process"
|
||||||
|
|
||||||
@@ -38,6 +39,12 @@ class ThresholdMask:
|
|||||||
def process(self, field: DataField, method: str, threshold: float, direction: str) -> tuple:
|
def process(self, field: DataField, method: str, threshold: float, direction: str) -> tuple:
|
||||||
data = field.data
|
data = field.data
|
||||||
|
|
||||||
|
raw_counts, bin_edges = np.histogram(data.ravel(), bins=256)
|
||||||
|
bin_centers = 0.5 * (bin_edges[:-1] + bin_edges[1:])
|
||||||
|
counts = raw_counts.astype(np.float64)
|
||||||
|
xmin = float(bin_centers[0]) if len(bin_centers) else 0.0
|
||||||
|
xmax = float(bin_centers[-1]) if len(bin_centers) else 1.0
|
||||||
|
|
||||||
if method == "otsu":
|
if method == "otsu":
|
||||||
from skimage.filters import threshold_otsu
|
from skimage.filters import threshold_otsu
|
||||||
t = threshold_otsu(data)
|
t = threshold_otsu(data)
|
||||||
@@ -49,12 +56,31 @@ class ThresholdMask:
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown threshold method: {method}")
|
raise ValueError(f"Unknown threshold method: {method}")
|
||||||
|
|
||||||
|
span = xmax - xmin if xmax != xmin else 1.0
|
||||||
|
threshold_frac = float(np.clip((t - xmin) / span, 0.0, 1.0))
|
||||||
|
|
||||||
|
emit_overlay({
|
||||||
|
"kind": "threshold_histogram",
|
||||||
|
"section_title": "Histogram",
|
||||||
|
"line": counts.tolist(),
|
||||||
|
"x_axis": bin_centers.tolist(),
|
||||||
|
"x_unit": field.si_unit_z,
|
||||||
|
"threshold_frac": threshold_frac,
|
||||||
|
"x_min": xmin,
|
||||||
|
"x_max": xmax,
|
||||||
|
"method": method,
|
||||||
|
"locked": method == "otsu",
|
||||||
|
})
|
||||||
|
|
||||||
if direction == "above":
|
if direction == "above":
|
||||||
mask = (data >= t).astype(np.uint8) * 255
|
mask = (data >= t).astype(np.uint8) * 255
|
||||||
else:
|
else:
|
||||||
mask = (data < t).astype(np.uint8) * 255
|
mask = (data < t).astype(np.uint8) * 255
|
||||||
|
|
||||||
overlay = _mask_overlay(field, mask)
|
emit_preview(encode_preview(_mask_overlay(field, mask)))
|
||||||
emit_preview(encode_preview(overlay))
|
|
||||||
|
|
||||||
return (mask,)
|
table = RecordTable([
|
||||||
|
{"quantity": "threshold", "value": threshold, "unit": field.si_unit_xy},
|
||||||
|
])
|
||||||
|
|
||||||
|
return (mask, table)
|
||||||
2
demo
2
demo
Submodule demo updated: 124b84ca7c...0e24a1eb54
@@ -8,6 +8,7 @@ const CropBoxOverlay = lazy(() => import('./CropBoxOverlay'));
|
|||||||
const MaskPaintOverlay = lazy(() => import('./MaskPaintOverlay'));
|
const MaskPaintOverlay = lazy(() => import('./MaskPaintOverlay'));
|
||||||
const MarkupOverlay = lazy(() => import('./MarkupOverlay'));
|
const MarkupOverlay = lazy(() => import('./MarkupOverlay'));
|
||||||
const AngleMeasureOverlay = lazy(() => import('./AngleMeasureOverlay'));
|
const AngleMeasureOverlay = lazy(() => import('./AngleMeasureOverlay'));
|
||||||
|
const ThresholdHistogram = lazy(() => import('./ThresholdHistogram'));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getSpecTypeAndOptions, isDataSocketSpec, SOCKET_WIDGET_TYPES, TYPE_COLORS, CAT_COLORS,
|
getSpecTypeAndOptions, isDataSocketSpec, SOCKET_WIDGET_TYPES, TYPE_COLORS, CAT_COLORS,
|
||||||
@@ -971,6 +972,9 @@ function CustomNode({ id, data }) {
|
|||||||
visibleInputNames.add(name);
|
visibleInputNames.add(name);
|
||||||
} else if (opts?.hidden) {
|
} else if (opts?.hidden) {
|
||||||
hiddenWidgets.add(name);
|
hiddenWidgets.add(name);
|
||||||
|
} else if (opts?.socket_only) {
|
||||||
|
dataInputs.push({ name, type, label: formatUiLabel(opts?.label || name) });
|
||||||
|
visibleInputNames.add(name);
|
||||||
} else {
|
} else {
|
||||||
widgets.push({ name, type, opts: opts || {}, socketType: SOCKET_WIDGET_TYPES.has(type) ? type : null });
|
widgets.push({ name, type, opts: opts || {}, socketType: SOCKET_WIDGET_TYPES.has(type) ? type : null });
|
||||||
}
|
}
|
||||||
@@ -1079,6 +1083,7 @@ function CustomNode({ id, data }) {
|
|||||||
hiddenWidgets.has('x1')
|
hiddenWidgets.has('x1')
|
||||||
|| data.overlay.kind === 'mask_paint'
|
|| data.overlay.kind === 'mask_paint'
|
||||||
|| data.overlay.kind === 'markup'
|
|| data.overlay.kind === 'markup'
|
||||||
|
|| data.overlay.kind === 'threshold_histogram'
|
||||||
);
|
);
|
||||||
const hidePreviewForInteractiveMask = data.overlay?.kind === 'mask_paint' || data.overlay?.kind === 'markup';
|
const hidePreviewForInteractiveMask = data.overlay?.kind === 'mask_paint' || data.overlay?.kind === 'markup';
|
||||||
const overlayTitle = data.overlay?.section_title
|
const overlayTitle = data.overlay?.section_title
|
||||||
@@ -1286,6 +1291,21 @@ function CustomNode({ id, data }) {
|
|||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Threshold histogram — rendered before preview so it sits above the mask image */}
|
||||||
|
{data.overlay?.kind === 'threshold_histogram' && (
|
||||||
|
<CollapsibleSection title={overlayTitle} defaultOpen={true}>
|
||||||
|
<Suspense fallback={<div className="node-preview" style={{color:'var(--text-muted)',padding:4}}>Loading...</div>}>
|
||||||
|
<ThresholdHistogram
|
||||||
|
overlay={data.overlay}
|
||||||
|
threshold={data.widgetValues.threshold}
|
||||||
|
thresholdConnected={connectedInputs?.has('threshold')}
|
||||||
|
nodeId={id}
|
||||||
|
onWidgetChange={ctx.onWidgetChange}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</CollapsibleSection>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Collapsible preview image */}
|
{/* Collapsible preview image */}
|
||||||
{data.previewImage
|
{data.previewImage
|
||||||
&& !hidePreviewForInteractiveMask
|
&& !hidePreviewForInteractiveMask
|
||||||
@@ -1313,7 +1333,7 @@ function CustomNode({ id, data }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Interactive cross-section overlay */}
|
{/* Interactive cross-section overlay */}
|
||||||
{hasInteractiveOverlay && (
|
{hasInteractiveOverlay && data.overlay?.kind !== 'threshold_histogram' && (
|
||||||
<CollapsibleSection title={overlayTitle} defaultOpen={true}>
|
<CollapsibleSection title={overlayTitle} defaultOpen={true}>
|
||||||
<Suspense fallback={<div className="node-preview" style={{color:'var(--text-muted)',padding:4}}>Loading...</div>}>
|
<Suspense fallback={<div className="node-preview" style={{color:'var(--text-muted)',padding:4}}>Loading...</div>}>
|
||||||
{data.overlay.kind === 'line_plot' ? (
|
{data.overlay.kind === 'line_plot' ? (
|
||||||
@@ -1371,6 +1391,13 @@ function CustomNode({ id, data }) {
|
|||||||
nodeId={id}
|
nodeId={id}
|
||||||
onWidgetChange={ctx.onWidgetChange}
|
onWidgetChange={ctx.onWidgetChange}
|
||||||
/>
|
/>
|
||||||
|
) : data.overlay.kind === 'threshold_histogram' ? (
|
||||||
|
<ThresholdHistogram
|
||||||
|
overlay={data.overlay}
|
||||||
|
threshold={data.widgetValues.threshold}
|
||||||
|
nodeId={id}
|
||||||
|
onWidgetChange={ctx.onWidgetChange}
|
||||||
|
/>
|
||||||
) : data.overlay.kind === 'angle_measure' ? (
|
) : data.overlay.kind === 'angle_measure' ? (
|
||||||
<AngleMeasureOverlay
|
<AngleMeasureOverlay
|
||||||
image={data.overlay.image}
|
image={data.overlay.image}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
|
import { getAxisScale } from './valueFormatting';
|
||||||
|
|
||||||
const ASPECT_RATIO = 3.2 / 2.2;
|
const ASPECT_RATIO = 3.2 / 2.2;
|
||||||
const MARGINS = { top: 18, right: 16, bottom: 34, left: 56 };
|
const MARGINS = { top: 18, right: 16, bottom: 34, left: 56 };
|
||||||
@@ -168,6 +169,8 @@ export default function LinePlotOverlay({
|
|||||||
const yTickCount = Math.max(2, Math.min(5, Math.floor(plotHeight / 40)));
|
const yTickCount = Math.max(2, Math.min(5, Math.floor(plotHeight / 40)));
|
||||||
const xTicks = makeTicks(xMin, xMax, xTickCount);
|
const xTicks = makeTicks(xMin, xMax, xTickCount);
|
||||||
const yTicks = makeTicks(yMin, yMax, yTickCount);
|
const yTicks = makeTicks(yMin, yMax, yTickCount);
|
||||||
|
const xRepresentative = Math.max(Math.abs(xMin), Math.abs(xMax));
|
||||||
|
const { scale: xScale, unitLabel: xUnitLabel } = getAxisScale(xRepresentative, overlay?.x_unit);
|
||||||
const plotStroke = clamp(plotWidth / 240, 1.4, 2.6);
|
const plotStroke = clamp(plotWidth / 240, 1.4, 2.6);
|
||||||
const gridStroke = clamp(plotWidth / 900, 0.6, 1.1);
|
const gridStroke = clamp(plotWidth / 900, 0.6, 1.1);
|
||||||
const cursorStroke = clamp(plotWidth / 220, 1.4, 2.2);
|
const cursorStroke = clamp(plotWidth / 220, 1.4, 2.2);
|
||||||
@@ -225,11 +228,14 @@ export default function LinePlotOverlay({
|
|||||||
<g key={`x-${tick}`}>
|
<g key={`x-${tick}`}>
|
||||||
<line x1={x} y1={plotTop} x2={x} y2={plotTop + plotHeight} stroke="var(--border-default)" strokeWidth={gridStroke} opacity="0.45" />
|
<line x1={x} y1={plotTop} x2={x} y2={plotTop + plotHeight} stroke="var(--border-default)" strokeWidth={gridStroke} opacity="0.45" />
|
||||||
<text x={x} y={height - 10} textAnchor="middle" fontSize="11" fill="var(--text-secondary)">
|
<text x={x} y={height - 10} textAnchor="middle" fontSize="11" fill="var(--text-secondary)">
|
||||||
{formatTick(tick)}
|
{formatTick(tick / xScale)}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{xUnitLabel && (
|
||||||
|
<text x={plotLeft + plotWidth} y={height - 1} textAnchor="end" fontSize="10" fill="var(--text-muted)">{xUnitLabel}</text>
|
||||||
|
)}
|
||||||
|
|
||||||
{yTicks.map((tick) => {
|
{yTicks.map((tick) => {
|
||||||
const y = scaleY(tick);
|
const y = scaleY(tick);
|
||||||
|
|||||||
@@ -462,9 +462,59 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
|||||||
}
|
}
|
||||||
}, [applyCameraState, decode, meshData, scheduleViewportSync, updateDiagnostics]);
|
}, [applyCameraState, decode, meshData, scheduleViewportSync, updateDiagnostics]);
|
||||||
|
|
||||||
// Prevent scroll events from propagating to React Flow
|
// Gesture-aware wheel handling: only capture scroll when it started inside the view.
|
||||||
const onWheel = useCallback((e) => {
|
// Uses capture phase to disable OrbitControls zoom before it fires when gesture started outside.
|
||||||
e.stopPropagation();
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const onEnter = () => {
|
||||||
|
isInsideRef.current = true;
|
||||||
|
pointerEnteredAtRef.current = Date.now();
|
||||||
|
};
|
||||||
|
const onLeave = () => {
|
||||||
|
isInsideRef.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Capture phase: fires before OrbitControls on renderer.domElement
|
||||||
|
const onWheelCapture = () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const msSinceLastWheel = now - lastWheelAtRef.current;
|
||||||
|
const msSinceEnter = now - pointerEnteredAtRef.current;
|
||||||
|
lastWheelAtRef.current = now;
|
||||||
|
|
||||||
|
if (msSinceLastWheel > 300) {
|
||||||
|
gestureStartedInsideRef.current = isInsideRef.current && msSinceEnter > 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gesture started outside — disable OrbitControls zoom so it doesn't intercept
|
||||||
|
if (!gestureStartedInsideRef.current && threeRef.current) {
|
||||||
|
threeRef.current.controls.enableZoom = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bubble phase: fires after OrbitControls has already run (or skipped due to enableZoom=false)
|
||||||
|
const onWheelBubble = (e) => {
|
||||||
|
if (threeRef.current) {
|
||||||
|
threeRef.current.controls.enableZoom = true;
|
||||||
|
}
|
||||||
|
if (gestureStartedInsideRef.current) {
|
||||||
|
e.stopPropagation(); // prevent React Flow from panning when interacting with the 3D view
|
||||||
|
}
|
||||||
|
// else: let event propagate to React Flow so canvas panning continues
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addEventListener('wheel', onWheelCapture, { capture: true, passive: true });
|
||||||
|
container.addEventListener('wheel', onWheelBubble, { passive: false });
|
||||||
|
container.addEventListener('pointerenter', onEnter);
|
||||||
|
container.addEventListener('pointerleave', onLeave);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener('wheel', onWheelCapture, { capture: true });
|
||||||
|
container.removeEventListener('wheel', onWheelBubble);
|
||||||
|
container.removeEventListener('pointerenter', onEnter);
|
||||||
|
container.removeEventListener('pointerleave', onLeave);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onContextMenu = useCallback((e) => {
|
const onContextMenu = useCallback((e) => {
|
||||||
@@ -476,8 +526,7 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
|||||||
<div className="surface-view-shell">
|
<div className="surface-view-shell">
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="nodrag nowheel surface-view-container"
|
className="nodrag surface-view-container"
|
||||||
onWheel={onWheel}
|
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
/>
|
/>
|
||||||
{showDiagnostics ? (
|
{showDiagnostics ? (
|
||||||
|
|||||||
220
frontend/src/ThresholdHistogram.jsx
Normal file
220
frontend/src/ThresholdHistogram.jsx
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
|
import { getAxisScale } from './valueFormatting';
|
||||||
|
|
||||||
|
const ASPECT_RATIO = 3.2 / 2.2;
|
||||||
|
const MARGINS = { top: 18, right: 16, bottom: 34, left: 56 };
|
||||||
|
|
||||||
|
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
|
||||||
|
function round4(v) { return parseFloat(v.toFixed(4)); }
|
||||||
|
function trimZeros(t) { return t.replace(/(?:\.0+|(\.\d+?)0+)$/, '$1'); }
|
||||||
|
|
||||||
|
function formatTick(value) {
|
||||||
|
const abs = Math.abs(value);
|
||||||
|
if (abs === 0) return '0';
|
||||||
|
if (abs >= 1e4 || abs < 1e-3) return value.toExponential(1).replace('e+', 'e');
|
||||||
|
if (abs >= 100) return trimZeros(value.toFixed(0));
|
||||||
|
if (abs >= 10) return trimZeros(value.toFixed(1));
|
||||||
|
if (abs >= 1) return trimZeros(value.toFixed(2));
|
||||||
|
return trimZeros(value.toFixed(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTicks(min, max, count = 5) {
|
||||||
|
if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) return [min];
|
||||||
|
return Array.from({ length: count }, (_, i) => min + (max - min) * i / (count - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExtent(values, fallbackMin = 0, fallbackMax = 1) {
|
||||||
|
if (!Array.isArray(values) || !values.length) return [fallbackMin, fallbackMax];
|
||||||
|
let min = Infinity, max = -Infinity;
|
||||||
|
for (const v of values) { if (Number.isFinite(v)) { if (v < min) min = v; if (v > max) max = v; } }
|
||||||
|
return (Number.isFinite(min) && Number.isFinite(max)) ? [min, max] : [fallbackMin, fallbackMax];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ThresholdHistogram({ overlay, threshold, thresholdConnected, nodeId, onWidgetChange }) {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const [dragging, setDragging] = useState(false);
|
||||||
|
const [size, setSize] = useState({ width: 0 });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return undefined;
|
||||||
|
const update = () => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
setSize({ width: Math.max(1, Math.round(containerRef.current.clientWidth || 320)) });
|
||||||
|
};
|
||||||
|
update();
|
||||||
|
if (typeof ResizeObserver === 'function') {
|
||||||
|
const ro = new ResizeObserver((entries) => {
|
||||||
|
const e = entries[0];
|
||||||
|
if (e) setSize({ width: Math.max(1, Math.round(e.contentRect.width)) });
|
||||||
|
});
|
||||||
|
ro.observe(containerRef.current);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', update);
|
||||||
|
return () => window.removeEventListener('resize', update);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const xValues = Array.isArray(overlay?.x_axis) && overlay.x_axis.length === overlay.line?.length
|
||||||
|
? overlay.x_axis : overlay?.line?.map((_, i) => i) || [];
|
||||||
|
const yValues = Array.isArray(overlay?.line) ? overlay.line : [];
|
||||||
|
const method = overlay?.method ?? 'absolute';
|
||||||
|
const locked = (overlay?.locked ?? false) || !!thresholdConnected;
|
||||||
|
const xMin = overlay?.x_min ?? 0;
|
||||||
|
const xMax = overlay?.x_max ?? 1;
|
||||||
|
|
||||||
|
const width = size.width || 320;
|
||||||
|
const height = Math.round(width / ASPECT_RATIO);
|
||||||
|
const plotLeft = MARGINS.left;
|
||||||
|
const plotTop = MARGINS.top;
|
||||||
|
const plotWidth = Math.max(1, width - MARGINS.left - MARGINS.right);
|
||||||
|
const plotHeight = Math.max(1, height - MARGINS.top - MARGINS.bottom);
|
||||||
|
|
||||||
|
const [xExtMin, xExtMax] = getExtent(xValues, 0, 1);
|
||||||
|
const [yMinRaw, yMaxRaw] = getExtent(yValues, 0, 1);
|
||||||
|
const yPad = yMinRaw === yMaxRaw ? 1 : (yMaxRaw - yMinRaw) * 0.08;
|
||||||
|
const yMin = yMinRaw - yPad;
|
||||||
|
const yMax = yMaxRaw + yPad;
|
||||||
|
|
||||||
|
const scaleX = useCallback((v) => {
|
||||||
|
if (xExtMax === xExtMin) return plotLeft + plotWidth / 2;
|
||||||
|
return plotLeft + (v - xExtMin) / (xExtMax - xExtMin) * plotWidth;
|
||||||
|
}, [plotLeft, plotWidth, xExtMin, xExtMax]);
|
||||||
|
|
||||||
|
const scaleY = useCallback((v) => {
|
||||||
|
if (yMax === yMin) return plotTop + plotHeight / 2;
|
||||||
|
return plotTop + (1 - (v - yMin) / (yMax - yMin)) * plotHeight;
|
||||||
|
}, [plotTop, plotHeight, yMin, yMax]);
|
||||||
|
|
||||||
|
// Compute marker x-fraction from current threshold widget value
|
||||||
|
const markerFrac = (() => {
|
||||||
|
if (locked) return clamp(overlay?.threshold_frac ?? 0.5, 0, 1);
|
||||||
|
const t = threshold ?? 0;
|
||||||
|
if (method === 'relative') return clamp(t, 0, 1);
|
||||||
|
return (xMax === xMin) ? 0.5 : clamp((t - xMin) / (xMax - xMin), 0, 1);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const markerX = plotLeft + markerFrac * plotWidth;
|
||||||
|
|
||||||
|
// Snap marker circle to histogram line height
|
||||||
|
const markerY = (() => {
|
||||||
|
if (!xValues.length || !yValues.length) return plotTop + plotHeight / 2;
|
||||||
|
const targetX = xExtMin + markerFrac * (xExtMax - xExtMin);
|
||||||
|
let best = 0, bestDist = Infinity;
|
||||||
|
for (let i = 0; i < xValues.length; i++) {
|
||||||
|
const d = Math.abs(xValues[i] - targetX);
|
||||||
|
if (d < bestDist) { bestDist = d; best = i; }
|
||||||
|
}
|
||||||
|
return scaleY(yValues[best]);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const handleDrag = useCallback((e) => {
|
||||||
|
if (!onWidgetChange || !nodeId || locked || !containerRef.current) return;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
const frac = clamp((e.clientX - rect.left - plotLeft) / plotWidth, 0, 1);
|
||||||
|
// Relative threshold is a 0-1 fraction — round to 4 dp is fine.
|
||||||
|
// Absolute threshold is in SI units (could be nm/m scale) — keep full float precision.
|
||||||
|
const newThreshold = method === 'relative'
|
||||||
|
? round4(frac)
|
||||||
|
: xMin + frac * (xMax - xMin);
|
||||||
|
onWidgetChange(nodeId, 'threshold', newThreshold);
|
||||||
|
}, [onWidgetChange, nodeId, locked, plotLeft, plotWidth, method, xMin, xMax]);
|
||||||
|
|
||||||
|
const onPointerDown = useCallback((e) => {
|
||||||
|
if (locked) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.currentTarget.setPointerCapture(e.pointerId);
|
||||||
|
setDragging(true);
|
||||||
|
}, [locked]);
|
||||||
|
|
||||||
|
const onPointerMove = useCallback((e) => {
|
||||||
|
if (dragging) handleDrag(e);
|
||||||
|
}, [dragging, handleDrag]);
|
||||||
|
|
||||||
|
const onPointerUp = useCallback(() => setDragging(false), []);
|
||||||
|
|
||||||
|
const path = yValues.map((y, i) => `${i === 0 ? 'M' : 'L'} ${scaleX(xValues[i])} ${scaleY(y)}`).join(' ');
|
||||||
|
const xTickCount = Math.max(2, Math.min(5, Math.floor(plotWidth / 70)));
|
||||||
|
const yTickCount = Math.max(2, Math.min(5, Math.floor(plotHeight / 40)));
|
||||||
|
const xTicks = makeTicks(xExtMin, xExtMax, xTickCount);
|
||||||
|
const yTicks = makeTicks(yMin, yMax, yTickCount);
|
||||||
|
const xRepresentative = Math.max(Math.abs(xExtMin), Math.abs(xExtMax));
|
||||||
|
const { scale: xScale, unitLabel: xUnitLabel } = getAxisScale(xRepresentative, overlay?.x_unit);
|
||||||
|
const plotStroke = clamp(plotWidth / 240, 1.4, 2.6);
|
||||||
|
const gridStroke = clamp(plotWidth / 900, 0.6, 1.1);
|
||||||
|
const cursorStroke = clamp(plotWidth / 220, 1.4, 2.2);
|
||||||
|
const markerRadius = clamp(plotWidth / 42, 5.5, 9);
|
||||||
|
const markerLabelSize = clamp(plotWidth / 34, 8, 11);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="nodrag nowheel lineplot-overlay"
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onLostPointerCapture={onPointerUp}
|
||||||
|
>
|
||||||
|
{locked && (
|
||||||
|
<div style={{ fontSize: 10, color: 'var(--danger-locked)', padding: '0 4px 3px', textAlign: 'right', letterSpacing: '0.04em' }}>
|
||||||
|
{thresholdConnected ? 'Locked — driven by socket' : 'Locked — Otsu auto-threshold'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} className="lineplot-svg">
|
||||||
|
<rect x="0" y="0" width={width} height={height} fill="var(--bg-deep)" />
|
||||||
|
|
||||||
|
{xTicks.map((tick) => {
|
||||||
|
const x = scaleX(tick);
|
||||||
|
return (
|
||||||
|
<g key={`x-${tick}`}>
|
||||||
|
<line x1={x} y1={plotTop} x2={x} y2={plotTop + plotHeight} stroke="var(--border-default)" strokeWidth={gridStroke} opacity="0.45" />
|
||||||
|
<text x={x} y={height - 10} textAnchor="middle" fontSize="11" fill="var(--text-secondary)">{formatTick(tick / xScale)}</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{xUnitLabel && (
|
||||||
|
<text x={plotLeft + plotWidth} y={height - 1} textAnchor="end" fontSize="10" fill="var(--text-muted)">{xUnitLabel}</text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{yTicks.map((tick) => {
|
||||||
|
const y = scaleY(tick);
|
||||||
|
return (
|
||||||
|
<g key={`y-${tick}`}>
|
||||||
|
<line x1={plotLeft} y1={y} x2={plotLeft + plotWidth} y2={y} stroke="var(--border-default)" strokeWidth={gridStroke} opacity="0.45" />
|
||||||
|
<text x={plotLeft - 10} y={y + 4} textAnchor="end" fontSize="11" fill="var(--text-secondary)">{formatTick(tick)}</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<rect x={plotLeft} y={plotTop} width={plotWidth} height={plotHeight} fill="none" stroke="var(--border-default)" strokeWidth={gridStroke + 0.3} />
|
||||||
|
<path d={path} fill="none" stroke="var(--plot-line)" strokeWidth={plotStroke} strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
|
||||||
|
{/* Threshold marker line */}
|
||||||
|
<line
|
||||||
|
x1={markerX} y1={plotTop} x2={markerX} y2={plotTop + plotHeight}
|
||||||
|
stroke="var(--marker)" strokeWidth={cursorStroke} strokeDasharray="10 6" opacity="0.95"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Threshold marker circle */}
|
||||||
|
<g onPointerDown={onPointerDown} style={{ cursor: locked ? 'default' : 'ew-resize' }}>
|
||||||
|
<circle
|
||||||
|
cx={markerX}
|
||||||
|
cy={markerY}
|
||||||
|
r={markerRadius}
|
||||||
|
className={`lineplot-marker ${locked ? 'lineplot-marker-locked' : ''}`}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={markerX}
|
||||||
|
y={markerY}
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="middle"
|
||||||
|
fontSize={markerLabelSize}
|
||||||
|
className="lineplot-marker-label"
|
||||||
|
pointerEvents="none"
|
||||||
|
>
|
||||||
|
T
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -104,6 +104,21 @@ function choosePrefixExponent(value, power) {
|
|||||||
return candidates.reduce((best, candidate) => (candidate.absScaled > best.absScaled ? candidate : best));
|
return candidates.reduce((best, candidate) => (candidate.absScaled > best.absScaled ? candidate : best));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a representative axis value and a unit string, returns the scale factor
|
||||||
|
* and prefixed unit label to use for a whole axis.
|
||||||
|
* All tick values should be divided by `scale` before display, and `unitLabel` shown once.
|
||||||
|
*/
|
||||||
|
export function getAxisScale(representativeValue, unit) {
|
||||||
|
if (!unit || typeof representativeValue !== 'number' || !Number.isFinite(representativeValue) || representativeValue === 0) {
|
||||||
|
return { scale: 1, unitLabel: unit || '' };
|
||||||
|
}
|
||||||
|
const { valueText, unitText } = applySIPrefix(representativeValue, unit);
|
||||||
|
const scaled = parseFloat(valueText);
|
||||||
|
if (!Number.isFinite(scaled) || scaled === 0) return { scale: 1, unitLabel: unit };
|
||||||
|
return { scale: representativeValue / scaled, unitLabel: unitText };
|
||||||
|
}
|
||||||
|
|
||||||
export function applySIPrefix(value, unit) {
|
export function applySIPrefix(value, unit) {
|
||||||
const formattedUnit = formatDisplayUnit(unit);
|
const formattedUnit = formatDisplayUnit(unit);
|
||||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import numpy as np
|
|||||||
sys.path.insert(0, ".")
|
sys.path.insert(0, ".")
|
||||||
from backend.data_types import DataField
|
from backend.data_types import DataField
|
||||||
from backend.nodes.fft_2d import FFT2D
|
from backend.nodes.fft_2d import FFT2D
|
||||||
from backend.nodes.inverse_fft_2d import InverseFFT2D
|
from backend.nodes.fft_2d_invert import InverseFFT2D
|
||||||
|
|
||||||
|
|
||||||
def make_field(data, xreal=1e-6, yreal=1e-6):
|
def make_field(data, xreal=1e-6, yreal=1e-6):
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def make_field(data, xreal=1e-6, yreal=1e-6):
|
|||||||
def test_threshold_otsu_bimodal():
|
def test_threshold_otsu_bimodal():
|
||||||
"""Otsu on a clean bimodal image should separate the two populations."""
|
"""Otsu on a clean bimodal image should separate the two populations."""
|
||||||
print("=== Test: Otsu on bimodal image ===")
|
print("=== Test: Otsu on bimodal image ===")
|
||||||
from backend.nodes.threshold_mask import ThresholdMask
|
from backend.nodes.mask_threshold import ThresholdMask
|
||||||
node = ThresholdMask()
|
node = ThresholdMask()
|
||||||
|
|
||||||
data = np.zeros((128, 128))
|
data = np.zeros((128, 128))
|
||||||
@@ -50,7 +50,7 @@ def test_threshold_otsu_bimodal():
|
|||||||
def test_threshold_relative_range():
|
def test_threshold_relative_range():
|
||||||
"""Relative threshold at 0.5 should be the midpoint of [min, max]."""
|
"""Relative threshold at 0.5 should be the midpoint of [min, max]."""
|
||||||
print("=== Test: Relative threshold at midpoint ===")
|
print("=== Test: Relative threshold at midpoint ===")
|
||||||
from backend.nodes.threshold_mask import ThresholdMask
|
from backend.nodes.mask_threshold import ThresholdMask
|
||||||
node = ThresholdMask()
|
node = ThresholdMask()
|
||||||
|
|
||||||
data = np.full((64, 64), 2.0)
|
data = np.full((64, 64), 2.0)
|
||||||
@@ -68,7 +68,7 @@ def test_threshold_relative_range():
|
|||||||
def test_threshold_empty_mask():
|
def test_threshold_empty_mask():
|
||||||
"""Very high absolute threshold on low data should produce an empty mask."""
|
"""Very high absolute threshold on low data should produce an empty mask."""
|
||||||
print("=== Test: Empty mask from high threshold ===")
|
print("=== Test: Empty mask from high threshold ===")
|
||||||
from backend.nodes.threshold_mask import ThresholdMask
|
from backend.nodes.mask_threshold import ThresholdMask
|
||||||
node = ThresholdMask()
|
node = ThresholdMask()
|
||||||
|
|
||||||
data = np.ones((64, 64))
|
data = np.ones((64, 64))
|
||||||
@@ -82,7 +82,7 @@ def test_threshold_empty_mask():
|
|||||||
def test_threshold_full_mask():
|
def test_threshold_full_mask():
|
||||||
"""Very low absolute threshold should produce an all-white mask."""
|
"""Very low absolute threshold should produce an all-white mask."""
|
||||||
print("=== Test: Full mask from low threshold ===")
|
print("=== Test: Full mask from low threshold ===")
|
||||||
from backend.nodes.threshold_mask import ThresholdMask
|
from backend.nodes.mask_threshold import ThresholdMask
|
||||||
node = ThresholdMask()
|
node = ThresholdMask()
|
||||||
|
|
||||||
data = np.ones((64, 64)) * 5.0
|
data = np.ones((64, 64)) * 5.0
|
||||||
@@ -316,7 +316,7 @@ def test_adjacent_grains_connectivity():
|
|||||||
def test_pipeline_synthetic():
|
def test_pipeline_synthetic():
|
||||||
"""Full pipeline on a synthetic image with known geometry."""
|
"""Full pipeline on a synthetic image with known geometry."""
|
||||||
print("=== Test: Full pipeline on synthetic grains ===")
|
print("=== Test: Full pipeline on synthetic grains ===")
|
||||||
from backend.nodes.threshold_mask import ThresholdMask
|
from backend.nodes.mask_threshold import ThresholdMask
|
||||||
from backend.nodes.grain_analysis import GrainAnalysis
|
from backend.nodes.grain_analysis import GrainAnalysis
|
||||||
|
|
||||||
N = 200
|
N = 200
|
||||||
@@ -372,7 +372,7 @@ def test_pipeline_demo_image():
|
|||||||
"""Run the full pipeline on the bundled demo nanoparticles image."""
|
"""Run the full pipeline on the bundled demo nanoparticles image."""
|
||||||
print("=== Test: Full pipeline on demo nanoparticles.npy ===")
|
print("=== Test: Full pipeline on demo nanoparticles.npy ===")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from backend.nodes.threshold_mask import ThresholdMask
|
from backend.nodes.mask_threshold import ThresholdMask
|
||||||
from backend.nodes.grain_analysis import GrainAnalysis
|
from backend.nodes.grain_analysis import GrainAnalysis
|
||||||
from backend.runtime_paths import demo_dir
|
from backend.runtime_paths import demo_dir
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def make_field(data=None, shape=(64, 64), xreal=1e-6, yreal=1e-6):
|
|||||||
|
|
||||||
def test_gaussian_filter():
|
def test_gaussian_filter():
|
||||||
print("=== Test: GaussianFilter ===")
|
print("=== Test: GaussianFilter ===")
|
||||||
from backend.nodes.gaussian_filter import GaussianFilter
|
from backend.nodes.filter_gaussian import GaussianFilter
|
||||||
node = GaussianFilter()
|
node = GaussianFilter()
|
||||||
field = make_field()
|
field = make_field()
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ def test_gaussian_filter():
|
|||||||
|
|
||||||
def test_median_filter():
|
def test_median_filter():
|
||||||
print("=== Test: MedianFilter ===")
|
print("=== Test: MedianFilter ===")
|
||||||
from backend.nodes.median_filter import MedianFilter
|
from backend.nodes.filter_median import MedianFilter
|
||||||
node = MedianFilter()
|
node = MedianFilter()
|
||||||
|
|
||||||
# Median filter should remove salt-and-pepper noise
|
# Median filter should remove salt-and-pepper noise
|
||||||
@@ -68,7 +68,7 @@ def test_median_filter():
|
|||||||
|
|
||||||
def test_crop_resize_field():
|
def test_crop_resize_field():
|
||||||
print("=== Test: CropResizeField ===")
|
print("=== Test: CropResizeField ===")
|
||||||
from backend.nodes.crop_resize_field import CropResizeField
|
from backend.nodes.crop_resize import CropResizeField
|
||||||
node = CropResizeField()
|
node = CropResizeField()
|
||||||
|
|
||||||
data = np.arange(32, dtype=np.float64).reshape(4, 8)
|
data = np.arange(32, dtype=np.float64).reshape(4, 8)
|
||||||
@@ -167,7 +167,7 @@ def test_crop_resize_field():
|
|||||||
|
|
||||||
def test_rotate_field():
|
def test_rotate_field():
|
||||||
print("=== Test: RotateField ===")
|
print("=== Test: RotateField ===")
|
||||||
from backend.nodes.rotate_field import RotateField
|
from backend.nodes.rotate import RotateField
|
||||||
node = RotateField()
|
node = RotateField()
|
||||||
|
|
||||||
data = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float64)
|
data = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float64)
|
||||||
@@ -230,7 +230,7 @@ def test_rotate_field():
|
|||||||
|
|
||||||
def test_rotate_field_overlay_warning():
|
def test_rotate_field_overlay_warning():
|
||||||
print("=== Test: RotateField overlay warning ===")
|
print("=== Test: RotateField overlay warning ===")
|
||||||
from backend.nodes.rotate_field import RotateField
|
from backend.nodes.rotate import RotateField
|
||||||
|
|
||||||
node = RotateField()
|
node = RotateField()
|
||||||
warnings = []
|
warnings = []
|
||||||
@@ -258,7 +258,7 @@ def test_rotate_field_overlay_warning():
|
|||||||
|
|
||||||
def test_flip_field():
|
def test_flip_field():
|
||||||
print("=== Test: FlipField ===")
|
print("=== Test: FlipField ===")
|
||||||
from backend.nodes.flip_field import FlipField
|
from backend.nodes.flip import FlipField
|
||||||
from backend.node_registry import get_node_info
|
from backend.node_registry import get_node_info
|
||||||
|
|
||||||
node = FlipField()
|
node = FlipField()
|
||||||
@@ -420,7 +420,7 @@ def test_edge_detect():
|
|||||||
|
|
||||||
def test_fft_filter_1d():
|
def test_fft_filter_1d():
|
||||||
print("=== Test: FFTFilter1D ===")
|
print("=== Test: FFTFilter1D ===")
|
||||||
from backend.nodes.fft_filter_1d import FFTFilter1D
|
from backend.nodes.filter_fft_1d import FFTFilter1D
|
||||||
node = FFTFilter1D()
|
node = FFTFilter1D()
|
||||||
|
|
||||||
# Signal: low-frequency sine + high-frequency sine
|
# Signal: low-frequency sine + high-frequency sine
|
||||||
@@ -464,7 +464,7 @@ def test_fft_filter_1d():
|
|||||||
|
|
||||||
def test_fft_filter_2d():
|
def test_fft_filter_2d():
|
||||||
print("=== Test: FFTFilter2D ===")
|
print("=== Test: FFTFilter2D ===")
|
||||||
from backend.nodes.fft_filter_2d import FFTFilter2D
|
from backend.nodes.filter_fft_2d import FFTFilter2D
|
||||||
node = FFTFilter2D()
|
node = FFTFilter2D()
|
||||||
|
|
||||||
N = 128
|
N = 128
|
||||||
@@ -506,7 +506,7 @@ def test_fft_filter_2d():
|
|||||||
|
|
||||||
def test_plane_level():
|
def test_plane_level():
|
||||||
print("=== Test: PlaneLevelField ===")
|
print("=== Test: PlaneLevelField ===")
|
||||||
from backend.nodes.plane_level_field import PlaneLevelField
|
from backend.nodes.level_plane import PlaneLevelField
|
||||||
node = PlaneLevelField()
|
node = PlaneLevelField()
|
||||||
|
|
||||||
# Create a tilted plane + small signal
|
# Create a tilted plane + small signal
|
||||||
@@ -554,8 +554,8 @@ def test_plane_level():
|
|||||||
def test_facet_level():
|
def test_facet_level():
|
||||||
print("=== Test: FacetLevelField ===")
|
print("=== Test: FacetLevelField ===")
|
||||||
from backend.node_registry import get_node_info
|
from backend.node_registry import get_node_info
|
||||||
from backend.nodes.facet_level_field import FacetLevelField
|
from backend.nodes.level_facet import FacetLevelField
|
||||||
from backend.nodes.plane_level_field import PlaneLevelField
|
from backend.nodes.level_plane import PlaneLevelField
|
||||||
|
|
||||||
def fit_pixel_plane(data: np.ndarray, region: np.ndarray) -> tuple[float, float, float]:
|
def fit_pixel_plane(data: np.ndarray, region: np.ndarray) -> tuple[float, float, float]:
|
||||||
yy, xx = np.mgrid[0:data.shape[0], 0:data.shape[1]]
|
yy, xx = np.mgrid[0:data.shape[0], 0:data.shape[1]]
|
||||||
@@ -628,7 +628,7 @@ def test_facet_level():
|
|||||||
|
|
||||||
def test_poly_level():
|
def test_poly_level():
|
||||||
print("=== Test: PolyLevelField ===")
|
print("=== Test: PolyLevelField ===")
|
||||||
from backend.nodes.poly_level_field import PolyLevelField
|
from backend.nodes.level_poly import PolyLevelField
|
||||||
node = PolyLevelField()
|
node = PolyLevelField()
|
||||||
|
|
||||||
N = 64
|
N = 64
|
||||||
@@ -966,7 +966,7 @@ def test_angle_measure():
|
|||||||
|
|
||||||
def test_statistics():
|
def test_statistics():
|
||||||
print("=== Test: Statistics ===")
|
print("=== Test: Statistics ===")
|
||||||
from backend.nodes.statistics_node import Statistics
|
from backend.nodes.statistics import Statistics
|
||||||
node = Statistics()
|
node = Statistics()
|
||||||
|
|
||||||
data = np.array([[1, 2], [3, 4]], dtype=np.float64)
|
data = np.array([[1, 2], [3, 4]], dtype=np.float64)
|
||||||
@@ -1194,7 +1194,7 @@ def test_cross_section():
|
|||||||
|
|
||||||
def test_threshold_mask():
|
def test_threshold_mask():
|
||||||
print("=== Test: ThresholdMask ===")
|
print("=== Test: ThresholdMask ===")
|
||||||
from backend.nodes.threshold_mask import ThresholdMask
|
from backend.nodes.mask_threshold import ThresholdMask
|
||||||
node = ThresholdMask()
|
node = ThresholdMask()
|
||||||
|
|
||||||
# Clear bimodal data: left half = 0, right half = 1
|
# Clear bimodal data: left half = 0, right half = 1
|
||||||
@@ -1346,7 +1346,7 @@ def test_mask_operations():
|
|||||||
|
|
||||||
def test_draw_mask():
|
def test_draw_mask():
|
||||||
print("=== Test: DrawMask ===")
|
print("=== Test: DrawMask ===")
|
||||||
from backend.nodes.draw_mask import DrawMask
|
from backend.nodes.mask_draw import DrawMask
|
||||||
node = DrawMask()
|
node = DrawMask()
|
||||||
|
|
||||||
field = make_field(data=np.zeros((32, 32), dtype=np.float64))
|
field = make_field(data=np.zeros((32, 32), dtype=np.float64))
|
||||||
@@ -1582,7 +1582,7 @@ def test_load_file():
|
|||||||
|
|
||||||
def test_save_image():
|
def test_save_image():
|
||||||
print("=== Test: SaveImage (Save Layers) ===")
|
print("=== Test: SaveImage (Save Layers) ===")
|
||||||
from backend.nodes.save_image import SaveImage
|
from backend.nodes.save_layers import SaveImage
|
||||||
import tifffile
|
import tifffile
|
||||||
node = SaveImage()
|
node = SaveImage()
|
||||||
input_types = SaveImage.INPUT_TYPES()
|
input_types = SaveImage.INPUT_TYPES()
|
||||||
@@ -1686,7 +1686,7 @@ def test_save_image():
|
|||||||
|
|
||||||
def test_color_map_node():
|
def test_color_map_node():
|
||||||
print("=== Test: ColorMap ===")
|
print("=== Test: ColorMap ===")
|
||||||
from backend.nodes.color_map import ColorMap
|
from backend.nodes.colormap import ColorMap
|
||||||
|
|
||||||
node = ColorMap()
|
node = ColorMap()
|
||||||
|
|
||||||
@@ -1712,7 +1712,7 @@ def test_color_map_node():
|
|||||||
|
|
||||||
def test_font_node():
|
def test_font_node():
|
||||||
print("=== Test: Font ===")
|
print("=== Test: Font ===")
|
||||||
from backend.nodes.font_node import Font
|
from backend.nodes.font import Font
|
||||||
from backend.data_types import CUSTOM_FILE_FONT, SYSTEM_DEFAULT_FONT
|
from backend.data_types import CUSTOM_FILE_FONT, SYSTEM_DEFAULT_FONT
|
||||||
|
|
||||||
node = Font()
|
node = Font()
|
||||||
@@ -1796,7 +1796,7 @@ def test_preview_image():
|
|||||||
def test_annotations():
|
def test_annotations():
|
||||||
print("=== Test: Annotations ===")
|
print("=== Test: Annotations ===")
|
||||||
from backend.nodes.annotations import Annotations
|
from backend.nodes.annotations import Annotations
|
||||||
from backend.nodes.font_node import Font
|
from backend.nodes.font import Font
|
||||||
from backend.data_types import ImageData
|
from backend.data_types import ImageData
|
||||||
from backend.execution_context import active_node, execution_callbacks
|
from backend.execution_context import active_node, execution_callbacks
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user