Abstract
There is renewed interest in the role of optics in human vision. At the same time there have been advances that allow for routine standardized measurement of the wavefront aberrations of the human eye. Computational methods have been developed to convert these measurements to a description of the human visual optical point spread function (PSF), and to thereby calculate the retinal image. However, tools to implement these calculations for vision science are not widely available or widely understood. In this report we describe software to compute the human optical PSF, and we discuss constraints and limitations.
To motivate the subsequent developments, we provide a very brief example of the use of this software. We begin with a list of 12 Zernike coefficients,
zc = {{2,−2,−0.0946},{2,0,0.0969},{2,2,0.305},{3,−3,0.0459},{3,−1,−0.121},{3,1,0.0264},{3,3,−0.113},{4,−4,0.0292},{4,−2,0.03},{4,0,0.0294},{4,2,0.0163},{4,4,0.064}};
psf = ZernikePointSpread[zc];
We have an image of the letter “E” in the Sloan font with a height of 10 arcmin, corresponding to acuity of 20/40.
We convolve the PSF with the letter image, and display the result,
blurredletter = ImageConvolve[letter, Wrap[psf]]
We begin with a set of Zernike coefficients. These are from right eye #1 at 6 mm from the database discussed below (
ThibosHongBradleyChengData; Thibos, Hong et al.,
2002), from order 2 to order 6.
{{2, −2,−0.0946},{2,0,0.0969},{2,2,0.305},{3,−3,0.0459},{3,−1,−0.121},{3,1,0.0264},{3,3,−0.113},{4,−4,0.0292},{4,−2,0.03},{4,0,0.0294},{4,2,0.0163},{4,4,0.064},{5,−5,0.0499},{5,−3,−0.0252},{5,−1,0.00744},{5,1,0.00155},{5,3,−0.00686},{5,5,0.0288},{6,−6,0.00245},{6,−4,0.00185},{6,−2,0.00122},{6,0,−0.00755},{6,2,−0.000693},{6,4,0.000551},{6,6,−0.0148}}
These can be plotted against the single number index mode, with the function ZernikePlot.
ZernikePlot[TestCoefficients]
If the eye is viewing an image that is spectrally homogeneous (it varies only in intensity, not spectrally), then we can compute an appropriate single PSF for the relevant spectrum. We do this by representing the spectrum by a set of sampled wavelengths and corresponding weights. We then compute a set of PSFs, one for each wavelength, with chromatic aberration, as in the example above. Then we multiply by the corresponding weights and sum them up.
Here is an example spectrum. The weights add up to one.
spectrum = {{455,0.00477},{475,0.0727},{495,0.175},{515,0.22},{535,0.198},{555,0.143},{575,0.0896},{595,0.0502},{615,0.0258},{635,0.0123},{655,0.00558},{675,0.0024}}
ListLinePlot[spectrum,ZernikeStyles]
First we will create the individual PSFs separately, for illustration, and plot them labeled by their wavelengths.
wavelengths = First /@ spectrum;
psfs = ZernikePointSpread[TestCoefficients,wavelengths] ;
figs = PSFPlot[#,ImageSize→100,FrameLabel→None,FrameTicks→None]& /@ psfs;
Grid[Partition[MapThread[
Labeled[##1,Top, LabelStyle→{12, FontFamily→"Helvetica"}]&
,{figs,wavelengths}],6],Spacings→0]
Combining these with appropriate weights, we get the single PSF.
weights = Last /@ spectrum;
psf = Total[psfs weights];
Here is how to create the polychromatic PSF in a single call.
psf = ZernikePointSpread[TestCoefficients,spectrum];
In modern electronic displays, color images are produced by combining three (or more) images, each created with a specific primary. The individual images are spectrally homogeneous, but their individual spectra may be broad band. In this case, the spectrum argument to ZernikePointSpread is a list of spectra, and the result is a list of PSFs (or OTFs, or {PSF, OTF} pairs, depending on the OTF option).
Here is an example of spectra for three bands, nominally red, green, and blue. These are approximations to spectra of organic light-emitting diode (OLED) display primaries. Each has weights that sum to 1.
spectra = {{535,0.00986},{555,0.088},{575,0.163},{595,0.19},{615,0.176},{635,0.143},{655,0.106},{675,0.0741},{695,0.0493}},{{455,0.00477},{475,0.0727},{495,0.175},{515,0.22},{535,0.198},{555,0.143},{575,0.0896},{595,0.0502},{615,0.0258},{635,0.0123},{655,0.00558},{675,0.0024},{695,0.000991}},{{415,0.000458},{435,0.0503},{455,0.157},{475,0.217},{495,0.204},{515,0.154},{535,0.0994},{555,0.0578},{575,0.031},{595,0.0156},{615,0.00744},{635,0.0034},{655,0.0015},{675,0.000641},{695,0.000266}}};
ListLinePlot[spectra,PlotStyle→{Red,Green,Blue},
FrameLabel→{"nm”,"weight"}, ZernikeStyles]
We compute both PSF and OTF for the three primaries.
tri = ZernikePointSpread[TestCoefficients,spectra,OTF→"Both"];
The result has the following dimensions.
For clarity, we rearrange and name the parts.
{psfs,otfs} = Transpose[tri];
PSFPlot[#1, ImageSize→160, FrameLabel→None, FrameTicks→None,
PlotLabel→Style[#2, #3]]& ,
{tri[[All,1]], {"R,” “G,” “B"}, {Red, Green, Blue}}]]
We compare the radial MTFs. The green primary has the best gain, consistent with the most compact PSF.
RadialMTF[otfs, PlotStyle→{Red, Green, Blue}]
To illustrate the use of the trichromatic PSF to compute the trichromatic retinal image, we again create a letter image. The letter is again 10 arcmin in size, but this time the image is only 0.25°.
fontsizepixels = fontsize imagesize/degrees;
letter = LetterImage["Z”, ImageSize→imagesize, FontFamily→"Sloan”,
We blur this image by each of the three trichromatic PSFs, creating three blurred images that represent the red, green, and blue color channels
blurredcolorletter = ImageConvolve[letter,Wrap[#]]& /@ psfs;
We look at the individual blurred color channels.
Row[Framed /@ blurredcolorletter]
We simulate the blurred color image by combining the three blurred color channels as an RGB image.
Framed @ ColorCombine[blurredcolorletter ,"RGB"]
Here we apply the PSF to a more complicated and colorful image. This image is 160 × 107 pixels, or 0.312° × 0.209° (512 pixels/°).
We use the Mathematica functions ColorSeparate and ColorCombine to separate the three color channels, and to recombine them after filtering.
ColorCombine[MapThread[ImageConvolve,
{ColorSeparate[colorimage], Reverse[Wrap[#]]& /@ psfs}]]
Zernike coefficients are measured over a pupil of a specific diameter. Measurements at several pupil diameters for the same eye may be available. It is also possible to numerically transform Zernike coefficients from one pupil diameter to another (usually smaller) diameter (Bara et al.,
2006; Dai,
2006; Díaz et al.,
2009; Janssen & Dirksen,
2006; Mahajan,
2010; Schwiegerling,
2002). We make use of the formula of Dai (
2006) for the new coefficients
b based on the old coefficients
a,
where
r is the ratio of new to old pupil diameters,
o is the highest order considered. The formulas of Diaz et al. (
2009) and Janssen et al. (
2006) are equally valid; we use Dai (
2006) because it is a relatively simple expression and it appears to be the fastest to compute in our environment. We note that this formula can be used to scale for smaller or larger pupils (ratios less than or greater than 1), though the error is larger for ratios greater than 1 (Bará, Pailos, Arines, López-Gil, & Thibos,
2014; Dai,
2011; Ommani, Hutchings, Thapa, & Lakshminarayanan,
2014).
We provide the function ZernikeScalePupil to scale coefficients for smaller or larger pupils. In this example, we scale the TestCoefficients from their original 6-mm pupil diameter to a new 3-mm diameter. The second argument is the ratio of new to old pupil diameters. We plot the old and new coefficients.
zc = ZernikeScalePupil[TestCoefficients,3/6];
ZernikePlot[{TestCoefficients,zc},
PlotLegends→Placed[{"6 mm”,"3 mm"},{Right,Top}]]
We create the corresponding PSFs and OTFs.
{psfs, otfs} = Transpose[MapThread[
ZernikePointSpread[#1, PupilDiameter→#2, OTF→"Both"] &,
{{TestCoefficients, zc}, {6, 3}}]];
The 3-mm PSF is more compact and shows fewer obvious aberrations.
Likewise the radial MTF shows higher gain over most frequencies for the 3-mm pupil diameter.
RadialMTF[otfs, PlotStyle→{Red,Blue},
PlotLegends→Placed[{"6 mm”,"3 mm"},{Left,Bottom}]]
Defocus and astigmatism are of interest in part because they are aberrations that can be corrected with spectacle lenses. We can easily compute PSFs and OTFs with varying amounts of defocus or astigmatism. For defocus we use the function InverseEquivalentDefocus to compute the magnitude of the Zernike coefficient corresponding to defocus. For example, for a 6-mm pupil and 1 diopter of defocus,
c = InverseEquivalentDefocus[1., 6]
To update a list of coefficients we simply append the defocus term, with the appropriate values of order, frequency, and magnitude: {2,0,1.29904}. Note that it does not matter whether a defocus term already exists in the list (as it does here); the two terms will be correctly combined by ZernikePointSpread.
Append[TestCoefficients, {2, 2, c}]
{{2,−2,−0.0946},{2,0,0.0969},{2,2,0.305},{3,−3,0.0459},{3,−1,−0.121},{3,1,0.0264},{3,3,−0.113},{4,−4,0.0292},{4,−2,0.03},{4,0,0.0294},{4,2,0.0163},{4,4,0.064},{5,−5,0.0499},{5,−3,−0.0252},{5,−1,0.00744},{5,1,0.00155},{5,3,−0.00686},{5,5,0.0288},{6,−6,0.00245},{6,−4,0.00185},{6,−2,0.00122},{6,0,−0.00755},{6,2,−0.000693},{6,4,0.000551},{6,6,−0.0148},{2,2,1.3}}
To create the corresponding PSF we would simply supply this new list to ZernikePointSpread. Here we illustrate creation of a family of PSF and OTF, with defocus proceeding from 0 to 2 diopters in steps of 1/2 diopter. We set Degrees → 1 to allow room for the large PSF at the largest defocus.
result = ZernikePointSpread[
Append[TestCoefficients, {2, 0, InverseEquivalentDefocus[#, 6]}],
Degrees→1., OTF→"Both"] & /@ Range[0, 2, .5];
Now we plot the PSFs and radial MTFs.
Row[PSFPlot[#, Degrees→1, ImageSize→100,
FrameTicks → {{−20, 0, 20}, None, None, None},
FrameLabel→None] & /@ result[[All, 1]]]
Row[RadialMTF[#, Degrees→1, ImageSize→100,
FrameTicks→{{1, 10, 50}, None, None, None},
FrameLabel→None] & /@ result[[All, 2]]]
Turning now to astigmatism, we use a more general function that allows the user to specify the defocus (in diopters), the astigmatism (in diopters), and the angle of the astigmatism (in radians). For example, 1 diopter of defocus and 0.2 diopters of astigmatism at an angle of 0.3 at a pupil diameter of 6 mm, would yield
SpheroCylindricalCoefficients[1., 0.2, 0.3, 6]
{{2,−2,0.207},{2,0,1.3},{2,2,0.303}}
The function returns a list of second order Zernike coefficients that can then be supplied to ZernikePointSpread. In the following example, we show a sequence of PSFs and OTFs with constant defocus (0.5 diopters) and increasing astigmatism at angle 0. Astigmatism results in blur in one direction defined by the angle, so the PSF gets broader in that dimension, while the MTF gets narrower.
defocus = 0.5; pupil = 6; angle = 0.;
options = Sequence @@ {ImageSize→180, FrameTicks→{None, Automatic}};
Grid[Transpose[({psf, otf} = ZernikePointSpread[
SpheroCylindricalCoefficients[defocus, #, angle, pupil],
PupilDiameter→pupil, OTF→"Both"];
{PSFPlot[psf, FrameLabel→{None, “arcmin"}, options],
MTFPlot[otf, FrameLabel→{None, “cycles/deg"},
Magnification→4, options]}
In the following example we fix the defocus, and vary the angle of astigmatism. Note that the MTF is always elongated orthogonal to the PSF.
defocus = 0.5; pupil = 6; astigmatism = 0.2;
Sequence @@ {ImageSize→180, FrameTicks→{None, Automatic}};
Grid[Transpose[({psf, otf} = ZernikePointSpread[
SpheroCylindricalCoefficients[defocus, astigmatism, #, pupil],
PupilDiameter→pupil, OTF→"Both"];
{PSFPlot[psf, FrameLabel→{None, “arcmin"}, options],
MTFPlot[otf, FrameLabel→{None, “cycles/deg"},
Magnification→4, options]}
A second consideration is that the spacing of samples in the OTF is 1/d. Likewise the lowest frequency represented (apart from zero) is 1/d. Thus if an accurate and fine-grained representation of the OTF or MTF is desired, or if one is interested in behavior at low spatial frequencies, a relatively large value of d must be used. In the following example, we compute the OTF from the coefficients of the right eye of observer 10, using a PSF size of 0.25 degree or 2.0 degrees. As can be seen, several features of the MTF are evident only for the larger value.
zc = ThibosHongBradleyChengData[[3, 10, 2]];
otfs = ZernikePointSpread[zc,
Degrees→#,OTF→True,ImageSamples→1024]& /@ {.25,2};
Show[Reverse@MapThread[RadialMTF[#1,PlotStyle→#2,Degrees→#3]&,
{otfs,{{Red,Dashed},Blue},{.25,2}}]
,Graphics[{Text["Degrees”,Scaled[{.8,.9}]],
Red,Text["0.25”,Scaled[{.8,.7}]],
Blue,Text["2.0”,Scaled[{.8,.8}]]}]]