add flip node
This commit is contained in:
@@ -39,6 +39,7 @@ MENU_LAYOUT: dict[str, list[str]] = {
|
|||||||
"ColormapAdjust",
|
"ColormapAdjust",
|
||||||
"CropResizeField",
|
"CropResizeField",
|
||||||
"RotateField",
|
"RotateField",
|
||||||
|
"FlipField",
|
||||||
],
|
],
|
||||||
"Filter": [
|
"Filter": [
|
||||||
"GaussianFilter",
|
"GaussianFilter",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from backend.nodes import (
|
|||||||
colormap_adjust,
|
colormap_adjust,
|
||||||
crop_resize_field,
|
crop_resize_field,
|
||||||
rotate_field,
|
rotate_field,
|
||||||
|
flip_field,
|
||||||
# Level
|
# Level
|
||||||
plane_level_field,
|
plane_level_field,
|
||||||
poly_level_field,
|
poly_level_field,
|
||||||
|
|||||||
93
backend/nodes/flip_field.py
Normal file
93
backend/nodes/flip_field.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from backend.data_types import DataField
|
||||||
|
from backend.node_registry import register_node
|
||||||
|
|
||||||
|
|
||||||
|
@register_node(display_name="Flip")
|
||||||
|
class FlipField:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"field": ("DATA_FIELD",),
|
||||||
|
"axis": (["x", "y"], {"default": "x"}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("DATA_FIELD",)
|
||||||
|
RETURN_NAMES = ("field",)
|
||||||
|
FUNCTION = "process"
|
||||||
|
|
||||||
|
DESCRIPTION = (
|
||||||
|
"Reflect a DATA_FIELD across the X axis (top/bottom) or Y axis (left/right). "
|
||||||
|
"Physical extents are preserved, and stored markup overlays are mirrored with the data."
|
||||||
|
)
|
||||||
|
|
||||||
|
def process(self, field: DataField, axis: str) -> tuple:
|
||||||
|
axis_name = str(axis).strip().lower()
|
||||||
|
if axis_name == "x":
|
||||||
|
flipped = np.flipud(field.data).copy()
|
||||||
|
elif axis_name == "y":
|
||||||
|
flipped = np.fliplr(field.data).copy()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown flip axis: {axis}")
|
||||||
|
|
||||||
|
return (
|
||||||
|
field.replace(
|
||||||
|
data=np.asarray(flipped, dtype=np.float64),
|
||||||
|
overlays=self._flip_overlays(field.overlays, axis_name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _flip_overlays(cls, overlays: list[dict] | None, axis: str) -> list[dict]:
|
||||||
|
if not isinstance(overlays, list):
|
||||||
|
return []
|
||||||
|
|
||||||
|
flipped_overlays = []
|
||||||
|
for overlay in overlays:
|
||||||
|
if not isinstance(overlay, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
next_overlay = deepcopy(overlay)
|
||||||
|
if str(next_overlay.get("kind", "")).strip().lower() == "markup":
|
||||||
|
next_overlay["shapes"] = [
|
||||||
|
cls._flip_markup_shape(shape, axis)
|
||||||
|
for shape in next_overlay.get("shapes", [])
|
||||||
|
if isinstance(shape, dict)
|
||||||
|
]
|
||||||
|
flipped_overlays.append(next_overlay)
|
||||||
|
return flipped_overlays
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _flip_markup_shape(shape: dict, axis: str) -> dict:
|
||||||
|
flipped = deepcopy(shape)
|
||||||
|
kind = str(flipped.get("kind", "")).strip().lower()
|
||||||
|
|
||||||
|
try:
|
||||||
|
x1 = float(flipped.get("x1"))
|
||||||
|
y1 = float(flipped.get("y1"))
|
||||||
|
x2 = float(flipped.get("x2"))
|
||||||
|
y2 = float(flipped.get("y2"))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return flipped
|
||||||
|
|
||||||
|
if axis == "x":
|
||||||
|
y1, y2 = 1.0 - y1, 1.0 - y2
|
||||||
|
else:
|
||||||
|
x1, x2 = 1.0 - x1, 1.0 - x2
|
||||||
|
|
||||||
|
if kind in {"rectangle", "circle"}:
|
||||||
|
x1, x2 = sorted((x1, x2))
|
||||||
|
y1, y2 = sorted((y1, y2))
|
||||||
|
|
||||||
|
flipped["x1"] = float(np.clip(x1, 0.0, 1.0))
|
||||||
|
flipped["y1"] = float(np.clip(y1, 0.0, 1.0))
|
||||||
|
flipped["x2"] = float(np.clip(x2, 0.0, 1.0))
|
||||||
|
flipped["y2"] = float(np.clip(y2, 0.0, 1.0))
|
||||||
|
return flipped
|
||||||
@@ -256,6 +256,81 @@ def test_rotate_field_overlay_warning():
|
|||||||
print(" PASS\n")
|
print(" PASS\n")
|
||||||
|
|
||||||
|
|
||||||
|
def test_flip_field():
|
||||||
|
print("=== Test: FlipField ===")
|
||||||
|
from backend.nodes.flip_field import FlipField
|
||||||
|
from backend.node_registry import get_node_info
|
||||||
|
|
||||||
|
node = FlipField()
|
||||||
|
data = np.arange(1, 10, dtype=np.float64).reshape(3, 3)
|
||||||
|
markup_overlay = {
|
||||||
|
"kind": "markup",
|
||||||
|
"shapes": [
|
||||||
|
{"kind": "line", "x1": 0.1, "y1": 0.2, "x2": 0.9, "y2": 0.8, "width": 2, "color": "#ffffff"},
|
||||||
|
{"kind": "rectangle", "x1": 0.15, "y1": 0.1, "x2": 0.45, "y2": 0.6, "width": 3, "color": "#ff0000"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
annotation_overlay = {
|
||||||
|
"kind": "annotation",
|
||||||
|
"show_scale_bar": True,
|
||||||
|
"show_color_map": False,
|
||||||
|
"text_size": 14.0,
|
||||||
|
}
|
||||||
|
field = DataField(
|
||||||
|
data=data,
|
||||||
|
xreal=3.0,
|
||||||
|
yreal=4.0,
|
||||||
|
xoff=10.0,
|
||||||
|
yoff=20.0,
|
||||||
|
si_unit_xy="nm",
|
||||||
|
si_unit_z="nm",
|
||||||
|
overlays=[markup_overlay, annotation_overlay],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert get_node_info("FlipField")["category"] == "Modify"
|
||||||
|
|
||||||
|
flipped_x, = node.process(field, axis="x")
|
||||||
|
assert np.array_equal(flipped_x.data, np.flipud(data))
|
||||||
|
assert flipped_x.xreal == field.xreal
|
||||||
|
assert flipped_x.yreal == field.yreal
|
||||||
|
assert flipped_x.xoff == field.xoff
|
||||||
|
assert flipped_x.yoff == field.yoff
|
||||||
|
assert flipped_x.si_unit_xy == field.si_unit_xy
|
||||||
|
assert flipped_x.si_unit_z == field.si_unit_z
|
||||||
|
assert np.isclose(flipped_x.overlays[0]["shapes"][0]["x1"], 0.1)
|
||||||
|
assert np.isclose(flipped_x.overlays[0]["shapes"][0]["y1"], 0.8)
|
||||||
|
assert np.isclose(flipped_x.overlays[0]["shapes"][0]["x2"], 0.9)
|
||||||
|
assert np.isclose(flipped_x.overlays[0]["shapes"][0]["y2"], 0.2)
|
||||||
|
assert np.isclose(flipped_x.overlays[0]["shapes"][1]["x1"], 0.15)
|
||||||
|
assert np.isclose(flipped_x.overlays[0]["shapes"][1]["y1"], 0.4)
|
||||||
|
assert np.isclose(flipped_x.overlays[0]["shapes"][1]["x2"], 0.45)
|
||||||
|
assert np.isclose(flipped_x.overlays[0]["shapes"][1]["y2"], 0.9)
|
||||||
|
assert flipped_x.overlays[1] == annotation_overlay
|
||||||
|
|
||||||
|
flipped_y, = node.process(field, axis="y")
|
||||||
|
assert np.array_equal(flipped_y.data, np.fliplr(data))
|
||||||
|
assert np.isclose(flipped_y.overlays[0]["shapes"][0]["x1"], 0.9)
|
||||||
|
assert np.isclose(flipped_y.overlays[0]["shapes"][0]["y1"], 0.2)
|
||||||
|
assert np.isclose(flipped_y.overlays[0]["shapes"][0]["x2"], 0.1)
|
||||||
|
assert np.isclose(flipped_y.overlays[0]["shapes"][0]["y2"], 0.8)
|
||||||
|
assert np.isclose(flipped_y.overlays[0]["shapes"][1]["x1"], 0.55)
|
||||||
|
assert np.isclose(flipped_y.overlays[0]["shapes"][1]["y1"], 0.1)
|
||||||
|
assert np.isclose(flipped_y.overlays[0]["shapes"][1]["x2"], 0.85)
|
||||||
|
assert np.isclose(flipped_y.overlays[0]["shapes"][1]["y2"], 0.6)
|
||||||
|
assert flipped_y.overlays[1] == annotation_overlay
|
||||||
|
|
||||||
|
assert field.overlays[0]["shapes"][0]["x1"] == markup_overlay["shapes"][0]["x1"]
|
||||||
|
assert field.overlays[0]["shapes"][0]["y1"] == markup_overlay["shapes"][0]["y1"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
node.process(field, axis="diagonal")
|
||||||
|
raise AssertionError("Expected invalid flip axis to raise ValueError")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(" PASS\n")
|
||||||
|
|
||||||
|
|
||||||
def test_view3d_normalizes_small_physical_extents_for_display():
|
def test_view3d_normalizes_small_physical_extents_for_display():
|
||||||
print("=== Test: View3D extent normalization ===")
|
print("=== Test: View3D extent normalization ===")
|
||||||
from backend.nodes.view_3d import View3D
|
from backend.nodes.view_3d import View3D
|
||||||
@@ -2437,6 +2512,7 @@ if __name__ == "__main__":
|
|||||||
test_median_filter()
|
test_median_filter()
|
||||||
test_crop_resize_field()
|
test_crop_resize_field()
|
||||||
test_rotate_field()
|
test_rotate_field()
|
||||||
|
test_flip_field()
|
||||||
test_colormap_adjust()
|
test_colormap_adjust()
|
||||||
test_edge_detect()
|
test_edge_detect()
|
||||||
test_fft_filter_1d()
|
test_fft_filter_1d()
|
||||||
|
|||||||
Reference in New Issue
Block a user