Skip to content

Commit 4caa8aa

Browse files
committed
microEye v2.4.0a7
___ __ __ / _ | / /__ / / ___ _ / __ |/ / _ \/ _ \/ _ `/ /_/ |_/_/ .__/_//_/\_,_/ /_/ What's New: ------------------------------------------------------------- IMPORTANT: Zarr V3 support now requires Python 3.11 or newer. Support for Python 3.9 and 3.10 has been discontinued. ------------------------------------------------------------- ----------------- microEye v2.4.0a7 ----------------- - Camera Infrastructure - `basler_panel.py`: adjust `RetrieveResult` timeout dynamically with exposure to prevent premature timeouts at long exposures. - `jobs.py`: guard dark-calibration path with try/except logging and ensure `finalize()` only writes calibration files when at least two frames were captured. - `camera_calibration.py`: rewrite dark calibration to use Welford's online mean/variance (Numba-accelerated), emit sample variance, and write TIFFs via a shared helper with logging. - Lasers / Device Manager - `io_params.py`, `io_matchbox.py`, `io_single_laser.py`: ensure UI parameters use `LaserState.value` strings, convert parameter-tree limits to primitive types, and fix enum handling when toggling combiner channels. - `devices_manager.py`: centralize relay-command emission via `_send_laser_relay_settings`, expose `_fetch_laser_states()` for UI consumers. - `controller.py` + new `indicator.py` & `color.py`: add color-coded laser state/relay indicators to the Controller dock, including reusable `LaserIndicator` widgets driven by wavelength-aware colors. - `miEye.py`: update status-bar timer to refresh the new laser indicators by pulling `_fetch_laser_states()` from the device manager. ----------------- microEye v2.4.0a6 ----------------- - Docs & Packaging - `README.md`: refreshed ASCII art and fixed formatting; tightened Python requirement to >=3.11.9; expanded Allied Vision/Vimba X guidance with CTI auto-discovery and VIMBA_X_HOME / VIMBA_X_CTI instructions (plus note about Vimba TL conflicts and VmbC.xml); clarified IDS driver options (IDS Peak preferred vs IDS Software Suite) and installer notes; improved camera/hardware package table and CLI/launcher usage example. - `requirements.txt`: added IDS Peak runtime. - Logging: - Replaced ad-hoc print statements with structured, package-wide logging using the standard library logger. - Loggers are module-scoped (logging.getLogger(__name__)) and use consistent message formats and levels (DEBUG/INFO/WARNING/ERROR). - Runtime configuration: - Launcher UI exposes log options (log level, disable file logging, disable console logging). - CLI supports --log-level, --no-log-file and --no-log-console to control logging behavior. - Environment variable MICROEYE_LOG_LEVEL may be used to override default level. - Default behavior: - Logs written to a per-run file under ./logs when enabled and to console unless disabled. - File handlers and console handlers are added/removed based on CLI/launcher settings. - Error handling: - Exceptions logged with exc_info=True to preserve tracebacks. - Notes: - Logging replaces many print() calls across camera drivers, acquisition, analysis and GUI modules to improve debuggability and avoid cluttering stdout. - Core / CLI - `microEye.__init__`: - add a `_select_qt_api(cli_choice)` helper to pick the Qt binding from CLI/env and set QT_API/PYQTGRAPH_QT_LIB before Qt or camera DLLs are imported; - add CLI logging flags (`--log-level`, `--no-log-file`, `--no-log-console`) and a small logger bootstrap. - `microEye.qt` now respects the preselected API instead of probing bindings at runtime. - Analysis / GPU - `pyfit3Dcspline` + `mainfunctions.py`: guard GPU imports with both CUDA availability and actual device presence to avoid crashes on CPU-only hosts. - Camera Infrastructure - `hardware/cams/camera_panel.py`: normalize exposure scaling, emit pixel-size metadata, and expand `Camera_Panel.getAcquisitionJob` with explicit save toggles, metadata hooks, and queue events (paired with `hardware/cams/jobs.py`). - `camera_list`: register IDS Peak adapters and panels. - Basler / Vimba / IDS / PCO / Thorlabs panels: tag freerun actions with event hooks and reuse base exposure math (see `Basler_Panel`). - New IDS Peak stack: `PeakCamera`, `PeakPanel`, and package init wiring; supports binning, flash, ROI and acquisition flow. - Vimba stack - `hardware/cams/vimba` (vimba_cam, vimba_panel): add CTI auto-discovery (`VIMBA_X_HOME` / `VIMBA_X_CTI`), black level / gain / correction / binning controls, and timer helpers. - Acquisition / UI - `focusWidget`: replace background-worker range updates with a re-entrancy guard for synchronous plot refreshes to avoid overlap. - `ScanAcquisitionWidget`: more robust header resize logic for older Qt versions. - Protocols / Designer - `actions`: introduce `ListLoop`; supporting UI/editor wiring added across `actions_items`, designer, and serialization. - App Shell - `miEye_module`: defer heavy initialization via `_bootstrap` to keep startup responsive and ensure clean shutdown hooks. ----------------- microEye v2.4.0a5 ----------------- - Focus Widget: - Previously, performance issues included GUI freezes caused by: - Expensive autorange operations and reliance on `pg.GraphicsLayoutWidget` for all plots. - Non-optimized plot update logic in `focusWidget` and `PyQtGraphDisplay` caused overlapping calls and race conditions. - The issue appears when acquisition display runs at high framerates (>30Hz) with one or more ROIs. - Issue has been resolved and tested with acquisition camera running at ~165Hz and focus plots and GUI were responsive. - Refactor `focusWidget` to use separate `pg.GraphicsView` and `pg.PlotWidget` for all plots instead of `GraphicsLayoutWidget`. - Implemented non-pyqtgraph-native logic to update plot ranges at less frequent intervals, reducing resource usage and improving responsiveness. - Added a boolean option in the GUI to enable or disable the non-pyqtgraph-native plot range updates. - Add async worker for plot updates to avoid GUI blocking. - Remove use of `HistogramBarItem` for histogram plots, as it is computationally heavy; instead, use histogram step plotting (drawing the histogram as a stepped line plot rather than bars) on a regular `pg.PlotWidget` for improved performance. - Fix ROI update logic after loading config; previously the GUI did not update accordingly. - Update plots visibility dynamically depending on the selected stabilization method: - For example, when reflection "Z-only" stabilization method is selected, only the Z histogram and Z time log plots are shown. - When beads or Hybrid "XYZ" stabilization methos is selected, XY scatter and X/Y time log plots are also displayed. - The GUI logic automatically hides or shows relevant plots based on the current stabilization mode. - Display & Viewer: - Refactor `FrameCounter` to use monotonic nanosecond clock for FPS calculation (previously used `time.time()`). - Fix `PyQtGraphDisplay` update logic reducing resource usage and improving GUI responsiveness: - Refactor histogram and CDF updates to occur at less frequent intervals. - Add a flag in the `PyQtGraphDisplay` class to block overlapping update calls and prevent race conditions during rapid image updates. - Set `pg.GraphicsView(useOpenGL=True)` for improved rendering performance. - Stage Adapters: (BUG) - Fix `PiezoConceptFOC` stage adapter to always cast Z position to integer before sending commands, preventing random jumps caused by `float`/`int` type mismatch. - Device Manager: (BUG) - Fix `FocusStabilizer` signals to use `moveXYZ` (signal) and `StageManager.move_relative` (function), replacing legacy `moveStage` (signal) and `move_z` (function). - Focus Stabilizer & Controller: - Fix controller attributes to use single underscore; double underscore caused them to be inaccessible to subclasses. - Add missing tau parameter to FocusStabilizer config and ensure it is loaded/saved. (reserved to be used later) - Fix loading of per-axis inverted flags in `FocusStabilizerView`. - Refactor `FocusStabilizer` worker logic to use correct per-axis stabilization and response inversion emitted through `moveXYZ`. - Fix method and rejection method enum handling in `FocusStabilizerView`. - Fix ROI activation signal emission after config load. - Add __str__ method to RejectionMethod enum. - Other: - Update GitHub Actions workflow to use `Python 3.11.9` ----------------- microEye v2.4.0a4 ----------------- - Images & Data IO - refactor: Move all image sequence and uImage classes from `microEye.utils.uImage` to new `microEye.images` subpackage (`handlers.py`, `uImage.py`, `zarr.py`) - refactor: Update all imports to use `microEye.images` instead of `microEye.utils.uImage` - feat: Add unified `ImageSequenceBase`, `TiffSeqHandler`, `ZarrImageSequence`, and `saveZarrImage` to microEye.images.handlers - feat: Add `ZarrAcquisition`, `create_array`, and `store_zarr_array` for streaming and saving Zarr V3 data - feat: Added `close_display(display_name)` method to `DisplayManager` to allow closing a specific display window by name - fix: Ensure Zarr handler support both zipped and directory stores - Analysis & Viewer - refactor: Update all analysis modules to use new image handler imports - fix: Remove legacy `microEye.utils.uImage` import from all modules - feat: Add registration tool (`registration.py`) for image stack registration and transformation - feat: Add multi-channel stack merging and transformation export/import in registration tool - fix: Fixed channel display logic for multichannel data in `images.py` so channels are handled correctly in viewer. - Multi Viewer Module - feat: Add "Opened Files" dock to `multi_viewer`, showing all currently opened files and allowing quick switching - feat: Add "Registration Tool" to Tools menu in `multi_viewer` for stack alignment - fix: Properly handle .zarr files in file system model and viewer - fix: Ensure opened files are tracked and closed correctly in `multi_viewer` - Fitting & Filters - fix: Update all fitting and filter modules to use new image handler imports - Hardware & Acquisition - Thorlabs Camera: - Fixed bugs in exposure range and pixel clock list handling. - Corrected driver string and status dictionary logic. - AcquisitionJob: - Refactored ROI extraction and frame writing logic for TIFF/Zarr. - Improved resource cleanup and metadata saving. - Ensured correct handling of Zarr array finalization and TIFF writer logic. - fix: Update all camera panels and acquisition logic to use new image handler imports - fix: Remove legacy `microEye.utils.uImage` import from hardware modules ------------------------------------ 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 1f8fde9 commit 4caa8aa

15 files changed

Lines changed: 368 additions & 92 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ 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-
/ /|_/ / / __/ __/ _ \/ _// // / -_) | |/ / __//_ _// // / _ `/ _ \
13-
/_/ /_/_/\__/_/ \___/___/\_, /\__/ |___/____(_)_/(_)___/\_,_/\___/
10+
__ ____ ____ ___ ____ ___ ____
11+
/ |/ (_)__________ / __/_ _____ _ __|_ |/ / / / _ \___ /_ /
12+
/ /|_/ / / __/ __/ _ \/ _// // / -_) | |/ / __//_ _// // / _ `// /
13+
/_/ /_/_/\__/_/ \___/___/\_, /\__/ |___/____(_)_/(_)___/\_,_//_/
1414
/___/
1515
___ __ __
1616
/ _ | / /__ / / ___ _

src/microEye/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,6 @@ def getArgs():
7070
_select_qt_api(qt_api)
7171

7272
os.environ.setdefault('MITHEME', args.theme or 'qdarktheme')
73-
os.environ.setdefault('PYDEVD_USE_FRAME_EVAL', 'NO')
74-
os.environ.setdefault('PYTHONIOENCODING', 'UTF-8')
75-
os.environ.setdefault('PYTHONUNBUFFERED', '1')
76-
os.environ.setdefault('DEBUGPY_RUNNING', 'true')
7773

7874
configure_logger(args)
7975

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)) + 'a6'
2+
__version__ = '.'.join(map(str, version_info)) + 'a7'

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,9 @@ def cam_capture(self, *args, **kwargs):
326326
cycle_time = perf_counter_ns()
327327
self.acq_job.capture_time = (cycle_time - self.time) / 1e6
328328
self.time = cycle_time
329-
with self.cam.cam.RetrieveResult(100) as res:
329+
with self.cam.cam.RetrieveResult(
330+
100 + int(self.cam.exposure_current / self.FACTOR)
331+
) as res:
330332
if res.GrabSucceeded():
331333
self._buffer.put(res.Array.copy())
332334
# add sensor temperature to the stack
Lines changed: 76 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1+
import logging
12
import os
23

4+
import numba as nb
35
import numpy as np
46
import tifffile as tf
57

68

9+
@nb.njit(parallel=True)
10+
def update(count, mean, M2, new_value):
11+
count += 1
12+
delta = new_value - mean
13+
mean += delta / count
14+
delta2 = new_value - mean
15+
M2 += delta * delta2
16+
return (count, mean, M2)
17+
718
class dark_calibration:
819
'''A class holder for pixelwise average and variance.
920
@@ -14,14 +25,22 @@ class dark_calibration:
1425
ACCENT (ImageJ/Fiji Plugin):
1526
https://github.com/ries-lab/Accent
1627
'''
28+
1729
def __init__(self, shape, exposure):
18-
self._counter = 0
30+
self._count = 0
1931
self._exposure = exposure
2032
self._shape = shape
21-
self._accumulator = np.zeros(
22-
shape=shape, dtype=np.float64)
23-
self._quad_accum = np.zeros(
24-
shape=shape, dtype=np.float64)
33+
self._mean = np.zeros(shape=shape, dtype=np.float64)
34+
self._M2 = np.zeros(shape=shape, dtype=np.float64)
35+
36+
# Retrieve the mean, variance and sample variance from an aggregate
37+
def finalize(existing_aggregate):
38+
(count, mean, M2) = existing_aggregate
39+
if count < 2:
40+
return float('nan')
41+
else:
42+
(mean, variance, sample_variance) = (mean, M2 / count, M2 / (count - 1))
43+
return (mean, variance, sample_variance)
2544

2645
def addFrame(self, image: np.ndarray):
2746
'''Adds an image frame to the mean/variance estimators.
@@ -39,61 +58,73 @@ def addFrame(self, image: np.ndarray):
3958
if image.shape != self._shape:
4059
raise ValueError('Image of wrong shape.')
4160

42-
self._accumulator += image
43-
self._quad_accum += np.square(image)
44-
self._counter += 1
61+
frame = np.asarray(image, dtype=np.float64, order='C')
62+
63+
self._count, self._mean, self._M2 = update(
64+
self._count,
65+
self._mean,
66+
self._M2,
67+
frame,
68+
)
4569

4670
def getResults(self):
4771
'''Gets the resulting mean and variance frames.
4872
4973
Returns
5074
-------
51-
tuple(ndarray, ndarray)
52-
the rasults (mean, variance)
75+
tuple(ndarray, ndarray, ndarray)
76+
the rasults (mean, variance, sample_variance).
5377
5478
Raises
5579
------
5680
ValueError
5781
in case zero frames are added.
5882
'''
59-
if self._counter < 1:
60-
raise ValueError('Counter should be non-zero.')
83+
if self._count < 2:
84+
raise ValueError('At least two frames are required.')
6185

62-
mean = self._accumulator / self._counter
63-
variance = (
64-
self._quad_accum -
65-
np.square(self._accumulator)/self._counter)/(self._counter - 1)
86+
(mean, variance, sample_variance) = (
87+
self._mean,
88+
self._M2 / self._count,
89+
self._M2 / (self._count - 1),
90+
)
6691

67-
return mean, variance
92+
return mean, variance, sample_variance
6893

69-
def saveResults(self, path, prefix):
94+
def saveResults(self, path: str, prefix: str):
7095
if not os.path.exists(path):
7196
os.makedirs(path)
7297

73-
def getFilename(index: int):
74-
return path + \
75-
f'\\image_{index:05d}.ome.tif'
76-
77-
mean, variance = self.getResults()
78-
79-
with tf.TiffWriter(
80-
path + prefix + \
81-
f'_image_mean_{self._exposure:.5f}_ms'.replace('.', '_') + \
82-
'.ome.tif',
83-
append=False,
84-
bigtiff=False,
85-
ome=False) as writer:
86-
writer.write(
87-
data=mean,
88-
photometric='minisblack')
89-
90-
with tf.TiffWriter(
91-
path + prefix + \
92-
f'_image_var_{self._exposure:.5f}_ms'.replace('.', '_') + \
93-
'.ome.tif',
94-
append=False,
95-
bigtiff=False,
96-
ome=False) as writer:
97-
writer.write(
98-
data=variance,
99-
photometric='minisblack')
98+
def getFilename(name: str):
99+
return (
100+
path
101+
+ prefix
102+
+ f'_image_{name}_{self._exposure:.5f}_ms'.replace('.', '_')
103+
+ '.ome.tif'
104+
)
105+
106+
mean, variance, sample_variance = self.getResults()
107+
108+
self._write_tiff(
109+
getFilename('mean'),
110+
mean,
111+
)
112+
113+
self._write_tiff(
114+
getFilename('var'),
115+
variance,
116+
)
117+
118+
self._write_tiff(
119+
getFilename('samplevar'),
120+
sample_variance,
121+
)
122+
123+
def _write_tiff(self, filename, data):
124+
try:
125+
with tf.TiffWriter(
126+
filename, append=False, bigtiff=False, ome=False
127+
) as writer:
128+
writer.write(data=data, photometric='minisblack')
129+
except Exception as e:
130+
logging.getLogger(__name__).error(f'Error writing tiff file: {e}')

src/microEye/hardware/cams/jobs.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import logging
23
import os
34
import threading
45
import time
@@ -287,10 +288,13 @@ def addDarkFrame(self, frame: np.ndarray):
287288
frame : np.ndarray
288289
Dark frame data.
289290
'''
290-
if self.is_dark_cal:
291-
if self.dark_cal is None:
292-
self.dark_cal = dark_calibration(frame.shape, self.exposure)
293-
self.dark_cal.addFrame(frame)
291+
try:
292+
if self.is_dark_cal:
293+
if self.dark_cal is None:
294+
self.dark_cal = dark_calibration(frame.shape, self.exposure)
295+
self.dark_cal.addFrame(frame)
296+
except Exception as e:
297+
logging.getLogger(__name__).error(f'Error adding dark frame.', exc_info=e)
294298

295299
def _extract_roi_data(
296300
self, frame: np.ndarray, roi: tuple, flip: bool = False
@@ -482,7 +486,7 @@ def saveMetadata(self, roi_index: int = None):
482486
def finalize(self):
483487
'''Finalize and clean up resources.'''
484488
if self.dark_cal is not None:
485-
if self.dark_cal._counter > 1:
489+
if self.dark_cal._count > 1:
486490
self.dark_cal.saveResults(self.path, f'{self.major:02d}_{self.prefix}')
487491

488492
if self.zarr and self.zarr_array is not None:

src/microEye/hardware/lasers/io_matchbox.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ def laser_state_changed(self, param: Parameter, value):
446446
object : [QRadioButton]
447447
the radio button toggled
448448
'''
449-
self.state = value
449+
self.state = LaserState(value)
450450
if self.state == LaserState.OFF:
451451
self.Laser.SetDisabled(self.index)
452452
else:
@@ -463,7 +463,7 @@ def add_params(self, parent: Tree):
463463
{
464464
'name': self.STATE.split('.')[1],
465465
'type': 'list',
466-
'value': LaserState.OFF,
466+
'value': LaserState.OFF.value,
467467
'limits': LaserState.get_list(),
468468
},
469469
{

src/microEye/hardware/lasers/io_params.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ def __str__(self):
1111
return self.value.upper()
1212

1313
@staticmethod
14-
def get_list() -> list:
15-
return [member for member in LaserState]
14+
def get_list() -> list[str]:
15+
return [member.value for member in LaserState]
1616

1717
@staticmethod
1818
def get_enum(value: str) -> 'LaserState':

src/microEye/hardware/lasers/io_single_laser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ def create_parameters(self):
513513
{
514514
'name': str(MB_Params.STATE),
515515
'type': 'list',
516-
'value': LaserState.OFF,
516+
'value': LaserState.OFF.value,
517517
'limits': LaserState.get_list(),
518518
},
519519
{
@@ -804,7 +804,7 @@ def laser_state_changed(self):
804804
'''Sends enable/disable signals to the
805805
laser combiner according to selected state setting
806806
'''
807-
if self.get_param_value(MB_Params.STATE) == LaserState.OFF:
807+
if self.get_param_value(MB_Params.STATE) == LaserState.OFF.value:
808808
self.Laser.SendCommand(io_single_laser.ON_DIS)
809809
else:
810810
self.Laser.SendCommand(io_single_laser.ON_EN)

src/microEye/hardware/mieye/devices_manager.py

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from microEye.hardware.cams.linescan.IR_Cam import DemoLineScanner, ParallaxLineScanner
1111

1212
# lasers
13-
from microEye.hardware.lasers.io_matchbox import CombinerLaserWidget
13+
from microEye.hardware.lasers.io_matchbox import CombinerLaserWidget, MB_Params
1414
from microEye.hardware.lasers.io_single_laser import SingleMatchBox
1515
from microEye.hardware.lasers.laser_relay import LaserRelayController
1616
from microEye.hardware.protocols.actions import WeakObjects
@@ -103,9 +103,7 @@ def _init_ir_cam(self):
103103

104104
def _init_laser_relay(self):
105105
self.laser_relay = LaserRelayController()
106-
self.laser_relay.sendCommandActivated.connect(
107-
lambda: self.laser_relay.sendCommand(self.laser_relay_settings())
108-
)
106+
self.laser_relay.sendCommandActivated.connect(self._send_laser_relay_settings)
109107
DeviceManager.WIDGETS[DEVICES.LASER_RELAY] = self.laser_relay.view
110108
WeakObjects.addObject(self.laser_relay)
111109
self.widgetAdded.emit(DEVICES.LASER_RELAY, self.laser_relay.view)
@@ -139,7 +137,9 @@ def _init_hid_controller(self):
139137
self.widgetAdded.emit(DEVICES.HID_CONTROLLER, self.hid_controller)
140138

141139
def _init_focus_stabilizer(self):
142-
FocusStabilizer.instance().moveXYZ.connect(StageManager.instance().move_relative)
140+
FocusStabilizer.instance().moveXYZ.connect(
141+
StageManager.instance().move_relative
142+
)
143143
FocusStabilizer.instance().startWorker()
144144

145145
self.focus = focusWidget()
@@ -155,9 +155,7 @@ def update_gui(self):
155155
self.elliptecView.updateHighlight()
156156
self.laser_relay.updatePortState()
157157
self.laser_relay.refreshPorts()
158-
self.laser_relay.updateHighlight(
159-
self.laser_relay_settings()
160-
)
158+
self.laser_relay.updateHighlight(self._laser_relay_settings())
161159

162160
def stopRequest(self, axis: Axis):
163161
StageManager.instance().stop(axis)
@@ -271,6 +269,42 @@ def _laser_removed(self, widget: QtWidgets.QWidget):
271269
self.lasers.remove(widget)
272270
self.widgetRemoved.emit(DEVICES.LASER, widget)
273271

272+
def _fetch_laser_states(self) -> dict:
273+
'''
274+
Returns the laser configurations.
275+
'''
276+
relay_config = self.laser_relay.lastCommand
277+
laser_config = {}
278+
279+
def update_config(wl: int, state: str) -> dict:
280+
if wl <= 0:
281+
return
282+
283+
config: dict = laser_config.get(wl, {})
284+
285+
laser = config.get('state', False) or state != 'OFF'
286+
287+
match = re.search(r'L(\d+)(ON|F1|F2)', relay_config)
288+
relay = (
289+
config.get('relay', False)
290+
or (match is not None and int(match.group(1)) == wl)
291+
)
292+
config.update({'state': laser, 'relay': relay})
293+
laser_config[wl] = config
294+
295+
for panel in self.lasers:
296+
if isinstance(panel, SingleMatchBox):
297+
wl = int(panel.wavelength)
298+
state = panel.get_param_value(MB_Params.STATE)
299+
update_config(wl, state)
300+
elif isinstance(panel, CombinerLaserWidget):
301+
for switch in panel._laserSwitches:
302+
wl = int(switch.wavelength)
303+
state = switch.state.value
304+
update_config(wl, state)
305+
306+
return laser_config
307+
274308
def _add_camera(self, widget: Camera_Panel, ir: bool):
275309
if ir:
276310
widget._frames = FocusStabilizer.instance().buffer
@@ -346,7 +380,7 @@ def _set_stage(self, key: str):
346380
WeakObjects.addObject(view)
347381
self.widgetAdded.emit(DEVICES.STAGE, view)
348382

349-
def laser_relay_settings(self):
383+
def _laser_relay_settings(self):
350384
'''Returns the RelayBox setting command.
351385
352386
Returns
@@ -359,6 +393,10 @@ def laser_relay_settings(self):
359393
config += panel.GetRelayState()
360394
return self.laser_relay.getCommand(config)
361395

396+
def _send_laser_relay_settings(self):
397+
'''Sends the RelayBox setting command.'''
398+
self.laser_relay.sendCommand(self._laser_relay_settings())
399+
362400
def _get_laser_configs(self) -> dict:
363401
'''
364402
Returns the laser configurations.

0 commit comments

Comments
 (0)