-
Notifications
You must be signed in to change notification settings - Fork 41
[feat] save drift corrector images #3487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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): | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add type annotation to all methods. |
||||||||||||||
| """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) | ||||||||||||||
|
Comment on lines
+63
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Clear Unsubscribing the callback stops future filename updates but leaves 🔧 Proposed fix else:
self._sparc_acq_tab._acquisition_controller.filename.unsubscribe(self._on_sparc_acq_ctrl_filename)
+ self._sparc_acq_tab.tab_data_model.driftCorrector.log_path = None
logging.debug("Save drift corrector images unchecked, will not acquire drift corrector images")📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| logging.debug("Save drift corrector images unchecked, will not acquire drift corrector images") | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+152
to
+156
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. filename is the basename of log_path (see line 152), when you split that (line 156) the path variable is just going to be an empty string.
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you do this, you also need to change |
||||||||||||||||||||||||||||||||||||||||||||||||
| fn = f"drift_{self._image_counter:05d}_" + base | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you add drift at the beginning of the filename, all the "drift_xxx" file images will be next to each other in the directory. I think it'd be nicer to use |
||||||||||||||||||||||||||||||||||||||||||||||||
| exporter.export(data, os.path.join(path, fn)) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+152
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Directory path is lost — drift images save to the current working directory instead of the intended directory.
Example: Per coding guidelines, prefer 🐛 Proposed fix using pathlib.Path- 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
+ if self._log_path is not None:
+ log_path = Path(self._log_path)
+ if not log_path.name:
+ raise ValueError("Filename is not found on log path.")
+ exporter = dataio.find_fittest_converter(log_path.name)
+ fn = f"drift_{self._image_counter:05d}_{log_path.name}"
+ exporter.export(data, str(log_path.parent / fn))
+ self._image_counter += 1📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||||||||||||||||||||||||||
| self._image_counter += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+150
to
+159
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Wrap image export in error handling to prevent debugging saves from crashing the acquisition. Both the 🛡️ Proposed fix # 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
+ try:
+ log_path = Path(self._log_path)
+ if not log_path.name:
+ logging.warning("Cannot save drift corrector image: filename is empty in log path '%s'", self._log_path)
+ else:
+ exporter = dataio.find_fittest_converter(log_path.name)
+ fn = f"drift_{self._image_counter:05d}_{log_path.name}"
+ exporter.export(data, str(log_path.parent / fn))
+ self._image_counter += 1
+ except Exception:
+ logging.exception("Failed to save drift corrector image to '%s'", self._log_path)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # 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 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: delmic/odemis
Length of output: 212
🏁 Script executed:
Repository: delmic/odemis
Length of output: 8063
Add type hints and rST docstrings to the new SaveDriftCorrectorImgPlugin
File: plugins/sparc_save_drift_corrector_images.py
SaveDriftCorrectorImgPlugin(lines 30-31) is missing a class docstring.__init__(self, microscope, main_app)(lines 36-37) is missing type hints for parameters/return type and has no docstring._save_drift_corrector_images(self, menu_item)(lines 54-62) is missing type hints formenu_itemand the return type (it already has a docstring).🤖 Prompt for AI Agents
Source: Coding guidelines