GUI: visualisation of target position added to GUI
This commit is contained in:
1 parent
6e5de34d46
commit
5cbc0171f3
4 files changed
+185
-7
No files matched your search
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user