%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Class:     Psych 221/EE 362
% Tutorial:  Image Formation
% Author:    Wandell
% Purpose:   Introduce visual angle, linespread, pointspread,
% 	     and chromatic aberration
% Date:      01.02.96	
% Duration:  60 minutes
%	
% Matlab 5:  Checked 01.06.98, BW
% 01.07.99:  Added IjSpeert calls, BW
% 01.12.01   Added new graphs for ijspeert curves and Williams et al. data, BW
%
% 01.14.02   Adjusted convolution, adjusted directory location.
%            More fixes shortly, but this is usable. 
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% Change the matlab process so that it is executing from inside
% the tutorials directory.  This will make my life easier.

% cd /usr/class/psych221/tutorials
cd ImageFormation

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Visual Angle
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% To think about the effect of an image on the eye, we must
% specify the image in terms of degrees of visual angle.  As an
% example for how to compute spacing in terms of degrees of
% visual angle, consider a printer whose dots occupy (dots per inch)
dpi = 600

% Suppose we read the paper at a viewing distance (inches)
viewingDistance = 12

% The viewing geometry is illustrated in this figure:
%
f = figure
set(f,'Position',[700 600 384 256])
line([0 viewingDistance 0 0],[0 0 1 0]); 
axis equal, axis square
set(gca,'xlim',[-2 20]), grid on
xlabel('Viewing Distance (inch)')
ylabel('Spacing on Paper (inch)')

% In radians, the viewing angle, phi, satisfies 
% tan(phi) = (opposite/adjacent).
deg2rad = 2*pi/360;
rad2deg = 360/(2*pi);
phi = atan(1/viewingDistance)*rad2deg  % phi in degrees

% There are 600 dots per inch, so that each dot occupies
%
DegPerDot = phi/dpi

% There are 60 min of visual angle per deg,
%
MinPerDot = 60*DegPerDot

% and 60 sec of visual angle per min,
%
SecPerDot = 60*MinPerDot

% As you will see later in the course, experiments have shown
% that people can localize the position of a line to a spatial
% position of roughly 6 sec of visual angle.  Hence, at this
% viewing distance and with this many dots per inch, the dot
% spacing is wider than the spacing that can be just
% discriminated by the human eye.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% The Westheimer linespread function
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% Westheimer calculated that the linespread function of the human
% eye, specified in terms of minutes of arc and using a 3mm
% pupil, should be approximated using the following formula
%
% LineSpread = 0.47*exp(-3.3 *(x.^2)) + 0.53*exp(-0.93*abs(x));
%

% Suppose we wish to plot the function by defining the spatial
% variable, x, in terms of seconds of arc,
xSec = -300:1:300;	
xMin = xSec/60;
ls = 0.47*exp(-3.3 *(xMin.^2)) + 0.53*exp(-0.93*abs(xMin));
ls = ls / sum(ls);

figure
plot(xSec,ls)
set(gca,'xlim',[-240 240],'xtick',[-240:60:240]), grid  on
xlabel('Arc sec'), ylabel('Responsivity'), title('Westheimer Linespread')

% From our previous calculation, we observed that the dots in a
% 600 dpi printer, viewed at 12 inches, are spaced 28.5819 sec of
% visual angle apart.  At this distance, the linespread has
% fallen to about one-half of its peak value.

% Hence, if we could control the intensity and color of the
% printed dots -- which we cannot do on conventional laser
% printers -- then at this viewing distance we would be able to
% produce images that were very realistic in their appearance.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Convolution of the image and linespread
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% We can estimate the visual image created by an image printed at
% 600 dpi using the following simple convolution
% calculation. Let's create an image that spans 0.2 deg and has a
% dot every 30 sec.

secPerDeg = 60*60;
x = 1:0.2*secPerDeg;
im = zeros(1,length(x));
im(1:30:length(im)) = ones(size(im(1:30:length(im))));

% Here is an image showing the sampled line positions
%
imshow(im(ones(1,128),1:512),32)

% Each line in the physical image adds a unit linespread to the
% retinal image.  We can compute the retinal image by forming the
% convolution of the image with the Westheimer linespread
% function.  Remember: we sampled the linespread once every sec
% of arc. So, we can simply convolve the image and the linespread
% function now as:

retIm = conv2(ls,im,'full');
figure
plot(retIm),grid on
title('The one-dimensional retinal image')
xlabel('Sec of arc'), ylabel('Image intensity')

figure; 
imagesc(retIm(ones(1,128),1:length(retIm))); colormap(gray(256));


% (If the colors in the image look funny, make sure to place your
% cursor inside the window.  This may change the local color
% map).

% While the original image varies from black to white, after
% blurring by the eye's optics, there is only a small amount of 
% residual variation in the retinal image.  Because of the
% blurring, the retinal image is much more like the image of a
% bar than it is the image of a set of individual lines. 

% In fact, the dots placed on the page are not perfect line
% samples.  Each ink line has some width.  So, a more realistic
% input image might be created by blurring the stimulus and then
% convolving with the linespread.

gKernel = mkGaussKernel([1,30],[1 8]);
blurIm = conv2(im,gKernel,'full');
blurIm = scale(blurIm,1,32);
retIm = conv2(blurIm,ls,'full');

% Notice the very small ripples left in the curve after taking
% into account the blurring by the physical display and by the
% eye. 
figure
plot(retIm), axis square, grid on
title('Retinal image of blurred lines')
xlabel('Sec of arc'), ylabel('Intensity')

figure; 
imagesc(retIm(ones(1,128),1:length(retIm))); colormap(gray(256));

% The question you might ask yourself now is this: will those
% small ripples be detectable by the observers?  How can we tell?
% You might also ask what will happen when we view the page at 6
% inches, or at 24 inches.  What if we increase the printer
% resolution to 1200 dpi?  What if we introduce some ability to
% modulate the density of the ink and hence the light scattered
% back to the eye?

close all

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Looking at defocus in the frequency domain
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% First, make a new linespread function that is smaller and
% easier to compute with.  Have it extend over 1 deg (60 min) so
% the Fourier Transform is easier to interpret

xMin = -30:1:29;	
ls = 0.47*exp(-3.3 *(xMin.^2)) + 0.53*exp(-0.93*abs(xMin));
ls = ls / sum(ls);

figure
plot(xMin,ls), grid on
xlabel('Min of arc'),ylabel('Linespread value')

% Now, consider the retinal image that is formed by some simple
% harmonic functions.  Here is a sinusoid that varies at 1 cycle
% per degree of visual angle.

nSamples = length(xMin);
f = 1;
harmonic = cos(2*pi*f*xMin/nSamples);

figure
plot(xMin,harmonic), grid on
title('Sampled cosinusoid')
xlabel('Arc sec'), ylabel('Intensity')

% Now, we will show what happens to cosinusoids at different
% spatial frequencies.  Notice that the amplitude of the
% cosinusoid falls off as the spatial frequency increases.  We
% will store the amplitude of the cosinusoid in the variable
% "peak".

figure
peak = []; freq =[1 5 10 15]; 
for i = 1:length(freq)
 harmonic = cos(2*pi*freq(i)*xMin/nSamples);
% retIm = convolvecirc(harmonic,ls);
 retIm = conv2([harmonic,harmonic],ls,'same');
 subplot(2,2,i)
 plot(retIm), grid on, set(gca,'ylim',[-1 1],'xlim',[0 64]);
 xlabel('Arc sec')
 peak = [peak max(retIm(:))];
end

% We can plot the amplitude of the retinal cosinusoid, and its
% amplitude decreases with the input frequency.  I also use the
% fact that at f = 0 the amplitude = 1 (because the area under
% the linesspread is 1).

figure
plot([0 freq],[1 peak],'-')
set(gca,'ylim',[0 1])
xlabel('Spatial freq (cpd)'), ylabel('Transfer')

% Let's compare the values we obtain from the convolution with
% the values we obtain by calculating the amplitude of the
% Fourier Transform of the linespread function.  Remember, the
% linespread was built so that it spans 1 deg, hence frequency is
% in cycles per degree.

mtf = abs(fft(ls));
hold on, plot(freq,mtf(freq + 1),'ro');
hold off

% The values we obtain from convolution are plotted as solid line,
% whereas the amplitude of the Fourier Transform of the linespread
% function is plotted as a red circles at each frequency.

% The functions match, which should give you some intuition about
% what the amplitude of the Fourier Transform represents.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% 
% Comparison of the pointspread and linespread
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% When working with two-dimensional inputs, we must consider the
% pointspread function, that is the response to an input that is
% a point of light. A standard formula for the cross-section of
% the pointspread function of the human eye for a 3mm pupil is
% also provided by Westheimer.  We can compare the linespread and
% the cross-section of the pointspread in the following graphs.

xSec = -300:300;
xMin = xSec/60;
ls = 0.47*exp(-3.3 *(xMin.^2)) + 0.53*exp(-0.93*abs(xMin));
ps = 0.952*exp(-2.59*abs(xMin).^1.36) + 0.048*exp(-2.43*abs(xMin).^1.74);

figure
p = plot(xSec,ps,'r-',xSec,ls,'b--'), grid on
set(gca,'xlim',[-180 180])
xlabel('Arc sec'), ylabel('LS or PS amplitude')
legend(p,'Pointspread','Linespread')

% Next, we can create a graph of the pointspread.  First, create
% a matrix whose entries are the distance from the origin

xSec = -240:10:240;
xMin = xSec/60;
X = xMin(ones(1,length(xMin)),:); Y = X';
D = X.^2 + Y.^2; D = D.^0.5;

% If you want to see the distance from the origin, you might show
% this image: colormap(gray(32)),imagesc(D), axis image

% Then, compute the pointspread function and make a picture of it
%
ps = 0.952*exp(-2.59*abs(D).^1.36) + 0.048*exp(-2.43*abs(D).^1.74);
figure
colormap(cool(64)), mesh(ps)

% To see the pointspread as an image, rather than as a mesh plot,
% you might make this figure: colormap(gray(32)),imagesc(ps), axis image

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% 
% Chromatic aberration:  How the linespread varies with wavelength
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% The linespread varies quite strongly with wavelength.  When the
% eye is in good focus at 580 nm (yellow-part of the spectrum)
% the light in the short-wavelength (400-450nm) is blurred quite
% strongly and light in the long-wavelength part of the spectrum
% is blurred, too, though somewhat less.  We can calculate the
% linespread as a function of wavelength (Marimont and Wandell,
% 1993) from basic principles.  The linespreads a various
% wavelengths are contained in the data file:

load chromAb/lineSpread;

% linespread 370-730nm, in 1nm steps.
% This was generated by the script ChromAb.m by Wandell and
% Marimont. xDim is in degrees of visual angle.

% We select three wavelengths and plot their linespread functions
% together. Notice that for the shorter wavelength, the
% linespread function is much more spread-out than for the middle
% and long wavelengths.

figure
plot(xDim, lineSpread(80, :), 'b-', xDim, lineSpread(200,:), ...
      'g:', xDim, lineSpread(361, :), 'r--' );
legend('wavelength 450', 'wavelength 570','wavelength 730');
xlabel('Degrees'); ylabel('Image Intensity');
title('Linespread functions for three wavelengths');

%  Look at the line spread functions for all wavelengths

lw = 1:10:length(wave);
figure
colormap(hot(32));
mesh(xDim, wave(lw), lineSpread(lw,:)); 
set(gca,'xlim',[-1 1],'ylim',[350 730])
ylabel('wavelength (nm)'); xlabel('degrees'); zlabel('intensity');

% Different wavelength components of an image are blurred to
% different extents by the eye.  We use a set of lines again as
% an example.  For this computation, we will assume that the
% input begins with equal energy at all wavelengths from 370 to
% 730 nm.

% Here, we create and display the image of sample lines.

im = reshape([0 0 0 1 0 0 0 0]' * ones(1, 16), 1, 128);
imshow(im(ones(100,1), 1:128), 32);

% To calculate the retinal image for this pattern, we
% convolve each wavelength component of the image with the
% appropriate linespread function.  The routine conv2 takes the
% input image (im, size(im) = 1 128) and convolves it with each
% of the linespread functions in lineSpread size(lineSpread =
% 361,65).  This results in 361 images (one for each
% wavelength).  

retIm = conv2(im, lineSpread, 'full');


% We must remember the size (in deg) of each sample point this way.
% 
X = [-size(retIm,2)/2 : size(retIm, 2)/2-1] / 64;

% We can plot the retinal image corresponding to two wavelengths
% this way.

figure

subplot(2,1,1)
plot(X,retIm(200,:),'g-')
set(gca,'ylim',[0 0.5])
grid on

subplot(2,1,2)
plot(X,retIm(50,:),'b-')
set(gca,'ylim',[0 0.5])
grid on

% Notice that the two images have the same mean, they only differ
% in terms of the contrast:  the green image has a lot more
% contrast, but the same mean.

mean(retIm(50,:)')
mean(retIm(200,:)')

% The short wavelength (420 nm) component is blurred much more than
% the longer wavelength component of the image.  Hence, the image
% has very low amplitude ripples, and appears almost like a
% single, uniform bar.  The 570nm component, however, has high
% amplitude ripples that are quite distinct.  Hence, the
% short-wavelength variation would be very hard to detect in the
% retinal image, while the 570 nm component would be quite easy
% to detect.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Chromatic aberration in the frequency domain
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% Finally, let's make a few graphs of the modulation transfer
% function of the eye's optical system for individual
% wavelengths.  For short wavelength lights, high spatial
% frequency contrast is attenuated a lot by the optical path of
% the eye.

% Load the MTFs for wavelengths from 370-730nm.  These were
% calculated using the methods in Marimont and Wandell that are
% in a script in the /local/class/psych221/tutorials/chromAb sub-directory.

load chromAb/combinedOtf;

% Here is a graph of a few of the MTFs
% 
figure
plot(sampleSf, combinedOtf(80, :), 'b-', ...
    sampleSf, combinedOtf(200,:), ...
    'g:', sampleSf, combinedOtf(361, :), 'r--' );
legend('wavelength 450', 'wavelength 570','wavelength 730');
xlabel('Frequency (CPD)'); ylabel('Scale factor'); grid on
title('Modulation transfer functions for 3 wavelengths');

% Notice that the amplitude of the short-wavelength becomes
% negative. This occurs because the blurring is so severe that
% the harmonic function is reproduced in the opposite phase
% compared to the input harmonic.  Hence, the amplitude is
% represented by a negative number.  This was illustrated in
% class using the slide projector, and the phenomenon is called
% "spurious resolution."


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% More modern Linespreads, Pointspreads, and MTFs  (01.07.99, BW)
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% In recent years, Ijspeert and others in the Netherlands
% developed a more extensive set of functions to predict the
% basic image formation variables in the average human eye.  The
% curves they derived were based on empirical inspection of data
% sets, and do not have any strong theoretical foundation.
% Still, they are probably more accurate than the Westheimer
% function and they are parameterized for the subject's age,
% pigmentation, and pupil diameter.  Hence, for practical
% computation, these are probably more useful.

% A student in Psych 221 developed the code to compute these
% values in the function "ijspeert."  Here is an example call
% 
age = 20; 				% Subject's age
pupil = 3.0; 				% diameter in mm
pigmentation = 0.142; 			% Caucasian
freqIndexRange = 1:50; 			% The spatial frequency range

% Set the span to be 1 deg, so we know that 1 cycle in the MTF
% corresponds to 1 cycle per deg
% 
angleInDeg = [-.25:.005:.25];
angleInSec = angleInDeg*3600;
angleInRad = angleInDeg*deg2rad;

[iMTF, iPSF, iLSF] = ijspeertMTF(age, pupil, pigmentation, ...
    freqIndexRange, angleInRad);
 
 
% The functions should be normalized so that the area under the
% linespread and the first value of the MTF are one.
% 
iMTF = iMTF/iMTF(1);
iLSF = iLSF/sum(iLSF);

% These are the modulation transfer function and linespread
% for the ijspeert data
clf
subplot(1,2,1), plot(iMTF); grid on
set(gca,'xtick',[0:10:80],'xlim',[0 80]);
xlabel('Spatial frequency (cpd)');
ylabel('Amplitude');
title('MTF')

% Here is the linespread function

subplot(1,2,2), plot(angleInSec,iLSF); grid on
xlabel('Position (sec)');
ylabel('Intensity');
set(gca,'xtick',[-500:250:500],'xlim',[-500 500]);
title('Line spread')

% We can compare the ijspeert and westheimer linespread functions 
%
xMin = angleInSec/60;
ls = 0.47*exp(-3.3 *(xMin.^2)) + 0.53*exp(-0.93*abs(xMin));
ls = ls / sum(ls);
westMTF = abs(fft(ls));

clf
subplot(1,2,1)
plot(angleInSec/60,iLSF,'b-',angleInSec/60,ls,'r-')
set(gca,'xtick',[-8:2:8],'xlim',[-6 6]);
xlabel('Position (min)'), ylabel('Intensity'), title('Linespread')
grid on

% And here is a comparison of the MTFs with the Williams data superimposed
% 
load data/williams

subplot(1,2,2) 
n = length(iMTF);
freq = 0:(n-1);
plot(freq,iMTF(1:n),'b-',freq,westMTF(1:n),'r-')
hold on
plot(dataF,dhb,'ro',dataF,drw,'go',dataF,rnb,'bo')
xlabel('Spatial frequency (cpd)'), ylabel('Amplitude'), title('MTF')
set(gca,'ylim',[0 1])
grid on
hold off
legend('Ijspeert','Westheimer','Data')

% What does the pointspread function look like in 2D?
% The PSF is circularly symmetric.  So, we can accumulate 
% the 1D values into a 2D surface.
%

angleInRad2D = linspace(min(angleInRad)/4,max(angleInRad)/4,length(angleInRad)/4);
nSamples = length(angleInRad2D);
[X,Y] = meshgrid(angleInRad2D,angleInRad2D);

% Because the pointspread function is symmetric, we can calculate the value by
% building a matrix D that measures only the distance from the center of matrix.  
% Try plotting this distance matrix using the command:
%
%   imagesc(D); colorbar; axis image
%
D = sqrt(X.^2 + Y.^2);

% Now, loop through the rows of D to calculate the pointspread values
% as a function of angle.
%
iPSF2D = zeros(size(D));
for ii=1:nSamples
   a = D(ii,:);
   [iMTF, iPSF2D(ii,:)] = ijspeertMTF(age, pupil, pigmentation, ...
      freqIndexRange, a);
end

clf;
colormap(cool(64));
surf(angleInRad2D,angleInRad2D,iPSF2D);

% END OF TUTORIAL
