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

453 lines
18 KiB
Matlab
Raw 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.
% LOAD_PROJECTIONS_FAST load reconstructed projections from disk to RAM
%
% [stack_object, theta,num_proj, par] = load_projections_fast(par, exclude_scans, dims_ob, theta, custom_preprocess_fun)
%
% Inputs:
% **par - parameter structure
% **exclude_scans - list of scans to be excluded from loading, [] = none
% **dims_ob - dimension of the object
% **theta - angles of the scans
% **custom_preprocess_fun - function to be applied on the loaded data, eg cropping , rotation, etc
%
% *returns*
% ++stack_object - loaded complex-valued projections
% ++theta - angles corresponding to the loaded projections, angles for missing projections are removed
% ++num_proj - number of projections
% ++par - updated parameter structure
%*-----------------------------------------------------------------------*
%|                                                                       |
%|  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 [stack_object, theta,num_proj, par] = load_projections_fast(par, exclude_scans, dims_ob, theta, custom_preprocess_fun)
import ptycho.*
import utils.*
import io.*
import plotting.imagesc3D
utils.verbose(struct('prefix', 'loading'))
if nargin < 5
custom_preprocess_fun = [];
end
if ~isempty(custom_preprocess_fun) && ishandle(custom_preprocess_fun) && ~strcmpi(func2str(custom_preprocess_fun), '@(x)x')
custom_preprocess_fun = [] ;
end
scanstomo = par.scanstomo;
% avoid loading scans listed in 'exclude_scans'
if ~isempty(exclude_scans)
ind = ismember(scanstomo, exclude_scans);
%% clear values corresponding to excluded scans
scanstomo(ind) = [];
theta(ind) = [];
par.subtomos(ind) = [];
end
% % plot average vibrations for each of the loaded projections
% utils.verbose(0,'Checking stability of the projections')
% poor_projections = prepare.plot_sample_stability(par, scanstomo, ~par.online_tomo, par.pixel_size);
% if sum(poor_projections) && ...
% (par.online_tomo || ~strcmpi(input(sprintf('Remove %i low stability projections: [Y/n]\n',sum(poor_projections)), 's'), 'n') )
% theta(poor_projections) = [];
% scanstomo(poor_projections) = [];
% else
% utils.verbose(0,'All projections are fine')
% end
verbose(0,'Checking available files')
verbose(0); % make it quiet
missing_scans = [];
proj_file_names = cell(length(scanstomo),1);
for num = 1:length(scanstomo)
progressbar(num, length(scanstomo))
filename = find_projection_files_names(par, scanstomo(num));
if isempty(filename)
missing_scans(end+1) = scanstomo(num);
continue
end
proj_file_names{num} = filename;
end
verbose(par.verbose_level); % return to original settings
if ~isempty(missing_scans)
plotting.smart_figure(1)
subplot(2,1,1)
hold on
plot(missing_scans, theta(ismember(scanstomo, missing_scans)), 'rx', 'Linewidth', 2)
hold off
legend({'Measured angles', 'Missing scans'})
axis tight
drawnow
ind = ismember(scanstomo, missing_scans);
verbose(1,['Scans not found are ' num2str(missing_scans)])
verbose(1,['Projections not found are ' num2str(find(ind))])
%% clear values corresponding to measured but missing scans (not reconstructed)
scanstomo(ind) = [];
theta(ind) = [];
proj_file_names(ind) = [];
par.subtomos(ind) = [];
else
verbose(1,'All projections found')
end
num_proj = length(scanstomo);
if isfield(par, 'fp16_precision') && par.fp16_precision
dtype = uint16(1i); % use uint32 to store half floar precision data
else
dtype= single(1i);
end
downsample = 2^par.downsample_projections; % calculate downsample factor for binning , default par.downsample_projections = 0;
pixel_size =zeros(num_proj,2);
energy = zeros(num_proj,1);
stack_object=zeros(ceil(dims_ob(1) / downsample),ceil(dims_ob(2) / downsample),num_proj, 'like', dtype);
residua = zeros(num_proj,1);
%disp(size(stack_object))
tic
% load at least 10 frames per worker to use well the resources
block_size = max(1, feature('numcores'))*4;
object_ROI = {ceil(1+par.asize(1)/2/downsample):ceil((dims_ob(1)-par.asize(1)/2)/downsample),ceil(1+par.asize(2)/2/downsample):ceil((dims_ob(2)-par.asize(2)/2)/downsample)};
verbose(1,'Loading projections ...')
missing_all = [];
t0 = tic;
%% load data, use parfor but process blockwise to avoid large memory use and also allow user stopping during MEX reading
for block_id = 1:ceil(num_proj/block_size)
block_inds = 1+(block_id-1)*block_size: min(num_proj, block_id*block_size);
utils.progressbar(block_id, ceil(num_proj/block_size))
if strcmpi(par.file_extension, 'h5') && ~verLessThan('matlab', '9.4') && ...
(isfield(par, 'use_mex_loader') && par.use_mex_loader ) % only matlab newer than R2018a is supported
object_block = [];
try
% fast MEX loader, sometimes it tends to fail and needs to
% be run again to load the data corectly
[object_block,missing_tmp] = mex_read(par.dims_ob_loaded, proj_file_names(block_inds), par.Nthreads_mexread);
catch Err
disp('Error in loading using MEX, falling back to matlab reader, try to reduce par.Nthreads_mexread is this warning repeats often')
disp(Err)
end
% if loading was not succeful ..
if isempty(object_block)
[object_block, missing_tmp] = matlab_read(par.dims_ob_loaded, proj_file_names(block_inds));
end
else
% loading using matlab for original MAT file data or old matlab
[object_block, missing_tmp] = matlab_read(par.dims_ob_loaded, proj_file_names(block_inds));
end
missing_all = [missing_all, block_inds(missing_tmp)];
% read additional information
for jj = setdiff(block_inds, block_inds(missing_tmp)) % remove missing projection from loading
if strcmpi(par.file_extension, 'h5')
try
pixel_size(jj,:) = h5read(proj_file_names{jj}, '/reconstruction/p/dx_spec');
energy(jj) = h5read(proj_file_names{jj}, '/reconstruction/p/energy');
catch err
disp(err)
keyboard
end
else
% load it from the matlab file is not supported (it is too slow)
pixel_size(jj,:) = par.pixel_size;
energy(jj) = nan;
end
end
%% apply custom data proprocessing and caculate basic statistics, DO IT ON GPU
[object_block, residua(block_inds,1), projection_value(block_inds)] = ...
tomo.block_fun(@process_projection_block, object_block, custom_preprocess_fun, par, object_ROI,pixel_size(block_inds,:), struct('verbose_level', 0));
% convert data to fp16 precision if requested
if isfield(par, 'fp16_precision') && par.fp16_precision
object_block = fp16.set(object_block);
end
stack_object(:,:,block_inds) = object_block;
end
pixel_size = min(pixel_size,[],2); % projection were already rescaled to provide the same pixel size in each dimension
verbose(1, 'Data loaded in %is', round(toc(t0)))
% downsample the illum_sum if requested
if downsample > 0
par.illum_sum = utils.binning_2D(crop_pad(par.illum_sum, ceil(dims_ob/downsample)*downsample) , downsample);
par.asize = ceil(par.asize / downsample);
end
failed_projections = projection_value < 0.1*median(projection_value) | ismember(1:num_proj, missing_all) | ~isfinite(projection_value);
if any(failed_projections )
verbose(0,['Projections failed are ' num2str(find(failed_projections))])
verbose(0,['Scans failed are ' num2str(scanstomo(failed_projections))])
else
verbose(0,'All loaded projections seems OK')
end
[Nprojections] = size(stack_object,3);
poor_projections = false;
if ~par.is_laminography
% laminography has a more complex definition of field of view ->
% currently not implemented
poor_projections = (residua(:)' > par.max_residua_limit) ; % ignore in the case of laminography
verbose(1, 'Found %i/%i projections with more than %i residues ', sum(poor_projections), Nprojections, par.max_residua_limit)
if any(poor_projections)
verbose(1,['Projections with residua are ' num2str(find(poor_projections))])
verbose(1,['Scans with residua are ' num2str(scanstomo(poor_projections))])
end
end
verbose(1, 'Find residua done')
% avoid also empty projections
which_remove = poor_projections | failed_projections;
%%% Getting rid of missing projections %%%
if any(which_remove)
[Nx,Ny,~] = size(stack_object);
title_extra = {};
for ii = 1:num_proj
if which_remove(ii)
title_extra{end+1} = sprintf(' N residua: %i',residua(ii));
end
end
verbose(1,' %i failed projections shown in figure(1) \n', sum(which_remove))
tomo.show_projections(stack_object(:,:,which_remove), theta(which_remove), par, 'fnct', @angle, ...
'title', 'Projection to be removed','plot_residua', true, 'title_extra', title_extra, ...
'rectangle_pos', [par.asize(2)/2,Ny-par.asize(2)/2, par.asize(1)/2,Nx-par.asize(1)/2], 'figure_id', 1)
if par.online_tomo || debug() || ~strcmpi(input(sprintf('Do you want remove %i failed/wrong projections and keep going (Y/n)?',sum(which_remove)),'s'),'n')
verbose(0,'Removing failed/wrong projections. stack_object, scanstomo, theta and num_proj are modified')
stack_object(:,:,which_remove) = [];
scanstomo(which_remove)=[];
theta(which_remove)=[];
pixel_size(which_remove,:) = [];
energy(which_remove,:) = [];
residua(which_remove)=[];
par.subtomos(which_remove) = [];
verbose(0,'Done')
else
verbose(0,'Keeping empty spaces for failed projections. Problems are expected if you continue.')
end
end
par.scanstomo = scanstomo;
par.num_proj=numel(scanstomo);
% store number of residua for later processing
par.nresidua_per_frame = residua(:)';
pixel_scale = pixel_size ./ min(pixel_size(:)); % just in case that the axis do not have indetical pixel size, NOT TESTED YET
assert(par.num_proj > 0, 'No projections loaded')
if all(all(abs(pixel_scale)-1 < 1e-6)) || ~any(isfinite(mean(pixel_scale)))
%if all datasets have the same pixel scale
pixel_scale = [1,1];
else
warning('Datasets do not have equal pixel sizes, auto-rescaling projections')
% use FFT base rescaling -> apply illumination function first to remove
% effect of the noise out of the reconstruction region
rot_fun = @(x,sx,sy)(utils.imrescale_frft(x .* par.illum_sum, sx, sy)) ./ ( max(0,utils.imrescale_frft(par.illum_sum,sx,sy))+1e-2*max(par.illum_sum(:)));
stack_object = tomo.block_fun(rot_fun,stack_object, pixel_scale(:,1),pixel_scale(:,2));
pixel_scale = [1,1];
end
par.pixel_scale = pixel_scale;
par.energy = energy;
if size(stack_object,3) ~= length(theta) || length(theta) ~= par.num_proj
error('Inconsistency between number of angles and projections')
end
utils.verbose(struct('prefix', 'template'))
end
function [object_block, residua, projection_value] = process_projection_block(object_block, custom_preprocess_fun, par, object_ROI, pixel_size)
% auxiliary function used to apply various preprocessing steps, ie custom_preprocess_fun, binning, clipping and residua calculation on the
% object_block on GPU -> avoid CPU-GPU transfer overhead
% returns:
% object_block - processed complex valued projections
% residua - number of residua in each frame
% projection_value - average amplitude of the projection
% pixel_size in each dimension
% apply additional processing, e.g. rotation
if ~isempty(custom_preprocess_fun)
object_block = custom_preprocess_fun(object_block);
end
if any(pixel_size(:,1) ~= pixel_size(:,2))
% in the case of asymmetric pixel size,
% upsample the data in the dimennsion with lower resolution (-> at least relax issues in tomography interpolation)
pixel_scale = pixel_size ./ min(pixel_size,[],2) ;
assert(all(std(pixel_scale) < 1e-3), 'Variable resolution between projection and asymmetric pixel size is not implemented')
Npix = size(object_block);
dims_ob_new = round(Npix(1:2) .* pixel_scale(1,:));
object_block = utils.interpolateFT(object_block, dims_ob_new);
end
Npix = size(object_block);
downsample = 2^par.downsample_projections;
% downsample the data if requested
if downsample > 1
object_block = utils.binning_2D(utils.crop_pad(object_block, ceil(Npix/downsample)*downsample) , downsample);
end
%% clip the projections amplitude by quantile filter
if par.clip_amplitude_quantile > 0 && par.clip_amplitude_quantile < 1
MAX = quantile(reshape(abs(object_block(1:10:end,1:10:end,:)), [], Npix(3)), par.clip_amplitude_quantile ,1);
MAX = reshape(MAX,1,1,[]);
clip_fun = @(x,M)(min(abs(x),M) .* x ./ (abs(x) + 1e-5));
object_block = clip_fun(object_block, MAX);
end
residua = squeeze(math.sum2(abs(utils.findresidues(object_block(object_ROI{:},:)))>0.1));
projection_value = squeeze(math.sum2(abs(object_block)));
end
function [object_block, missing] = matlab_read(dims_ob, proj_file_names)
% projection loading using matlab
% Inputs:
% dims_ob - projection size
% proj_file_names - cell of filenames to be loaded
% Outputs:
% object_block - loaded projection
% missing - list of missing (failed) projections
object_block = zeros([dims_ob,length(proj_file_names)], 'like', single(1i));
loaded = false(length(proj_file_names),1);
for jj = 1:length(proj_file_names)
utils.verbose(2,['Reading file: ' proj_file_names{jj}])
try
object = io.load_ptycho_recons(proj_file_names{jj}, 'object');
object = single(object.object);
object = prod(object,4); % use only the eDOF object if multiple layers are available
object_block(:,:,jj) = utils.crop_pad(object, dims_ob);
loaded(jj) = true;
catch
utils.verbose(-1,'Loading of file %s failed', proj_file_names{jj})
end
end
missing = find(~loaded);
end
function [object_block, missing] = mex_read(dims_ob, proj_file_names, Nthreads)
% fast projection loader by MEX with paralelization
% Inputs:
% dims_ob - projection size
% proj_file_names - cell of filenames to be loaded
% Nthreads - number of threads used to load the projections in parallel
% Outputs:
% object_block - loaded projection
% missing - list of missing (failed) projections
Nthreads = min(length(proj_file_names),Nthreads );
proj_file_names = reshape(proj_file_names, 1,[]);
% load complex-valued projections using parallel MEX
try
[object_block, missing] = io.ptycho_read(Nthreads, 'single', dims_ob, '/reconstruction/object', proj_file_names);
catch err
if strcmpi(err.identifier, 'ptycho:read:failed')
Nthreads = 5;
[object_block, missing] = io.ptycho_read(Nthreads, 'single', dims_ob, '/reconstruction/object', proj_file_names);
warning off backtrace
warning('===================================================================================================================================')
warning('Loading of projections failed due to too high multithreading, if this warning repeats, consider lowering par.Nthreads_mexread value')
warning('===================================================================================================================================')
warning on backtrace
else
rethrow(err)
end
end
assert(ndims(object_block) <= 5, 'Unexpected dimensionality of inputs')
object_block = permute(object_block, [2,1,3,4,5]); % transpose loaded reconstructions
object_block = prod(object_block,4) ; % get one eDoF frame if ML reconstruction is used
object_block = squeeze(object_block); % get rid of extra dimensions
assert(ndims(object_block) == 3, 'Unexpected dimensionality of inputs')
end