抗偏位非球面人工晶体设计
先给非眼科学的读者介绍一下知识背景。1949年,Ridly爵士发明了人工晶状体(Intraocluar lens,IOL,人工晶体),是一枚有机玻璃镜片,可以植入到眼内,替代人眼的天然晶状体。这是唯一,也是非常有效的治疗白内障的方法,将因为白内障浑浊的天然晶状体,更换成人工晶体。
早期人工晶体的加工和光学设计显然是和光学镜头一致的,是球面镜。人工晶体的材料逐渐从硬质的PMMA,进化到了有弹性可折叠的硅胶、亲水/疏水丙烯酸酯、胶原等材料,切口也越来越小。但人工晶体的光学设计一直到1990s末才发生改变。
人眼的角膜,是具有球差的,是一个“正球差”的凸透镜,也就是说,周边的屈光力更强,中央的弱一些,周边的光线会被聚焦得更近 ,人眼的晶状体,这东西不但形状不是标准球面镜,内部的折射率还是渐变的,最终的结果是人眼的晶状体是一个“负球差”的透镜。一定程度上,两个透镜的结合导致球差能够互相抵消一部分。
对于球面镜的人工晶体,一定是一枚“正球差”的凸透镜,所以,即使屈光度计算正确,仍然会与角膜搭配增加球差。于是眼科医生们希望能够设计出非球面的人工晶体。
- 参考文献1:姚克, 陆道炎, 王丽天, 等. 非球面等视像后房型人工晶状体的临床应用. 中华眼科杂志, 1989, 25:262-264.
- 参考文献2: A new intraocular lens design to reduce spherical aberration of pseudophakic eyes.
第一枚上市的非球面人工晶体是AMO公司(现在属于强生)的Tecnis Z9000, FDA: 09/05/2001
非球面IOL上市以后,人们发现,由于IOL是通过手术植入的,多多少少可能有些偏位,也就是人工晶体的光学中心与角膜的光轴可能有差距。这个量很小,只有0.x mm。当消球差的两个非球面镜之间发生偏位的时候,平行光入射,会发生偏移,会形成新的像差,叫做慧差,于是一部分厂商认为慧差也是需要纠正的。
但在术前并无法知道术后IOL的偏位量,所以博士伦公司推出了“零球差”的IOL,也就是较少矫正角膜球差,虽然可能有偏位,但并不因为偏位而产生慧差。
在此之后,更多的厂商利用光学设计推出了“抗偏位”的非球面人工晶体,也就是说,在矫正角膜球差的基础上,即使发生了偏位,也不会引起比较大的慧差。
相应的,各个厂商也给自己的非球面人工晶体起了各种名字,比如:
- HOYA: 非球面平衡曲线(ABC曲线)
- 爱博诺德:高次非球面
- 蕾明视康:轴向渐进非球面
- 蔡司:非恒定像差
那么接下来就讲解抗偏位非球面人工晶体设计的基本原理。
定理¶
“当你需要深入了解时,一定在眼科书里找不到详细。”
工具¶
- 工业用光学设计软件:
- Zemax: 专业工具,人工晶体设计厂商常用,昂贵
- OSLO EDU:教学版本免费,一些功能受到限制
此次演示使用Python,我们从0开始,演示如何建一个轮子。
- 需要的知识:
- 高数:求微分
- 线性代数:向量乘法
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
from lmfit import minimize, Parameters
#matplotlib inline
非球面的表示法¶
$$ z(r)=\frac{r^{2}}{R\left(1+\sqrt{1-(1 + \kappa) \frac{r^{2}}{R^{2}}}\right)}+\alpha_{4} r^{4}+\alpha_{6} r^{6}+\cdots $$- r 曲面到光轴的距离,假定是轴对称的曲面,r只有偶数次幂级数
描述一个非球面镜的参数:¶
- R:曲率半径
- $\kappa$
- 眼科常使用Q来代替$\kappa$
$\kappa$ | 意义 |
---|---|
$\kappa$ < -1 | 双曲线 |
$\kappa$ = -1 | 抛物线 |
-1 < $\kappa$ < 0 | 椭圆(prolate spheroid) |
$\kappa$ = 0 | 圆 |
$\kappa$ > 0 | 椭圆 (oblate spheroid) |
- 高次项系数:$\alpha_{4},\alpha_{6},\alpha_{8},\alpha_{10},\cdots$
- 镜面中心顶点的位置
- 镜面两侧介质的折射率
def z(r,surface_parameter):
R=surface_parameter["R"]
kappa=surface_parameter["kappa"]
a=surface_parameter['a']
partA = r**2 / (R*(1+np.sqrt(1-(1+kappa)*(r**2)/(R**2))))
partB = 0
for i in range(len(a)):
partB += a[i]*(r**(i*2+2))
return partA+partB
def surface(r, surface_parameter):
(z0,r0)=surface_parameter["apex"]
return z(r-r0, surface_parameter)+z0
ABC曲线图示:¶
如果直接看镜片的剖面,肉眼是很难分辨出来到底是球面,还是非球面,还是其他更复杂的非球面。于是有一种给眼科大夫们介绍的时候,厂商会使用一种光焦度图,横轴是IOL上某一点到光轴的距离r,纵轴是光焦度或者说屈光度。
对函数z(r)上每一点的曲率半径: $$ Radius=\frac{\left|z^{\prime \prime}(r)\right|}{\left(1+z^{\prime 2}(r)\right)^{3 / 2}} $$ 换算成屈光度 $$ Power=\frac{N_2-N_1}{Radius} \\ Power = \frac{(N_2-N_1)\left(1+z^{\prime 2}(r)\right)^{3 / 2}}{\left|z^{\prime \prime}(r)\right|} $$ 其中$N_1,N_2$是镜面两侧的折射率
已知:
$$ z(r)=\frac{r^{2}}{R\left(1+\sqrt{1-(1 + \kappa) \frac{r^{2}}{R^{2}}}\right)}+\alpha_{4} r^{4}+\alpha_{6} r^{6}+\cdots $$一阶导数: $$ z^{\prime}=\frac {\partial z}{\partial r} = \frac{r}{R \sqrt{1-\frac{r^{2}(1+\kappa)}{R^{2}}}} + 4 a_4 r^{3}+6 a_6 r^{5}+ \cdots $$ 二阶导数:
$$ z^{\prime \prime}=\frac {\partial ^2 z}{\partial r^2} =\frac{R^{3} \sqrt{1-\frac{r^{2}(1+\kappa)}{R^{2}}}}{\left(R^{2}-r^{2}(1+\kappa)\right)^{2}}+12 a_4 r^{2}+30 a_6 r^{4}+ \cdots $$def dz(r,surface_parameter):
R=surface_parameter["R"]
kappa=surface_parameter["kappa"]
a=surface_parameter['a']
# z'
dz_partA= r/(R* np.sqrt(1-(1+kappa)*r**2/R**2))
dz_partB = 0
for i in range(len(a)):
dz_partB +=(i*2+2) * a[i]*(r**(i*2+1))
return dz_partA+dz_partB
def dz2(r,surface_parameter):
R=surface_parameter["R"]
kappa=surface_parameter["kappa"]
a=surface_parameter['a']
dz2_partA= R**3 * np.sqrt(1-r**2 * (1+kappa)/R**2) / ((R**2-r**2 * (1+kappa))**2)
dz2_partB = 0
for i in range(len(a)):
dz2_partB +=(i*2+2)*(i*2+1) * a[i]*(r**(i*2))
return dz2_partA+dz2_partB
def surface_power(r,surface_parameter):
(n1,n2)=surface_parameter['refractive_index']
d1=dz(r,surface_parameter)
d2=dz2(r,surface_parameter)
power=(n2-n1)*(1+d1**2)**(3/2)/ np.abs(d2)
return power
测试绘图¶
def draw_surface(r, surface_parameter, ax):
ax.plot(surface(r,surface_parameter),r)
ax.scatter(surface_parameter['apex'][0],surface_parameter['apex'][1])
return ax
def draw_power(r, surface_parameter, ax):
ax.plot(r,surface_power(r,surface_parameter))
return ax
surface_0={
"R":10,
"kappa": +0.5, # 𝜅 > 0 椭圆 (oblate spheroid)
"a":[0,0,0,0],
'apex':[0,0],
'refractive_index':[1,1/0.9] # before and after
}
透镜剖面图¶
r = np.linspace(-3, 3, 500)
fig, ax = plt.subplots()
plt.axis('equal')
ax=draw_surface(r, surface_0, ax)
plt.show()
透镜屈光度图¶
𝜅 > 0 椭圆 (oblate spheroid),中央屈光度更高,周边逐渐降低,这是一个“负球差”透镜
r = np.linspace(0, 3, 100)
fig, ax = plt.subplots()
ax=draw_power(r, surface_0, ax)
plt.show()
Ray Tracing(光路追迹,光线追踪)¶
- 光线在均一介质中传播
- 光线在界面上的折射
光线在均一介质中传播¶
直线的方程, 从点$P_0$沿着方向矢量$\vec l$ 传播距离d, 到达点$P(d)$
$$ P(d)=P_0+d \vec l $$考虑r方向和z方向两个分量 $$ P(d)_r=P_{0r}+d_r l_r \\ P(d)_z=P_{0z}+d_z l_z \\ 其中 \frac{d_r}{d_z}=\frac{l_r}{l_z} $$
def travel_in_space(p0,vec_l,dz):
vec_lz =vec_l[:,0].reshape(-1,1)
vec_l_in_z=vec_l/vec_lz
dz=dz.reshape(-1,1)
pdz=p0+dz*vec_l_in_z
return pdz
折射¶
$$ n_1 Sin(\theta_1) = n_2 Sin(\theta_2) $$参考 https://en.wikipedia.org/wiki/Snell's_law 中的矢量形式
- 入射光方向: $\vec l$
- 曲面的法向量: $\vec n$
- 入射面介质折射率: $n_1$
- 出射面介质折射率: $n_2$
def V_refract(vec_l,vec_n,n1,n2):
r=n1/n2
if len(vec_l.shape)>1:
c=np.diag(-np.matmul(vec_n, np.transpose(vec_l))).reshape(-1,1)
else:
c=-np.matmul(vec_n.reshape(1,-1),vec_l.reshape(-1,1))
V=r*vec_l + (r*c-np.sqrt(1-r**2*(1-c**2))) * vec_n
return V/np.linalg.norm(V,axis=1,keepdims=True)
# 测试:
vec_l=np.asarray([[0.707107,-0.707107],[0.707107,-0.707107],[0.707107,-0.707107]])
vec_n=np.asarray([[0,1],[0,1],[0,1]])
n1=1
n2=1/0.9
V_refract(vec_l,vec_n,n1,n2)
# should be:
# [ 0.6364, -0.7714]
曲面的法向量: $\vec n$¶
- 简化,仅仅考虑二维
已知有曲面 $$ z=z(r) $$ 则曲面在(z0,r0)位置的法向量: $$ \left[ \begin{array}{c} -1\\ \frac{\partial }{\partial r}z\left(r \right) |_{r=r0} \end{array} \right] $$
def norm_vec(r,surface_parameter):
R=surface_parameter["R"]
kappa=surface_parameter["kappa"]
a=surface_parameter['a']
V=-1*np.ones([r.shape[0],2])
V[:,1]=dz(r,surface_parameter)
return V/np.linalg.norm(V,axis=1,keepdims=True )
绘制曲面上的法向量¶
fig, ax = plt.subplots()
plt.axis('equal')
# 绘制曲面
r = np.linspace(-4, 4, 500)
ax=draw_surface(r, surface_0, ax)
# 计算法向量
N=5
rn = np.linspace(-3, 3, N)
norm_v=norm_vec(rn,surface_0)
zn = surface(rn, surface_0)
zn1 = zn+norm_v[:,0]
rn1 = rn+norm_v[:,1]
for i in range(N):
ax.plot([zn[i],zn1[i]],[rn[i],rn1[i]])
plt.show()
在曲面上折射¶
def refract_from_surface(p, vec_l,surface_parameter):
(z0,r0)=surface_parameter["apex"]
(n1,n2)=surface_parameter['refractive_index']
r = p[:,1]
r = r-r0
vec_n = norm_vec(r,surface_parameter)
return V_refract(vec_l,vec_n,n1,n2)
薄透镜近似¶
- 薄透镜
- 直线与曲线的交点,化简成为直线与直线的交点. 这个简化大大降低了运算难度,但也引入很大误差
- 暂不考虑瞳孔大小
已知模型,模拟成像过程¶
一组平行光,经过两个非球面镜,聚焦到屏幕上。
- 非球面镜0: 镜头垂直于光轴,镜头中心位于光轴上,是一个简单的椭球面
- 非球面镜1: 镜头垂直于光轴,但镜头中心有一定的偏位
- 屏幕:垂直于光轴
surface_0={
"R":8,
"kappa": -0.8,
"a":[0],
'apex':[0,0],
'refractive_index':[1,1.3] # before and after
}
surface_1={
"R":7,
"kappa": 0.5,
"a":[0,0],
'apex':[4,1],
'refractive_index':[1.3, 1.5] # before and after
}
surface_2={
"R":1e+10,
"kappa": 0,
"a":[0,0],
'apex':[30,0],
'refractive_index':[1.5, 1.5] # before and after
}
Ray Tracing(光路追迹,光线追踪)¶
追踪每一条光线所走的路径:
- 在均一介质内直线行进:位置改变,方向不变
- 在镜头界面上折射:位置不变,方向改变
依次穿过每一个透镜界面,计算出最终在屏幕上光线的位置
def ray_tracing(light_position_start, light_direction_start, surfaces):
# 记录光线的位置
light_postion_list=[light_position_start]
# 从被追踪光线的起始位置和起始方向开始:
light_position=light_position_start
light_direction=light_direction_start
for s in surfaces: # 遍历每一个曲面
#光线在均匀介质内传播,到达该曲面
dz=s['apex'][0]-light_position[:,0]
light_position=travel_in_space(light_position,light_direction,dz)
#光线经过该曲面发生折射
light_direction=refract_from_surface(light_position, light_direction,s)
#记录下光线的位置
light_postion_list.append(light_position)
return light_postion_list
def init_light(N):
#初始化光线
light_position_start=-5*np.ones([N,2]) #从z=-5平面开始发出光线
light_position_start[:,1]= np.linspace(-3, 3, N)
light_direction_start=np.zeros([N,2])
light_direction_start[:,0]=1 #方向矢量(1,0),指向Z轴方向
return light_position_start,light_direction_start
def draw_ray_tracing(N,surfaces):
# 绘制出各个曲面的位置:
fig, ax = plt.subplots()
plt.axis('equal')
r = np.linspace(-5, 5, 500)
for s in surfaces:
ax=draw_surface(r, s, ax)
# 光线的起点和方向
# N=3 # 光线数量
light_position_start,light_direction_start = init_light(N)
# 追踪光线,获得光线在各个曲面上经过时的坐标
light_postion_list=ray_tracing(light_position_start, light_direction_start, surfaces)
# 绘制出每一根光线
for i in range(N):
x=[light_postion[i,0] for light_postion in light_postion_list]
y=[light_postion[i,1] for light_postion in light_postion_list]
ax.plot(x,y)
# ax.set_xlim(28, 32)
# ax.set_ylim(-2,2)
plt.show()
# 绘制ray tracing过程:
draw_ray_tracing(10,[surface_0,surface_1,surface_2])
优化模型¶
当非球面镜1偏位时,每一条光线最好都能够落在焦点附近,
- 注意:由于镜头偏位,所以焦点并不一定在光轴上,
- “焦点”,各个光线汇聚的点,或者说,光线的平均位置。
- 简化:每一条光线在屏上的位置 尽量靠近 非球面镜1的偏位距离
- 尽量靠近:到达给定位置的平方和最小
优化问题: 求lossfunction的最小值: $$ loss =\sum{偏位距离=-\Delta}^{+\Delta} \sum_{光线_i =0 }^{N} |光线_i 在屏上位置- 非球面镜_1 的偏位距离|^2 $$
def loss_function(params, light_position_start,light_direction_start ,data=None):
spots_on_screen_list=[]
Delta=0.9
for apex_r in np.linspace(-Delta,Delta,10): # 将曲面在一定范围内移动
surface_t={
"R":params["R"],
"kappa": params["kappa"],
"a":[params["a4"],params["a6"],params['a8'],params['a10']],
'apex':[4,apex_r],
'refractive_index':[1.3, 1.5] # before and after
}
surfaces=[surface_0,surface_t,surface_2]
# 追踪光线,获得光线在各个曲面上经过时的坐标
light_postion_list=ray_tracing(light_position_start, light_direction_start, surfaces)
# 希望屏上的光线落在偏位点的附近
spots_on_screen=(light_postion_list[-1][:,-1]-apex_r)
spots_on_screen_list.append(np.abs(spots_on_screen))
return spots_on_screen_list
优化器¶
lmfit,scipy优化器的封装,
- 设定好需要优化的参数: $ R, \kappa, a_4,a_6,a_8,a_{10} $
- 设定目标loss_function
求解使得loss_function取得最小值的参数表
from lmfit import Minimizer, Parameters, report_fit
# 定义待求解的参数,以及初值,参数的数值范围
params = Parameters()
# add with tuples:(NAME, 初始值, 求解, Min MAX EXPR BRUTE_STEP)
params.add_many( ('R', 7, True, 6, 12, None, None),
('kappa', 0, True, None, None, None, None),
('a4', 0, True, None, None, None, None),
('a6', 0, True, None, None, None, None),
('a8', 0, True, None, None, None, None),
('a10', 0, True, None, None, None, None),
)
# 输入N条光线
N=100
light_position_start,light_direction_start = init_light(N)
# 求解参数
minner = Minimizer(loss_function, params,
fcn_args=(light_position_start,light_direction_start,_),
nan_policy="propagate")
result = minner.minimize()
结果¶
print("R={:.2f}".format(result.params["R"].value))
print("kappa={:.2f}".format(result.params["kappa"].value))
print("a4={:.2e}".format(result.params["a4"].value))
print("a6={:.2e}".format(result.params["a6"].value))
print("a8={:.2e}".format(result.params["a8"].value))
print("a10={:.2e}".format(result.params["a10"].value))
def get_surface_target(result,apex_r):
return {
"R":result.params["R"].value,
"kappa": result.params["kappa"].value,
"a":[result.params["a4"].value,
result.params["a6"].value,
result.params['a8'].value,
result.params['a10'].value],
'apex':[4,apex_r],
'refractive_index':[1.3, 1.5] # before and after
}
# surface 0 的光焦度曲线
fig, ax = plt.subplots()
r = np.linspace(0, 2.5, 500)
ax=draw_power(r, surface_0, ax)
plt.show()
# surface 1 的光焦度曲线
fig, ax = plt.subplots()
# plt.axis('equal')
r = np.linspace(0, 3, 500)
# ax=draw_surface(r, surface_0, ax)
ax=draw_power(r, get_surface_target(result,1), ax)
plt.show()
# 不同偏位状态时的聚焦情况
for apex_r in np.linspace(0,1,3):
draw_ray_tracing(10,[surface_0,get_surface_target(result,apex_r),surface_2])