Merge remote-tracking branch 'origin/x10sa' into x10sa
This commit is contained in:
commit
c5f5198d8d
13 files changed
+393
-88
No files matched your search
@@ -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))
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
+181
-14
@@ -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
|
||||
|
||||
+10
-4
@@ -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")
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
|
||||
@@ -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 10<sup>9</sup> 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
|
||||
|
||||
Reference in new issue
Block a user