diff --git a/plugins/sparc_save_drift_corrector_images.py b/plugins/sparc_save_drift_corrector_images.py new file mode 100644 index 0000000000..57b8ca6e21 --- /dev/null +++ b/plugins/sparc_save_drift_corrector_images.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" +Created on 11 June 2026 + +@author: Nandish Patel + +Copyright © 2026 Nandish Patel, Delmic + +This file is part of Odemis. + +Odemis is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License version 2 as published by the Free Software Foundation. + +Odemis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even +the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +Public License for more details. + +You should have received a copy of the GNU General Public License along with Odemis. If not, +see http://www.gnu.org/licenses/. +""" + +import logging + +import wx + +from odemis.gui.model import TabName +from odemis.gui.plugin import Plugin + + +class SaveDriftCorrectorImgPlugin(Plugin): + name = "Save drift corrector images" + __version__ = "1.0" + __author__ = "Nandish Patel" + __license__ = "GPLv2" + + def __init__(self, microscope, main_app): + super().__init__(microscope, main_app) + + # It only makes sense if the SPARC acquisition tab is present + try: + sparc_acq_tab = main_app.main_data.getTabByName(TabName.SPARC_ACQUI) + except LookupError: + logging.debug( + "Not loading save drift corrector images tool since SPARC acquisition tab is not present." + ) + return + + self._sparc_acq_tab = sparc_acq_tab + self.addMenu("Help/Development/Save drift corrector images", + self._save_drift_corrector_images, + item_kind=wx.ITEM_CHECK, + pass_menu_item=True) + + def _on_sparc_acq_ctrl_filename(self, filename): + self._sparc_acq_tab.tab_data_model.driftCorrector.log_path = filename + + def _save_drift_corrector_images(self, menu_item): + """Menu callback for: Help/Development/Save drift corrector images""" + checked = menu_item.IsChecked() + if checked: + self._sparc_acq_tab._acquisition_controller.filename.subscribe(self._on_sparc_acq_ctrl_filename, init=True) + logging.debug("Save drift corrector images checked, will acquire drift corrector images") + else: + self._sparc_acq_tab._acquisition_controller.filename.unsubscribe(self._on_sparc_acq_ctrl_filename) + logging.debug("Save drift corrector images unchecked, will not acquire drift corrector images") diff --git a/src/odemis/acq/drift/__init__.py b/src/odemis/acq/drift/__init__.py index e2f66929e7..619b37b8b8 100644 --- a/src/odemis/acq/drift/__init__.py +++ b/src/odemis/acq/drift/__init__.py @@ -23,12 +23,13 @@ import itertools import logging import math +import os import threading -import numpy import cv2 +import numpy -from odemis import model +from odemis import dataio, model from odemis.acq.align.shift import MeasureShift MIN_RESOLUTION = (20, 20) # sometimes 8x8 works, but it's not reliable enough @@ -46,7 +47,7 @@ class AnchoredEstimator(object): to measure the drift. """ - def __init__(self, scanner, detector, region, dwell_time, max_pixels=MAX_PIXELS, follow_drift=True): + def __init__(self, scanner, detector, region, dwell_time, max_pixels=MAX_PIXELS, follow_drift=True, log_path=None): """ scanner (Emitter) detector (Detector) @@ -58,11 +59,14 @@ def __init__(self, scanner, detector, region, dwell_time, max_pixels=MAX_PIXELS, follow_drift (bool): If True, the anchor region position is adjusted based on the drift measured. It is useful when drift compensation is done by adjusting the scanner settings. If False, the anchor region is fixed. It is useful when drift compensation is based on beam shift or stage movement. + log_path (Optional[str]): directory and filename pattern to save drift corrector images for debugging """ self._emitter = scanner self._semd = detector self._dwell_time = dwell_time self._follow_drift = follow_drift + self._log_path = log_path + self._image_counter = 0 # Latest drift vector from the previous acquisition self.drift = (0, 0) # in sem px @@ -143,6 +147,17 @@ def acquire(self): if data.shape[::-1] != self._res: logging.warning("Shape of data is %s instead of %s", data.shape[::-1], self._res) + # Save all the drift region scans for offline autocorrelation purposes + if self._log_path is not None: + filename = os.path.basename(self._log_path) + if not filename: + raise ValueError("Filename is not found on log path.") + exporter = dataio.find_fittest_converter(filename) + path, base = os.path.split(filename) + fn = f"drift_{self._image_counter:05d}_" + base + exporter.export(data, os.path.join(path, fn)) + self._image_counter += 1 + # TODO: allow to record just every Nth image, and separately record the # drift after every measurement # In the mean time, we only save the 1st, 2nd and last two images diff --git a/src/odemis/acq/leech.py b/src/odemis/acq/leech.py index b4636c192d..ae64391d80 100644 --- a/src/odemis/acq/leech.py +++ b/src/odemis/acq/leech.py @@ -151,6 +151,7 @@ def __init__(self, scanner, detector): self._detector = detector self._dc_estimator = None self._period_acq = None # number of acq left until next drift correction is performed + self.log_path = None # roi: the anchor region, it must be set to something different from # UNDEFINED_ROI to run. @@ -280,7 +281,8 @@ def series_start(self): self._dc_estimator = drift.AnchoredEstimator(self._scanner, self._detector, self.roi.value, - self.dwellTime.value) + self.dwellTime.value, + log_path=self.log_path) # First acquisition of anchor area self._dc_estimator.acquire()