其实这又是一篇optiland的教程。
更新:第一版的顶点距离设定错了,设定成了1e-4,因为是从我其他程序里copy出来的,那个程序里在后续位置设定了框架镜。这一版修订成12mm,所以每毫米眼轴增长对应的近视度数结果略有差异。
一个常见的临床问题:患者眼轴增长了1mm,近视度数会增长多少?我们可以实际算一算。注意,我们这里使用的仍然是Gullstand模型眼,其实它并不适用于近视,所以只是一个半定量的计算,主要目的还是为了熟悉optiland的使用。
建立模型眼
当然我们先要导入必要的库
try:
from optiland.optic import Optic
except:
! pip install optiland[torch]
from optiland.optic import Optic
import numpy as np
import matplotlib.pyplot as plt
from optiland.materials import AbbeMaterial
from optiland import analysis, optimization, solves
from optiland.mtf import FFTMTF, SampledMTF
from optiland.solves.quick_focus import QuickFocusSolve
我们按照之前的教程:用optiland建立一个Gullstrand模型眼建立一个Gullstrand模拟眼。为了后续方便,这次使用python的“类”,也就是把上次一条一条写的模拟眼设定,给放在一起,以后方便调用。一个类里面,首先要有一个初始化的函数__init__
class Model_Eye(Optic):
def __init__(self,
power=0.0,
pupil_size=4.0
):
super().__init__()
# wavelength
self.wavelength_F= 0.48613
self.wavelength_d= 0.58756
self.wavelength_C= 0.65627
self.add_wavelength(value = self.wavelength_F)
self.add_wavelength(value = self.wavelength_d)
self.add_wavelength(value = self.wavelength_C)
# material
material_aqueous = AbbeMaterial(n=1.336981, abbe=52.658991)
material_cornea = AbbeMaterial(n=1.376981, abbe=56.279936)
material_lens = AbbeMaterial(n=1.419976, abbe= 51.226142)
material_vitreous = AbbeMaterial(n=1.335982, abbe=53.342173)
# aperture
self.set_aperture(aperture_type="float_by_stop_size", value = pupil_size )
# FOV
self.set_field_type(field_type="angle")
self.add_field(y=0)
f = 1000/power if power != 0 else np.inf
surface_index = 0
self.add_surface(index = surface_index , radius = np.inf, thickness =np.inf, comment = 'Object')
surface_index += 1
self.add_surface(index = surface_index, comment = 'Phoropter',
surface_type = 'paraxial',
thickness = 12, f = f
)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Cornea',
surface_type='standard',
radius = 7.70, thickness = 0.55,
material=material_cornea,
aperture=8)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Aqueous',
surface_type='standard',
radius = 6.80, thickness = 3.6,
material=material_aqueous,
aperture=8)
surface_index += 1
self.add_surface(index = surface_index, comment = 'Pupil',
surface_type='standard',
radius = np.inf, thickness = 0,
material=material_aqueous,
aperture = pupil_size,
is_stop=True)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Lens front',
surface_type='standard',
radius = 10.0, thickness = 0.546,
material=AbbeMaterial(n=1.386, abbe=50.23),
aperture=8)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Lens front core',
surface_type='standard',
radius = 7.91, thickness = 2.419,
material=AbbeMaterial(n=1.406, abbe=50.23),
aperture=8)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Lens back core',
surface_type='standard',
radius = -5.76, thickness = 0.635,
material=AbbeMaterial(n=1.386, abbe=50.23),
aperture=8)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Vitreous',
surface_type='standard',
radius = -6.0,
thickness = 17.185,
material=material_vitreous,
aperture=8)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Retina',
surface_type='standard',
radius = -12.0, thickness =0,
aperture=8)
通过对焦计算玻璃体腔厚度
这就是上一次建立模拟眼的过程。
然后,我们加入一个对焦的函数,让它来修改玻璃体厚度,这也在上一讲中讲过。这个函数也是放在类的定义中的。
class Model_Eye(Optic):
def __init__(self,
power=0.0,
pupil_size=4.0
):
......
def def quick_focus(self):
......
具体内容也和上次差不多,只是定位surface_number的时候,用了整个总面数来推导,在optiland中是从0开始计数的,所以物体面是第0面,比如一共10个面,那么视网膜面就是第9号面,而玻璃体面就是再之前一个面,是总面数-2。
def quick_focus(self):
problem = optimization.OptimizationProblem()
problem.add_variable(self, "thickness", surface_number = self.surface_group.num_surfaces-2 , min_val=10, max_val=30)
input_data = {
"optic": self,
"surface_number": -1,
"Hx": 0,
"Hy": 0,
"num_rays":16,
"wavelength": self.wavelength_d,
"distribution": "uniform",
}
problem.add_operand(
operand_type = "rms_spot_size",
target=0.0,
weight =10,
input_data=input_data,
)
optimizer = optimization.OptimizerGeneric(problem)
optimizer.optimize()
# Update the vitreous humor thickness with the optimized value
optimized_thickness = problem.variables[0].value
self.surface_group.surfaces[8].thickness = optimized_thickness
计算眼轴长
在计算眼轴长的时候,可以把视网膜面的位置(position)减去角膜面的位置。当然也可以直接把各个面的厚度加起来,但没有这样来得优雅。@property是一个“装饰器”,这样读取眼轴长的时候,就可以写成Model_Eye.axial_length
@property
def axial_length(self):
AL=self.surface_group.positions[-1]-self.surface_group.positions[-1-7]
return AL
整合在一起
把模拟眼这个“类”整合在一起的结果就是这样的:
class Model_Eye(Optic):
def __init__(self,
power=0.0,
pupil_size=4.0
):
super().__init__()
# wavelength
self.wavelength_F= 0.48613
self.wavelength_d= 0.58756
self.wavelength_C= 0.65627
self.add_wavelength(value = self.wavelength_F)
self.add_wavelength(value = self.wavelength_d)
self.add_wavelength(value = self.wavelength_C)
# material
material_aqueous = AbbeMaterial(n=1.336981, abbe=52.658991)
material_cornea = AbbeMaterial(n=1.376981, abbe=56.279936)
material_lens = AbbeMaterial(n=1.419976, abbe= 51.226142)
material_vitreous = AbbeMaterial(n=1.335982, abbe=53.342173)
# aperture
self.set_aperture(aperture_type="float_by_stop_size", value = pupil_size )
# FOV
self.set_field_type(field_type="angle")
self.add_field(y=0)
f = 1000/power if power != 0 else np.inf
surface_index = 0
self.add_surface(index = surface_index , radius = np.inf, thickness =np.inf, comment = 'Object')
surface_index += 1
self.add_surface(index = surface_index, comment = 'Phoropter',
surface_type = 'paraxial',
thickness = 1e-4, f = f
)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Cornea',
surface_type='standard',
radius = 7.70, thickness = 0.55,
material=material_cornea,
aperture=8)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Aqueous',
surface_type='standard',
radius = 6.80, thickness = 3.6,
material=material_aqueous,
aperture=8)
surface_index += 1
self.add_surface(index = surface_index, comment = 'Pupil',
surface_type='standard',
radius = np.inf, thickness = 0,
material=material_aqueous,
aperture = pupil_size,
is_stop=True)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Lens front',
surface_type='standard',
radius = 10.0, thickness = 0.546,
material=AbbeMaterial(n=1.386, abbe=50.23),
aperture=8)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Lens front core',
surface_type='standard',
radius = 7.91, thickness = 2.419,
material=AbbeMaterial(n=1.406, abbe=50.23),
aperture=8)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Lens back core',
surface_type='standard',
radius = -5.76, thickness = 0.635,
material=AbbeMaterial(n=1.386, abbe=50.23),
aperture=8)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Vitreous',
surface_type='standard',
radius = -6.0,
thickness = 17.185,
material=material_vitreous,
aperture=8)
surface_index += 1
self.add_surface(index = surface_index , comment = 'Retina',
surface_type='standard',
radius = -12.0, thickness =0,
aperture=8)
def quick_focus(self):
problem = optimization.OptimizationProblem()
problem.add_variable(self, "thickness", surface_number = self.surface_group.num_surfaces-2 , min_val=10, max_val=30)
input_data = {
"optic": self,
"surface_number": -1,
"Hx": 0,
"Hy": 0,
"num_rays":16,
"wavelength": self.wavelength_d,
"distribution": "uniform",
}
problem.add_operand(
operand_type = "rms_spot_size",
target=0.0,
weight =10,
input_data=input_data,
)
optimizer = optimization.OptimizerGeneric(problem)
optimizer.optimize()
# Update the vitreous humor thickness with the optimized value
optimized_thickness = problem.variables[0].value
self.surface_group.surfaces[8].thickness = optimized_thickness
def reset_phoropter(self,power=0.0):
self.surface_group.surfaces[1].f = 1000/self.power if power != 0 else np.inf
@property
def axial_length(self):
AL=self.surface_group.positions[-1]-self.surface_group.positions[-1-7]
return AL
计算近视度数与眼轴长之间的关系
当这个模拟眼设计好了以后,我们就可以代入不同的近视度数,计算其眼轴长,得到结果了。注意这并非一个严谨的测试,因为Gullstrand模型眼并非适用于近视。我们只是假定了除了玻璃体厚度,眼球的任何结构与近视发展都无关,这个假设其实并不成立。更合适的方法是使用近视专用的模型眼,比如Atchison模型眼,但它需要使用梯度折射率来描述晶状体,当前的optiland还不支持。
下面这一段代码,一口气画出了从-10D到+5D的度数与眼轴的关系,
power_list = np.linspace(-10,5,16)
AL_list = np.zeros(len(power_list))
for power in power_list:
my_eye = Model_Eye(power = power, pupil_size= 4)
my_eye.quick_focus()
AL_list[power_list==power]=my_eye.axial_length
plt.plot(AL_list, power_list,)
plt.scatter(AL_list, power_list)
plt.ylabel('Power (dB)')
plt.xlabel('Axial Length (mm)')
plt.show()

可以看出几乎是线性的。
我们做一个简单的线性拟合:
# Fit a linear equation to the data
coefficients = np.polyfit(AL_list,power_list, 1)
# Print the coefficients (slope and intercept)
print(f"Slope: = {coefficients[0]:.2f}D")
x = np.linspace(21,29,100)
y = coefficients[0]*x+coefficients[1]
plt.plot(x,y, "--")
plt.scatter(AL_list, power_list)
plt.ylabel('Power (dB)')
plt.xlabel('Axial Length (mm)')
plt.show()

计算得到斜率 = -2.67, 也就是1mm眼轴增长,大致对应的是-2.67D近视度数。