fix preview inputs and markup preview

This commit is contained in:
2026-03-27 21:34:51 -07:00
parent 66f1bca046
commit 63bdc70456
13 changed files with 501 additions and 316 deletions

View File

@@ -1088,48 +1088,62 @@ def test_font_node():
def test_preview_image():
print("=== Test: PreviewImage ===")
from backend.nodes.preview_image import PreviewImage
from backend.data_types import ImageData
from backend.execution_context import active_node, execution_callbacks
node = PreviewImage()
# Set up a capture for the broadcast
captured = []
PreviewImage._broadcast_fn = lambda node_id, data_uri: captured.append(data_uri)
PreviewImage._current_node_id = "test"
with execution_callbacks(preview=lambda nid, data_uri: captured.append(data_uri)), active_node("test"):
# Preview with a DataField
field = make_field()
node.preview(colormap="viridis", input=field)
assert len(captured) == 1
assert captured[0].startswith("data:image/png;base64,")
# Preview with a DataField
field = make_field()
node.preview(colormap="viridis", field=field)
assert len(captured) == 1
assert captured[0].startswith("data:image/png;base64,")
# Preview with field overlay metadata
captured.clear()
field_with_overlay = field.replace(overlays=[{"kind": "annotation", "show_scale_bar": True, "show_color_map": False, "text_size": 14.0}])
node.preview(colormap="viridis", input=field_with_overlay)
assert len(captured) == 1
assert captured[0].startswith("data:image/png;base64,")
# Preview with field overlay metadata
captured.clear()
field_with_overlay = field.replace(overlays=[{"kind": "annotation", "show_scale_bar": True, "show_color_map": False, "text_size": 14.0}])
node.preview(colormap="viridis", field=field_with_overlay)
assert len(captured) == 1
assert captured[0].startswith("data:image/png;base64,")
# Preview with a custom colormap input
captured.clear()
custom_colormap = {
"mode": "custom",
"stops": [
{"position": 0.0, "color": "#000000"},
{"position": 0.5, "color": "#ff0000"},
{"position": 1.0, "color": "#ffffff"},
],
}
node.preview(colormap="auto", input=field, colormap_map=custom_colormap)
assert len(captured) == 1
assert captured[0].startswith("data:image/png;base64,")
# Preview with a custom colormap input
captured.clear()
custom_colormap = {
"mode": "custom",
"stops": [
{"position": 0.0, "color": "#000000"},
{"position": 0.5, "color": "#ff0000"},
{"position": 1.0, "color": "#ffffff"},
],
}
node.preview(colormap="auto", field=field, colormap_map=custom_colormap)
assert len(captured) == 1
assert captured[0].startswith("data:image/png;base64,")
# Preview with an IMAGE array
captured.clear()
arr = np.random.default_rng(5).integers(0, 256, (32, 32), dtype=np.uint8)
node.preview(colormap="gray", input=arr)
assert len(captured) == 1
# Preview with an IMAGE array
captured.clear()
arr = np.random.default_rng(5).integers(0, 256, (32, 32), dtype=np.uint8)
node.preview(colormap="gray", image=arr)
assert len(captured) == 1
# Preview with an ANNOTATION_SOURCE carrying a DataField
captured.clear()
node.preview(colormap="auto", input=field_with_overlay)
assert len(captured) == 1
assert captured[0].startswith("data:image/png;base64,")
# Preview with an ANNOTATION_SOURCE carrying an ImageData
captured.clear()
annotated_image = ImageData(
np.zeros((24, 24, 3), dtype=np.uint8),
metadata={"annotation_context": {"xreal": 1e-6, "si_unit_xy": "m"}},
)
node.preview(colormap="auto", input=annotated_image)
assert len(captured) == 1
assert captured[0].startswith("data:image/png;base64,")
# Clean up
PreviewImage._broadcast_fn = None
print(" PASS\n")
@@ -1138,12 +1152,11 @@ def test_annotations():
from backend.nodes.annotations import Annotations
from backend.nodes.font_node import Font
from backend.data_types import ImageData
from backend.execution_context import active_node, execution_callbacks
node = Annotations()
font_node = Font()
warnings = []
Annotations._broadcast_warning_fn = lambda nid, msg: warnings.append(msg)
Annotations._current_node_id = "test"
field = DataField(
data=np.linspace(0.0, 1.0, 64 * 64, dtype=np.float64).reshape(64, 64),
xreal=1e-6,
@@ -1157,98 +1170,101 @@ def test_annotations():
plain_preview = render_datafield_preview(field, "viridis")
assert np.array_equal(plain_preview, base)
plain_field, = node.render(input=field, colormap="auto", show_scale_bar=False, show_color_map=False)
assert isinstance(plain_field, DataField)
assert np.array_equal(plain_field.data, field.data)
assert plain_field.colormap == "viridis"
assert plain_field.overlays[-1]["kind"] == "annotation"
plain = render_datafield_preview(plain_field, plain_field.colormap)
assert plain.shape == base.shape
assert np.array_equal(plain, base)
with execution_callbacks(warning=lambda nid, msg: warnings.append(msg)), active_node("test"):
plain_field, = node.render(input=field, colormap="auto", show_scale_bar=False, show_color_map=False)
assert isinstance(plain_field, DataField)
assert np.array_equal(plain_field.data, field.data)
assert plain_field.colormap == "viridis"
assert plain_field.overlays[-1]["kind"] == "annotation"
plain = render_datafield_preview(plain_field, plain_field.colormap)
assert plain.shape == base.shape
assert np.array_equal(plain, base)
with_scale_field, = node.render(input=field, colormap="auto", show_scale_bar=True, show_color_map=False)
with_scale = render_datafield_preview(with_scale_field, with_scale_field.colormap)
assert with_scale.shape == base.shape
assert not np.array_equal(with_scale, base)
with_scale_field, = node.render(input=field, colormap="auto", show_scale_bar=True, show_color_map=False)
with_scale = render_datafield_preview(with_scale_field, with_scale_field.colormap)
assert with_scale.shape == base.shape
assert not np.array_equal(with_scale, base)
with_legend_field, = node.render(input=field, colormap="auto", show_scale_bar=False, show_color_map=True)
with_legend = render_datafield_preview(with_legend_field, with_legend_field.colormap)
assert with_legend.shape[0] == base.shape[0]
assert with_legend.shape[1] > base.shape[1]
assert with_legend.shape[2] == 3
with_legend_field, = node.render(input=field, colormap="auto", show_scale_bar=False, show_color_map=True)
with_legend = render_datafield_preview(with_legend_field, with_legend_field.colormap)
assert with_legend.shape[0] == base.shape[0]
assert with_legend.shape[1] > base.shape[1]
assert with_legend.shape[2] == 3
larger_legend_field, = node.render(
input=field,
colormap="auto",
show_scale_bar=False,
show_color_map=True,
text_size=28.0,
)
larger_legend_text = render_datafield_preview(larger_legend_field, larger_legend_field.colormap)
assert larger_legend_text.shape == with_legend.shape
assert not np.array_equal(larger_legend_text, with_legend)
larger_legend_field, = node.render(
input=field,
colormap="auto",
show_scale_bar=False,
show_color_map=True,
text_size=28.0,
)
larger_legend_text = render_datafield_preview(larger_legend_field, larger_legend_field.colormap)
assert larger_legend_text.shape[0] == with_legend.shape[0]
assert larger_legend_text.shape[1] > with_legend.shape[1]
assert larger_legend_text.shape[2] == with_legend.shape[2]
assert not np.array_equal(larger_legend_text, with_legend)
annotation_font, = font_node.build("Arial")
with_font_field, = node.render(
input=field,
colormap="auto",
show_scale_bar=False,
show_color_map=True,
text_size=28.0,
font=annotation_font,
)
assert with_font_field.overlays[-1]["font"] == {"family": "Arial", "path": ""}
with_font = render_datafield_preview(with_font_field, with_font_field.colormap)
assert with_font.shape == with_legend.shape
annotation_font, = font_node.build("Arial")
with_font_field, = node.render(
input=field,
colormap="auto",
show_scale_bar=False,
show_color_map=True,
text_size=28.0,
font=annotation_font,
)
assert with_font_field.overlays[-1]["font"] == {"family": "Arial", "path": ""}
with_font = render_datafield_preview(with_font_field, with_font_field.colormap)
assert with_font.shape[0] == with_legend.shape[0]
assert with_font.shape[1] > with_legend.shape[1]
assert with_font.shape[2] == with_legend.shape[2]
with_both_field, = node.render(input=field, colormap="auto", show_scale_bar=True, show_color_map=True)
with_both = render_datafield_preview(with_both_field, with_both_field.colormap)
assert with_both.shape == with_legend.shape
assert not np.array_equal(with_both[:, :base.shape[1]], base)
with_both_field, = node.render(input=field, colormap="auto", show_scale_bar=True, show_color_map=True)
with_both = render_datafield_preview(with_both_field, with_both_field.colormap)
assert with_both.shape == with_legend.shape
assert not np.array_equal(with_both[:, :base.shape[1]], base)
viewport_image = ImageData(
np.zeros((48, 64, 3), dtype=np.uint8),
metadata={
"annotation_context": {
"xreal": 2e-6,
"si_unit_xy": "m",
"legend_min": -1.5,
"legend_mid": 0.0,
"legend_max": 1.5,
"legend_unit": "V",
"colormap": "viridis",
viewport_image = ImageData(
np.zeros((48, 64, 3), dtype=np.uint8),
metadata={
"annotation_context": {
"xreal": 2e-6,
"si_unit_xy": "m",
"legend_min": -1.5,
"legend_mid": 0.0,
"legend_max": 1.5,
"legend_unit": "V",
"colormap": "viridis",
},
},
},
)
annotated_image, = node.render(
input=viewport_image,
colormap="auto",
show_scale_bar=True,
show_color_map=True,
text_size=18.0,
)
assert isinstance(annotated_image, ImageData)
assert annotated_image.shape[0] == viewport_image.shape[0]
assert annotated_image.shape[1] > viewport_image.shape[1]
assert annotated_image.metadata["annotation_context"]["legend_unit"] == "V"
assert not np.array_equal(np.asarray(annotated_image)[:, :viewport_image.shape[1]], np.asarray(viewport_image))
assert warnings == []
)
annotated_image, = node.render(
input=viewport_image,
colormap="auto",
show_scale_bar=True,
show_color_map=True,
text_size=18.0,
)
assert isinstance(annotated_image, ImageData)
assert annotated_image.shape[0] == viewport_image.shape[0]
assert annotated_image.shape[1] > viewport_image.shape[1]
assert annotated_image.metadata["annotation_context"]["legend_unit"] == "V"
assert not np.array_equal(np.asarray(annotated_image)[:, :viewport_image.shape[1]], np.asarray(viewport_image))
assert warnings == []
plain_image = ImageData(np.zeros((32, 40, 3), dtype=np.uint8))
passthrough_image, = node.render(
input=plain_image,
colormap="auto",
show_scale_bar=True,
show_color_map=True,
text_size=18.0,
)
assert isinstance(passthrough_image, ImageData)
assert passthrough_image.shape == plain_image.shape
assert np.array_equal(np.asarray(passthrough_image), np.asarray(plain_image))
assert len(warnings) == 1
assert "no scale metadata" in warnings[0]
Annotations._broadcast_warning_fn = None
plain_image = ImageData(np.zeros((32, 40, 3), dtype=np.uint8))
passthrough_image, = node.render(
input=plain_image,
colormap="auto",
show_scale_bar=True,
show_color_map=True,
text_size=18.0,
)
assert isinstance(passthrough_image, ImageData)
assert passthrough_image.shape == plain_image.shape
assert np.array_equal(np.asarray(passthrough_image), np.asarray(plain_image))
assert len(warnings) == 1
assert "no scale metadata" in warnings[0]
print(" PASS\n")
@@ -1257,67 +1273,74 @@ def test_markup():
print("=== Test: Markup ===")
from backend.nodes.markup import Markup
from backend.data_types import ImageData, _preview_markup_stroke_width
from backend.execution_context import active_node, execution_callbacks
node = Markup()
field = make_field(data=np.linspace(0.0, 1.0, 48 * 48, dtype=np.float64).reshape(48, 48))
base = render_datafield_preview(field, field.colormap)
required_inputs = Markup.INPUT_TYPES()["required"]
assert _preview_markup_stroke_width(5, 128, 128) == 5
assert _preview_markup_stroke_width(5, 2048, 2048) > 5
assert required_inputs["shape"][1]["default"] == "arrow"
assert required_inputs["stroke_color"][1]["default"] == "#ff0000"
overlays = []
Markup._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
Markup._current_node_id = "test"
with execution_callbacks(overlay=lambda nid, data: overlays.append(data)), active_node("test"):
plain_field, = node.process(
input=field,
shape="line",
stroke_color="#ffd54f",
stroke_width=3,
markup_shapes="[]",
)
assert isinstance(plain_field, DataField)
assert plain_field.overlays[-1]["kind"] == "markup"
plain = render_datafield_preview(plain_field, plain_field.colormap)
assert np.array_equal(plain, base)
assert overlays[-1]["kind"] == "markup"
assert overlays[-1]["shape"] == "line"
assert overlays[-1]["stroke_color"] == "#ffd54f"
assert overlays[-1]["stroke_width"] == 3
assert overlays[-1]["image"].startswith("data:image/png;base64,")
plain_field, = node.process(
input=field,
shape="line",
stroke_color="#ffd54f",
stroke_width=3,
markup_shapes="[]",
)
assert isinstance(plain_field, DataField)
assert plain_field.overlays[-1]["kind"] == "markup"
plain = render_datafield_preview(plain_field, plain_field.colormap)
assert np.array_equal(plain, base)
assert overlays[-1]["kind"] == "markup"
assert overlays[-1]["image"].startswith("data:image/png;base64,")
shapes = json.dumps([
{"kind": "line", "x1": 0.1, "y1": 0.1, "x2": 0.9, "y2": 0.9, "width": 3, "color": "#ff0000"},
{"kind": "rectangle", "x1": 0.2, "y1": 0.2, "x2": 0.8, "y2": 0.5, "width": 2, "color": "#00ff00"},
{"kind": "circle", "x1": 0.25, "y1": 0.55, "x2": 0.55, "y2": 0.85, "width": 2, "color": "#4fc3f7"},
{"kind": "arrow", "x1": 0.15, "y1": 0.85, "x2": 0.85, "y2": 0.2, "width": 4, "color": "#ffffff"},
])
marked_field, = node.process(
input=field,
shape="arrow",
stroke_color="#ffffff",
stroke_width=4,
markup_shapes=shapes,
)
marked = render_datafield_preview(marked_field, marked_field.colormap)
assert marked.shape == base.shape
assert not np.array_equal(marked, base)
assert overlays[-1]["shape"] == "arrow"
assert overlays[-1]["stroke_color"] == "#ffffff"
assert overlays[-1]["stroke_width"] == 4
shapes = json.dumps([
{"kind": "line", "x1": 0.1, "y1": 0.1, "x2": 0.9, "y2": 0.9, "width": 3, "color": "#ff0000"},
{"kind": "rectangle", "x1": 0.2, "y1": 0.2, "x2": 0.8, "y2": 0.5, "width": 2, "color": "#00ff00"},
{"kind": "circle", "x1": 0.25, "y1": 0.55, "x2": 0.55, "y2": 0.85, "width": 2, "color": "#4fc3f7"},
{"kind": "arrow", "x1": 0.15, "y1": 0.85, "x2": 0.85, "y2": 0.2, "width": 4, "color": "#ffffff"},
])
marked_field, = node.process(
input=field,
shape="arrow",
stroke_color="#ffffff",
stroke_width=4,
markup_shapes=shapes,
)
marked = render_datafield_preview(marked_field, marked_field.colormap)
assert marked.shape == base.shape
assert not np.array_equal(marked, base)
viewport_image = ImageData(
np.zeros((48, 48, 3), dtype=np.uint8),
metadata={"annotation_context": {"xreal": 1e-6, "si_unit_xy": "m"}},
)
image_markup, = node.process(
input=viewport_image,
shape="line",
stroke_color="#ff0000",
stroke_width=4,
markup_shapes=json.dumps([
{"kind": "line", "x1": 0.1, "y1": 0.2, "x2": 0.9, "y2": 0.8, "width": 4, "color": "#ff0000"},
]),
)
assert isinstance(image_markup, ImageData)
assert image_markup.metadata["annotation_context"]["si_unit_xy"] == "m"
assert not np.array_equal(np.asarray(image_markup), np.asarray(viewport_image))
viewport_image = ImageData(
np.zeros((48, 48, 3), dtype=np.uint8),
metadata={"annotation_context": {"xreal": 1e-6, "si_unit_xy": "m"}},
)
image_markup, = node.process(
input=viewport_image,
shape="line",
stroke_color="#ff0000",
stroke_width=4,
markup_shapes=json.dumps([
{"kind": "line", "x1": 0.1, "y1": 0.2, "x2": 0.9, "y2": 0.8, "width": 4, "color": "#ff0000"},
]),
)
assert isinstance(image_markup, ImageData)
assert image_markup.metadata["annotation_context"]["si_unit_xy"] == "m"
assert not np.array_equal(np.asarray(image_markup), np.asarray(viewport_image))
Markup._broadcast_overlay_fn = None
print(" PASS\n")