├── DWT.m ├── HWT.m ├── README.md ├── cconvn.m ├── dhe.m ├── espirit2.m ├── espirit3.m ├── fft3.m ├── fistaN.m ├── grappa2.m ├── grappa3.m ├── head.mat ├── homodyne.m ├── ifft3.m ├── matched_filter.m ├── meas_MID00382_FID42164_clean_fl2_m400.mat ├── optimal_shrinkage.m ├── partial_echo.mat ├── pcgL1.m ├── pcgpc.m ├── phantom.mat ├── phantom3D_6coil.mat ├── sake2.m └── sake3.m /DWT.m: -------------------------------------------------------------------------------- 1 | classdef DWT 2 | % Q = DWT(sizeINI) 3 | % 4 | % Daubechies wavelet transform (db2) 5 | % 6 | % notes: 7 | % -always assumes periodic boundary conditions 8 | % -relaxed about input shape (vectorized ok) 9 | % -accepts leading or trailing coil dimensions 10 | % -Q.thresh(x,sparsity) does soft thresholding 11 | % to a given sparsity (e.g. 0.25 => 25% zeros) 12 | % 13 | % Example: 14 | %{ 15 | x = (1:8) + 0.1*randn(1,8); 16 | Q = DWT(size(x)); 17 | y = Q * x; % forward 18 | z = Q'* y; % inverse 19 | norm(x-z,Inf) 20 | ans = 2.3801e-12 21 | z = Q.thresh(x,0.25); % 25% zeros 22 | norm(x-z,Inf) 23 | ans = 0.2362 24 | [x;z] 25 | ans = 26 | 0.9468 1.9106 3.1145 4.0127 4.7686 6.0869 6.9622 8.1000 27 | 0.8835 1.9429 2.9707 3.8405 4.7527 5.8872 6.9622 7.8637 28 | Q*[x;z] 29 | ans = 30 | 4.7292 3.8104 6.3904 10.4568 -1.1663 0.1082 0.1412 3.9703 31 | 4.5880 3.6692 6.2492 10.3156 -1.0251 0 0 3.8291 32 | %} 33 | properties (SetAccess = private) 34 | sizeINI 35 | trans = false 36 | end 37 | 38 | methods 39 | 40 | %% constructor 41 | function obj = DWT(sizeINI) 42 | 43 | if ~isnumeric(sizeINI) || ~isvector(sizeINI) 44 | error('sizeINI must be the output of size().'); 45 | end 46 | while numel(sizeINI)>2 && sizeINI(end)==1 47 | sizeINI(end) = []; % remove trailing ones 48 | end 49 | if numel(sizeINI)==1 50 | sizeINI(end+1) = 1; % append a trailing one 51 | end 52 | if any(mod(sizeINI,2) & sizeINI~=1) 53 | error('only even dimensions supported.'); 54 | end 55 | obj.sizeINI = reshape(sizeINI,1,[]); 56 | 57 | end 58 | 59 | %% y = Q*x or y = Q'*x 60 | function y = mtimes(obj,x) 61 | 62 | % loop over extra dimensions (coils) 63 | [nc dim sx] = get_coils(obj,x); 64 | 65 | if nc>1 66 | 67 | % expand coil dimension 68 | y = reshape(x,sx); 69 | 70 | % indices for each dimension 71 | ix = repmat({':'},numel(sx),1); 72 | 73 | % transform coils separately 74 | for c = 1:nc 75 | ix{dim} = c; 76 | y(ix{:}) = obj * y(ix{:}); 77 | end 78 | 79 | % original shape 80 | y = reshape(y,size(x)); 81 | 82 | else 83 | 84 | % correct shape 85 | y = reshape(x,obj.sizeINI); 86 | 87 | % convolutions 88 | LoD = [ 1-sqrt(3); 3-sqrt(3); 3+sqrt(3); 1+sqrt(3)] / sqrt(32); 89 | HiD = [-1-sqrt(3); 3+sqrt(3);-3+sqrt(3); 1-sqrt(3)] / sqrt(32); 90 | LoR = [ 1+sqrt(3); 3+sqrt(3); 3-sqrt(3); 1-sqrt(3)] / sqrt(32); 91 | HiR = [ 1-sqrt(3);-3+sqrt(3); 3+sqrt(3);-1-sqrt(3)] / sqrt(32); 92 | 93 | LoD = cast(LoD,'like',y); HiD = cast(HiD,'like',y); 94 | LoR = cast(LoR,'like',y); HiR = cast(HiR,'like',y); 95 | 96 | if obj.trans==0 97 | 98 | % forward transform 99 | for d = 1:numel(obj.sizeINI) 100 | if obj.sizeINI(d) > 1 101 | 102 | ylo = cconvn(y,LoD); 103 | yhi = cconvn(y,HiD); 104 | 105 | ix = repmat({':'},numel(obj.sizeINI),1); 106 | ix{d} = 1:2:obj.sizeINI(d); % odd indices 107 | 108 | ylo = ylo(ix{:}); 109 | yhi = yhi(ix{:}); 110 | 111 | y = cat(d,ylo,yhi); 112 | 113 | end 114 | LoD = reshape(LoD,[1 size(LoD)]); 115 | HiD = reshape(HiD,[1 size(HiD)]); 116 | end 117 | 118 | else 119 | 120 | % inverse transform 121 | for d = 1:numel(obj.sizeINI) 122 | if obj.sizeINI(d) > 1 123 | 124 | ix = repmat({':'},numel(obj.sizeINI),1); 125 | lo = ix; lo{d} = 1:obj.sizeINI(d)/2; 126 | hi = ix; hi{d} = 1+obj.sizeINI(d)/2:obj.sizeINI(d); 127 | 128 | ylo = y(lo{:}); 129 | yhi = y(hi{:}); 130 | 131 | ix = repmat({':'},numel(obj.sizeINI),1); 132 | ix{d} = 2:2:obj.sizeINI(d); % even indices 133 | 134 | tmp = zeros(size(y),'like',y); 135 | tmp(ix{:}) = ylo; ylo = tmp; 136 | tmp(ix{:}) = yhi; yhi = tmp; 137 | 138 | y = cconvn(ylo,LoR) + cconvn(yhi,HiR); 139 | end 140 | LoR = reshape(LoR,[1 size(LoR)]); 141 | HiR = reshape(HiR,[1 size(HiR)]); 142 | end 143 | 144 | end 145 | 146 | % original shape 147 | y = reshape(y,size(x)); 148 | 149 | end 150 | 151 | %% get number of coils 152 | function [nc dim sx] = get_coils(obj,x) 153 | 154 | sx = size(x); 155 | 156 | % number of coils 157 | nc = prod(sx) / prod(obj.sizeINI); 158 | 159 | if mod(nc,1) 160 | error('Expansion not compatible with sizeINI=[%s].',num2str(obj.sizeINI,'%i ')); 161 | end 162 | 163 | % get coil dimension 164 | dim = 0; 165 | for d = 1:numel(sx) 166 | if sx(d)~=dimsize(obj,d) 167 | dim = d; 168 | break; 169 | end 170 | end 171 | 172 | % expand array dimension, e.g. [2n] => [n 2] 173 | if dim>1 || iscolumn(x) 174 | sx = [obj.sizeINI nc]; 175 | dim = numel(sx); 176 | end 177 | 178 | end 179 | 180 | end 181 | 182 | %% threshold wavelet coefficients 183 | function [y lambda] = thresh(obj,x,sparsity) 184 | 185 | if nargin<3 186 | error('Not enough input arguments.'); 187 | end 188 | if ~isscalar(sparsity) || ~isreal(sparsity) || sparsity<0 || sparsity>1 189 | error('sparsity must be a scalar between 0 and 1.') 190 | end 191 | 192 | % to wavelet domain 193 | y = obj * x; 194 | 195 | % soft threshold coils separately 196 | y = reshape(y,prod(obj.sizeINI),[]); 197 | 198 | absy = abs(y); 199 | signy = sign(y); 200 | 201 | v = sort(absy,'ascend'); 202 | index = round(prod(obj.sizeINI) * sparsity); 203 | 204 | if index==0 205 | lambda = cast(0,'like',v); 206 | else 207 | lambda = v(index,:); 208 | y = signy .* max(absy-lambda,0); 209 | end 210 | 211 | % to image domain 212 | y = obj' * reshape(y,size(x)); 213 | 214 | end 215 | 216 | %% detect Q' and set flag 217 | function obj = ctranspose(obj) 218 | 219 | obj.trans = ~obj.trans; 220 | 221 | end 222 | 223 | %% dimension size 224 | function n = dimsize(obj,dim) 225 | 226 | if nargin<2 227 | n = obj.sizeINI; 228 | elseif ~isscalar(dim) || ~isnumeric(dim) || ~isreal(dim) || dim<1 || mod(dim,1)~=0 229 | error('Dimension argument must be a positive integer scalar within indexing range.'); 230 | elseif dim <= numel(obj.sizeINI) 231 | n = obj.sizeINI(dim); 232 | else 233 | n = 1; 234 | end 235 | 236 | end 237 | 238 | end 239 | 240 | end 241 | 242 | -------------------------------------------------------------------------------- /HWT.m: -------------------------------------------------------------------------------- 1 | classdef HWT 2 | % Q = HWT(sizeINI) 3 | % 4 | % Haar wavelet transform 5 | % 6 | % notes: 7 | % -always assumes periodic boundary conditions 8 | % -relaxed about input shape (vectorized ok) 9 | % -accepts leading or trailing coil dimensions 10 | % -Q.thresh(x,sparsity) does soft thresholding 11 | % to a given sparsity (e.g. 0.25 => 25% zeros) 12 | % 13 | % Example: 14 | %{ 15 | x = (1:8) + 0.1*randn(1,8); 16 | Q = HWT(size(x)); 17 | y = Q * x; % forward 18 | z = Q'* y; % inverse 19 | norm(x-z,Inf) 20 | ans = 2.3801e-12 21 | z = Q.thresh(x,0.25); % 25% zeros 22 | norm(x-z,Inf) 23 | ans = 0.2362 24 | [x;z] 25 | ans = 26 | 0.9468 1.9106 3.1145 4.0127 4.7686 6.0869 6.9622 8.1000 27 | 0.8835 1.9429 2.9707 3.8405 4.7527 5.8872 6.9622 7.8637 28 | Q*[x;z] 29 | ans = 30 | 4.7292 3.8104 6.3904 10.4568 -1.1663 0.1082 0.1412 3.9703 31 | 4.5880 3.6692 6.2492 10.3156 -1.0251 0 0 3.8291 32 | %} 33 | properties (SetAccess = private) 34 | sizeINI 35 | trans = false 36 | end 37 | 38 | methods 39 | 40 | %% constructor 41 | function obj = HWT(sizeINI) 42 | 43 | if ~isnumeric(sizeINI) || ~isvector(sizeINI) 44 | error('sizeINI must be the output of size().'); 45 | end 46 | while numel(sizeINI)>2 && sizeINI(end)==1 47 | sizeINI(end) = []; % remove trailing ones 48 | end 49 | if any(mod(sizeINI,2) & sizeINI~=1) 50 | error('only even dimensions supported.'); 51 | end 52 | obj.sizeINI = reshape(sizeINI,1,[]); 53 | 54 | end 55 | 56 | %% y = Q*x or y = Q'*x 57 | function y = mtimes(obj,x) 58 | 59 | % loop over extra dimensions (coils) 60 | [nc dim sx] = get_coils(obj,x); 61 | 62 | if nc>1 63 | 64 | % expand coil dimension 65 | y = reshape(x,sx); 66 | 67 | % indices for each dimension 68 | ix = repmat({':'},numel(sx),1); 69 | 70 | % transform coils separately 71 | for c = 1:nc 72 | ix{dim} = c; 73 | y(ix{:}) = obj * y(ix{:}); 74 | end 75 | 76 | % original shape 77 | y = reshape(y,size(x)); 78 | 79 | else 80 | 81 | % correct shape 82 | y = reshape(x,obj.sizeINI); 83 | 84 | if obj.trans==0 85 | 86 | % forward transform 87 | for d = 1:numel(obj.sizeINI) 88 | if obj.sizeINI(d) > 1 89 | 90 | ix = repmat({':'},numel(obj.sizeINI),1); 91 | odd = ix; odd{d} = 1:2:obj.sizeINI(d); 92 | even = ix; even{d} = 2:2:obj.sizeINI(d); 93 | 94 | yodd = y(odd{:}); 95 | yeven = y(even{:}); 96 | 97 | y = cat(d,yodd+yeven,yodd-yeven) / sqrt(2); 98 | 99 | end 100 | end 101 | 102 | else 103 | 104 | % inverse transform 105 | for d = 1:numel(obj.sizeINI) 106 | if obj.sizeINI(d) > 1 107 | 108 | ix = repmat({':'},numel(obj.sizeINI),1); 109 | 110 | lo = ix; lo{d} = 1:obj.sizeINI(d)/2; 111 | hi = ix; hi{d} = 1+obj.sizeINI(d)/2:obj.sizeINI(d); 112 | 113 | ylo = y(lo{:}); 114 | yhi = y(hi{:}); 115 | 116 | ix = repmat({':'},numel(obj.sizeINI),1); 117 | odd = ix; odd{d} = 1:2:obj.sizeINI(d); 118 | even = ix; even{d} = 2:2:obj.sizeINI(d); 119 | 120 | y(odd{:}) = (ylo+yhi) / sqrt(2); 121 | y(even{:}) = (ylo-yhi) / sqrt(2); 122 | 123 | end 124 | end 125 | 126 | end 127 | 128 | % original shape 129 | y = reshape(y,size(x)); 130 | 131 | end 132 | 133 | %% get number of coils 134 | function [nc dim sx] = get_coils(obj,x) 135 | 136 | sx = size(x); 137 | 138 | % number of coils 139 | nc = prod(sx) / prod(obj.sizeINI); 140 | 141 | if mod(nc,1) 142 | error('Expansion not compatible with sizeINI=[%s].',num2str(obj.sizeINI,'%i ')); 143 | end 144 | 145 | % get coil dimension 146 | dim = 0; 147 | for d = 1:numel(sx) 148 | if sx(d)~=dimsize(obj,d) 149 | dim = d; 150 | break; 151 | end 152 | end 153 | 154 | % expand array dimension, e.g. [2n] => [n 2] 155 | if dim>1 || iscolumn(x) 156 | sx = [obj.sizeINI nc]; 157 | dim = numel(sx); 158 | end 159 | 160 | end 161 | 162 | end 163 | 164 | %% threshold wavelet coefficients 165 | function [y lambda] = thresh(obj,x,sparsity) 166 | 167 | if nargin<3 168 | error('Not enough input arguments.'); 169 | end 170 | if ~isscalar(sparsity) || ~isreal(sparsity) || sparsity<0 || sparsity>1 171 | error('sparsity must be a scalar between 0 and 1.') 172 | end 173 | 174 | % to wavelet domain 175 | y = obj * x; 176 | 177 | % soft threshold coils separately 178 | y = reshape(y,prod(obj.sizeINI),[]); 179 | 180 | absy = abs(y); 181 | signy = sign(y); 182 | 183 | v = sort(absy,'ascend'); 184 | index = round(prod(obj.sizeINI) * sparsity); 185 | 186 | if index==0 187 | lambda = cast(0,'like',v); 188 | else 189 | lambda = v(index,:); 190 | y = signy .* max(absy-lambda,0); 191 | end 192 | 193 | % to image domain 194 | y = obj' * reshape(y,size(x)); 195 | 196 | end 197 | 198 | %% detect Q' and set flag 199 | function obj = ctranspose(obj) 200 | 201 | obj.trans = ~obj.trans; 202 | 203 | end 204 | 205 | %% dimension size 206 | function n = dimsize(obj,dim) 207 | 208 | if nargin<2 209 | n = obj.sizeINI; 210 | elseif ~isscalar(dim) || ~isnumeric(dim) || ~isreal(dim) || dim<1 || mod(dim,1)~=0 211 | error('Dimension argument must be a positive integer scalar within indexing range.'); 212 | elseif dim <= numel(obj.sizeINI) 213 | n = obj.sizeINI(dim); 214 | else 215 | n = 1; 216 | end 217 | 218 | end 219 | 220 | end 221 | 222 | end 223 | 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parallel 2 | A few parallel MRI reconstruction algorithms (HOMODYNE GRAPPA2D GRAPPA3D SAKE2D SAKE3D DHE2D). MATLAB implementations. May require functions from [util](https://github.com/marcsous/util). 3 | 4 | ## Adaptive reconstruction of phased array MR imagery (Matched Filter) 5 | 6 | David O. Walsh, Arthur F. Gmitro, Michael W. Marcellin 7 | 8 | https://pubmed.ncbi.nlm.nih.gov/10800033/ 9 | 10 | ## Generalized autocalibrating partially parallel acquisitions (GRAPPA) 11 | 12 | Mark A. Griswold, Peter M. Jakob, Robin M. Heidemann, Mathias Nittka, Vladimir Jellus, Jianmin Wang, Berthold Kiefer, Axel Haase 13 | 14 | https://doi.org/10.1002/mrm.10171 15 | 16 | 17 | ## Calibrationless parallel imaging reconstruction based on structured low‐rank matrix completion (SAKE) 18 | 19 | Peter J. Shin, Peder E. Z. Larson, Michael A. Ohliger, Michael Elad, John M. Pauly, Daniel B. Vigneron, Michael Lustig 20 | 21 | https://doi.org/10.1002/mrm.24997 22 | 23 | 24 | ## Low-Rank Modeling of Local k-Space Neighborhoods for Constrained MRI (LORAKS) 25 | 26 | Justin P. Haldar 27 | 28 | https://doi.org/10.1109/TMI.2013.2293974 29 | 30 | 31 | ## Minimizing echo and repetition times in magnetic resonance imaging using a double half‐echo k‐space acquisition and low‐rank reconstruction (DHE) 32 | 33 | Mark Bydder, Fadil Ali, Vahid Ghodrati, Peng Hu, Jingwen Yao, Benjamin M. Ellingson 34 | 35 | https://doi.org/10.1002/nbm.4458 36 | 37 | 38 | ## Optimal Shrinkage of Singular Values 39 | 40 | Danny Barash, Matan Gavish 41 | 42 | https://arxiv.org/pdf/1405.7511v2 43 | 44 | Code Supplement: https://purl.stanford.edu/kv623gt2817 45 | -------------------------------------------------------------------------------- /cconvn.m: -------------------------------------------------------------------------------- 1 | function C = cconvn(A,B) 2 | 3 | % cconvn N-dimensional circular convolution 4 | 5 | sA = size(A); 6 | sB = size(B); 7 | 8 | % indices with wrapped endpoints 9 | for k = 1:numel(sA) 10 | if sA(k)==1 || k>numel(sB) || sB(k)==1 11 | s{k} = ':'; 12 | else 13 | s{k} = [sA(k)-ceil(sB(k)/2)+2:sA(k) 1:sA(k) 1:floor(sB(k)/2)]; 14 | end 15 | end 16 | 17 | % pad array for convn valid 18 | C = convn(A(s{:}),B,'valid'); 19 | 20 | -------------------------------------------------------------------------------- /dhe.m: -------------------------------------------------------------------------------- 1 | function [ksp basic snorm opts] = dhe(fwd,rev,varargin) 2 | % [ksp basic snorm opts] = dhe(fwd,rev,varargin) 3 | % 4 | % Double Half Echo Reconstruction (2D only) 5 | % 6 | % fwd = kspace with forward readouts [nx ny nc ne] 7 | % rev = kspace with reverse readouts [nx ny nc ne] 8 | % 9 | % In the interests of using the same recon to do 10 | % comparisons, code accepts a single fwd dataset. 11 | % 12 | % -ksp is the reconstructed kspace for fwd/rev 13 | % -basic is a basic non-low rank reconstruction 14 | % -snorm is the convergence history (frobenius) 15 | % -opts returns the options (opts.freq) 16 | % 17 | % Note: don't remove readout oversampling before 18 | % calling this function. With partial sampling the 19 | % fft+crop method is incorrect (specify opts.osf). 20 | % 21 | % Ref: https://dx.doi.org/10.1002/nbm.4458 22 | % https://doi.org/10.1016/j.mri.2022.08.017 23 | %% example dataset 24 | 25 | if nargin==0 26 | disp('Running example...') 27 | load meas_MID00382_FID42164_clean_fl2_m400.mat 28 | data = squeeze(data); fwd = data(:,:,1); rev = data(:,:,2); 29 | varargin = {'center',[],'delete1st',[2 0],'readout',2}; 30 | clearvars -except fwd rev varargin 31 | end 32 | 33 | %% setup 34 | 35 | % default options 36 | opts.width = [5 5]; % kernel width (in kx ky) 37 | opts.radial = 1; % use radial kernel 38 | opts.loraks = 0; % conjugate symmetry 39 | opts.tol = 1e-5; % relative tolerance 40 | opts.gpu = 1; % use gpu if available 41 | opts.maxit = 1e3; % maximum no. iterations 42 | opts.std = []; % noise std dev, if available 43 | opts.center = []; % center of kspace, if available 44 | opts.delete1st = [1 0]; % delete [first last] readout pts 45 | opts.readout = 1; % readout dimension (1 or 2) 46 | opts.osf = 2; % readout oversampling factor (default 2) 47 | opts.freq = []; % off resonance in deg/dwell ([] = auto) 48 | 49 | % varargin handling (must be option/value pairs) 50 | for k = 1:2:numel(varargin) 51 | if k==numel(varargin) || ~ischar(varargin{k}) 52 | error('''varargin'' must be option/value pairs.'); 53 | end 54 | if ~isfield(opts,varargin{k}) 55 | error('''%s'' is not a valid option.',varargin{k}); 56 | end 57 | opts.(varargin{k}) = varargin{k+1}; 58 | end 59 | 60 | %% argument checks 61 | if ndims(fwd)<2 || ndims(fwd)>4 || ~isfloat(fwd) || isreal(fwd) 62 | error('''fwd'' must be a 2d-4d complex float array.') 63 | end 64 | if ~exist('rev','var') || isempty(rev) 65 | nh = 1; % no. half echos 66 | rev = []; % no rev echo 67 | else 68 | nh = 2; % no. half echos 69 | if ndims(rev)<2 || ndims(rev)>4 || ~isfloat(rev) || isreal(fwd) 70 | error('''rev'' must be a 2d-4d complex float array.') 71 | end 72 | if ~isequal(size(fwd),size(rev)) 73 | error('''fwd'' and ''rev'' must be same size.') 74 | end 75 | end 76 | if opts.osf<1 77 | error('osf must be >=1'); 78 | end 79 | if mod(size(fwd,opts.readout)/2/opts.osf,1) 80 | error('readout dim (%i) not divisible by 2*osf.',size(fwd,opts.readout)); 81 | end 82 | if opts.width(1)>size(fwd,1) || opts.width(end)>size(fwd,2) 83 | error('width [%ix%i] not compatible with matrix.\n',opts.width(1),opts.width(end)); 84 | end 85 | if isscalar(opts.width) 86 | opts.width = [opts.width opts.width]; 87 | elseif opts.readout==2 88 | opts.width = flip(opts.width); 89 | end 90 | if opts.readout==2 91 | fwd = permute(fwd,[2 1 3 4]); 92 | rev = permute(rev,[2 1 3 4]); 93 | elseif opts.readout~=1 94 | error('readout must be 1 or 2.'); 95 | end 96 | if isequal(opts.std,0) || numel(opts.std)>1 97 | error('noise std must be a non-zero scalar'); 98 | end 99 | if any(mod(opts.delete1st,1)) || any(opts.delete1st<0) 100 | error('delete1st must be a nonnegative integer.'); 101 | end 102 | if isscalar(opts.delete1st) 103 | opts.delete1st = [opts.delete1st 0]; 104 | end 105 | if ~isempty(opts.freq) && ~isscalar(opts.freq) 106 | error('freq must be scalar)'); 107 | end 108 | 109 | %% initialize 110 | [nx ny nc ne] = size(fwd); 111 | 112 | % convolution kernel indicies 113 | [x y] = ndgrid(-ceil(opts.width(1)/2):ceil(opts.width(1)/2), ... 114 | -ceil(opts.width(2)/2):ceil(opts.width(2)/2)); 115 | if opts.radial 116 | k = hypot(abs(x)/max(1,opts.width(1)),abs(y)/max(1,opts.width(2)))<=0.5; 117 | else 118 | k = abs(x)/max(1,opts.width(1))<=0.5 & abs(y)/max(1,opts.width(2))<=0.5; 119 | end 120 | opts.kernel.x = x(k); 121 | opts.kernel.y = y(k); 122 | nk = nnz(k); 123 | 124 | % dimensions of the dataset 125 | opts.dims = [nx ny nc ne nh nk 1]; 126 | if opts.loraks; opts.dims(7) = 2; end 127 | 128 | % concatenate fwd/rev echos 129 | data = cat(5,fwd,rev); 130 | mask = any(data,3); 131 | 132 | % delete 1st (and last) ADC "warm up" points on kx 133 | if any(opts.delete1st) 134 | for e = 1:ne 135 | for h = 1:nh 136 | kx = 1; init = any(mask(kx,:,1,e,h)); % is kx(1) sampled? 137 | while kx1 && ~isequal(opts.freq,0) 204 | 205 | % frequency: unit = deg/dwell 206 | opts.kx = (-nx/2:nx/2-1)' * pi / 180; 207 | 208 | % quick scan to find global minimum 209 | opts.range = linspace(-3,3,11); 210 | for k = 1:numel(opts.range) 211 | opts.nrm(k) = myfun(opts.range(k),data,opts); 212 | end 213 | [~,k] = min(opts.nrm); best = opts.range(k); 214 | 215 | % precalculate derivative matrix 216 | roll = cast(i*opts.kx,'like',data); 217 | tmp = repmat(-roll,1,ny,nc,ne); 218 | tmp = cat(5,tmp,repmat(+roll,1,ny,nc,ne)); 219 | tmp = reshape(tmp,size(data)); % make sure 220 | opts.P = make_data_matrix(tmp,opts); 221 | 222 | % off resonance (nuclear norm) 223 | if isempty(opts.freq) 224 | fopts = optimset('Display','off','GradObj','on'); 225 | nrm = median(abs(nonzeros(data))); % mitigate poor scaling 226 | opts.freq = fminunc(@(f)myfun(f,data/nrm,opts),best,fopts); 227 | end 228 | 229 | % off resonance correction 230 | roll = exp(i*opts.kx*opts.freq); 231 | data(:,:,:,:,1) = data(:,:,:,:,1)./roll; 232 | data(:,:,:,:,2) = data(:,:,:,:,2).*roll; 233 | 234 | % phase correction 235 | r = dot(data(:,:,:,:,1),data(:,:,:,:,2)); 236 | d = dot(data(:,:,:,:,1),data(:,:,:,:,1)); 237 | r = reshape(r,[],1); d = reshape(real(d),[],1); 238 | phi = angle((r'*d) / (d'*d)) / 2; 239 | 240 | data(:,:,:,:,1) = data(:,:,:,:,1)./exp(i*phi); 241 | data(:,:,:,:,2) = data(:,:,:,:,2).*exp(i*phi); 242 | 243 | % units: phi=radians freq=deg/dwell 244 | fprintf('Corrections: ϕ=%.2frad Δf=%.2fdeg/dwell\n',phi,opts.freq); 245 | 246 | % clear memory on GPU 247 | opts.P = []; clear tmp roll r d nrm 248 | 249 | end 250 | 251 | %% basic algorithm (average in place) 252 | 253 | basic = sum(data.*mask,5)./max(sum(mask,5),1); 254 | 255 | %% Cadzow algorithm 256 | 257 | ksp = zeros(size(data),'like',data); 258 | 259 | for iter = 1:max(1,opts.maxit) 260 | 261 | % data consistency 262 | ksp = ksp + bsxfun(@times,data-ksp,mask); 263 | 264 | % make calibration matrix 265 | [A opts] = make_data_matrix(ksp,opts); 266 | 267 | % row space and singular values 268 | if size(A,1)<=size(A,2) 269 | [~,S,V] = svd(A,'econ'); 270 | S = diag(S); 271 | V = V(:,1:numel(S)); 272 | else 273 | [V S] = svd(A'*A); 274 | S = sqrt(diag(S)); 275 | end 276 | 277 | % minimum variance filter 278 | f = max(0,1-noise_floor^2./S.^2); 279 | A = A * (V * diag(f) * V'); 280 | 281 | % undo hankel structure 282 | [ksp opts] = undo_data_matrix(A,opts); 283 | 284 | % check convergence 285 | snorm(iter) = norm(S,2); 286 | if iter<10 || snorm(iter) 1 || tol 1 313 | ok = nx/opts.osf/2+(1:nx/opts.osf); 314 | ksp = fftshift(ifft(ksp,[],1)); 315 | ksp = ksp(ok,:,:,:,:); 316 | ksp = fft(ifftshift(ksp),[],1); 317 | 318 | basic = fftshift(ifft(basic,[],1)); 319 | basic = basic(ok,:,:,:,:); 320 | basic = fft(ifftshift(basic),[],1); 321 | end 322 | 323 | % restore original orientation 324 | if opts.readout==2 325 | ksp = permute(ksp,[2 1 3 4 5]); 326 | basic = permute(basic,[2 1 3 4]); 327 | end 328 | 329 | % only return first/last nrm 330 | snorm = snorm(:,[1 end]); 331 | 332 | % avoid dumping to screen 333 | if nargout==0; clear; end 334 | 335 | %% make data matrix 336 | function [A opts] = make_data_matrix(data,opts) 337 | 338 | nx = size(data,1); 339 | ny = size(data,2); 340 | nc = size(data,3); 341 | ne = size(data,4); 342 | nh = size(data,5); 343 | nk = opts.dims(6); 344 | 345 | % precompute the circshifts with fast indexing 346 | if ~isfield(opts,'ix') 347 | opts.ix = repmat(1:uint32(nx*ny*nc*ne*nh),[1 nk]); 348 | opts.ix = reshape(opts.ix,[nx ny nc ne nh nk]); 349 | for k = 1:nk 350 | x = opts.kernel.x(k); 351 | y = opts.kernel.y(k); 352 | opts.ix(:,:,:,:,:,k) = circshift(opts.ix(:,:,:,:,:,k),[x y]); 353 | end 354 | if isa(data,'gpuArray'); opts.ix = gpuArray(opts.ix); end 355 | end 356 | A = data(opts.ix); 357 | 358 | if opts.loraks 359 | A = cat(7,A,conj(A(opts.flip.x,opts.flip.y,:,:,:,:))); 360 | end 361 | 362 | A = reshape(A,nx*ny,[]); 363 | 364 | %% undo data matrix 365 | function [data opts] = undo_data_matrix(A,opts) 366 | 367 | nx = opts.dims(1); 368 | ny = opts.dims(2); 369 | nc = opts.dims(3); 370 | ne = opts.dims(4); 371 | nh = opts.dims(5); 372 | nk = opts.dims(6); 373 | 374 | A = reshape(A,nx,ny,nc,ne,nh,nk,[]); 375 | 376 | if opts.loraks 377 | A(opts.flip.x,opts.flip.y,:,:,:,:,2) = conj(A(:,:,:,:,:,:,2)); 378 | end 379 | 380 | % precompute the circshifts with fast indexing 381 | if ~isfield(opts,'xi') 382 | opts.xi = reshape(1:uint32(numel(A)),size(A)); 383 | for k = 1:nk 384 | x = opts.kernel.x(k); 385 | y = opts.kernel.y(k); 386 | opts.xi(:,:,:,:,:,k,:) = circshift(opts.xi(:,:,:,:,:,k,:),-[x y]); 387 | end 388 | if isa(A,'gpuArray'); opts.xi = gpuArray(opts.xi); end 389 | end 390 | A = A(opts.xi); 391 | 392 | data = mean(reshape(A,nx,ny,nc,ne,nh,[]),6); 393 | 394 | %% off resonance + phase penalty function 395 | function [nrm grd] = myfun(freq,data,opts) 396 | 397 | nx = opts.dims(1); 398 | 399 | % off resonance correction 400 | roll = exp(i*opts.kx*freq(1)); 401 | data(:,:,:,:,1) = data(:,:,:,:,1)./roll; 402 | data(:,:,:,:,2) = data(:,:,:,:,2).*roll; 403 | 404 | % phase correction (not necessary but why not?) 405 | r = dot(data(:,:,:,:,1),data(:,:,:,:,2)); 406 | d = dot(data(:,:,:,:,1),data(:,:,:,:,1)); 407 | r = reshape(r,[],1); d = reshape(d,[],1); 408 | phi = angle((r'*d) / (d'*d)) / 2; 409 | 410 | data(:,:,:,:,1) = data(:,:,:,:,1)./exp(i*phi); 411 | data(:,:,:,:,2) = data(:,:,:,:,2).*exp(i*phi); 412 | 413 | % for nuclear norm 414 | A = make_data_matrix(data,opts); 415 | 416 | % gradient 417 | if nargout<2 418 | if size(A,1)<=size(A,2) 419 | S = svd(A,0); 420 | else 421 | S = svd(A'*A); 422 | S = sqrt(S); 423 | end 424 | dS = []; 425 | else 426 | if size(A,1)<=size(A,2) 427 | [~,S,V] = svd(A,0); 428 | S = diag(S); 429 | V = V(:,1:numel(S)); 430 | else 431 | [V S] = svd(A'*A); 432 | S = sqrt(diag(S)); 433 | end 434 | dA = A.*opts.P; 435 | dS = real(diag(V'*(A'*dA)*V))./S; 436 | end 437 | 438 | % plain doubles for fminunc 439 | nrm = gather(sum( S,'double')); 440 | grd = gather(sum(dS,'double')); 441 | 442 | %% show plots of various things 443 | function display(S,f,noise_floor,ksp,iter,snorm,tol,mask,opts) 444 | 445 | nx = opts.dims(1); 446 | ne = opts.dims(4); 447 | nh = opts.dims(5); 448 | 449 | % plot singular values 450 | subplot(2,4,1); plot(S/S(1)); title(sprintf('rank %i/%i',nnz(f),numel(f))); 451 | hold on; plot(f,'--'); hold off; xlim([0 numel(f)+1]); ylim([0 1]); grid on; 452 | line(xlim,min(1,gather([1 1]*noise_floor/S(1))),'linestyle',':','color','black'); 453 | legend({'singular vals.','sing. val. filter','noise floor'}); 454 | 455 | % plot change in metrics 456 | subplot(2,4,5); 457 | if iter==1 && isfield(opts,'nrm') && opts.dims(5)>1 % only if nh>1 458 | plot(opts.range,opts.nrm,'-o'); title('off-resonance'); yticklabels([]); 459 | axis tight; ylabel('||A||_*','fontweight','bold'); xlabel('freq (deg/dwell)'); 460 | line([1 1]*opts.freq,ylim,'linestyle','--','color','red'); grid on; 461 | else 462 | semilogy(snorm); grid on; xlim([1 iter]); xlabel('iters'); 463 | legend('||A||_F','location','northeast'); title(sprintf('tol %.2e',tol)); 464 | end 465 | 466 | % mask on iter=1 to show the blackness of kspace 467 | if iter==1; ksp = bsxfun(@times,ksp,mask); end 468 | 469 | % prefer ims over imagesc 470 | if exist('ims','file'); imagesc = @(x)ims(x,-0.99); end 471 | 472 | % show current kspace (lines show center) 473 | subplot(2,4,2); imagesc(log(sum(abs(ksp(:,:,:,1,1)),3))); 474 | xlabel(num2str(size(ksp,2),'ky [%i]')); 475 | ylabel(num2str(size(ksp,1),'kx [%i]')); 476 | if nh==2; title('kspace (fwd)'); else; title('kspace (echo 1)'); end 477 | line(xlim,[opts.center(1) opts.center(1)]); 478 | line([opts.center(2) opts.center(2)],ylim); 479 | if nh==2 480 | subplot(2,4,6); imagesc(log(sum(abs(ksp(:,:,:,1,2)),3))); 481 | xlabel(num2str(size(ksp,2),'ky [%i]')); 482 | ylabel(num2str(size(ksp,1),'kx [%i]')); 483 | title('kspace (rev)'); 484 | line(xlim,[opts.center(1) opts.center(1)]); 485 | line([opts.center(2) opts.center(2)],ylim); 486 | elseif ne>1 487 | subplot(2,4,6); imagesc(log(sum(abs(ksp(:,:,:,ne,1)),3))); 488 | xlabel(num2str(size(ksp,2),'ky [%i]')); 489 | ylabel(num2str(size(ksp,1),'kx [%i]')); 490 | title(sprintf('kspace (echo %i)',ne)); 491 | line(xlim,[opts.center(1) opts.center(1)]); 492 | line([opts.center(2) opts.center(2)],ylim); 493 | else 494 | subplot(2,4,6); imagesc(0); axis off; 495 | end 496 | 497 | % switch to image domain 498 | ksp = fftshift(ifft2(ifftshift(ksp))); 499 | 500 | % remove oversampling 501 | if opts.osf > 1 502 | nx = size(ksp,1); 503 | ok = (nx/opts.osf/2)+(1:nx/opts.osf); 504 | ksp = ksp(ok,:,:,:,:); 505 | end 506 | 507 | % show current image 508 | subplot(2,4,3); imagesc(sum(abs(ksp(:,:,:,1,1)),3)); 509 | xlabel(num2str(size(ksp,2),'y [%i]')); 510 | ylabel(num2str(size(ksp,1),'x [%i]')); 511 | if nh==2; title(sprintf('iter %i (fwd)',iter)); else; title(sprintf('iter %i (echo 1)',iter)); end 512 | if nh==2 513 | subplot(2,4,7); imagesc(sum(abs(ksp(:,:,:,1,2)),3)); 514 | xlabel(num2str(size(ksp,2),'y [%i]')); 515 | ylabel(num2str(size(ksp,1),'x [%i]')); 516 | title(sprintf('iter %i (rev)',iter)); 517 | elseif ne>1 518 | subplot(2,4,7); imagesc(sum(abs(ksp(:,:,:,ne,1)),3)); 519 | xlabel(num2str(size(ksp,2),'y [%i]')); 520 | ylabel(num2str(size(ksp,1),'x [%i]')); 521 | title(sprintf('iter %i (echo %i)',iter,ne)); 522 | else 523 | subplot(2,4,7); imagesc(0); axis off; 524 | end 525 | 526 | % show one coil image phase 527 | subplot(2,4,4); imagesc(angle(ksp(:,:,1,1,1))); 528 | xlabel(num2str(size(ksp,2),'y [%i]')); 529 | ylabel(num2str(size(ksp,1),'x [%i]')); 530 | if nh==2; title(sprintf('phase (fwd)')); else; title(sprintf('phase (echo %i)',1)); end 531 | if nh==2 532 | subplot(2,4,8); imagesc(angle(ksp(:,:,1,1,2))); 533 | xlabel(num2str(size(ksp,2),'y [%i]')); 534 | ylabel(num2str(size(ksp,1),'x [%i]')); 535 | title(sprintf('phase (rev)')); 536 | elseif ne>1 537 | subplot(2,4,8); imagesc(angle(ksp(:,:,1,ne,1))); 538 | xlabel(num2str(size(ksp,2),'y [%i]')); 539 | ylabel(num2str(size(ksp,1),'x [%i]')); 540 | title(sprintf('phase (echo %i)',ne)); 541 | else 542 | subplot(2,4,8); imagesc(0); axis off; 543 | end 544 | drawnow; 545 | -------------------------------------------------------------------------------- /espirit2.m: -------------------------------------------------------------------------------- 1 | function im = espirit2(data,varargin) 2 | %im = espirit2(data,varargin) 3 | % 4 | % Implementation of ESPIRIT 2D. 5 | % 6 | % Inputs: 7 | % - data is kspace [nx ny nc] with zeros in empty points 8 | % - varargin options pairs (e.g. 'width',4) 9 | % 10 | % Output: 11 | % - im is the coil-combined image(s) [nx ny ni] 12 | % 13 | % Example: 14 | if nargin==0 15 | disp('Running example...') 16 | load head 17 | data=fftshift(ifft2(data)); 18 | mask = false(1,256); 19 | mask(1:3:end) = 1; mask(125:132) = 1; 20 | data = bsxfun(@times,data,mask); 21 | varargin = {'std',2.5e-5,'beta',0.01,'sparsity',0.25}; 22 | clearvars -except data varargin 23 | end 24 | 25 | %% options 26 | 27 | opts.width = 5; % kernel width 28 | opts.radial = 1; % use radial kernel 29 | opts.ni = 2; % no. image components 30 | opts.tol = 1e-6; % pcg tolerance 31 | opts.maxit = 1000; % pcg max iterations 32 | opts.std = []; % noise std dev, if available 33 | opts.sparsity = 0; % L1 sparsity (0.2=20% zeros) 34 | opts.beta = 0; % L2 Tikhonov regulariztaion 35 | opts.gpu = 1; % use gpu, if available 36 | 37 | % varargin handling (must be option/value pairs) 38 | for k = 1:2:numel(varargin) 39 | if k==numel(varargin) || ~ischar(varargin{k}) 40 | if isempty(varargin{k}); continue; end 41 | error('''varargin'' must be option/value pairs.'); 42 | end 43 | if ~isfield(opts,varargin{k}) 44 | error('''%s'' is not a valid option.',varargin{k}); 45 | end 46 | opts.(varargin{k}) = varargin{k+1}; 47 | end 48 | 49 | %% initialize 50 | [nx ny nc] = size(data); 51 | 52 | % sampling mask [nx ny] 53 | mask = any(data,3); 54 | 55 | % estimate noise std (heuristic) 56 | if isempty(opts.std) 57 | tmp = data(repmat(mask,[1 1 nc])); 58 | tmp = sort([real(tmp); imag(tmp)]); 59 | k = ceil(numel(tmp)/10); tmp = tmp(k:end-k+1); % trim 20% 60 | opts.std = 1.4826 * median(abs(tmp-median(tmp))) * sqrt(2); 61 | end 62 | noise_floor = opts.std * sqrt(nnz(data)/nc); 63 | 64 | fprintf('ESPIRIT noise std = %.1e\n',opts.std) 65 | 66 | %% ESPIRIT setup 67 | 68 | % convolution kernel indicies 69 | [x y] = ndgrid(-fix(opts.width/2):fix(opts.width/2)); 70 | if opts.radial 71 | k = sqrt(x.^2+y.^2)<=opts.width/2; 72 | else 73 | k = abs(x)<=opts.width/2 & abs(y)<=opts.width/2; 74 | end 75 | nk = nnz(k); 76 | kernel.x = x(k); 77 | kernel.y = y(k); 78 | kernel.mask = k; 79 | 80 | fprintf('ESPIRIT kernel width = %i\n',opts.width) 81 | fprintf('ESPIRIT radial kernel = %i\n',opts.radial) 82 | fprintf('ESPIRIT kernel points = %i\n',nk) 83 | 84 | R = numel(mask)/nnz(mask); 85 | fprintf('ESPIRIT acceleration = %.2f\n',R) 86 | 87 | %% detect autocalibration samples 88 | 89 | % points that satisfy acs (cconvn => wrap) 90 | acs = cconvn(mask,kernel.mask)==nk; 91 | na = nnz(acs); 92 | 93 | % expand coils (save looping) 94 | acs = repmat(acs,[1 1 nc]); 95 | 96 | fprintf('ESPIRIT ACS lines = %i\n',round(na/nx)); 97 | 98 | %% calibration matrix 99 | C = zeros(na*nc,nk,'like',data); 100 | 101 | for k = 1:nk 102 | x = kernel.x(k); 103 | y = kernel.y(k); 104 | tmp = circshift(data,[x y]); 105 | C(:,k) = tmp(acs); 106 | end 107 | 108 | % put in matrix form 109 | C = reshape(C,na,nc*nk); 110 | fprintf('ESPIRIT calibration matrix = %ix%i\n',size(C)); 111 | 112 | % define dataspace vectors 113 | [~,S,V] = svd(C,'econ'); 114 | S = diag(S); 115 | nv = nnz(S > noise_floor); 116 | V = reshape(V(:,1:nv),nc,nk,nv); % only keep dataspace 117 | fprintf('ESPIRIT dataspace vectors = %i (out of %i)\n',nv,nc*nk) 118 | 119 | plot(S); xlim([0 numel(S)]); title('svals'); 120 | line(xlim,[noise_floor noise_floor],'linestyle',':'); drawnow 121 | if nv==0; error('No dataspace vectors - check noise std.'); end 122 | 123 | % dataspace vectors as convolution kernels 124 | C = zeros(nv,nc,nx,ny,'like',data); 125 | for k = 1:nk 126 | x = mod(kernel.x(k)+nx-1,nx)+1; % wrap in x 127 | y = mod(kernel.y(k)+ny-1,ny)+1; % wrap in y 128 | C(:,:,x,y) = permute(V(:,k,:),[3 1 2]); 129 | end 130 | 131 | %% ESPIRIT coil sensitivities 132 | 133 | % fft convolution <=> image multiplication 134 | C = fft(fft(C,nx,3),ny,4); % nv nc nx ny 135 | 136 | % optimal passband per pixel 137 | [~,~,C] = pagesvd(C,'econ'); 138 | 139 | % discard small features 140 | C = C(:,1:opts.ni,:,:); 141 | 142 | % reorder: nx ny nc ni 143 | C = permute(C,[3 4 1 2]); 144 | 145 | % normalize phase to coil 1 146 | C = bsxfun(@times,C,exp(-i*angle(C(:,:,1,:)))); 147 | 148 | %% switch to GPU (move up if pagesvd available on GPU) 149 | 150 | if opts.gpu 151 | C = gpuArray(C); 152 | mask = gpuArray(mask); 153 | data = gpuArray(data); 154 | end 155 | 156 | %% solve for image components 157 | 158 | % min (1/2)||A'Ax-A'b||_2 + lambda||Qx||_1 159 | AA = @(x)myfunc(x,C,mask,opts.beta); 160 | Ab = bsxfun(@times,conj(C),fft2(data)); 161 | Ab = reshape(sum(Ab,3),[],1); 162 | try 163 | Q = HWT([nx ny]); % set up Haar wavelet transform 164 | catch 165 | Q = 1; 166 | warning('HWT failed => using sparsity in image domain.'); 167 | end 168 | 169 | % solve by pcg/minres 170 | if opts.sparsity 171 | [im lambda resvec] = pcgL1(AA,Ab,opts.sparsity,opts.tol,opts.maxit,Q); 172 | z = abs(Q * im); sparsity = nnz(z <= 2*eps(max(z))) / numel(z); 173 | fprintf('ESPIRIT sparsity %f (lambda=%.2e)\n',sparsity,lambda); 174 | else 175 | [im,~,~,~,resvec] = minres(AA,Ab,opts.tol,opts.maxit); 176 | end 177 | im = reshape(im,nx,ny,opts.ni); 178 | 179 | % display 180 | for k = 1:opts.ni 181 | subplot(1,opts.ni,k); ims(abs(im(:,:,k))); 182 | title(sprintf('ESPIRIT component %i',k)); 183 | end 184 | 185 | % avoid dumping output to screen 186 | if nargout==0; clear; end 187 | 188 | 189 | %% ESPIRIT operator 190 | function r = myfunc(im,C,mask,beta) 191 | 192 | [nx ny nc ni] = size(C); 193 | im = reshape(im,nx,ny,1,ni); 194 | 195 | % normal equations 196 | r = bsxfun(@times,C,im); 197 | r = sum(r,4); 198 | r = ifft2(r); 199 | r = bsxfun(@times,r,mask); 200 | r = fft2(r); 201 | r = bsxfun(@times,conj(C),r); 202 | r = sum(r,3); 203 | 204 | % Tikhonov 205 | r = r+beta^2*im; 206 | 207 | % vector for solver 208 | r = reshape(r,[],1); 209 | 210 | 211 | % lsqr version - too clunky with rho / lsqrL1.m 212 | % 213 | % tricky: if rho is non-empty it appends onto A 214 | % as [A;rhoI]. caller must also append a vector 215 | % onto b as [b;rho*z] 216 | %A = @(x,flag,rho)myfun2(x,flag,rho,C,mask,opts.beta); 217 | %b = [reshape(data,[],1);zeros(nx*ny*opts.ni,1,'like',data)]; 218 | %b = b * sqrt(nx*ny); 219 | % 220 | %[im,~,~,~,resvec] = lsqr(@(x,flag)A(x,flag,[]),b,opts.tol,opts.maxit); 221 | %[im lambda resvec] = lsqrL1(A,b,opts.sparsity,opts.tol,opts.maxit); 222 | % 223 | % function r = myfunc(im,flag,rho,C,mask,beta) 224 | % 225 | % [nx ny nc ni] = size(C); 226 | % 227 | % % flag = 'transp'(lsqr) or 2(lsmr) 228 | % if isequal(flag,'transp') || isequal(flag,2) 229 | % 230 | % im = reshape(im,nx,ny,[],1); 231 | % 232 | % y = beta * im(:,:,nc+1:nc+2); 233 | % if ~isempty(rho) 234 | % z = rho * im(:,:,nc+3:nc+4); 235 | % end 236 | % 237 | % r = im(:,:,1:nc); 238 | % r = bsxfun(@times,r,mask); 239 | % r = fft2(r) / sqrt(nx*ny); 240 | % r = bsxfun(@times,conj(C),r); 241 | % r = sum(r,3); 242 | % r = r+reshape(y,nx,ny,1,2); 243 | % 244 | % if ~isempty(rho) 245 | % r = r+reshape(z,nx,ny,1,2); 246 | % end 247 | % 248 | % else 249 | % 250 | % im = reshape(im,nx,ny,1,ni); 251 | % r = bsxfun(@times,C,im); 252 | % r = ifft2(r) * sqrt(nx*ny); 253 | % r = bsxfun(@times,r,mask); 254 | % r = sum(r,4); 255 | % r = cat(3,r,beta*reshape(im,nx,ny,ni)); 256 | % 257 | % if ~isempty(rho) 258 | % r = cat(3,r,rho*reshape(im,nx,ny,ni)); 259 | % end 260 | % 261 | % end 262 | % 263 | % % vector for solver 264 | % r = reshape(r,[],1); -------------------------------------------------------------------------------- /espirit3.m: -------------------------------------------------------------------------------- 1 | function im = espirit3(data,varargin) 2 | %im = espirit3(data,varargin) 3 | % 4 | % Implementation of ESPIRIT 3D. 5 | % 6 | % Inputs: 7 | % - data is kspace [nx ny nz nc] with zeros in empty points 8 | % - varargin options pairs (e.g. 'width',4) 9 | % 10 | % Output: 11 | % - im is the coil-combined image(s) (nx ny nz ni) 12 | % 13 | % Example: 14 | if nargin==0 15 | disp('Running example...') 16 | load phantom3D_6coil.mat 17 | mask = false(size(data,1),size(data,2),size(data,3)); 18 | mask(:,1:2:end,1:2:end) = 1; % undersample 2x2 19 | mask(:,3:4:end,:) = circshift(mask(:,3:4:end,:),[0 0 1]); % shifted 20 | mask(size(data,1)/2+(-9:9),size(data,2)/2+(-9:9),size(data,3)/2+(-9:9)) = 1; % self calibration 21 | varargin = {'std',1e-5,'beta',0.01,'sparsity',0.25}; 22 | data = bsxfun(@times,data,mask); 23 | clearvars -except data varargin 24 | end 25 | 26 | %% options 27 | 28 | opts.width = 3; % kernel width (scalar) 29 | opts.radial = 0; % use radial kernel 30 | opts.ni = 1; % no. image components 31 | opts.tol = 1e-6; % pcg tolerance 32 | opts.maxit = 1000; % max pcg iterations 33 | opts.std = []; % noise std dev, if available 34 | opts.sparsity = 0; % L1 sparsity (0.1=10% zeros) 35 | opts.beta = 0; % L2 Tikhonov regularization 36 | opts.gpu = 1; % use gpu, if available 37 | 38 | % varargin handling (must be option/value pairs) 39 | for k = 1:2:numel(varargin) 40 | if k==numel(varargin) || ~ischar(varargin{k}) 41 | if isempty(varargin{k}); continue; end 42 | error('''varargin'' must be option/value pairs.'); 43 | end 44 | if ~isfield(opts,varargin{k}) 45 | error('''%s'' is not a valid option.',varargin{k}); 46 | end 47 | opts.(varargin{k}) = varargin{k+1}; 48 | end 49 | 50 | %% initialize 51 | [nx ny nz nc] = size(data); 52 | 53 | % circular wrap requires even dimensions 54 | if any(mod([nx ny nz],2)) 55 | error('Code requires even numbered dimensions'); 56 | end 57 | fprintf('ESPIRIT dataset = [%i %i %i %i]\n',nx,ny,nz,nc) 58 | 59 | % sampling mask [nx ny nz] 60 | mask = any(data,4); 61 | 62 | % estimate noise std (heuristic) 63 | if isempty(opts.std) 64 | tmp = data(repmat(mask,[1 1 1 nc])); 65 | tmp = sort([real(tmp); imag(tmp)]); 66 | k = ceil(numel(tmp)/10); tmp = tmp(k:end-k+1); % trim 20% 67 | opts.std = 1.4826 * median(abs(tmp-median(tmp))) * sqrt(2); 68 | end 69 | noise_floor = opts.std * sqrt(nnz(data)/nc); 70 | 71 | fprintf('ESPIRIT noise std = %.1e\n',opts.std) 72 | 73 | %% ESPIRIT setup 74 | 75 | % convolution kernel indicies 76 | [x y z] = ndgrid(-fix(opts.width/2):fix(opts.width/2)); 77 | if opts.radial 78 | k = sqrt(x.^2+y.^2+z.^2)<=opts.width/2; 79 | else 80 | k = abs(x)<=opts.width/2 & abs(y)<=opts.width/2 & abs(z)<=opts.width/2; 81 | end 82 | nk = nnz(k); 83 | kernel.x = x(k); 84 | kernel.y = y(k); 85 | kernel.z = z(k); 86 | kernel.mask = k; 87 | 88 | fprintf('ESPIRIT kernel width = %i\n',opts.width) 89 | fprintf('ESPIRIT radial kernel = %i\n',opts.radial) 90 | fprintf('ESPIRIT kernel points = %i\n',nk) 91 | 92 | R = numel(mask)/nnz(mask); 93 | fprintf('ESPIRIT acceleration = %.2f\n',R) 94 | 95 | %% detect autocalibration samples 96 | 97 | % points that satisfy acs (cconvn => wrap) 98 | acs = cconvn(mask,kernel.mask)==nk; 99 | na = nnz(acs); 100 | 101 | % expand coils (save looping) 102 | acs = repmat(acs,[1 1 1 nc]); 103 | 104 | fprintf('ESPIRIT ACS lines = %i\n',round(na/nx)); 105 | 106 | %% calibration matrix 107 | C = zeros(na*nc,nk,'like',data); 108 | 109 | for k = 1:nk 110 | x = kernel.x(k); 111 | y = kernel.y(k); 112 | z = kernel.z(k); 113 | tmp = circshift(data,[x y z]); 114 | C(:,k) = tmp(acs); 115 | end 116 | 117 | % put in matrix form 118 | C = reshape(C,na,nc*nk); 119 | fprintf('ESPIRIT calibration matrix = %ix%i\n',size(C)); 120 | 121 | % define dataspace vectors 122 | [~,S,V] = svd(C,'econ'); 123 | S = diag(S); 124 | nv = nnz(S > noise_floor); 125 | V = reshape(V(:,1:nv),nc,nk,nv); % only keep dataspace 126 | fprintf('ESPIRIT dataspace vectors = %i (out of %i)\n',nv,nc*nk) 127 | 128 | plot(S); xlim([0 numel(S)]); title('svals'); 129 | line(xlim,[noise_floor noise_floor],'linestyle',':'); drawnow 130 | 131 | % dataspace vectors as convolution kernels 132 | C = zeros(nv,nc,nx,ny,nz,'like',data); 133 | for k = 1:nk 134 | x = mod(kernel.x(k)+nx-1,nx)+1; % wrap in x 135 | y = mod(kernel.y(k)+ny-1,ny)+1; % wrap in y 136 | z = mod(kernel.z(k)+nz-1,nz)+1; % wrap in z 137 | C(:,:,x,y,z) = permute(V(:,k,:),[3 1 2]); 138 | end 139 | 140 | %% ESPIRIT coil sensitivities 141 | 142 | % fft convolution <=> image multiplication 143 | C = fft(fft(fft(C,nx,3),ny,4),nz,5); % nv nc nx ny nz 144 | 145 | % optimal passband per pixel 146 | [~,~,C] = pagesvd(C,'econ'); 147 | 148 | % discard small features 149 | C = C(:,1:opts.ni,:,:,:); 150 | 151 | % reorder: nx ny nz nc ni 152 | C = permute(C,[3 4 5 1 2]); 153 | 154 | % normalize phase to coil 1 155 | C = bsxfun(@times,C,exp(-i*angle(C(:,:,:,1,:)))); 156 | 157 | %% switch to GPU (move up if pagesvd available on GPU) 158 | 159 | if opts.gpu 160 | C = gpuArray(C); 161 | mask = gpuArray(mask); 162 | data = gpuArray(data); 163 | end 164 | 165 | %% solve for image components 166 | 167 | % min (1/2)||A'Ax-A'b||_2 + lambda||Qx||_1 168 | AA = @(x)myfunc(C,x,mask,opts.beta); 169 | Ab = bsxfun(@times,conj(C),fft3(data)); 170 | Ab = reshape(sum(Ab,4),[],1); 171 | try 172 | Q = HWT([nx ny nz]); % set up Haar wavelet transform 173 | catch 174 | Q = 1; 175 | warning('HWT failed => using sparsity in image domain.'); 176 | end 177 | 178 | % solve by pcg/minres 179 | if opts.sparsity 180 | [im lambda] = pcgL1(AA,Ab,opts.sparsity,opts.tol,opts.maxit,Q); 181 | z = abs(Q * im); sparsity = nnz(z <= 2*eps(max(z))) / numel(z); 182 | fprintf('ESPIRIT sparsity %f (lambda=%.2e)\n',sparsity,lambda); 183 | else 184 | [im,~,~,~,resvec] = minres(AA,Ab,opts.tol,opts.maxit); 185 | end 186 | im = reshape(im,nx,ny,nz,opts.ni); 187 | 188 | % display 189 | slice = floor(nx/2+1); % middle slice in x 190 | for k = 1:opts.ni 191 | subplot(1,opts.ni,k); 192 | imagesc(squeeze(abs(im(slice,:,:,k)))); 193 | title(sprintf('ESPIRIT component %i',k)); 194 | xlabel('z'); ylabel('y'); drawnow; 195 | end 196 | 197 | % avoid dumping output to screen 198 | if nargout==0; clear; end 199 | 200 | %% ESPIRIT operator 201 | function r = myfunc(C,im,mask,beta) 202 | 203 | [nx ny nz nc ni] = size(C); 204 | im = reshape(im,nx,ny,nz,1,ni); 205 | 206 | % normal equations 207 | r = bsxfun(@times,C,im); 208 | r = sum(r,5); 209 | r = ifft3(r); 210 | r = bsxfun(@times,r,mask); 211 | r = fft3(r); 212 | r = bsxfun(@times,conj(C),r); 213 | r = sum(r,4); 214 | 215 | % Tikhonov 216 | r = r+beta^2*im; 217 | 218 | % vector for solver 219 | r = reshape(r,[],1); 220 | -------------------------------------------------------------------------------- /fft3.m: -------------------------------------------------------------------------------- 1 | function arg = fft3(arg,m,n,p) 2 | 3 | if nargin<2; m = size(arg,1); end 4 | if nargin<3; n = size(arg,2); end 5 | if nargin<4; p = size(arg,3); end 6 | 7 | arg = fft(arg,m,1); 8 | arg = fft(arg,n,2); 9 | arg = fft(arg,p,3); 10 | -------------------------------------------------------------------------------- /fistaN.m: -------------------------------------------------------------------------------- 1 | function [x lambda errvec] = fistaN(AA,Ab,sparsity,tol,maxit,Q,svals) 2 | % [x lambda errvec] = fista(AA,Ab,sparsity,tol,maxit,Q) 3 | % 4 | % Solves the following problem via FISTA (normal eq): 5 | % 6 | % minimize (1/2)||AAx-Ab||_2^2 + lambda*||Qx||_1 7 | % 8 | % -AA is an [n x n] matrix (or function handle) 9 | % -sparsity is the fraction of zeros (0.1=10% zeros) 10 | % -tol/maxit are tolerance and max. no. iterations 11 | % -Q is a wavelet transform (Q*x and Q'*x - see HWT) 12 | % -svals is [largest smallest] singular values of AA 13 | % 14 | % -lambda that yields the required sparsity (scalar) 15 | % -errvec is the rel. change at each iteration (vector) 16 | % 17 | %% check arguments 18 | if nargin<3 || nargin>7 19 | error('Wrong number of input arguments'); 20 | end 21 | if nargin<4 || isempty(tol) 22 | tol = 1e-3; 23 | end 24 | if nargin<5 || isempty(maxit) 25 | maxit = 100; 26 | end 27 | if nargin<6 || isempty(Q) 28 | Q = 1; 29 | end 30 | if nargin<7 || isempty(svals) 31 | svals = []; 32 | else 33 | normAA = svals(1); 34 | end 35 | if isnumeric(AA) 36 | AA = @(arg) AA*arg; 37 | end 38 | if ~iscolumn(Ab) 39 | error('Ab must be a column vector'); 40 | end 41 | if ~isscalar(sparsity) || sparsity<0 || sparsity>1 42 | error('sparsity must be a scalar between 0 and 1.'); 43 | end 44 | 45 | %% solve by FISTA 46 | time = tic(); 47 | 48 | z = Ab; 49 | t = 1; 50 | 51 | for iter = 1:maxit 52 | 53 | Az = AA(z); 54 | 55 | % steepest descent along Ab 56 | if iter==1 57 | alpha = (z'*z) / (Az'*Az); 58 | z = alpha * z; 59 | x = z; 60 | end 61 | 62 | % power iteration to get norm(AA) 63 | if ~exist('normAA','var') 64 | tmp = Az/norm(Az); 65 | for k = 1:20 66 | if k>1 67 | tmp = tmp/normAA(k-1); 68 | end 69 | tmp = AA(tmp); 70 | normAA(k) = norm(tmp); 71 | end 72 | normAA = norm(tmp); 73 | end 74 | 75 | z = z + (Ab - Az) / normAA; 76 | 77 | xold = x; 78 | 79 | x = Q*z; 80 | [x lambda(iter)] = shrinkage(x,sparsity); 81 | x = Q'*x; 82 | 83 | errvec(iter) = norm(x-xold)/norm(x); 84 | 85 | if errvec(iter) < tol 86 | break; 87 | end 88 | 89 | % FISTA-ADA 90 | %if numel(svals)==2 91 | % r = 4 * (1-sqrt(prod(svals)))^2 / abs(1-prod(svals)); 92 | %else 93 | r = 4; 94 | %end 95 | t0 = t; 96 | t = (1+sqrt(1+r*t^2))/2; 97 | z = x + ((t0-1)/t) * (x-xold); 98 | 99 | end 100 | 101 | % report convergence 102 | if iter < maxit 103 | fprintf('%s converged at iteration %i to a solution with relative error %.1e. ',mfilename,iter,errvec(iter)); 104 | else 105 | fprintf('%s stopped at iteration %i with relative error %.1e without converging to the desired tolerance %.1e. ',mfilename,iter,errvec(iter),tol); 106 | end 107 | toc(time); 108 | 109 | %% shrink based on sparsity => return lambda 110 | function [z lambda] = shrinkage(z,sparsity) 111 | 112 | absz = abs(z); 113 | v = sort(absz,'ascend'); 114 | lambda = interp1(v,sparsity*numel(z),'linear',0); 115 | 116 | z = sign(z) .* max(absz-lambda, 0); % complex ok 117 | -------------------------------------------------------------------------------- /grappa2.m: -------------------------------------------------------------------------------- 1 | function ksp = grappa2(data,mask,varargin) 2 | % 3 | % Implementation of GRAPPA for 2D images (y-direction). 4 | % 5 | % Inputs: 6 | % -data is kspace [nx ny nc] with zeros in the empty lines 7 | % -mask is binary array [nx ny] or can be vector of indices 8 | % -varargin: pairs of options/values (e.g. 'width',3) 9 | % 10 | % Output: 11 | % -ksp is reconstructed kspace [nx ny nc] for each coil 12 | % 13 | % Notes: 14 | % -automatic detection of speedup factor and acs lines 15 | % -assumes center of kspace is at the center of the array 16 | % -requires uniform outer line spacing and fully sampled acs 17 | % 18 | %% example dataset 19 | 20 | if nargin==0 21 | disp('Running example...') 22 | load phantom 23 | data = fftshift(fft2(data)); % center k-space 24 | mask = 1:2:256; % undersampling (indices) 25 | %mask = union(mask,121:136); % self-calibration 26 | varargin = {'cal',data(63:194,121:136,:)}; % separate calibration 27 | end 28 | 29 | %% options 30 | 31 | opts.idx = -2:2; % neighborhood to use in readout (kx) 32 | opts.width = 2; % no. neighbors to use in phase (ky) 33 | opts.cal = []; % separate calibration data, if available 34 | opts.tol = []; % svd tolerance for calibration 35 | opts.readout = 1; % readout dimension (default=1) 36 | 37 | % varargin handling (must be option/value pairs) 38 | for k = 1:2:numel(varargin) 39 | if k==numel(varargin) || ~ischar(varargin{k}) 40 | if isempty(varargin{k}); continue; end 41 | error('''varargin'' must be option/value pairs.'); 42 | end 43 | if ~isfield(opts,varargin{k}) 44 | error('''%s'' is not a valid option.',varargin{k}); 45 | end 46 | opts.(varargin{k}) = varargin{k+1}; 47 | end 48 | 49 | %% initialize 50 | 51 | % argument checks 52 | if ndims(data)<2 || ndims(data)>3 || ~isfloat(data) || isreal(data) 53 | error('Argument ''data'' must be a 3d complex float array.') 54 | end 55 | 56 | % switch readout direction 57 | if opts.readout==2 58 | data = permute(data,[2 1 3]); 59 | if exist('mask','var'); mask = permute(mask,[2 1 3]); end 60 | if isfield(opts,'cal'); opts.cal = permute(opts.cal,[2 1 3]); end 61 | elseif opts.readout~=1 62 | error('Option ''readout'' must be 1 or 2.'); 63 | end 64 | [nx ny nc] = size(data); 65 | 66 | if ~exist('mask','var') || isempty(mask) 67 | mask = any(data,3); % 2d mask [nx ny] 68 | warning('Argument ''mask'' not supplied - guessing.') 69 | elseif isvector(mask) 70 | if nnz(mask~=0 & mask~=1) 71 | index = unique(mask); % allow indices 72 | mask = false(1,ny); mask(index) = 1; 73 | end 74 | mask = repmat(reshape(mask,1,ny),nx,1); 75 | end 76 | mask = reshape(mask~=0,nx,ny); % size/class compatible 77 | 78 | % non-sampled points must be zero 79 | data = bsxfun(@times,data,mask); 80 | 81 | %% detect sampling 82 | 83 | % indices of sampled phase encode lines 84 | pe = find(any(mask,1)); 85 | 86 | % detect speedup factor (equal spaced lines) 87 | for R = 1:nc+1 88 | eq = pe(1):R:pe(end); % equal spaced 89 | if all(ismember(eq,pe)); break; end 90 | end 91 | if R>nc; error('Sampling in ky not equal spaced (or too wide).'); end 92 | 93 | % indices of sampled readout points 94 | ro = find(any(mask,2)); 95 | 96 | % can only handle contiguous readout points 97 | if any(diff(ro)>1) 98 | error('Sampling must be contiguous in kx-direction.') 99 | end 100 | 101 | % display 102 | fprintf('Line spacing R = %i (speedup %.2f)\n',R,ny/numel(pe)); 103 | fprintf('Phase encodes = %i (out of %i)\n',numel(pe),ny); 104 | fprintf('Readout points = %i (out of %i)\n',numel(ro),nx); 105 | fprintf('Number of coils = %i\n',nc); 106 | 107 | %% GRAPPA kernel and acs 108 | 109 | % indices for the kernel (ky) 110 | for k = 1:opts.width 111 | idy(k) = 1+power(-1,k-1)*floor(k/2)*R; 112 | end 113 | idy = sort(idy); 114 | idx = sort(opts.idx); 115 | fprintf('Kernel: idx=[%s\b] and idy=[%s\b]\n',sprintf('%i ',idx),sprintf('%i ',idy)) 116 | 117 | % handle calibration data 118 | if isempty(opts.cal) 119 | 120 | % data is self-calibrated 121 | cal = data; 122 | 123 | % detect ACS lines (no wrap) 124 | acs = []; 125 | for j = 1:numel(pe) 126 | if all(ismember(pe(j)-idy,pe)) 127 | acs = [acs pe(j)]; 128 | end 129 | end 130 | 131 | % valid points along kx (no wrap) 132 | valid = ro(1)+max(idx):ro(end)+min(idx); 133 | 134 | else 135 | 136 | % separate calibration data 137 | cal = cast(opts.cal,'like',data); 138 | 139 | if size(cal,3)~=nc || ndims(cal)~=ndims(data) 140 | error('separate calibration data must have %i coils.',nc); 141 | end 142 | 143 | % detect ACS lines (assume fully sampled) 144 | acs = []; 145 | for j = 1:size(cal,2) 146 | if all(ismember(j-idy,1:size(cal,2))) 147 | acs = [acs j]; 148 | end 149 | end 150 | 151 | % valid points along kx (assume fully sampled) 152 | valid = 1+max(idx):size(cal,1)+min(idx); 153 | 154 | end 155 | 156 | na = numel(acs); 157 | nv = numel(valid); 158 | 159 | if nv<1 160 | error('Not enough ACS points in kx (%i)',nv); 161 | end 162 | if na<1 163 | error('Not enough ACS lines in ky (%i)',na); 164 | else 165 | fprintf('ACS region = [%i x %i] ',nv,na); 166 | if isempty(opts.cal) 167 | fprintf('(self cal)\n'); 168 | else 169 | fprintf('(separate cal)\n'); 170 | end 171 | end 172 | 173 | %% GRAPPA calibration: solve AX=B 174 | 175 | % convolution matrix (compatible with convn) 176 | A = zeros(nv,na,numel(idx),numel(idy),nc,'like',data); 177 | for j = 1:na 178 | for k = 1:numel(idx) 179 | A(:,j,k,:,:) = cal(valid-idx(k),acs(j)-idy,:); 180 | end 181 | end 182 | B = cal(valid,acs,:); 183 | 184 | % reshape into matrices 185 | A = reshape(A,nv*na,numel(idx)*numel(idy)*nc); 186 | B = reshape(B,nv*na,nc); 187 | 188 | % linear solution X = pinv(A)*B 189 | [V S] = svd(A'*A); S = diag(S); 190 | if isempty(opts.tol) 191 | tol = eps(S(1)); 192 | else 193 | tol = opts.tol; 194 | end 195 | invS = sqrt(S)./(S+tol); % tikhonov 196 | invS(~isfinite(invS.^2)) = 0; 197 | X = V*(invS.^2.*(V'*(A'*B))); 198 | 199 | fprintf('SVD tolerance = %.1e (%.1e%%)\n',tol,100*tol/S(1)); 200 | 201 | % reshape for convolution 202 | X = reshape(X,numel(idx),numel(idy),nc,nc); 203 | 204 | %% GRAPPA reconstruction 205 | 206 | % variable to return 207 | ksp = data; 208 | 209 | for k = 1:R-1 210 | neq = mod(eq,ny)+1; % new lines to reconstruct 211 | ksp(:,neq,:) = 0; % wipe any nonzero data 212 | for m = 1:nc 213 | for j = 1:nc 214 | ksp(:,neq,m) = ksp(:,neq,m) + cconvn(ksp(:,eq,j),X(:,:,j,m)); 215 | end 216 | end 217 | eq = neq; % new lines are now existing 218 | %ksp(:,pe,:) = data(:,pe,:); % reinsert original data (optional) 219 | end 220 | 221 | %% for partial Fourier sampling, zero out invalid lines 222 | 223 | if pe(1)>R || pe(end)R 225 | ksp(:,1:pe(1)-1,:) = 0; 226 | end 227 | if pe(end)R || ro(end)R 234 | ksp(1:ro(1)-1,:,:) = 0; 235 | end 236 | if ro(end)4 || ~isfloat(data) || isreal(data) 162 | error('Argument ''data'' must be a 4d complex float array.') 163 | end 164 | 165 | % switch readout direction 166 | if opts.readout==2 167 | data = permute(data,[2 1 3 4]); 168 | if isfield(opts,'cal'); opts.cal = permute(opts.cal,[2 1 3 4]); end 169 | elseif opts.readout==3 170 | data = permute(data,[3 2 1 4]); 171 | if isfield(opts,'cal'); opts.cal = permute(opts.cal,[3 2 1 4]); end 172 | elseif opts.readout~=1 173 | error('Readout dimension must be 1, 2 or 3'); 174 | end 175 | [nx ny nz nc] = size(data); 176 | 177 | % sampling mask [nx ny nz] 178 | mask = any(data,4); 179 | 180 | %% basic checks 181 | 182 | % overall speedup factor 183 | R = numel(mask)/nnz(mask); 184 | if R>nc 185 | warning('Speed up greater than no. coils (%.2f vs %i)',R,nc); 186 | end 187 | 188 | % indices of sampled points 189 | kx = find(any(any(mask,2),3)); 190 | if max(diff(kx))>1 191 | warning('Sampling must be contiguous in kx-direction.') 192 | end 193 | 194 | % initial ky-kz sampling 195 | yz = reshape(any(mask),ny,nz); 196 | fprintf('Kspace coverage before recon: %f\n',1/R); 197 | 198 | % sampling pattern after each pass of reconstruction 199 | for j = 1:numel(pattern) 200 | s{j} = cconvn(yz,pattern{j})==nnz(pattern{j}); 201 | yz(s{j}) = 1; % we now consider these lines sampled 202 | fprintf('Kspace coverage after pass %i: %f\n',j,nnz(yz)/(ny*nz)); 203 | end 204 | 205 | %% display 206 | 207 | fprintf('Data size = %s\n',sprintf('%i ',size(data))); 208 | fprintf('Readout points = %i (out of %i)\n',numel(kx),nx); 209 | disp(opts); 210 | 211 | subplot(1,3,1); imagesc(yz+reshape(any(mask),ny,nz),[0 2]); 212 | title(sprintf('sampling (pattern=%i)',opts.pattern)); 213 | xlabel('kz'); ylabel('ky'); 214 | 215 | subplot(1,3,2); im = sum(abs(ifft3(data)),4); 216 | slice = floor(nx/2+1); % the middle slice in x 217 | imagesc(squeeze(im(slice,:,:))); title(sprintf('slice %i (R=%.1f)',slice,R)); 218 | xlabel('z'); ylabel('y'); drawnow; 219 | 220 | % check for adequate kspace coverage post-reconstruction 221 | if nnz(yz)/numel(yz) < 0.95 222 | warning('inadequate coverage - check patterns (could be partial Fourier?).') 223 | end 224 | 225 | %% see if gpu is possible 226 | 227 | if opts.gpu 228 | data = gpuArray(data); 229 | mask = gpuArray(mask); 230 | gpu = gpuDevice; 231 | fprintf('GPU found = %s (%.1f Gb)\n',gpu.Name,gpu.AvailableMemory/1e9); 232 | end 233 | 234 | %% detect acs 235 | 236 | if isempty(opts.cal) 237 | 238 | % data is self-calibrated 239 | cal = data; 240 | 241 | % phase encode sampling 242 | yz = reshape(gather(any(mask)),ny,nz); 243 | 244 | % valid points along kx 245 | valid = kx(1)+max(opts.idx):kx(end)+min(opts.idx); 246 | 247 | else 248 | 249 | % separate calibration data 250 | cal = cast(opts.cal,'like',data); 251 | 252 | if size(cal,4)~=nc || ndims(cal)~=ndims(data) 253 | error('Separate calibration data must have %i coils.',nc); 254 | end 255 | 256 | % phase encode sampling (assume fully sampled) 257 | yz = true(size(cal,2),size(cal,3)); 258 | 259 | % valid points along kx (assume fully sampled) 260 | valid = 1+max(opts.idx):size(cal,1)+min(opts.idx); 261 | 262 | end 263 | 264 | nv = numel(valid); 265 | if nv<1 266 | error('Not enough ACS points in kx (%i).',nv); 267 | end 268 | 269 | % acs for each pattern (don't assume cal is symmetric => convn) 270 | for j = 1:numel(pattern) 271 | 272 | % matching lines in cal 273 | a = pattern{j}; 274 | a(center{j}) = 1; 275 | acs{j} = find(convn(yz,a,'same')==nnz(a)); 276 | na(j) = numel(acs{j}); 277 | 278 | fprintf('No. ACS lines for pattern %i = %i ',j,na(j)); 279 | if isempty(opts.cal) 280 | fprintf('(self cal)\n'); 281 | else 282 | fprintf('(separate cal)\n'); 283 | end 284 | if na(j)<1 285 | error('Not enough ACS lines (%i).',na(j)); 286 | end 287 | 288 | % exclude acs lines from being reconstructed 289 | if isempty(opts.cal); s{j}(acs{j}) = 0; end 290 | 291 | end 292 | 293 | %% calibration: linear equation AX=B 294 | t = tic(); 295 | 296 | % concatenate ky-kz to use indices (easier!) 297 | cal = reshape(cal,size(cal,1),[],nc); 298 | 299 | for j = 1:numel(pattern) 300 | 301 | % convolution matrix (compatible with convn) 302 | A = zeros(nv,na(j),numel(opts.idx),nnz(pattern{j}),nc,'like',data); 303 | 304 | % acs points in ky and kz 305 | [y z] = ind2sub(size(yz),acs{j}); 306 | 307 | % offsets to neighbors in ky and kz 308 | [dy dz] = ind2sub(size(pattern{j}),find(pattern{j})); 309 | 310 | % center the offsets 311 | dy = dy-center{j}(1); 312 | dz = dz-center{j}(2); 313 | 314 | % convolution matrix 315 | for k = 1:nnz(pattern{j}) 316 | 317 | % neighbors in ky and kz as indices 318 | idyz = sub2ind(size(yz),y-dy(k),z-dz(k)); 319 | 320 | for m = 1:numel(opts.idx) 321 | A(:,:,m,k,:) = cal(valid-opts.idx(m),idyz,:); 322 | end 323 | 324 | end 325 | B = cal(valid,acs{j},:); 326 | 327 | % reshape into matrix form 328 | B = reshape(B,[],nc); 329 | A = reshape(A,size(B,1),[]); 330 | 331 | % linear solution X = pinv(A)*B 332 | [V S] = svd(A'*A); S = diag(S); 333 | if isempty(opts.tol) 334 | tol = eps(S(1)); 335 | else 336 | tol = opts.tol; 337 | end 338 | invS = sqrt(S)./(S+tol); % tikhonov 339 | invS(~isfinite(invS.^2)) = 0; 340 | X = V*(invS.^2.*(V'*(A'*B))); 341 | 342 | % convert X from indices to convolution kernels 343 | Y = zeros(numel(opts.idx),numel(pattern{j}),nc,nc,'like',X); 344 | for k = 1:nc 345 | Y(:,find(pattern{j}),:,k) = reshape(X(:,k),numel(opts.idx),[],nc); 346 | end 347 | kernel{j} = reshape(Y,[numel(opts.idx) size(pattern{j}) nc nc]); 348 | 349 | clear A B V invS X Y % reduce memory for next loop 350 | end 351 | fprintf('SVD tolerance = %.1e (%.1e%%)\n',tol,100*tol/S(1)); 352 | fprintf('GRAPPA calibration: '); toc(t); 353 | 354 | %% reconstruction in multiple passes 355 | t = tic(); 356 | 357 | for j = 1:numel(pattern) 358 | 359 | ksp = data; 360 | 361 | for m = 1:nc 362 | 363 | ksp_coil_m = ksp(:,:,:,m); 364 | 365 | for k = 1:nc 366 | tmp = cconvn(data(:,:,:,k),kernel{j}(:,:,:,k,m)); 367 | ksp_coil_m(:,s{j}) = ksp_coil_m(:,s{j})+tmp(:,s{j}); 368 | end 369 | 370 | ksp(:,:,:,m) = ksp_coil_m; 371 | 372 | end 373 | 374 | data = ksp; 375 | 376 | end 377 | 378 | fprintf('GRAPPA reconstruction: '); toc(t); 379 | 380 | % unswitch readout direction 381 | if opts.readout==2 382 | ksp = permute(ksp,[2 1 3 4]); 383 | elseif opts.readout==3 384 | ksp = permute(ksp,[3 2 1 4]); 385 | end 386 | 387 | %% display 388 | 389 | subplot(1,3,3); im = sum(abs(ifft3(ksp)),4); 390 | imagesc(squeeze(im(slice,:,:))); 391 | title(sprintf('slice %i (R=%.1f)',slice,R)); 392 | xlabel('z'); ylabel('y'); drawnow; 393 | 394 | if nargout==0; clear; end % avoid dumping to screen 395 | 396 | -------------------------------------------------------------------------------- /head.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcsous/parallel/84f5994dc920975710e85364855f9d4a26468966/head.mat -------------------------------------------------------------------------------- /homodyne.m: -------------------------------------------------------------------------------- 1 | function [image phase] = homodyne(kspace,varargin) 2 | %[image phase] = homodyne(kspace,varargin) 3 | % 4 | % Partial Fourier reconstruction for 2D or 3D datasets. 5 | % Leave k-space zeroed where unsampled so the code can 6 | % figure out the sampling automatically. 7 | % 8 | % In this code we obey the laws of physics (1 dim only). 9 | % 10 | % Inputs: 11 | % -kspace is partially filled kspace (2D or 3D) single coil 12 | % -varargin: pairs of options/values (e.g. 'radial',1) 13 | % 14 | % Options: 15 | % -opts.method ('homodyne','pocs','least-squares','compressed-sensing') 16 | % -opts.window ('step','ramp','quad','cube','quartic') 17 | 18 | %% options 19 | 20 | opts.method = 'homodyne'; % 'homodyne','pocs','least-squares','compressed-sensing' 21 | opts.window = 'cubic'; % 'step','ramp','quad','cubic','quartic' 22 | opts.removeOS = 0; % remove 2x oversampling in specified dimension (0=off) 23 | 24 | % regularization terms (only apply to least squares/compressed sensing) 25 | opts.damp = 1e-4; % L2 penalty on solution norm 26 | opts.lambda = 1e-2; % L2 penalty on imag norm 27 | opts.cs = 5e-4; % L1 penalty on tranform norm 28 | 29 | % varargin handling (must be option/value pairs) 30 | for k = 1:2:numel(varargin) 31 | if k==numel(varargin) || ~ischar(varargin{k}) 32 | error('''varargin'' must be option/value pairs.'); 33 | end 34 | if ~isfield(opts,varargin{k}) 35 | warning('''%s'' is not a valid option.',varargin{k}); 36 | end 37 | opts.(varargin{k}) = varargin{k+1}; 38 | end 39 | 40 | %% handle looping over multi-coil / echo 41 | 42 | [nx ny nz nc] = size(kspace); 43 | 44 | if nx==1 || ny==1 45 | error('only 2D or 3D kspace allowed'); 46 | end 47 | 48 | if nx==0 || ny==0 || nz==0 || nc==0 49 | error('empty kspace not allowed'); 50 | end 51 | 52 | if nc>1 53 | 54 | for c = 1:nc 55 | [image(:,:,:,c) phase(:,:,:,c)] = homodyne(kspace(:,:,:,c),varargin{:}); 56 | end 57 | 58 | sz = size(kspace); 59 | if opts.removeOS; sz(opts.removeOS) = sz(opts.removeOS)/2; end 60 | image = reshape(image,sz); 61 | phase = reshape(phase,sz); 62 | 63 | else 64 | 65 | % detect sampling 66 | mask = (kspace~=0); 67 | kx = find(any(any(mask,2),3)); 68 | ky = find(any(any(mask,1),3)); 69 | kz = find(any(any(mask,1),2)); 70 | 71 | if any(diff(kx)~=1) || any(diff(ky)~=1) || any(diff(kz)~=1) 72 | warning('kspace not centered or not contiguous'); 73 | end 74 | 75 | % fraction of sampling in kx, ky, kz 76 | f = [numel(kx)/nx numel(ky)/ny]; 77 | if nz>1; f(3) = numel(kz)/nz; end 78 | 79 | % some checks 80 | [~,dim] = min(f); 81 | fprintf('partial sampling: [%s]. Using dimension %i.\n',num2str(f,'%.2f '),dim); 82 | 83 | if min(f<0.5) 84 | error('kspace is too undersampled - must be at least 0.5'); 85 | end 86 | 87 | if all(f>0.95) 88 | warning('kspace is fully sampled - skipping homodyne'); 89 | opts.method = 'none'; % fully sampled - bypass recon 90 | end 91 | 92 | %% set up filters 93 | 94 | if ~isequal(opts.method,'none') 95 | 96 | if dim==1; H = zeros(nx,1,1); index = kx; end 97 | if dim==2; H = zeros(1,ny,1); index = ky; end 98 | if dim==3; H = zeros(1,1,nz); index = kz; end 99 | H(index) = 1; 100 | 101 | % high pass filter 102 | H = H + flip(1-H); 103 | 104 | % symmetric center of kspace 105 | center = find(H==1); 106 | center(end+1) = numel(H)/2+1; % make sure 107 | center = unique(center); 108 | center = [center(1)-1;center(:);center(end)+1]; % pad by 1 point 109 | ramp = linspace(H(center(1)),H(center(end)),numel(center)); % symmetric points sum to 2 110 | 111 | switch opts.window 112 | case 'step' 113 | H(center) = 1; 114 | case {'linear','ramp'} 115 | H(center) = ramp; 116 | case {'quadratic','quad'} 117 | H(center) = (ramp-1).^2.*sign(ramp-1)+1; 118 | case {'cubic','cube'} 119 | H(center) = (ramp-1).^3+1; 120 | case {'quartic'} 121 | H(center) = (ramp-1).^4.*sign(ramp-1)+1; 122 | otherwise 123 | error('opts.window not recognized'); 124 | end 125 | 126 | % low pass filter 127 | L = sqrt(max(0,1-(H-1).^2)); 128 | 129 | % low resolution phase 130 | phase = bsxfun(@times,L,kspace); 131 | if false 132 | % smoothing in the other in-plane dimension (no clear benefit) 133 | if dim~=1; phase = bsxfun(@times,phase,sin(linspace(0,pi,nx)')); end 134 | if dim~=2; phase = bsxfun(@times,phase,sin(linspace(0,pi,ny) )); end 135 | end 136 | phase = angle(ifftn(ifftshift(phase))); 137 | end 138 | 139 | %% reconstruction 140 | 141 | maxit = 10; % no. of iterations to use for iterative opts.methods 142 | 143 | switch(opts.method) 144 | 145 | case 'homodyne' 146 | 147 | image = bsxfun(@times,H,kspace); 148 | image = ifftn(ifftshift(image)).*exp(-i*phase); 149 | image = abs(real(image)); 150 | 151 | case 'pocs' 152 | 153 | tmp = kspace; 154 | 155 | for iter = 1:maxit 156 | 157 | % abs and low res phase 158 | image = abs(ifftn(tmp)); 159 | tmp = image.*exp(i*phase); 160 | 161 | % data consistency 162 | tmp = fftshift(fftn(tmp)); 163 | tmp(mask) = kspace(mask); 164 | 165 | end 166 | 167 | case 'least-squares' 168 | 169 | % L2 penalized least squares requires pcgpc.m 170 | b = reshape(exp(-i*phase).*ifftn(ifftshift(kspace)),[],1); 171 | tmp = pcgpc(@(x)pcpop(x,mask,phase,opts.lambda,opts.damp),b,[],maxit); 172 | image = abs(real(reshape(tmp,size(phase)))); 173 | 174 | case 'compressed-sensing' 175 | 176 | % L1 penalized least squares requires pcgpc.m 177 | Q = DWT([nx ny nz],'db2'); % wavelet transform 178 | b = reshape(Q*(exp(-i*phase).*ifftn(ifftshift(kspace))),[],1); 179 | tmp = pcgL1(@(x)pcpop(x,mask,phase,opts.lambda,opts.damp,Q),b,opts.cs); 180 | image = abs(real(reshape(Q'*tmp,size(phase)))); 181 | 182 | case 'none' 183 | 184 | tmp = ifftn(kspace); 185 | image = abs(tmp); 186 | phase = angle(tmp); 187 | 188 | otherwise 189 | 190 | error('unknown opts.method ''%s''',opts.method); 191 | 192 | end 193 | 194 | if opts.removeOS 195 | 196 | image = fftshift(image); 197 | phase = fftshift(phase); 198 | 199 | switch opts.removeOS 200 | case 1; ok = nx/4 + (1:nx/2); 201 | image = image(ok,:,:,:); 202 | phase = phase(ok,:,:,:); 203 | case 2; ok = ny/4 + (1:ny/2); 204 | image = image(:,ok,:,:); 205 | phase = phase(:,ok,:,:); 206 | case 3; ok = nz/4 + (1:nz/2); 207 | image = image(:,:,ok,:); 208 | phase = phase(:,:,ok,:); 209 | otherwise 210 | error('removeOS dimension not supported'); 211 | end 212 | 213 | end 214 | 215 | end 216 | 217 | %% phase constrained projection operator (image <- image) 218 | function y = pcpop(x,mask,phase,lambda,damp,Q) 219 | % y = P' * F' * W * F * P * x + i * imag(x) + damp * x 220 | x = reshape(x,size(phase)); 221 | if exist('Q','var'); x = Q'*x; end 222 | y = exp(i*phase).*x; 223 | y = fftn(y); 224 | y = fftshift(mask).*y; 225 | y = ifftn(y); 226 | y = exp(-i*phase).*y; 227 | y = y + lambda*i*imag(x) + damp*x; 228 | if exist('Q','var'); y = Q*y; end 229 | y = reshape(y,[],1); 230 | -------------------------------------------------------------------------------- /ifft3.m: -------------------------------------------------------------------------------- 1 | function arg = ifft3(arg,m,n,p,varargin) 2 | 3 | if nargin<2; m = size(arg,1); end 4 | if nargin<3; n = size(arg,2); end 5 | if nargin<4; p = size(arg,3); end 6 | 7 | arg = ifft(arg,m,1,varargin{:}); 8 | arg = ifft(arg,n,2,varargin{:}); 9 | arg = ifft(arg,p,3,varargin{:}); 10 | -------------------------------------------------------------------------------- /matched_filter.m: -------------------------------------------------------------------------------- 1 | function [im coils noise] = matched_filter(data,np) 2 | % Matched filter coil combination (Walsh MRM 2000;43:682) 3 | % 4 | % Inputs: 5 | % data = complex images [nx ny nz nc (ne)] 6 | % np = no. pixels in the neighborhood (100) 7 | % 8 | % Output: 9 | % im = the combined image [nx ny nz (ne)] 10 | % coils = the optimal coil filters 11 | % noise = noise std estimate (maybe?) 12 | 13 | %% parse inputs 14 | [nx ny nz nc ne] = size(data); 15 | 16 | % try to accomodate 2D, 3D and a time dimension (echo) 17 | if isempty(data) 18 | error('data cannot be empty'); 19 | elseif ndims(data)==3 20 | nc = nz; nz = 1; 21 | elseif ndims(data)<4 || ndims(data)>5 22 | error('data must be [nx ny nz nc ne]'); 23 | end 24 | 25 | % coil dimension (must be 4) 26 | dim = 4; 27 | data = reshape(data,[nx ny nz nc ne]); 28 | 29 | % np=200 is 90% optimal but most benefit comes earlier 30 | if ~exist('np','var') 31 | np = 100; 32 | else 33 | np = max(nc,np); % lower limit 34 | end 35 | 36 | % warn about silliness 37 | if np*ne > 1000 38 | warning('effective neighborhood size (ne*np=%i) is excessively large',np*ne); 39 | end 40 | 41 | %% neighborhood of np nearest pixels 42 | 43 | % polygon of sides L 44 | L(3) = min(nz,np^(1/3)); 45 | L(2) = (np/L(3))^(1/2); 46 | L(1) = (np/L(3))^(1/2); 47 | 48 | [x y z] = ndgrid(-ceil(L(1)/2):ceil(L(1)/2), ... 49 | -ceil(L(2)/2):ceil(L(2)/2), ... 50 | -fix(L(3)/2):fix(L(3)/2)); 51 | 52 | % sort by radius 53 | r = sqrt(x.^2 + y.^2 + z.^2); 54 | [r k] = sort(reshape(r,[],1)); 55 | 56 | % round to nearest symmetric kernel 57 | ok = find(diff(r)); 58 | [~,j] = min(abs(np-ok)); 59 | np = ok(j); k = k(1:np); 60 | 61 | % keep nearest np points 62 | x = x(k); y = y(k); z = z(k); 63 | 64 | disp([mfilename ': [' num2str(size(data)) '] np=' num2str(np) ' r=' num2str(r(np),'%f')]); 65 | 66 | %% convolution matrix 67 | coils = zeros(nx,ny,nz,nc,ne,np,'like',data); 68 | 69 | for p = 1:np 70 | shift = [x(p) y(p) z(p)]; 71 | coils(:,:,:,:,:,p) = circshift(data,shift); 72 | end 73 | 74 | % fold ne into np dimension 75 | coils = reshape(coils,[nx ny nz nc ne*np]); 76 | 77 | % reorder for pagesvd: np nc nx ny nz 78 | coils = permute(coils,[5 4 1 2 3]); 79 | 80 | % optimal filter per pixel 81 | [~,noise,coils] = pagesvd(coils,'econ','vector'); 82 | 83 | % largest component only 84 | coils = coils(:,1,:,:,:); 85 | 86 | % reorder for dot: nx ny nz nc 1 87 | coils = permute(coils,[3 4 5 1 2]); 88 | 89 | % noise std estimate? 90 | noise = mean(reshape(noise(2:end,:,:,:,:),[],1)) / sqrt(ne*np); 91 | 92 | %% final image 93 | im = sum(coils.*data,dim); 94 | 95 | % collape coil dimension 96 | im = reshape(im,[nx ny nz ne]); 97 | -------------------------------------------------------------------------------- /meas_MID00382_FID42164_clean_fl2_m400.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcsous/parallel/84f5994dc920975710e85364855f9d4a26468966/meas_MID00382_FID42164_clean_fl2_m400.mat -------------------------------------------------------------------------------- /optimal_shrinkage.m: -------------------------------------------------------------------------------- 1 | function [singvals sigma] = optimal_shrinkage(singvals,beta,loss,sigma) 2 | 3 | % function singvals = optimal_shrinkage(singvals,beta,sigma_known) 4 | % 5 | % Perform optimal shrinkage (w.r.t one of a few possible losses) on data 6 | % singular values, when the noise is assumed white, and the noise level is known 7 | % or unknown. 8 | % 9 | % IN: 10 | % singvals: a vector of data singular values, obtained by running svd 11 | % on the data matrix 12 | % beta: aspect ratin m/n of the m-by-n matrix whose singular values 13 | % are given 14 | % loss: the loss function for which the shrinkage should be optimal 15 | % presently implmented: 'fro' (Frobenius or square Frobenius norm loss = MSE) 16 | % 'nuc' (nuclear norm loss) 17 | % 'op' (operator norm loss) 18 | % sigma: (optional) noise standard deviation (of each entry of the noise matrix) 19 | % if this argument is not provided, the noise level is estimated 20 | % from the data. 21 | % 22 | % OUT: 23 | % singvals: the vector of singular values after performing optimal shrinkage 24 | % 25 | % Usage: 26 | % Given an m-by-n matrix Y known to be low rank and observed in white noise 27 | % with zero mean, form a denoied matrix Xhat by: 28 | % 29 | % [U D V] = svd(Y,'econ'); 30 | % y = diag(Y); 31 | % y = optimal_shrinkage(y,m/n,'fro'); 32 | % Xhat = U * diag(y) * V'; 33 | % 34 | % where you can replace 'fro' with one of the other losses. 35 | % if the noise level sigma is known, in the third line use instead 36 | % y = optimal_shrinkage(y,m/n,'fro',sigma); 37 | % 38 | % ----------------------------------------------------------------------------- 39 | % Authors: Matan Gavish and David Donoho @stanford.edu, 2013 40 | % 41 | % This program is free software: you can redistribute it and/or modify it under 42 | % the terms of the GNU General Public License as published by the Free Software 43 | % Foundation, either version 3 of the License, or (at your option) any later 44 | % version. 45 | % 46 | % This program is distributed in the hope that it will be useful, but WITHOUT 47 | % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 48 | % FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 49 | % details. 50 | % 51 | % You should have received a copy of the GNU General Public License along with 52 | % this program. If not, see . 53 | % ----------------------------------------------------------------------------- 54 | % 55 | % 56 | % 2018 MB: NOTE THIS WAS MODIFIED TO RETURN SIGMA AND REDUCE ROUNDOFF ERRORS. 57 | % 58 | % 59 | assert(prod(size(beta))==1) 60 | assert(beta<=1) 61 | assert(beta>0) 62 | assert(prod(size(singvals))==length(singvals)) 63 | assert(ismember(loss,{'fro','op','nuc'})) 64 | 65 | % estimate sigma if needed 66 | if nargin<4 || isempty(sigma) 67 | warning('off','MATLAB:quadl:MinStepSize') 68 | MPmedian = MedianMarcenkoPastur(beta); 69 | sigma = median(singvals) / sqrt(MPmedian); 70 | fprintf('estimated sigma=%0.2e \n',sigma); 71 | end 72 | 73 | singvals = optshrink_impl(singvals,beta,loss,sigma); 74 | 75 | end 76 | 77 | function singvals = optshrink_impl(singvals,beta,loss,sigma) 78 | 79 | %y = @(x)( (1+sqrt(beta)).*(x<=beta^0.25) + sqrt((x+1./x) ... 80 | % .* (x+beta./x)).*(x>(beta^0.25)) ); 81 | assert(sigma>0) 82 | assert(prod(size(sigma))==1) 83 | 84 | sqrt0 = @(arg)sqrt(max(arg,0)); % MB no sqrt negatives 85 | x = @(y)( sqrt0(0.5*((y.^2-beta-1 )+sqrt0((y.^2-beta-1).^2 - 4*beta) ))... 86 | .* (y>=1+sqrt(beta))); 87 | 88 | 89 | opt_fro_shrink = @(y)( sqrt0(((y.^2-beta-1).^2 - 4*beta) ) ./ y); 90 | opt_op_shrink = @(y)(max(x(y),0)); 91 | opt_nuc_shrink = @(y)(max(0, (x(y).^4 - sqrt(beta)*x(y).*y - beta)) ... 92 | ./((x(y).^2) .* y)); 93 | 94 | y = singvals/sigma; 95 | 96 | switch loss 97 | case 'fro' 98 | singvals = sigma * opt_fro_shrink(y); 99 | case 'nuc' 100 | singvals = sigma * opt_nuc_shrink(y); 101 | singvals((x(y).^4 - sqrt(beta)*x(y).*y - beta)<=0)=0; 102 | case 'op' 103 | singvals = sigma * opt_op_shrink(y); 104 | otherwise 105 | error('loss unknown') 106 | end 107 | 108 | % MB handle roundoff errors 109 | for k = 2:numel(singvals) 110 | if singvals(k)>singvals(k-1) 111 | singvals(k) = 0; 112 | end 113 | end 114 | 115 | end 116 | 117 | 118 | function I = MarcenkoPasturIntegral(x,beta) 119 | if beta <= 0 | beta > 1, 120 | error('beta beyond') 121 | end 122 | lobnd = (1 - sqrt(beta))^2; 123 | hibnd = (1 + sqrt(beta))^2; 124 | if (x < lobnd) | (x > hibnd), 125 | error('x beyond') 126 | end 127 | dens = @(t) sqrt((hibnd-t).*(t-lobnd))./(2*pi*beta.*t); 128 | I = quadl(dens,lobnd,x); 129 | fprintf('x=%.3f,beta=%.3f,I=%.3f\n',x,beta,I); 130 | end 131 | 132 | 133 | function med = MedianMarcenkoPastur(beta) 134 | MarPas = @(x) 1-incMarPas(x,beta,0); 135 | lobnd = (1 - sqrt(beta))^2; 136 | hibnd = (1 + sqrt(beta))^2; 137 | change = 1; 138 | while change & (hibnd - lobnd > .001), 139 | change = 0; 140 | x = linspace(lobnd,hibnd,5); 141 | for i=1:length(x), 142 | y(i) = MarPas(x(i)); 143 | end 144 | if any(y < 0.5), 145 | lobnd = max(x(y < 0.5)); 146 | change = 1; 147 | end 148 | if any(y > 0.5), 149 | hibnd = min(x(y > 0.5)); 150 | change = 1; 151 | end 152 | end 153 | med = (hibnd+lobnd)./2; 154 | end 155 | 156 | function I = incMarPas(x0,beta,gamma) 157 | if beta > 1, 158 | error('betaBeyond'); 159 | end 160 | topSpec = (1 + sqrt(beta))^2; 161 | botSpec = (1 - sqrt(beta))^2; 162 | MarPas = @(x) IfElse((topSpec-x).*(x-botSpec) >0, ... 163 | sqrt((topSpec-x).*(x-botSpec))./(beta.* x)./(2 .* pi), ... 164 | 0); 165 | if gamma ~= 0, 166 | fun = @(x) (x.^gamma .* MarPas(x)); 167 | else 168 | fun = @(x) MarPas(x); 169 | end 170 | I = quadl(fun,x0,topSpec); 171 | 172 | function y=IfElse(Q,point,counterPoint) 173 | y = point; 174 | if any(~Q), 175 | if length(counterPoint) == 1, 176 | counterPoint = ones(size(Q)).*counterPoint; 177 | end 178 | y(~Q) = counterPoint(~Q); 179 | end 180 | 181 | end 182 | end 183 | 184 | 185 | -------------------------------------------------------------------------------- /partial_echo.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcsous/parallel/84f5994dc920975710e85364855f9d4a26468966/partial_echo.mat -------------------------------------------------------------------------------- /pcgL1.m: -------------------------------------------------------------------------------- 1 | function [x lambda resvec] = pcgL1(A,b,sparsity,tol,maxit,Q) 2 | % [x lambda resvec] = pcgL1(A,b,sparsity,tol,maxit,Q) 3 | % 4 | % Solves the following problem via ADMM: 5 | % 6 | % minimize (1/2)||Ax-b||_2^2 + lambda*||Qx||_1 7 | % 8 | % -A is a symmetric positive definite matrix (handle) 9 | % -sparsity is the fraction of zeros (0.1=10% zeros) 10 | % -tol/maxit are tolerance and max. no. iterations 11 | % -Q is a wavelet transform Q*x and Q'*x (see HWT.m) 12 | % 13 | % -lambda that yields the required sparsity (scalar) 14 | % -resvec is the residual at each iteration (vector) 15 | % 16 | % Derived from lasso_lsqr.m: 17 | % https://web.stanford.edu/~boyd/papers/admm/lasso/lasso_lsqr.html 18 | % 19 | %% check arguments 20 | if nargin<3 || nargin>6 21 | error('Wrong number of input arguments'); 22 | end 23 | if nargin<4 || isempty(tol) 24 | tol = 1e-6; 25 | end 26 | if nargin<5 || isempty(maxit) 27 | maxit = 100; 28 | end 29 | if nargin<6 || isempty(Q) 30 | Q = 1; 31 | end 32 | if isnumeric(A) 33 | A = @(arg) A * arg; 34 | end 35 | if ~iscolumn(b) 36 | error('b must be a column vector'); 37 | end 38 | if ~isscalar(sparsity) || sparsity<0 || sparsity>1 39 | error('sparsity must be a scalar between 0 and 1.'); 40 | end 41 | 42 | % check A is [n x n] 43 | n = numel(b); 44 | try 45 | Ab = A(b); 46 | catch 47 | error('A(x) failed when passed a vector of length %i',n); 48 | end 49 | if ~iscolumn(Ab) || numel(Ab)~=n 50 | error('A(x) did not return a vector of length %i',n); 51 | end 52 | ncalls = 1; % keep count of no. of matrix ncalls 53 | 54 | % check positive definiteness (50% chance of catching it) 55 | bAb = b'*Ab; 56 | if abs(imag(bAb)) > eps(bAb) || real(bAb) < -eps(bAb) 57 | error('Matrix operator A is not positive definite.'); 58 | end 59 | 60 | %% solve 61 | 62 | alpha = (b'*b) / bAb; 63 | x = alpha * b; 64 | 65 | z = zeros(size(b),'like',b); 66 | u = zeros(size(b),'like',b); 67 | 68 | for iter = 1:maxit 69 | 70 | % x-update 71 | Ak = @(x)A(x) + x * (iter>1); 72 | bk = b + (z - u); 73 | [x,flag,~,~,tmp] = minres(Ak,bk,[],[],[],[],x); 74 | 75 | ncalls = ncalls + numel(tmp); 76 | 77 | if flag && (iter>1) 78 | warning('minres returned error flag %i',flag); 79 | end 80 | 81 | % z-update 82 | z = Q * (x + u); 83 | 84 | [z lambda(iter)] = shrinkage(z, sparsity); 85 | 86 | z = Q' * z; 87 | 88 | u = u + (x - z); 89 | 90 | % check convergence 91 | resvec(iter) = norm(x-z) / norm(x); 92 | 93 | if resvec(iter) < tol 94 | break; 95 | end 96 | 97 | end 98 | x = z; 99 | 100 | % report convergence 101 | if iter < maxit 102 | fprintf('%s converged at iteration %i (%i function calls) to a solution with relative residual %.1e.\n',mfilename,iter,ncalls,resvec(iter)); 103 | else 104 | fprintf('%s stopped at iteration %i (%i function calls) with relative residual %.1e without converging to the desired tolerance %.1e.\n',mfilename,iter,ncalls,resvec(iter),tol); 105 | end 106 | 107 | %% shrink based on sparsity => return lambda 108 | function [z lambda] = shrinkage(z,sparsity) 109 | 110 | absz = abs(z); 111 | 112 | v = sort(absz,'ascend'); 113 | 114 | index = round(sparsity*numel(z)); 115 | 116 | if index==0 117 | lambda = cast(0,'like',v); 118 | else 119 | lambda = v(index); 120 | end 121 | 122 | z = sign(z) .* max(absz-lambda, 0); % complex ok 123 | -------------------------------------------------------------------------------- /pcgpc.m: -------------------------------------------------------------------------------- 1 | function [x,flag,relres,iter,resvec] = pcgpc(A,b,tol,maxit,M1,M2,x0) 2 | % Preconditioned conjugate gradient (pcg) with modifications to 3 | % support the use of a penalty on Im(x) aka a phase constraint. 4 | % 5 | % Meant to be used with anonymous functions, A = @(x)myfunc(x), 6 | % where myfunc(x) should return: 7 | % - A*x : to minimize ||b-Ax||^2 8 | % - A*x+λ*x : to minimize ||b-Ax||^2 + λ^2||x||^2 9 | % - A*x+λ*i*Im(x) : to minimize ||b-Ax||^2 + λ^2||Im(x)||^2 10 | % 11 | % References: 12 | % - An Introduction to the CG Method Without the Agonizing Pain 13 | % Jonathan Richard Shewchuk (1994) 14 | % - Partial Fourier Partially Parallel Imaging 15 | % Mark Bydder and Matthew D. Robson (MRM 2005;53:1393) 16 | % 17 | % Modifications: 18 | % - uses the real part only of the dot products 19 | % - allows multiple RHS vectors (b = [b1 b2 ...]) 20 | % - mostly compatible with pcg (except M2 and flag) 21 | % 22 | % Usage: see Matlab's pcg function (help pcg) 23 | 24 | % check arguments 25 | if nargin<2; error('Not enough input arguments.'); end 26 | if ~exist('tol') || isempty(tol); tol = 1e-6; end 27 | if ~exist('maxit') || isempty(maxit); maxit = 20; end 28 | if ~exist('M1') || isempty(M1); M1 = @(arg) arg; end 29 | if exist('M2') && ~isempty(M2); error('M2 argument not supported'); end 30 | if ~ismatrix(b); error('b argument must be a column vector or 2d array'); end 31 | validateattributes(tol,{'numeric'},{'scalar','nonnegative','finite'},'','tol'); 32 | validateattributes(maxit,{'numeric'},{'scalar','nonnegative','integer'},'','maxit'); 33 | 34 | % not intended for matrix inputs but they can be supported 35 | if isnumeric(A); A = @(arg) A * arg; end 36 | if isnumeric(M1); M1 = @(arg) M1 \ arg; end 37 | 38 | % initialize 39 | t = tic; 40 | iter = 1; 41 | flag = 1; 42 | if ~exist('x0') || isempty(x0) 43 | r = b; 44 | x = zeros(size(b),'like',b); 45 | else 46 | if ~isequal(size(x0),size(b)) 47 | error('x0 must be a column vector of length %i to match the problem size.',numel(b)); 48 | end 49 | x = x0; 50 | r = A(x); 51 | if ~isequal(size(r),size(b)) 52 | error('A(x) must return a column vector of length %i to match the problem size.',numel(b)); 53 | end 54 | r = b - r; 55 | end 56 | d = M1(r); 57 | if ~isequal(size(d),size(b)) 58 | error('M1(x) must return a column vector of length %i to match the problem size.',numel(b)); 59 | end 60 | delta0 = vecnorm(b); 61 | delta_new = real(dot(r,d)); 62 | resvec(iter,:) = vecnorm(r); 63 | solvec(iter,:) = vecnorm(x); 64 | 65 | % min norm solution 66 | xmin = x; 67 | imin = zeros(1,size(b,2)); 68 | 69 | % main loop 70 | while maxit 71 | 72 | iter = iter+1; 73 | clear q; q = A(d); 74 | alpha = delta_new./real(dot(d,q)); 75 | 76 | % unsuccessful termination 77 | if ~all(isfinite(alpha)); flag = 4; break; end 78 | 79 | x = x + alpha.*d; 80 | 81 | % recalculate residual occasionally 82 | if mod(iter,20)==0 83 | r = b - A(x); 84 | else 85 | r = r - alpha.*q; 86 | end 87 | 88 | % residual and solution vectors 89 | resvec(iter,:) = vecnorm(r); 90 | solvec(iter,:) = vecnorm(x); 91 | 92 | % keep best solution 93 | ok = resvec(iter,:) < min(resvec(1:iter-1,:)); 94 | if any(ok) 95 | xmin(:,ok) = x(:,ok); 96 | imin(ok) = iter; 97 | end 98 | 99 | % successful termination 100 | if all(resvec(iter,:)maxit; flag = 1; break; end 104 | 105 | clear q; q = M1(r); 106 | delta_old = delta_new; 107 | delta_new = real(dot(r,q)); 108 | 109 | % unsuccessful termination 110 | if all(delta_new<=0); flag = 4; break; end 111 | 112 | beta = delta_new./delta_old; 113 | d = q + beta.*d; 114 | 115 | end 116 | 117 | % min norm solution 118 | ok = imin==iter; 119 | if any(~ok) 120 | flag = 3; 121 | x(:,~ok) = xmin(:,~ok); 122 | end 123 | 124 | % remeasure final residual 125 | if nargout>2 126 | resvec(end,:) = vecnorm(b-A(x)); 127 | end 128 | relres = resvec(end,:)./delta0; 129 | 130 | % only display if flag not supplied 131 | if nargout<2 132 | for k = 1:size(b,2) 133 | fprintf('pcg terminated at iteration %i (flag %i): relres = %e.\n',imin(k),flag,relres(k)); 134 | end 135 | toc(t); 136 | end 137 | -------------------------------------------------------------------------------- /phantom.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcsous/parallel/84f5994dc920975710e85364855f9d4a26468966/phantom.mat -------------------------------------------------------------------------------- /phantom3D_6coil.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcsous/parallel/84f5994dc920975710e85364855f9d4a26468966/phantom3D_6coil.mat -------------------------------------------------------------------------------- /sake2.m: -------------------------------------------------------------------------------- 1 | function ksp = sake2(data,varargin) 2 | % ksp = sake2(data,varargin) 3 | % 4 | % 2D MRI reconstruction based on matrix completion. 5 | % 6 | % Singular value filtering is done based on opts.std 7 | % which is a key parameter affecting the image quality. 8 | % 9 | % Conjugate symmetry requires the center of kspace to be 10 | % at the center of the array so that flip works correctly. 11 | % 12 | % Inputs: 13 | % -data [nx ny nc]: 2D kspace data array from nc coils 14 | % -varargin: pairs of options/values (e.g. 'radial',1) 15 | % 16 | % Outputs: 17 | % -ksp [nx ny nc]: 2D kspace data array from nc coils 18 | % 19 | % References: 20 | % -Shin PJ et al. SAKE. Magn Resonance Medicine 2014;72:959 21 | % -Haldar JP et al. LORAKS. IEEE Trans Med Imag 2014;33:668 22 | % 23 | %% example dataset 24 | 25 | if nargin==0 26 | disp('Running example...') 27 | load phantom 28 | data = fftshift(fft2(data)); 29 | mask = false(256,256); 30 | R = 3; mask(:,1:R:end) = 1; % undersampling 31 | mask(:,124:134) = 1; % self calibration (or separate) 32 | varargin{1} = 'loraks'; varargin{2} = 1; % employ conjugate symmetry 33 | data = bsxfun(@times,data,mask); clearvars -except data varargin 34 | end 35 | 36 | %% setup 37 | 38 | % default options 39 | opts.width = 5; % kernel width 40 | opts.radial = 1; % use radial kernel 41 | opts.loraks = 0; % conjugate coils (loraks) 42 | opts.maxit = 1e3; % maximum no. iterations 43 | opts.tol = 1e-6; % convergence tolerance 44 | opts.pnorm = 2; % singular filter shape (>=1) 45 | opts.std = []; % noise std dev, if available 46 | opts.cal = []; % separate calibration data, if available 47 | opts.sparsity = 0; % sparsity in wavelet domain (0.1=10% zeros) 48 | 49 | % varargin handling (must be option/value pairs) 50 | for k = 1:2:numel(varargin) 51 | if k==numel(varargin) || ~ischar(varargin{k}) 52 | if isempty(varargin{k}); continue; end 53 | error('''varargin'' must be option/value pairs.'); 54 | end 55 | if ~isfield(opts,varargin{k}) 56 | warning('''%s'' is not a valid option.',varargin{k}); 57 | end 58 | opts.(varargin{k}) = varargin{k+1}; 59 | end 60 | 61 | %% initialize 62 | 63 | % argument checks 64 | if ndims(data)<2 || ndims(data)>3 || ~isfloat(data) || isreal(data) 65 | error('''data'' must be a 3d complex float array.') 66 | end 67 | if numel(opts.width)~=1 68 | error('width must be scalar.'); 69 | end 70 | [nx ny nc] = size(data); 71 | 72 | % convolution kernel indicies 73 | [x y] = ndgrid(-fix(opts.width/2):fix(opts.width/2)); 74 | if opts.radial 75 | k = hypot(x,y)<=opts.width/2; 76 | else 77 | k = abs(x)<=opts.width/2 & abs(y)<=opts.width/2; 78 | end 79 | nk = nnz(k); 80 | opts.kernel.x = x(k); 81 | opts.kernel.y = y(k); 82 | opts.kernel.mask = k; 83 | 84 | % estimate center of kspace (heuristic) 85 | [~,k] = max(reshape(data,[],nc)); 86 | [x y] = ind2sub([nx ny],k); 87 | opts.center(1) = gather(round(median(x))); 88 | opts.center(2) = gather(round(median(y))); 89 | 90 | % estimate noise std (heuristic) 91 | if isempty(opts.std) 92 | tmp = nonzeros(data); tmp = sort([real(tmp); imag(tmp)]); 93 | k = ceil(numel(tmp)/10); tmp = tmp(k:end-k+1); % trim 20% 94 | opts.std = 1.4826 * median(abs(tmp-median(tmp))) * sqrt(2); 95 | end 96 | noise_floor = opts.std * sqrt(nnz(data)/nc); 97 | 98 | % conjugate symmetric coils 99 | if opts.loraks 100 | nc = 2*nc; 101 | data = cat(3,data,conj(flip(flip(data,1),2))); 102 | if ~isempty(opts.cal) 103 | error('Conjugate symmetry not compatible with separate calibration.'); 104 | end 105 | end 106 | 107 | % dimensions of the dataset 108 | opts.dims = [nx ny nc nk]; 109 | 110 | % set up wavelet transform 111 | if opts.sparsity; Q = HWT([nx ny]); end 112 | 113 | % display 114 | disp(rmfield(opts,{'kernel'})); 115 | fprintf('Density = %f\n',nnz(data)/numel(data)); 116 | 117 | %% see if gpu is possible 118 | 119 | try 120 | if verLessThan('matlab','8.4'); error('GPU needs MATLAB R2014b.'); end 121 | gpu = gpuDevice; data = gpuArray(data); 122 | fprintf('GPU found: %s (%.1f Gb)\n',gpu.Name,gpu.AvailableMemory/1e9); 123 | catch ME 124 | data = gather(data); 125 | warning('%s Using CPU.', ME.message); 126 | end 127 | 128 | %% separate calibration data 129 | 130 | if ~isempty(opts.cal) 131 | 132 | if size(opts.cal,3)~=nc 133 | error('Calibration data has %i coils (data has %i).',size(opts.cal,3),nc); 134 | end 135 | 136 | A = make_data_matrix(cast(opts.cal,'like',data),opts); 137 | [V S] = svd(A'*A); % could truncate V based on S... 138 | 139 | end 140 | 141 | %% Cadzow algorithm 142 | 143 | ksp = data; 144 | 145 | for iter = 1:opts.maxit 146 | 147 | % make calibration matrix 148 | if iter==1 149 | A = make_data_matrix(ksp,opts); 150 | ix = (A ~= 0); val = A(ix); 151 | else 152 | A(ix) = val; % data consistency 153 | end 154 | 155 | % row space and singular values 156 | if isempty(opts.cal) 157 | [V S] = svd(A'*A); 158 | S = sqrt(diag(S)); 159 | else 160 | S = sqrt(svd(A'*A)); 161 | end 162 | 163 | % singular value filter 164 | f = max(0,1-(noise_floor./S).^opts.pnorm); 165 | A = A * (V * diag(f) * V'); 166 | 167 | % undo hankel structure 168 | if iter==1 169 | xi = 1:uint32(numel(A)); 170 | if isa(ix,'gpuArray') 171 | xi = gpuArray(xi); 172 | end 173 | xi = undo_data_matrix(xi,opts); 174 | end 175 | ksp = mean(A(xi),4); 176 | 177 | % sparsity 178 | if opts.sparsity 179 | ksp = fft2(ksp); % to image 180 | ksp = Q.thresh(ksp,opts.sparsity); 181 | ksp = ifft2(ksp); % to kspace 182 | end 183 | 184 | % schatten p-norm 185 | snorm(iter) = norm(S,opts.pnorm); 186 | if iter<10 || snorm(iter) 1 || tol=1) 47 | opts.std = []; % noise std dev, if available 48 | opts.cal = []; % separate calibration data, if available 49 | opts.gpu = 1; % use GPU, if available (often faster without) 50 | opts.sparsity = 0; % sparsity in wavelet domain (0.1=10% zeros) 51 | 52 | % varargin handling (must be option/value pairs) 53 | for k = 1:2:numel(varargin) 54 | if k==numel(varargin) || ~ischar(varargin{k}) 55 | if isempty(varargin{k}); continue; end 56 | error('''varargin'' must be option/value pairs.'); 57 | end 58 | if ~isfield(opts,varargin{k}) 59 | error('''%s'' is not a valid option.',varargin{k}); 60 | end 61 | opts.(varargin{k}) = varargin{k+1}; 62 | end 63 | 64 | %% initialize 65 | 66 | % argument checks 67 | if ndims(data)<3 || ndims(data)>4 || ~isfloat(data) || isreal(data) 68 | error('''data'' must be a 4d complex float array.') 69 | end 70 | if numel(opts.width)==1 71 | opts.width = [1 1 1] * opts.width; 72 | elseif numel(opts.width)~=3 73 | error('width must have 1 or 3 elements.'); 74 | end 75 | [nx ny nz nc] = size(data); 76 | if nz==1; opts.width(3) = 1; end % handle 2D case 77 | 78 | % convolution kernel indicies 79 | [x y z] = ndgrid(-ceil(opts.width(1)/2):ceil(opts.width(1)/2), ... 80 | -ceil(opts.width(2)/2):ceil(opts.width(2)/2), ... 81 | -ceil(opts.width(3)/2):ceil(opts.width(3)/2)); 82 | if opts.radial 83 | k = x.^2/max(1,opts.width(1)^2)+y.^2/max(1,opts.width(2)^2)+z.^2/max(1,opts.width(3)^2) <= 0.25; 84 | else 85 | k = abs(x)/max(1,opts.width(1))<=0.5 & abs(y)/max(1,opts.width(2))<=0.5 & abs(z)/max(1,opts.width(3))<=0.5; 86 | end 87 | nk = nnz(k); 88 | opts.kernel.x = x(k); 89 | opts.kernel.y = y(k); 90 | opts.kernel.z = z(k); 91 | opts.kernel.mask = k; 92 | 93 | % estimate center of kspace (heuristic) 94 | [~,k] = max(reshape(data,[],nc)); 95 | [x y z] = ind2sub([nx ny nz],k); 96 | opts.center(1) = gather(round(median(x))); 97 | opts.center(2) = gather(round(median(y))); 98 | opts.center(3) = gather(round(median(z))); 99 | 100 | % estimate noise std (heuristic) 101 | if isempty(opts.std) 102 | tmp = nonzeros(data); tmp = sort([real(tmp); imag(tmp)]); 103 | k = ceil(numel(tmp)/10); tmp = tmp(k:end-k+1); % trim 20% 104 | opts.std = 1.4826 * median(abs(tmp-median(tmp))) * sqrt(2); 105 | end 106 | noise_floor = opts.std * sqrt(nnz(data)/nc); 107 | 108 | % conjugate symmetric coils 109 | if opts.loraks 110 | nc = 2*nc; 111 | data = cat(4,data,conj(flip(flip(flip(data,1),2),3))); 112 | if ~isempty(opts.cal) 113 | error('Conjugate symmetry not compatible with separate calibration.'); 114 | end 115 | end 116 | 117 | % dimensions of the data set 118 | opts.dims = [nx ny nz nc nk]; 119 | 120 | % set up wavelet transform: 121 | if opts.sparsity; Q = HWT([nx ny nz]); end 122 | 123 | % display 124 | disp(rmfield(opts,{'kernel'})); 125 | fprintf('Density = %f\n',nnz(data)/numel(data)); 126 | 127 | if ~nnz(data) || ~isfinite(noise_floor) 128 | error('data all zero or contains Inf/NaN.'); 129 | end 130 | 131 | %% see if gpu is possible 132 | 133 | try 134 | if ~opts.gpu; error('GPU option set to off.'); end 135 | if verLessThan('matlab','8.4'); error('GPU needs MATLAB R2014b.'); end 136 | gpu = gpuDevice; data = gpuArray(data); 137 | fprintf('GPU found: %s (%.1f Gb)\n',gpu.Name,gpu.AvailableMemory/1e9); 138 | catch ME 139 | data = gather(data); 140 | warning('%s Using CPU.', ME.message); 141 | end 142 | 143 | %% separate calibration data 144 | 145 | if ~isempty(opts.cal) 146 | 147 | if size(opts.cal,4)~=nc 148 | error('Calibration data has %i coils (data has %i).',size(opts.cal,4),nc); 149 | end 150 | 151 | cal = cast(opts.cal,'like',data); 152 | AA = make_data_matrix(cal,opts); 153 | [V S] = svd(AA); % could truncate V based on S... 154 | 155 | end 156 | 157 | %% Cadzow algorithm 158 | 159 | mask = (data ~= 0); % sampling mask 160 | ksp = zeros(size(data),'like',data); 161 | 162 | for iter = 1:opts.maxit 163 | 164 | % data consistency 165 | ksp = ksp + bsxfun(@times,data-ksp,mask); 166 | 167 | % impose sparsity 168 | if opts.sparsity 169 | ksp = fft3(ksp); % to image 170 | ksp = Q.thresh(ksp,opts.sparsity); 171 | ksp = ifft3(ksp); % to kspace 172 | end 173 | 174 | % normal calibration matrix 175 | AA = make_data_matrix(ksp,opts); 176 | 177 | % row space and singular values 178 | if isempty(opts.cal) 179 | [V S] = svd(AA); 180 | S = sqrt(diag(S)); 181 | else 182 | S = sqrt(svd(AA)); 183 | end 184 | 185 | % singular value filter 186 | f = max(0,1-(noise_floor./S).^opts.pnorm); 187 | F = V * diag(f) * V'; 188 | 189 | % hankel structure (average along anti-diagonals) 190 | ksp = undo_data_matrix(F,ksp,opts); 191 | 192 | % schatten p-norm of A 193 | snorm(iter) = norm(S,opts.pnorm); 194 | if iter<10 || snorm(iter) 5 || tol < opts.tol 204 | if t(1)==t(2) 205 | fprintf('Iterations per second: %.2f\n',(iter-1)/toc(t(2))); 206 | end 207 | display(S,f,noise_floor,ksp,iter,tol,snorm,mask,opts); t(2) = tic(); 208 | end 209 | 210 | % finish when nothing left to do 211 | if tol < opts.tol; break; end 212 | 213 | end 214 | 215 | fprintf('Iterations performed: %i (%.0f sec)\n',iter,toc(t(1))); 216 | if opts.gpu; ksp = gather(ksp); end % return on CPU 217 | if nargout==0; clear; end % avoid dumping to screen 218 | 219 | %% make normal calibration matrix (low memory) 220 | function AA = make_data_matrix(data,opts) 221 | 222 | nx = size(data,1); 223 | ny = size(data,2); 224 | nz = size(data,3); 225 | nc = size(data,4); 226 | nk = opts.dims(5); 227 | 228 | AA = zeros(nc,nk,nc,nk,'like',data); 229 | 230 | for j = 1:nk 231 | 232 | x = opts.kernel.x(j); 233 | y = opts.kernel.y(j); 234 | z = opts.kernel.z(j); 235 | row = circshift(data,[x y z]); % rows of A.' 236 | 237 | for k = j:nk 238 | 239 | if j==k 240 | AA(:,j,:,k) = reshape(row,[],nc)' * reshape(row,[],nc); 241 | else 242 | x = opts.kernel.x(k); 243 | y = opts.kernel.y(k); 244 | z = opts.kernel.z(k); 245 | col = circshift(data,[x y z]); % cols of A 246 | 247 | % fill normal matrix (conjugate symmetric) 248 | tmp = reshape(row,[],nc)' * reshape(col,[],nc); 249 | AA(:,j,:,k) = tmp; 250 | AA(:,k,:,j) = tmp'; 251 | end 252 | 253 | end 254 | 255 | end 256 | AA = reshape(AA,nc*nk,nc*nk); 257 | 258 | %% undo calibration matrix (low memory) 259 | function ksp = undo_data_matrix(F,data,opts) 260 | 261 | nx = opts.dims(1); 262 | ny = opts.dims(2); 263 | nz = opts.dims(3); 264 | nc = opts.dims(4); 265 | nk = opts.dims(5); 266 | 267 | F = reshape(F,nc,nk,nc,nk); 268 | 269 | ksp = zeros(nx,ny,nz,nc,'like',data); 270 | 271 | for j = 1:nk 272 | 273 | x = opts.kernel.x(j); 274 | y = opts.kernel.y(j); 275 | z = opts.kernel.z(j); 276 | col = circshift(data,[x y z]); % cols of A 277 | 278 | for k = 1:nk 279 | 280 | tmp = reshape(col,[],nc) * reshape(F(:,j,:,k),nc,nc); 281 | tmp = reshape(tmp,nx,ny,nz,nc); 282 | 283 | % average along rows 284 | x = opts.kernel.x(k); 285 | y = opts.kernel.y(k); 286 | z = opts.kernel.z(k); 287 | ksp = ksp + circshift(tmp,-[x y z]) / nk; 288 | 289 | end 290 | 291 | end 292 | 293 | %% show plots of various things 294 | function display(S,f,noise_floor,ksp,iter,tol,snorm,mask,opts) 295 | 296 | % plot singular values 297 | subplot(1,4,1); plot(S/S(1)); title(sprintf('rank %i',nnz(f))); 298 | hold on; plot(f,'--'); hold off; xlim([0 numel(f)+1]); grid on; 299 | line(xlim,gather([1 1]*noise_floor/S(1)),'linestyle',':','color','black'); 300 | legend({'singular vals.','sing. val. filter','noise floor'}); 301 | 302 | % mask on iter=1 to show the blackness of kspace 303 | if iter==1 304 | ksp = ksp .* mask; 305 | if opts.loraks; ksp = ksp(:,:,:,1:size(ksp,4)/2); end 306 | end 307 | 308 | % prefer ims over imagesc 309 | if exist('ims','file'); imagesc = @(x)ims(x,-0.99); end 310 | 311 | % show current kspace (center of kx) 312 | subplot(1,4,2); 313 | tmp = squeeze(log(sum(abs(ksp(opts.center(1),:,:,:)),4))); 314 | imagesc(tmp); xlabel('kz'); ylabel('ky'); title('kspace'); 315 | line(xlim,[opts.center(2) opts.center(2)]); 316 | line([opts.center(3) opts.center(3)],ylim); 317 | 318 | % show current image (center of x) 319 | subplot(1,4,3); slice = floor(size(ksp,1)/2+1); % middle slice in x 320 | tmp = ifft(ksp); tmp = squeeze(tmp(slice,:,:,:)); 321 | imagesc(sum(abs(ifft2(tmp)),3)); xlabel('z'); ylabel('y'); 322 | title(sprintf('iter %i',iter)); 323 | 324 | % plot change in metrics 325 | subplot(1,4,4); plot(snorm); xlabel('iters'); xlim([0 iter+1]); grid on; 326 | title(sprintf('tol %.2e',tol)); legend('||A||_p','location','northwest'); 327 | 328 | drawnow; 329 | --------------------------------------------------------------------------------