Source code for mesoSPIM.src.mesoSPIM_Optimizer

'''
Frontend of the optimizer which allows to use auto-focus or optimization of other microscope parameters
Auto-focus is based on Autopilot paper (Royer at al, Nat Biotechnol. 2016 Dec;34(12):1267-1278. doi: 10.1038/nbt.3708.)
author: Nikita Vladimirov, @nvladimus, 2021
License: GPL-3
'''

import time
import numpy as np
from .utils.optimization import shannon_dct, fit_gaussian_1d, gaussian_1d
import pyqtgraph as pg

import logging
logger = logging.getLogger(__name__)

from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.uic import loadUi


[docs] class mesoSPIM_Optimizer(QtWidgets.QWidget): sig_state_request = QtCore.pyqtSignal(dict) sig_move_absolute = QtCore.pyqtSignal(dict) def __init__(self, parent=None): '''Parent must be an mesoSPIM_MainWindow() object''' super().__init__() self.parent = parent # the mesoSPIM_MainWindow() object self.core = parent.core self.cfg = parent.cfg # initial config file self.state = self.parent.state # the mesoSPIM_StateSingleton() object self.results_window = self.graphics_widget = None self.modes_list = ['etl_offset', 'etl_amp', 'focus'] self.mode = self.modes_list[0] self.n_points = 5 self.search_amplitude = 0.5 self.state_key = None self.image = self.roi = self.roi_dims = None self.ini_state = self.ini_metric = self.new_state = self.min_value = self.max_value = None self.search_grid = self.metric_array = self.fit_grid = self.gaussian_values = None self.delay_s = 0.4 # time delay between snaps to avoid state update hickups, esp for heavy lens-camera assembly during AF loadUi('gui/mesoSPIM_Optimizer.ui', self) self.setWindowTitle('mesoSPIM-Optimizer') self.show() # initialize #self.set_parameters({'mode': 'etl_offset', 'amplitude': 0.5, 'n_points': 7}) # signal switchboard self.core.camera_worker.sig_camera_frame.connect(self.update_image) self.runButton.clicked.connect(self.run_optimization) self.closeButton.clicked.connect(self.close_window) self.parent.camera_window.sig_update_roi.connect(self.get_roi_dims) self.sig_move_absolute.connect(self.core.move_absolute) self.comboBoxMode.currentTextChanged.connect(self.set_mode_from_gui) @QtCore.pyqtSlot() def update_image(self): logger.debug(f"Optimizer: updating image, len(self.parent.core.frame_queue_display) = {len(self.parent.core.frame_queue_display)}") self.image = self.parent.core.frame_queue_display[0] self.roi = self.image[self.roi_dims[1]:self.roi_dims[1] + self.roi_dims[3], self.roi_dims[0]:self.roi_dims[0] + self.roi_dims[2]] @QtCore.pyqtSlot(tuple) def get_roi_dims(self, roi_dims): self.roi_dims = np.array(roi_dims).clip(min=0).astype(int)
[docs] def set_roi(self, orientation='h', roi_perc=0.25): img_w, img_h = self.parent.camera_window.get_image_shape() if orientation == 'h': self.parent.camera_window.set_roi('box', (0, img_h*(1-roi_perc)//2, img_w, int(img_h*roi_perc))) elif orientation == 'v': self.parent.camera_window.set_roi('box', (img_w*(1-roi_perc)//2, 0, int(img_w*roi_perc), img_h)) elif orientation == 'c': self.parent.camera_window.set_roi('box', (img_h * (1 - roi_perc) // 2, (img_w * (1 - roi_perc)) // 2, int(img_h * roi_perc), int(img_w * roi_perc))) elif orientation is None: self.parent.camera_window.set_roi(None, (0, 0, img_w, img_h)) else: raise ValueError("Orientation must be one of ('h', 'v', None).")
[docs] def set_parameters(self, param_dict=None, update_gui=True): if param_dict: if 'mode' in param_dict.keys(): self.mode = param_dict['mode'] if 'amplitude' in param_dict.keys(): self.search_amplitude = param_dict['amplitude'] if 'n_points' in param_dict.keys(): self.n_points = param_dict['n_points'] if self.mode == 'focus': self.state_key = 'position' elif self.mode == 'etl_offset': assert self.state['shutterconfig'] in ('Left', 'Right'), f"Shutter config must be in ('Left', 'Right'), got {self.state['shutterconfig']}" self.state_key = 'etl_l_offset' if self.state['shutterconfig'] == 'Left' else 'etl_r_offset' elif self.mode == 'etl_amp': ini_etl_amp = 0.1 # so that we never start from zero self.state_key = 'etl_l_amplitude' if self.state['shutterconfig'] == 'Left' else 'etl_r_amplitude' if self.state[self.state_key] == 0: self.core.sig_state_request.emit({self.state_key: ini_etl_amp}) print(f"Initial ETL amp set to {ini_etl_amp}") if update_gui: self.update_gui()
[docs] def update_gui(self): if self.mode == 'focus': self.searchAmpDoubleSpinBox.setSuffix(" \u03BCm") self.searchAmpDoubleSpinBox.setDecimals(0) self.set_roi('c') else: self.searchAmpDoubleSpinBox.setSuffix(" V") self.searchAmpDoubleSpinBox.setDecimals(3) self.set_roi('v') if self.mode == 'etl_offset' else self.set_roi('h') mode_index = self.modes_list.index(self.mode) if mode_index != self.comboBoxMode.currentIndex(): self.comboBoxMode.setCurrentIndex(mode_index) if self.search_amplitude != self.searchAmpDoubleSpinBox.value(): self.searchAmpDoubleSpinBox.setValue(self.search_amplitude) if self.n_points != self.nPointsSpinBox.value(): self.nPointsSpinBox.setValue(self.n_points)
@QtCore.pyqtSlot(str) def set_mode_from_gui(self, choice): if choice == "ETL offset": self.set_parameters({'mode': 'etl_offset', 'amplitude': 0.2, 'n_points': 7}) elif choice == "ETL amplitude": self.set_parameters({'mode': 'etl_amp', 'amplitude': 0.1, 'n_points': 7}) elif choice == "Focus": self.set_parameters({'mode': 'focus', 'amplitude': 300, 'n_points': 7}) else: raise ValueError(f"{choice} value is not allowed.")
[docs] def get_params_from_gui(self): self.mode = self.modes_list[self.comboBoxMode.currentIndex()] self.search_amplitude = self.searchAmpDoubleSpinBox.value() self.n_points = self.nPointsSpinBox.value()
[docs] def set_state(self, new_val): if self.mode == 'focus': self.sig_move_absolute.emit({'f_abs': new_val}) else: self.core.sig_state_request.emit({self.state_key: new_val})
[docs] def set_etl_amp_to_zero(self): if self.state_key == 'etl_l_offset': self.core.sig_state_request.emit({'etl_l_amplitude': 0}) print("ETL offset optimization: ETL amplitude (L) set to 0") elif self.state_key == 'etl_r_offset': self.core.sig_state_request.emit({'etl_r_amplitude': 0}) print("ETL offset optimization: ETL amplitude (R) set to 0")
@QtCore.pyqtSlot() def run_optimization(self): self.parent.sig_state_request.emit({'state': 'idle'}) # stop Live if it is running time.sleep(0.5) self.get_params_from_gui() self.set_etl_amp_to_zero() self.ini_state = self.state[self.state_key]['f_pos'] if self.mode == 'focus' else self.state[self.state_key] self.min_value = self.ini_state - self.search_amplitude if self.mode in ('etl_offset', 'etl_amp'): self.min_value = max(self.min_value, 0) # clip negative values for ETL self.max_value = self.ini_state + self.search_amplitude assert self.n_points % 2 == 1, f"Number of points must be odd, got {self.n_points} instead." self.search_grid = np.linspace(self.min_value, self.max_value, self.n_points) self.metric_array = np.zeros(len(self.search_grid)) print(f"Initial value: {self.ini_state:.3f}, searching in ({self.min_value:.3f}, {self.max_value:.3f}), n_points {self.n_points}") for i, v in enumerate(self.search_grid): self.set_state(v) time.sleep(self.delay_s) if i == 0: self.core.snap(write_flag=False, laser_blanking=True) # clears the first image from buffer self.core.snap(write_flag=False, laser_blanking=True) # this shares downsampled image via slot self.set_image() self.metric_array[i] = shannon_dct(self.roi) self.set_state(self.ini_state) # Reset to initial state time.sleep(self.delay_s) # give it some time to settle self.core.snap(write_flag=False) # this shares downsampled image via slot self.set_image() self.ini_metric = shannon_dct(self.roi) #fit with Gaussian fit_center, fit_sigma, fit_amp, fit_offset = fit_gaussian_1d(self.metric_array, self.search_grid) self.fit_grid = np.linspace(min(self.search_grid), max(self.search_grid), 51) self.gaussian_values = gaussian_1d(self.fit_grid, fit_center, fit_sigma, fit_amp, fit_offset) self.new_state = fit_center # Plot the results self.create_results_window() self.plot_results()
[docs] def create_results_window(self): self.results_window = loadUi('gui/mesoSPIM_Optimizer_Results.ui') self.results_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.results_window.setWindowTitle('Optimization results') # signal switchboard for results window self.results_window.acceptButton.clicked.connect(self.accept_new_state) self.results_window.discardButton.clicked.connect(self.discard_new_state)
[docs] def plot_results(self): layout = QtWidgets.QGridLayout() self.results_window.setLayout(layout) self.graphics_widget = pg.GraphicsLayoutWidget(show=True) plot0 = self.graphics_widget.addPlot(title='Image metric') plot0.addLegend(offset=(0, 0)) plot0.plot(self.search_grid, self.metric_array, pen=None, symbolBrush=(150,0,150), name='measured') plot0.plot(x=[self.ini_state], y=[self.ini_metric], symbolBrush=(0,0,250), name='old state') plot0.plot(x=[self.new_state], y=[max(self.gaussian_values)], symbolBrush=(250, 0, 0), name='new state') plot0.plot(self.fit_grid, self.gaussian_values, pen=(200, 0, 0), symbol=None, name='fitted') labelStyle = {'color': '#FFF', 'font-size': '14pt'} plot0.setLabel('bottom', self.state_key, **labelStyle) plot0.setLabel('left', 'Shannon(DCT), AU', **labelStyle) plot0.showGrid(x=True) plot0.setYRange(min(self.gaussian_values.min(), self.metric_array.min())*0.7, max(self.gaussian_values.max(), self.metric_array.max())*1.2) self.results_window.label_results.setText(self.results_string()) layout.addWidget(self.graphics_widget, 0, 0, 1, 2, QtCore.Qt.AlignHCenter) layout.addWidget(self.results_window.label_results, 1, 0, 1, 2, QtCore.Qt.AlignHCenter) layout.addWidget(self.results_window.acceptButton, 2, 0, 1, 1, QtCore.Qt.AlignHCenter) layout.addWidget(self.results_window.discardButton, 2, 1, 1, 1, QtCore.Qt.AlignHCenter) self.results_window.show()
[docs] def results_string(self): """Pretty formatting""" if self.mode == 'focus': return f"Old: {self.ini_state:.0f}\t New: {self.new_state:.0f}\t Diff: {(self.new_state - self.ini_state):.0f}" else: return f"Old: {self.ini_state:.3f}\t New: {self.new_state:.3f}\t Diff: {(self.new_state - self.ini_state):.3f}"
@QtCore.pyqtSlot() def accept_new_state(self): self.set_state(float(self.new_state)) print(f"Fitted value: {self.new_state:.3f}") time.sleep(self.delay_s) self.core.snap(write_flag=False, laser_blanking=True) state_str = f"{self.state[self.state_key]}" if self.mode == 'focus' else f"{self.state[self.state_key]:.3f}" print(f"New {self.state_key}:{state_str}") self.results_window.deleteLater() self.results_window = None @QtCore.pyqtSlot() def discard_new_state(self): self.new_state = None self.results_window.deleteLater() self.results_window = None @QtCore.pyqtSlot() def close_window(self): self.parent.camera_window.set_roi(None) if self.results_window: self.results_window.deleteLater() self.close()