fix H5 scaling and 3D view, carousel reset
This commit is contained in:
51
arhdf.log
Normal file
51
arhdf.log
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
=== Channel: Adhesion:Retrace ===
|
||||||
|
DimScaling shape: (2, 2)
|
||||||
|
DimScaling[0,:]: [8.656250e-06 1.665625e-05] (row 0)
|
||||||
|
DimScaling[1,:]: [-4.e-06 4.e-06] (row 1)
|
||||||
|
--- If [start, end] interpretation ---
|
||||||
|
xreal (row1 range) = 8e-06
|
||||||
|
yreal (row0 range) = 7.999999999999998e-06
|
||||||
|
--- If [step, offset] interpretation ---
|
||||||
|
row0: step=8.656249999999648e-06 offset=1.6656249999999646e-05
|
||||||
|
row1: step=-4e-06 offset=4e-06
|
||||||
|
DimUnits: ['m' 'm']
|
||||||
|
DataUnits: N
|
||||||
|
DimExtents ('Resolution 0'): [512 384]
|
||||||
|
|
||||||
|
=== Channel: FFMZSensor:Retrace ===
|
||||||
|
DimScaling shape: (2, 2)
|
||||||
|
DimScaling[0,:]: [8.656250e-06 1.665625e-05] (row 0)
|
||||||
|
DimScaling[1,:]: [-4.e-06 4.e-06] (row 1)
|
||||||
|
--- If [start, end] interpretation ---
|
||||||
|
xreal (row1 range) = 8e-06
|
||||||
|
yreal (row0 range) = 7.999999999999998e-06
|
||||||
|
--- If [step, offset] interpretation ---
|
||||||
|
row0: step=8.656249999999648e-06 offset=1.6656249999999646e-05
|
||||||
|
row1: step=-4e-06 offset=4e-06
|
||||||
|
DimUnits: ['m' 'm']
|
||||||
|
DataUnits: m
|
||||||
|
DimExtents ('Resolution 0'): [512 384]
|
||||||
|
|
||||||
|
=== Channel: MaxForce:Retrace ===
|
||||||
|
DimScaling shape: (2, 2)
|
||||||
|
DimScaling[0,:]: [8.656250e-06 1.665625e-05] (row 0)
|
||||||
|
DimScaling[1,:]: [-4.e-06 4.e-06] (row 1)
|
||||||
|
--- If [start, end] interpretation ---
|
||||||
|
xreal (row1 range) = 8e-06
|
||||||
|
yreal (row0 range) = 7.999999999999998e-06
|
||||||
|
--- If [step, offset] interpretation ---
|
||||||
|
row0: step=8.656249999999648e-06 offset=1.6656249999999646e-05
|
||||||
|
row1: step=-4e-06 offset=4e-06
|
||||||
|
DimUnits: ['m' 'm']
|
||||||
|
DataUnits: N
|
||||||
|
DimExtents ('Resolution 0'): [512 384]
|
||||||
|
|
||||||
|
=== 2D dataset shapes ===
|
||||||
|
Image/DataSet/Resolution 0/Frame 0/Adhesion:Retrace/Image shape=(512, 384)
|
||||||
|
Image/DataSet/Resolution 0/Frame 0/FFMZSensor:Retrace/Image shape=(512, 384)
|
||||||
|
Image/DataSet/Resolution 0/Frame 0/MaxForce:Retrace/Image shape=(512, 384)
|
||||||
|
Image/DataSetInfo/Global/Channels/Adhesion:Retrace/Thumbnail shape=(128, 128)
|
||||||
|
Image/DataSetInfo/Global/Channels/FFMZSensor:Retrace/Thumbnail shape=(128, 128)
|
||||||
|
Image/DataSetInfo/Global/Channels/MaxForce:Retrace/Thumbnail shape=(128, 128)
|
||||||
|
Image/DataSetInfo/Global/Thumbnail shape=(128, 128)
|
||||||
@@ -5,9 +5,10 @@ Asylum Research instruments store scan metadata in a sidecar group rather
|
|||||||
than as dataset attributes. This importer reads physical dimensions from:
|
than as dataset attributes. This importer reads physical dimensions from:
|
||||||
|
|
||||||
Image/DataSetInfo/Global/Channels/<channel>/ImageDims
|
Image/DataSetInfo/Global/Channels/<channel>/ImageDims
|
||||||
DimScaling – (2,2) array: [[px_size_x, offset_x], [px_size_y, offset_y]]
|
DimScaling – (2,2) array: [[Y_start, Y_end], [X_start, X_end]]
|
||||||
DimExtents – pixel counts [xres, yres] (stored in a child group)
|
absolute physical coordinate ranges in DimUnits
|
||||||
DimUnits – lateral unit strings
|
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
|
DataUnits – Z unit string
|
||||||
|
|
||||||
If the sidecar group is absent (generic HDF5), standard dataset attributes
|
If the sidecar group is absent (generic HDF5), standard dataset attributes
|
||||||
@@ -78,6 +79,12 @@ def _ar_image_dims(f, ds_name: str) -> dict | None:
|
|||||||
and the metadata lives at:
|
and the metadata lives at:
|
||||||
"Image/DataSetInfo/Global/Channels/<channel>/ImageDims"
|
"Image/DataSetInfo/Global/Channels/<channel>/ImageDims"
|
||||||
|
|
||||||
|
DimScaling is a (2, 2) array of *absolute physical coordinate ranges*
|
||||||
|
(not per-pixel step sizes), stored Y-first:
|
||||||
|
scaling[0, :] = [Y_start, Y_end]
|
||||||
|
scaling[1, :] = [X_start, X_end]
|
||||||
|
Both values are in the unit given by DimUnits.
|
||||||
|
|
||||||
Returns a dict with xreal, yreal, xoff, yoff, si_unit_xy, si_unit_z,
|
Returns a dict with xreal, yreal, xoff, yoff, si_unit_xy, si_unit_z,
|
||||||
or None if the group isn't found.
|
or None if the group isn't found.
|
||||||
"""
|
"""
|
||||||
@@ -93,30 +100,22 @@ def _ar_image_dims(f, ds_name: str) -> dict | None:
|
|||||||
if not isinstance(grp, h5py.Group):
|
if not isinstance(grp, h5py.Group):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
scaling = grp.attrs.get("DimScaling") # shape (2, 2): [[px_x, off_x], [px_y, off_y]]
|
scaling = grp.attrs.get("DimScaling") # shape (2, 2): [[Y_start, Y_end], [X_start, X_end]]
|
||||||
dim_units = grp.attrs.get("DimUnits") # array of unit strings, e.g. ['m', 'm']
|
dim_units = grp.attrs.get("DimUnits") # array of unit strings, e.g. ['m', 'm'] (Y then X)
|
||||||
data_units = grp.attrs.get("DataUnits") # Z unit string, e.g. 'N'
|
data_units = grp.attrs.get("DataUnits") # Z unit string, e.g. 'N'
|
||||||
|
|
||||||
if scaling is None or np.asarray(scaling).shape != (2, 2):
|
if scaling is None or np.asarray(scaling).shape != (2, 2):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
scaling = np.asarray(scaling, dtype=np.float64)
|
scaling = np.asarray(scaling, dtype=np.float64)
|
||||||
px_x, off_x = float(scaling[0, 0]), float(scaling[0, 1])
|
# Y axis first (row-major), then X — matching numpy's (rows, cols) convention.
|
||||||
px_y, off_y = float(scaling[1, 0]), float(scaling[1, 1])
|
y_start, y_end = float(scaling[0, 0]), float(scaling[0, 1])
|
||||||
|
x_start, x_end = float(scaling[1, 0]), float(scaling[1, 1])
|
||||||
|
|
||||||
# DimExtents gives pixel counts; use to compute total physical size.
|
xreal = abs(x_end - x_start) or 1e-6
|
||||||
extents_grp = None
|
yreal = abs(y_end - y_start) or 1e-6
|
||||||
for child_name in grp:
|
xoff = min(x_start, x_end)
|
||||||
child = grp[child_name]
|
yoff = min(y_start, y_end)
|
||||||
if isinstance(child, h5py.Group) and "DimExtents" in child.attrs:
|
|
||||||
extents_grp = child
|
|
||||||
break
|
|
||||||
|
|
||||||
xres, yres = 1, 1
|
|
||||||
if extents_grp is not None:
|
|
||||||
ext = np.asarray(extents_grp.attrs["DimExtents"])
|
|
||||||
if ext.size >= 2:
|
|
||||||
xres, yres = int(ext[0]), int(ext[1])
|
|
||||||
|
|
||||||
def _decode(raw, default="m") -> str:
|
def _decode(raw, default="m") -> str:
|
||||||
if raw is None:
|
if raw is None:
|
||||||
@@ -127,12 +126,20 @@ def _ar_image_dims(f, ds_name: str) -> dict | None:
|
|||||||
return raw.decode("utf-8", errors="replace").strip() or default
|
return raw.decode("utf-8", errors="replace").strip() or default
|
||||||
return str(raw).strip() or default
|
return str(raw).strip() or default
|
||||||
|
|
||||||
|
# DimUnits is [Y_unit, X_unit]; X unit is the canonical lateral unit.
|
||||||
|
if dim_units is not None and len(dim_units) >= 2:
|
||||||
|
xy_unit = _decode(dim_units[1])
|
||||||
|
elif dim_units is not None and len(dim_units) >= 1:
|
||||||
|
xy_unit = _decode(dim_units[0])
|
||||||
|
else:
|
||||||
|
xy_unit = "m"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"xreal": abs(px_x * xres) or 1e-6,
|
"xreal": xreal,
|
||||||
"yreal": abs(px_y * yres) or 1e-6,
|
"yreal": yreal,
|
||||||
"xoff": off_x,
|
"xoff": xoff,
|
||||||
"yoff": off_y,
|
"yoff": yoff,
|
||||||
"si_unit_xy": _decode(dim_units[0] if dim_units is not None and len(dim_units) >= 1 else None),
|
"si_unit_xy": xy_unit,
|
||||||
"si_unit_z": _decode(data_units),
|
"si_unit_z": _decode(data_units),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import base64
|
|||||||
import io
|
import io
|
||||||
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_mesh
|
from backend.execution_context import emit_mesh, emit_warning
|
||||||
from backend.data_types import (
|
from backend.data_types import (
|
||||||
COLORMAPS,
|
COLORMAPS,
|
||||||
DataField,
|
DataField,
|
||||||
@@ -164,6 +164,14 @@ class View3D:
|
|||||||
data = field.data
|
data = field.data
|
||||||
yres, xres = data.shape
|
yres, xres = data.shape
|
||||||
|
|
||||||
|
phys_ratio = field.xreal / field.yreal if field.yreal else 1.0
|
||||||
|
pixel_ratio = xres / yres if yres else 1.0
|
||||||
|
if abs(phys_ratio / pixel_ratio - 1.0) > 0.02:
|
||||||
|
emit_warning(
|
||||||
|
f"Non-square pixels ({xres}\u00d7{yres} px). "
|
||||||
|
f"The 3D surface shows the physical scan area, not the pixel grid."
|
||||||
|
)
|
||||||
|
|
||||||
step_y = max(1, yres // resolution)
|
step_y = max(1, yres // resolution)
|
||||||
step_x = max(1, xres // resolution)
|
step_x = max(1, xres // resolution)
|
||||||
z = data[::step_y, ::step_x].astype(np.float32)
|
z = data[::step_y, ::step_x].astype(np.float32)
|
||||||
|
|||||||
@@ -852,6 +852,7 @@ function Flow() {
|
|||||||
const [status, setStatus] = useState({ text: 'Connecting…', level: 'info' });
|
const [status, setStatus] = useState({ text: 'Connecting…', level: 'info' });
|
||||||
const [contextMenu, setContextMenu] = useState(null);
|
const [contextMenu, setContextMenu] = useState(null);
|
||||||
const [isCanvasRightZooming, setIsCanvasRightZooming] = useState(false);
|
const [isCanvasRightZooming, setIsCanvasRightZooming] = useState(false);
|
||||||
|
const [executingNodeId, setExecutingNodeId] = useState(null);
|
||||||
|
|
||||||
const flowContainerRef = useRef(null);
|
const flowContainerRef = useRef(null);
|
||||||
const panTimerRef = useRef(null);
|
const panTimerRef = useRef(null);
|
||||||
@@ -1285,15 +1286,19 @@ function Flow() {
|
|||||||
...n,
|
...n,
|
||||||
data: { ...n.data, processingTimeMs: null },
|
data: { ...n.data, processingTimeMs: null },
|
||||||
})));
|
})));
|
||||||
|
setExecutingNodeId(null);
|
||||||
setStatus({ text: 'Running workflow…', level: 'info' });
|
setStatus({ text: 'Running workflow…', level: 'info' });
|
||||||
break;
|
break;
|
||||||
case 'executing':
|
case 'executing':
|
||||||
|
setExecutingNodeId(String(msg.data.node));
|
||||||
setStatus({ text: `Executing node ${msg.data.node}…`, level: 'info' });
|
setStatus({ text: `Executing node ${msg.data.node}…`, level: 'info' });
|
||||||
break;
|
break;
|
||||||
case 'execution_complete':
|
case 'execution_complete':
|
||||||
|
setExecutingNodeId(null);
|
||||||
setStatus({ text: 'Done.', level: 'info' });
|
setStatus({ text: 'Done.', level: 'info' });
|
||||||
break;
|
break;
|
||||||
case 'execution_error':
|
case 'execution_error':
|
||||||
|
setExecutingNodeId(null);
|
||||||
setStatus({ text: 'Error: ' + msg.data.message, level: 'error' });
|
setStatus({ text: 'Error: ' + msg.data.message, level: 'error' });
|
||||||
console.error('[tono] execution error', msg.data);
|
console.error('[tono] execution error', msg.data);
|
||||||
break;
|
break;
|
||||||
@@ -1924,7 +1929,8 @@ function Flow() {
|
|||||||
onResizeGroup: resizeGroup,
|
onResizeGroup: resizeGroup,
|
||||||
onRenameGroup: renameGroup,
|
onRenameGroup: renameGroup,
|
||||||
onUngroup: ungroupGroup,
|
onUngroup: ungroupGroup,
|
||||||
}), [onRuntimeValuesChange, onWidgetChange, openFileBrowser, onManualTrigger, renameGroup, resizeGroup, toggleGroupCollapse, ungroupGroup]);
|
executingNodeId,
|
||||||
|
}), [onRuntimeValuesChange, onWidgetChange, openFileBrowser, onManualTrigger, renameGroup, resizeGroup, toggleGroupCollapse, ungroupGroup, executingNodeId]);
|
||||||
|
|
||||||
const clearGraph = useCallback(() => {
|
const clearGraph = useCallback(() => {
|
||||||
setNodes([]);
|
setNodes([]);
|
||||||
|
|||||||
@@ -413,9 +413,16 @@ function LayerGalleryPreview({ overlay }) {
|
|||||||
const layers = Array.isArray(overlay?.layers) ? overlay.layers : [];
|
const layers = Array.isArray(overlay?.layers) ? overlay.layers : [];
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
|
|
||||||
|
// Reset to 0 only when the layer names change (different file/channels loaded),
|
||||||
|
// not on every graph re-run which produces a new overlay object reference.
|
||||||
|
const layerNamesKey = layers.map((l) => l.name ?? '').join('\0');
|
||||||
|
const prevLayerNamesKeyRef = useRef(layerNamesKey);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIndex(0);
|
if (layerNamesKey !== prevLayerNamesKeyRef.current) {
|
||||||
}, [overlay]);
|
prevLayerNamesKeyRef.current = layerNamesKey;
|
||||||
|
setIndex(0);
|
||||||
|
}
|
||||||
|
}, [layerNamesKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (layers.length === 0) {
|
if (layers.length === 0) {
|
||||||
@@ -1168,6 +1175,8 @@ function CustomNode({ id, data }) {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{ctx?.executingNodeId === id && <div className="node-executing-glow" aria-hidden="true" />}
|
||||||
<div className="custom-node">
|
<div className="custom-node">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="node-title drag-handle" style={{ background: catColor }}>
|
<div className="node-title drag-handle" style={{ background: catColor }}>
|
||||||
@@ -1519,6 +1528,7 @@ function CustomNode({ id, data }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ html, body, #root {
|
|||||||
|
|
||||||
/* ── Custom node ───────────────────────────────────────────────────── */
|
/* ── Custom node ───────────────────────────────────────────────────── */
|
||||||
.custom-node {
|
.custom-node {
|
||||||
|
position: relative;
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border: 1px solid var(--border-default);
|
border: 1px solid var(--border-default);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -1427,6 +1428,43 @@ html, body, #root {
|
|||||||
height: 8px !important;
|
height: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Executing node glow ───────────────────────────────────────────── */
|
||||||
|
.node-executing-glow {
|
||||||
|
position: absolute;
|
||||||
|
inset: -3px;
|
||||||
|
border-radius: 9px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.node-executing-glow::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
/* large centered square so conic sweep stays circular on any node size */
|
||||||
|
width: 800px;
|
||||||
|
height: 800px;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -400px;
|
||||||
|
margin-left: -400px;
|
||||||
|
background: conic-gradient(
|
||||||
|
from 0deg,
|
||||||
|
transparent 0deg,
|
||||||
|
transparent 310deg,
|
||||||
|
rgba(59, 130, 246, 0.12) 330deg,
|
||||||
|
rgba(99, 102, 241, 0.55) 348deg,
|
||||||
|
rgba(167, 139, 250, 1) 354deg,
|
||||||
|
rgba(255, 255, 255, 0.9) 357deg,
|
||||||
|
rgba(167, 139, 250, 0.7) 360deg
|
||||||
|
);
|
||||||
|
animation: node-glow-rotate 1.4s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
@keyframes node-glow-rotate {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Text Note node ────────────────────────────────────────────────── */
|
/* ── Text Note node ────────────────────────────────────────────────── */
|
||||||
.text-note-node {
|
.text-note-node {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
69
scripts/inspect_h5.py
Normal file
69
scripts/inspect_h5.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
"""
|
||||||
|
Inspect DimScaling and DimExtents metadata in an Asylum Research HDF5 file.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/inspect_h5.py /path/to/your/file.h5
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
try:
|
||||||
|
import h5py
|
||||||
|
except ImportError:
|
||||||
|
print("pip install h5py")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python scripts/inspect_h5.py <file.h5>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
path = sys.argv[1]
|
||||||
|
|
||||||
|
with h5py.File(path, "r") as f:
|
||||||
|
channels_path = "Image/DataSetInfo/Global/Channels"
|
||||||
|
grp = f.get(channels_path)
|
||||||
|
if grp is None:
|
||||||
|
print("No Image/DataSetInfo/Global/Channels group found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
for ch_name in grp:
|
||||||
|
dims_path = f"{channels_path}/{ch_name}/ImageDims"
|
||||||
|
dims_grp = f.get(dims_path)
|
||||||
|
if dims_grp is None:
|
||||||
|
print(f"{ch_name}: no ImageDims group")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n=== Channel: {ch_name} ===")
|
||||||
|
|
||||||
|
scaling = dims_grp.attrs.get("DimScaling")
|
||||||
|
if scaling is not None:
|
||||||
|
s = np.asarray(scaling, dtype=float)
|
||||||
|
print(f" DimScaling shape: {s.shape}")
|
||||||
|
print(f" DimScaling[0,:]: {s[0]} (row 0)")
|
||||||
|
print(f" DimScaling[1,:]: {s[1]} (row 1)")
|
||||||
|
print(f" --- If [start, end] interpretation ---")
|
||||||
|
print(f" xreal (row1 range) = {abs(s[1,1] - s[1,0])}")
|
||||||
|
print(f" yreal (row0 range) = {abs(s[0,1] - s[0,0])}")
|
||||||
|
print(f" --- If [step, offset] interpretation ---")
|
||||||
|
print(f" row0: step={s[0,0]} offset={s[0,1]}")
|
||||||
|
print(f" row1: step={s[1,0]} offset={s[1,1]}")
|
||||||
|
else:
|
||||||
|
print(" DimScaling: not found")
|
||||||
|
|
||||||
|
dim_units = dims_grp.attrs.get("DimUnits")
|
||||||
|
print(f" DimUnits: {dim_units}")
|
||||||
|
|
||||||
|
data_units = dims_grp.attrs.get("DataUnits")
|
||||||
|
print(f" DataUnits: {data_units}")
|
||||||
|
|
||||||
|
for child_name in dims_grp:
|
||||||
|
child = dims_grp[child_name]
|
||||||
|
if isinstance(child, h5py.Group) and "DimExtents" in child.attrs:
|
||||||
|
ext = np.asarray(child.attrs["DimExtents"])
|
||||||
|
print(f" DimExtents ('{child_name}'): {ext}")
|
||||||
|
|
||||||
|
print("\n=== 2D dataset shapes ===")
|
||||||
|
def _visit(name, obj):
|
||||||
|
if isinstance(obj, h5py.Dataset) and obj.ndim == 2:
|
||||||
|
print(f" {name} shape={obj.shape}")
|
||||||
|
f.visititems(_visit)
|
||||||
Reference in New Issue
Block a user