% ALIGN_TOMO_INITIAL Get fast initial guess of the vertical and horizontal alignment % % [optimal_shift] = align_tomo_initial(stack_object, shift_init, angles, param, varargin) % % Inputs: % **stack_object - complex-valued input array that will be unwrapped and used for alignment % **shift_init - initial guess of the shifts % **angles - corresponding angles (used only for sorting the projections) % *optional*: % if not provided, value from param is used as default % **air_gap - empty region around sample where phase = 0 is assumed % **vert_range - vertical range used for alignment , try to avoid highly % scattering / residual features % **phase_jumps_threshold - threshold above which the phase difference % is assumed to be wrong and masked out % **alignment_invariant - choose: phase_2D, phase_1D, phase_derivative, goldstein % **use_vertical_xcorr_guess - if true, use crosscorrelation for initial guess % **data_filter - high pass filter constant 0=none, 0.005-0.02 seems to be optimal % OTHER INPUTS DESCRIBED IN CODE % % *returns* % ++optimal_shift - (Nangles x 1 array) = vertical shift to be applied on the stack_object in order to minimize the vertical mass fluctuation %*-----------------------------------------------------------------------* %|                                                                       | %|  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 [optimal_shift] = align_tomo_initial(stack_object, shift_init, angles, ROI, param, varargin) if nargin < 3 param = struct(); end parser = inputParser; parser.addParameter('vert_range', [] , @isnumeric ) % vertical range used for alignment parser.addParameter('air_gap', [50, 50] , @isnumeric ) % rough estimate of air region parser.addParameter('phase_jumps_threshold', 1 , @isnumeric ) % threshold above which the phase difference is assume to be wrong parser.addParameter('alignment_invariant', 'phase_2D' , @isstr ) % name of the invariant used for alignment parser.addParameter('showsorted', true , @islogical ) % if the projections should be plotted sorted by angle parser.addParameter('use_vertical_xcorr_guess', true , @islogical ) % get an initial guess by Xcorr, -> avoid trapping in local minima parser.addParameter('data_filter', 0.02 , @isnumeric ) % high pass filtering to remove low spatial freq. errors %% internal variables, usually no need to change parser.addParameter('outer_loops_refinement', 3 , @isnumeric ) % number of outer loops for linear refinement step parser.addParameter('N_SVD_modes', 10 , @isnumeric ) % number of SVD modes used to fill empty gaps in phase invariant parser.addParameter('weights', [] , @isnumeric ) % numeric of logical array contaning weights for each projection and each pixels for 2D unwrapping parser.addParameter('windowautopos', true , @islogical ) % distribute the plots over screen autimatically parser.parse(varargin{:}) r = parser.Results; % load all to the param structure for name = fieldnames(r)' if ~isfield(param, name{1}) % prefer values in param structure param.(name{1}) = r.(name{1}); end end import tomo.* import utils.* import math.* utils.verbose(struct('prefix', 'align')) if ~isempty(param.vert_range) ROI{1} = ROI{1}(max(1,param.vert_range(1)):min(end,param.vert_range(end))); end Nlayers = length(ROI{1}); Nw = length(ROI{2}); Nangles = length(angles); switch param.alignment_invariant case 'phase_1D' %% standard vertical mass fluctuation utils.verbose(0,'Fast 1D FFT unwrapping') [phase, phase_diff, residues] = tomo.block_fun(@unwrap2D_fft, stack_object, 2, param.air_gap, struct('ROI', {ROI}, 'use_fp16', false)); invar0 = max(0,squeeze(sum(-phase,2))); jumps = abs(phase_diff) > param.phase_jumps_threshold; mask_invar = squeeze(any(jumps,2)); %% relevance weights for each line mask_residues = squeeze(sum(residues,2))>0; mask_residues = conv2(mask_residues,ones(3,1),'same')>0; mask_invar(2:end,:) = mask_invar(2:end,:) | mask_residues; case 'phase_2D' utils.verbose(0,'Fast 2D FFT unwrapping') % standard vertical mass fluctuation % fft-based unwrapping [phase, residues] = unwrap2D_fft2_split(stack_object, param.air_gap,1,param.weights,param.GPU_list,ROI); invar0 = squeeze(sum(phase,2)); mask_invar = squeeze(sum(residues,2))>0; % mask_invar = conv2(mask_invar,ones(3,1),'same')>0; case 'phase_derivative' % vertical derivative fluctuation utils.verbose(0,'Get phase gradient') phase_diff = tomo.block_fun(@get_phase_gradient_1D,stack_object, 1,1, struct('ROI', {ROI}, 'use_fp16', false)); invar0 = squeeze(sum(phase_diff,2)); jumps = abs(phase_diff) > param.phase_jumps_threshold; jumps(:,[1:2,end-1:end],:) = 0; % avoid jumps caused by phase ramp mask_invar = squeeze(sum(jumps,2) > 1); %% relevance weights for each line case 'phase_goldstein' utils.verbose(0,'Estimating residua') residues = abs(findresidues(stack_object)) > 0.1; mask_invar = squeeze(sum(residues,2))>0; if sum2(mask_invar) > 1 warning('Selected range contains %i residua', sum2(mask_invar)) end phase = zeros(Nlayers, Nw, Nangles, 'single'); parfor ii = 1:Nangles utils.progressbar(ii, Nangles) o = stack_object(:,:,ii) phase(:,:,ii) = utils.goldsteinunwrap2(angle(o(ROI{:}))); end phase = utils.remove_sinogram_ramp(phase,param.air_gap, true); invar0 = squeeze(sum(phase,2)); otherwise error('Missing option %s', par.alignment_invariant) end clear jumps % move to GPU, always assume that GPU is availible invar0 = Garray(invar0); if any(sum(invar0)==0) error('Some projections are empty') end if ~exist('residues', 'var') && ~strcmpi(param.alignment_invariant, 'phase_derivative') utils.verbose(0,'Estimating residua') residues = tomo.block_fun(@(x)(abs(findresidues(x)) > 0.1), stack_object); mask_invar = mask_invar | squeeze(sum(residues,2))>0; end if any(mean(mask_invar) > 0.9) wrong = param.scanstomo(mean(mask_invar) > 0.9); error(sprintf(['Too many phase jumps in %i angles, alignment will fail \n try to increase par.phase_jumps_threshold or change par.alignment_invariant\n Wrong scans: ', repmat('%i ',1,length(wrong)), ' \n quitting'], length(wrong), wrong )) elseif any(mean(mask_invar) > 0.7) wrong = param.scanstomo(mean(mask_invar) > 0.7); warning(sprintf(['Too many phase jumps in %i angles, alignment will most likely fail \n try to increase par.phase_jumps_threshold or change par.alignment_invariant\n Wrong scans: ', repmat('%i ',1,length(wrong)), ], length(wrong), wrong )) end if isempty(shift_init) shift_init = zeros(Nangles, 1); end Nplots = 2+param.use_vertical_xcorr_guess; if param.showsorted [sangles,plot_sort] = sort(angles); x_axis = sangles; x_label = 'Angled [deg]'; else plot_sort = 1:Nangles; x_axis = param.scanstomo; x_label = 'Scan number'; end weight = ~mask_invar; % remove linear offset -> prevents boundary problems invar = remove_linear_ramp(invar0); % apply only integer shift shift_Y = shift_init; invar = imshift_fft_ax(invar, shift_Y, 1); weight = imshift_linear_ax(weight, shift_Y, 1, 'nearest', 0); % select range without boundary issues offset= max(abs(shift_Y)); range = round(2+offset : Nlayers - offset-1); if length(range) < 20; error('Too small range for vertical alignment'); end invar = invar(range,:); weight = weight(range,:); Nlayers = length(range); % remove linear offset -> prevents boundary problems invar = remove_linear_ramp(invar); %% plot initial alignment fig_id = 5667; if param.windowautopos && ~ishandle(fig_id) % autopositioning only if the figure does not exists yet plotting.smart_figure(fig_id) set(gcf,'units','normalized','outerposition',[0.2 0.2 0.8 0.8]) else plotting.smart_figure(fig_id) end ax(1)=subplot(Nplots,3,1); invar_tmp = invar; invar_tmp = imfilter_high_pass_1d(invar_tmp, 1, param.data_filter, Nlayers/2); imagesc(x_axis, 1:Nlayers, invar_tmp(:,plot_sort), quantile(invar_tmp(~isnan(invar_tmp)), [1e-2,1-1e-2])) axis xy grid on title('No alignment, linear ramp removed') ylabel('Vertical axis [pixels]') xlabel(x_label) subplot(Nplots,3,2) invar_tmp(~weight) = nan ; plot(invar_tmp) xlabel('Vertical pixels') axis tight ax(4)=subplot(Nplots,3,3); imagesc(x_axis, 1:Nlayers, 1-weight(:,plot_sort)) axis xy title('Phase jumps / Residues mask') utils.verbose(0,'Vertical alignment - initial guess') utils.verbose(0,'Damaged pixels: %3.2g%%', mean2(~weight)*100) %subtitle('Tomography invariant vertical alignment') ylabel('Vertical axis [pixels]') xlabel(x_label) if param.use_vertical_xcorr_guess %% use cross correlation as the first guess [shift_Y,invar_filtered] = ... cross_correlation_estimation(invar, weight, angles, param.N_SVD_modes, param.data_filter); % try to be smart and avoid drastic jumps % shift_Y = max(shift_Y, quantile(shift_Y, 1e-2)); % shift_Y = min(shift_Y, quantile(shift_Y, 1-1e-2)); % minimize the shift offset shift_Y = shift_Y - (max(shift_Y)+min(shift_Y))/2; %shift_Y = shift_Y - median(shift_Y); % perform only nearest neighbor shift invar_filtered = imshift_linear_ax(invar_filtered, shift_Y, 1, 'circ'); weight_shifted = imshift_linear_ax(weight, shift_Y, 1, 'nearest',0); %% plot current estimation ax(2)=subplot(Nplots,3,4); imagesc(x_axis, 1:Nlayers, invar_filtered(:,plot_sort), quantile(invar_filtered(:), [1e-2,1-1e-2])) title('X-corr based guess - highpass filtered') axis xy grid on ylabel('Vertical axis [pixels]') xlabel(x_label) subplot(Nplots,3,5) invar_filtered(~weight_shifted) = nan; plot(invar_filtered) axis tight xlabel('Vertical pixels') title('Line plot - highpass filtered') subplot(Nplots,3,6) plot(x_axis, shift_Y(plot_sort)) axis tight title('Applied shift') ylabel('Shift [pixels]') grid on utils.verbose(0,'Vertical alignment - iterative refinement') xlabel(x_label) else shift_Y = zeros(Nangles,1); end %% iterative vertical position refinement for ii = 1:param.outer_loops_refinement progressbar(ii, param.outer_loops_refinement) % shift sinograms invar_shifted = imshift_fft_ax(invar, squeeze(shift_Y),1); weights_shifted = imshift_linear_ax(weight, squeeze(shift_Y),1,'nearest',0); % select range without boundary issues offset= max(abs(shift_Y)); range = round(1+offset : Nlayers - offset); assert(length(range) > 30, 'Too small vertical range for alignment') % crop to the undamaged region by boundary issues invar_shifted = invar_shifted(range,:); weights_shifted = weights_shifted(range,:); % perform alignment [shift_update, invar_shifted,weights_shifted] = linear_iterative_refinement(invar_shifted, weights_shifted, param.data_filter); shift_Y = shift_Y + shift_update; end if param.outer_loops_refinement > 0 %% plot results ax(3)=subplot(Nplots,3,3*Nplots-2); imagesc(x_axis, range , invar_shifted(:,plot_sort), quantile(invar_shifted(:), [1e-2,1-1e-2])) axis xy grid on title('Iterative refinement') xlabel(x_label) ylabel('Vertical axis [pixels]') %% plot results subplot(Nplots,3,3*Nplots-1) invar_shifted_plot = invar_shifted; invar_shifted_plot(weights_shifted==0) = nan; plot(invar_shifted_plot) xlabel('Vertical pixels') axis tight title('Line plot - highpass filtered') subplot(Nplots,3,3*Nplots) plot(x_axis, shift_Y(plot_sort)) axis tight title('Applied shift') ylabel('Shift [pixels]') grid on xlabel(x_label) end try linkaxes(ax, 'xy'); end drawnow if exist(param.output_folder, 'file') && ~debug() try paths{1} = [param.output_folder, '/vertical_alignment.png']; if param.online_tomo paths{2} = [param.output_folder, '/vertical_alignment.png']; end for ii = 1:length(ii) print(['-f', num2str(fig_id)],'-dpng', paths{ii} ) utils.verbose(0,['Plot saved to:', paths{ii}]) system(sprintf('convert -trim %s %s', paths{ii}, paths{ii})); end catch err warning('vertical_alignment.png saving failed with error:\n "%s"', err.message) end end optimal_shift = shift_Y + shift_init; optimal_shift = optimal_shift - median(optimal_shift); end function [shift_Y, invar, weight] = cross_correlation_estimation(invar0, weight, angles, N_SVD_modes, data_filter) %% cross-corelation based alignment guess %% make a fast initial guess based on the tomography invariant and cross-correlation % take several angles at the beginning to get some initial guess of the % vertical fluctuation shape and use Xcorr to find the optimal shifts import utils.Garray % remove linear offset [Nlayers, Nangles]= size(invar0); [~,angle_sort] = sort(angles); % move on GPU invar0 = Garray(invar0); weight = Garray(weight); % helps a lot in case of golden ratio datasets invar0 = invar0(:,angle_sort); weight = weight(:,angle_sort); % apply high pass filter invar = imfilter_high_pass_1d(invar0, 1, data_filter, Nlayers/2); % further suppress boundary effects invar = invar.* tukeywin(Nlayers, 0.1); % update weight of inreliable pixels range = quantile(invar(:), [0.01 , 0.99]); weight_invar = invar > range(1) & invar < range(2) & weight; % crop to the limited range invar = max(min(invar, range(2)), range(1)); % fill missing data invar = fill_gaps_1D(invar,~weight_invar, N_SVD_modes, 20); % find 5 of the most representative angles to be used as referene [~, ~, ~, D] = kmeans(invar0',1); % using invar before highpass filter to find optimal cluster center seems to work better [~,ind] = sort(D); invar_reference = median(invar(:,ind(1:5)),2); % find optimal shift using Xcorr method shift_Y = -utils.find_shift_fast_1D(invar,invar_reference,1,0)'; % provide some extra robustness by using median filter -> assume that % neighboring projections are quite well aligned shift_Y = gather(shift_Y); medfilt_win = 3; mshift_Y = medfilt1(shift_Y,medfilt_win, 'truncate'); medfilt_resid = shift_Y - mshift_Y; % residuum betw % avoid too large jumps with respect to rest of the shifts range = 2*quantile(medfilt_resid, [0.001, 0.999]); medfilt_resid = max(min(medfilt_resid, range(2)), range(1)); shift_Y = mshift_Y + medfilt_resid; weight = weight & weight_invar; %% resort to original order [~,scan_sort] = sort(angle_sort); shift_Y = shift_Y(scan_sort); weight = weight(:,scan_sort); invar = invar(:,scan_sort); % move from GPU invar = gather(invar); weight = gather(weight); end function invar = fill_gaps_1D(invar0,mask_invar, N_SVD_modes, Niter) % try to repair failed values , iterativelly replace them using SVD % method by most propable value import math.* if ~any(mask_invar(:)) invar = invar0; return; end invar = invar0; range = quantile(invar(~mask_invar), [0.01, 0.99]); for i = 1:Niter % slowly increase complexity [U,S,V]=fsvd(invar,N_SVD_modes); invar_filt = U * S*V'; % %get smooth estimate from SVD invar = invar.*~mask_invar + invar_filt .* mask_invar ; %% replace missing by a smooth curve % avoid outliers invar = max(min(invar, range(2)), range(1)); end end function array = remove_linear_ramp(array) % auxiliary function to subtract linear ramp from sinogram % it is important to avoid edge ringing and other artefacts when FFT % filtering is applied on the 2D array [Nlayers]= size(array,1); Nedge = 5; % number of averaged edge layers top = mean(array(1:Nedge,:)); bottom = mean(array(end-Nedge:end,:)); ramp = interp1([0,Nlayers]',[top;bottom], 1:Nlayers); array = array - ramp; end function [total_shift_Y, invar,weights] = linear_iterative_refinement(invar_0, weights_0, data_filter) %% ITERATIVE REFINEMENT OF VERTICAL ALIGNMENT METHOD % method based on optical flow, it can deal better with the missing / % damaged data compared to the Xcorr based methods -> it us used for % refinement of the Xcorr guess import utils.* import math.* [Nlayers,Nangles] = size(invar_0); total_shift_Y = zeros(Nangles,1); invar_0 = Garray(invar_0); weights_0 = Garray(single(weights_0)); total_shift_Y = Garray(total_shift_Y); % apply high pass filter invar_0 = remove_linear_ramp(invar_0); invar_0 = imfilter_high_pass_1d(invar_0, 1, data_filter, Nlayers/2); % further suppress boundary effects invar_0 = invar_0 .* tukeywin(Nlayers, 0.1); % fill missing data invar_0 = fill_gaps_1D(invar_0,~weights_0, 2, 20); % img = imshift_2D(ones(10), randn(10,1)*2) X = utils.Garray(1:Nlayers); Y = utils.Garray(1:Nangles); [X,Y] = meshgrid(X,Y); relax_step = 0.9; % avoid too large steps for i = 1:1e3 % run till convergence criterion is reached invar = imshift_fft_ax(invar_0, total_shift_Y, 1); weights = interp2(weights_0, Y',X'+total_shift_Y', 'nearest', 0); % apply high pass filter => get rid of phase artefacts invar = imfilter_high_pass_1d(invar,1,data_filter, Nlayers/2); % further suppress boundary effects invar = invar .* tukeywin(Nlayers, 0.2); % take median over all the positions m_invar = sum(invar .* weights,2) ./ (sum(weights,2)+1e-3); % get gradient by convoolution to avoid edge issues when using fft md_invar = math.get_img_grad_conv(m_invar,2,1); % in vertical direction use shift of the invariant => more robust and faster DY = m_invar-invar; shift_Y = -squeeze(sum(weights .* (DY .* md_invar) ,1) ./... sum(weights .* md_invar.^2,1)); % avoid too large steps where linear approximation is not valid anymore shift_Y = relax_step * min(0.5,abs(shift_Y)) .* sign(shift_Y); total_shift_Y = total_shift_Y + shift_Y'; err(i) = gather(mean2(weights .* DY.^2)); if i > 2 && err(i-1) < err(i) || max(abs(shift_Y)) < 1e-2 break % it will stop when alignment reaches the numerical precision end end total_shift_Y = gather(total_shift_Y); invar = gather(invar); end