diff --git a/src/aare/gui/panels/prediction_metrics_panel.py b/src/aare/gui/panels/prediction_metrics_panel.py index 4df322e5..895bb07e 100644 --- a/src/aare/gui/panels/prediction_metrics_panel.py +++ b/src/aare/gui/panels/prediction_metrics_panel.py @@ -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: diff --git a/src/aare/gui/panels/target_stability_panel.py b/src/aare/gui/panels/target_stability_panel.py index e65fc739..51346a28 100644 --- a/src/aare/gui/panels/target_stability_panel.py +++ b/src/aare/gui/panels/target_stability_panel.py @@ -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) diff --git a/src/aare/gui/threads/prediction_subscriber.py b/src/aare/gui/threads/prediction_subscriber.py index acb1f45f..92667958 100644 --- a/src/aare/gui/threads/prediction_subscriber.py +++ b/src/aare/gui/threads/prediction_subscriber.py @@ -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)