From a7b83e7a9dedc676620fb84dd365548893fd9daf Mon Sep 17 00:00:00 2001 From: appleb_m Date: Mon, 20 Apr 2026 17:26:49 +0200 Subject: [PATCH] GUI: added legend to the GUI, target position display. --- src/aare/gui/main_window.py | 39 +++++ src/aare/gui/panels/samcam_panel.py | 55 +++++++ src/aare/gui/widgets/camera_image.py | 211 ++++++++++++++++++++++----- 3 files changed, 269 insertions(+), 36 deletions(-) diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index dd40a9f3..2a930c67 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -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}") diff --git a/src/aare/gui/panels/samcam_panel.py b/src/aare/gui/panels/samcam_panel.py index 22292a67..6124d867 100644 --- a/src/aare/gui/panels/samcam_panel.py +++ b/src/aare/gui/panels/samcam_panel.py @@ -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: diff --git a/src/aare/gui/widgets/camera_image.py b/src/aare/gui/widgets/camera_image.py index 7795b9a8..2268761a 100644 --- a/src/aare/gui/widgets/camera_image.py +++ b/src/aare/gui/widgets/camera_image.py @@ -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