GUI: improved performance for prediction subscriber, prediction_metrics_panel.py and target_stability_panel.py.

This commit is contained in:
appleb_m committed 2026-04-22 11:35:44 +02:00
1 parent 6a6d183284
commit b9b253f6fb
3 files changed
+120 -94

No files matched your search

@@ -33,12 +33,9 @@ from PySide6.QtWidgets import (
QPushButton,
QGroupBox,
QGridLayout,
QCheckBox,
QSpinBox,
QFileDialog,
QSplitter,
QScrollArea,
QFrame,
)
from aare.common.logger_config import setup_logger
@@ -515,9 +512,14 @@ class PredictionMetricsPanel(QWidget):
super().__init__(parent)
self._last_frame_ts: float | None = None
self._frame_times: deque[float] = deque(maxlen=30)
self._history: deque[PredictionFrame] = deque(maxlen=5000)
self._history: deque[PredictionFrame] = deque(maxlen=1500)
self._paused = False
self._track_only_when_visible = True
self._input_min_interval_s = 0.20
self._last_input_ts = 0.0
self._dirty = False
self._setup_ui()
self._setup_refresh_timer()
@@ -641,6 +643,22 @@ class PredictionMetricsPanel(QWidget):
self.status_label.setText("Waiting for predictions...")
self.status_label.setStyleSheet("color: #888;")
def showEvent(self, event) -> None:
super().showEvent(event)
if self._refresh_timer is not None and not self._refresh_timer.isActive():
self._refresh_timer.start()
if self._chart_refresh_timer is not None and not self._chart_refresh_timer.isActive():
self._chart_refresh_timer.start()
self._dirty = True
def hideEvent(self, event) -> None:
super().hideEvent(event)
if self._track_only_when_visible:
if self._refresh_timer is not None:
self._refresh_timer.stop()
if self._chart_refresh_timer is not None:
self._chart_refresh_timer.stop()
@Slot(dict)
def update_from_prediction(self, payload: dict):
"""
@@ -659,7 +677,12 @@ class PredictionMetricsPanel(QWidget):
if self._paused:
return
if self._track_only_when_visible and not self.isVisible():
return
now = time.monotonic()
if (now - self._last_input_ts) < self._input_min_interval_s:
return
# Calculate frame time
frame_time_ms = 0.0
@@ -684,7 +707,6 @@ class PredictionMetricsPanel(QWidget):
cls_id = box.get("cls", box.get("class", -1))
conf = box.get("conf", box.get("confidence", 0.0))
else:
# Handle object with attributes
cls_id = getattr(box, "cls", -1)
conf = getattr(box, "conf", 0.0)
if hasattr(cls_id, "value"):
@@ -697,7 +719,6 @@ class PredictionMetricsPanel(QWidget):
mean_conf = sum(confidences) / len(confidences) if confidences else 0.0
max_conf = max(confidences) if confidences else 0.0
# Create frame record
frame = PredictionFrame(
timestamp=now,
boxes=boxes,
@@ -712,12 +733,14 @@ class PredictionMetricsPanel(QWidget):
self.rolling_chart.add_frame(frame)
self.error_tracking.update_from_frame(frame)
self._update_status()
self._dirty = True
def _refresh_display(self):
"""Refresh all display components except rolling chart."""
if not self._history:
return
if not self._dirty and self.isVisible():
return
latest = self._history[-1]
@@ -742,6 +765,9 @@ class PredictionMetricsPanel(QWidget):
mean_confidence=latest.mean_confidence,
)
self._update_status()
self._dirty = False
def _export_csv(self):
"""Export history to CSV file."""
if not self._history:
+81 -81
View File
@@ -142,10 +142,16 @@ class TargetStabilityPanel(QWidget):
self._auto_scale_enabled = True
self._active_trace_target = self.TRACE_TARGET_SIGMA
self._track_only_when_visible = True
self._metrics_update_interval_s = 0.5
self._last_metrics_update_ts = 0.0
self._stability_ref_px = 1.0
self._input_min_interval_s = 0.10
self._last_input_ts = 0.0
self._chart_dirty = False
self._samples = deque(maxlen=10000)
self._plot_points = deque(maxlen=10000)
self._sigma_samples = deque(maxlen=10000)
@@ -156,6 +162,11 @@ class TargetStabilityPanel(QWidget):
self._rolling_sum_dx2 = 0.0
self._rolling_sum_dy2 = 0.0
self._rolling_step_count = 0
self._rolling_step_dx2_sum = 0.0
self._rolling_step_dy2_sum = 0.0
self._rolling_step_xy2_sum = 0.0
self._kahan_c_dx = 0.0
self._kahan_c_dy = 0.0
self._kahan_c_dx2 = 0.0
@@ -263,45 +274,46 @@ class TargetStabilityPanel(QWidget):
self.series = QLineSeries()
self.series.setName("Rolling σ")
self.series.setPointsVisible(True)
self.series.setPointsVisible(False)
self.series.setPen(QPen(QColor(self.SIGMA_COLOR), 2.0))
self.sigma_x_series = QLineSeries()
self.sigma_x_series.setName("σx")
self.sigma_x_series.setPointsVisible(True)
self.sigma_x_series.setPointsVisible(False)
self.sigma_x_series.setPen(QPen(QColor(self.SIGMA_X_COLOR), 2.0))
self.sigma_y_series = QLineSeries()
self.sigma_y_series.setName("σy")
self.sigma_y_series.setPointsVisible(True)
self.sigma_y_series.setPointsVisible(False)
self.sigma_y_series.setPen(QPen(QColor(self.SIGMA_Y_COLOR), 2.0))
self.distance_series = QLineSeries()
self.distance_series.setName("Distance")
self.distance_series.setPointsVisible(True)
self.distance_series.setPointsVisible(False)
self.distance_series.setPen(QPen(QColor(self.DISTANCE_COLOR), 2.0))
self.dx_series = QLineSeries()
self.dx_series.setName("Distance X (dx)")
self.dx_series.setPointsVisible(True)
self.dx_series.setPointsVisible(False)
self.dx_series.setPen(QPen(QColor(self.DX_COLOR), 2.0))
self.dy_series = QLineSeries()
self.dy_series.setName("Distance Y (dy)")
self.dy_series.setPointsVisible(True)
self.dy_series.setPointsVisible(False)
self.dy_series.setPen(QPen(QColor(self.DY_COLOR), 2.0))
self.score_series = QLineSeries()
self.score_series.setName("Stability score")
self.score_series.setPointsVisible(True)
self.score_series.setPointsVisible(False)
self.score_series.setPen(QPen(QColor(self.SCORE_COLOR), 2.0))
self.step_series = QLineSeries()
self.step_series.setName("Rolling RMS step motion")
self.step_series.setPointsVisible(True)
self.step_series.setPointsVisible(False)
self.step_series.setPen(QPen(QColor(self.STEP_COLOR), 2.0))
self.chart = QChart()
self.chart.setAnimationOptions(QChart.AnimationOption.NoAnimation)
for series in (
self.series,
self.sigma_x_series,
@@ -433,7 +445,7 @@ class TargetStabilityPanel(QWidget):
self._update_metrics_label(force=True)
self._refresh_timer = QTimer(self)
self._refresh_timer.setInterval(250)
self._refresh_timer.setInterval(500)
self._refresh_timer.timeout.connect(self._refresh_chart)
self._refresh_timer.start()
@@ -593,30 +605,7 @@ class TargetStabilityPanel(QWidget):
}
def _current_step_jitter_stats(self) -> dict[str, float]:
if len(self._sigma_samples) < 2:
return {
"rms_step_dx": 0.0,
"rms_step_dy": 0.0,
"rms_step_xy": 0.0,
}
step_dx2_sum = 0.0
step_dy2_sum = 0.0
step_xy2_sum = 0.0
count = 0
prev = None
for sample in self._sigma_samples:
if prev is not None:
ddx = float(sample["dx"]) - float(prev["dx"])
ddy = float(sample["dy"]) - float(prev["dy"])
step_dx2_sum += ddx * ddx
step_dy2_sum += ddy * ddy
step_xy2_sum += ddx * ddx + ddy * ddy
count += 1
prev = sample
if count <= 0:
if self._rolling_step_count <= 0:
return {
"rms_step_dx": 0.0,
"rms_step_dy": 0.0,
@@ -624,9 +613,9 @@ class TargetStabilityPanel(QWidget):
}
return {
"rms_step_dx": math.sqrt(step_dx2_sum / count),
"rms_step_dy": math.sqrt(step_dy2_sum / count),
"rms_step_xy": math.sqrt(step_xy2_sum / count),
"rms_step_dx": math.sqrt(self._rolling_step_dx2_sum / self._rolling_step_count),
"rms_step_dy": math.sqrt(self._rolling_step_dy2_sum / self._rolling_step_count),
"rms_step_xy": math.sqrt(self._rolling_step_xy2_sum / self._rolling_step_count),
}
def _current_score_value(self) -> float:
@@ -780,11 +769,26 @@ class TargetStabilityPanel(QWidget):
self._update_status_label()
self._update_metrics_label(force=True)
def showEvent(self, event) -> None:
super().showEvent(event)
if self._refresh_timer is not None and not self._refresh_timer.isActive():
self._refresh_timer.start()
self._chart_dirty = True
def hideEvent(self, event) -> None:
super().hideEvent(event)
if self._track_only_when_visible and self._collecting_until is None and self._refresh_timer is not None:
self._refresh_timer.stop()
@Slot(dict)
def update_target_point(self, payload: dict) -> None:
if self._paused or self._beam_center is None:
return
if self._track_only_when_visible and not self.isVisible() and self._collecting_until is None:
return
raw = payload.get("target_point")
target_xy = self._coerce_target_point(raw)
if target_xy is None:
@@ -845,6 +849,7 @@ class TargetStabilityPanel(QWidget):
self._append_plot_point(sample)
def _append_plot_point(self, sample: dict) -> None:
prev_sample = self._sigma_samples[-1] if self._sigma_samples else None
self._sigma_samples.append(sample)
dx = float(sample["dx"])
@@ -852,41 +857,47 @@ class TargetStabilityPanel(QWidget):
self._rolling_count += 1
# Kahan summation for dx
y = dx - self._kahan_c_dx
t = self._rolling_sum_dx + y
self._kahan_c_dx = (t - self._rolling_sum_dx) - y
self._rolling_sum_dx = t
# Kahan summation for dy
y = dy - self._kahan_c_dy
t = self._rolling_sum_dy + y
self._kahan_c_dy = (t - self._rolling_sum_dy) - y
self._rolling_sum_dy = t
# Kahan summation for dx²
dx2 = dx * dx
y = dx2 - self._kahan_c_dx2
t = self._rolling_sum_dx2 + y
self._kahan_c_dx2 = (t - self._rolling_sum_dx2) - y
self._rolling_sum_dx2 = t
# Kahan summation for dy²
dy2 = dy * dy
y = dy2 - self._kahan_c_dy2
t = self._rolling_sum_dy2 + y
self._kahan_c_dy2 = (t - self._rolling_sum_dy2) - y
self._rolling_sum_dy2 = t
if prev_sample is not None:
ddx = dx - float(prev_sample["dx"])
ddy = dy - float(prev_sample["dy"])
self._rolling_step_count += 1
self._rolling_step_dx2_sum += ddx * ddx
self._rolling_step_dy2_sum += ddy * ddy
self._rolling_step_xy2_sum += ddx * ddx + ddy * ddy
sample["_step_from_prev"] = (ddx, ddy)
self._trim_sigma_samples(float(sample["ts"]))
sigma_stats = self._current_sigma_stats()
step_xy = self._current_step_jitter_stats()["rms_step_xy"]
if self.score_basis_combo.currentText() == self.SCORE_FROM_SIGMA_XY:
score = self._stability_score(sigma_stats["sigma"])
else:
score = self._stability_score(step_xy)
score = (
self._stability_score(float(sigma_stats["sigma"]))
if self.score_basis_combo.currentText() == self.SCORE_FROM_SIGMA_XY
else self._stability_score(step_xy)
)
self._plot_points.append({
"ts": float(sample["ts"]),
@@ -910,38 +921,46 @@ class TargetStabilityPanel(QWidget):
self._rolling_count -= 1
# Kahan subtraction for dx
y = -dx - self._kahan_c_dx
t = self._rolling_sum_dx + y
self._kahan_c_dx = (t - self._rolling_sum_dx) - y
self._rolling_sum_dx = t
# Kahan subtraction for dy
y = -dy - self._kahan_c_dy
t = self._rolling_sum_dy + y
self._kahan_c_dy = (t - self._rolling_sum_dy) - y
self._rolling_sum_dy = t
# Kahan subtraction for dx²
dx2 = dx * dx
y = -dx2 - self._kahan_c_dx2
t = self._rolling_sum_dx2 + y
self._kahan_c_dx2 = (t - self._rolling_sum_dx2) - y
self._rolling_sum_dx2 = t
# Kahan subtraction for dy²
dy2 = dy * dy
y = -dy2 - self._kahan_c_dy2
t = self._rolling_sum_dy2 + y
self._kahan_c_dy2 = (t - self._rolling_sum_dy2) - y
self._rolling_sum_dy2 = t
step = sample.get("_step_from_prev")
if step is not None and self._rolling_step_count > 0:
ddx, ddy = step
self._rolling_step_count -= 1
self._rolling_step_dx2_sum -= ddx * ddx
self._rolling_step_dy2_sum -= ddy * ddy
self._rolling_step_xy2_sum -= ddx * ddx + ddy * ddy
if self._rolling_count <= 0:
self._rolling_count = 0
self._rolling_sum_dx = 0.0
self._rolling_sum_dy = 0.0
self._rolling_sum_dx2 = 0.0
self._rolling_sum_dy2 = 0.0
self._rolling_step_count = 0
self._rolling_step_dx2_sum = 0.0
self._rolling_step_dy2_sum = 0.0
self._rolling_step_xy2_sum = 0.0
self._kahan_c_dx = 0.0
self._kahan_c_dy = 0.0
self._kahan_c_dx2 = 0.0
@@ -1180,12 +1199,15 @@ class TargetStabilityPanel(QWidget):
self._update_metrics_label()
return
if not self._chart_dirty and not self._auto_scale_enabled:
self._update_metrics_label()
return
if self._paused and self._collecting_until is None and self._frozen_plot_points is None:
self._update_status_label()
self._update_metrics_label()
return
# Use frozen data if available, otherwise use live data
if self._frozen_plot_points is not None:
plot_data = list(self._frozen_plot_points)
ref_time = plot_data[0]["ts"] if plot_data else now
@@ -1193,38 +1215,16 @@ class TargetStabilityPanel(QWidget):
plot_data = list(self._plot_points)
ref_time = now
sigma_points = [
QPointF(float(point["ts"]) - ref_time, float(point["sigma"]))
for point in plot_data
]
sigma_x_points = [
QPointF(float(point["ts"]) - ref_time, float(point["sigma_x"]))
for point in plot_data
]
sigma_y_points = [
QPointF(float(point["ts"]) - ref_time, float(point["sigma_y"]))
for point in plot_data
]
distance_points = [
QPointF(float(point["ts"]) - ref_time, float(point["distance"]))
for point in plot_data
]
dx_points = [
QPointF(float(point["ts"]) - ref_time, float(point["dx"]))
for point in plot_data
]
dy_points = [
QPointF(float(point["ts"]) - ref_time, float(point["dy"]))
for point in plot_data
]
score_points = [
QPointF(float(point["ts"]) - ref_time, float(point["score"]))
for point in plot_data
]
step_points = [
QPointF(float(point["ts"]) - ref_time, float(point["step_xy"]))
for point in plot_data
]
self._chart_dirty = False
sigma_points = [QPointF(float(point["ts"]) - ref_time, float(point["sigma"])) for point in plot_data]
sigma_x_points = [QPointF(float(point["ts"]) - ref_time, float(point["sigma_x"])) for point in plot_data]
sigma_y_points = [QPointF(float(point["ts"]) - ref_time, float(point["sigma_y"])) for point in plot_data]
distance_points = [QPointF(float(point["ts"]) - ref_time, float(point["distance"])) for point in plot_data]
dx_points = [QPointF(float(point["ts"]) - ref_time, float(point["dx"])) for point in plot_data]
dy_points = [QPointF(float(point["ts"]) - ref_time, float(point["dy"])) for point in plot_data]
score_points = [QPointF(float(point["ts"]) - ref_time, float(point["score"])) for point in plot_data]
step_points = [QPointF(float(point["ts"]) - ref_time, float(point["step_xy"])) for point in plot_data]
self.series.replace(sigma_points)
self.sigma_x_series.replace(sigma_x_points)
@@ -13,13 +13,8 @@ from aare.common.logger_config import setup_logger
logger = setup_logger("aareGUI")
class PredictionSubscriber(QThread):
# emits parsed JSON payload (dict with keys: time, frame_id, shape, boxes)
prediction = Signal(dict)
# emit parsed target-point payload
target_point = Signal(dict)
# NEW: emit the image that came with the prediction stream
image = Signal(QPixmap)
def __init__(self, pred_zmq_url: str, topic: bytes | str = b"", parent=None):
@@ -29,6 +24,8 @@ class PredictionSubscriber(QThread):
self._sock.setsockopt(zmq.RCVTIMEO, 500)
self._sock.setsockopt(zmq.LINGER, 0)
self._emit_images = True
if isinstance(topic, str):
self._sock.setsockopt_string(zmq.SUBSCRIBE, topic)
elif isinstance(topic, bytes):
@@ -39,6 +36,9 @@ class PredictionSubscriber(QThread):
self._sock.connect(pred_zmq_url)
self.running = True
def set_emit_images(self, enabled: bool) -> None:
self._emit_images = enabled
def _try_parse_json(self, part: bytes) -> dict | None:
try:
decoded = json.loads(part.decode("utf-8"))
@@ -108,7 +108,7 @@ class PredictionSubscriber(QThread):
image_bytes = max(non_json_parts, key=len) if non_json_parts else None
if header and image_bytes:
if self._emit_images and header and image_bytes:
pix = self._decode_image(header, image_bytes)
if pix is not None and self.running:
self.image.emit(pix)