GUI: visualisation of target position added to GUI

This commit is contained in:
appleb_m committed 2026-04-20 16:14:18 +02:00
1 parent 6e5de34d46
commit 5cbc0171f3
4 files changed
+185 -7

No files matched your search

+4
View File
@@ -384,6 +384,9 @@ class MainWindow(QMainWindow):
self.sample_camera.load_image.connect(self.raster.load_image)
self.sample_camera.switch_raster_grid.connect(self.data_collection.switch_to_raster)
self.beamline.samcam.show_detections_changed.connect(self.sample_camera.set_show_detections)
self.beamline.samcam.show_target_point_changed.connect(self.sample_camera.set_show_target_point)
self.beamline.samcam.show_target_coordinates_changed.connect(self.sample_camera.set_show_target_coordinates)
self.beamline.samcam.target_color_changed.connect(self.sample_camera.set_target_color)
if zmq_addr is not None:
self.camera_thread = SampleCameraThread(zmq_url=zmq_addr)
@@ -403,6 +406,7 @@ class MainWindow(QMainWindow):
logger.debug(f"Starting prediction subscriber thread {pred_zmq_addr}")
self.prediction_thread = PredictionSubscriber(pred_zmq_url=pred_zmq_addr, topic="detections")
self.prediction_thread.prediction.connect(self.sample_camera.update_detections)
self.prediction_thread.target_point.connect(self.sample_camera.update_target_point)
self.prediction_thread.start()
else:
self.prediction_thread = None
+31 -1
View File
@@ -1,5 +1,5 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QDoubleSpinBox, QCheckBox, QLineEdit, \
QPushButton
QPushButton, QComboBox
from PySide6.QtCore import Signal, Slot
from aare.common.models import SampleCameraSettings, DAQStatusModel
@@ -10,6 +10,9 @@ class SamcamPanel(QWidget):
# Signals for when exposure or gain values change
changed = Signal(SampleCameraSettings)
show_detections_changed = Signal(bool)
show_target_point_changed = Signal(bool)
show_target_coordinates_changed = Signal(bool)
target_color_changed = Signal(str)
screenshot_requested = Signal(str, str)
old_settings = SampleCameraSettings(gain=100, exposure=0.001)
@@ -72,6 +75,30 @@ class SamcamPanel(QWidget):
self.show_detections_checkbox.toggled.connect(self.show_detections_changed.emit)
detections_layout.addWidget(self.show_detections_checkbox)
# Show target point checkbox
target_point_layout = QHBoxLayout()
self.show_target_point_checkbox = QCheckBox("Show target point")
self.show_target_point_checkbox.setChecked(True)
self.show_target_point_checkbox.toggled.connect(self.show_target_point_changed.emit)
target_point_layout.addWidget(self.show_target_point_checkbox)
# Show target coordinates checkbox
target_coords_layout = QHBoxLayout()
self.show_target_coordinates_checkbox = QCheckBox("Show target coordinates")
self.show_target_coordinates_checkbox.setChecked(True)
self.show_target_coordinates_checkbox.toggled.connect(self.show_target_coordinates_changed.emit)
target_coords_layout.addWidget(self.show_target_coordinates_checkbox)
# Target color
target_color_layout = QHBoxLayout()
target_color_label = QLabel("Target colour:")
self.target_color_combo = QComboBox()
self.target_color_combo.addItems(["Cyan", "Dark Blue", "Dark Red"])
self.target_color_combo.setCurrentText("Cyan")
self.target_color_combo.currentTextChanged.connect(self.target_color_changed.emit)
target_color_layout.addWidget(target_color_label)
target_color_layout.addWidget(self.target_color_combo)
# Add controls to main layout
layout.addLayout(exposure_layout)
layout.addLayout(gain_layout)
@@ -79,6 +106,9 @@ class SamcamPanel(QWidget):
layout.addLayout(screenshot_message_layout)
layout.addWidget(self.screenshot_button)
layout.addLayout(detections_layout)
layout.addLayout(target_point_layout)
layout.addLayout(target_coords_layout)
layout.addLayout(target_color_layout)
self.setLayout(layout)
def __changed(self):
@@ -16,6 +16,9 @@ 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)
@@ -25,14 +28,14 @@ class PredictionSubscriber(QThread):
self._sock = self._ctx.socket(zmq.SUB)
self._sock.setsockopt(zmq.RCVTIMEO, 500)
self._sock.setsockopt(zmq.LINGER, 0)
if isinstance(topic, str):
self._sock.setsockopt_string(zmq.SUBSCRIBE, topic)
elif isinstance(topic, bytes):
self._sock.setsockopt(zmq.SUBSCRIBE, topic)
else:
self._sock.setsockopt(zmq.SUBSCRIBE, b"")
self._sock.connect(pred_zmq_url)
self.running = True
@@ -101,6 +104,7 @@ class PredictionSubscriber(QThread):
None,
)
detections = next((d for d in json_dicts if "boxes" in d), None)
target = next((d for d in json_dicts if "target_point" in d), None)
image_bytes = max(non_json_parts, key=len) if non_json_parts else None
@@ -112,6 +116,9 @@ class PredictionSubscriber(QThread):
if detections and self.running:
self.prediction.emit(detections)
if target and self.running:
self.target_point.emit(target)
except Exception as e:
if self.running:
logger.exception(f"PredictionSubscriber error: {e}")
+141 -4
View File
@@ -1,7 +1,8 @@
import math
import time
from enum import Enum
from PySide6.QtCore import Qt, QTimer, QRect, QPoint, Signal, Slot, QPointF
from PySide6.QtCore import Qt, QTimer, QRect, QPoint, Signal, Slot, QPointF, QRectF
from PySide6.QtGui import (
QPixmap,
QPainter,
@@ -10,7 +11,7 @@ from PySide6.QtGui import (
QWheelEvent,
QTransform,
QCursor,
QLinearGradient, QFont, QFontMetrics, QPolygonF,
QLinearGradient, QFont, QFontMetrics, QPolygonF, QBrush,
)
from PySide6.QtWidgets import (
QMenu,
@@ -84,6 +85,13 @@ class SampleCameraImageLabel(QGraphicsView):
self.__bounding_box = None
self.__show_detections = True
self.__show_target_point = True
self.__show_target_coordinates = True
self.__target_point = None
self.__target_shape = None
self.__target_color_name = "Cyan"
self.__last_target_update_ts = 0.0
self.__target_min_update_interval_s = 0.25
self.start_point = None # Starting point of the rectangle
self.end_point = None # Ending point of the rectangle
@@ -164,6 +172,21 @@ class SampleCameraImageLabel(QGraphicsView):
self.__detections = []
self.update()
@Slot(dict)
def update_target_point(self, payload: dict):
now = time.monotonic()
if now - self.__last_target_update_ts < self.__target_min_update_interval_s:
return
try:
self.__target_shape = payload.get("shape", self.__det_shape)
self.__target_point = payload.get("target_point")
self.__last_target_update_ts = now
except Exception as e:
logger.debug(f"Exception in update_target_point: {e}")
self.__target_point = None
self.update()
def __draw_busy_overlay(self, painter: QPainter):
if not self.__is_daq_busy:
@@ -298,6 +321,7 @@ class SampleCameraImageLabel(QGraphicsView):
self.__draw_session_overlay(painter)
self.__draw_camera_unavailable_overlay(painter)
self.__draw_detections(painter, rect)
self.__draw_target_point(painter)
def resizeEvent(self, event):
super().resizeEvent(event)
@@ -554,8 +578,6 @@ class SampleCameraImageLabel(QGraphicsView):
elif self.__state == SampleCameraImageState.BEAM_MARKING:
self.__state = SampleCameraImageState.IDLE
def __left_single_click(self, event):
if not self.__camera_interaction_enabled():
logger.info("Ignoring click because sample camera feed is unavailable")
@@ -652,12 +674,127 @@ class SampleCameraImageLabel(QGraphicsView):
painter.setPen(QPen(QColor(255, 255, 255)))
painter.drawText(QPoint(int(x1) + 2, int(y1 - 4)), f"{label} {conf:.2f}")
def __target_color(self) -> QColor:
color_map = {
"Cyan": QColor(0, 255, 255),
"Dark Blue": QColor(0, 70, 160),
"Dark Red": QColor(140, 25, 25),
}
return color_map.get(self.__target_color_name, QColor(0, 255, 255))
def __draw_target_point(self, painter: QPainter):
if not self.__show_target_point:
return
if self.__target_point is None:
return
if self.pixmap_item is None:
return
pix = self.pixmap_item.pixmap()
if pix.isNull():
return
shape = self.__target_shape or self.__det_shape
if shape is None or len(shape) < 2:
return
try:
img_h, img_w = int(shape[0]), int(shape[1])
raw = self.__target_point
if isinstance(raw, dict):
tx = float(raw["x"])
ty = float(raw["y"])
elif isinstance(raw, (list, tuple)) and len(raw) >= 2:
tx = float(raw[0])
ty = float(raw[1])
else:
return
except Exception as e:
logger.debug(f"Error parsing target point {self.__target_point}: {e}")
return
sx = pix.width() / float(img_w)
sy = pix.height() / float(img_h)
px = tx * sx
py = ty * sy
color = self.__target_color()
glow = QColor(color)
glow.setAlpha(70)
painter.save()
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QBrush(glow))
painter.drawEllipse(QPointF(px, py), 20, 20)
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.setPen(QPen(color, 3, Qt.PenStyle.SolidLine))
painter.drawEllipse(QPointF(px, py), 12, 12)
painter.setPen(QPen(QColor(255, 255, 255), 1, Qt.PenStyle.SolidLine))
painter.drawEllipse(QPointF(px, py), 5, 5)
painter.setPen(QPen(color, 2, Qt.PenStyle.SolidLine))
painter.drawLine(QPointF(px - 20, py), QPointF(px - 7, py))
painter.drawLine(QPointF(px + 7, py), QPointF(px + 20, py))
painter.drawLine(QPointF(px, py - 20), QPointF(px, py - 7))
painter.drawLine(QPointF(px, py + 7), QPointF(px, py + 20))
label_text = "Target"
if self.__show_target_coordinates:
label_text = f"Target ({tx:.1f}, {ty:.1f})"
font = QFont()
font.setPointSize(10)
font.setBold(True)
painter.setFont(font)
fm = QFontMetrics(font)
text_rect = fm.boundingRect(label_text)
bubble_rect = QRectF(
px + 16,
py - 28,
text_rect.width() + 16,
text_rect.height() + 10
)
painter.setPen(QPen(color, 2))
painter.setBrush(QColor(20, 20, 20, 190))
painter.drawRoundedRect(bubble_rect, 8, 8)
painter.setPen(QPen(QColor(255, 255, 255), 1))
painter.drawText(
QPointF(bubble_rect.left() + 8, bubble_rect.top() + 7 + fm.ascent()),
label_text
)
painter.restore()
@Slot(bool)
def set_show_detections(self, show: bool):
"""Slot to enable/disable showing ML detections"""
self.__show_detections = show
self.update() # Trigger redraw
@Slot(bool)
def set_show_target_point(self, show: bool):
"""Slot to enable/disable showing target point"""
self.__show_target_point = show
self.update()
@Slot(bool)
def set_show_target_coordinates(self, show: bool):
self.__show_target_coordinates = show
self.update()
@Slot(str)
def set_target_color(self, color_name: str):
self.__target_color_name = color_name
self.update()
def __draw_ml_bounding_box(self, painter: QPainter):
if self.__bounding_box is None:
return