GUI: improved performance for prediction subscriber, prediction_metrics_panel.py and target_stability_panel.py.
This commit is contained in:
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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user