From 703a2057f1293642f5459690dcda0acac61b1ee4 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Thu, 3 Oct 2024 11:42:33 +0200 Subject: [PATCH 1/3] removed t_f field that was not used any longer --- .../algorithms/custom_iter_dual_reference.py | 103 ++++++++++++------ openwfs/algorithms/genetic.py | 2 +- openwfs/algorithms/utilities.py | 5 - 3 files changed, 72 insertions(+), 38 deletions(-) diff --git a/openwfs/algorithms/custom_iter_dual_reference.py b/openwfs/algorithms/custom_iter_dual_reference.py index 73b083b..f5c9643 100644 --- a/openwfs/algorithms/custom_iter_dual_reference.py +++ b/openwfs/algorithms/custom_iter_dual_reference.py @@ -45,8 +45,16 @@ class IterativeDualReference: https://opg.optica.org/oe/ abstract.cfm?uri=oe-27-8-1167 """ - def __init__(self, feedback: Detector, slm: PhaseSLM, phase_patterns: tuple[nd, nd], group_mask: nd, - phase_steps: int = 4, iterations: int = 4, analyzer: Optional[callable] = analyze_phase_stepping): + def __init__( + self, + feedback: Detector, + slm: PhaseSLM, + phase_patterns: tuple[nd, nd], + group_mask: nd, + phase_steps: int = 4, + iterations: int = 4, + analyzer: Optional[callable] = analyze_phase_stepping, + ): """ Args: feedback: The feedback source, usually a detector that provides measurement data. @@ -62,7 +70,9 @@ def __init__(self, feedback: Detector, slm: PhaseSLM, phase_patterns: tuple[nd, A, B, A, B, A. Should be at least 2 analyzer: The function used to analyze the phase stepping data. Must return a WFSResult object. Defaults to `analyze_phase_stepping` """ - if (phase_patterns[0].shape[0:2] != group_mask.shape) or (phase_patterns[1].shape[0:2] != group_mask.shape): + if (phase_patterns[0].shape[0:2] != group_mask.shape) or ( + phase_patterns[1].shape[0:2] != group_mask.shape + ): raise ValueError("The phase patterns and group mask must all have the same shape.") if iterations < 2: raise ValueError("The number of iterations must be at least 2.") @@ -74,13 +84,18 @@ def __init__(self, feedback: Detector, slm: PhaseSLM, phase_patterns: tuple[nd, self.phase_steps = phase_steps self.iterations = iterations self.analyzer = analyzer - self.phase_patterns = (phase_patterns[0].astype(np.float32), phase_patterns[1].astype(np.float32)) + self.phase_patterns = ( + phase_patterns[0].astype(np.float32), + phase_patterns[1].astype(np.float32), + ) mask = group_mask.astype(bool) self.masks = (~mask, mask) # masks[0] is True for group A, mask[1] is True for group B # Pre-compute the conjugate modes for reconstruction - self.modes = [np.exp(-1j * self.phase_patterns[side]) * np.expand_dims(self.masks[side], axis=2) for side in - range(2)] + self.modes = [ + np.exp(-1j * self.phase_patterns[side]) * np.expand_dims(self.masks[side], axis=2) + for side in range(2) + ] def execute(self, capture_intermediate_results: bool = False, progress_bar=None) -> WFSResult: """ @@ -104,23 +119,36 @@ def execute(self, capture_intermediate_results: bool = False, progress_bar=None) # Initialize storage lists t_set_all = [None] * self.iterations results_all = [None] * self.iterations # List to store all results - results_latest = [None, None] # The two latest results. Used for computing fidelity factors. - intermediate_results = np.zeros(self.iterations) # List to store feedback from full patterns + results_latest = [ + None, + None, + ] # The two latest results. Used for computing fidelity factors. + intermediate_results = np.zeros( + self.iterations + ) # List to store feedback from full patterns # Prepare progress bar if progress_bar: - num_measurements = np.ceil(self.iterations / 2) * self.modes[0].shape[2] \ - + np.floor(self.iterations / 2) * self.modes[1].shape[2] + num_measurements = ( + np.ceil(self.iterations / 2) * self.modes[0].shape[2] + + np.floor(self.iterations / 2) * self.modes[1].shape[2] + ) progress_bar.total = num_measurements # Switch the phase sets back and forth multiple times for it in range(self.iterations): side = it % 2 # pick set A or B for phase stepping - ref_phases = -np.angle(t_full) # use the best estimate so far to construct an optimized reference + ref_phases = -np.angle( + t_full + ) # use the best estimate so far to construct an optimized reference side_mask = self.masks[side] # Perform WFS experiment on one side, keeping the other side sized at the ref_phases - result = self._single_side_experiment(mod_phases=self.phase_patterns[side], ref_phases=ref_phases, - mod_mask=side_mask, progress_bar=progress_bar) + result = self._single_side_experiment( + mod_phases=self.phase_patterns[side], + ref_phases=ref_phases, + mod_mask=side_mask, + progress_bar=progress_bar, + ) # Compute transmission matrix for the current side and update # estimated transmission matrix @@ -139,23 +167,33 @@ def execute(self, capture_intermediate_results: bool = False, progress_bar=None) intermediate_results[it] = self.feedback.read() # Compute average fidelity factors - fidelity_noise = weighted_average(results_latest[0].fidelity_noise, - results_latest[1].fidelity_noise, results_latest[0].n, - results_latest[1].n) - fidelity_amplitude = weighted_average(results_latest[0].fidelity_amplitude, - results_latest[1].fidelity_amplitude, results_latest[0].n, - results_latest[1].n) - fidelity_calibration = weighted_average(results_latest[0].fidelity_calibration, - results_latest[1].fidelity_calibration, results_latest[0].n, - results_latest[1].n) - - result = WFSResult(t=t_full, - t_f=None, - n=self.modes[0].shape[2] + self.modes[1].shape[2], - axis=2, - fidelity_noise=fidelity_noise, - fidelity_amplitude=fidelity_amplitude, - fidelity_calibration=fidelity_calibration) + fidelity_noise = weighted_average( + results_latest[0].fidelity_noise, + results_latest[1].fidelity_noise, + results_latest[0].n, + results_latest[1].n, + ) + fidelity_amplitude = weighted_average( + results_latest[0].fidelity_amplitude, + results_latest[1].fidelity_amplitude, + results_latest[0].n, + results_latest[1].n, + ) + fidelity_calibration = weighted_average( + results_latest[0].fidelity_calibration, + results_latest[1].fidelity_calibration, + results_latest[0].n, + results_latest[1].n, + ) + + result = WFSResult( + t=t_full, + n=self.modes[0].shape[2] + self.modes[1].shape[2], + axis=2, + fidelity_noise=fidelity_noise, + fidelity_amplitude=fidelity_amplitude, + fidelity_calibration=fidelity_calibration, + ) # TODO: document the t_set_all and results_all attributes result.t_set_all = t_set_all @@ -163,8 +201,9 @@ def execute(self, capture_intermediate_results: bool = False, progress_bar=None) result.intermediate_results = intermediate_results return result - def _single_side_experiment(self, mod_phases: nd, ref_phases: nd, mod_mask: nd, - progress_bar=None) -> WFSResult: + def _single_side_experiment( + self, mod_phases: nd, ref_phases: nd, mod_mask: nd, progress_bar=None + ) -> WFSResult: """ Conducts experiments on one part of the SLM. diff --git a/openwfs/algorithms/genetic.py b/openwfs/algorithms/genetic.py index 519aa54..360d690 100644 --- a/openwfs/algorithms/genetic.py +++ b/openwfs/algorithms/genetic.py @@ -99,7 +99,7 @@ def execute(self, *, progress_bar=None) -> WFSResult: # Terminate after the specified number of generations, return the best wavefront if i >= self.generations: - return WFSResult(t=np.exp(-1.0j * population[sorted_indices[-1]]), t_f=None, axis=2) + return WFSResult(t=np.exp(-1.0j * population[sorted_indices[-1]]), axis=2) # We keep the elite individuals, and regenerate the rest by mixing the elite # For this mixing, the probability of selecting an individual is proportional to its measured intensity. diff --git a/openwfs/algorithms/utilities.py b/openwfs/algorithms/utilities.py index 545cd1d..2a323a8 100644 --- a/openwfs/algorithms/utilities.py +++ b/openwfs/algorithms/utilities.py @@ -30,7 +30,6 @@ class WFSResult: def __init__( self, t: np.ndarray, - t_f: np.ndarray, axis: int, fidelity_noise: ArrayLike = 1.0, fidelity_amplitude: ArrayLike = 1.0, @@ -60,7 +59,6 @@ def __init__( """ self.t = t - self.t_f = t_f self.axis = axis self.fidelity_noise = np.atleast_1d(fidelity_noise) self.n = np.prod(t.shape[0:axis]) if n is None else n @@ -119,7 +117,6 @@ def select_target(self, b) -> "WFSResult": """ return WFSResult( t=self.t.reshape((*self.t.shape[0:2], -1))[:, :, b], - t_f=self.t_f.reshape((*self.t_f.shape[0:2], -1))[:, :, b], axis=self.axis, intensity_offset=self.intensity_offset[:][b], fidelity_noise=self.fidelity_noise[:][b], @@ -151,7 +148,6 @@ def weighted_average(attribute): return WFSResult( t=weighted_average("t"), - t_f=weighted_average("t_f"), n=n, axis=axis, fidelity_noise=weighted_average("fidelity_noise"), @@ -255,7 +251,6 @@ def analyze_phase_stepping(measurements: np.ndarray, axis: int, A: Optional[floa return WFSResult( t, - t_f=t_f, axis=axis, fidelity_amplitude=amplitude_factor, fidelity_noise=noise_factor, From eaa6d4e89d2d642faf9b15247c03bf74d1c7e6d5 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Thu, 3 Oct 2024 12:30:34 +0200 Subject: [PATCH 2/3] cleaning up example code --- examples/micro_manager_microscope.py | 60 +++++-------------- examples/micro_manager_scanning_microscope.py | 14 ++--- examples/sample_microscope.py | 57 ++++++------------ examples/wfs_demonstration_experimental.py | 14 ++--- openwfs/simulation/mockdevices.py | 12 ++-- 5 files changed, 51 insertions(+), 106 deletions(-) diff --git a/examples/micro_manager_microscope.py b/examples/micro_manager_microscope.py index 2b2603b..b329376 100644 --- a/examples/micro_manager_microscope.py +++ b/examples/micro_manager_microscope.py @@ -1,11 +1,11 @@ -""" Sample microscope +""" Micro-Manager simulated microscope ======================= -This script simulates a microscopic imaging system, generating a random noise image as a mock source and capturing it -through a microscope with adjustable magnification, numerical aperture, and wavelength. It visualizes the original and -processed images dynamically, demonstrating how changes in optical parameters affect image quality and resolution. +This script simulates a microscope with a random noise image as a mock specimen. +The numerical aperture, stage position, and other parameters can be modified through the Micro-Manager GUI. +To use this script as a device in Micro-Manager, make sure you have the PyDevice adapter installed and +select this script in the hardware configuration wizard for the PyDevice component. -This script should be opened from the μManager microscope GUI software using the PyDevice plugin. -To do so, add a PyDevice adapter to the μManager hardware configuration, and select this script as the device script. +See the 'Sample Microscope' example for a microscope simulation that runs from Python directly. """ import astropy.units as u @@ -13,29 +13,18 @@ from openwfs.simulation import Microscope, StaticSource -# height × width, and resolution, of the specimen image -specimen_size = (1024, 1024) -specimen_resolution = 60 * u.nm - -# magnification from object plane to camera. -magnification = 40 - -# numerical aperture of the microscope objective -numerical_aperture = 0.85 - -# wavelength of the light, for computing diffraction. -wavelength = 532.8 * u.nm - -# Size of the pixels on the camera -pixel_size = 6.45 * u.um - -# number of pixels on the camera -camera_resolution = (256, 256) +specimen_resolution = (1024, 1024) # height × width in pixels of the specimen image +specimen_pixel_size = 60 * u.nm # resolution (pixel size) of the specimen image +magnification = 40 # magnification from object plane to camera. +numerical_aperture = 0.85 # numerical aperture of the microscope objective +wavelength = 532.8 * u.nm # wavelength of the light, for computing diffraction. +camera_resolution = (256, 256) # number of pixels on the camera +camera_pixel_size = 6.45 * u.um # Size of the pixels on the camera # Create a random noise image with a few bright spots src = StaticSource( - data=np.maximum(np.random.randint(-10000, 100, specimen_size, dtype=np.int16), 0), - pixel_size=specimen_resolution, + data=np.maximum(np.random.randint(-10000, 100, specimen_resolution, dtype=np.int16), 0), + pixel_size=specimen_pixel_size, ) # Create a microscope with the given parameters @@ -51,25 +40,8 @@ shot_noise=True, digital_max=255, data_shape=camera_resolution, - pixel_size=pixel_size, + pixel_size=camera_pixel_size, ) -# expose the xy-stage of the microscope -stage = mic.xy_stage - # construct dictionary of objects to expose to Micro-Manager devices = {"camera": cam, "stage": stage} - -if __name__ == "__main__": - # When running this script directly (not from μManager) - # the code below shows how to operate the stage and change the numerical aperture of the microscope - import matplotlib.pyplot as plt - - for i in range(20): - stage.x = stage.x + 2 * u.um - mic.numerical_aperture -= 0.025 - plt.imshow(mic.read(), cmap="gray") - if i == 0: - plt.colorbar() - plt.show(block=False) - plt.pause(0.5) diff --git a/examples/micro_manager_scanning_microscope.py b/examples/micro_manager_scanning_microscope.py index d49e093..b4e5973 100644 --- a/examples/micro_manager_scanning_microscope.py +++ b/examples/micro_manager_scanning_microscope.py @@ -1,16 +1,16 @@ -""" -Constructs a scanning microscope controller for use with Micro-Manager - -The microscope object can be loaded into Micro-Manager through the PyDevice -device adapter. +""" Micro-Manager simulated scanning microscope +======================= +This script simulates a scanning microscope with a pre-set image as a mock specimen. +The scan parameters can be modified through the Micro-Manager GUI. +To use this script as a device in Micro-Manager, make sure you have the PyDevice adapter installed and +select this script in the hardware configuration wizard for the PyDevice component. """ import astropy.units as u -import skimage - # add 'openwfs' to the search path. This is only needed when developing openwfs # otherwise it is just installed as a package import set_path # noqa +import skimage from openwfs.devices import ScanningMicroscope, Axis from openwfs.devices.galvo_scanner import InputChannel diff --git a/examples/sample_microscope.py b/examples/sample_microscope.py index 9259594..898604c 100644 --- a/examples/sample_microscope.py +++ b/examples/sample_microscope.py @@ -11,42 +11,23 @@ import set_path # noqa - needed for setting the module search path to find openwfs from openwfs.plot_utilities import grab_and_show, imshow from openwfs.simulation import Microscope, StaticSource -from openwfs.utilities import set_pixel_size -# Parameters that can be altered - -img_size_x = 1024 -# Determines how wide the image is. - -img_size_y = 1024 -# Determines how high the image is. - -magnification = 40 -# magnification from object plane to camera. - -numerical_aperture = 0.85 -# numerical aperture of the microscope objective - -wavelength = 532.8 * u.nm -# wavelength of the light, different wavelengths are possible, units can be adjusted accordingly. - -pixel_size = 6.45 * u.um -# Size of the pixels on the camera - -camera_resolution = (256, 256) -# number of pixels on the camera - -p_limit = 100 -# Number of iterations. Influences how quick the 'animation' is complete. - -# Code -img = set_pixel_size( - np.maximum( - np.random.randint(-10000, 100, (img_size_y, img_size_x), dtype=np.int16), 0 - ), - 60 * u.nm, +specimen_resolution = (1024, 1024) # height × width in pixels of the specimen image +specimen_pixel_size = 60 * u.nm # resolution (pixel size) of the specimen image +magnification = 40 # magnification from object plane to camera. +numerical_aperture = 0.85 # numerical aperture of the microscope objective +wavelength = 532.8 * u.nm # wavelength of the light, for computing diffraction. +camera_resolution = (256, 256) # number of pixels on the camera +camera_pixel_size = 6.45 * u.um # Size of the pixels on the camera +p_limit = 100 # Number steps in the animation + +# Create a random noise image with a few bright spots +src = StaticSource( + data=np.maximum(np.random.randint(-10000, 100, specimen_resolution, dtype=np.int16), 0), + pixel_size=specimen_pixel_size, ) -src = StaticSource(img) + +# Create a microscope with the given parameters mic = Microscope( src, magnification=magnification, @@ -59,7 +40,7 @@ shot_noise=True, digital_max=255, data_shape=camera_resolution, - pixel_size=pixel_size, + pixel_size=camera_pixel_size, ) devices = {"camera": cam, "stage": mic.xy_stage} @@ -67,7 +48,7 @@ import matplotlib.pyplot as plt plt.subplot(1, 2, 1) - imshow(img) + imshow(src.data) plt.title("Original image") plt.subplot(1, 2, 2) plt.title("Scanned image") @@ -76,7 +57,5 @@ mic.xy_stage.x = p * 1 * u.um mic.numerical_aperture = 1.0 * (p + 1) / p_limit # NA increases to 1.0 ax = grab_and_show(cam, ax) - plt.title( - f"NA: {mic.numerical_aperture}, δ: {mic.abbe_limit.to_value(u.um):2.2} μm" - ) + plt.title(f"NA: {mic.numerical_aperture}, δ: {mic.abbe_limit.to_value(u.um):2.2} μm") plt.pause(0.2) diff --git a/examples/wfs_demonstration_experimental.py b/examples/wfs_demonstration_experimental.py index b2a16f2..a3fc701 100644 --- a/examples/wfs_demonstration_experimental.py +++ b/examples/wfs_demonstration_experimental.py @@ -2,6 +2,9 @@ WFS demo experiment ===================== This script demonstrates how to perform a wavefront shaping experiment using the openwfs library. +It assumes that you have a genicam camera and an SLM connected to your computer. +Please adjust the path to the camera driver and (when needed) the monitor id in the Camera and SLM objects. + """ import astropy.units as u @@ -19,16 +22,12 @@ # constructs the actual slm for wavefront shaping, and a monitor window to display the current phase pattern slm = SLM(monitor_id=2, duration=2) -monitor = slm.clone( - monitor_id=0, pos=(0, 0), shape=(slm.shape[0] // 4, slm.shape[1] // 4) -) +monitor = slm.clone(monitor_id=0, pos=(0, 0), shape=(slm.shape[0] // 4, slm.shape[1] // 4)) # we are using a setup with an SLM that produces 2pi phase shift # at a gray value of 142 slm.lookup_table = range(142) -alg = FourierDualReference( - feedback=roi_detector, slm=slm, slm_shape=[800, 800], k_radius=7 -) +alg = FourierDualReference(feedback=roi_detector, slm=slm, slm_shape=[800, 800], k_radius=7) result = alg.execute() print(result) @@ -46,6 +45,3 @@ plt.pause(1.0) slm.set_phases(0.0) plt.pause(1.0) - -# plt.show() -# input("press any key") diff --git a/openwfs/simulation/mockdevices.py b/openwfs/simulation/mockdevices.py index 79a3b83..ebfb051 100644 --- a/openwfs/simulation/mockdevices.py +++ b/openwfs/simulation/mockdevices.py @@ -7,7 +7,7 @@ from ..core import Detector, Processor, Actuator from ..processors import CropProcessor -from ..utilities import ExtentType, get_pixel_size +from ..utilities import ExtentType, get_pixel_size, set_pixel_size class StaticSource(Detector): @@ -42,6 +42,8 @@ def __init__( pixel_size = Quantity(extent) / data.shape else: pixel_size = get_pixel_size(data) + else: + data = set_pixel_size(data, pixel_size) # make sure the data array holds the pixel size if ( pixel_size is not None @@ -176,9 +178,7 @@ def _fetch(self, data) -> np.ndarray: # noqa if self.analog_max == 0.0: # auto scaling max_value = np.max(data) if max_value > 0.0: - data = data * ( - self.digital_max / max_value - ) # auto-scale to maximum value + data = data * (self.digital_max / max_value) # auto-scale to maximum value else: data = data * (self.digital_max / self.analog_max) @@ -186,9 +186,7 @@ def _fetch(self, data) -> np.ndarray: # noqa data = self._rng.poisson(data) if self._gaussian_noise_std > 0.0: - data = data + self._rng.normal( - scale=self._gaussian_noise_std, size=data.shape - ) + data = data + self._rng.normal(scale=self._gaussian_noise_std, size=data.shape) return np.clip(np.rint(data), 0, self.digital_max).astype("uint16") From 72ac9338944a215e4a3a09802f8adcbd286c40df Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Thu, 3 Oct 2024 12:39:47 +0200 Subject: [PATCH 3/3] cleaning up example code --- examples/micro_manager_scanning_microscope.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/micro_manager_scanning_microscope.py b/examples/micro_manager_scanning_microscope.py index b4e5973..5e69694 100644 --- a/examples/micro_manager_scanning_microscope.py +++ b/examples/micro_manager_scanning_microscope.py @@ -7,6 +7,7 @@ """ import astropy.units as u + # add 'openwfs' to the search path. This is only needed when developing openwfs # otherwise it is just installed as a package import set_path # noqa