Skip to content

Commit 981fb8a

Browse files
committed
microEye v2.4.0a3
___ __ __ / _ | / /__ / / ___ _ / __ |/ / _ \/ _ \/ _ `/ /_/ |_/_/ .__/_//_/\_,_/ /_/ What's New: ------------------------------------ Focus Stabilizer Refactor (v2.4.0a3) ------------------------------------ - Major refactor of the Focus Stabilizer backend and frontend: - Modularized stabilization logic into `stabilization.controller` and `stabilization.methods` submodules. - Each axis (X, Y, Z) now has independent PID controller gains and calibration coefficients. - Outlier rejection for shift estimation is now configurable (Standard Deviation, MAD, or None). - Added support for XY stabilization and hybrid modalities (under development). - New data logging system for time, X/Y/Z shifts, and parameter values. - Visualization improvements: - Z histogram, time log of X/Y/Z, XY scatter, and localization overlays. - All plots update in real time with stabilization. - FocusStabilizerView and focusWidget updated for new parameter tree and ROI management. - Configuration save/load for all stabilizer and ROI parameters. - Improved thread safety and async handling for stabilization worker. - Refactored stage and device manager integration for stabilization toggling and lock state. - All stabilization methods now use unified interface for parameter fitting and shift calculation. - Internal API changes: - `FocusStabilizer` and `FocusStabilizerView` now use enums for all axis and parameter references. - `StageManager` and `DeviceManager` updated to use new stabilization API. - Controller widget now supports toggling XY and Z stabilization independently. - Added `FocusPlot` enum and unified plotting logic in `focusWidget`. - Camera/Acquisition/Stage: - All camera classes now implement a `snap_image()` method for single-frame acquisition (Basler, Vimba, uEye, Thorlabs, PCO, PycroManager, Dummy). - Improved dummy camera (`miDummy`) astigmatic fiducials pattern simulation and parameters. - `CameraManager` is now the singleton for all camera lists; all references to `CameraList.CAMERAS` replaced with `CameraManager.CAMERAS`. - Acquisition manager and scan/z-stack logic updated to use new focus stabilization API and per-axis calibration. - KinesisXY and AbstractStage now use unified async worker logic and busy checks. - PycroStageXY async move logic improved and now waits for hardware busy state. - Spatial filter optimizations: - Difference of Gaussians (DoG) and PointGaussFilter now use `fftconvolve` for significantly improved filtering speed. - Radial coordinate calculations for Fourier filters are now Numba-accelerated for faster execution. - General Refactoring: - Moved focus stabilization logic out of monolithic files into dedicated submodules under `stabilization/`. - Minor bugfixes for timeouts, async workers, and config serialization. - Replaced all delay and timeout time measurements to use `monotonic` clocks instead of `time` for improved reliability. - Bugfixes: - Fixed config loading to ensure Pycro-Manager instances and bridges are started before device configuration is loaded, preventing initialization errors. ----------------- microEye v2.4.0a2 ----------------- ------------- Slides Widget ------------- - Add a new `SlideWidget` for visualizing and selecting slide channels - Support multiple slide types and channel geometries (sticky-Slide VI 0.4, μ-Slide VI 0.1, 8/18 Well) - Implement interactive channel selection, highlight, and move-to-center logic in `SlideWidget` - Show current stage position and delta in info bar - Draw position cross and center cross overlays - Emit signals for channel selection and move the stage to the channel center - Add context menu for slide type, orientation (swap XY), and axis inversion - Persist and restore slide widget configuration (`slide`, `swap_xy`, `invert_x`, `invert_y`) in `config.json` - Add "Slides" dock to `miEye_module` ------------------------------------ Stage Metadata & Editable Parameters ------------------------------------ - Add editable X/Y/Z center and max parameters to `StageView` parameter tree - Add `get_center`, `set_center`, `get_max` methods to `AbstractStage` for axis metadata - Update `StageView` to support editing center/max for each axis - Persist and restore center/max values in stage config ------------- Stage Manager ------------- - Implement `StageManager.center(axis)` to move X, Y, or Z stage to editable center position - Refactor `StageManager.move_absolute` to accept optional coordinates and units -------------- Device Manager -------------- - Add unified `update_gui` methods to `DeviceManager` and `CameraList` - Add `centerRequest` method which accepts `Axis` enum for stage centering. - Refactor main window timer to call widget GUI updates ------------------------------ Other Refactors & Improvements ------------------------------ - refactor `miEye_module` to accommodate all the changes and new widgets. - Add XY and Z "Center" buttons to the Controller widget for moving stages to center positions - Fix mouse event handling in `TiledImageSelector' for Qt6 compatibility - Update `config.json` to persist new dock and widget states ----------------- microEye v2.4.0a1 ----------------- -------------------------------------------------- Unified Abstract Stage Class & Stage View Refactor -------------------------------------------------- - The `AbstractStage` class was refactored to unify the interface for all stage types (Z, XY, multi-axis, etc.), supporting: - Consistent axis handling via the `axes` property. - Unified position, movement, and configuration methods for all axes. - Standardized unit conversion and metadata storage. - Improved signal handling for async operations. - The new `StageView provides` a generic parameter tree UI for any stage, automatically adapting to supported axes and serial configuration. - Legacy stage-specific views and controllers (e.g., PiezoConcept, Kinesis) are now replaced or wrapped by the unified view. ---------------------------------- Stage Manager & Stage Manager View ---------------------------------- - Added `StageManager` singleton class to manage all connected stages, their assignment to axes, and movement coordination. - Supports adding/removing stages, axis assignment, and step/jump configuration. - Handles open/close, busy state, and movement requests for all axes. - Added `StageManagerView` for GUI management of stages, drivers, axis assignment, and step/jump sizes. - Stages are now dynamically added/removed and assigned to axes via the manager, supporting multiple hardware backends. -------------- Device Manager -------------- - Removed support for legacy stages and related stage selection logic. - Updated stage management to use the new unified `StageManager` and its API for adding/removing stages and axis assignment. - Updated configuration loading/saving to use the new stage config format. ----------------------------------------- SmarAct Stage Support (Under Development) ----------------------------------------- - Initial implementation of `MCS2Stage` for SmarAct MCS2 controllers. - Device discovery, connection, and configuration support. - Not yet fully integrated into the main GUI, but ready for further development. ------------------------ Camera List Improvements ------------------------ - The `CameraList` now supports dynamic addition/removal of cameras, including PycroManager and hardware cameras. - Improved support for saving/loading camera configuration in the main config file. - Generalized population logic of available connected cameras. ----------------- Base Camera Class ----------------- - Added support for property trees and parameter updates for camera settings. - Improved ROI handling: added `set_roi`, `get_roi`, and `reset_roi` methods. - Added metadata retrieval with get_metadata. - Implemented `update_cam` for dynamic parameter changes. - Added class methods for available camera list enumeration. ------------ Camera Panel ------------ - Unified logic for exposure, framerate, ROI, and capture handling in `Camera_Panel`. - Removed duplicated exposure/framerate/ROI logic from subclasses (Basler, Pycro, Dummy, IDS, Thorlabs, Vimba, PCO). - Subclasses now delegate to base class for common parameter setup and signal connections. - Capture and update methods standardized; device-specific logic moved to base class where possible. - Clean up resources used by the camera panel using overrides of `Camera_Panel.dispose` if needed. - Subclass panels now only implement device-specific overrides and minimal customizations. --------------------- Basler Camera Support --------------------- - Added full support for Basler cameras via the `Basler_Panel` and `basler_cam`. - Camera options, ROI, exposure, and acquisition are integrated into the unified camera panel system. - Basler cameras are now included in config save/load and dynamic camera list management. - SDK seems to conflict with Vimba and Vimba X SDK transport layers. ----------------------------------- Allied Vision (Vimba X SDK) Support ----------------------------------- - Updated support for Allied Vision cameras to use the new Vimba X SDK (`vmbpy`), replacing legacy Vimba SDK. - All Vimba-related panels and camera classes now use the new SDK and are compatible with the unified camera panel and camera list system. - Refactored the `with` context from the main scope to `vimba_cam`. - SDK seems to conflict with Basler Pylon SDK transport layers. ------------------------ PycroManager Refactoring ------------------------ - PycroManager camera and stage classes are now split into their own submodules: - `PycroCamera` in `microEye/hardware/cams/pycromanager/pycro_cam.py`. - `PycroStageZ` and `PycroStageXY` in `microEye/hardware/stages/pycromanager/core.py`. - `PycroPanel` in `microEye/hardware/cams/pycromanager/pycro_panel.py`. - Stage selection and port management are now handled via dialogs and the stage manager. ----------------------------- Save/Load Config Improvements ----------------------------- - The config system now saves and loads all stages and cameras, including their assignment, configuration, and dynamic addition/removal. - When loading a config, available stages and cameras are automatically added if detected. - Dock widget positions, visibility, and floating state are still preserved in config. ------------------------------ Other Refactors & Improvements ------------------------------ - The miEye module has minor updates to accommodate the changes above. - Updated controller and device views to use the new signal and parameter system. - The controller signals now use the improved `Axis` enum type for move requests. -------------- Future Updates -------------- - Complete SmarAct MCS2 stage integration and testing. - Develop the Focus Stabilization alternative modalities. (Under Dev) - MacOS compatibility testing. - Acquisition Experiments Designer tools. - 3D Localization fitting from Experimental PSF. - Introduction of 2D/3D Single-Particle Tracking.
1 parent 128cc37 commit 981fb8a

32 files changed

Lines changed: 2422 additions & 922 deletions

File tree

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ The **`microEye`** is a Python toolkit for fluorescence microscopy that supports
77
This toolkit is compatible with the [hardware](#hardware) used in our microscope. For further details, refer to the [miEye microscope paper](https://doi.org/10.1016/j.ohx.2022.e00368) and [OSF project](http://doi.org/10.17605/osf.io/j2fqy).
88

99
```bash
10-
__ ____ ____ ___ ____ ___ ___
11-
/ |/ (_)__________ / __/_ _____ _ __|_ |/ / / / _ \___ _|_ |
12-
/ /|_/ / / __/ __/ _ \/ _// // / -_) | |/ / __//_ _// // / _ `/ __/
10+
__ ____ ____ ___ ____ ___ ____
11+
/ |/ (_)__________ / __/_ _____ _ __|_ |/ / / / _ \___ _|_ /
12+
/ /|_/ / / __/ __/ _ \/ _// // / -_) | |/ / __//_ _// // / _ `//_ <
1313
/_/ /_/_/\__/_/ \___/___/\_, /\__/ |___/____(_)_/(_)___/\_,_/____/
1414
/___/
15-
___ __ __ ___ __
16-
/ _ | / /__ / / ___ _ / _ \___ / /__ ___ ____ ___
17-
/ __ |/ / _ \/ _ \/ _ `/ / , _/ -_) / -_) _ `(_-</ -_)
18-
/_/ |_/_/ .__/_//_/\_,_/ /_/|_|\__/_/\__/\_,_/___/\__/
19-
/_/
15+
___ __ __
16+
/ _ | / /__ / / ___ _
17+
/ __ |/ / _ \/ _ \/ _ `/
18+
/_/ |_/_/ .__/_//_/\_,_/
19+
/_/
2020
```
2121

2222
![Package Health](https://snyk.io/advisor/python/microEye/badge.svg)

examples/miEye_scripts/AcquisitionLoopWithIntervals.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from microEye.hardware.cams import CameraList, Vimba_Panel
1+
from microEye.hardware.cams import CameraManager, Vimba_Panel
22
from microEye.qt import QtCore
33
from microEye.utils.thread_worker import QThreadWorker
44

5-
panel: Vimba_Panel = CameraList.CAMERAS['Vimba'][0]
5+
panel: Vimba_Panel = CameraManager.CAMERAS['Vimba'][0]
66

77
iterations = 5 # number of iterations
88
delay = 10 # delay in seconds

src/microEye/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
version_info = (2, 4, 0)
2-
__version__ = '.'.join(map(str, version_info)) + 'a2'
2+
__version__ = '.'.join(map(str, version_info)) + 'a3'

src/microEye/analysis/filters/spatial.py

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Optional
44

55
import cv2
6+
import numba as nb
67
import numpy as np
78
from scipy import signal
89
from scipy.fftpack import fft2, fftshift, ifft2, ifftshift
@@ -64,22 +65,8 @@ def gaussian_kernel(dim, sigma):
6465
return kernel
6566

6667
def run(self, image: np.ndarray) -> np.ndarray:
67-
rows, cols = image.shape
68-
nrows = cv2.getOptimalDFTSize(rows)
69-
ncols = cv2.getOptimalDFTSize(cols)
70-
pad_rows = nrows - rows
71-
pad_cols = ncols - cols
72-
73-
# Ensure that pad_cols[1] and pad_rows[1] are at least 1
74-
pad_rows = (pad_rows // 2, max(1, pad_rows - pad_rows // 2))
75-
pad_cols = (pad_cols // 2, max(1, pad_cols - pad_cols // 2))
76-
77-
nimg = np.pad(image, (pad_rows, pad_cols), mode='reflect')
78-
7968
res = cv2.normalize(
80-
signal.convolve2d(nimg, np.rot90(self.dog), mode='same')[
81-
pad_rows[0] : -pad_rows[1], pad_cols[0] : -pad_cols[1]
82-
],
69+
signal.fftconvolve(image, np.rot90(self.dog), mode='same'),
8370
None,
8471
0,
8572
255,
@@ -140,17 +127,15 @@ def gaussian_kernel(dim, sigma):
140127
return kernel
141128

142129
def run(self, image: np.ndarray) -> np.ndarray:
143-
point = signal.fftconvolve(image, np.rot90(self.point_kernel), mode='same')
144-
145-
return cv2.normalize(
146-
signal.fftconvolve(point, np.rot90(self.gauss), mode='same'),
147-
None,
148-
0,
149-
255,
150-
cv2.NORM_MINMAX,
151-
cv2.CV_8U,
130+
# precompute combined kernel if possible
131+
_combined_kernel = signal.convolve2d(
132+
np.rot90(self.gauss), np.rot90(self.point_kernel), mode='full'
152133
)
153134

135+
point = signal.fftconvolve(image, np.rot90(_combined_kernel), mode='same')
136+
137+
return cv2.normalize(point, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
138+
154139
def get_tree_parameters(self):
155140
'''Return the parameters for the pyqtgraph tree view.'''
156141
return {
@@ -171,6 +156,30 @@ def get_tree_parameters(self):
171156
}
172157

173158

159+
@nb.njit(parallel=True)
160+
def radial_coordinates_nb(shape):
161+
'''Generates a 2D array with radial cordinates
162+
with according to the first two axis of the
163+
supplied shape tuple
164+
165+
Returns
166+
-------
167+
R, Rsq
168+
Radius 2d matrix (R) and radius squared matrix (Rsq)
169+
'''
170+
R = np.zeros(shape, dtype=np.float32)
171+
Rsq = np.zeros(shape, dtype=np.float32)
172+
173+
center = (shape[0] // 2, shape[1] // 2)
174+
175+
for i in nb.prange(shape[0]):
176+
for j in range(shape[1]):
177+
R[i, j] = np.sqrt((i - center[0]) ** 2 + (j - center[1]) ** 2)
178+
Rsq[i, j] = R[i, j] ** 2
179+
180+
return R, Rsq
181+
182+
174183
class FourierFilter(SpatialFilter):
175184
class PROFILES(Enum):
176185
Gaussian = 1
@@ -241,14 +250,7 @@ def radial_coordinates(self, shape):
241250
R, Rsq
242251
Radius 2d matrix (R) and radius squared matrix (Rsq)
243252
'''
244-
y_len = np.arange(-shape[0] // 2, shape[0] // 2)
245-
x_len = np.arange(-shape[1] // 2, shape[1] // 2)
246-
247-
X, Y = np.meshgrid(x_len, y_len)
248-
249-
Rsq = X**2 + Y**2
250-
251-
self._radial_coordinates = (np.sqrt(Rsq), Rsq)
253+
self._radial_coordinates = radial_coordinates_nb(shape[:2])
252254

253255
return self._radial_coordinates
254256

src/microEye/hardware/cams/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from microEye.hardware.cams.camera_calibration import dark_calibration
2-
from microEye.hardware.cams.camera_list import CameraList
2+
from microEye.hardware.cams.camera_list import CameraList, CameraManager
33
from microEye.hardware.cams.camera_panel import Camera_Panel, CamParams
44
from microEye.hardware.cams.jobs import AcquisitionJob
55
from microEye.hardware.cams.linescan.IR_Cam import (

src/microEye/hardware/cams/basler/basler_cam.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,9 @@ def GrabOne(self) -> Optional[np.ndarray]:
994994
if res.GrabSucceeded():
995995
return res.GetArray()
996996

997+
def snap_image(self):
998+
return self.GrabOne()
999+
9971000
def RetrieveResult(self, timeout: int) -> Optional[pylon.GrabResult]:
9981001
'''Retrieve the result of the grabbing
9991002

src/microEye/hardware/cams/camera_list.py

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,12 @@
8080
},
8181
}
8282

83-
class CameraList(QtWidgets.QWidget):
83+
# singleton camera manager class
84+
class CameraManager(QtCore.QObject):
8485
'''
85-
A widget for displaying and managing a list of cameras.
86+
A singleton class to manage camera instances.
8687
'''
87-
88-
cameraAdded = Signal(Camera_Panel, bool)
89-
cameraRemoved = Signal(Camera_Panel, bool)
88+
_instance = None
9089

9190
CAMERAS : dict[str, list[dict]] = {
9291
'Basler': [],
@@ -97,6 +96,62 @@ class CameraList(QtWidgets.QWidget):
9796
'Vimba': [],
9897
}
9998

99+
def __new__(cls, *args, **kwargs):
100+
if not cls._instance:
101+
cls._instance = super().__new__(cls, *args, **kwargs)
102+
return cls._instance
103+
104+
@classmethod
105+
def instance(cls):
106+
if cls._instance is None:
107+
return CameraManager()
108+
109+
return cls._instance
110+
111+
def __init__(self):
112+
super().__init__()
113+
114+
self.cam_list = None
115+
self.cached_autofocusCam = None
116+
117+
@property
118+
def autofocusCam(self) -> typing.Union[Camera_Panel, None]:
119+
'''
120+
Get the autofocus camera panel.
121+
122+
Returns
123+
-------
124+
Camera_Panel | None
125+
The autofocus camera panel, or None if no autofocus camera is available.
126+
'''
127+
if self.cached_autofocusCam is None:
128+
self.cached_autofocusCam = next(
129+
(
130+
cam['Panel']
131+
for _, cam_list in CameraManager.CAMERAS.items()
132+
for cam in cam_list
133+
if cam['IR']
134+
),
135+
None,
136+
)
137+
return self.cached_autofocusCam
138+
139+
@classmethod
140+
def snap_ir_image(cls):
141+
'''
142+
Snap an image on the IR camera if available.
143+
'''
144+
if cls.instance().autofocusCam is not None:
145+
return cls.instance().autofocusCam.cam.snap_image()
146+
147+
class CameraList(QtWidgets.QWidget):
148+
'''
149+
A widget for displaying and managing a list of cameras.
150+
'''
151+
152+
cameraAdded = Signal(Camera_Panel, bool)
153+
cameraRemoved = Signal(Camera_Panel, bool)
154+
100155
def __init__(self, parent: typing.Optional['QtWidgets.QWidget'] = None):
101156
'''
102157
Initialize the camera list widget.
@@ -108,9 +163,10 @@ def __init__(self, parent: typing.Optional['QtWidgets.QWidget'] = None):
108163
'''
109164
super().__init__(parent=parent)
110165

166+
self.__camera_manager = CameraManager.instance()
167+
111168
self.cam_list = None
112169
self.item_model = QtGui.QStandardItemModel()
113-
self.cached_autofocusCam = None
114170

115171
# Layout
116172
self.InitLayout()
@@ -172,17 +228,7 @@ def autofocusCam(self) -> typing.Union[Camera_Panel, None]:
172228
Camera_Panel | None
173229
The autofocus camera panel, or None if no autofocus camera is available.
174230
'''
175-
if self.cached_autofocusCam is None:
176-
self.cached_autofocusCam = next(
177-
(
178-
cam['Panel']
179-
for _, cam_list in CameraList.CAMERAS.items()
180-
for cam in cam_list
181-
if cam['IR']
182-
),
183-
None,
184-
)
185-
return self.cached_autofocusCam
231+
return self.__camera_manager.autofocusCam
186232

187233
def add_camera_clicked(self):
188234
'''
@@ -244,7 +290,6 @@ def add_camera(self, cam, mini=False):
244290
else:
245291
self._display_warning_message('Device is in use or already added.')
246292

247-
248293
def _add_camera_generic(self, cam, mini, config):
249294
'''
250295
Generic camera/panel adder.
@@ -277,7 +322,7 @@ def _add_camera_generic(self, cam, mini, config):
277322
panel : Camera_Panel = panel_class(*args)
278323

279324
# Add to CAMERAS dict
280-
CameraList.CAMERAS[driver].append(
325+
CameraManager.CAMERAS[driver].append(
281326
{'Camera': panel.cam, 'Panel': panel, 'IR': mini, 'Info': cam}
282327
)
283328
return panel
@@ -344,7 +389,7 @@ def remove_camera(self, cam):
344389
cam : dict
345390
The camera information dictionary.
346391
'''
347-
cams: list[dict] = CameraList.CAMERAS.get(cam['Driver'], [])
392+
cams: list[dict] = CameraManager.CAMERAS.get(cam['Driver'], [])
348393
if cams:
349394
for item in cams:
350395
pan: Camera_Panel = item['Panel']
@@ -377,7 +422,7 @@ def removeAllCameras(self):
377422
Remove all cameras.
378423
Stops any active acquisitions and properly disposes of camera resources.
379424
'''
380-
for _, camera_list in self.CAMERAS.items():
425+
for _, camera_list in CameraManager.CAMERAS.items():
381426
# Create a copy of the list since we'll be modifying it during iteration
382427
for camera_info in camera_list[:]:
383428
panel: Camera_Panel = camera_info['Panel']
@@ -485,22 +530,22 @@ def update_list(self):
485530
self.cam_table.setModel(self.item_model)
486531

487532
# Update the cached_autofocusCam value
488-
self.cached_autofocusCam = None
533+
self.__camera_manager.cached_autofocusCam = None
489534

490535
@classmethod
491536
def update_gui(cls):
492537
'''
493538
Update the GUI of all added cameras.
494539
'''
495-
for _, cam_list in cls.CAMERAS.items():
540+
for _, cam_list in CameraManager.CAMERAS.items():
496541
for cam in cam_list:
497542
cam['Panel'].updateInfo()
498543

499544
def snap_image(self):
500545
'''
501546
Snap an image on all non IR cameras.
502547
'''
503-
for _, cam_list in CameraList.CAMERAS.items():
548+
for _, cam_list in CameraManager.CAMERAS.items():
504549
for cam in cam_list:
505550
if not cam['IR']:
506551
cam['Panel'].capture_image()
@@ -518,7 +563,7 @@ def get_config(self):
518563
'''
519564
config : list[dict] = []
520565

521-
for _, cam_list in CameraList.CAMERAS.items():
566+
for _, cam_list in CameraManager.CAMERAS.items():
522567
for cam in cam_list:
523568
cam_config : dict = cam['Info'].copy()
524569
cam_config['IR'] = cam['IR']

src/microEye/hardware/cams/dummy/dummy_panel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def cam_capture(self, *args, **kwargs):
133133

134134
self._buffer.put(
135135
self._cam.get_dummy_image_from_pattern(
136-
self.acq_job.frames_captured / 100
136+
cycle_time * 1e-9
137137
).tobytes()
138138
)
139139
# add sensor temperature to the stack

0 commit comments

Comments
 (0)