Files
ykcheungab c377c14474 1. added blender deformation probe for marker size picking and optimization
2. added unwarp map export in current cli, don't need to generate by precompute in sensor running
2026-09-25 15:44:44 +08:00

273 lines
9.7 KiB
Python

"""Tests for `probe_plot`: the optical-flow figures.
matplotlib is an optional extra, so the whole module is skipped when it is absent —
but nothing here needs Blender or cv2. Every figure is built from synthetic arrays and
tiny JSON fixtures, which is deliberate: the figures are a view of measurements, so
they must be testable without making a measurement.
"""
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import pytest
pytest.importorskip("matplotlib")
from orisys_sensor_design import probe_plot # noqa: E402
PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
def _flow(height: int = 60, width: int = 60, value: float = 0.4) -> np.ndarray:
flow = np.zeros((height, width, 2), dtype=np.float32)
flow[..., 0] = value
return flow
def _panel(**overrides) -> dict:
panel = {
"label": "nx=31, dis_mode=1",
"flow": _flow(),
"roi": {"u": 30.0, "v": 30.0, "radius": 6.6},
"cell_px": 16.13,
"effective_patch_px": 16.0,
"applied_px": 1.325,
"response_px": 0.886,
"direction_px": [1.0, 0.0],
"scale_note": "render 500x500 | patch 8 px, finest 1",
"note": "",
}
panel.update(overrides)
return panel
def _assert_png(path: Path) -> None:
assert path.is_file()
assert path.stat().st_size > 0
assert path.read_bytes()[:8] == PNG_MAGIC
def test_flow_comparison_writes_a_png(tmp_path: Path) -> None:
out = probe_plot.render_flow_comparison(
panels=[_panel(label="nx=31"), _panel(label="nx=80", cell_px=6.25, flow=_flow(value=0.9))],
out_path=tmp_path / "flow.png",
)
_assert_png(out)
def test_flow_comparison_annotates_a_missing_flow_array(tmp_path: Path) -> None:
"""A failed setting must appear as a named placeholder, not vanish."""
out = probe_plot.render_flow_comparison(
panels=[
_panel(label="ok"),
_panel(label="broken", flow=None, note="missing flow array 010_x.npy"),
],
out_path=tmp_path / "flow_missing.png",
)
_assert_png(out)
def test_flow_comparison_handles_empty_and_tiny_inputs(tmp_path: Path) -> None:
out = probe_plot.render_flow_comparison(panels=[], out_path=tmp_path / "empty.png")
_assert_png(out)
tiny = _flow(height=3, width=3, value=0.1)
out = probe_plot.render_flow_comparison(
panels=[_panel(flow=tiny, roi={"u": 1.0, "v": 1.0, "radius": 0.5})],
out_path=tmp_path / "tiny.png",
)
_assert_png(out)
def test_tuning_writes_both_panels(tmp_path: Path) -> None:
out = probe_plot.render_tuning(
series=[
{
"label": "render | dis_mode=1",
"points": [
{"nx": 31, "cell_px": 16.13, "effective_patch_px": 16.0, "score": 69.2},
{"nx": 80, "cell_px": 6.25, "effective_patch_px": 16.0, "score": 40.5},
],
},
{
"label": "render | dis_mode=0",
"points": [
{"nx": 31, "cell_px": 16.13, "effective_patch_px": 8.0, "score": 20.0},
{"nx": 80, "cell_px": 6.25, "effective_patch_px": 8.0, "score": 55.0},
],
},
],
out_path=tmp_path / "tuning.png",
patch_px=8.0,
)
_assert_png(out)
def test_tuning_with_no_points_still_writes_a_placeholder(tmp_path: Path) -> None:
out = probe_plot.render_tuning(series=[], out_path=tmp_path / "tuning_empty.png")
_assert_png(out)
def test_response_curves_writes_with_a_per_curve_floor(tmp_path: Path) -> None:
out = probe_plot.render_response_curves(
curves=[
{
"label": "nx=80 r=0.5 mm dm=1",
"verdict": "resolvable",
"score": 57.7,
"floor_px": 0.0155,
"points": [
{"step_mm": 0.01, "response_px": 0.08, "applied_px": 0.13},
{"step_mm": 0.10, "response_px": 0.89, "applied_px": 1.32},
],
},
{
"label": "nx=31 r=0.5 mm dm=1",
"verdict": "resolvable",
"score": 12.3,
# A different floor, so the two must get their own lines.
"floor_px": 0.0372,
"points": [
{"step_mm": 0.01, "response_px": 0.08, "applied_px": 0.13},
{"step_mm": 0.10, "response_px": 0.48, "applied_px": 1.32},
],
},
],
out_path=tmp_path / "response.png",
axis="x",
)
_assert_png(out)
def test_response_curves_adds_a_retention_panel(tmp_path: Path) -> None:
"""Retention is the panel that exposes a pattern collapsing under noise."""
out = probe_plot.render_response_curves(
curves=[
{
"label": "nx=80 r=0.5 mm dm=1",
"points": [{"step_mm": 0.10, "response_px": 0.90, "applied_px": 1.32}],
"floor_px": 0.0155,
"retention": [
{"sigma": 4.0, "response_retention": 0.99},
{"sigma": 16.0, "response_retention": 0.84},
],
},
{
"label": "nx=31 r=0.5 mm dm=1",
"points": [{"step_mm": 0.10, "response_px": 0.48, "applied_px": 1.32}],
"floor_px": 0.0372,
"retention": [
{"sigma": 4.0, "response_retention": 0.72},
{"sigma": 16.0, "response_retention": 0.59},
],
},
],
out_path=tmp_path / "retention.png",
axis="x",
)
_assert_png(out)
def test_response_curve_labels_must_be_unique_to_be_useful(tmp_path: Path) -> None:
"""Regression: the old label was `nx=.. dm..` and omitted the radius, so two runs
(nx80@r0.5@dm0 and nx80@r1.5@dm0) produced identical legend entries."""
runs = [
("nx=80 r=0.5 mm dm=0", 0.03),
("nx=80 r=1.5 mm dm=0", 0.09),
]
out = probe_plot.render_response_curves(
curves=[
{
"label": label,
"points": [{"step_mm": 0.10, "response_px": 0.9, "applied_px": 1.32}],
"floor_px": floor,
}
for label, floor in runs
],
out_path=tmp_path / "unique.png",
axis="x",
)
_assert_png(out)
assert len({label for label, _ in runs}) == 2
def test_response_curves_without_curves(tmp_path: Path) -> None:
out = probe_plot.render_response_curves(
curves=[], out_path=tmp_path / "response_empty.png"
)
_assert_png(out)
def _write_run(directory: Path, *, nx: int = 31, include_flow: bool = True) -> None:
directory.mkdir(parents=True, exist_ok=True)
frames_dir = directory / "frames"
frames_dir.mkdir(exist_ok=True)
frame_name = "000_reference"
(frames_dir / f"{frame_name}.png").write_bytes(PNG_MAGIC)
manifest = {
"width": 500,
"height": 500,
"probe_roi_px": {"u": 30.0, "v": 30.0, "radius": 6.6},
"frames": [
{"index": 0, "name": frame_name, "axis": None, "step_mm": 0.0,
"png": f"frames/{frame_name}.png", "centroid_px": [30.0, 30.0]},
{"index": 1, "name": "001_x_+0.10mm", "axis": "x", "step_mm": 0.10,
"png": "frames/001_x_+0.10mm.png", "centroid_px": [31.3, 30.0]},
],
}
(directory / "render_manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
flow_report = {
"tracker": {"patch_px": 8, "finest_scale": 1},
"scales": [
{"name": "render", "width": 500, "height": 500,
"frames": [{"name": "001_x_+0.10mm",
"roi": {"u": 30.0, "v": 30.0, "radius": 6.6, "pixels": 139}}]}
],
}
(directory / "flow_report.json").write_text(json.dumps(flow_report), encoding="utf-8")
sensitivity = {
"scales": [
{"name": "render", "cell_px": 500.0 / nx, "effective_patch_px": 16.0,
"axes": [{"axis": "x", "direction_px": [1.0, 0.0],
"points": [{"step_mm": 0.10, "response_px": 0.886,
"applied_px": 1.325}]}]}
]
}
(directory / "sensitivity_report.json").write_text(json.dumps(sensitivity), encoding="utf-8")
if include_flow:
flow_dir = directory / "flow" / "render"
flow_dir.mkdir(parents=True, exist_ok=True)
np.save(flow_dir / "001_x_+0.10mm.npy", _flow())
def test_panel_from_run_reads_the_artifacts(tmp_path: Path) -> None:
_write_run(tmp_path, nx=31)
panel = probe_plot.panel_from_run(label="nx=31", output_dir=tmp_path, axis="x", step_mm=0.10)
assert panel["label"] == "nx=31"
assert panel["flow"] is not None
assert panel["response_px"] == pytest.approx(0.886)
assert panel["applied_px"] == pytest.approx(1.325)
assert panel["cell_px"] == pytest.approx(500.0 / 31)
assert panel["effective_patch_px"] == pytest.approx(16.0)
assert "patch 8 px" in panel["scale_note"]
def test_panel_from_run_names_a_missing_flow_array(tmp_path: Path) -> None:
_write_run(tmp_path, include_flow=False)
panel = probe_plot.panel_from_run(label="broken", output_dir=tmp_path, axis="x", step_mm=0.10)
assert panel["flow"] is None
assert "missing flow array" in panel["note"]
def test_panel_from_run_survives_no_artifacts_at_all(tmp_path: Path) -> None:
empty = tmp_path / "nothing"
empty.mkdir()
panel = probe_plot.panel_from_run(label="empty", output_dir=empty, axis="x", step_mm=0.10)
assert panel["flow"] is None
assert panel["cell_px"] is None
# And the figure must still be drawable from it.
out = probe_plot.render_flow_comparison(panels=[panel], out_path=tmp_path / "none.png")
_assert_png(out)