added reference png export

This commit is contained in:
2026-06-23 12:34:48 +03:00
parent 716fd0b07a
commit 02bbe83446
4 changed files with 141 additions and 0 deletions
@@ -3,6 +3,7 @@
from __future__ import annotations
from python_app.gui.preprocess_dialog import PreprocessDialog
from python_app.gui.trace_png_export import export_trace_png
from python_app.orchestration.preprocess_assets import (
PREPROCESS_ASSET_SPECS,
VISIBLE_PREPROCESS_ASSET_KEYS,
@@ -701,6 +702,12 @@ class AppWindowPreprocessMixin:
f"{display_name} sequence completed and saved: set={set_name}, "
f"radar_variants={len(saved_sets)} [{saved_summary}]"
)
self._export_multi_radar_preview_pngs(
session=session,
saved_sets=saved_sets,
kind=kind,
set_name=set_name,
)
else:
dialog.set_status(f"{display_name} set saved: {set_name} ({len(collection.traces)} traces)")
self._log(f"{display_name} sequence completed and saved: set={set_name}, key={radar_key}")
@@ -708,6 +715,50 @@ class AppWindowPreprocessMixin:
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to save preprocess set", exc)
def _export_multi_radar_preview_pngs(
self,
*,
session: MultiRadarSequentialCaptureSession,
saved_sets: list,
kind: str,
set_name: str,
) -> None:
"""Save amplitude/phase PNGs for every captured combo across all radar configs.
The live preview only shows the last config of each combo, so this persists a
graph for the other configs too, under the store's ``preview_png/`` tree. The
set is already saved by the time we get here, so any rendering failure is
logged but never aborts the save.
"""
channel = preprocess_asset_channel(kind)
display_name = preprocess_asset_display_name(kind)
# batch.traces and saved_sets are both ordered by the session's radar
# variants, so index i refers to the same config in both.
saved_count = 0
for batch in session.captured_batches():
for trace, saved in zip(batch.traces, saved_sets):
combo_label = f"input={trace.combo.input} output={trace.combo.output}"
try:
png_path = self._store.preview_png_dir(kind, set_name, saved.radar_key) / (
f"i{trace.combo.input}_o{trace.combo.output}.png"
)
export_trace_png(
trace,
png_path,
channel=channel,
title=f"{display_name} | {saved.display_name} | {combo_label}",
)
saved_count += 1
except Exception as exc: # noqa: BLE001
self._log(
f"Failed to save preview PNG for {saved.display_name} ({combo_label}): {exc}"
)
if saved_count and saved_sets:
png_root = self._store.preview_png_dir(kind, set_name, saved_sets[0].radar_key).parent
self._log(
f"Saved {saved_count} preview PNG(s) for {len(saved_sets)} radar config(s) under {png_root}"
)
def _abort_capture_sequence(self, *, resume_pipeline: bool = True) -> None:
"""Abort active capture session and optionally resume pipeline."""
if self._capture_session is None:
+77
View File
@@ -0,0 +1,77 @@
"""Render captured traces (amplitude + phase) to standalone PNG files.
The preprocess preview pane only shows the *last* radar variant of each combo,
so during a multi-config reference capture the operator never sees the other
variants. This helper renders the same amplitude/phase plot pair used in the
preview into off-screen PNG files, letting us persist a graph for every config.
"""
from __future__ import annotations
import logging
from pathlib import Path
import numpy as np
import pyqtgraph as pg
from PyQt6.QtCore import QRectF
from pyqtgraph.exporters import ImageExporter
from python_app.models.dataset_model import TraceData
logger = logging.getLogger(__name__)
# Matches the preview pane styling in preprocess_dialog so saved PNGs and the
# on-screen preview look the same.
_BACKGROUND = "#101418"
_MAGNITUDE_PEN = pg.mkPen("#4cc9f0", width=1.8)
_PHASE_PEN = pg.mkPen("#f48c06", width=1.8)
_EXPORT_SIZE = (1400, 520)
def export_trace_png(
trace: TraceData,
output_path: Path,
*,
channel: str,
title: str,
) -> None:
"""Render one trace's amplitude (dB) and wrapped phase (deg) panes to a PNG.
Builds an off-screen ``GraphicsLayoutWidget`` (never shown), plots the same
curves as the live preview, and exports the scene as a PNG. Requires a
running ``QApplication`` (always true inside the GUI).
"""
samples = trace.s11 if channel == "s11" else trace.s21
magnitude_db = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12))
phase_deg = np.degrees(np.angle(samples))
layout = pg.GraphicsLayoutWidget(show=False, size=_EXPORT_SIZE)
layout.setBackground(_BACKGROUND)
try:
layout.addLabel(title, row=0, col=0, colspan=2)
magnitude_plot = layout.addPlot(row=1, col=0)
magnitude_plot.showGrid(x=True, y=True, alpha=0.2)
magnitude_plot.setLabel("bottom", "Frequency", units="Hz")
magnitude_plot.setLabel("left", "Magnitude", units="dB")
magnitude_plot.plot(trace.frequency_hz, magnitude_db, pen=_MAGNITUDE_PEN)
phase_plot = layout.addPlot(row=1, col=1)
phase_plot.showGrid(x=True, y=True, alpha=0.2)
phase_plot.setLabel("bottom", "Frequency", units="Hz")
phase_plot.setLabel("left", "Phase", units="deg")
phase_plot.plot(trace.frequency_hz, phase_deg, pen=_PHASE_PEN)
# An off-screen widget never receives a resizeEvent, so its central
# GraphicsLayout keeps its small preferred size and both plots would be
# squeezed into the left edge of the image. Force the layout geometry to
# the target size, doing manually what a resizeEvent normally triggers.
layout.ci.setGeometry(QRectF(0, 0, _EXPORT_SIZE[0], _EXPORT_SIZE[1]))
output_path.parent.mkdir(parents=True, exist_ok=True)
exporter = ImageExporter(layout.scene())
exporter.parameters()["width"] = _EXPORT_SIZE[0]
exporter.export(str(output_path))
finally:
layout.close()
layout.deleteLater()