diff --git a/src/aare/common/autofocus_tools.py b/src/aare/common/autofocus_tools.py new file mode 100644 index 00000000..66747022 --- /dev/null +++ b/src/aare/common/autofocus_tools.py @@ -0,0 +1,59 @@ +import cv2 +import numpy as np + +def focus_measure_edges(gray: np.ndarray, mask: np.ndarray | None = None) -> float: + # mild denoise (optional but usually stabilizes the curve) + gray = cv2.GaussianBlur(gray, (3, 3), 0) + + gx = cv2.Scharr(gray, cv2.CV_64F, 1, 0) + gy = cv2.Scharr(gray, cv2.CV_64F, 0, 1) + g2 = gx * gx + gy * gy + + roi = g2[mask] if mask is not None else g2.reshape(-1) + if roi.size == 0: + return 0.0 + + # threshold relative to median -> knocks out noise floor + t = float(np.median(roi) * 3.0) + strong = roi[roi > t] + + if strong.size == 0: + return 0.0 + + print(f"mask pixels: {mask.sum()}, " + f"focus={strong.mean():.2f}" + f"strong_size={strong.size}") + + return float(strong.mean()) # higher = sharper + +def focus_measure_blob_size(gray: np.ndarray, mask: np.ndarray | None = None) -> float: + """ + Measures sharpness for a single bright blob. + Higher = sharper (smaller blob). + """ + g = gray.astype(np.float64) + + if mask is not None: + g = np.where(mask, g, 0.0) + + # Background subtraction is crucial for blob metrics + # Use a large-ish blur as background estimate (tune ksize to your scale) + bg = cv2.GaussianBlur(g, (0, 0), sigmaX=10.0, sigmaY=10.0) + s = g - bg + s[s < 0] = 0.0 + + total = float(s.sum()) + if total <= 0: + return 0.0 + + h, w = s.shape + y, x = np.mgrid[0:h, 0:w] + + cx = float((s * x).sum() / total) + cy = float((s * y).sum() / total) + + # intensity-weighted second central moment (variance) + var = float((s * ((x - cx) ** 2 + (y - cy) ** 2)).sum() / total) + + # smaller var => sharper, so invert + return float(1.0 / (var + 1e-9)) \ No newline at end of file diff --git a/src/aare/common/diffraction_geometry.py b/src/aare/common/diffraction_geometry.py index d91e9846..92f29a97 100644 --- a/src/aare/common/diffraction_geometry.py +++ b/src/aare/common/diffraction_geometry.py @@ -36,7 +36,7 @@ class DiffractionGeometry(BaseModel): def resolution_angstrom(self, exp_dtz_mm: float) -> float: if exp_dtz_mm <= 0: - raise ValueError("Detector distance must be positive") + raise ValueError(f"Detector distance must be positive {exp_dtz_mm}") theta = math.atan(self.detector_radius_mm / exp_dtz_mm)*0.5 return self.wavelength_angstrom / (2 * math.sin(theta)) diff --git a/src/aare/common/models.py b/src/aare/common/models.py index 67a5273e..da028d0a 100644 --- a/src/aare/common/models.py +++ b/src/aare/common/models.py @@ -673,8 +673,8 @@ def zoom_manager(mode: ZoomModeEnum = ZoomModeEnum.User, beamline: MXBeamline = class AutofocusSettings(BaseModel): - center_x_pxl: float - center_y_pxl: float + center_x_pxl: float | None # Use beam center + center_y_pxl: float | None # Use beam center radius_pxl: float z_range_um: float z_steps: int @@ -682,7 +682,8 @@ class AutofocusSettings(BaseModel): class BeamlineStatus(BaseModel): name: str ring_current_mA: float - light: Annotated[float, Field(ge=0.0, le=100.0)] + front_light: Annotated[float, Field(ge=0.0, le=100.0)] + back_light: Annotated[float, Field(ge=0.0, le=100.0)] cryojet_K: float shutter_open: bool exp_shutter_open: bool | None @@ -729,7 +730,7 @@ class BeamlineSettingsModel(BaseModel): camera_max_magnification: float | None = 1.0 camera_min_magnification: float | None = 500.0 camera_translation_factor_a: float | None = 0.00253 - camera_translation_factor_b: float | None = 255.0 + camera_translation_factor_b: float | None = 512.0 class CryojetSettingsModel(BaseModel): cryojet_park_position: float | None = 12.0 diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index 462abcb7..61fcae0d 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -13,7 +13,7 @@ import numpy as np import aare.common.face_detection as fd from aare.daq import workflows from aare.daq.aaredb import AareWrapper - +from aare.common.autofocus_tools import focus_measure_edges from aare.daq.config import BeamlineConfig from aare.daq.config import BeamlineStateEnum from aare.daq.devices import BeamlineDevices @@ -25,7 +25,7 @@ from aare.common.logger_config import setup_logger from aare.common.models import ( SampleShortInfo, PuckLoadedInfo, - SampleShortInfoList, + SampleShortInfoList, AutofocusSettings, DAQStatusModel, BeamlineStatus, SessionStatus, SampleCameraSettings, ZoomModeEnum, SimpleScanParameters, MLBoxModel, FluorescenceSpectrumParameterModel, FluorescenceSpectrumOutputModel) @@ -198,6 +198,7 @@ class AareDAQ: def __omega(self, val: float): self.__saved_box = None + print(f"Set omega to {val}") if -2000 < val < 2000: try: self.__devs.aerotech_omega = val @@ -234,12 +235,9 @@ class AareDAQ: self.__cfg.zoom_mode = ZoomModeEnum.BeamLocation else: self.__cfg.zoom_mode = ZoomModeEnum.User - zoom_settings = self.__cfg.zoom_settings.get_camera_settings(val) - - self.samcam_settings = zoom_settings @property - def light(self) -> float: + def front_light(self) -> float: val = self.__devs.lamp_light if val <= 1.0: return 0 @@ -248,12 +246,28 @@ class AareDAQ: else: return (val - 1.0) / 1.5 * 100.0 - @light.setter - def light(self, f: float): + @front_light.setter + def front_light(self, f: float): conv = (f / 100.0 * 1.5) + 1.0 print(f"Light {f} -> {conv}") self.__devs.lamp_light = conv + @property + def back_light(self) -> float: + val = self.__devs.back_light + if val <= 0.0: + return 0.0 + elif val <= 3.0: + return val / 3.0 * 100.0 + else: + return 100.0 + + @back_light.setter + def back_light(self, f: float): + conv = f / 100.0 * 3.0 + print(f"Backlight {f} -> {conv}") + self.__devs.back_light = conv + @property def sample(self) -> SampleShortInfo | None: if self.__cfg.current_sample is not None and self.__cfg.current_sample.location is None: @@ -338,6 +352,158 @@ class AareDAQ: def list_loaded_pucks(self) -> List[PuckLoadedInfo]: return [] + def __auto_focus(self, settings: AutofocusSettings, settle_time_s: float = 1.0) -> float: + """ + Scan smargon Z and find the position with maximum focus measure. + + Args: + settings: AutofocusSettings with center, radius, range, and steps + settle_time_s: Time to wait after each move before capturing image + + Returns: + Best Z position (mm) found during the scan + """ + geom = self.sample_geometry + current_smargon = self.__devs.smargon_pos + + # Get center for the mask (use beam center if not specified) + center_x = geom.beam_location_pxl.x + center_y = geom.beam_location_pxl.y + radius_pxl = 30 + + # Convert z_range from um to mm + z_range_mm = settings.z_range_um / 1000.0 + n_steps = settings.z_steps + + # Starting Z position (current sh_mm) + z_start = 0.0 + z_min = z_start - z_range_mm / 2.0 + z_max = z_start + z_range_mm / 2.0 + z_step = z_range_mm / (n_steps - 1) if n_steps > 1 else 0.0 + + # Pre-compute mask (will be created on first image) + focus_mask: np.ndarray | None = None + + # Collect focus measures at each Z position + z_positions: list[float] = [] + focus_values: list[float] = [] + + print(f"Starting autofocus: z_range={z_range_mm * 1000:.1f}um, steps={n_steps}, " + f"center=({center_x:.1f}, {center_y:.1f}), radius={radius_pxl:.1f}px") + + + for i in range(n_steps): + z_pos = z_min + i * z_step + + # Move to position + target = SmargonCoordinate( + chi_deg=current_smargon.chi_deg, + phi_deg=current_smargon.phi_deg, + sh_mm=geom.beamline_to_smargon(Coordinate(z=z_pos)) + ) + self.__devs.smargon_pos = target + self.__devs.smargon_wait(timeout=30) + + # Wait for mechanical settling and image stabilization + time.sleep(settle_time_s) + + # Capture image + gray = self.camera_image_gray + + if gray is None: + logger.warning(f"Failed to get image at z={z_pos:.4f}") + continue + + # Create mask on first valid image + if focus_mask is None or focus_mask.shape != gray.shape: + h, w = gray.shape + y, x = np.ogrid[:h, :w] + focus_mask = (x - center_x) ** 2 + (y - center_y) ** 2 <= radius_pxl ** 2 + + print(focus_mask.shape) + print(gray.shape) + + # Calculate focus measure + fm = focus_measure_edges(gray, focus_mask) + + z_positions.append(z_pos) + focus_values.append(fm) + logger.debug(f"Autofocus step {i + 1}/{n_steps}: z={z_pos:.4f}mm, focus={fm:.2f}") + print(f"Autofocus step {i + 1}/{n_steps}: z={z_pos:.4f}mm, focus={fm:.2f}") + if len(z_positions) < 3: + logger.error("Autofocus failed: not enough valid measurements") + # Return to original position + self.__devs.smargon_pos = current_smargon + self.__devs.smargon_wait(timeout=30) + return z_start + + # Find best position - use parabolic fit around the peak for sub-step precision + z_arr = np.array(z_positions) + fm_arr = np.array(focus_values) + + # Find index of maximum + peak_idx = int(np.argmax(fm_arr)) + + # Try parabolic fit if peak is not at the edge + if 0 < peak_idx < len(fm_arr) - 1: + # Fit parabola to 3 points around peak: f(z) = a*z^2 + b*z + c + z_fit = z_arr[peak_idx - 1: peak_idx + 2] + fm_fit = fm_arr[peak_idx - 1: peak_idx + 2] + try: + coeffs = np.polyfit(z_fit, fm_fit, 2) + a, b, c = coeffs + if a < 0: # Parabola opens downward (valid peak) + best_z = -b / (2 * a) + # Sanity check: best_z should be within the fitted range + if z_fit[0] <= best_z <= z_fit[2]: + logger.info(f"Autofocus: parabolic fit found peak at z={best_z:.4f}mm") + else: + best_z = z_arr[peak_idx] + logger.info(f"Autofocus: parabolic fit out of range, using sample peak z={best_z:.4f}mm") + else: + best_z = z_arr[peak_idx] + logger.info(f"Autofocus: invalid parabola, using sample peak z={best_z:.4f}mm") + except Exception as e: + logger.warning(f"Parabolic fit failed: {e}, using sample peak") + best_z = z_arr[peak_idx] + else: + best_z = z_arr[peak_idx] + logger.warning(f"Autofocus: peak at edge of scan range, z={best_z:.4f}mm") + + # Move to best position + best_target = SmargonCoordinate( + chi_deg=current_smargon.chi_deg, + phi_deg=current_smargon.phi_deg, + sh_mm=geom.beamline_to_smargon(Coordinate(z=best_z)), + ) + self.__devs.smargon_pos = best_target + self.__devs.smargon_wait(timeout=30) + + logger.warning(f"Autofocus complete: best_z={best_z:.4f}mm, " + f"focus_range=[{min(fm_arr):.2f}, {max(fm_arr):.2f}]") + + return best_z + + def auto_focus(self, settings: AutofocusSettings) -> float: + """ + Public autofocus method. Only allowed in SampleAlignment state. + + Args: + settings: AutofocusSettings with center, radius, range, and steps + + Returns: + Best Z position (mm) found during the scan + """ + self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment) + try: + best_z = self.__auto_focus(settings) + self.__cfg.state_busy = False + return best_z + except Exception as e: + logger.error(f"Autofocus failed: {e}") + self.__cfg.state_busy = False + raise + def __auto_center(self, grid: RasterGridRequest) -> CompletedRasterGrid | None: sample = self.sample @@ -507,15 +673,15 @@ class AareDAQ: def sample_geometry(self) -> SampleGeometryModel: zoom = self.__devs.zoom aerotech_pos_ref = self.__cfg.abr_meas_pos - aerotech_coord = self.__devs.aerotech_pos - aerotech_pos_ref + aerotech_pos = self.__devs.aerotech_pos sample_geom = SampleGeometryModel( beam_location_pxl=self.__cfg.beam_mark_coeff.apply(zoom), pixel_in_mm=self.__cfg.pixel_to_mm(zoom), omega_deg=self.__devs.aerotech_omega, smargon=self.__devs.smargon_pos, beam_size_mm=self.__cfg.beam_size_mm, - aerotech=aerotech_coord, - aerotech_meas=self.__devs.aerotech_pos + aerotech=aerotech_pos - aerotech_pos_ref, + aerotech_meas=aerotech_pos ) return sample_geom @@ -1209,7 +1375,8 @@ class AareDAQ: def beamline_status(self) -> BeamlineStatus: return BeamlineStatus( ring_current_mA=self.__devs.ring_current, - light=self.light, + front_light=self.front_light, + back_light=self.back_light, cryojet_K=self.__devs.cryojet_temp, shutter_open=self.__devs.shutter, exp_shutter_open=self.__devs.shutter, @@ -1219,8 +1386,8 @@ class AareDAQ: transmission=self.__devs.transmission, zoom=self.__devs.zoom, commissioning_mode=self.__cfg.commissioning_mode, - dtz_min=self.__devs.dtz_low, - dtz_max=self.__devs.dtz_high, + dtz_min=20, + dtz_max=1000, ) @property diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index aea88904..f482271b 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -102,13 +102,19 @@ async def omega(val: float, token: str = Depends(oauth2_scheme)): daq.omega_rel(val) return "OK" -@app.put("/beamline/light") -async def light(val: float, token: str = Depends(oauth2_scheme)): +@app.put("/beamline/front_light") +async def front_light(val: float, token: str = Depends(oauth2_scheme)): logger.debug(f"Setting light to {val}") auth.check_jwt_rw(cfg, auth.parse_token(token)) - daq.light = val + daq.front_light = val return "OK" +@app.put("/beamline/back_light") +async def back_light(val: float, token: str = Depends(oauth2_scheme)): + logger.debug(f"Setting back light to {val}") + auth.check_jwt_rw(cfg, auth.parse_token(token)) + daq.back_light = val + return "OK" @app.put("/beamline/zoom") async def zoom(val: float, token: str = Depends(oauth2_scheme)): @@ -195,7 +201,7 @@ async def samcam_settings(s: SampleCameraSettings, token: str = Depends(oauth2_s async def samcam_autofocus(s: AutofocusSettings, token: str = Depends(oauth2_scheme)): logger.debug(f"SamCam AutoFocus") auth.check_jwt_rw(cfg, auth.parse_token(token)) - daq.autofocus(s) + daq.auto_focus(s) return "OK" @app.post("/beamline/shutter") diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index 9b6cb104..6c4f576f 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -28,11 +28,13 @@ def auth(base_url: str | None) -> str: "Content-Type": "application/x-www-form-urlencoded" } ) - if response.status_code == 200: response_json = response.json() if "access_token" in response_json: return response_json["access_token"] + else: + logger.error(f"Authentication request failed: {response.content}") + raise Exception("Authentication request failed") except requests.RequestException as e: logger.error(f"Authentication request failed: {e}") diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index 53919128..4a044cd8 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -28,7 +28,8 @@ from aare.gui.scan_logic.raster_grid_manager import RasterGridManager from aare.gui.scan_logic.rotation_scan_manager import RotationScanManager from aare.gui.scan_logic.sample_mount_logic import SampleMountLogic from aare.gui.threads.axis_video_thread import VideoThread -from aare.gui.threads.camera_thread import SampleCameraThread, PredictionSubscriber +from aare.gui.threads.camera_thread import SampleCameraThread +from aare.gui.threads.prediction_subscriber import PredictionSubscriber from aare.gui.threads.daq_worker import DAQWorker from aare.gui.threads.jfjoch_viewer import JFJochDBusClient from aare.gui.widgets.camera_image import SampleCameraImageLabel @@ -232,7 +233,9 @@ class MainWindow(QMainWindow): self.beamline.omega_panel.set_omega.connect(self.daq.set_omega) self.sample_camera.set_omega.connect(self.daq.set_omega) self.beamline.zoom_panel.zoom.connect(self.daq.zoom) - self.beamline.illumination_panel.light.connect(self.daq.light) + self.beamline.illumination_panel.front_light.connect(self.daq.front_light) + self.beamline.illumination_panel.back_light.connect(self.daq.back_light) + if self.__decoded_token.staff: self.beamline.abr_tweak.abr_tweak.connect(self.daq.abr_tweak) self.beamline.abr_tweak.abr_save.connect(self.daq.abr_save) @@ -250,6 +253,7 @@ class MainWindow(QMainWindow): self.camera_thread = SampleCameraThread(zmq_url=zmq_addr) self.camera_thread.camera_image.connect(self.sample_camera.update_pixmap) self.camera_thread.start() + self.camera_thread.focus_measure.connect(self.status_bar.update_sharpness) else: self.camera_thread = None @@ -336,6 +340,7 @@ class MainWindow(QMainWindow): self.daq.update.connect(self.sample_camera.update_daq_status) self.daq.update.connect(self.tell_samples.update_daq_status) self.daq.update.connect(self.ref_tools_panel.update_daq_status) + self.daq.update.connect(self.camera_thread.update_daq_status) if self.__decoded_token.staff: self.daq.update.connect(self.beamline.beam_size.update_daq_status) diff --git a/src/aare/gui/panels/illumination_panel.py b/src/aare/gui/panels/illumination_panel.py index 2fb84eee..376b2c94 100644 --- a/src/aare/gui/panels/illumination_panel.py +++ b/src/aare/gui/panels/illumination_panel.py @@ -1,38 +1,57 @@ from PySide6.QtCore import Qt, Signal, Slot -from PySide6.QtWidgets import QWidget, QGridLayout, QSlider +from PySide6.QtWidgets import QWidget, QGridLayout, QSlider, QLabel from aare.common.models import DAQStatusModel from aare.gui.widgets.title_label import TitleLabel class IlluminationPanel(QWidget): - light = Signal(int) + front_light = Signal(int) + back_light = Signal(int) def __init__(self, parent=None): super().__init__(parent) grid_layout = QGridLayout(self) - grid_layout.addWidget(TitleLabel("Front light", self), 0, 0, 1, 2) + grid_layout.addWidget(TitleLabel("Light", self), 0, 0, 1, 2) + front_label = QLabel("Front light", parent=self) + front_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + grid_layout.addWidget(front_label, 1, 0, 1, 2) self.is_sliding = False - self.slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self) - self.slider.setRange(0, 100) - self.slider.sliderPressed.connect(self.on_slider_pressed) - self.slider.sliderReleased.connect(self.on_slider_released) + self.front_light_slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self) + self.front_light_slider.setRange(0, 100) + self.front_light_slider.sliderPressed.connect(self.on_slider_pressed) + self.front_light_slider.sliderReleased.connect(self.on_front_slider_released) + grid_layout.addWidget(self.front_light_slider, 2, 0, 1, 2) - grid_layout.addWidget(self.slider, 1, 0, 1, 2) + back_label = QLabel("Back light", parent=self) + back_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + grid_layout.addWidget(back_label, 3, 0, 1, 2) + + self.back_light_slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self) + self.back_light_slider.setRange(0, 100) + self.back_light_slider.sliderPressed.connect(self.on_slider_pressed) + self.back_light_slider.sliderReleased.connect(self.on_back_slider_released) + grid_layout.addWidget(self.back_light_slider, 4, 0, 1, 2) @Slot() def on_slider_pressed(self): self.is_sliding = True @Slot() - def on_slider_released(self): + def on_front_slider_released(self): self.is_sliding = False - self.light.emit(self.slider.value()) + self.front_light.emit(self.front_light_slider.value()) + + @Slot() + def on_back_slider_released(self): + self.is_sliding = False + self.back_light.emit(self.back_light_slider.value()) @Slot(DAQStatusModel) def update_daq_status(self, s: DAQStatusModel): if not self.is_sliding: # Update only if not sliding - self.slider.setValue(round(s.bl.light)) + self.front_light_slider.setValue(round(s.bl.front_light)) + self.back_light_slider.setValue(round(s.bl.back_light)) diff --git a/src/aare/gui/threads/camera_thread.py b/src/aare/gui/threads/camera_thread.py index 6ac9837d..15214f48 100644 --- a/src/aare/gui/threads/camera_thread.py +++ b/src/aare/gui/threads/camera_thread.py @@ -3,13 +3,16 @@ import json import cv2 import numpy as np import zmq -from PySide6.QtCore import QThread, Signal +from PySide6.QtCore import QThread, Signal, Slot from PySide6.QtGui import QImage, QPixmap +from aare.common.autofocus_tools import focus_measure_edges +from aare.common.models import DAQStatusModel class SampleCameraThread(QThread): # Define a signal to communicate messages from the thread to the main GUI camera_image = Signal(QPixmap) + focus_measure = Signal(float) # Emits Laplacian variance (higher = sharper) def __init__(self, zmq_url: str, parent=None): super().__init__(parent) @@ -20,6 +23,25 @@ class SampleCameraThread(QThread): self.__socket.connect(zmq_url) self.running = True + self.__measure_focus = True + self.__focus_mask = None + self.__beam_x = 0 + self.__beam_y = 0 + self.__last_beam_pos = None + self.__radius = 40 + + @Slot(DAQStatusModel) + def update_daq_status(self, s: DAQStatusModel): + self.__beam_x = s.geom.beam_location_pxl.x + self.__beam_y = s.geom.beam_location_pxl.y + if (self.__beam_x, self.__beam_y) != self.__last_beam_pos: + self.__focus_mask = None # Invalidate cache + self.__last_beam_pos = (self.__beam_x, self.__beam_y) + + @Slot(bool) + def enable_focus_measurement(self, enabled: bool = True): + """Enable or disable focus measurement.""" + self.__measure_focus = enabled def run(self): while self.running: @@ -33,11 +55,24 @@ class SampleCameraThread(QThread): if header["type"] == "uint8" and len(header_shape) == 2: bayer_image = np.frombuffer(data, dtype=np.uint8) bayer_image = bayer_image.reshape(header_shape) - rgb_image = cv2.cvtColor(bayer_image, cv2.COLOR_BAYER_RG2RGB) + rgb_image = cv2.cvtColor(bayer_image, cv2.COLOR_BAYER_GB2RGB) #cv2.COLOR_BAYER_GB2RGB for ethernet connection rgb_image = rgb_image[:, ::-1, :].copy() - qimage = QImage(rgb_image.data, header_shape[1], header_shape[0], #For ethernet need header_shape[0], header_shape[1], header_shape[0]*3, - QImage.Format.Format_RGB888) + if self.__measure_focus: + gray = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2GRAY) + if self.__focus_mask is None or self.__focus_mask.shape != gray.shape: + height, width = gray.shape + y, x = np.ogrid[:height, :width] + self.__focus_mask = (x - self.__beam_x) ** 2 + (y - self.__beam_y) ** 2 <= self.__radius ** 2 + + sharpness = focus_measure_edges(gray, self.__focus_mask) + print(f"GUI: mask pixels: {self.__focus_mask.sum()}, " + f"center=({self.__beam_x:.1f}, {self.__beam_y:.1f}), " + f"radius={self.__radius}, " + f"focus={sharpness:.2f}") + self.focus_measure.emit(sharpness) + + qimage = QImage(rgb_image.data, header_shape[1], header_shape[0], QImage.Format.Format_RGB888) self.camera_image.emit(QPixmap.fromImage(qimage)) else: print("Sample camera image has wrong dimensions") @@ -54,45 +89,3 @@ class SampleCameraThread(QThread): self.__socket.close() self.quit() self.wait() - -class PredictionSubscriber(QThread): - # emits parsed JSON payload (dict with keys: time, frame_id, shape, boxes) - prediction = Signal(dict) - - def __init__(self, pred_zmq_url: str, topic: bytes = b"", parent=None): - super().__init__(parent) - self._ctx = zmq.Context() - self._sock = self._ctx.socket(zmq.SUB) - if topic: - self._sock.setsockopt(zmq.SUBSCRIBE, topic) - else: - self._sock.setsockopt(zmq.SUBSCRIBE, b"") - self._sock.connect(pred_zmq_url) - self.running = True - - def run(self): - while self.running: - try: - parts = self._sock.recv_multipart() - if not parts: - continue - # publisher sends either raw JSON or [topic, json] - payload_bytes = parts[-1] - try: - payload = json.loads(payload_bytes.decode("utf-8")) - except Exception: - continue - self.prediction.emit(payload) - except Exception as e: - print("PredictionSubscriber error:", e) - break - - def stop(self): - self.running = False - try: - self._sock.close() - except Exception: - pass - self.quit() - print("Prediction thread stopped. Exiting...") - self.wait() \ No newline at end of file diff --git a/src/aare/gui/threads/daq_worker.py b/src/aare/gui/threads/daq_worker.py index 5cbbf222..12214701 100644 --- a/src/aare/gui/threads/daq_worker.py +++ b/src/aare/gui/threads/daq_worker.py @@ -195,8 +195,12 @@ class DAQWorker(QObject): self.generic_put(f"beamline/zoom?val={f:.3f}") @Slot(int) - def light(self, v: int): - self.generic_put(f"beamline/light?val={v:d}") + def front_light(self, v: int): + self.generic_put(f"beamline/front_light?val={v:d}") + + @Slot(int) + def back_light(self, v: int): + self.generic_put(f"beamline/back_light?val={v:d}") @Slot() def close_shutter(self): diff --git a/src/aare/gui/threads/prediction_subscriber.py b/src/aare/gui/threads/prediction_subscriber.py new file mode 100644 index 00000000..4e506465 --- /dev/null +++ b/src/aare/gui/threads/prediction_subscriber.py @@ -0,0 +1,42 @@ +import json + +import zmq +from PySide6.QtCore import QThread, Signal + +class PredictionSubscriber(QThread): + # emits parsed JSON payload (dict with keys: time, frame_id, shape, boxes) + prediction = Signal(dict) + + def __init__(self, pred_zmq_url: str, topic: bytes = b"", parent=None): + super().__init__(parent) + self._ctx = zmq.Context() + self._sock = self._ctx.socket(zmq.SUB) + if topic: + self._sock.setsockopt(zmq.SUBSCRIBE, topic) + else: + self._sock.setsockopt(zmq.SUBSCRIBE, b"") + self._sock.connect(pred_zmq_url) + self.running = True + + def run(self): + while self.running: + try: + parts = self._sock.recv_multipart() + if not parts: + continue + # publisher sends either raw JSON or [topic, json] + payload_bytes = parts[-1] + try: + payload = json.loads(payload_bytes.decode("utf-8")) + except Exception: + continue + self.prediction.emit(payload) + except Exception as e: + print("PredictionSubscriber error:", e) + break + + def stop(self): + self.running = False + self._sock.close() + self.quit() + self.wait() \ No newline at end of file diff --git a/src/aare/gui/widgets/camera_image.py b/src/aare/gui/widgets/camera_image.py index 19e54aef..eefdcf24 100644 --- a/src/aare/gui/widgets/camera_image.py +++ b/src/aare/gui/widgets/camera_image.py @@ -371,9 +371,10 @@ class SampleCameraImageLabel(QGraphicsView): self.__screenshot_with_dialog(overlay=True) elif action == autofocus_action: c = self.mapToScene(event.pos()) - self.autofocus.emit(AutofocusSettings(center_x_pxl=c.x(), center_y_pxl=c.y(), - radius_pxl=100, z_range_um=0.5, - z_steps=25)) + self.autofocus.emit(AutofocusSettings(center_x_pxl=None, center_y_pxl=None, + radius_pxl=30, + z_range_um=2000, + z_steps=10)) elif action == beam_mark_action: c = self.mapToScene(event.pos()) self.update_beam_mark.emit(c.x(), c.y()) diff --git a/src/aare/gui/widgets/status_bar.py b/src/aare/gui/widgets/status_bar.py index 2075de40..0e0f037b 100644 --- a/src/aare/gui/widgets/status_bar.py +++ b/src/aare/gui/widgets/status_bar.py @@ -32,6 +32,7 @@ class StatusBar(QStatusBar): self.__is_staff = self.__decoded_token.staff self.__allowed_pgroups = self.__decoded_token.pgroups + self.sharpness = ValueLabel("Samcam image sharpness", "", self) self.flux = ValueLabel("Flux", "x 109 ph/s", self) self.transmission = ValueLabel("Transmission", "", self) self.ring_current = ValueLabel("Ring current", "mA", self) @@ -53,6 +54,7 @@ class StatusBar(QStatusBar): self.shutter_label.clicked.connect(self.show_shutter_menu) self.exp_shutter_label = ValueLabel("ExpHutch Shutter", "", self) + self.addPermanentWidget(self.sharpness) self.addPermanentWidget(self.flux) self.addPermanentWidget(self.transmission) self.addPermanentWidget(self.ring_current) @@ -66,6 +68,10 @@ class StatusBar(QStatusBar): self.addPermanentWidget(self.busy_label) self.addPermanentWidget(self.session_label) + @Slot(float) + def update_sharpness(self, val: float): + self.sharpness.set_value(f"{val:.3f}") + @Slot(DAQStatusModel) def update_daq_status(self, status: DAQStatusModel): self.__status = status