#!/usr/bin/env python
# coding: utf-8
# # Calibration of the XY-axes in a laser scanning microscope
#
# In this notebook, we present the procedure to calibrate the XY scanner (e.g., galvo mirrors) of a microscope. We used a reflective [grid array](https://www.thorlabs.com/thorproduct.cfm?partnumber=R1L3S3PR) sample from Thorlabs. The reference grid we used is the one with spacing Δx = 10 µm.
# In[1]:
import numpy as np
import numpy.fft as ft
import matplotlib.pyplot as plt
from skimage.feature import peak_local_max
import brighteyes_ism.dataio.mcs as mcs
import brighteyes_ism.analysis.Tools_lib as tool
import brighteyes_ism.analysis.Graph_lib as gr
import os
# ## Data loading
# In this case, the data are stored as an hdf5 file generated by the [BrightEyes-MCS](https://github.com/VicidominiLab/BrightEyes-MCS) software.
# In[2]:
path = r'\\iitfsvge101.iit.local\mms\Data MMS server\STED-ISM\SetupDiagnostics\calibrations\grid\10um\B'
for file in sorted( os.listdir(path) ):
if file.endswith('.h5'):
print(file)
break
fullpath = os.path.join(path, file)
data, meta = mcs.load(fullpath)
# To perform the acquisition, we used a 640 nm CW laser.
# The microscope is a custom ISM setup, equipped with a 60x/1.4 oil objective lens.
# The detector array size is 1.4 Airy Units.
# We print the metadata, to show the acquisition parameters.
# In[3]:
meta.Print()
# The ISM data structure is (repetition, z, y, x, time, channel).
# Since we are not interested in having high-resolution and the sample is flat, we keep only the spatial dimensions (y, x).
# In[4]:
img = np.sum(data, axis = (0,1,4,5))
# We have a look at the image of the grid.
# In[5]:
fig, ax = plt.subplots(1,1, figsize = (6,6))
fig, ax = gr.ShowImg(img, meta.dx, clabel = meta.pxdwelltime, fig = fig, ax = ax)
# ## Calculation of the reciprocal lattice
#
# We calculate the absolute value of the Fourier transform of the image.
# In[6]:
f_ax = ft.fftshift( ft.fftfreq(meta.nx) )
imgF = ft.fftshift( ft.fft2(img) )
F = np.abs(imgF)
# ## Measurement of the harmonics coordinates
#
# We measure the positions of the peaks of the reciprocal lattice.
# In[7]:
idx_max = np.unravel_index(np.argmax(F), np.array(F).shape)
peak_idx = peak_local_max(F, threshold_rel = 0.2)
# We have a look at the grid in the frequency space.
# We highlight the positions of the peaks with a marker.
# In[8]:
plt.figure(figsize = (6,6))
plt.imshow(F)
plt.xlim( [950,1050] )
plt.ylim( [950,1050] )
plt.plot(peak_idx[:,0], peak_idx[:,1], 'ro', markersize = 10, markerfacecolor='none')
# We change the reference frame of the coordinates, such that they are relative to the zero-order position.
# In[9]:
rel_peak = peak_idx - idx_max
# We select the first harmonic on each axis and calculate the corresponding period in space.
# In[10]:
peak_x = np.abs( rel_peak[:,0] )
peak_y = np.abs( rel_peak[:,1] )
dfx = 1 / meta.nx
dfy = 1 / meta.ny
X = 1 / ( np.min( peak_x[np.nonzero(peak_x)] ) * dfx ) # pixel
Y = 1 / ( np.min( peak_y[np.nonzero(peak_y)] ) * dfy ) # pixel
# ## Calibration
#
# Knowing the real spacing of the grid, we can calculate how many pixel fit in the period of the grid.
# In[11]:
T = 10 # um
px_x = T / X # um / px
px_y = T / Y # um / px
# Lastly, we convert the pixels into volts to find the calibration values.
# In[12]:
calib_x = px_x * meta.calib_x / meta.dx
calib_y = px_y * meta.calib_y / meta.dy
print(f'calib_x = {calib_x} um/V')
print(f'calib_y = {calib_y} um/V')