File: PsychOpticsTest.m

package info (click to toggle)
psychtoolbox-3 3.0.19.14.dfsg1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 86,796 kB
  • sloc: ansic: 176,245; cpp: 20,103; objc: 5,393; sh: 2,753; python: 1,397; php: 384; makefile: 193; java: 113
file content (228 lines) | stat: -rw-r--r-- 11,006 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
% Basic tests and comparisons of PsychOptics routines
%
% Description:
%     Basic tests and comparisons of PsychOptics routines, in particular our ability
%     to go back and forth between LSFs and PSFs and between PSFs and OTFs.
% 
%     Also useful for remembering the usage of various routines.
% 
%     This also makes some useful plots that compare different estimates of
%     monochromatic human optics from the older literature.  These estimates
%     are probably not as good as using wavefront methods (see isetbio at
%     isetbio.org for data and code), but it is useful to have them for
%     comparing with calculations in the literature that used these estimates.

% History:
%   01/04/18  dhb   Added some regression tests.
%   01/25/18  dhb   Typo fix on direction of one check conversion.

%% Clear
clear; close all; 

%% LSF <-> PSF conversions

% Compute Westheimer and Davila/Geisler LSFs from the formulae provided.
%
% Set up spatial sampling in one-dimension, both in space domain and
% corresponding frequency domain.
% For the symmetric phase component to work nSamples MUST BE EVEN and the
% 1D LSF support must be long enough so that the LSF goes to zero

% Define spatial support.
%
% Number of samples can be even or odd.
nSpatialSamples = 513;
centerPosition = floor(nSpatialSamples/2)+1;
if (rem(nSpatialSamples,2) == 0)
    integerSamples1D = -nSpatialSamples/2:nSpatialSamples/2-1;
else
    integerSamples1D = -floor(nSpatialSamples/2):floor(nSpatialSamples/2);
end
maxPositionMinutes = 8;
maxSfCyclesPerDegree = 60*(nSpatialSamples/2)/(2*maxPositionMinutes);
positionMinutes1D = maxPositionMinutes*integerSamples1D/(centerPosition-1);

% These produce similar lsf's for the human eye, from the literature.
WestLSF = WestLSFMinutes(abs(positionMinutes1D));
GeislerLSF = GeislerLSFMinutes(abs(positionMinutes1D));
DavilaGeislerLSF = DavilaGeislerLSFMinutes(abs(positionMinutes1D));

% Westheimer also gives a formula for the PSF (in addition to the LSF).
%
% Get this for comparison.
[xGridMinutes,yGridMinutes] = meshgrid(positionMinutes1D,positionMinutes1D);
radiusMinutes2D = sqrt(xGridMinutes.^2 + yGridMinutes.^2);
WestPSFFormula = WestPSFMinutes(abs(radiusMinutes2D));
WestPSFFormula = WestPSFFormula/sum(WestPSFFormula(:));
if (WestPSFFormula(centerPosition,centerPosition) ~= max(WestPSFFormula(:)))
    error('We don''t understand spatial coordinates as well as we should.');
end

%% Get PSFs from LSF
WestPSFDerived = LsfToPsf(WestLSF);
GeislerPSFDerived = LsfToPsf(GeislerLSF);
DavilaGeislerPSFDerived = LsfToPsf(DavilaGeislerLSF);

%% And get LSF back again.
%
% This is done by convolution and if things are working will produce
% what we started with to good approximation.
WestLSFFromPSFFormula = PsfToLsf(WestPSFFormula);
WestLSFFromPSFDerived = PsfToLsf(WestPSFDerived);
DavilaGeislerLSFFromPSFDerived = PsfToLsf(DavilaGeislerPSFDerived);

%% Check that max of returned psf is where we think it should be.
%
% This check does assume an PSF with its max at 0, which is true for the
% Westheimer and Davila-Geisler cases.
if (WestPSFDerived(centerPosition,centerPosition) ~= max(WestPSFDerived(:)))
    error('We don''t understand spatial coordinates as well as we should.');
end
if (DavilaGeislerPSFDerived(centerPosition,centerPosition) ~= max(DavilaGeislerPSFDerived(:)))
    error('We don''t understand spatial coordinates as well as we should.');
end

%% Make a figure that compares the original and derived LSFs
%
% The LSF we get by going from LSF -> PSF -> LSF matches pretty
% well.  The LSF we get from Westheimer's PSF differs, which I
% believe is real inconsitency between Westheimer's PSF and LSF.
fig1 = figure;
set(gcf,'Position',[100 100 1200 800]);
set(gca, 'FontSize', 14);
subplot(2,2,1); hold on
plot(positionMinutes1D,WestLSF,'r','LineWidth',4);
plot(positionMinutes1D,WestLSFFromPSFDerived,'g-', 'LineWidth', 2);
plot(positionMinutes1D,WestLSFFromPSFFormula,'k-', 'LineWidth', 2);
xlim([-4 4]);
xlabel('Position (minutes');
ylabel('Normalized LSF');
title('Westheimer')
legend({'Original','Recovered from Derived PSF','Recovered from Formula PSF'},'Location','NorthEast');
if (max(abs(WestLSF(:)-WestLSFFromPSFDerived(:))) > 5e-3)
    error('Westheimer LSF -> PSF -> LSF is not close enough');
end

subplot(2,2,2); hold on
plot(positionMinutes1D,DavilaGeislerLSF,'r','LineWidth',4);
plot(positionMinutes1D,DavilaGeislerLSFFromPSFDerived,'g-', 'LineWidth', 2);
xlim([-4 4]);
xlabel('Position (minutes)');
ylabel('Normalized LSF');
title('Davila-Geisler');
legend({'Original','Recovered from PSF'},'Location','NorthEast');
if (max(abs(DavilaGeislerLSF(:)-DavilaGeislerLSFFromPSFDerived(:))) > 5e-3)
    error('DavilaGeisler LSF -> PSF -> LSF is not close enough');
end    

subplot(2,2,3); hold on
plot(positionMinutes1D,WestPSFDerived(centerPosition,:)/max(WestPSFDerived(centerPosition,:)),'r','LineWidth',4);
plot(positionMinutes1D,WestPSFFormula(centerPosition,:)/max(WestPSFFormula(centerPosition,:)),'k-','LineWidth',2);
xlim([-4 4]);
title('Westheimer')
xlabel('Position (minutes)');
ylabel('Normalized PSF Slice');
legend({'Derived from LSF','Formula PSF'},'Location','NorthEast');

subplot(2,2,4); hold on
plot(positionMinutes1D,GeislerPSFDerived(centerPosition,:)/max(GeislerPSFDerived(centerPosition,:)),'r','LineWidth',3);
plot(positionMinutes1D,DavilaGeislerPSFDerived(centerPosition,:)/max(DavilaGeislerPSFDerived(centerPosition,:)),'b','LineWidth',3);
xlim([-4 4]);
xlabel('Position (minutes');
ylabel('Normalized PSF Slice');
title('Davila-Geisler and Williams et al. PSFs')

%% PSF <-> OTF conversions

% Make a diffraction limited psf and convert to OTF.
%
% This is a nice case because we have independent analytic formulae for
% both psf and otf.
%
% The AiryPattern function takes its angle in radians, just to keep us on
% our toes.  So we get the analytic psf and then convert it to the otf.
Diffraction_3_633_PSFAnalytic = AiryPattern((pi/180)*(radiusMinutes2D/60),3,633);
Diffraction_3_633_PSFAnalytic = Diffraction_3_633_PSFAnalytic/sum(Diffraction_3_633_PSFAnalytic(:));
[xSfGridCyclesDeg,ySfGridCyclesDeg,Diffraction_3_633_OTFFromPSFAnalytic] = PsfToOtf(xGridMinutes,yGridMinutes,Diffraction_3_633_PSFAnalytic);

% Convert the otf back to psf.  This should definitely match what we started with or else
% something is badly wrong.
[xGridMinutes,yGridMinutes,Diffraction_3_633_PSFFromOTFFromPSFAnalytic] = OtfToPsf(xSfGridCyclesDeg,ySfGridCyclesDeg,Diffraction_3_633_OTFFromPSFAnalytic);

% Make an OTF directly from an analytic formula that is different from the Airy function and convert that back to PSF
radiusSfCyclesDeg2D = sqrt(xSfGridCyclesDeg.^2 + ySfGridCyclesDeg.^2);
Diffraction_3_633_OTFFromAnalytic = DiffractionMTF(radiusSfCyclesDeg2D,3,633);
[xGridMinutes1,yGridMinutes1,Diffraction_3_633_PSFFromOTFAnalytic] = OtfToPsf(xSfGridCyclesDeg,ySfGridCyclesDeg,Diffraction_3_633_OTFFromAnalytic);

% Let's get the point spread function from Williams et al.
WilliamsOTF = WilliamsMTF(radiusSfCyclesDeg2D);
[xGridMinutes2,yGridMinutes2,WilliamsPSF] = OtfToPsf(xSfGridCyclesDeg,ySfGridCyclesDeg,WilliamsOTF);
[WilliamsTablePositions,WilliamsTablePSF] = WilliamsTabulatedPSF;

% Compare these wonderful things
%
% First the otfs. These are very close, although not exactly the same.  Not
% sure if this is just a numerical thing, or maybe the result of not quite
% converting between position and spatial frequency exactly right, or some similar
% subtle problem in the routines that computer the Airy pattern and the diffraction
% limited otf.  Clearly this is working well enough for practical purposes.
fig2 = figure;
set(gcf,'Position',[100 100 1200 800]);
set(gca, 'FontSize', 14);
subplot(2,2,1); hold on
plot(xSfGridCyclesDeg(centerPosition,:),abs(Diffraction_3_633_OTFFromPSFAnalytic(centerPosition,:)),'r','LineWidth',4);
plot(xSfGridCyclesDeg(centerPosition,:),abs(Diffraction_3_633_OTFFromAnalytic(centerPosition,:)),'g-','LineWidth',2);
xlim([-100 100]); ylim([0 1]);
xlabel('Cycles/Deg');
ylabel('OTF');
title('Diffraction Limited OTFs');
legend({'Derived from Anaytic PSF', 'Analytic OTF'},'Location','NorthEast');
if (max(abs(Diffraction_3_633_OTFFromPSFAnalytic(:) - Diffraction_3_633_OTFFromAnalytic(:))) > 5e-2)
    error('Diffraction limited analytic and derived OTFs are not close enough');
end  

% Then the psfs.
subplot(2,2,2); hold on
plot(xGridMinutes(centerPosition,:),Diffraction_3_633_PSFFromOTFFromPSFAnalytic(centerPosition,:)/max(Diffraction_3_633_PSFFromOTFFromPSFAnalytic(centerPosition,:)),'r','LineWidth',4);
plot(positionMinutes1D,Diffraction_3_633_PSFAnalytic(centerPosition,:)/max(Diffraction_3_633_PSFAnalytic(centerPosition,:)),'g-','LineWidth',2);
xlim([-4 4]);
xlabel('Position (minutes');
ylabel('Normalized PSF Slice');
title('Diffraction Limited PSFs')
legend({'Derived from Anaytic OTF', 'Analytic PSF'},'Location','NorthEast');
if (max(abs(Diffraction_3_633_PSFFromOTFFromPSFAnalytic(:)/max(Diffraction_3_633_PSFFromOTFFromPSFAnalytic(centerPosition,:)) - Diffraction_3_633_PSFAnalytic(:)/max(Diffraction_3_633_PSFAnalytic(centerPosition,:)))) > 1e-10)
    error('Diffraction limited analytic and derived PSFs are not close enough');
end  

% Williams otf along with tabulated points from their Table 1.
% The fit in the paper smooths the measurements and by eye the deviations
% are not out of line with measurement variability.
subplot(2,2,3); hold on
plot(xSfGridCyclesDeg(centerPosition,:),abs(WilliamsOTF(centerPosition,:)),'r','LineWidth',4);
plot([10 20 30 40 50],[0.458 0.291 0.178 0.147 0.119],'ko','MarkerSize',4,'MarkerFaceColor','k');
xlim([-100 100]); ylim([0 1]);
xlabel('Cycles/Deg');
ylabel('OTF');
title('Williams et al. OTF');
legend({'Williams et al. formula','Tabulated data'});

% And the corresponding PSF, along with their tabulated PSF from their Table 2.
% The agreement here is reassuring, since those points were computed from
% the same otf many years ago, albeit through a different bit of code than
% is being used here.  The fft still works.
subplot(2,2,4); hold on
plot(xGridMinutes2(centerPosition,:),WilliamsPSF(centerPosition,:)/max(WilliamsPSF(centerPosition,:)),'r','LineWidth',4);
plot(WilliamsTablePositions,WilliamsTablePSF/max(WilliamsTablePSF(:)),'ko','MarkerSize',4,'MarkerFaceColor','k');
xlim([-4 4]);
xlabel('Position (minutes');
ylabel('Normalized PSF Slice');
title('Williams et al. PSF')
legend({'Derived PSF','Tabulated data'});

% And stick the Williams PSF into Figure 1, for comparison to
% Davila-Geisler.
figure(fig1);
subplot(2,2,4); hold on
plot(xGridMinutes2(centerPosition,:),WilliamsPSF(centerPosition,:)/max(WilliamsPSF(centerPosition,:)),'g-','LineWidth',2);
legend({'G Derived from LSF', 'D-G Derived from LSF', 'Williams et al. from OTF'},'Location','NorthEast');