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 - + wxVERTICAL - + #E5E5E5 16 @@ -15790,8 +15790,8 @@ def __init_resources(): 24 def - - Attempts to auto-calibrate the zero-th order peak for grating alignment. + + Auto-calibrate grating offset by centering the zero-th order peak. 1 diff --git a/src/odemis/gui/xmlh/resources/panel_tab_sparc2_align.xrc b/src/odemis/gui/xmlh/resources/panel_tab_sparc2_align.xrc index 01dbe65d24..4407d4ef07 100644 --- a/src/odemis/gui/xmlh/resources/panel_tab_sparc2_align.xrc +++ b/src/odemis/gui/xmlh/resources/panel_tab_sparc2_align.xrc @@ -402,12 +402,12 @@ 5 - + wxVERTICAL - + #E5E5E5 16 @@ -423,8 +423,8 @@ 24 def - - Attempts to auto-calibrate the zero-th order peak for grating alignment. + + Auto-calibrate grating offset by centering the zero-th order peak. 1