image thumbnail
from Slant Edge Script by Patrick Granton
This code can be used to measure the pre-sampled MTF of an image.

MTFscript.m
%{
 Slant Edge Script
 File: MTFscript.m
 Written by: Patrick Granton, 
 September 2, 2010 
 Contact e-mail, patrick.granton@maastro.nl
 
Tested using Matlab 7.7.0.471 (R2008b)
including the imaging toolbox on a mac using OSX 10.5.8
 
A Gaussian fitting tool has been used in this code. You can download this 
tool on the Matlab central website here: http://www.mathworks.com/matlabcentral/fileexchange/11733-gaussian-curve-fit
 
 
%%%%%%%%%%%%%%%%%%%%Notes on Script%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
General comments:
 
This code can be used to measure the pre-sampled MTF of an image, which is a 
quantity that describes the resolution of linear imaging system. The code
is based on measuring the pre-sample MTF using a precision machined 
edge that is aligned - with respect to the columns or rows of an image - 
at an angle between 1-5 degrees.  
 
To learn more about the pre-sampled MTF consult the references at the end
of this script.

When you run this script your image containing the edge with appear with the 
cropping tool. Crop your image to the region containing only the edge. 
Double-click your cropped area for the script to continue. 


Samei, Flynn, and Reimann (Ref.2 ) have a good description on how to 
fabricate the edge to measure the pre-sampled MTF. 
 
%%%%%%%%%%%%%%%%%%%%%Begin Script%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
%}
%Identify and initialize all variables
 
isotropicpixelspacing = 0.2; 
% isotropic detector pixel spacing in mm, (i.e. pixel pitch).  
% set this value to your detector
 
pixel_subdivision = 0.10; 
% keep between 0.03 - > 0.15  
% Samei, Flynn, and Reimann (Ref.2 )suggest that 0.1 subpixel bin spacing 
% provides a good trade-off between sampling uniformity and noise.
 
bin_pad = 0.0001;
% Bin_pad adds a small space so that all values are included the histogram 
% of edge spread distances.
 
span = 10;
% span is used to smooth the edge spread function 
% Samei, Flynn, and Reimann (Ref.2 ) use a forth weighted,
% Gaussian-weighted moving polynomial. 
 
edge_span = 4;
% Used to improve the location of the edge
 
boundplusminus = 2; 
% boundplusminus is a variable that is used to crop a small section of the
% edge in order to used to find the subpixel interpolated edge position.
 
 
boundplusminus_extra = 6;
% boundplusminus_extra incorperates addition pixel values near the edge to 
% include in the binned histogram. 
 
 
%%%%%%%%%%%%%%%%%%%%%%%%%LOAD IMAGE%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
 
Button_Image_Import = questdlg('What type of image would you like to import ?',...
    'Image type?',...
    'Dicom','Tiff or Jpeg','Matlab file','Tiff or Jpeg');
 
switch Button_Image_Import,
    
    case 'Dicom',
 
     [image_file, path_name] = uigetfile('*.dcm','Please select the dicom image you wish to import');
 
     image = double(dicomread([path_name image_file]));
    
    case 'Tiff or Jpeg'  
 
    [image_file, path_name] = uigetfile({'*.tif; *.tiff; *.jpg; *.jpeg'},'Please select the jpeg or tiff image you wish to import');
    
    image = double(imread([path_name image_file]))
 
    case 'Matlab file' 
 
    [image_file, path_name] = uigetfile('*.mat','Please select the Matlab file containing an image named "image"in you wish to load');
    
    matstruct = load([path_name image_file]);
       
    image = double(matstruct.image);
      
end % switch
 
 
% crop image to 50 % air and 50 % edge 
 
h = figure('Name','Please select a region contain 50% Air and 50% of the edge'); hold on
 
imshow(image,[]);
 
image = imcrop(h);
 
close(h);
 
 
%{
 Threshold image using Otsu's threshold criterion on cropped image
 
 Then find the average of the two regions (i.e. air, Plexiglas)
 
 The difference between's Otsu's threshold and the mean threshold is small 
%}
 
level = graythresh(image);
 
threshold =  double(((max(max(image)) - min(min(image))))*level + min(min(image)));
 
BW1 = roicolor(image,threshold,max(max(image)));
 
BW2 = roicolor(image,min(min(image)),threshold);
 
Area1 = sum(image(BW1))/sum(sum(BW1));
 
Area2 = sum(image(BW2))/sum(sum(BW2));
 
threshold = (Area1 - Area2)/2 + Area2;  
 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
% Detect edge and orientation
 
BW_edge = edge(double(image),'canny',level);
 
% Locate edge positions
 
[A_row_pos B_column_pos] = find(BW_edge==1);
 
% Fit edge positions
 
P = polyfit(B_column_pos,flipud(A_row_pos),1);
 
% determine rough edge angle to determine orientation
 
Angle_radians = atan(P(1));
 
% show the determined edge
 
% imshow(image + BW_edge*edge_intensity,[])
 
[rowlength, columnlength] = size(image);
 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
 
% Determine the sub pixel edge in a particular direction. 
% 
 
 
if abs(Angle_radians) > pi/4 % i.e. edge is vertical
    
    % if the edge is vertical the image keeps the same orientation
    % else we transpose the image
    
    start_row = boundplusminus_extra;
    end_row = rowlength - boundplusminus_extra;
    
else % the edge is horizontal and the image is transposed
    
    image = image';
    
    [rowlength, columnlength] = size(image);
  
    start_row = boundplusminus_extra;
    end_row = rowlength - boundplusminus_extra;
end
    
    
      for i = start_row:end_row
          
        %%%%%%%%%%%%%%%%%%%  USED FOR finding edge %%%%%%%%%%%%%%%%%
 
        ii = i - start_row +1;
        
        strip = image(i,:);
        
        % smooth edge to find approximate edge area
        
        window = ones(edge_span,1)/edge_span;
        
        strip_smoothed = convn(strip',window,'valid');
        
        app_edge = find(abs(diff(strip_smoothed))== max(abs(diff(strip_smoothed)))) + 2;
        
        %  in case there are more than one maxima, take the first one
        
        bound_edge_left = app_edge(1) - boundplusminus;
        
        bound_edge_right = app_edge(1) + boundplusminus;
        
        strip_cropped = (image(i,bound_edge_left:bound_edge_right));
        
        temp_y = 1:length(strip_cropped);
        
        edge_position_temp = interp1(strip_cropped,temp_y,threshold,'pchip');  
        
        edge_position(ii) = edge_position_temp + bound_edge_left - 1;
        
        
        %%%%%%%%%%%%%%%%%%%  USED FOR HISOGRAM %%%%%%%%%%%%%%%%%%%%
        
        
        bound_edge_left_expand = app_edge(1) - boundplusminus_extra;   
        
        bound_edge_right_expand = app_edge(1) + boundplusminus_extra;   
 
        array_values_near_edge(ii,:) = (image(i,bound_edge_left_expand:bound_edge_right_expand)); 
 
        arraypositions(ii,:) = [bound_edge_left_expand:bound_edge_right_expand];
        
       
      end
    
    %{
      
         Fit a curve to the edge of a polynomial degree 1 
         y = ax^2 + bx + c, x = (y-b)/m
    
      
         *some use a degree 2 polynomial fit though for lens aberrations in XRI. 
        Padgett and Kotre (Ref.1) suggest an iteration process could be used to
        optimize the angle of the edge but generally not necessary. 
      
      %}
      
        y = 1:length(edge_position);
        
        P = polyfit(edge_position,y,1); 
 
        m = P(1);
 
        b = P(2);
 
        xfit = ((y - b)/m);
        
        distance_from_edge = edge_position - xfit;
 
        
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% generate edge bins %%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
          
   
        array_positions_by_edge = arraypositions - (edge_position')*(ones(1,(1 + 2*boundplusminus_extra)));
       
        % size of matrix
        
        [m n] = size(array_positions_by_edge);
        
        array_values_by_edge = array_values_near_edge(1:(m*n)); 
        
        array_positions_by_edge = array_positions_by_edge(1:(m*n));
        
       
 
% Determine bin spacing 
 
topEdge = ((max(array_positions_by_edge))+ bin_pad + pixel_subdivision);
botEdge = ((min(array_positions_by_edge))- bin_pad);
binEdges = botEdge:pixel_subdivision:topEdge;
numBins = length(binEdges) - 1;
 
binPositions = binEdges(1:end-1) + 1/2*pixel_subdivision;
 
% Rebinning portion
 
[h,whichBin] = histc(array_positions_by_edge,binEdges);
 
for i = 1:numBins
    flagBinMembers = (whichBin == i);
    binMembers = array_values_by_edge(flagBinMembers);
    binMean(i) = mean(binMembers);
    
end
 
 
 
ESF = binMean(2:numBins - 1); % Eliminate first, second and last array position
 
xESF = binPositions(2:numBins - 1); % same as above comment
 
% fill in missing data
 
ESF_nonan = ESF(logical(1-isnan(ESF)));
 
xESF_nonan = xESF(logical(1-isnan(ESF)));
 
ESF = interp1(xESF_nonan,ESF_nonan,xESF,'pchip');
 
 
% smooth the edge spread function
 
window = ones(span,1)/span;
 
smoothed = convn(ESF,window','valid');
 
subplot(2,2,1)
 
plot(xESF,ESF,xESF(round(span/2):round((length(xESF)-span/2))),smoothed);
 
title('\fontname{Arial} The Edge Spread Function')
 
legend('Raw ESF','Smoothed ESF (Gaussian)')
 
xlabel('distance along the edge in pixels')
 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
 
% Perform the MTF calculations
 
LSF_raw = diff(ESF);
 
LSF = -diff(smoothed);
 
 
Nans = isnan(LSF); % remove any Nans
 
LSF(Nans==1) = 0;
 
 
% Normalize the LSF
 
LSF = LSF/sum(LSF);
 
xLSF_fit = 1:length(LSF);
 
LSF_base_raw= LSF_raw/sum(LSF_raw);
 
LSF_raw = LSF_raw(span/2:1:(length(LSF_raw)-span/2));
 
LSF_raw = LSF_raw/sum(LSF_raw);
 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%Fit a Gaussian to the LSF fit%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
 
% acquire this function
% here:http://www.mathworks.com/matlabcentral/fileexchange/11733-gaussian-curve-fit
 
[sigma,mu,A] = mygaussfit(xLSF_fit,LSF); 
 
yLSF_fit = A*exp(-(xLSF_fit-mu).^2/(2*sigma^2));
 
 
 
subplot(2,2,2)
 
plot(xESF(round(span/2):round((length(xESF)-span/2)-1)),LSF,xESF(round(span/2):round((length(xESF)-span/2)-1)),yLSF_fit,xESF(1:(length(xESF)-1)),LSF_base_raw);
 
title('\fontname{Arial} The Line Spread Function of the Edge')
 
legend('Smoothed LSF','Fitted LSF (Gaussian)','Raw LSF')
 
xlabel('distance along the edge in pixels')
 
 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
 
% Convert the spatial domain to frequency domain
  
N = length(LSF);
 
%generate the frequency axis
 
 if mod(N,2)==0
 q=-N/2:N/2-1; % N even
 else
 q=-(N-1)/2:(N-1)/2; % N odd
 end
    
 %{
 
% Convert the spatial domain to frequency domain
  
%}
 
 
Fs = 1/(isotropicpixelspacing*pixel_subdivision);% sampling rate in samples per mm
   
T = N/Fs;
    
freq = q/T;
 
 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
 %%%%Generate your MTF%%%%%
    
MTF= abs(fft(LSF));  
 
MTF_fit = abs(fft(yLSF_fit));
 
MTF_raw = abs(fft(LSF_raw));
 
subplot(2,2,3)
 
plot(freq,fftshift(MTF),freq,fftshift(MTF_fit),freq,fftshift(MTF_raw));
 
title('\fontname{Arial} The Modulation Transfer Function of the Edge')
 
legend('Smoothed MTF','Fitted LSF (Gaussian)','Raw MTF')
 
xlabel('frequency distribution')
 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
zero_freq = find(freq == 0);
 
freq = freq(zero_freq:length(freq));
 
MTF = fftshift(MTF);
 
MTF = MTF(zero_freq:length(MTF));
 
MTF_fit = fftshift(MTF_fit);
 
MTF_fit = MTF_fit(zero_freq:length(MTF_fit));
 
MTF_raw = fftshift(MTF_raw);
 
MTF_raw = MTF_raw(zero_freq:length(MTF_raw));
 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
 
% Equivalent line pairs per mm @ 10% MTF
% 2.5 LPP is equivalent to 200 um, DU et al
% Other institutions use 50% 
 
    
YMTFfine = 0:0.01:1;
 
mtfpoint = find(MTF<0.05);
 
YMTF = interp1(MTF(1:mtfpoint(1)),freq(1:mtfpoint(1)),YMTFfine,'pchip');
 
mtfpoint = find(MTF<0.05);
 
YMTFit = interp1(MTF_fit(1:mtfpoint(1)),freq(1:mtfpoint(1)),YMTFfine,'pchip');
 
mtfpoint = find(MTF<0.05);
 
YMTraw = interp1(MTF_raw(1:mtfpoint(1)),freq(1:mtfpoint(1)),YMTFfine,'pchip');
 
 
LPP = YMTF(YMTFfine==0.1);
 
Resolution_smoothed= (1/(LPP*2)) % in mm 
 
LPP = YMTFit(YMTFfine==0.1);
 
Resolution_fit = (1/(LPP*2)) % in mm 
 
LPP = YMTraw(YMTFfine==0.1);
 
Resolution_raw = (1/(LPP*2)) % in mm 
 
 
 
 
subplot(2,2,4)
 
shortaxis = length(freq)/4;
 
plot(freq(1:shortaxis),MTF(1:shortaxis),freq(1:shortaxis),MTF_fit(1:shortaxis),freq(1:shortaxis),MTF_raw(1:shortaxis));
 
title('\fontname{Arial} The Modulation Transfer Function of the Edge (ZOOMED)')
 
legend(sprintf('Smoothed MTF Resolution = %3.1f microns',1000*Resolution_smoothed), sprintf('Fitted LSF (Gaussian) MTF Resolution = %3.1f microns', 1000*Resolution_fit),sprintf('Raw MTF Resolution = %3.1f microns',1000*Resolution_raw))
 
xlabel('The pre-sampled MTF in line pairs per mm')
 
 
 
%{
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
 
References:
 
(1) Padgett and Kotre, 
    "Development and application of programs to measure modulation transfer
    function, noise power spectrum and detective quantum efficiency,"
    Radiation Protection Dosimetry, Vol. 117, No. 1-3, pp 283-287, (2005)
 
(2) Samei, Flynn, and Reimann,
    "Measuring the presampled MTF of digital radiographic systems"
    Medical Physics, Vol. 25, No.1, pp 102-113, (1998)
 
(3) Judy,
    "The line spread function and modulation transfer function of a computer
    tomographic scanner,"
    Medical Physics, Vol.3, No.4, pp 233-236, (1975)
 
(4) Fujitia et al. 
    " A simple Method for determining the modulation transfer function in 
    digital radiography,"
    IEEE transactions on medical imaging, Vol. 11, No.1, (1992)
 
(5) Du et al. 
    " A quality assurance phantom for the performance evaluation of
    volumetric micro-CT systems,"
    Physics Medicine Biology, Vol. 52, pp 7087-7108, (2007)
    
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
%}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


Contact us at files@mathworks.com