diff --git a/src/odemis/acq/align/goffset.py b/src/odemis/acq/align/goffset.py
index 437d8ba324..51f5bf3d27 100644
--- a/src/odemis/acq/align/goffset.py
+++ b/src/odemis/acq/align/goffset.py
@@ -531,6 +531,29 @@ def _checkCancelled(future: "model.ProgressiveFuture") -> None:
raise CancelledError()
+def _ensure_horizontal_bin_1(detector: model.Detector) -> Dict[model.VigilantAttribute, Any]:
+ """
+ Ensure that the detector is set to horizontal binning of 1, while keeping the same intensity
+ level per pixel (by increasing the exposure time proportionally).
+
+ :param detector: CCD to adjust. If binning is not supported, nothing is done.
+ :return: dict of attributes to restore to the previous value
+ """
+ restore_attrs = {}
+ if not model.hasVA(detector, "binning") or detector.binning.value[0] == 1:
+ return restore_attrs
+
+ restore_attrs[detector.binning] = detector.binning.value
+ bin_x = detector.binning.value[0]
+ detector.binning.value = (1, detector.binning.value[1])
+
+ # increase exposure time to maintain same intensity per pixel
+ exp_t = detector.exposureTime.value
+ restore_attrs[detector.exposureTime] = exp_t
+ detector.exposureTime.value = detector.exposureTime.clip(exp_t * bin_x)
+
+ return restore_attrs
+
def _total_alignment_time(n_gratings: int,
n_detectors: int) -> float:
"""
@@ -623,10 +646,10 @@ def _do_auto_align_grating_detector_offsets(future: model.ProgressiveFuture,
:return: dict mapping (grating, detector) to alignment success boolean
:raises CancelledError: if the operation is cancelled
"""
-
results: Dict[tuple, bool] = {}
original_pos = {k: v for k, v in spectrograph.position.value.items()
if k in ("wavelength", "grating")}
+ restore_attrs : Dict[model.VigilantAttribute, Any] = {} # VAs -> value to restore
gratings = sorted(list(spectrograph.axes["grating"].choices.keys()))
logging.info(f"Available gratings: {list(spectrograph.axes['grating'].choices.keys())}")
@@ -671,6 +694,10 @@ def is_current_detector(d):
logging.info("Setting optical path to alignment mode: %s",align_mode)
future._subfuture = opm.setPath(align_mode, detector=first_detector)
+ # in the meantime, adjust the horizontal binning to the minimum, to get the best results
+ for d in detectors:
+ restore_attrs.update(_ensure_horizontal_bin_1(d))
+
future._subfuture.result()
_checkCancelled(future)
@@ -702,10 +729,10 @@ def is_current_detector(d):
logging.info("Finished alignment | Detector: %s | Grating: %s", d.name, g0)
+ # align remaining gratings using the first detector
if selector:
selector.moveAbsSync({selector_axes: detector_to_selector[first_detector]})
- # align remaining gratings using the first detector
for g in gratings[1:]:
_checkCancelled(future)
logging.info("Switching to grating: %s", g)
@@ -734,12 +761,18 @@ def is_current_detector(d):
raise
finally:
- logging.info("Turning off brightlight")
+ logging.info("Restoring previous state")
try:
bl.power.value = bl.power.range[0]
except Exception:
logging.exception("Failed to turn off the light during alignment cleanup")
+ try:
+ for va, value in restore_attrs.items():
+ va.value = value
+ except Exception:
+ logging.exception("Failed to restore previous detector settings")
+
try:
spectrograph.moveAbsSync(original_pos)
except Exception:
diff --git a/src/odemis/acq/align/test/goffset_test.py b/src/odemis/acq/align/test/goffset_test.py
index 8d20c5c185..9fee7f6f8d 100644
--- a/src/odemis/acq/align/test/goffset_test.py
+++ b/src/odemis/acq/align/test/goffset_test.py
@@ -154,6 +154,7 @@ def setUpClass(cls):
cls.microscope = model.getMicroscope()
cls.optmngr = path.OpticalPathManager(cls.microscope)
+ # The simulator only simulates the 0th order when wavelength is set to 0nm, so force it.
cls.spgr.moveAbsSync({"wavelength": 0.0})
cls._original_position = cls.spgr.position.value.copy()
@@ -200,7 +201,7 @@ def test_scale_not_misaligned(self):
# If peak is already centered, the algorithm exits immediately
# so goffset should not change.
- self.assertAlmostEqual(start_goffset, end_goffset, places=6,
+ self.assertAlmostEqual(start_goffset, end_goffset, delta=1,
msg="goffset changed even though peak was already centered (scale estimation likely ran)")
def test_scale_estimation_misaligned(self):
@@ -265,6 +266,7 @@ def test_single_detector_updates_grating(self):
except Exception:
logging.debug("Selector move to secondary failed or not present; continuing")
+ self.spgr.moveRelSync({"goffset": 321}) # intentionally misalign
start_goffset = self.spgr.position.value["goffset"]
f = sparc_auto_grating_offset(self.spgr, spccd, max_it=50)
diff --git a/src/odemis/driver/simcam.py b/src/odemis/driver/simcam.py
index a2d2b1dc47..ae90d8a502 100644
--- a/src/odemis/driver/simcam.py
+++ b/src/odemis/driver/simcam.py
@@ -40,6 +40,7 @@
GOFFSET_TO_PIXEL = 0.25 # Conversion factor for grating offset to image pixels.
PEAK_WIDTH = 2.5 # Width of the simulated spectrograph peak in pixels (before binning).
+
class Camera(model.DigitalCamera):
'''
This represent a fake digital camera, which generates as data the image
@@ -429,7 +430,7 @@ def _simulate(self) -> model.DataArray:
if self._spectrograph and self._spectrograph.position.value["wavelength"] < 10e-9:
current_offset = self._spectrograph.position.value["goffset"]
- ccd_center_x = self._img_res[0] / 2 # find the x-coordinate of the center of the ccd
+ ccd_center_x = (self._img_res[0] - 1) / 2 # find the x-coordinate of the center of the ccd
x0_px = ccd_center_x + current_offset * GOFFSET_TO_PIXEL
roi_left = center[0] + trans[0] + stage_shift[0] - (res[0] / 2) * binning[0]
diff --git a/src/odemis/gui/cont/tabs/sparc2_align_tab.py b/src/odemis/gui/cont/tabs/sparc2_align_tab.py
index aa4b129154..2e52409919 100644
--- a/src/odemis/gui/cont/tabs/sparc2_align_tab.py
+++ b/src/odemis/gui/cont/tabs/sparc2_align_tab.py
@@ -207,6 +207,7 @@ def __init__(self, name, button, panel, main_frame, main_data):
self.panel.vp_align_lens_ext.view.show_pixelvalue.value = False
self.panel.vp_align_light.view.show_crosshair.value = False
self.panel.vp_align_light_ar.view.show_crosshair.value = False
+ self._grating_calib_prev_crosshair = False # Previous value, used after grating calibration
# Will show the (pulsed) ebeam blanker settings, if available, otherwise will do nothing
self._ebeam_blanker_ctrl = EBeamBlankerSettingsController(panel, tab_data)
@@ -785,18 +786,18 @@ def add_axis(axisname, comp, label=None):
# calibration of the trigger delays for temporal resolved acq
# * light-in-align: engage or retract the folding mirror to switch to
# internal or external spectrograph
- self._alignbtn_to_mode = collections.OrderedDict((
- (panel.btn_align_lens, "lens-align"),
- (panel.btn_align_mirror, "mirror-align"),
- (panel.btn_align_lens2, "lens2-align"),
- (panel.btn_align_centering, "center-align"),
- (panel.btn_align_ek, "ek-align"),
- (panel.btn_align_fiber, "fiber-align"),
- (panel.btn_align_streakcam, "streak-align"),
- (panel.btn_align_light_in, "light-in-align"),
- (panel.btn_align_light_in_ar, "light-in-align-ar"),
- (panel.btn_align_tunnel_lens, "tunnel-lens-align"),
- ))
+ self._alignbtn_to_mode = {
+ panel.btn_align_lens: "lens-align",
+ panel.btn_align_mirror: "mirror-align",
+ panel.btn_align_lens2: "lens2-align",
+ panel.btn_align_centering: "center-align",
+ panel.btn_align_ek: "ek-align",
+ panel.btn_align_fiber: "fiber-align",
+ panel.btn_align_streakcam: "streak-align",
+ panel.btn_align_light_in: "light-in-align",
+ panel.btn_align_light_in_ar: "light-in-align-ar",
+ panel.btn_align_tunnel_lens: "tunnel-lens-align",
+ }
# The GUI mode to the optical path mode (see acq.path.py)
self._mode_to_opm = {
@@ -875,6 +876,16 @@ def add_axis(axisname, comp, label=None):
# To hold the progressive future connector during calibration
self._pfc_grating_calibration = None
+ # To disable the buttons during automated calibration/alignment procedures
+ tab_data.main.is_acquiring.subscribe(self._on_acquisition)
+
+ @call_in_wx_main
+ def _on_acquisition(self, acquiring):
+ for btn in self._alignbtn_to_mode.keys():
+ btn.Enable(not acquiring)
+
+ # the ActuatorController takes care of the buttons for moving the actuators
+
def _on_btn_auto_align(self, evt):
"""
Handle the "Auto alignment" button click.
@@ -1191,6 +1202,7 @@ def _onAlignMode(self, mode):
self.panel.pnl_lens_mover.Enable(False) # Will be enabled once the lens is at the correct place
self.panel.pnl_lens_switch.Show(False)
self.panel.pnl_focus.Show(True)
+ self.panel.pnl_grating.Show(True)
self.panel.pnl_focus_ext.Show(False)
self.panel.gauge_autofocus.Enable(True)
self.panel.btn_autofocus.Enable(True)
@@ -1216,6 +1228,7 @@ def _onAlignMode(self, mode):
self.panel.pnl_lens_mover.Show(False)
self.panel.pnl_lens_switch.Show(False)
self.panel.pnl_focus.Show(False)
+ self.panel.pnl_grating.Show(False)
self.panel.pnl_focus_ext.Show(False)
self.panel.pnl_fibaligner.Show(False)
self.panel.pnl_streak.Show(False)
@@ -1235,6 +1248,7 @@ def _onAlignMode(self, mode):
self.panel.pnl_lens_switch.Show(True)
self.panel.pnl_lens_switch.Enable(False) # Will be enabled once the lens is at the correct place
self.panel.pnl_focus.Show(True)
+ self.panel.pnl_grating.Show(False)
self.panel.pnl_focus_ext.Show(False)
self.panel.gauge_autofocus.Enable(True)
self.panel.btn_autofocus.Enable(True)
@@ -1257,6 +1271,7 @@ def _onAlignMode(self, mode):
self.panel.pnl_lens_mover.Show(False)
self.panel.pnl_lens_switch.Show(False)
self.panel.pnl_focus.Show(False)
+ self.panel.pnl_grating.Show(False)
self.panel.pnl_focus_ext.Show(False)
self.panel.pnl_fibaligner.Show(False)
self.panel.pnl_streak.Show(False)
@@ -1289,6 +1304,7 @@ def _onAlignMode(self, mode):
self.panel.pnl_lens_mover.Show(False)
self.panel.pnl_lens_switch.Show(False)
self.panel.pnl_focus.Show(False)
+ self.panel.pnl_grating.Show(False)
self.panel.pnl_focus_ext.Show(False)
self.panel.pnl_fibaligner.Show(False)
self.panel.pnl_streak.Show(False)
@@ -1307,6 +1323,7 @@ def _onAlignMode(self, mode):
self.panel.pnl_lens_mover.Show(False)
self.panel.pnl_lens_switch.Show(False)
self.panel.pnl_focus.Show(False)
+ self.panel.pnl_grating.Show(False)
self.panel.pnl_focus_ext.Show(False)
self.panel.pnl_fibaligner.Show(True)
# Disable the buttons until the fiber box is ready
@@ -1347,6 +1364,7 @@ def _onAlignMode(self, mode):
else:
self.panel.pnl_focus.Show(True)
self.panel.pnl_focus_ext.Show(False)
+ self.panel.pnl_grating.Show(False)
self.panel.btn_autofocus.Enable(False)
self.panel.btn_autofocus_ext.Enable(False)
self.panel.gauge_autofocus.Enable(False)
@@ -1395,6 +1413,7 @@ def _onAlignMode(self, mode):
self.panel.pnl_focus.Show(True)
self.panel.btn_autofocus.Enable(False)
self.panel.gauge_autofocus.Enable(False)
+ self.panel.pnl_grating.Show(False)
self.panel.pnl_focus_ext.Show(False)
self.panel.pnl_fibaligner.Show(False)
self.panel.pnl_streak.Show(False)
@@ -1434,6 +1453,7 @@ def _onAlignMode(self, mode):
self.panel.pnl_focus.Show(True)
self.panel.btn_autofocus.Enable(False)
self.panel.gauge_autofocus.Enable(False)
+ self.panel.pnl_grating.Show(False)
self.panel.pnl_focus_ext.Show(False)
self.panel.pnl_fibaligner.Show(False)
self.panel.pnl_streak.Show(False)
@@ -1457,6 +1477,7 @@ def _onAlignMode(self, mode):
self.panel.pnl_lens_mover.Show(False)
self.panel.pnl_lens_switch.Show(False)
self.panel.pnl_focus.Show(False)
+ self.panel.pnl_grating.Show(False) # TODO: should allow grating calibration once it is compatible
self.panel.pnl_focus_ext.Show(True)
self.panel.pnl_fibaligner.Show(False)
self.panel.pnl_streak.Show(False)
@@ -1549,8 +1570,9 @@ def _on_align_mode_done(self, f):
logging.debug("Optical path was updated.")
- # Auto-Calibration
+ # Grating auto-calibration
def _on_btn_grating_calibration(self, evt):
+ # Procedure is running -> the button is used to cancel it
if not self._grating_calibration_future.done():
self._grating_calibration_future.cancel()
return
@@ -1559,32 +1581,27 @@ def _on_btn_grating_calibration(self, evt):
self.panel.gauge_auto_grating_center.SetValue(0)
self.panel.btn_auto_grating_center.SetLabel("Cancel")
- wx.CallAfter(self._start_grating_calibration)
+ self._start_grating_calibration()
def _start_grating_calibration(self):
main = self.tab_data_model.main
align_mode = self.tab_data_model.align_mode.value
- opm = main.opm
+ viewport = None # Viewport that is used in the current mode
# Set the optical path according to the align mode
- if align_mode == "streak-align":
- if (main.streak_ccd
- and main.spectrograph_ded
- and main.streak_ccd.name in main.spectrograph_ded.affects.value):
- opath = "streak-focus-ext"
- else:
- opath = "streak-focus"
- elif align_mode == "tunnel-lens-align":
- opath = "spec-focus-ext"
- elif align_mode in ("lens-align", "lens2-align", "light-in-align"):
+ if align_mode in ("lens-align", "lens2-align", "light-in-align"):
opath = "spec-focus"
+ viewport = self.panel.vp_align_lens
+ # TODO: support external spectrograph calibration
+ # elif align_mode == "tunnel-lens-align":
+ # opath = "spec-focus-ext"
else:
logging.warning("Auto calibration requested not compatible with requested alignment mode %s. Do nothing.",
align_mode)
return
# Pick the right hardware based on whether opath is external or internal
- if opath in ("spec-focus-ext", "streak-focus-ext"):
+ if opath == "spec-focus-ext":
bl = main.brightlight_ext
spectrograph = main.spectrograph_ded
selector = getattr(main, "spec_ded_det_selector", None)
@@ -1613,15 +1630,31 @@ def _start_grating_calibration(self):
# Start alignment procedure
self._grating_calibration_future = auto_align_grating_detector_offsets(
- spectrograph, detectors, opm, opath, bl, selector=selector)
+ spectrograph, detectors, main.opm, opath, bl, selector=selector)
# Bind progress & done callbacks
self._grating_calibration_future.add_done_callback(self._on_grating_calibration_done)
self.panel.btn_auto_grating_center.SetLabel("Cancel")
+ # Show the cross-hair, at the middle, to confirm the alignment is correct.
+ if viewport:
+ viewport.canvas.fit_view_to_content()
+ self._grating_calib_prev_crosshair = viewport.view.show_crosshair.value
+ viewport.view.show_crosshair.value = True
+
+ # Disable the rest of the GUI
+ main.is_acquiring.value = True
+ if align_mode == "tunnel-lens-align":
+ self.panel.btn_manual_focus_ext.Enable(False)
+ else:
+ self.panel.btn_manual_focus.Enable(False)
+ self._enableFocusComponents(manual=False, ccd_stream=False)
+ self.panel.btn_bkg_acquire.Enable(False)
+
self._pfc_grating_calibration = ProgressiveFutureConnector(self._grating_calibration_future,
self.panel.gauge_auto_grating_center)
+ @call_in_wx_main
def _on_grating_calibration_done(self, f):
try:
result = f.result()
@@ -1631,12 +1664,17 @@ def _on_grating_calibration_done(self, f):
except Exception:
logging.exception("Grating calibration failed")
- finally:
- self._grating_calibration_future = model.InstantaneousFuture()
- self._pfc_grating_calibration = None
+ self.tab_data_model.main.is_acquiring.value = False
+ self._grating_calibration_future = model.InstantaneousFuture()
+ self._pfc_grating_calibration = None
+
+ # Go back to "normal" mode, which is the simplest to enable the right widgets and play the rigth stream.
+ self._onAlignMode(self.tab_data_model.align_mode.value)
+
+ self.panel.vp_align_lens.view.show_crosshair.value = self._grating_calib_prev_crosshair
- wx.CallAfter(self.panel.btn_auto_grating_center.SetLabel, "Auto center")
- wx.CallAfter(self.panel.gauge_auto_grating_center.SetValue, 0)
+ wx.CallAfter(self.panel.btn_auto_grating_center.SetLabel, "Auto calib")
+ wx.CallAfter(self.panel.gauge_auto_grating_center.SetValue, 0)
@call_in_wx_main
def _on_lens_align_done(self, f):
@@ -2006,6 +2044,7 @@ def _onManualFocus(self, event):
self._enableFocusComponents(manual=True, ccd_stream=False)
self._stream_controller.pauseStreams()
self.panel.btn_bkg_acquire.Enable(False)
+ self.panel.btn_auto_grating_center.Enable(False)
self._mf_future = Sparc2ManualFocus(main.opm, opath, toggled=True)
self._mf_future.add_done_callback(self._onManualFocusReady)
@@ -2045,6 +2084,7 @@ def _onManualFocusFinished(self, future):
if future.cancelled():
return
+ self.panel.btn_auto_grating_center.Enable(True)
self._onAlignMode(self.tab_data_model.align_mode.value)
@call_in_wx_main
@@ -2080,6 +2120,7 @@ def _onAutofocus(self, active):
self._enableFocusComponents(manual=False, ccd_stream=False)
self._stream_controller.pauseStreams()
self.panel.btn_bkg_acquire.Enable(False)
+ self.panel.btn_auto_grating_center.Enable(False)
# No manual autofocus for now
self._autofocus_f = Sparc2AutoFocus(focus_mode, main.opm, ss, start_autofocus=True)
@@ -2102,6 +2143,7 @@ def _onAutofocus(self, active):
logging.error("Unexpected autofocus mode '%s'", self._autofocus_align_mode)
return
btn.SetLabel("Auto focus")
+ self.panel.btn_auto_grating_center.Enable(True)
@call_in_wx_main
def _on_autofocus_done(self, future):
diff --git a/src/odemis/gui/main_xrc.py b/src/odemis/gui/main_xrc.py
index e84e01c8a2..76c3eef90c 100644
--- a/src/odemis/gui/main_xrc.py
+++ b/src/odemis/gui/main_xrc.py
@@ -1287,7 +1287,7 @@ def __init__(self, parent):
self.cmb_focus_detectors = xrc.XRCCTRL(self, "cmb_focus_detectors")
self.cmb_focus_gratings_label = xrc.XRCCTRL(self, "cmb_focus_gratings_label")
self.cmb_focus_gratings = xrc.XRCCTRL(self, "cmb_focus_gratings")
- self.pnl_calibrate = xrc.XRCCTRL(self, "pnl_calibrate")
+ self.pnl_grating = xrc.XRCCTRL(self, "pnl_grating")
self.btn_auto_grating_center = xrc.XRCCTRL(self, "btn_auto_grating_center")
self.gauge_auto_grating_center = xrc.XRCCTRL(self, "gauge_auto_grating_center")
self.pnl_focus_ext = xrc.XRCCTRL(self, "pnl_focus_ext")
@@ -15769,12 +15769,12 @@ def __init_resources():
5