GUI: added legend to the GUI, target position display.
This commit is contained in:
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user