基于白光干涉实现的表面形貌仪

原理

  • 白光相干性较低、相干长度很小,使用白光的迈克尔逊干涉仪时,仅在光程差为0附近有较明显的干涉图样,呈现出双缝干涉套上高斯波包的形式
  • 在光程差0附近的图样,可以看作是不同波数$k$在相位$\phi$下叠加得到
  • 对于白光干涉图样的离散采样,通过傅里叶变换应可以找到一个平均的波数成分$k_0$(对所有像素按采样顺序拍成的序列分别计算FFT,然后计算k对强度的加权平均即可,注意去掉直流成分)
  • 可以将$\phi$写成:

    $$\phi=\phi_0+(k-k_0)\frac{d\phi}{d k}\bigg|_{k_0}+\frac{(k-k_0)^2}{2}\frac{d^2\phi}{d k^2}\bigg|_{k_0}+...$$

    其中$\phi_0=k_0Z_0$

    于是$G_0=\frac{d\phi}{dk}\bigg|_{k_0}=Z_0 + k \frac{d Z}{dk}\bigg|_{k_0}$,其中$G_0$是$Z_0$处的群速度,代回第一个式子后发现$G_0$也是$\phi$随$k$变化的一阶项系数

  • 通过FFT,我们可以得到波数$k$及其对应的相位信息
  • 在$k_0$附近,相位应该是随着$k$递增的,因为$\phi=kZ$,而$Z$与物体实际的高度有关(需要单独处理,因为FFT计算得到复数,使用np.angle()得到的角度在[-$\pi$,$\pi$]范围内,不保证递增性)
  • 简单来说,通过计算$2nh=\frac{d\phi}{dk}\bigg|_{k_0}$已经可以知道物体的高度了,但有个问题是对于不同的h,$\phi$是周期性变化的,这样的算法不能区分出$h$和$h+\frac{\lambda}{2n}$,因此需要做一次Unwrap操作
  • 论文Surface Profiling by Analysis of White-light Interferograms in the Spatial Frequency Domain给出的方法是,采用上面计算的h,考虑$\phi_0$的信息再进行一次计算(文中使用拟合的方法计算$G_0$,拟合同时给出了截距信息$\phi_0$),采用公式:

    $h'=\frac{1}{2n}\{\frac{\phi_0-\alpha}{k_0}-\frac{2\pi}{k_0}Int[\frac{(\phi_0-\alpha)-(2nhk_0)}{2\pi}]\}$,其中$Int[a]$表示不超过a的最大整数

  • 实质上是一次Unwrap操作,在python里其实有更好的二维Unwrap函数可供使用

计算部分代码

import gzip
import pickle
import os
import time
import pylab
import numpy as np

import matplotlib.pyplot as plt
# print(plt.style.available)  # show available template
plt.style.use(['seaborn-ticks','seaborn-paper'])  # use a templet

import matplotlib as mpl
# mpl.rcParams['lines.linewidth'] = 2
# mpl.rcParams['lines.color'] = 'r'
params = {
    'figure.figsize': [8, 6], # Note! figure unit is inch!  scale fontz size 2.54 to looks like unit cm
    'axes.labelsize': 7*2.54, # scale 2.54 to change to figure unit looks as cm
    'font.size':  7*2.54,
    'lines.linewidth':2,
    'legend.fontsize': 7*2.54,
    'xtick.labelsize': 6*2.54,
    'ytick.labelsize': 6*2.54,
    'text.usetex': False,  
    'xtick.direction': "in",
    'ytick.direction': "in", # ticket inside
    'legend.frameon' : True, 
    'legend.edgecolor': 'black',
    'legend.shadow': True,
    'legend.framealpha':1,
#     'patch.linewidth' : 0.5, 
}
mpl.rcParams.update(params)

with gzip.open('data.pklz', 'rb') as f:
    frames = pickle.load(f)

frames_N = len(frames)

Pi = np.pi

def phase_unwrap(Raw):
    Len_Raw = len(Raw)
    Threshold = 1.5
    Unwraped = Raw
    for i in range(Len_Raw-1):
        Unwraped[i+1] = Raw[i+1] + 2*Pi*np.rint((Unwraped[i]-Raw[i+1])/(2.0*Pi))
        if (Unwraped[i+1]<Unwraped[i])and(np.abs(Unwraped[i]-Unwraped[i+1])>Threshold) : 
          Unwraped[i+1] += 2*Pi
    return Unwraped

def Smooth(Raw):
    Threshold = 0.5
    Smooth_Img = Raw
    Width , Height = Raw.shape
    for i in range(Width-2):
        for j in range(Height-2):
            Tmp = (Raw[i,j+1] + Raw[i+2,j+1] + Raw[i+1,j] + Raw[i+1,j+2])/4.0
            if np.abs(Tmp-Smooth_Img[i+1,j+1])>Threshold :
                Smooth_Img[i+1,j+1] = Tmp
    return Smooth_Img

def ProfileCalculation(frames, Step, fit_window=2, SkipZero=5, Threshold = 1/10.0):

    Width , Height = frames[0].shape
    
    N = len(frames)
    Len = Width * Height

    lines = [frame.reshape(-1, 1) for frame in frames]
    lines = np.concatenate(tuple(lines), axis=1)
    
    Angles = []
    Data = []  #Store the phase & k after FT
    
    Bar = 0
    Count =0
    Part = 0 #Relate to Progress Bar
    
    for i, line in enumerate(lines):
        line = np.concatenate((np.ones(len(line),dtype=np.uint8)*int(np.average(line)),line))
        rfft = np.fft.rfft(line)
        rfft_abs = np.abs(rfft)
        kn = np.argmax(rfft_abs[SkipZero:-1]) + SkipZero 
        Data.append([rfft_abs[kn], kn])
        Angles.append(np.angle(rfft)) # angle is in range(-Pi,Pi)
        Count += 1
        if int(100.0*Count / Len)!=Bar :
            Bar +=1
            #os.system("cls")
            print("Processing ... %.2f%%" %(50*(Count / Len) + Part)) 
    Part = 50
    Bar =0
    Count =0

    Data = np.array(Data)

    kn_Max_Value = np.max(Data[:, 0])
    Mask = Data[:, 0] > kn_Max_Value * Threshold  
        #Throw the background data which the value(k=kn) less than max_value /10.0
    Data = Data[Mask, :]

    k0 = np.sum(Data[:, 0]*Data[:, 1])/np.sum(Data[:, 0]) #Calculate the most efficient k0 of white light
    k0_int = int(k0) 
    Fit_Range = np.array(range((k0_int - fit_window), (k0_int + fit_window + 1)))

    Image = []

    for i, angle in enumerate(Angles):
        if Mask[i] :
            Count += 1
            angle = phase_unwrap(angle[Fit_Range])
                #Using np.angle we get the phase in range of (-Pi,Pi)
                #Around the k0, the phi is equal k times Z,
                #since the Z is a const relate to the real distance of the object
                #the phase should be increasing.
            k , Slope , Phi0 = (np.polyfit(Fit_Range-k0,angle,2)) 
                #Polyfit around k0
                #Slope -> G0
                #Fit_Range-k0 so that intercept is Phi0 in k=k0
            Image.append(Phi0-2*np.pi*np.rint((Phi0-k0_int * Slope)/(2*np.pi)))
        else :
            Count += 1
            Image.append(0)
                #Backgroud is simply set as h=0
        if int(100*Count /Len)!=Bar :
            Bar += 1
            #os.system("cls")
            print("Processing ... %.2f%%" %(50*(Count / Len)+ Part))

    Image = np.array(Image).reshape(Width,Height)
    #for i in range(Width):
    #    Image[i,:] = unwrap_phase(Image[i,:])
    Image = unwrap_phase(Image)
    k = 2 * np.pi * k0 / (Step * N)
    Image = Image/k/2
    return Image

from skimage.restoration import unwrap_phase
    
image = ProfileCalculation(frames[:], Step=0.1)
image = Smooth(image)

img = plt.imshow(image)
img.set_cmap('Spectral_r')
plt.colorbar()
pylab.show()
Last modification:May 17, 2021
(๑´ڡ`๑)