new_gui: live smargon-trace dialog + camera fast-scaling (lag fix)
- replace CSV-polling smargon trace with a live QtCharts dialog (deltaX/Y/Z + distance from a reference, fed by the status stream); launch from camera strip - camera live feed uses fast (nearest) scaling instead of smooth — smooth rescaling every frame caused lag at low frame rates / long exposures Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
49dfb40eff
commit
53aeabd99e
@@ -44,8 +44,11 @@ Backend contract is documented in the `new-gui-backend-paths` memory.
|
||||
(new — smargon-z move), staff-only **ABR** (GMX/GMY/GMZ + save/goto) under
|
||||
SMARGON. Energy lives in the staff Tools dialog (not the motors panel).
|
||||
- **Camera exp/gain** are inline in the camera tab strip (Exp/Gain → `samcam_settings`).
|
||||
- **Prediction metrics** dialog (per-class counts, confidence, FPS) from the
|
||||
prediction stream (launched from the camera strip). (smargon-trace still deferred.)
|
||||
- **Prediction metrics** dialog (per-class counts, confidence, FPS) and a **live
|
||||
smargon-trace** dialog (ΔX/ΔY/ΔZ + distance from a reference, fed by the status
|
||||
stream — no CSV) from the camera strip.
|
||||
- Live camera uses fast (nearest) scaling to avoid lag at low frame rates / long
|
||||
exposures.
|
||||
- **Polish (Phase 4)**: keyboard shortcuts (Ctrl+1/2 mode, Alt+1-4 camera tabs,
|
||||
Ctrl+T tools, F1 help, Ctrl+Q quit); window-geometry + mode persistence
|
||||
(QSettings); a **filename builder** (prefix + auto-incrementing run number) that
|
||||
|
||||
@@ -217,6 +217,7 @@ class MainWindow(QWidget):
|
||||
cc.face_detect_clicked.connect(self._open_face_detection)
|
||||
cc.stability_clicked.connect(self._open_stability)
|
||||
cc.metrics_clicked.connect(self._open_metrics)
|
||||
cc.trace_clicked.connect(self._open_trace)
|
||||
if hasattr(self.daq, "raster_scan_completed"):
|
||||
self.daq.raster_scan_completed.connect(self.manual.on_raster_completed)
|
||||
|
||||
@@ -414,6 +415,14 @@ class MainWindow(QWidget):
|
||||
self._stability_dialog.show()
|
||||
self._stability_dialog.raise_()
|
||||
|
||||
def _open_trace(self) -> None:
|
||||
from aare.gui.new_gui.widgets.smargon_trace_dialog import SmargonTraceDialog
|
||||
if getattr(self, "_trace_dialog", None) is None:
|
||||
self._trace_dialog = SmargonTraceDialog(self._palette, self)
|
||||
self.daq.update.connect(self._trace_dialog.update_daq_status)
|
||||
self._trace_dialog.show()
|
||||
self._trace_dialog.raise_()
|
||||
|
||||
def _open_metrics(self) -> None:
|
||||
from aare.gui.new_gui.widgets.prediction_metrics_dialog import (
|
||||
PredictionMetricsDialog,
|
||||
|
||||
@@ -362,8 +362,11 @@ class CameraViewport(QWidget):
|
||||
r = self.rect()
|
||||
|
||||
if self._pixmap is not None and not self._pixmap.isNull():
|
||||
# Fast (nearest) scaling for the live feed — smooth scaling every
|
||||
# frame is the main source of perceived camera lag, especially at
|
||||
# low frame rates / long exposures.
|
||||
scaled = self._pixmap.scaled(
|
||||
r.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation
|
||||
r.size(), Qt.KeepAspectRatio, Qt.FastTransformation
|
||||
)
|
||||
x = (r.width() - scaled.width()) // 2
|
||||
y = (r.height() - scaled.height()) // 2
|
||||
|
||||
@@ -27,6 +27,7 @@ class CameraControls(QWidget):
|
||||
face_detect_clicked = Signal()
|
||||
stability_clicked = Signal()
|
||||
metrics_clicked = Signal()
|
||||
trace_clicked = Signal()
|
||||
|
||||
def __init__(self, palette: Palette, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -62,6 +63,7 @@ class CameraControls(QWidget):
|
||||
("⊙ Face", "Face detection", self.face_detect_clicked.emit),
|
||||
("∿ Stability", "Target stability", self.stability_clicked.emit),
|
||||
("◧ Metrics", "Prediction metrics", self.metrics_clicked.emit),
|
||||
("⤳ Trace", "Smargon trace", self.trace_clicked.emit),
|
||||
):
|
||||
b = QPushButton(text)
|
||||
b.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Live smargon trace — plots sample-holder drift from a reference over time.
|
||||
|
||||
Driven by the status stream (no CSV file polling): captures a reference sh_mm,
|
||||
then plots ΔX/ΔY/ΔZ (and total distance) vs time with QtCharts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
|
||||
from PySide6.QtCharts import QChart, QChartView, QLineSeries, QValueAxis
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QPainter
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from aare.gui.new_gui.theme import build_qss
|
||||
|
||||
|
||||
class SmargonTraceDialog(QDialog):
|
||||
def __init__(self, palette, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Smargon trace")
|
||||
self.setModal(False)
|
||||
self.resize(640, 440)
|
||||
self.setStyleSheet(build_qss(palette))
|
||||
self._ref = None
|
||||
self._t0 = time.monotonic()
|
||||
self._last = 0.0
|
||||
self._maxpts = 600
|
||||
|
||||
lay = QVBoxLayout(self)
|
||||
top = QHBoxLayout()
|
||||
self._stat = QLabel("Waiting for smargon position…")
|
||||
self._stat.setStyleSheet(f"color:{palette.text_muted};")
|
||||
top.addWidget(self._stat, 1)
|
||||
ref = QPushButton("Set reference"); ref.clicked.connect(self._set_ref)
|
||||
clr = QPushButton("Clear"); clr.clicked.connect(self._clear)
|
||||
for b in (ref, clr):
|
||||
b.setCursor(Qt.PointingHandCursor)
|
||||
top.addWidget(ref); top.addWidget(clr)
|
||||
lay.addLayout(top)
|
||||
|
||||
self._sx = QLineSeries(); self._sx.setName("ΔX")
|
||||
self._sy = QLineSeries(); self._sy.setName("ΔY")
|
||||
self._sz = QLineSeries(); self._sz.setName("ΔZ")
|
||||
self._sd = QLineSeries(); self._sd.setName("Distance")
|
||||
self._chart = QChart()
|
||||
for s in (self._sx, self._sy, self._sz, self._sd):
|
||||
self._chart.addSeries(s)
|
||||
self._ax = QValueAxis(); self._ax.setTitleText("Time [s]")
|
||||
self._ay = QValueAxis(); self._ay.setTitleText("Δ [mm]")
|
||||
self._chart.addAxis(self._ax, Qt.AlignBottom)
|
||||
self._chart.addAxis(self._ay, Qt.AlignLeft)
|
||||
for s in (self._sx, self._sy, self._sz, self._sd):
|
||||
s.attachAxis(self._ax); s.attachAxis(self._ay)
|
||||
view = QChartView(self._chart); view.setRenderHint(QPainter.Antialiasing)
|
||||
lay.addWidget(view, 1)
|
||||
|
||||
def _set_ref(self) -> None:
|
||||
self._ref = None # re-captured on next status
|
||||
|
||||
def _clear(self) -> None:
|
||||
for s in (self._sx, self._sy, self._sz, self._sd):
|
||||
s.clear()
|
||||
self._ref = None
|
||||
self._t0 = time.monotonic()
|
||||
|
||||
def update_daq_status(self, s) -> None:
|
||||
if not self.isVisible():
|
||||
return
|
||||
now = time.monotonic()
|
||||
if now - self._last < 0.2:
|
||||
return
|
||||
self._last = now
|
||||
geom = getattr(s, "geom", None)
|
||||
smg = getattr(geom, "smargon", None) if geom else None
|
||||
sh = getattr(smg, "sh_mm", None) if smg else None
|
||||
if sh is None:
|
||||
return
|
||||
cur = (float(sh.x), float(sh.y), float(sh.z))
|
||||
if self._ref is None:
|
||||
self._ref = cur
|
||||
dx, dy, dz = (cur[0] - self._ref[0], cur[1] - self._ref[1], cur[2] - self._ref[2])
|
||||
dist = math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||
t = now - self._t0
|
||||
for series, val in ((self._sx, dx), (self._sy, dy), (self._sz, dz),
|
||||
(self._sd, dist)):
|
||||
series.append(t, val)
|
||||
if series.count() > self._maxpts:
|
||||
series.remove(0)
|
||||
self._stat.setText(
|
||||
f"ΔX {dx:+.4f} ΔY {dy:+.4f} ΔZ {dz:+.4f} · dist {dist:.4f} mm")
|
||||
self._rescale()
|
||||
|
||||
def _rescale(self) -> None:
|
||||
pts = self._sd.pointsVector() if hasattr(self._sd, "pointsVector") else self._sd.points()
|
||||
if not pts:
|
||||
return
|
||||
xs = [p.x() for p in pts]
|
||||
self._ax.setRange(min(xs), max(xs) + 0.01)
|
||||
vals = []
|
||||
for series in (self._sx, self._sy, self._sz, self._sd):
|
||||
sp = series.points()
|
||||
vals.extend(p.y() for p in sp)
|
||||
if vals:
|
||||
lo, hi = min(vals), max(vals)
|
||||
pad = max(0.001, (hi - lo) * 0.1)
|
||||
self._ay.setRange(lo - pad, hi + pad)
|
||||
Reference in New Issue
Block a user