Files
fold_slice/ptycho/utils/aligned_FSC.m
T
2026-08-07 15:56:42 +09:00

606 lines
22 KiB
Matlab

% [resolution stat] = aligned_FSC(file1,file2,param)
%
% Receives two filenames with path for ptychography reconstructions and a
% structure with parameters. The routine reads the reconstructions, matches
% the linear phase between them, registers the images, and returns the
% resolution estimates based on first and last crossing of the FSC with the
% threshold.
%
% Modified by YJ for electron ptychography
%
% References relevant to this code:
% For using this FSC code with ptychography: J. Vila-Comamala, et al., "Characterization of high-resolution diffractive X-ray optics by ptychographic coherent diffractive imaging," Opt. Express 19, 21333-21344 (2011).
% For subpixel alignment: M. Guizar-Sicairos, et al., "Efficient subpixel image registration algorithms," Opt. Lett. 33, 156 (2008).
% For matching of phase ramp by approximate least squared error: M. Guizar-Sicairos, et al., "Phase tomography from x-ray coherent diffractive imaging projections," Opt. Express 19, 21345-21357 (2011).
%
% Outputs:
%
% resolution A two element variable that contains the resolution
% obtained from first and last crossing of the FSC curve with
% the threshold curve.
% stat structure containing other statistics such as
% spectral signal to noise ratio (SSNR), average SNR and area under FSC curve
%
% Inputs:
%
% file1 Filename with path of reconstruction 1 or directly a 2D numerical array
% file2 Filename with path of reconstruction 2 or directly a 2D numerical array
% param Structure with parameters as describred below
%
% param.flipped_images Flip one input image horizontally (= true or false).
% Useful when comparing 0 and 180 degree projections
% in tomography (default = false).
% param.crop = ''; for using the default half size of the probe
% = 'manual' for using GUI to select region. This will display the range, e.g. {600:800, 600:800}
% = {600:800, 600:800} for custom vertical and horizontal cropping, respectively
% param.GUIguess To click for an initial alignment guess, if used it ignores
% the values of param.guessx and param.guessy (default
% = false)
% param.guessx
% param.guessy An intial guess for x and y alignment (default = [])
% param.remove_ramp Try to remove linear phase from whole image before initial
% alignment (default = true)
% param.image_prop = 'complex'
% = 'phasor' (phase with unit amplitude, default)
% = 'phase' (Note: phase should not be used if there is phase wrapping)
% param.taper = 20 (default) Pixels to taper images - Increase until the FSC does not change anymore
% param.plotting Display plots (default = false)
% param.dispfsc Display FSC plot (default = true)
% param.SNRt SNR for FSC threshold curve
% SNRt = 0.2071 for 1/2 bit threshold for resolution of the average of the 2 images
% SNRt = 0.5 for 1 bit threshold for resolution of each individual image (default)
% param.thickring Thickness of Fourier domain ring for FSC in pixels (default = 1)
% param.freq_thr To ignore the crossings before freq_thr for determining resolution (default 0.02)
% param.out_fn Filename of output of jpeg for FSC
% param.pixel_size Pixel size in the reconstruction, it is used only
% if file1 / file2 are not paths to the reconsturcted files
function [resolution,stat] = aligned_FSC(file1,file2,param)
import utils.*
import math.*
import io.*
import plotting.*
file{1} = file1;
file{2} = file2;
for ii = 1:2
if ischar(file{ii})
[~,file_path{ii},~] = fileparts(file{ii});
else
file_path{ii} = sprintf('image_id_%i', ii);
end
end
%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Checks and defaults %%%
if isfield(param,'flag_imread')
flag_imread = param.flag_imread;
else
if ~(ischar(file1) && ischar(file2)) || (~isempty(regexpi(file1,'\.mat|\.h5')) && ~isempty(regexpi(file2,'\.mat|\.h5')))
flag_imread = false;
else
flag_imread = true;
warning('Files not in .mat: using ''imread'' for loading; image in real number (ignoring remove_ramp and image_prop)')
end
end
% parse inputs
check_input = @(x) islogical(x) || isnumeric(x);
check_crop = @(x) isempty(x) || ischar(x) || iscell(x);
check_image_prop = @(x) assert(any(contains({'complex', 'phasor', 'phase', 'variation'}, x)), ...
'image_prop must be either "complex", "phasor", "phase" or "variation".');
parse_param = inputParser;
parse_param.KeepUnmatched = true;
parse_param.addParameter('flipped_images', false, check_input)
parse_param.addParameter('crop', '', check_crop)
parse_param.addParameter('GUIguess', false, check_input)
parse_param.addParameter('guessx', [], check_input)
parse_param.addParameter('guessy', [], check_input)
parse_param.addParameter('plotting',false, check_input)
parse_param.addParameter('remove_ramp', false, check_input)
parse_param.addParameter('image_prop', 'phasor', check_image_prop)
parse_param.addParameter('SNRt', 0.5, @isnumeric)
parse_param.addParameter('thickring', 1, @isnumeric)
parse_param.addParameter('freq_thr', 0.02, @isnumeric)
parse_param.addParameter('prop_obj', [], check_input)
parse_param.addParameter('apod', [], check_input)
parse_param.addParameter('filter_FFT', [], check_input)
parse_param.addParameter('crop_factor', 1, @isnumeric)
parse_param.addParameter('crop_asize', [], @isnumeric)
parse_param.addParameter('z_lens', [], @isnumeric)
parse_param.addParameter('fourier_ptycho', false, check_input)
parse_param.addParameter('lambda', [], @isnumeric)
parse_param.addParameter('pixel_size', [], @isnumeric)
parse_param.addParameter('electron', false, check_input) %added by YJ for electron ptychography
parse_param.addParameter('verbose_level', 3, @isnumeric)
parse_param.addParameter('fname', [], @iscell)
parse_param.addParameter('show_summary', true, check_input)
parse_param.addParameter('xlabel_type', 'nyquist', @(x)ismember(lower(x), {'nyquist', 'resolution'})) % select X axis units
parse_param.parse(param)
param = parse_param.Results;
if isempty(param.crop)
utils.verbose(3,'Cropping half size of the probe (default)')
end
if isfield(param,'taper')
taper = param.taper;
else
taper = 20;
utils.verbose(3,'Using taper = 20 (default)')
end
if isempty(param.fname)
param.fname = file_path;
end
utils.verbose(param.verbose_level);
utils.verbose(struct('prefix', {'FSC'}))
% set dispfsc value - used in utils.fourier_shell_corr_3D_2
param.dispfsc = (param.plotting > 0);
%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%
if ischar(file{1})
if ~flag_imread
%%% Checking if file exist and loading %%%
if exist(file{1},'file')
param.fourier_ptycho = io.HDF.hdf5_load(file{1}, '/reconstruction/p/fourier_ptycho');
utils.verbose(2, ['Loading: ' file{1}])
recons{1} = load_ptycho_recons(file{1}, 'object');
if param.fourier_ptycho
recons{1} = FP_FSC_preprocess(recons{1}, file{1}, param);
end
else
error(['Not found: ' file{1}])
end
if exist(file{2},'file')
utils.verbose(2, ['Loading: ' file{2}])
recons{2} = load_ptycho_recons(file{2}, 'object');
if param.fourier_ptycho
recons{2} = FP_FSC_preprocess(recons{2}, file{2}, param);
end
else
error(['Not found: ' file{2}])
end
img1 = recons{1}.object;
img2 = recons{2}.object;
% load additional parameters
asize = double(io.HDF.hdf5_load(file{1}, '/reconstruction/p/asize'));
if ~param.fourier_ptycho
param.pixel_size = io.HDF.hdf5_load(file{1}, '/reconstruction/p/dx_spec');
else
param.pixel_size = recons{1}.p.objpix;
end
try
if isempty(param.lambda) && io.HDF.hdf5_dset_exists(file{1}, 'lambda', '/reconstruction/p', true)
param.lambda = io.HDF.hdf5_load(file{1}, '/reconstruction/p/lambda');
end
end
else
if ~isnumeric(file{1})
img1 = imread(file{1});
else
img1 = file{1}; %input provides directly the numeric array
end
if ~isnumeric(file{2})
img2 = imread(file{2});
else
img2 = file{2}; %input provides directly the numeric array
end
asize = [1 1];
if isempty(param.pixel_size)
param.pixel_size = 1e-6;
warning('Using pixel size 1um')
end
end
else
img1 = file{1}; %input provides directly the numeric array
img2 = file{2}; %input provides directly the numeric array
if ~isfield(param, 'asize')
asize = [1 1];
else
asize = param.asize;
end
if isempty(param.pixel_size)
param.pixel_size = 1e-6;
warning('Using pixel size 1um')
end
end
if param.flipped_images
img2 = fliplr(img2);
end
% apply apodization
% check if apodization was used in the reconstruction
if isempty(param.apod)
try
param.apod = io.HDF.hdf5_load(file{1}, '/reconstruction/p/plot/obj_apod');
catch
warning('Unable to load apodization parameter from reconstruction file.')
param.apod = false;
end
end
if param.apod
img1 = apply_apod(img1, asize);
img2 = apply_apod(img2, asize);
end
% propagate if needed
if isempty(param.prop_obj) || param.prop_obj
if isempty(param.lambda)
try
param.lambda = io.HDF.hdf5_load(file{1}, '/reconstruction/p/lambda');
catch
try
param.energy = io.HDF.hdf5_load(file{1}, '/reconstruction/p/energy');
param.lambda = 12.4/param.energy*1e-10;
catch
error('Please specify your wavelength (param.lambda).')
end
end
end
if isempty(param.prop_obj) || islogical(param.prop_obj) && param.prop_obj
% get values from file
try
param.prop_obj = io.HDF.hdf5_load(file{1}, '/reconstruction/p/prop_obj');
catch
error('Please specify the propagation distance or set it to "false" (param.prop_obj).')
end
end
img1 = utils.prop_free_nf(img1, param.lambda, param.prop_obj, param.pixel_size);
img2 = utils.prop_free_nf(img2, param.lambda, param.prop_obj, param.pixel_size);
end
screensize = get( 0, 'Screensize' );
% Show phase images (not cropped)%
if param.plotting > 1
plotting.smart_figure(21)
set(gcf,'Outerposition',[1 screensize(4)-550 500 500]) %[left, bottom, width, height
if ~isreal(img1)
imagesc(angle(img1), math.sp_quantile(angle(img1),[1e-2, 1-1e-2],10));
if ~param.fourier_ptycho
rectangle('Position',[asize([2,1])/2, [size(img1,2),size(img1,1)]-asize([2,1])], 'EdgeColor', 'red')
end
else
imagesc(img1, math.sp_quantile((img1),[1e-2, 1-1e-2],10));
end
axis xy equal tight
colormap bone
colorbar
if param.prop_obj
[si_unit, val] = utils.get_unit_length(param.prop_obj);
title(sprintf([param.fname{1} '\npropagated by %d %s'], val, si_unit),'interpreter','none')
else
title(param.fname{1},'interpreter','none')
end
plotting.smart_figure(22)
if ~isreal(img1)
imagesc(angle(img2), math.sp_quantile(angle(img2),[1e-2, 1-1e-2],10));
else
imagesc(img2, math.sp_quantile(img2,[1e-2, 1-1e-2],10));
end
if ~param.fourier_ptycho
rectangle('Position',[asize([2,1])/2, [size(img2,2),size(img2,1)]-asize([2,1])], 'EdgeColor', 'red')
end
axis xy equal tight
colormap bone
colorbar
if param.prop_obj
[si_unit, val] = utils.get_unit_length(param.prop_obj);
title(sprintf([param.fname{2} '\npropagated by %d %s'], val, si_unit),'interpreter','none')
else
title(param.fname{2},'interpreter','none')
end
set(gcf,'Outerposition',[500 screensize(4)-550 500 500]) %[left, bottom, width, height
end
% Crop images - default is half the size of the probe on each side plus
% whatever needed to make them of equal size
if isempty(param.crop)
minsize = min(size(img1),size(img2))-asize;
img1 = crop_pad(img1,minsize );
img2 = crop_pad(img2,minsize );
elseif strcmpi(param.crop, 'manual')
figure()
imagesc(angle(img1), math.sp_quantile(angle(img1),[1e-2, 1-1e-2],10));
if ~param.fourier_ptycho
rectangle('Position',[asize([2,1])/2, [size(img1,2),size(img1,1)]-asize([2,1])], 'EdgeColor', 'red')
end
colormap bone
axis image xy
title('Select compared region')
disp('Manually select the compared region ... ')
rect = round(getrect);
param.crop = {rect(2)+(1:rect(4)),rect(1)+(1:rect(3))};
disp('===========================')
fprintf('Selected region: {%i:%i,%i:%i}\n',rect(2), rect(2)+rect(4), rect(1), rect(1)+rect(3));
disp('===========================')
pause(1)
end
if ~isempty(param.crop)
img1 = img1(param.crop{:});
img2 = img2(param.crop{:});
end
if param.GUIguess
plotting.smart_figure(21)
disp(['Click on a feature on figure 2'])
[xin yin] = ginput(1);
plotting.smart_figure(22)
disp(['Click on a feature on figure 3'])
[xin2 yin2] = ginput(1);
param.guessx = round(xin-xin2);
param.guessy = round(yin-yin2);
end
if ~isempty(param.guessx)
switch sign(param.guessx)
case 1
img1 = img1(:,1+param.guessx:end);
img2 = img2(:,1:end-param.guessx);
case -1
img1 = img1(:,1:end+param.guessx);
img2 = img2(:,1-param.guessx:end);
end
end
if ~isempty(param.guessy)
switch sign(param.guessy)
case 1
img1 = img1(1+param.guessy:end,:);
img2 = img2(1:end-param.guessy,:);
case -1
img1 = img1(1:end+param.guessy,:);
img2 = img2(1-param.guessy:end,:);
end
end
% Remove ramp
if param.remove_ramp
utils.verbose(3,'Removing ramp for initial alignment')
img1 = utils.stabilize_phase(img1,'binning', 4);
img2 = utils.stabilize_phase(img2, img1, 'binning', 4);
end
if param.plotting >2
plotting.smart_figure(23)
set(gcf,'Outerposition',[1 1 500 476]) %[left, bottom, width, height
if ~isreal(img1)
imagesc(angle(img1));
else
imagesc(img1);
end
axis xy equal tight
colormap bone; colorbar
ax23 = gca;
title(param.fname{1},'interpreter','none')
plotting.smart_figure(24);
if ~isreal(img2)
imagesc(angle(img2));
else
imagesc(img2);
end
axis xy equal tight
colormap bone; colorbar
title(param.fname{2},'interpreter','none')
set(gcf,'Outerposition',[500 1 500 476]) %[left, bottom, width, height]
ax34 = gca;
linkaxes([ax23, ax34], 'xy')
end
% img1 = img1 - utils.imgaussfilt2_fft(img1, 20);
% img2 = img2 - utils.imgaussfilt2_fft(img2, 20);
% image_prop= 'complex';
%%% Initial alignment %%%
utils.verbose(2,'Initial alignment')
if ~flag_imread
switch lower(param.image_prop)
case 'complex'
imgalign1 = img1;
imgalign2 = img2;
utils.verbose(2,'Registering complex valued images')
case 'phasor'
imgalign1 = ones(size(img1)).*exp(1i*angle(img1));
imgalign2 = ones(size(img2)).*exp(1i*angle(img2));
utils.verbose(2,'Registering phasor of complex valued images')
case 'phase'
imgalign1 = angle(img1);
imgalign2 = angle(img2);
utils.verbose(2,'Registering phase of complex valued images')
case 'variation'
[dX,dY] = math.get_phase_gradient_2D(img1);
imgalign1 = sqrt(dX.^2+dY.^2);
[dX,dY] = math.get_phase_gradient_2D(img2);
imgalign2 = sqrt(dX.^2+dY.^2);
end
else
imgalign1 = img1;
imgalign2 = img2;
end
upsamp = 100;
displ = utils.verbose>3;
W = 1;
x1 = [];%[1:150];
x2 = x1;
y1 = [];%[1:238];
y2 = y1;
% imgalign2 = shiftpp2(imgalign2,10,-10); % To test range adjustment
[subim1, subim2, delta, deltafine, regionsout] = registersubimages_2(imgalign1,imgalign2, x1, y1, x2, y2, upsamp, displ,1);
%%% Fine alignment (second round) %%%
% Remove ramp for fine alignment
utils.verbose(2,'Removing ramp for fine alignment')
%%% A patch for deltafine large
if max(regionsout.y2+round(delta(1)))>size(img2,1)
warning('First subpixel registration refinement found large values')
regionsout.y2 = [min(regionsout.y2):size(img2,1)-round(delta(1))];
regionsout.y1 = regionsout.y2;
end
if max(regionsout.x2+round(delta(2)))>size(img2,2)
warning('First subpixel registration refinement found large values')
regionsout.x2 = [min(regionsout.x2):size(img2,2)-round(delta(2))];
regionsout.x1 = regionsout.x2;
end
%%%
subimg1 = img1(regionsout.y1,regionsout.x1);
subimg2 = img2(regionsout.y2+round(delta(1)),regionsout.x2+round(delta(2)));
if ~flag_imread
subimg1 = remove_linearphase_v2(subimg1,ones(size(subimg1)),100);
subimg2 = remove_linearphase_v2(subimg2,ones(size(subimg2)),100);
end
% Remove ramp
if param.remove_ramp
utils.verbose(2,'Removing ramp for initial alignment')
subimg1 = utils.stabilize_phase(subimg1,'binning', 4);
subimg2 = utils.stabilize_phase(subimg2, subimg1, 'binning', 4);
end
if ~flag_imread
switch lower(param.image_prop)
case 'complex'
subimgalign1 = subimg1;
subimgalign2 = subimg2;
utils.verbose(2,'Registering complex valued images')
case 'phasor'
subimgalign1 = ones(size(subimg1)).*exp(1i*angle(subimg1));
subimgalign2 = ones(size(subimg1)).*exp(1i*angle(subimg2));
utils.verbose(2,'Registering phasor of complex valued images')
case 'phase'
subimgalign1 = angle(subimg1);
subimgalign2 = angle(subimg2);
utils.verbose(2,'Registering phase of complex valued images')
case 'variation'
[dX,dY] = math.get_phase_gradient_2D(subimg1);
subimgalign1 = sqrt(dX.^2+dY.^2);
[dX,dY] = math.get_phase_gradient_2D(subimg2);
subimgalign2 = sqrt(dX.^2+dY.^2);
end
else
subimgalign1 = subimg1;
subimgalign2 = subimg2;
end
% Fine alignment %
utils.verbose(2,'Fine alignment')
[subim1, subim2, delta, deltafine, regionsout] = registersubimages_2(subimgalign1,subimgalign2, x1, y1, x2, y2, upsamp, displ,1);
%%% propare images for FSC if variation was used for alignement
if strcmpi(param.image_prop, 'variation')
subim1 = subimg1(regionsout.y1, regionsout.x1);
subim2 = subimg2(regionsout.y2, regionsout.x2);
subim2 = shiftpp2(subim2,-deltafine(1), -deltafine(2)); %% Suboptimal, change to use a routine that receives FT data
% convert images to phasor
subim1 = exp(1i*angle(subim1));
subim2 = exp(1i*angle(subim2));
end
%%% Tapering %%%
filterx = fract_hanning_pad(size(subim1,2),size(subim1,2),size(subim1,2)-2*taper);
filterx = fftshift(filterx(1,:));
filtery = fract_hanning_pad(size(subim1,1),size(subim1,1),size(subim1,1)-2*taper);
filtery = fftshift(filtery(:,1));
filterxy = filterx.*filtery;
% Taper subimages %
subim1 = subim1.*filterxy;% + (1-filterxy).*mean(subim1(:));
subim2 = subim2.*filterxy;% + (1-filterxy).*mean(subim2(:));
if param.plotting > 1
plotting.smart_figure(23)
set(gcf,'Outerposition',[1 1 500 476]) %[left, bottom, width, height
if strcmpi(param.image_prop,'phase') || flag_imread
imagesc(subim1);
else
imagesc(angle(subim1));
end
axis xy equal tight
colormap bone; colorbar
if param.prop_obj
[si_unit, val] = utils.get_unit_length(param.prop_obj);
title(sprintf([param.fname{1} '\npropagated by %d %s'], val, si_unit),'interpreter','none')
else
title(param.fname{1},'interpreter','none')
end
plotting.smart_figure(24)
if strcmpi(param.image_prop,'phase') || flag_imread
imagesc(real(subim2));
else
imagesc(angle(subim2));
end
axis xy equal tight
colormap bone; colorbar
if param.prop_obj
[si_unit, val] = utils.get_unit_length(param.prop_obj);
title(sprintf([param.fname{2} '\npropagated by %d %s'], val, si_unit),'interpreter','none')
else
title(param.fname{2},'interpreter','none')
end
set(gcf,'Outerposition',[500 1 500 476]) %[left, bottom, width, height
end
%% Computing the FSC
param.st_title = sprintf('%s\n %s\n flipped_images %d, taper %d',param.fname{1}, param.fname{2}, param.flipped_images, taper);
if flag_imread
subim1 = real(subim1);
subim2 = real(subim2);
warning('Assuming images are in real number!');
end
subim1 = utils.stabilize_phase(subim1, subim2, 'binning', 4, 'fourier_guess', false);
if param.electron %special version for electron ptychography. unit: angstrom
[resolution,FSC,T,freq,n,stat] = fourier_shell_corr_3D_2e(subim1,subim2, param);
else
[resolution,FSC,T,freq,n,stat] = fourier_shell_corr_3D_2(subim1,subim2, param);
end
%% visually compare alignment quality
if param.plotting>2
plotting.smart_figure(4545)
subplot(1,2,1)
imagesc3D(angle(subim1 .* conj( subim2)))
colormap bone
axis off image
colorbar
title('Phase difference between aligned sub-images')
subplot(1,2,2)
imagesc3D(cat(3,angle(subim1),angle(subim2)))
colormap bone
axis off image
title('Compare aligned sub-images')
colorbar
plotting.suptitle('Visually compare quality and verify alignement / drifts')
end
end
function img = apply_apod(img, asize)
ob_good_range = {asize(1)/2:size(img,1)-asize(1)/2, asize(2)/2:size(img,2)-asize(2)/2};
filt_size = [size(ob_good_range{1},2) size(ob_good_range{2},2)];
img = img.*fftshift(utils.filt2d_pad(size(img), max(1,filt_size), max(1,filt_size-min(floor(filt_size.*0.05)))));
end