Files
2026-08-07 15:56:42 +09:00

386 lines
17 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.
% AX_PARTIAL forward projector that allows to split the full volume into smaller pieces
% composed tomography projector based on ASTRA toolbox
% can be used either for data in RAM or on GPU (automatically decided from class of volData)
% * volume is split based on "split" parameter, 1 == no splitting
% * Ax_partial tries to split data if GPU limits are exceeded (ie texture memory limits)
%
% projData = Ax_partial(volData, cfg, vectors,split, varargin)
%
% Inputs:
% **volData - array Nx x Ny x Nz of projected volume
% **cfg - config structure generated by ASTRA_initialize
% **vectors - orientation of projections generated by ASTRA_initialize
% **split - 3 or 4 elements vector, [split X, split Y, split Z, split angle ]
% *optional*
% **deformation_fields - 3x1 cell contaning 3D arrays of local deformation of the object
% **GPU - GPU id to be used for reconstruction
% **verbose - verbose = 0 : (default) quiet, verbose = 1: standard info , verbose = 2: debug
% **keep_on_GPU - if true keep reconstructed volume on GPU to make is faster, default == false (the safe option)
%
% *returns*
% ++projData - projection of volData
%
% recompile commands
% (Linux, GCC 4.8.5) mexcuda -outdir private ASTRA_GPU_wrapper/ASTRA_GPU_wrapper.cu ASTRA_GPU_wrapper/util3d.cu ASTRA_GPU_wrapper/par3d_fp.cu ASTRA_GPU_wrapper/par3d_bp.cu
% (Windows) mexcuda -outdir private ASTRA_GPU_wrapper\ASTRA_GPU_wrapper.cu ASTRA_GPU_wrapper\util3d.cu ASTRA_GPU_wrapper\par3d_fp.cu ASTRA_GPU_wrapper\par3d_bp.cu
%*-----------------------------------------------------------------------*
%|                                                                       |
%|  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 projData = Ax_partial(volData, cfg, vectors,split, varargin)
import utils.*
import math.*
par = inputParser;
par.KeepUnmatched = true;
par.addOptional('deformation_fields', {}) % deformation_fields: 3x1 cell contaning 3D arrays of local deformation of the object
par.addOptional('GPU', []) % GPUs id to be used in reconstruction
par.addOptional('verbose', 0) % verbose = 0 : quiet, verbose : standard info , verbose = 2: debug
par.addOptional('keep_on_GPU', false) % true - keep reconstructed volume on GPU to make is faster
par.parse(varargin{:})
r = par.Results;
if isempty(r.deformation_fields); r.deformation_fields = {}; end
%% check the inputs + check memory availibility on GPU
assert(gpuDeviceCount>0, 'No CUDA enabled GPU availible')
if ~( (isa(volData, 'gpuArray') && strcmp(classUnderlying(volData), 'single')) || ...
isa(volData, 'single') ) || ~isreal(volData)
error('Only single precision real input array supported')
end
if ~isempty(r.deformation_fields)
assert(any(numel(r.deformation_fields) == [3,6]), 'Deformation field expected as 3x1 or 6x1 cell array')
for i = 1:numel( r.deformation_fields)
if ~( (isa(r.deformation_fields{i}, 'gpuArray') && strcmp(classUnderlying(r.deformation_fields{i}), 'single')) || ...
isa(r.deformation_fields{i}, 'single'))
error('Only single precision for deformation fields is supported')
end
r.deformation_fields{i} = gpuArray(r.deformation_fields{i}); % move on GPU, they are usually small
end
r.deformation_fields = r.deformation_fields' ; % transpose to that array(:) results in sorted field
end
gpu = gpuDevice();
if ~isempty(r.GPU) && gpu.Index ~= r.GPU(1)
% switch and !! reset !! GPU
if isa(volData, 'gpuArray'), error('Switching GPUs will reset content'); end
gpu = gpuDevice(r.GPU(1));
end
split = ceil(max(1,split));
if ismatrix(volData)
split = [split([1, min(2,end)]),1];
assert(all(size(volData)==[cfg.iVolX,cfg.iVolY]), 'Wrong inputs size')
else
assert(all(size(volData)==[cfg.iVolX,cfg.iVolY,cfg.iVolZ]), 'Wrong inputs size')
end
keep_on_GPU = isa(volData, 'gpuArray') || r.keep_on_GPU;
%% backprojector that allows to split the full volume into smaller pieces
assert(all(size(vectors,2)==12), 'Wrong "vectors" size')
assert(~isempty(vectors), 'Empty input "vectors"')
assert(cfg.iVolX*cfg.iVolY*cfg.iVolZ > 0, 'Inputs volume is empty');
assert(cfg.iProjU*cfg.iProjV*cfg.iProjAngles > 0, 'Projections are empty');
% be sure that ASTRA wrapper is feeded by doubles !!
for i = fieldnames(cfg)'
cfg.(i{1}) = double(cfg.(i{1}));
end
vectors = double(vectors);
split=double(split);
cfg.iProjAngles = size(vectors,1);
assert(cfg.iProjAngles > 1, 'Number of processed angles must be > 1')
% fix if the number of projections is > 1024, or arrays are too large
Nproj_groups = max(1,ceil(max([cfg.iProjAngles/1024, ... % ASTRA constant memory limit
cfg.iProjU*cfg.iProjV*cfg.iProjAngles*4 / gpu.AvailableMemory, ... % availible memory limit
cfg.iProjU * cfg.iProjV * cfg.iProjAngles / double(intmax('int32'))]))); % maximal array size allowed by CUDA limit
if length(split) > 3
Nproj_groups = max(split(4), Nproj_groups);
end
if all(split == 1) && Nproj_groups == 1
if numel(volData)*4 > 1024e6 % exceeded texture memory
nsubVol = ceil(numel(volData)*4 / 1024e6);
nsubVol = 2^nextpow2(nsubVol);
% split = ceil([sqrt(nsubVol),sqrt(nsubVol),1]);
split = [1,1,nsubVol];
if r.verbose>0; disp(['Volume array is larger than 1024MB, auto-splitting ', num2str(split)]); end
else
%% in the simple case call ASTRA_GPU_wrapper directly
volData = gpuArray(volData);
% call ASTRA
projData = astra.ASTRA_GPU_wrapper('fp',volData, cfg, vectors,[],r.deformation_fields{:});
clear volData
if gpu.AvailableMemory < 4*numel(projData)
projData = gather(projData); % prevent out of memory errors during next step
end
projData = astra2matlab(projData);
if ~keep_on_GPU; projData = gather(projData); end
return
end
end
%% otherwise prepare data for split and call ASTRA_GPU_wrapper on subvolumes
if isscalar(split)
split = split .* ones(ndims(volData),1);
end
assert(numel(volData)*4/prod(split) <= 1024e6, 'Volume array exceeded 1024MB, use more splitting')
Nvol_full = size(volData);
if numel(Nvol_full)<3
Nvol_full(3)=1;
end
Nvol_sub = Nvol_full'./reshape(split(1:3),[],1);
assert(all(mod(Nvol_sub,1)==0), sprintf('Volume size %ix%ix%i is not dividable by split %ix%ix%i',size(volData),split(1:3)))
if ismatrix(volData)
split(3) = 1;
Nvol_sub(3) = 1;
end
cfg.iVolX = cfg.iVolX/split(1);
cfg.iVolY = cfg.iVolY/split(2);
cfg.iVolZ = cfg.iVolZ/split(3);
cfg_orig = cfg;
if Nproj_groups > 1
% split the projections along the angles
for i = 1:Nproj_groups
ind = (1+(i-1)*ceil(cfg.iProjAngles/Nproj_groups)):i*ceil(cfg.iProjAngles/Nproj_groups);
ind = ind(ind <= cfg.iProjAngles);
vectors_tmp{i} = vectors(ind,:);
cfg_tmp{i} = cfg;
cfg_tmp{i}.iProjAngles = length(ind);
end
cfg = cfg_tmp; vectors = vectors_tmp;
else
Nproj_groups = 1;
cfg = {cfg};
vectors = {vectors};
end
clear ind
if r.verbose > 1
fprintf('Size of the full volume: %i %i %i\n', size(volData))
fprintf('Size of the subvolume: %i %i %i\n', Nvol_sub)
fprintf('Size of the one sinogram block: %i %i %i\n', cfg{1}.iProjU, cfg{1}.iProjV, cfg{1}.iProjAngles)
end
%%!!!! note that in rare cases astra my fail if sinogram width is too small
assert(prod(Nvol_sub) * 4 <= 1024e6, 'Volume exceeded maximal size of texture 1024MB')
% estimate required memory + (use only if 2x more memory is available)
required_mem = 2*(prod(Nvol_sub)*(2+~isa(volData, 'gpuArray')) + cfg_orig.iProjU * cfg_orig.iProjV * cfg_orig.iProjAngles )*4;
% keep projections on GPU only of there is enough memory
keep_projections_on_GPU = Nproj_groups == 1 || gpu.AvailableMemory > required_mem;
if cfg{1}.iProjU * cfg{1}.iProjV * cfg{1}.iProjAngles > intmax('int32')
error('Projection size exceeded maximum size allowed on GPU')
end
if (keep_on_GPU || prod(split(1:3)) == 1) && numel(volData) < intmax('int32') % keep volume on GPU
volData = gpuArray(volData);
end
inParpool = ~isempty(getCurrentTask());
iter = 1;
for m = 1:Nproj_groups
% split angularly (solve smaller groups of angles)
% allocate memory for the projections
projData{m} = gpuArray.zeros(cfg{m}.iProjU, cfg{m}.iProjV, cfg{m}.iProjAngles, 'single');
% split into volume cubes
for i = 1:split(1)
for j = 1:split(2)
for k = 1:split(3)
if r.verbose > 0
progressbar(iter, prod(split(1:3))*Nproj_groups+1, 20);
end
pos = [i,j,k];
for n = 1:3
ind{n} = (1+(pos(n)-1)*Nvol_sub(n)):(pos(n)*Nvol_sub(n));
%% find optimal shift of the subvolume
if mod(split(n),2)==1 %% odd
shift(n) = (pos(n) - ceil(split(n)/2))*Nvol_sub(n);
else
shift(n) = (pos(n) - ceil(split(n)/2)-1/2)*Nvol_sub(n);
end
end
% extract subvolume to be processed
if any(split(1:3)~=1) && isa(volData, 'gpuArray')
vol_small = volData(ind{:}); % take only small subvolume
elseif any(split(1:3)~=1)
vol_small = zeros(Nvol_sub','single');
utils.get_from_3D_projection(vol_small,volData,[ind{1}(1),ind{2}(1)]-1,ind{3});
else
vol_small = volData; % avoid data copying of possible
end
vol_small = gpuArray(vol_small);
% split deformation field for nonrigid tomography
if ~isempty(r.deformation_fields)
for ii = 1:3
N_deform = size(r.deformation_fields{ii}) ./ reshape(split(1:3),[],1)';
for jj = 1:3
ind_def{jj} = linspace(1+(pos(jj)-1)*N_deform(jj), pos(jj)*N_deform(jj), size(r.deformation_fields{ii},jj));
end
[X,Y,Z]= meshgrid(ind_def{:});
deformation_fields_sub{ii} = interp3(r.deformation_fields{ii},X,Y,Z);
end
else
deformation_fields_sub = {};
end
vec = vectors{m};
vec(:,4:6) = bsxfun(@minus, vec(:,4:6), shift);
req_mem = 2*numel(vol_small)*4 ;
if gpu.AvailableMemory < req_mem
!nvidia-smi
whos
error('Too low GPU memory, avail: %3.2gGB / req: %3.2gGB, GPU %i/%i, projection group %i/%i, keep_proj_on_GPU=%i', gpu.AvailableMemory/1e9,req_mem/1e9, gpu.Index, gpuDeviceCount, m , Nproj_groups, keep_projections_on_GPU)
end
try
% avoid memory allocation, write directly to projData{m} -> no output arguments are needed
astra.ASTRA_GPU_wrapper('fp',vol_small, cfg{m}, vec,projData{m}, deformation_fields_sub{:});
vol_small = []; % soft mem clean
catch err
if strcmpi(err.identifier,'parallel:gpu:array:OOM')
warning('Out of memory on GPU %i, try reset GPU or split the array onto smaller blocks', gpu.Index)
gpuDevice
reset(gpuDevice)
end
rethrow(err)
end
iter = iter+1;
if r.verbose>0
progressbar(iter, prod(split(1:3))*Nproj_groups+1, 20);
end
end
end
end
if ~keep_projections_on_GPU
projData{m} = gather(projData{m});
end
end
clear volData vol_small
% permute / concatenate
if gpu.AvailableMemory < 4*numel(projData{1})*max(2,Nproj_groups)
projData = gather_all(projData);
end
projData = astra2matlab(projData);
if gpu.AvailableMemory < 8*numel(projData{1})*Nproj_groups
projData = gather_all(projData);
end
% concatenate the projected data align the angular (3rd) axis
projData = merge_projections(projData);
if ~keep_on_GPU
projData = gather(projData);
elseif numel(projData) < intmax('int32') && gpu.AvailableMemory < 4*numel(projData)
% return to GPU if requested by 'keep_on_GPU' parameter
projData = gpuArray(projData);
end
end
function x = gather_all(x)
for i = 1:length(x)
x{i} = gather(x{i});
end
end
function projData = merge_projections(projData_blocks)
Nblocks = length(projData_blocks);
if Nblocks > 1
if isa(projData_blocks{1}, 'gpuArray')
projData = cat(3, projData_blocks{:});
else
% faster and more memory efficient version
proj_size = [size(projData_blocks{1},1),size(projData_blocks{1},2),sum(cellfun(@(x)size(x,3), projData_blocks))];
for ii = 1:10
try
projData = zeros(proj_size, 'single');
break
catch err
end
pause(1)
end
if ii == 10
warning('Unsufficient memory to allocate %3.2gGB RAM', prod(proj_size)*4/1e9)
utils.check_available_memory
rethrow(err)
end
offset = 0;
for ii = 1:Nblocks
tomo.set_to_array(projData, projData_blocks{ii}, offset);
offset = offset + size(projData_blocks{ii},3);
end
end
else
projData = projData_blocks{1};
end
end