#!/usr/bin/env python
# coding: utf-8
# # Calibration of the Z-axis in a laser scanning microscope
#
# In this notebook, we present the procedure to calibrate the Z-axis scanner (e.g., piezo motor or varifocal lens) of a microscope. We used the calibration sample from Argolight. The reference patterns we used are the [3D crossing stairs](https://argolight.notion.site/Argo-SIM-v1-6973c253a418422cadf224a5e55dd2ea#cd7464ab67bf470b947ac0d33e640f84) with axial step Δz = 1 µm.
# In[1]:
import numpy as np
import matplotlib.pyplot as plt
import brighteyes_ism.dataio.mcs as mcs
import brighteyes_ism.analysis.Tools_lib as tool
import brighteyes_ism.analysis.Graph_lib as gr
from skimage.filters import gaussian
from skimage.filters import threshold_otsu
from sklearn.cluster import KMeans
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\argolight\stairs\new_offset_5V'
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 488 nm CW laser. The power is roughly 50 µW on the focal plane.
# The microscope is a custom ISM setup, equipped with a 60x/1.4 oil objective lens.
# The detector array size is 1.8 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, we keep only the spatial dimensions (z, y, x).
# In[4]:
data = np.sum(data, axis = (0,4,5))
# We crop the edges of the stack, because we are interested only in the stairway structures.
# In[5]:
dset = data[:, 250:-250, 100:-100]
# We have a look at the z-stack.
# In[6]:
figsize = (7, 4.3)
fig = gr.ShowStack(dset, meta.dx, meta.dz/2, clabel = meta.pxdwelltime, projection = 'mip', figsize = figsize)
# ## Pre-processing
#
# We want to measure only the center-of-mass of each step of the stairway.
# Thus, we sacrifice some resolution to denoise the dataset with a Gaussian filter.
# In[7]:
dset = gaussian(dset, sigma = 2)
dset /= dset.max()
# We convert the z-stack (Z, Y, X) to a point cloud where each row contains the 3D coordinates and the corresponding intensity value.
# In[8]:
point_cloud_matrix = tool.point_cloud_from_img(dset)
# We remove the background with the Otsu method.
# In[9]:
thresh = threshold_otsu(dset)
cloud = point_cloud_matrix[ point_cloud_matrix[:, -1] > thresh ]
# We look at the thresholded scatter plot.
# In[10]:
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.scatter(cloud[:,1], cloud[:,2], cloud[:,0], c = cloud[:,3])
ax.set_xlabel(r'x')
ax.set_ylabel(r'y')
ax.set_zlabel(r'z')
# ## Data clustering
#
# We perform K-Means clustering to identify each step.
# In[11]:
model = KMeans(n_clusters = 22, init = "k-means++", max_iter = 300, n_init = 10, random_state = 0)
y_clusters = model.fit_predict(cloud)
cm = model.cluster_centers_
# We classify each stair using the y-coordinate.
# In[12]:
ym = cm[:, 1].mean()
cm_classes = cm[:, 1] < ym
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.scatter(cm[:, 1], cm[:, 2], cm[:, 0], c=cm_classes)
ax.set_xlabel(r'x')
ax.set_ylabel(r'y')
ax.set_zlabel(r'z')
# ## Calibration
#
# We sort the z-coordinates of each class in ascending order.
# Additionally, we convert the units into Volts.
# In[13]:
z_a = np.sort( cm[cm_classes==0,0] ) * meta.dz / meta.calib_z # Volt
z_b = np.sort( cm[cm_classes==1,0] ) * meta.dz / meta.calib_z # Volt
# We generate an array with spacing Δz = 1 µm.
# In[14]:
x = np.arange(len(z_a)) # micrometers
# We fit each stair to a line. The slope is the desired calibration value.
# In[15]:
poly = np.polyfit(x, z_a, 1)
fit = np.poly1d(poly)
z_calib = 1 / poly[0] # um/Volt
plt.figure()
plt.plot( z_a, 'o' )
plt.plot( fit(x), '-' )
plt.xlabel(r'z $(\mu m)$' )
plt.ylabel(r'Volt' )
print( f'z_calib = {z_calib:.2f} um/V' )
# We repeat the same process for the second stair. The two calibration values should match.
# In[16]:
poly = np.polyfit(x, z_b, 1)
fit = np.poly1d(poly)
z_calib = 1 / poly[0] # um/Volt
plt.figure()
plt.plot( z_b, 'o' )
plt.plot( fit(x), '-' )
plt.xlabel(r'z $(\mu m)$' )
plt.ylabel(r'Volt' )
print( f'z_calib = {z_calib:.2f} um/V' )