From 7401f14f60bba2486b4f614073765eab885b4faf Mon Sep 17 00:00:00 2001 From: Yu Xia de Jong Date: Mon, 4 May 2026 13:20:53 +0200 Subject: [PATCH 1/2] [feat] added save_images --- plugins/sparc_save_drift_corrector_images.py | 62 ++++++++++++++++++++ src/odemis/acq/drift/__init__.py | 23 +++++++- src/odemis/acq/leech.py | 7 ++- 3 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 plugins/sparc_save_drift_corrector_images.py diff --git a/plugins/sparc_save_drift_corrector_images.py b/plugins/sparc_save_drift_corrector_images.py new file mode 100644 index 0000000000..408a38a2a9 --- /dev/null +++ b/plugins/sparc_save_drift_corrector_images.py @@ -0,0 +1,62 @@ +# -*- 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.plugin import Plugin +from odemis.gui.model import TabName + + +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._drift_corrector = sparc_acq_tab.tab_data_model.driftCorrector + self.addMenu("Help/Development/Save drift corrector images", + self._save_drift_corrector_images, + item_kind=wx.ITEM_CHECK, + pass_menu_item=True) + + def _save_drift_corrector_images(self, menu_item): + """Menu callback for: Help/Development/Save drift corrector images""" + checked = menu_item.IsChecked() + if checked: + self._drift_corrector.save_images = True + logging.debug("Save drift corrector images checked, will acquire drift corrector images") + else: + self._drift_corrector.save_images = False + 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..e245d4dcce 100644 --- a/src/odemis/acq/drift/__init__.py +++ b/src/odemis/acq/drift/__init__.py @@ -20,20 +20,24 @@ Odemis. If not, see http://www.gnu.org/licenses/. """ +import datetime import itertools import logging import math import threading - +import os import numpy import cv2 from odemis import model from odemis.acq.align.shift import MeasureShift +from odemis.gui.util import get_picture_folder MIN_RESOLUTION = (20, 20) # sometimes 8x8 works, but it's not reliable enough MAX_PIXELS = 128 ** 2 # px +DRIFT_IMAGES_DIR = os.path.join(get_picture_folder(), "Drift Correction images") + class AnchoredEstimator(object): """ @@ -46,7 +50,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, save_images=False): """ scanner (Emitter) detector (Detector) @@ -63,6 +67,12 @@ def __init__(self, scanner, detector, region, dwell_time, max_pixels=MAX_PIXELS, self._semd = detector self._dwell_time = dwell_time self._follow_drift = follow_drift + self._save_images = save_images + self._session_id = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + self._image_counter = 0 + + if self._save_images: + os.makedirs(DRIFT_IMAGES_DIR, exist_ok=True) # Latest drift vector from the previous acquisition self.drift = (0, 0) # in sem px @@ -143,6 +153,15 @@ 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._save_images: + filename = os.path.join(DRIFT_IMAGES_DIR, f"drift_{self._session_id}_{self._image_counter:05d}.tif") + self._image_counter += 1 + + success = cv2.imwrite(filename, data) + if not success: + logging.warning("Failed to save drift image %s", filename) + # 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..aeccebc5ba 100644 --- a/src/odemis/acq/leech.py +++ b/src/odemis/acq/leech.py @@ -141,16 +141,18 @@ class AnchorDriftCorrector(LeechAcquirer): estimate current drift. """ - def __init__(self, scanner, detector): + def __init__(self, scanner, detector, save_images=False): """ :param scanner: (Emitter) A component with a .dwellTime, .translation, .scale. :param detector: (Detector) To acquire the signal. + :param save_images: (bool) Whether to save drift corrector images. """ super(AnchorDriftCorrector, self).__init__() self._scanner = scanner self._detector = detector self._dc_estimator = None self._period_acq = None # number of acq left until next drift correction is performed + self.save_images = save_images # roi: the anchor region, it must be set to something different from # UNDEFINED_ROI to run. @@ -280,7 +282,8 @@ def series_start(self): self._dc_estimator = drift.AnchoredEstimator(self._scanner, self._detector, self.roi.value, - self.dwellTime.value) + self.dwellTime.value, + save_images=self.save_images) # First acquisition of anchor area self._dc_estimator.acquire() From 0250ddbc8f66c0f8e2f6e2959c7a41710d267dbe Mon Sep 17 00:00:00 2001 From: Nandish Patel Date: Thu, 9 Jul 2026 09:53:12 +0200 Subject: [PATCH 2/2] [update] address pr comments --- plugins/sparc_save_drift_corrector_images.py | 11 ++++--- src/odemis/acq/drift/__init__.py | 34 +++++++++----------- src/odemis/acq/leech.py | 7 ++-- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/plugins/sparc_save_drift_corrector_images.py b/plugins/sparc_save_drift_corrector_images.py index 408a38a2a9..57b8ca6e21 100644 --- a/plugins/sparc_save_drift_corrector_images.py +++ b/plugins/sparc_save_drift_corrector_images.py @@ -23,8 +23,8 @@ import wx -from odemis.gui.plugin import Plugin from odemis.gui.model import TabName +from odemis.gui.plugin import Plugin class SaveDriftCorrectorImgPlugin(Plugin): @@ -45,18 +45,21 @@ def __init__(self, microscope, main_app): ) return - self._drift_corrector = sparc_acq_tab.tab_data_model.driftCorrector + 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._drift_corrector.save_images = True + 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._drift_corrector.save_images = False + 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 e245d4dcce..619b37b8b8 100644 --- a/src/odemis/acq/drift/__init__.py +++ b/src/odemis/acq/drift/__init__.py @@ -20,24 +20,21 @@ Odemis. If not, see http://www.gnu.org/licenses/. """ -import datetime import itertools import logging import math -import threading import os -import numpy +import threading + import cv2 +import numpy -from odemis import model +from odemis import dataio, model from odemis.acq.align.shift import MeasureShift -from odemis.gui.util import get_picture_folder MIN_RESOLUTION = (20, 20) # sometimes 8x8 works, but it's not reliable enough MAX_PIXELS = 128 ** 2 # px -DRIFT_IMAGES_DIR = os.path.join(get_picture_folder(), "Drift Correction images") - class AnchoredEstimator(object): """ @@ -50,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, save_images=False): + def __init__(self, scanner, detector, region, dwell_time, max_pixels=MAX_PIXELS, follow_drift=True, log_path=None): """ scanner (Emitter) detector (Detector) @@ -62,18 +59,15 @@ 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._save_images = save_images - self._session_id = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + self._log_path = log_path self._image_counter = 0 - if self._save_images: - os.makedirs(DRIFT_IMAGES_DIR, exist_ok=True) - # Latest drift vector from the previous acquisition self.drift = (0, 0) # in sem px # Total drift vector from the first acquisition @@ -154,14 +148,16 @@ def acquire(self): 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._save_images: - filename = os.path.join(DRIFT_IMAGES_DIR, f"drift_{self._session_id}_{self._image_counter:05d}.tif") + 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 - success = cv2.imwrite(filename, data) - if not success: - logging.warning("Failed to save drift image %s", filename) - # 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 aeccebc5ba..ae64391d80 100644 --- a/src/odemis/acq/leech.py +++ b/src/odemis/acq/leech.py @@ -141,18 +141,17 @@ class AnchorDriftCorrector(LeechAcquirer): estimate current drift. """ - def __init__(self, scanner, detector, save_images=False): + def __init__(self, scanner, detector): """ :param scanner: (Emitter) A component with a .dwellTime, .translation, .scale. :param detector: (Detector) To acquire the signal. - :param save_images: (bool) Whether to save drift corrector images. """ super(AnchorDriftCorrector, self).__init__() self._scanner = scanner self._detector = detector self._dc_estimator = None self._period_acq = None # number of acq left until next drift correction is performed - self.save_images = save_images + self.log_path = None # roi: the anchor region, it must be set to something different from # UNDEFINED_ROI to run. @@ -283,7 +282,7 @@ def series_start(self): self._detector, self.roi.value, self.dwellTime.value, - save_images=self.save_images) + log_path=self.log_path) # First acquisition of anchor area self._dc_estimator.acquire()