Files
fold_slice/tomo/+tomo/phase_ramp_removal_tomo.m
2026-08-07 15:56:42 +09:00

312 lines
14 KiB
Matlab
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
% PHASE_RAMP_REMOVAL_TOMO Use tomography consistency between measured and
% reconstructed phase to remove phase ramp from data
% This function uses mask in volume space to accuratelly find regions of
% air in the projection space. These regions are iterativelly forced
% towards zero
%
% Several iterations are performed to further improve precision
%
% object_full = phase_ramp_removal_tomo(object_full,object_ROI, theta, Npix, total_shift, par, varargin)
%
% Inputs:
% **object_full - complex-valued projections
% **object_ROI - reliable region used for reconstruction
% **theta - tomography angles
% **Npix - size of reconstruction
% **par - tomography parameter structure
% *optional* (or use values from par structure as default if provided)
% **binning = 4 - bin data to make reconstruction faster & more robust
% **positivity = true - apply positivity constaint
% **auto_weighting = true - give less weight to thic regions of the sample
% **fourier_guess = true - calculate FFT to find phase ramp, important if the phase ramp is more than 2pi per frame
% **Niter = 3 - number of iterations for phase removal
% **unwrap_data_method = 'fft_2d' - fft_2d , fft_1d
% **sino_weights = 1 - importance weights
% **CoR_offset = [] - offset of the center of rotation, default is center of projection
% **inplace_processing = false - process data inplace to save memory
%
% *returns*
% ++object_full - complex-valued projections after phase ramp removal
%*-----------------------------------------------------------------------*
%|                                                                       |
%|  Except where otherwise noted, this work is licensed under a          |
%|  Creative Commons Attribution-NonCommercial-ShareAlike 4.0            |
%|  International (CC BY-NC-SA 4.0) license.                             |
%|                                                                       |
%|  Copyright (c) 2017 by Paul Scherrer Institute (http://www.psi.ch)    |
%|                                                                       |
%|      Author: CXS group, PSI  |
%*-----------------------------------------------------------------------*
% You may use this code with the following provisions:
%
% If the code is fully or partially redistributed, or rewritten in another
% computing language this notice should be included in the redistribution.
%
% If this code, or subfunctions or parts of it, is used for research in a
% publication or if it is fully or partially rewritten for another
% computing language the authors and institution should be acknowledged
% in written form in the publication: “Data processing was carried out
% using the “cSAXS matlab package” developed by the CXS group,
% Paul Scherrer Institut, Switzerland.”
% Variations on the latter text can be incorporated upon discussion with
% the CXS group if needed to more specifically reflect the use of the package
% for the published work.
%
% A publication that focuses on describing features, or parameters, that
% are already existing in the code should be first discussed with the
% authors.
%
% This code and subroutines are part of a continuous development, they
% are provided “as they are” without guarantees or liability on part
% of PSI or the authors. It is the user responsibility to ensure its
% proper use and the correctness of the results.
function [object_full, W] = phase_ramp_removal_tomo(object_full,object_ROI, theta, Npix,total_shift, par, varargin)
import utils.*
verbose(struct('prefix', 'phase_ramp_remove'))
parser = inputParser;
parser.addParameter('binning', 4 , @isnumeric ) % bin data to make reconstruction faster & more robust
parser.addParameter('positivity', true , @islogical ) % apply positivity constaint
parser.addParameter('auto_weighting', true , @islogical ) % give less weight to thic regions of the sample
parser.addParameter('fourier_guess', true , @islogical ) % calculate FFT to find phase ramp, important if the phase ramp is more than 2pi per frame
parser.addParameter('Niter', 3 , @isnumeric ) % number of iterations for phase removal
parser.addParameter('unwrap_data_method', 'fft_2d' , @isstr ) % fft_2d , fft_1d
parser.addParameter('sino_weights', 1, @isnumeric ) % importance weights
parser.addParameter('CoR_offset', [] , @isnumeric ) % offset of the center of rotation, default is center of projection
parser.addParameter('CoR_offset_v', [] , @isnumeric ) % added by YJ. vertical offset of the center of rotation, default is center of projection
parser.addParameter('inplace_processing', false, @islogical ) % process data inplace to save memory
parser.parse(varargin{:})
r = parser.Results;
% load all varargins to the param structure
for name = fieldnames(r)'
if ~isfield(par, name{1}) || ~ismember(name, parser.UsingDefaults) % prefer values in param structure if parsers returns default value
par.(name{1}) = r.(name{1});
end
end
verbose(0,'Calculating phase ramp + amplitude correction')
Np_full = size(object_full);
verbose(1,'Binning: %i', par.binning)
if ismember(lower(par.unwrap_data_method), {'none', 'fft_2d'})
interp_sign = -1 ;
else
interp_sign = 1 ;
end
% use symmetrically expanded ROI, get more region around sample
for ii = 1:2
object_ROI{ii} = max(1, object_ROI{ii}(1)-ceil(par.asize(ii)/4)):min(Np_full(ii), ceil(object_ROI{ii}(end)+par.asize(ii)/4));
end
% shift the projections back to the "after loading" positions -> avoid boundary problems when
% the phase ramp removal is applied
% !! high accuracy downsampling and shift is not needed in this function !!
object = tomo.block_fun(@imshift_generic,object_full, -total_shift, [], [], 1, object_ROI, par.binning, 'fft', interp_sign,struct('use_fp16', false));
Npix = ceil(Npix / par.binning);
if isscalar(Npix)
Nlayers = size(object,1);
Npix = [Npix,Npix,Nlayers];
end
Ngpu = max(1,length(par.GPU_list));
if ~isscalar(par.sino_weights) && ~isempty(par.sino_weights)
sino_weights = tomo.block_fun(@imshift_generic,par.sino_weights, -total_shift, Np_full(1:2), [], 1, object_ROI, par.binning, 'linear', ...
struct('use_GPU', true, 'full_block_size', Np_full));
else
sino_weights = 1;
end
if all(mean(mean(abs(sino_weights-mean(mean(sino_weights))))) < 1e-2)
sino_weights = 1;
else
sino_weights = real(sino_weights ./ max(max(sino_weights)));
end
% if ismatrix(par.illum_sum)
% sino_weights = sino_weights .* imshift_generic(par.illum_sum,[0,0],Np_full(1:2), [], 1, object_ROI, par.binning, 'linear');
% end
gamma_tot = 1;
gamma_x_tot = 0;
gamma_y_tot = 0;
[~,circulo] = apply_3D_apodization(ones(Npix), 0, 0, 10);
for ii = 1:par.Niter
progressbar(ii, par.Niter)
phase = tomo.block_fun(@unwrap_object,object,sino_weights, par, struct('use_fp16', false, 'verbose_level', 0));
if par.positivity
% "positivity" constraint, useful for normal tomo but it has to be false for laminography
phase = min(0, phase);
end
[Nlayers,width_sinogram,~]=size(phase);
% find rotation center so that it stays consistent after binning
par.rotation_center = [Nlayers, width_sinogram]/2;
if ~isempty(par.CoR_offset) % important for laminography
par.rotation_center(2) = par.rotation_center(2) + par.CoR_offset/par.binning;
end
%added by YJ
if ~isempty(par.CoR_offset_v) % important for laminography
par.rotation_center(1) = par.rotation_center(1) + par.CoR_offset_v/par.binning;
end
par.rotation_center = par.rotation_center - total_shift(:,[2,1])/par.binning;
[cfg, vectors] = astra.ASTRA_initialize(Npix,[Nlayers,width_sinogram],theta,par.lamino_angle,par.tilt_angle, [par.horizontal_scale ; par.vertical_scale]', par.rotation_center);
split = astra.ASTRA_find_optimal_split(cfg,Ngpu,1,'back');
% get FBP reconstruction from the initial guess
rec = -tomo.FBP(phase, cfg, vectors, [1,1,Ngpu], 'GPU', par.GPU_list, 'split_sub', split, 'verbose',0);
clear phase
rec = rec .* circulo; % remove effect of unmeasured regions around sample
if par.positivity
% positivity constraint
rec = max(0, rec);
end
% find model projections for given reconstruction
split = astra.ASTRA_find_optimal_split(cfg,Ngpu,1,'fwd');
proj = tomo.Ax_sup_partial(rec, cfg, vectors, [1,1,Ngpu], 'GPU', par.GPU_list, 'split_sub', split ,'verbose',0);
if par.auto_weighting
%% zero weights to regions with sample compared to air regions
Thresh = graythresh(rec(:));
% find roughly region where is only air
mask = single(rec < Thresh);
% find the corresponding region in the projection space
proj_mask = tomo.Ax_sup_partial(mask, cfg, vectors, [1,1,Ngpu], 'GPU', par.GPU_list, 'split_sub', split ,'verbose',0);
proj_blank = astra.Ax_partial(ones(Npix,'single'), cfg, vectors, [1,1,Ngpu], 'GPU', par.GPU_list, 'split_sub', split ,'verbose',0);
% define corresponding mask
W = ((abs(proj_mask - proj_blank) ./ proj_blank) < 1e-2) .* sino_weights;
%size(proj_blank)
% try to estimate weights direclty from the projections -> just to account for
% case when mask == 0 everywhere
W = W + 1e-1*exp(-abs(proj).^2 / mean(abs(proj(:))).^2 );
else
W = sino_weights;
end
W([1,end],:,:) = 0; % avoid boundary effects
% find phase ramp so that the masked regions are zero, if not possible, just enforce
% consistency between the object and projection
[object, gamma, gamma_x, gamma_y] = stabilize_phase(object, exp(-1i*proj.* (1-W)), W, 'fourier_guess', false);
gamma_tot = gamma_tot .* gamma;
gamma_x_tot = gamma_x_tot + gamma_x;
gamma_y_tot = gamma_y_tot + gamma_y;
end
if any(isnan(gamma_tot)) || any(isnan(gamma_x_tot)) || any(isnan(gamma_y_tot))
error('Phase removal would result in NaNs')
end
if par.auto_weighting
% store the produced mask -> false for regions of air
projection_mask = ((abs(proj_mask - proj_blank) ./ proj_blank) < 1e-2) & (sino_weights > 0);
end
%% calculate amplitude correction
%use median of the masked regions to estimate amplitude correction factor
% use of median means the mask needs to be correct only in > 50% of the area
aobject = abs(object);
if par.auto_weighting
aobject(~projection_mask) = nan;
end
amp_correction = reshape(nanmedian(reshape(aobject,[],Np_full(3))),1,1,[]);
% just to be sure that there is some mask everywhere
amp_correction(isnan(amp_correction)) = mean(mean(abs(object(:,:,isnan(amp_correction)))));
%% apply the phase and amplitude correction to the original stack_object array
verbose(0,'Applying phase ramp + amplitude correction')
% Run locally on CPU , too slow GPU upload / download
cfg = struct('verbose_level',1,'inplace', par.inplace_processing, 'use_GPU', true);
object_full = tomo.block_fun(@apply_ramp_shifted,object_full, gather(gamma_tot), gather(gamma_x_tot)/par.binning, gather(gamma_y_tot)/par.binning,total_shift,amp_correction, cfg);
verbose(0,'Done')
verbose(struct('prefix', 'template'))
end
%%% AUXILIARY FUNCTION FOR FAST PROCESSING ON GPU
function phase = unwrap_object(object,sino_weights, par)
% get initial guess
switch lower(par.unwrap_data_method)
case 'none' %added by YJ
phase = angle(object);
case 'fft_1d'
phase = math.unwrap2D_fft(object,2,par.air_gap/par.binning);
case 'fft_2d'
phase = math.unwrap2D_fft2(object,par.air_gap/par.binning,0,sino_weights,1);
otherwise
error('Undefined unwrapping method')
end
end
%%%% AUXILIARY FUNCTION FOR PARALLEL GPU PROCESSING
function object_full = apply_ramp_shifted(object_full,gamma, gamma_x, gamma_y, total_shift, amp_correction)
% shift the projection to the original (ie after loading) positions to around ramp artefacts
% around edges if the projection was shifted too much
% it needs 2D circular shift (is nearest neighbor interpolation), FFT is not needed
object_full = utils.imshift_linear(object_full, -total_shift(:,1),-total_shift(:,2), 'circ');
[M,N,~] = size(object_full);
xramp = pi*(linspace(-1,1,M))';
yramp = pi*(linspace(-1,1,N));
if ~isa(object_full, 'gpuArray')
object_full = auxfun(object_full, gamma, gamma_x, gamma_y, xramp, yramp, amp_correction);
else
% use inplace GPU calculation
object_full = arrayfun(@auxfun, object_full, gamma, M*gamma_x, N*gamma_y, xramp, yramp, amp_correction);
end
object_full = utils.imshift_linear(object_full, total_shift(:,1),total_shift(:,2), 'circ');
end
function object = auxfun(object, gamma, gamma_x, gamma_y, xramp, yramp, amp_correction)
object = object .* (gamma./ amp_correction); % correct global phase and also amplitude
object = object .* exp(1i*xramp.*gamma_x);
object = object .* exp(1i*yramp.*gamma_y);
end