GUI: added legend to the GUI, target position display.

This commit is contained in:
2026-04-20 17:26:49 +02:00
parent 5cbc0171f3
commit a7b83e7a9d
3 changed files with 269 additions and 36 deletions
+39
View File
@@ -381,12 +381,16 @@ class MainWindow(QMainWindow):
self.sample_camera.update_beam_mark.connect(self.daq.beam_mark_add)
self.beamline.beam_center.beam_center.connect(self.daq.beam_center)
self.beamline.beam_size.beam_size.connect(self.daq.beam_size_mm)
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.show_overlay_legend_changed.connect(self.sample_camera.set_show_overlay_legend)
self.beamline.samcam.compact_overlay_legend_changed.connect(self.sample_camera.set_compact_overlay_legend)
self.beamline.samcam.target_color_changed.connect(self.sample_camera.set_target_color)
self._restore_samcam_overlay_settings()
if zmq_addr is not None:
self.camera_thread = SampleCameraThread(zmq_url=zmq_addr)
@@ -599,6 +603,40 @@ class MainWindow(QMainWindow):
# Initial load
QTimer.singleShot(1000, self.daq.workflow_load_queue)
def _restore_samcam_overlay_settings(self) -> None:
settings = QSettings("PSI", "AareGUI")
show_detections = settings.value("samcam/show_detections", True, type=bool)
show_target_point = settings.value("samcam/show_target_point", True, type=bool)
show_target_coordinates = settings.value("samcam/show_target_coordinates", True, type=bool)
show_overlay_legend = settings.value("samcam/show_overlay_legend", True, type=bool)
compact_overlay_legend = settings.value("samcam/compact_overlay_legend", False, type=bool)
target_color = settings.value("samcam/target_color", "Cyan", type=str)
self.beamline.samcam.apply_overlay_settings(
show_detections=show_detections,
show_target_point=show_target_point,
show_target_coordinates=show_target_coordinates,
show_overlay_legend=show_overlay_legend,
compact_overlay_legend=compact_overlay_legend,
target_color=target_color,
)
self.sample_camera.set_show_detections(show_detections)
self.sample_camera.set_show_target_point(show_target_point)
self.sample_camera.set_show_target_coordinates(show_target_coordinates)
self.sample_camera.set_show_overlay_legend(show_overlay_legend)
self.sample_camera.set_compact_overlay_legend(compact_overlay_legend)
self.sample_camera.set_target_color(target_color)
def _save_samcam_overlay_settings(self) -> None:
settings = QSettings("PSI", "AareGUI")
overlay = self.sample_camera.target_overlay_settings()
settings.setValue("samcam/show_detections", overlay["show_detections"])
settings.setValue("samcam/show_target_point", overlay["show_target_point"])
settings.setValue("samcam/show_target_coordinates", overlay["show_target_coordinates"])
settings.setValue("samcam/show_overlay_legend", overlay["show_overlay_legend"])
settings.setValue("samcam/compact_overlay_legend", overlay["compact_overlay_legend"])
settings.setValue("samcam/target_color", overlay["target_color"])
@Slot(QPixmap)
def _on_samcam_prediction_pixmap(self, pix: QPixmap) -> None:
self._last_pred_image_ts = time.monotonic()
@@ -1063,6 +1101,7 @@ class MainWindow(QMainWindow):
def closeEvent(self, event) -> None:
try:
self.state_manager.save_window(self)
self._save_samcam_overlay_settings()
except Exception as e:
logger.warning(f"Failed to save main window state: {e}")
+55
View File
@@ -12,6 +12,8 @@ class SamcamPanel(QWidget):
show_detections_changed = Signal(bool)
show_target_point_changed = Signal(bool)
show_target_coordinates_changed = Signal(bool)
show_overlay_legend_changed = Signal(bool)
compact_overlay_legend_changed = Signal(bool)
target_color_changed = Signal(str)
screenshot_requested = Signal(str, str)
old_settings = SampleCameraSettings(gain=100, exposure=0.001)
@@ -89,6 +91,20 @@ class SamcamPanel(QWidget):
self.show_target_coordinates_checkbox.toggled.connect(self.show_target_coordinates_changed.emit)
target_coords_layout.addWidget(self.show_target_coordinates_checkbox)
# Show legend checkbox
legend_layout = QHBoxLayout()
self.show_overlay_legend_checkbox = QCheckBox("Show overlay legend")
self.show_overlay_legend_checkbox.setChecked(True)
self.show_overlay_legend_checkbox.toggled.connect(self.show_overlay_legend_changed.emit)
legend_layout.addWidget(self.show_overlay_legend_checkbox)
# Compact legend checkbox
compact_legend_layout = QHBoxLayout()
self.compact_overlay_legend_checkbox = QCheckBox("Compact legend")
self.compact_overlay_legend_checkbox.setChecked(False)
self.compact_overlay_legend_checkbox.toggled.connect(self.compact_overlay_legend_changed.emit)
compact_legend_layout.addWidget(self.compact_overlay_legend_checkbox)
# Target color
target_color_layout = QHBoxLayout()
target_color_label = QLabel("Target colour:")
@@ -108,6 +124,8 @@ class SamcamPanel(QWidget):
layout.addLayout(detections_layout)
layout.addLayout(target_point_layout)
layout.addLayout(target_coords_layout)
layout.addLayout(legend_layout)
layout.addLayout(compact_legend_layout)
layout.addLayout(target_color_layout)
self.setLayout(layout)
@@ -121,6 +139,43 @@ class SamcamPanel(QWidget):
self.screenshot_message_edit.text(),
)
def apply_overlay_settings(
self,
*,
show_detections: bool,
show_target_point: bool,
show_target_coordinates: bool,
show_overlay_legend: bool,
compact_overlay_legend: bool,
target_color: str,
) -> None:
for widget in (
self.show_detections_checkbox,
self.show_target_point_checkbox,
self.show_target_coordinates_checkbox,
self.show_overlay_legend_checkbox,
self.compact_overlay_legend_checkbox,
self.target_color_combo,
):
widget.blockSignals(True)
self.show_detections_checkbox.setChecked(show_detections)
self.show_target_point_checkbox.setChecked(show_target_point)
self.show_target_coordinates_checkbox.setChecked(show_target_coordinates)
self.show_overlay_legend_checkbox.setChecked(show_overlay_legend)
self.compact_overlay_legend_checkbox.setChecked(compact_overlay_legend)
self.target_color_combo.setCurrentText(target_color)
for widget in (
self.show_detections_checkbox,
self.show_target_point_checkbox,
self.show_target_coordinates_checkbox,
self.show_overlay_legend_checkbox,
self.compact_overlay_legend_checkbox,
self.target_color_combo,
):
widget.blockSignals(False)
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
if self.old_settings != s.bl.sample_camera:
+175 -36
View File
@@ -87,11 +87,15 @@ class SampleCameraImageLabel(QGraphicsView):
self.__show_target_point = True
self.__show_target_coordinates = True
self.__show_overlay_legend = True
self.__compact_overlay_legend = False
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.__target_min_update_interval_s = 0.03
self.__target_smoothing_alpha = 0.45
self.__smoothed_target_point: tuple[float, float] | None = None
self.start_point = None # Starting point of the rectangle
self.end_point = None # Ending point of the rectangle
@@ -161,12 +165,9 @@ class SampleCameraImageLabel(QGraphicsView):
@Slot(dict)
def update_detections(self, payload: dict):
# payload: { 'time', 'frame_id', 'shape':[h,w], 'boxes':[{'x1',...,'label','conf'}] }
#print(f"DEBUG: Full payload received: {payload}")
try:
self.__det_shape = payload.get('shape', None)
self.__detections = payload.get('boxes', []) or []
except Exception as e:
logger.debug(f"Exception in update_detections: {e}")
self.__detections = []
@@ -180,24 +181,39 @@ class SampleCameraImageLabel(QGraphicsView):
try:
self.__target_shape = payload.get("shape", self.__det_shape)
self.__target_point = payload.get("target_point")
raw_target = payload.get("target_point")
target_xy = self.__coerce_target_point(raw_target)
if target_xy is None:
return
self.__target_point = raw_target
if self.__smoothed_target_point is None:
self.__smoothed_target_point = target_xy
else:
px, py = self.__smoothed_target_point
nx, ny = target_xy
alpha = self.__target_smoothing_alpha
self.__smoothed_target_point = (
(1.0 - alpha) * px + alpha * nx,
(1.0 - alpha) * py + alpha * ny,
)
self.__last_target_update_ts = now
except Exception as e:
logger.debug(f"Exception in update_target_point: {e}")
self.__target_point = None
self.__smoothed_target_point = None
self.update()
def __draw_busy_overlay(self, painter: QPainter):
if not self.__is_daq_busy:
return
painter.save()
painter.resetTransform()
font = QFont()
font.setPointSize(24) # Fixed size in points
font.setPointSize(24)
font.setBold(True)
painter.setFont(font)
@@ -209,8 +225,8 @@ class SampleCameraImageLabel(QGraphicsView):
font_metrics = QFontMetrics(font)
text_rect = font_metrics.boundingRect(text)
position_x = 50 # 50 pixels from left edge
position_y = 50 # 50 pixels from top edge
position_x = 50
position_y = 50
padding = 20
bg_rect = QRect(
@@ -223,10 +239,7 @@ class SampleCameraImageLabel(QGraphicsView):
painter.drawRoundedRect(bg_rect, 10, 10)
painter.setPen(QPen(QColor(255, 255, 255), 2, Qt.PenStyle.SolidLine))
text_pos = QPoint(
position_x,
position_y + font_metrics.ascent()
)
text_pos = QPoint(position_x, position_y + font_metrics.ascent())
painter.drawText(text_pos, text)
painter.restore()
@@ -322,6 +335,7 @@ class SampleCameraImageLabel(QGraphicsView):
self.__draw_camera_unavailable_overlay(painter)
self.__draw_detections(painter, rect)
self.__draw_target_point(painter)
self.__draw_overlay_legend(painter)
def resizeEvent(self, event):
super().resizeEvent(event)
@@ -616,19 +630,15 @@ class SampleCameraImageLabel(QGraphicsView):
if pix.isNull():
return
# size on disk / image from payload
det_shape = self.__det_shape # [h, w]
det_shape = self.__det_shape
if det_shape is None:
return
img_h, img_w = det_shape[0], det_shape[1]
# displayed pixmap size (in pixels)
disp_w = pix.width()
disp_h = pix.height()
# scale factors from model/image -> displayed pixmap coordinates
sx = disp_w / float(img_w)
sy = disp_h / float(img_h)
# color mapping requested
color_map = {
'pin': QColor('red'),
'loop_all': QColor('green'),
@@ -653,7 +663,6 @@ class SampleCameraImageLabel(QGraphicsView):
color = color_map.get(label, QColor('magenta'))
# Draw only the rectangle outline (no fill)
pen = QPen(color, 3)
painter.setPen(pen)
painter.setBrush(Qt.BrushStyle.NoBrush)
@@ -666,7 +675,6 @@ class SampleCameraImageLabel(QGraphicsView):
])
painter.drawPolygon(polygon)
# Draw label text with white text on colored background
painter.setPen(QPen(QColor(255, 255, 255), 1))
painter.setBrush(color)
text_bg_rect = QRect(int(x1), int(y1 - 16), int(8 + 7 * len(label)), 16)
@@ -682,10 +690,20 @@ class SampleCameraImageLabel(QGraphicsView):
}
return color_map.get(self.__target_color_name, QColor(0, 255, 255))
def __coerce_target_point(self, raw) -> tuple[float, float] | None:
try:
if isinstance(raw, dict):
return float(raw["x"]), float(raw["y"])
if isinstance(raw, (list, tuple)) and len(raw) >= 2:
return float(raw[0]), float(raw[1])
except Exception as e:
logger.debug(f"Error parsing target point {raw}: {e}")
return None
def __draw_target_point(self, painter: QPainter):
if not self.__show_target_point:
return
if self.__target_point is None:
if self.__smoothed_target_point is None:
return
if self.pixmap_item is None:
return
@@ -700,18 +718,9 @@ class SampleCameraImageLabel(QGraphicsView):
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
tx, ty = self.__smoothed_target_point
except Exception as e:
logger.debug(f"Error parsing target point {self.__target_point}: {e}")
logger.debug(f"Error using smoothed target point {self.__smoothed_target_point}: {e}")
return
sx = pix.width() / float(img_w)
@@ -773,15 +782,125 @@ class SampleCameraImageLabel(QGraphicsView):
painter.restore()
def __legend_should_show(self) -> bool:
return self.__show_overlay_legend and (
self.__show_target_point
or self.__show_detections
or self.__show_coords
or self.__state == SampleCameraImageState.BEAM_MARKING
)
def __legend_lines(self) -> list[tuple[str, QColor | None]]:
lines: list[tuple[str, QColor | None]] = []
if self.__compact_overlay_legend:
if self.__show_target_point:
target_label = "Target"
if self.__show_target_coordinates:
target_label += " + coords"
lines.append((target_label, self.__target_color()))
if self.__show_detections:
lines.extend([
("Pin", QColor("red")),
("Loop", QColor("green")),
("Face", QColor("yellow")),
("Crystal", QColor("blue")),
])
if self.__show_coords:
lines.append(("Coords tooltip", QColor(230, 230, 230)))
lines.append(("Beam marker: shutter open", QColor(0, 255, 0)))
lines.append(("Beam marker: idle", QColor(245, 121, 0)))
lines.append(("Beam marker: busy", QColor(255, 0, 0)))
return lines
if self.__show_target_point:
target_label = f"Target marker ({self.__target_color_name})"
if self.__show_target_coordinates:
target_label += " + coords"
lines.append((target_label, self.__target_color()))
if self.__show_detections:
lines.extend([
("Prediction: Pin", QColor("red")),
("Prediction: Loop_all", QColor("green")),
("Prediction: Loop_face", QColor("yellow")),
("Prediction: Crystal", QColor("blue")),
("Prediction: Needle", QColor("magenta")),
("Prediction: Ice", QColor("cyan")),
])
if self.__show_coords:
lines.append(("Cursor tooltip: pixel coordinates", QColor(230, 230, 230)))
lines.append(("Beam marker: shutter open", QColor(0, 255, 0)))
lines.append(("Beam marker: idle", QColor(245, 121, 0)))
lines.append(("Beam marker: busy", QColor(255, 0, 0)))
lines.append(("Beam marker: marking mode", QColor(102, 51, 153)))
return lines
def __draw_overlay_legend(self, painter: QPainter):
if not self.__legend_should_show():
return
painter.save()
painter.resetTransform()
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
font = QFont()
font.setPointSize(8 if self.__compact_overlay_legend else 9)
painter.setFont(font)
fm = QFontMetrics(font)
lines = self.__legend_lines()
line_height = max(14 if self.__compact_overlay_legend else 16, fm.height() + 2)
swatch_size = 8 if self.__compact_overlay_legend else 10
text_padding = 6 if self.__compact_overlay_legend else 8
section_padding = 8 if self.__compact_overlay_legend else 10
left = 18
top = self.viewport().height() - (len(lines) * line_height + 24)
max_text_width = 0
for text, _color in lines:
max_text_width = max(max_text_width, fm.horizontalAdvance(text))
width = swatch_size + text_padding + max_text_width + section_padding * 2
height = len(lines) * line_height + 16
bg_rect = QRectF(left, max(18, top), width, height)
painter.setPen(QPen(QColor(255, 255, 255, 60), 1))
painter.setBrush(QColor(20, 20, 20, 170))
painter.drawRoundedRect(bg_rect, 8, 8)
y = bg_rect.top() + 12
for text, color in lines:
if color is not None:
painter.setPen(QPen(color, 2))
painter.setBrush(color)
painter.drawRect(QRectF(bg_rect.left() + section_padding, y + 2, swatch_size, swatch_size))
else:
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.setPen(QPen(QColor(240, 240, 240), 1))
painter.drawText(
QPointF(bg_rect.left() + section_padding + swatch_size + text_padding, y + fm.ascent() + 1),
text,
)
y += line_height
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
self.update()
@Slot(bool)
def set_show_target_point(self, show: bool):
"""Slot to enable/disable showing target point"""
self.__show_target_point = show
self.update()
@@ -790,11 +909,31 @@ class SampleCameraImageLabel(QGraphicsView):
self.__show_target_coordinates = show
self.update()
@Slot(bool)
def set_show_overlay_legend(self, show: bool):
self.__show_overlay_legend = show
self.update()
@Slot(bool)
def set_compact_overlay_legend(self, compact: bool):
self.__compact_overlay_legend = compact
self.update()
@Slot(str)
def set_target_color(self, color_name: str):
self.__target_color_name = color_name
self.update()
def target_overlay_settings(self) -> dict:
return {
"show_target_point": self.__show_target_point,
"show_target_coordinates": self.__show_target_coordinates,
"show_detections": self.__show_detections,
"show_overlay_legend": self.__show_overlay_legend,
"compact_overlay_legend": self.__compact_overlay_legend,
"target_color": self.__target_color_name,
}
def __draw_ml_bounding_box(self, painter: QPainter):
if self.__bounding_box is None:
return