diff --git a/cheat_sheet.m b/cheat_sheet.m index a0fba8dc..b2c3efa1 100644 --- a/cheat_sheet.m +++ b/cheat_sheet.m @@ -1,3 +1,9 @@ +% CHEAT_SHEET - "Copy & paste" QUPS syntax and usage examples +% +% This script provides basic syntax and usage examples for a variety of +% QUPS classes, methods, and compute kernels (functions). It is meant to +% provide a list of "copy & paste" ready lines of code. + %% Transducers c0 = 1500; fc = 6e6; @@ -64,13 +70,13 @@ seq = SequenceRadial('type','PW', 'angles', -20 : 0.5 : 20); % ---------- Focused Sequences ---------- % -% setup a focused pulse (VS) sequence +% setup a focused pulse (FC) sequence zf = 60e-3; % focal depth xf = (-40 : 1 : 40) * 1e-3; % focal point lateral positions pf = [0,0,1]'.*zf + [1,0,0]'.*xf; % focal points -seq = Sequence('type','VS', 'focus', pf); +seq = Sequence('type','FC', 'focus', pf); -% setup a walking transmit aperture focused pulse (VS) for a linear transducer +% setup a walking transmit aperture focused pulse (FC) for a linear transducer xdc = TransducerArray(); pn = xdc.positions(); % element positions Na = floor(xdc.numel/2); % active aperture size @@ -83,10 +89,10 @@ % get focal positions centered on the active aperture pf(:,i) = mean(pn(:, logical(apod(:,i))),2); end -seq = Sequence('type','VS','focus',pf); +seq = Sequence('type','FC','focus',pf); seq.apodization_ = apod; % set hidden apodization matrix -% setup a walking transmit aperture focused pulse (VS) for a curvilinear array +% setup a walking transmit aperture focused pulse (FC) for a curvilinear array xdc = TransducerConvex(); th = xdc.orientations(); % element azimuth angles (deg) Na = floor(xdc.numel/2); % active aperture size @@ -101,7 +107,7 @@ end rfocal = 60e-3; %% focal range seq = SequenceRadial( ... - 'type','VS', ... + 'type','FC', ... 'angles',tha, ... 'ranges',norm(xdc.center) + rfocal, ... 'apex',xdc.center ... @@ -171,10 +177,10 @@ % (incompatible with non-standard Sequences) b = DAS(us, chd); -% (coming soon) look-up table (LUT) delay-and-sum (DAS) +% look-up table (LUT) delay-and-sum (DAS) tau_tx = zeros([us.scan.size, chd.M]); % delay tensor: pixels x transmits tau_rx = zeros([us.scan.size, chd.N]); % delay tensor: pixels x receives -% b = bfDASLUT(us, chd, tau_tx, tau_rx); +b = bfDASLUT(us, chd, tau_tx, tau_rx); % eikonal equation beamformer (FSA only) b = bfEikonal(us, chd, med); @@ -187,7 +193,8 @@ b = bfEikonal(us, chd, med, cscan); % frequency-domain adjoint Green's function beamformer -% (poor performance on 'VS' sequences) +% Note: you may get poor performance on virtual source ('FC'/'DV'/'VS') +% sequences, convex arrays, or rotated/offset transducers b = bfAdjoint(us, chd); % Stolt's f-k-migration with FFT padding and output scan (PW only) @@ -195,6 +202,7 @@ uspw.seq = SequenceRadial('type', 'PW','angles',-10:1/4:10); % use plane waves instead chdpw = focusTx(uspw, chd); % focus FSA into PW pulses [b, bscan] = bfMigration(uspw, chdpw, 'Nfft', [2*chd.T, 4*chd.N]); +% image using this scan i.e. with `imagesc(bscan, b);` % ----------------- Aperture Reduction Functions --------------- % bn = DAS(us, chd, 'keep_rx', true); % first, preserve the receive dimension @@ -216,7 +224,8 @@ % set aperture growth to limit f# >= 1 a = us.apApertureGrowth(1); -% apply apodization when beamforming +% apply apodization when beamforming +% (works in most cases with most beamformers) b = DAS(us, chd, 'apod', a); %% Channel Data @@ -334,7 +343,7 @@ % create some example objects and data for this section us = UltrasoundSystem(); [scan, seq, xdc] = deal(us.scan, us.seq, us.xdc); -us.seq.pulse = Waveform('t0',-1/xdc.fc, 'tend',1/xdc.fc,'fun', @(t)sinpi(2*xdc.fc*t)); +us.seq.pulse = Waveform('t0',-1/xdc.fc, 'tend',1/xdc.fc, 'fun',@(t)sinpi(2*xdc.fc*t)); med = Medium(); scat = Scatterers('pos', [0,0,30e-3]'); chd = greens(us, scat); @@ -354,7 +363,7 @@ plot(xdc, 'r+'); % plot the surface of the transducer elements -patch(scale(xdc,'dist',1e3)); shading faceted; +patch(xdc); shading faceted; % plot the impulse response (affectssimulation only) plot(xdc.impulse); @@ -381,18 +390,18 @@ % loop through transmits of channel data h = imagesc(hilbert(chd)); colormap(h.Parent, 'jet'); -animate(h, chd.data, 'loop', false); +animate(chd.data, h, 'loop', false); % display a b mode image h = imagesc(us.scan, b); colormap(h.Parent,'gray'); % loop through frames/transmits/receives of images data -animate(h, b, 'loop', false); +animate(b, h, 'loop', false); % animate multiple plots together nexttile(); h(1) = imagesc(hilbert(chd)); colormap(h(1).Parent,'jet'); nexttile(); h(2) = imagesc(us.scan, b); colormap(h(2).Parent,'gray'); -hmv = animate(h, {chd.data, b}, 'loop', false); +hmv = animate({chd.data, b}, h, 'loop', false); % save animation to a gif % NOTE: MATLAB may have a bug causing frame sizing to be inconsistent @@ -429,6 +438,10 @@ % k-Wave chd = kspaceFirstOrder(us, med, cscan); +% run on a local or remote cluster +clu = parcluster('local'); +chd = kspaceFirstOrder(us, med, cscan, 'parenv', clu); + %% Waveforms (affects Simulation only) % Waveforms are used when simulating with an excitation function and/or a @@ -453,7 +466,7 @@ % ----------- Signal processing ------------ % % sample the waveform -tau = zeros([1024, 1]); +tau = (0 : 1023) .* 0.1e-6; y = wv.sample(tau); % convolve waveforms diff --git a/example.mlx b/example.mlx index 6575311b..cdf6e9a4 100644 Binary files a/example.mlx and b/example.mlx differ diff --git a/example_.m b/example_.m index 3cdae865..3f9b128e 100644 --- a/example_.m +++ b/example_.m @@ -1,10 +1,10 @@ %% QUPS - Quick Ultrasound Processing & Simulation %% What? -% QUPS is designed to be an accessible, verastile, shareable, lightweight codebase +% QUPS is designed to be an accessible, versatile, shareable, lightweight codebase % designed to make developing and running ultrasound algorithms quick and easy! % It offers a standardization of data formatting that serves most common pulse-echo -% ultrasound systems and supports homogeneous sound speed simulators such as FieldII -% and MUST as well as heterogeneous simulation programs such as k-Wave. +% ultrasound systems and supports scatterer simulators such as FieldII and MUST +% as well as finite difference simulation programs such as k-Wave. %% Why? % This package seeks to lower the barrier to entry for doing research in ultrasound % by offering a lightweight, easy to use package. Most of the underlying implementations @@ -14,17 +14,20 @@ %% % * Create linear, curvilinear (convex), and matrix transducers, as well as % custom transducers -% * Define full-synthetic-aperture (FSA), focused (VS), diverging, plane-wave +% * Define full-synthetic-aperture (FSA), focused (FC), diverging (DV), plane-wave % (PW), or arbitrary pulse sequences % * Simulate point targets natively or via MUST or FieldII % * Simulate distributed media via k-Wave -% * Filter, demodulate, downsample, and resample channel data +% * Filter, downmix (demodulate), downsample (decimate), and resample channel +% data % * Define arbitrary apodization schemes across transmits, receives, and pixels, % jointly % * Beamform using traditional delay-and-sum, eikonal delays (via the FMM toolbox), % an adjoint matrix method, or Stolt's migration % * Focus FSA data to some pulse sequence, or retrospectively refocus data back % to FSA +% * Compute coherence based images with techniques including SLSC, DMAS, and +% coherence factor %% % Please submit issues, feature requests or documentation requests via ! @@ -37,7 +40,7 @@ %#ok<*UNRCH> ignore unreachable code due to constant values %#ok<*BDLGI> ignore casting numbers to logical values -dev = -logical(gpuDeviceCount); % select 0 for cpu, -1 for gpu if you have one +gpu = logical(gpuDeviceCount); % set to false to remain on the cpu if ~exist('UltrasoundSystem.m','file') % this setup function adds the proper paths to use QUPS: it only needs % to be run once. @@ -59,24 +62,31 @@ scat = Scatterers('pos', 1e-3 * ps(:,:), 'c0', 1500); % targets every 5mm case 'diffuse', N = 1000; % number of random scatterers sz = 1e-3*[40;10;60]; % rectangular region size - off = -[0.5;0.5;0]; % uniform distribution offset - scat = Scatterers('pos', sz.*(rand([3,N]) + off), 'amp', rand([1,N]), 'c0', 1500); % diffuse scattering + cen = [true;true;false]; % uniform distribution offset + scat = Scatterers('pos', sz.*(rand([3,N]) - 0.5*cen), 'amp', rand([1,N]), 'c0', 1500); % diffuse scattering end % Choose a Transducer switch "L11-5v" - case 'L11-5v', xdc = TransducerArray.L11_5v(); % linear array - case 'L12-3v', xdc = TransducerArray.L12_3v(); % another linear array - case 'P4-2v', xdc = TransducerArray.P4_2v(); % a phased array - case 'C5-2v' , xdc = TransducerConvex.C5_2v(); % convex array + case 'L11-5v', xdc = TransducerArray.L11_5v(); % linear array + case 'L12-3v', xdc = TransducerArray.L12_3v(); % another linear array + case 'P4-2v', xdc = TransducerArray.P4_2v(); % a phased array + case 'C5-2v' , xdc = TransducerConvex.C5_2v(); % convex array + case 'PO192O', xdc = TransducerMatrix.PO192O(); % matrix array end xdc.impulse = xdc.ultrasoundTransducerImpulse(); % set the impulse response function % Define the Simulation Region -tscan = ScanCartesian('x', 1e-3*(-50 : 1/16 : 50), 'z', 1e-3*(-20 : 1/16 : 60)); +if isa(xdc, 'TransducerMatrix') + tscan = ScanCartesian('x', 1e-3*(-10 : 1/16 : 10), 'z', 1e-3*(-5 : 1/16 : 50)); + tscan.y = tscan.x; % 3D grid +else + tscan = ScanCartesian('x', 1e-3*(-50 : 1/16 : 50), 'z', 1e-3*(-20 : 1/16 : 60)); % 2D grid + tscan.y = 0; % 2D grid +end % point per wavelength - aim for >2 for a simulation -ppw = scat.c0 / xdc.fc / min([tscan.dx, tscan.dz]); +ppw = scat.c0 / xdc.fc / min([tscan.dx, tscan.dy, tscan.dz], [], 'omitnan'); % Define the Transmit Pulse Sequence @@ -97,7 +107,7 @@ zf = 60 ; xf = linspace( -10 , 10 , 11 ); % Focused (VS) Sequence pf = 1e-3*([1,0,0]'.*xf + [0,0,zf]'); - seq = Sequence('type', 'VS', 'focus', pf); + seq = Sequence('type', 'FC', 'focus', pf); case 'Sector' %% @@ -108,7 +118,7 @@ else, cen = [0;0;0]; end seq = SequenceRadial( ... - 'type', 'VS', ... + 'type', 'FC', ... 'angles', th, ... 'ranges', norm(cen) + 1e-3*r, ... 'apex', cen ... @@ -134,11 +144,6 @@ scan = ScanCartesian('x', xb, 'z', zb); [scan.dx, scan.dz] = deal(dr); - % label the axes - scan.xlabel = "Lateral (m)"; - scan.ylabel = "Elevation (m)"; - scan.zlabel = "Axial (m)"; - % For convex transducers only! elseif isa(xdc, 'TransducerConvex') % ranges @@ -150,10 +155,6 @@ 'r', norm(xdc.center) + r... ); % R x A scan - % label the axes - scan.rlabel = "Range (m)"; - scan.alabel = "Angle (^o)"; - scan.ylabel = "Elevation (m)"; end %% % @@ -166,10 +167,9 @@ % make a scatterer, if not diffuse if scat.numScat < 500 - s_rad = max([tscan.dx, tscan.dz]); % scatterer radius - nextdim = @(p) ndims(p) + 1; % helper function - ifun = @(p) any(vecnorm(p - swapdim(scat.pos,2,nextdim(p)),2,1) < s_rad, nextdim(p)); % finds all points within scatterer radius - med = Medium('c0', scat.c0, 'rho0', rho0, 'pertreg', {{ifun, [scat.c0, rho0*2]}}); + s_rad = max([tscan.dx,tscan.dy,tscan.dz],[],'omitnan'); % scatterer radius + ifun = @(p) any(vecnorm(p - swapdim(scat.pos,2,5),2,1) < s_rad, 5); % finds all points within scatterer radius + med = Medium('c0', scat.c0, 'rho0', rho0, 'pertreg', {{ifun, [scat.c0, rho0*2]'}}); else med = Medium('c0', scat.c0, 'rho0', rho0); end @@ -181,54 +181,46 @@ % Show the transmit signal - a single point means it's a delta function if false, figure; plot(seq.pulse, '.-'); title('Transmit signal'); end -% Plot configuration of the simulation +% +% Plot configuration of the simulation figure; hold on; title('Geometry'); % plot the medium -switch "sound-speed" - case 'sound-speed', imagesc(med, tscan, 'props', 'c' ); colorbar; % show the background medium for the simulation/imaging region - case 'density', imagesc(med, tscan, 'props', 'rho'); colorbar; % show the background medium for the simulation/imaging region -end - -% plot the transducer -plot(xdc, 'r+', 'DisplayName', 'Elements'); % elements +imagesc(med, tscan, 'props', "c"); colorbar; % show the background medium for the simulation/imaging region -% plot the imaging region (Scan) -hps = plot(scan, 'w.', 'MarkerSize', 0.5, 'DisplayName', 'Image'); % the imaging points +% Construct an UltrasoundSystem object, combining all of these properties +fs = single(ceil(2*us.xdc.bw(end)/1e6)*1e6); +us = UltrasoundSystem('xdc', xdc, 'seq', seq, 'scan', scan, 'fs', fs, 'recompile', false); -% plot the transmit pulse sequence -switch seq.type - case 'PW', plot(seq, 3e-2, 'k.', 'DisplayName', 'Tx Sequence'); % scale the vectors for the plot - otherwise, plot(seq, 'k.', 'DisplayName', 'Tx Sequence'); % plot focal points, if they exist -end +% plot the system +hold on; +hs = plot(us); % plot the point scatterers if scat.numScat < 500 - plot(scat, 'k', 'LineStyle', 'none', 'Marker', 'diamond', 'MarkerSize', 5, 'DisplayName', 'Scatterers'); % point scatterers + hs(end+1) = plot(scat, 'k', 'LineStyle', 'none', 'Marker', 'diamond', 'MarkerSize', 5, 'DisplayName', 'Scatterers'); % point scatterers else disp('INFO: Too many scatterers to display.'); end -hl = legend(gca, 'Location','bestoutside'); -set(gca, 'YDir', 'reverse'); % set transducer at the top of the image +hl = legend(gca, 'Location','bestoutside'); %% Simulate the Point Scatterer(s) %% % -% Construct an UltrasoundSystem object, combining all of these properties -us = UltrasoundSystem('xdc', xdc, 'seq', seq, 'scan', scan, 'fs', single(40e6), 'recompile', false); % Simulate a point target tic; switch "Greens" case 'Greens' , chd0 = greens(us, scat); % use a Greens function with a GPU if available!su-vpn.stanford.edu - case 'FieldII', chd0 = calc_scat_all(us, scat); % use FieldII, - case 'FieldII-multi', chd0 = calc_scat_multi(us, scat); % use FieldII, - case 'SIMUS' , us.fs = 4 * us.fc; % to address a bug in MUST where fs must be a ~factor~ of 4 * us.fc + case 'FieldII', chd0 = calc_scat_all(us, scat); % use FieldII to simulate FSA, then focus in QUPS + case 'FieldII-multi', + chd0 = calc_scat_multi(us, scat); % use FieldII to simulate sequence directly + case 'SIMUS', us.fs = 4 * us.fc; % to address a bug in early versions of MUST where fs must be a ~factor~ of 4 * us.fc chd0 = simus(us, scat, 'periods', 1, 'dims', 3); % use MUST: note that we have to use a tone burst or LFM chirp, not seq.pulse - case 'kWave', chd0 = kspaceFirstOrder(us, med, tscan, 'CFL_max', 0.5, 'PML', [64 128], 'parenv', 0, 'PlotSim', true); % run locally, and use an FFT friendly PML size + case 'kWave', chd0 = kspaceFirstOrder(us, med, tscan, 'CFL_max', 0.5, 'PML', [64 128], 'parenv', 0, 'PlotSim', true); % run locally, and use an FFT friendly PML size end toc; chd0 @@ -237,12 +229,12 @@ % display the channel data across the transmits figure; -h = imagesc(mod2db(chd0)); +h = imagesc(chd0); colormap jet; colorbar; cmax = gather(mod2db(max(chd0.data(:)))); % maximum power caxis([-80 0] + cmax); % plot up to 80 dB down -% animate(h, chd0.data, 'fs', 10, 'loop', false); % show all transmits -for i = 1:chd0.M, h.CData(:) = mod2db(chd0.data(:,:,i)); drawnow limitrate; pause(1/10); end % implement above manually for live editor +animate(chd0.data, h, 'fs', 10, 'loop', false); % show all transmits +% for i = 1:chd0.M, h.CData(:) = mod2db(chd0.data(:,:,i)); drawnow limitrate; pause(1/10); end % implement above manually for live editor %% Create a B-mode Image @@ -250,7 +242,8 @@ % % Precondition the data -chd = singleT(chd0); % use less data +chd = singleT(chd0); % convert to single precision to use less data +if gpu, chd = gpuArray(chd); end % move data to GPU % optionally apply a passband filter to retain only the bandwidth of the transducer D = chd.getPassbandFilter(xdc.bw, 25); % get a passband filter for the transducer bandwidth @@ -271,14 +264,15 @@ chd = downmix(chd, fmod); % downmix - this reduces the central frequency of the data chd = downsample(chd, floor(chd.fs / fmod)); % now we can downsample without losing information -else,fmod = 0; +else + fmod = 0; end % demodulate and downsample (by any whole number) -if dev, chd = gpuArray(chd); end % move data to GPU % Choose how to scale apodization: laterally or angularly switch class(xdc) - case 'TransducerArray' , scale = xdc.pitch; % Definitions in elements - case 'TransducerConvex', scale = xdc.angular_pitch; % Definitions in degrees + case 'TransducerArray' , scl = xdc.pitch; % Definitions in elements + case 'TransducerConvex', scl = xdc.angular_pitch; % Definitions in degrees + case 'TransducerMatrix', scl = min(xdc.pitch); % Definitions in elements end % Choose the apodization (beamforming weights) @@ -290,7 +284,7 @@ apod = 1; if false, apod = apod .* apMultiline(us); end if false, apod = apod .* apScanline(us); end -if false, apod = apod .* apTranslatingAperture(us, 32*scale); end +if false, apod = apod .* apTranslatingAperture(us, 32*scl); end if false, apod = apod .* apApertureGrowth(us, 2); end if false, apod = apod .* apAcceptanceAngle(us, 50); end @@ -298,17 +292,17 @@ % Choose a beamforming method bf_args = {'apod', apod, 'fmod', fmod}; % arguments for all beamformers -bscan = us.scan; -switch "DAS-direct" +bscan = us.scan; % default b-mode image scan +switch "DAS" case "DAS-direct" - b = DAS(us, chd, bf_args{:}); % use a specialized delay-and-sum beamformer + b = DAS( us, chd, bf_args{:}); % use a specialized delay-and-sum beamformer case "DAS" - b = bfDAS(us, chd, bf_args{:}); % use a generic delay-and-sum beamformer + b = bfDAS( us, chd, bf_args{:}); % use a generic delay-and-sum beamformer case "Adjoint" - b = bfAdjoint(us, chd, 'fthresh', -20, bf_args{:}); % use an adjoint matrix method, top 20dB frequencies only + b = bfAdjoint(us, chd, bf_args{:}, 'fthresh', -20); % use an adjoint matrix method, top 20dB frequencies only case "Eikonal" b = bfEikonal(us, chd, med, tscan, bf_args{:}); % use the eikonal equation - case "Stolts-f-k-Migration" + case "Migration" % Stolt's f-k Migrartion (no apodization accepted) % NOTE: this function works best with small-angle (< 10 deg) plane waves [b, bscan] = bfMigration(us, chd, 'Nfft', [2*chd.T, 4*chd.N], 'fmod', fmod); diff --git a/examples/import/import_verasonics_data.m b/examples/import/import_verasonics_data.m new file mode 100644 index 00000000..be7f34d2 --- /dev/null +++ b/examples/import/import_verasonics_data.m @@ -0,0 +1,83 @@ +% Example script for importing Verasonics Vantage data into QUPS +% +% This example walks through how to import verasonics data into a QUPS +% compatible format. The example data contains multiple transmit foci and +% involves receive aperture multiplexing captured on an L12-3v transducer +% on a Vantage UTA-260-D platform. +% +% There is 1 TX per 1 Receive. Since every TX is duplicated, only the first +% of each duplicate pair is used to define the Sequence in QUPS' framework. +% The joining of the receive apertures must be handled outside of the QUPS. +% +% Verasonics' programming guide is confidential - therefore, the data, +% generation, and usage of the relevant properties are not described. +% + +%% 0) Load the data +fn = 'DATA_L12_3v_MultiFocal.mat'; % filename +dat = load(which(fn)); % req'd: Trans, TX, Receive, RcvData | opt: TW, PData, Resource + +%% 1) Construct the UltrasoundSystem piece by piece + +% reference sound speed +if isfield(dat, 'Resource'), c0 = dat.Resource.Parameters.speedOfSound; +else, c0 = 1540; % default +end + +% Transducer (req'd: Trans, c0) +xdc = Transducer.Verasonics(dat.Trans, c0); +lbda = c0 / xdc.fc; % wavelengths + +% Scan (req'd: PData, units) +if isfield(dat, 'PData') % import + switch dat.Trans.units, case "mm", scl = 1; case "wavelengths", scl = lbda; end + scan = Scan.Verasonics(dat.PData, scl); +else % declare + pn = xdc.positions; + scan = ScanCartesian('xb', pn(1,[1,end]), 'zb', [0 40e-3]); +end + +% Sequence (req'd: TX, Trans, c0 | opt: TW) +j = 1:2:numel(dat.TX); % data has multiplexing, so we only need every other TX +k = unique([dat.TX(j).waveform]); % select the appropriate transmit waveform +[seq, t0q] = Sequence.Verasonics(dat.TX(j), dat.Trans, dat.TW(k), 'c0', c0); % import +if isnan(t0q), t0q = 0; warning("Sequence import validation failed!"); end % validate the import + +% UltrasoundSystem +us = UltrasoundSystem('xdc', xdc, 'seq', seq, 'scan', scan); + +%% 2) Construct the ChannelData +% import 1 frame of data (req'd: RcvData, Receive | opt: Trans) +[chd, fmod] = ChannelData.Verasonics(dat.RcvData, dat.Receive, dat.Trans, 'frames', 1); + +% Fix the time axes (assumes Sequence import was valid) +tlens = - 2*d.Trans.lensCorrection / xdc.fc; % lens correction +chd.t0 = t0q + seq.pulse.t0 + tlens; % beamforming delay corrections + +%% 3) Tweak the output to handle any multiplexing +% We need to handle the rx-aperture multiplexing manually. This particular +% dataset multiplexes the receive aperture for an L12-3v with 192 elements +% connected to a UTA-260-D with 128 channels. The middle 64 elements +% overlap. +% +% We can either truncate the data to a 'left' and 'right' aperture of 96 +% elements each or average the 64 overlapping elements. + +switch "trunc" + case "trunc" % truncation method + [l, r] = deal(1:96, 97:192); % left and right halves + chd.data = cat(3, chd.data(:,1:2:end,l,:), chd.data(:,2:2:end,r,:)); % truncate and combine + + case "avg" % overlap averaging method + chd.data = chd.data(:,1:2:end,:,:) + chd.data(:,2:2:end,:,:); % sum + i = [true(1,64), false(1,64), true(1,64)]; % non-overlap region + chd.data(:,i,:,:) = 2 * chd.data(:,i,:,:); % double the non-overlapped region (equivalent to halving the overlap) +end + + + + + + + + diff --git a/examples/pulse-sequence-design/sequence_types.m b/examples/pulse-sequence-design/sequence_types.m index e09904e4..bf42ba72 100644 --- a/examples/pulse-sequence-design/sequence_types.m +++ b/examples/pulse-sequence-design/sequence_types.m @@ -54,12 +54,12 @@ % construct a plane wave pulse sequence seqp(i) = SequenceRadial('type','PW','angles', -10 : 0.5 : 10); - % construct a focus pulse sequence - seqf(i) = Sequence('type','VS', 'focus', pf); + % construct a focused pulse sequence + seqf(i) = Sequence('type','FC', 'focus', pf); seqf(i).apodization_ = aptx; % set the apodization explicitly % construct a diverging pulse sequence - seqv(i) = Sequence('type','VS', 'focus', pdv); + seqv(i) = Sequence('type','DV', 'focus', pdv); seqv(i).apodization_ = aptx; % set the apodization explicitly end @@ -109,8 +109,7 @@ % beamforming options fnbr = 1; % set the acceptance angle with an f# -bi = cell(size(us)); % pre-allocate -for i = 1:numel(us) +for i = numel(us):-1:1 tic, % create the receive apodization matrix a = us(i).apAcceptanceAngle(atand(0.5/fnbr)); @@ -119,7 +118,7 @@ bi{i} = DAS(us(i), chd(i), 'apod', a); % normalize to the number of pulses (for comparable scaling) - bi{i} = bi{i} ./ sqrt(us(i).seq.numPulse); + bi{i} = bi{i} ./ sqrt(us(i).seq.numPulse); toc, end @@ -177,7 +176,9 @@ end % apodization matrix (elems x txs) -aptx = cell2mat(arrayfun(@(i) {circshift([true(M,1);false(N-M,1)],i,1)}, 0 : S : N - M)); +aptx = [true(M,1); false(N-M,1)]; +aptx = (arrayfun(@(i) {circshift(aptx,i,1)}, 0 : S : N - M)); +aptx = cell2mat(aptx); end diff --git a/examples/simulation/multilayer_media.m b/examples/simulation/multilayer_media.m index 313cea00..8ae6ca3c 100644 --- a/examples/simulation/multilayer_media.m +++ b/examples/simulation/multilayer_media.m @@ -93,7 +93,7 @@ %% Show the channel data figure('Name', 'Simulation Channel Data'); him = imagesc(hilbert(chd0)); % show magnitude of the data in dB -animate(him, hilbert(chd0).data, 'loop', false, 'fs', 5); % make a short animation across transmits +animate(hilbert(chd0).data, him, 'loop', false, 'fs', 5); % make a short animation across transmits %% Beamform into a b-mode image diff --git a/kern/beamform.m b/kern/beamform.m index b1eac0f8..042f8d43 100644 --- a/kern/beamform.m +++ b/kern/beamform.m @@ -37,6 +37,11 @@ % y = BEAMFORM(..., 'plane-waves', ...) uses a plane-wave delay model % instead of a virtual-source delay model (default). % +% y = BEAMFORM(..., 'diverging-waves', ...) specifies a diverging-wave +% delay model, in which the time delay is always positive rather than +% negative prior to reaching the virtual source in the virtual-source delay +% model (default). +% % In a plane-wave model, time t == 0 is when the wave passes through the % origin of the coordinate system. Pv is the origin of the coordinate % system (typically [0;0;0]) and Nv is the normal vector of the plane wave. @@ -44,7 +49,9 @@ % In a virtual-source model, time t == 0 is when the wavefront (in theory) % passes through the focus. For a full-synthetic-aperture (FSA) % acquisition, Pv is the position of the transmitting element. In a focused -% or diverging wave transmit, Pv is the focus. Nv is ignored. +% or diverging wave transmit, Pv is the focus. For a focused wave, Nv is +% used to determine whether the theoretical wavefront has reached the focal +% point. For a diverging wave, Nv is ignored. % % y = BEAMFORM(..., 'apod', apod, ...) applies apodization across the image % and data dimensions. apod must be able to broadcast to size @@ -81,7 +88,8 @@ % TODO: switch to kwargs struct to support arguments block % default parameters -VS = true; +VS = true; % whither plane wave +DV = false; % whither diverging wave interp_type = 'linear'; apod = 1; isType = @(c,T) isa(c, T) || isa(c, 'gpuArray') && strcmp(classUnderlying(c), T); @@ -91,6 +99,8 @@ idataType = 'double'; elseif isType(x, 'halfT') idataType = 'halfT'; +else + idataType = 'double'; % default end if any(cellfun(@(c) isType(c, 'single'), {Pi, Pr, Pv, Nv})) posType = 'single'; @@ -113,6 +123,10 @@ VS = false; case 'virtual-source' VS = true; + case 'diverging-waves' + DV = true; + case 'focused-waves' + DV = false; case 'position-precision' n = n + 1; posType = varargin{n}; @@ -275,7 +289,7 @@ % set constant args k.setConstantMemory('QUPS_I', I); % gauranteed try k.setConstantMemory('QUPS_T', T); end % if not const compiled with ChannelData - try k.setConstantMemory('QUPS_M', M, 'QUPS_N', N, 'QUPS_VS', VS, 'QUPS_I1', Isz(1), 'QUPS_I2', Isz(2), 'QUPS_I3', Isz(3)); end % if not const compiled + try k.setConstantMemory('QUPS_M', M, 'QUPS_N', N, 'QUPS_VS', VS, 'QUPS_DV', DV, 'QUPS_I1', Isz(1), 'QUPS_I2', Isz(2), 'QUPS_I3', Isz(3)); end % if not const compiled % set kernel size k.ThreadBlockSize = nThreads; @@ -354,7 +368,8 @@ % transmit sensing vector rv = Pi - Pv; % 3 x I1 x I2 x I3 x 1 x M if VS % virtual source delays - dv = vecnorm(rv, 2, 1) .* sign(sum(rv .* Nv,1)); + if DV, s = 1; else, s = sign(sum(rv .* Nv,1)); end % diverging or focused + dv = vecnorm(rv, 2, 1) .* s; else % plane-wave delays dv = sum(rv .* Nv, 1); end % 1 x I1 x I2 x I3 x 1 x M diff --git a/kern/cohfac.m b/kern/cohfac.m index 50314bfc..9840305f 100644 --- a/kern/cohfac.m +++ b/kern/cohfac.m @@ -52,7 +52,7 @@ % % linkaxes(ax); % set both axes to scroll together % -% See also SLSC +% See also DMAS SLSC PCF arguments b {mustBeNumeric} diff --git a/kern/dmas.m b/kern/dmas.m index 1e66daae..8f967d35 100644 --- a/kern/dmas.m +++ b/kern/dmas.m @@ -1,13 +1,23 @@ -function b = dmas(bn, dim) +function b = dmas(bn, dim, L) % DMAS - Delay-Multiply-And-Sum (DMAS) % % b = DMAS(bn) computes the delay-multiply-and-sum image b from the b-mode -% image per receiver bn. +% image per receive element bn. % % b = DMAS(bn, dim) specifies the receive dimension of bn. The default is % the last non-singular dimension of bn. +% +% b = DMAS(bn, dim, lags) specifies the element lags to be included. The +% default is (1 : size(bn,dim) - 1). % -% About: Delay-multiply-and-sum is a contrast enhancement method. +% b = DMAS(bn, dim, L) where L is a scalar selects lags 1:L. To select only +% lag L, specify an array which includes 0 as in lags = [0, L]. +% +% About: Delay-multiply-and-sum is a contrast enhancement method based on +% correlations between pairs of signals across the aperture. +% +% For complex numbers, this implementation preserves the phase in the +% multiplication stage, which is the complex analog of preserving the sign. % % References: % [1] G. Matrone, A. S. Savoia, G. Caliano and G. Magenes, @@ -20,7 +30,6 @@ % us = UltrasoundSystem(); % get a default system % us.scan = ScanCartesian('x', (-20 : 20)*1e-3, 'z', (0 : 50) * 1e-3); % [us.scan.dx, us.scan.dz] = deal(us.lambda / 8); -% % % % Generate Scatterers % scat = Scatterers('pos', 1e-3*[0,0,30]', 'c0', us.sequence.c0); @@ -53,18 +62,19 @@ % ylim([ 27.5 32.5]*1e-3); % caxis(max(cax1(2), cax2(2)) + [-60 0]); % 60dB dynamic range % -% See also SLSC COHFAC +% See also SLSC COHFAC PCF arguments bn {mustBeNumeric} dim {mustBeInteger, mustBePositive} = find(size(bn) ~= 1, 1, 'last'); + L (1,:) {mustBeInteger, mustBeNonnegative} = 1:size(bn, dim)-1 % lags end b = 0; % init N = size(bn, dim); % aperture length -for i = 1:N-1 % multiply and sum along all non-identical pairs +if isscalar(L), lags = 1:L; else; lags = intersect(1:N-1, L); end +for i = lags % multiply and sum along all non-identical pairs b = b + sum(sub(bn,1:N-i,dim) .* sub(bn,1+i:N,dim), dim); - him.CData(:) = mod2db(b); end % re-scale the amplitude, preserve the phase (sign) diff --git a/kern/pcf.m b/kern/pcf.m new file mode 100644 index 00000000..d39e4972 --- /dev/null +++ b/kern/pcf.m @@ -0,0 +1,109 @@ +function [w, sf] = pcf(b, dim, gamma, kwargs) +% PCF - Phase Coherence Factor +% +% w = PCF(b) computes the phase coherence factor w from the pre-sum +% image b. +% +% w = PCF(b, dim) operates across dimension dim. The default is the last +% non-singular dimension of b. +% +% w = PCF(b, dim, gamma) adjusts the out-of-focus sensitivity. Larger +% values of gamma provide more suppresion. The default is 1. +% +% w = PCF(..., 'unwrap', 'auxiliary') uses the auxiliary phase described in +% [1] to unwrap the phase prior to computing the phase diversity. This is +% the default. +% +% [w, sf] = PCF(...) additionally returns the phase diversity estimate sf +% in radians. +% +% About: The phase coherence factor is a metric of the variance in the +% phase across the aperture. In medical ultrasound, it can been used to +% weight the aperture to improve image quality. +% +% References: +% [1] J. Camacho, M. Parrilla and C. Fritsch, "Phase Coherence Imaging," +% in IEEE Transactions on Ultrasonics, Ferroelectrics, and Frequency Control, +% vol. 56, no. 5, pp. 958-974, May 2009, +% doi: 10.1109/TUFFC.2009.1128 +% +% Example: +% % Choose a transducer +% xdc = TransducerConvex.C5_2v(); +% +% % Create a sector scan sequence +% th = asind(linspace(sind(-35), sind(35), 192)); % linear spacing in sin(th) +% rf = xdc.radius + 50e-3; % 50mm focal depth (w.r.t. center of convex radius) +% atx = abs(xdc.orientations()' - th) <= 30; % active aperture of +/- 30 degrees +% seq = SequenceRadial('type', 'VS', 'ranges', rf, 'angles', th, 'apex', xdc.center); +% seq.apodization_ = atx; % transmit apodoization +% +% % Create a sector scan imaging region +% scan = ScanPolar('rb', xdc.radius + [0, rf], 'a', th, 'origin', xdc.center); % along the scan lines +% us = UltrasoundSystem('seq', seq, 'xdc', xdc, 'scan', scan, 'fs', single(4*xdc.fc)); % get a default system +% us.scan.dr = us.lambda / 4; % set the imaging range resolution +% +% % Generate Scatterers +% pos = 1e-3.*[sind(-10) 0 cosd(-10)]'*(5 : 5 : 30); % scatterer positions +% scat = Scatterers('pos', pos, 'c0', seq.c0); +% +% % Compute the image +% chd = greens(us, scat); % compute the response +% b = DAS(us, chd, 'keep_rx', true); % beamform the data, keeping the receive aperture +% rxdim = ndims(b); % rx is the last dimension +% +% % Compute the Phase Coherence Factor across the receive aperture +% w = pcf(b, rxdim); +% +% % Display the images +% bs = {sum(b,rxdim), w, sum(b.*w,rxdim)}; % images +% ttls = ["B-mode", "Phase Coherence Factor (PCF)", "PCF-weighted B-mode"]; % titles +% figure; +% colormap gray; +% for i = 1 : numel(bs) +% nexttile(); +% imagesc(us.scan, bs{i}); +% title(ttls(i)); +% colorbar; +% end +% +% See also SLSC DMAS COHFAC + +arguments + b {mustBeNumeric, mustBeComplex} + dim (1,1) double {mustBeInteger, mustBePositive} = max([1, find(size(b)~=1, 1, 'last')]); + gamma (1,1) double {mustBeReal} = 1 + kwargs.unwrap (1,1) string {mustBeMember(kwargs.unwrap, ["auxiliary"])} = "auxiliary"; +end + +switch kwargs.unwrap + case "auxiliary" + % compute the phase and it's standard deviation + phi = angle(b); % phase (radians) + s0 = std(phi,1,dim,"omitnan"); % ref std. + + % compute the auxiliary phase and it's standard deviation + phi = phi - pi * sign(phi); + sa = std(phi,1,dim,"omitnan"); + + % get scaling factor as the lesser standard deviation + sf = min(s0, sa, "omitnan"); + otherwise + % TODO: add smarter methods of accounting for wrap-around + % + % Method 1) + % a) sort data then + % b) iteratively + % b-i) wrap min value + % b-ii) get std + % c) then take min std over all from (b) + +end + +% standard deviation of the distribution U(-pi, pi) +sg0 = sqrt(pi/3); + +% phase coherence factor for scaling the image +w = max(0, 1 - (gamma / sg0) .* sf); + +function mustBeComplex(b), if isreal(b), throwAsCaller(MException("QUPS:pcf:realInput","Input must be complex.")); end \ No newline at end of file diff --git a/kern/slsc.m b/kern/slsc.m index ec87a120..d0039f9e 100644 --- a/kern/slsc.m +++ b/kern/slsc.m @@ -15,9 +15,9 @@ % z = SLSC(x, dim, L, method) uses the specified method. Must be one % of {"ensemble" | "average"*}. % -% z = SLSC(x, dim, L, method, kdim) interprets kdim as the time dimension. -% When dimension kdim is non-singular, samples in time are treated as part -% of the same correlation. +% z = SLSC(x, dim, L, method, kdim) interprets kdim as the time kernel +% dimension. When dimension kdim is non-singular, samples in time are +% treated as part of the same correlation. % % About: % The short-lag spatial coherence measures how similar signals are across a @@ -38,7 +38,7 @@ % vol. 58, no. 7, pp. 1377-1388, July 2011. % doi: 10.1109/TUFFC.2011.1957 % -% Example: +% Example 1: % % This example requires kWave % if ~exist('kWaveGrid', 'class') % warning('kWave must be on the path to run this example.'); @@ -62,36 +62,48 @@ % end % med = Medium.Sampled(sscan, c, rho, 'c0', 1500); % -% % Setup a compute cluster -% clu = parcluster('local'); -% clu.NumWorkers = 1 + 2*gpuDeviceCount; -% clu.NumThreads = 4*gpuDeviceCount; -% % % Simulate the ChannelData -% if gpuDeviceCount, dtype = 'gpuArray-single'; -% else, dtype = 'single'; end -% [job, rfun] = kspaceFirstOrder(us, med, sscan, 'DataCast', dtype, 'parenv', clu); -% submit(job); % launch -% wait(job); -% chd = rfun(job); % read data into a ChannelData object +% chd = kspaceFirstOrder(us, med, sscan); % % % Pre-process the data % chd = hilbert(chd); % % % generate an image, preserving the receive dimension -% lambda = med.c0 / us.xdc.fc; % wavelength -% us.scan.dz = lambda / 4; % axial resolution -% us.scan.dx = lambda / 4; % lateral resolution -% b = DAS(us, chd, 'keep_rx', true); -% mdim = ndims(b); % the last dimension is the receiver dimension +% us.scan.dz = us.lambda / 4; % axial resolution +% us.scan.dx = us.lambda / 4; % lateral resolution +% b = DAS(us, chd, 'keep_rx', true); % preserve the receive aperture dimension +% mdim = ndims(b); % the last dimension is the receive aperture dimension % % % compute and show the SLSC image % z = slsc(b, mdim); % figure; % imagesc(us.scan, real(z)); % colorbar; +% +% % Example 2: +% % image with a small time kernel +% K = 3; % one-sided time kernel size +% t0 = chd.t0; % original start time +% dt = 1/chd.fs; % time shift +% for k = -K:K +% chd.t0 = t0 + k*dt; % shift time axis +% bk{k+K+1} = DAS(us, chd, 'keep_rx', true); % preserve the receive aperture dimension +% end +% chd.t0 = t0; % restore time axis +% mdim = ndims(bk{1}); % receive aperture dimension +% kdim = ndims(bk{1}) + 1; % find a free dimension +% bk = cat(kdim, bk{:}); +% +% % compute SLSC with a time kernel +% L = floor(us.rx.numel/4); % maximum lag +% zk = slsc(bk, mdim, L, "average", kdim); +% +% % show the SLSC image +% figure; +% imagesc(us.scan, real(zk)); +% colorbar; % -% See also COHFAC +% See also DMAS COHFAC PCF % defaults arguments @@ -113,20 +125,15 @@ A = gather(size(x,dim)); % the full size of the aperture [M, N] = ndgrid(1:A, 1:A); H = abs(M - N); % lags for each receiver/cross-receiver pair (M x M') -lags = L; % if isscalar(L), lags = 1:L; else, lags = L; end % chosen lags for adding (i.e. the short lags) +if isscalar(L), lags = 1:L; else, lags = L; end % chosen lags for adding (i.e. the short lags) S = ismember(H,lags); % selection mask for the lags (M x M') - true if (rx, xrx) is a short lag L = numel(lags); % number of (active) lags - this normalizes the sum of the average estimates -K = gather(size(x,kdim)); % number of samples in time % choose average or ensemble switch method case "average" - % normalize magnitude per time sample (averaging) - x = x ./ vecnorm(x,2,kdim) / K; - x(isnan(x)) = 0; % 0/0 -> 0 - - % TODO: test if norm(x) close to zero instead of assuming all nans - % are from computing 0/0 + % normalize magnitude per time sample (averaging) with 0/0 -> 0 + x = nan2zero(x ./ vecnorm(x,2,kdim)); % get weighting / filter across receiver pairs W = S ./ (A - H) / 2 / L; % weights per pair (debiased, pos & neg averaged, multi-correlation-averaged) @@ -159,8 +166,7 @@ b = b + sum(w .* conj(xc) .* xc, [dim, kdim],'omitnan'); % b-norm end - % normalize by the norm of the vector - z = z .* rsqrt(a) .* rsqrt(b); + % normalize by the norm of the vector with 0/0 -> 0 + z = z .* nan2zero(rsqrt(a) .* rsqrt(b)); end end - diff --git a/kern/wsinterpd2.m b/kern/wsinterpd2.m index d4d65fbf..1519a146 100644 --- a/kern/wsinterpd2.m +++ b/kern/wsinterpd2.m @@ -165,9 +165,9 @@ end % zeros: uint16(0) == storedInteger(half(0)), so this is okay % index label flags - iflags = zeros([1 maxdims], 'uint8'); - iflags(mdms) = 1; - iflags(rdmsx) = 2; + iflags = zeros([1 maxdims], 'uint8'); % increase index i + iflags(mdms) = 1; % increase index n + iflags(rdmsx) = 2; % increase index f % strides strides = cat(1,wstride,ystride,t1stride,t2stride,xstride); diff --git a/src/ChannelData.m b/src/ChannelData.m index fc38d783..865c4ef6 100644 --- a/src/ChannelData.m +++ b/src/ChannelData.m @@ -7,14 +7,13 @@ % QUPS. Most methods that affect the time axes, such as zeropad or filter, % will shift the time axes accordingly. % -% The underlying datacube can be N-dimensional as long as the first -% dimension is time. The second and third dimensions should be receivers -% and transmits respectively to be compatible with QUPS. All data must -% share the same sampling frequency fs, but the start time t0 may vary -% across any dimension(s) except for the first and second dimensions. For -% example, if each transmit has a different t0, this can be represented by -% an array of size [1,1,M]. -% +% The underlying datacube can be N-dimensional as long as the first three +% dimensions contain the (fast) time, receive and transmit dimensions. All +% data must share the same sampling frequency fs, but the start time t0 may +% vary across any dimension(s) except for the time and receiver dimensions. +% For example, if each transmit has a different t0, this can be represented +% by an array of size [1,1,M] where M is the number of transmits. +% % The underlying numeric type of the data can be cast by appending 'T' to % the type (e.g. singleT(chd) produces a ChannelData) whereas the datacube % itself can be cast using the numeric type constructor (e.g. single(chd) @@ -23,7 +22,7 @@ % type. This enables MATLABian casting rules to apply to the object, which % can be used by other functions. % -% See also SEQUENCE TRANSDUCER +% See also SEQUENCE TRANSDUCER ULTRASOUNDSYSTEM classdef ChannelData < matlab.mixin.Copyable @@ -162,6 +161,9 @@ switch seq.type case 'FSA', t0 = t0 - vecnorm(xdc.positions,2,1) ./ seq.c0; % delay transform from element to origin for FSA case 'VS', t0 = t0 - vecnorm(seq.focus, 2,1) ./ seq.c0; % transform for focal point to origin + case 'FC', t0 = t0 - vecnorm(seq.focus, 2,1) ./ seq.c0; % transform for focal point to origin + case 'DV', warning("QUPS:UFF:unvalidatedTransform", "Unvalidated import: please validate."); % TODO: validate + t0 = t0 + vecnorm(seq.focus, 2,1) ./ seq.c0; % transform for focal point to origin case 'PW' % no action necessary end @@ -176,6 +178,173 @@ 'order', 'TNM' ... ); end + + function [chd, fmod, smode] = Verasonics(RcvData, Receive, Trans, kwargs) + % VERASONICS - Construct ChannelData from a Verasonics struct + % + % chd = ChannelData.Verasonics(RcvData, Receive) constructs an + % array of ChannelData chd for each receive buffer referenced + % in the Verasonics 'RcvData' and 'Receive' struct. The data + % has size (time x acq x channel x frame). + % + % Within each buffer, the sampling frequency, samples per + % acquisition, demodulation frequency, and the number of + % acquisitions per frame must be constant for each buffer. + % + % chd = ChannelData.Verasonics(RcvData, Receive, Trans) maps + % channels to transducer elements and returns the data with + % size (time x acq x elem x frame). + % + % [chd, fmod] = ChannelData.Verasonics(...) additionally + % returns an array of the demodulation frequencies fmod. + % + % [chd, fmod, smode] = ChannelData.Verasonics(...) additionally + % returns an array the sample modes for each buffer. + % + % [...] = ChannelData.Verasonics(..., 'frames', f) specfies the + % frames. The default is unique([Receive.framenum]). + % + % [...] = ChannelData.Verasonics(..., 'buffer', b) specfies + % the buffer indices b corresponding to each element of + % RcvData. The default is unique([Receive.bufnum], 'stable'). + % + % [...] = ChannelData.Verasonics(..., 'insert0s', false) + % disables 0-insertion to replace missing samples when the + % buffer sample mode is one of ["BS100BW", "BS67BW", "BS50BW"]. + % The default is true. + % + % Example: + % chd = ChannelData.Verasonics(RcvData, Receive, Trans); + % chd = hilbert(singleT(chd)); + % figure; animate(chd.data, 'fs', 5); + % + % See also SEQUENCE.VERASONICS WAVEFORM.VERASONICS + arguments + RcvData cell + Receive struct + Trans struct {mustBeScalarOrEmpty} = struct.empty + kwargs.buffer (1,:) {mustBeNumeric, mustBeInteger} = unique([Receive.bufnum], 'stable') + kwargs.frames (1,:) {mustBeNumeric, mustBeInteger} = unique([Receive.framenum]) + kwargs.insert0s (1,1) logical = true + end + + % validate the RcvData + cellfun(@mustBeNumeric, RcvData); + + % for each buffer + B = numel(kwargs.buffer); + for i = B:-1:1 + % get relevant receive info + b = kwargs.buffer(i); + Rx = Receive((b == [Receive.bufnum]) & ismember([Receive.framenum], kwargs.frames)); % filter by buffer and frame + if isempty(Rx) + warning("No data found for buffer " + kwargs.buffer(i) + "."); + [smode(i), fmod(i), chd(i)] = deal("N/A", nan, ChannelData('order','TMNF')); + continue; + end + + % constants + fs = unique([Rx.decimSampleRate]); % get sampling frequency + fm = unique([Rx.demodFrequency ]); % get demodulation frequency + fr = unique([Rx.framenum]); % frames + sm = unique({Rx.sampleMode}); % sampling mode + F = numel(fr); % number of frames + + % validate sample mode + if isscalar(sm), sm = string(sm); + else , sm = "N/A"; + warning( ... + "QUPS:Verasonics:InconsistentAcquisitionSize", ... + "Buffer " + kwargs.buffer(i) + " contains multiple sample modes." ... + ) + end + + % validate sizing + dupCnt = @(x) unique(groupcounts(x(:))); % count duplicates + if any(F ~= cellfun(dupCnt, {[Rx.acqNum], [Rx.startSample], [Rx.endSample]})) + error( ... + "QUPS:Verasonics:InconsistentAcquisitionSize", ... + "Unable to parse buffer " + kwargs.buffer(i) + "." ... + + " The number of acquisitions and the acquisition sample indices must be constant across all frames." ... + ); + end + + % validate ordering + Rx = reshape( Rx, [], F); % -> acquisitions by frames + fnm = reshape([Rx.framenum], size(Rx)); + acq = reshape([Rx.acqNum ], size(Rx)); + A = numel(unique(acq)); % number of acquisitions + if ~(all(acq == acq(:,1),'all') && all(fnm == fnm(1,:),'all')) + error( ... + "QUPS:Verasonics:InconsistentAcquisitionOrder", ... + "Unable to parse buffer " + b + "." ... + + " The acquisition numbers and frame numbers must be separable" ... + + " when formed as a " + A + " x " + F + " array." ... + ); + end + + j = cellfun(@colon, {Rx.startSample}, {Rx.endSample}, 'UniformOutput', false); % sample indices + j = unique([j{:}], 'stable'); % should be identical across acquisitions + + % load data (time x acq x channel x frame) + x = RcvData{i}(j,:,fr); % only extract the filled portion + x = reshape(x, [], A, size(x,2), size(x,3)); % (T x A x Np x F) + + % if Trans exists, make chd.N match Trans + if ~isempty(Trans) + % get aperture indexing + if isfield(Rx, 'aperture') + aps = Trans.HVMux.ApertureES; + as = reshape([Rx.aperture], size(Rx)); % apertures + if(~all(as(:,1) == as, 'all')) + error( ... + "QUPS:Verasonics:InconsistentAcquisitionOrder", ... + "Unable to parse buffer " + b + "." ... + + " The apertures must be identical across frames." ... + ); + end + as = as(:,1); + else + aps = Trans.ConnectorES; + as = 1; + end + + % pre-allocate output + ysz = size(x); + ysz(2) = Trans.numelements; + y = zeros(ysz, 'like', x); + + % load into output + for a = unique(as)' % for each aperture + j = a == as; % matching aperture + k = aps(:,a); % channel indices + y(:,j,k~=0,:) = x(:,j,k(k~=0),:); % load + end + x = y; % (time x acq x elem x frame) + end + + % transform the sampled data depending on the sample mode + if kwargs.insert0s + switch sm + case "NS200BW", [N, K] = deal(0, 1); % fully sampled - insert N=0 0s every K=1 samples + case "BS100BW", [N, K] = deal(2, 2); % [1,1,0,0,] - insert N=2 0s every K=2 samples + case "BS67BW", [N, K] = deal(2, 1); % [1,0,0,] - insert N=2 0s every K=1 samples + case "BS50BW", [N, K] = deal(6, 2); % [1,1,0,0,0,0,0,0,] - insert N=6 0s every K=2 samples + otherwise, [N, K] = deal(0, 1); % unknown - insert N=0 0s every K=1 samples + end + dsz = size(x); % data size + x = reshape(x, [K, dsz(1)/K, dsz(2:end)]); % set sample singles/pairs in dim 1 + x(end+(1:N),:) = 0; % insert N 0s + x = reshape(x, [(K+N) * (dsz(1)/K), dsz(2:end)]); % return to original dimensions + end + + % construct ChannelData + % TODO: account for different sample modes + chd(i) = ChannelData('data', x, 'fs', 1e6*fs, 'order', 'TMNF'); + fmod(i) = fm; % assume only one demod frequency + smode(i) = sm; % assume only one sample mode + end + end end % helper functions @@ -496,15 +665,18 @@ if nargin < 3, dim = chd.tdim; end if nargin < 2 || isempty(N), N = size(chd.data, dim); end - % use MATLAB's optimized implementation - % chd = applyFun2Dim(chd, @hilbert, chd.tdim, varargin{:}); - - % apply natively to support half type - chd = fft(chd, N, chd.tdim); % send to freq domain - Nd2 = floor(N/2); % number of postive/negative frequencies - w = [1; 2*ones([Nd2-1,1]); 1+mod(N,2); zeros([N-Nd2-1,1])]; % hilbert weight vector - chd.data = chd.data .* shiftdim(w, 1-chd.tdim); % apply - chd = ifft(chd, N, chd.tdim); + if chd.tdim == 1 && ~isa(chd.data, 'halfT') + % use MATLAB's optimized implementation + chd = copy(chd); % copy semantics + chd.data = hilbert(chd.data, N); % in-place + else + % otherwise apply natively to support all types + chd = fft(chd, N, chd.tdim); % send to freq domain + Nd2 = floor(N/2); % number of postive/negative frequencies + w = [1; 2*ones([Nd2-1,1]); 1+mod(N,2); zeros([N-Nd2-1,1])]; % hilbert weight vector + chd.data = chd.data .* shiftdim(w, 1-chd.tdim); % apply + chd = ifft(chd, N, chd.tdim); + end end function chd = fft(chd, N, dim) % FFT - overload of fft @@ -992,7 +1164,7 @@ % See also IMAGESC arguments chd ChannelData - m {mustBeInteger} = floor((chd.M+1)/2); + m {mustBeInteger} = ceil(chd.M/2); end arguments(Repeating) varargin @@ -1156,13 +1328,14 @@ function gif(chd, filename, h, varargin) % identical in dimension dim for all ChannelData objects. % % See also CHANNELDATA/SPLICE - + + if isempty(chds), sz=size(chds); sz(dim)=1; chd=reshape(chds, sz); return; end % trivial case assert(isalmostn([chds.fs], repmat(median([chds.fs]), [1,numel(chds)]))); % sampling frequency must be identical assert(all(string(chds(1).order) == {chds.order})); % data orders are identical T_ = max([chds.T]); % maximum length of data chds = arrayfun(@(chds) zeropad(chds,0,T_ - chds.T), chds); % make all the same length - chd = ChannelData('t0', cat(dim,chds.t0), 'fs', median([chds.fs]), 'data', cat(dim, chds.data)); % combine + chd = ChannelData('t0', cat(dim,chds.t0), 'fs', median([chds.fs]), 'data', cat(dim, chds.data), 'order', chds(1).order); % combine if all(chd.t0 == sub(chd.t0,1,dim),'all'), chd.t0 = sub(chd.t0,1,dim); end % simplify for identical t0 end @@ -1178,6 +1351,11 @@ function gif(chd, filename, h, varargin) % chds = SPLICE(chd, dim, bsize) uses a maximum block size of % bsize to partition the ChannelData objects. The default is 1. % + % [chds, ix] = SPLICE(...) also returns the indices for each + % ChannelData in the spliced dimension. For example, if chd is + % spliced with a block size of 2 over a dimension of length 5, + % ix = {[1 2], [3 4], [5]}. + % % Example: % % % Create data diff --git a/src/Scan.m b/src/Scan.m index 718844bb..ef55cf38 100644 --- a/src/Scan.m +++ b/src/Scan.m @@ -388,6 +388,7 @@ function gif(scan, b_im, filename, h, varargin, kwargs) end arguments plot_args.?matlab.graphics.chart.primitive.Line + plot_args.DisplayName = 'Grid' kwargs.slice (1,1) char kwargs.index (1,1) {mustBeInteger, mustBeNonnegative} = 1 end diff --git a/src/ScanCartesian.m b/src/ScanCartesian.m index ebe20b4a..03504ccb 100644 --- a/src/ScanCartesian.m +++ b/src/ScanCartesian.m @@ -143,15 +143,17 @@ end function setImageGridOnSequence(self, seq) - % setImageGridOnSequence Align Scan to a Sequence + % setImageGridOnSequence - Align Scan to a Sequence % % setImageGridOnSequence(self, seq) modifies the Scan so that % it aligns with the virtual source Sequence seq. % % soft validate the transmit sequence type: it should be focused - if seq.type ~= "VS", warning(... - "Expected sequence type to be VS but instead got " + seq.type + ". This may produce unexpected results."... + styps = ["VS", "FC", "DV"]; % valid sequence types + if all(us.seq.type ~= styps), warning(... + "Expected a Sequence of type " + join(styps, " or ") + ... + ", but instead got a Sequence of type " + us.seq.type + ": This may produce unexpected results." ... ); end diff --git a/src/ScanGeneric.m b/src/ScanGeneric.m index 551fcb13..dc41d98f 100644 --- a/src/ScanGeneric.m +++ b/src/ScanGeneric.m @@ -255,7 +255,7 @@ end sz = scan.size; - P = scan.pos(); + P = scan.pos; X = reshape(sub(P, 1, 1), sz); Y = reshape(sub(P, 2, 1), sz); Z = reshape(sub(P, 3, 1), sz); diff --git a/src/Scatterers.m b/src/Scatterers.m index ad7b19f4..f360e029 100644 --- a/src/Scatterers.m +++ b/src/Scatterers.m @@ -250,6 +250,9 @@ else, axs = gca; end + % default plotting style + if isempty(varargin), varargin{1} = '.'; end + % plot plot_args = struct2nvpair(plot_args); h = plot(axs, self.pos(1,:), self.pos(3,:), varargin{:}, plot_args{:}); diff --git a/src/Sequence.m b/src/Sequence.m index 3636b36c..237fe803 100644 --- a/src/Sequence.m +++ b/src/Sequence.m @@ -4,43 +4,62 @@ % sequences and is used to define beamforming delays and apodizations % per element per pulse. The same waveform must be sent for all % elements and pulses. Delays and apodization matrices are generated -% when given a Transducer. -% -% The interpretation of the time-axis of the generated delays and the -% foci depend on the Sequence type property. For type 'PW', the foci -% are normal vectors and time 0 is when the wavefront passes through -% the spatial origin (i.e. x=y=z=0). For type 'VS', the foci are -% spatial positions and time 0 is when the wavefront passes through the -% foci. For type 'FSA', the foci are ignored and time 0 is when the -% wavefront passes through each element of the given Transducer. -% +% when given a Transducer using the delays() method. % +% The interpretation of the foci (i.e. the `focus` property) and the +% time-axis of the generated delays depend on the Sequence type property. +% +% For type 'FSA' (full synthetic aperture), the foci are ignored and time 0 +% is when the wavefront passes through each element of the given +% Transducer. % +% For type 'PW' (plane waves), the foci are normal vectors and time 0 is +% when the wavefront passes through the spatial origin (i.e. x=y=z=0). +% +% For types 'FC' (focused), 'DV' (diverging), the foci are spatial +% positions and time 0 is when the wavefront passes through the foci. +% +% Use type 'FSA' and set the hidden `delays_` and/or `apodization_` +% properties to use custom transmit delays and apodization. These will be +% compatible with all simulation methods. +% +% % See also: SEQUENCERADIAL SEQUENCEGENERIC WAVEFORM classdef Sequence < matlab.mixin.Copyable properties % TYPE - Type of pulse sequence definition % % SEQUENCE.TYPE determines the type of pulse sequence. It must be - % one of {'FSA', 'PW', 'VS'}. + % one of {'FSA', 'PW', 'DV', 'FC', 'VS'}. % % When the type is 'FSA', the pulse sequence represents a full % synthetic aperture acquisition where each pulse has one element - % transmitting at a time with no delays applied. When using this - % type, the numpulse property must be set. + % transmitting at a time, and each element transmits at time 0. + % When using this type, the numpulse property must be set. % % When the type is 'PW', the pulse sequence represents a plane-wave - % acquisition where the time delays are applied such that a planar - % wavefront forms that travels in the direction of the focal - % vectors and passes through the origin at time 0. + % acquisition where a planar wavefront forms that travels in the + % direction of the focal vectors and passes through the origin of + % the coordinate system at time 0. % - % When the type is 'VS', the pulse sequence represents a virtual - % source acquisition where the time delays are applied such that a - % wavefront forms that travels radially towards and/or away from - % each foci and passes through the foci at time 0. + % When the type is 'DV', the pulse sequence represents a diverging + % wave acquisition where a radial wavefront forms that travels + % radially away from each foci, starting from each foci at time 0. + % + % When the type is 'FC', the pulse sequence represents a focused + % transmit acquisition where a radial wavefront forms that travels + % radially towards, through, then away from each foci, passing + % through each foci at time 0. + % + % The type 'VS' is a legacy representation of a virtual-source that + % is either a focused or diverging wave transmit. This can be + % convenient as a placeholder when importing data. It's usage for + % beamforming is discouraged, as it can be difficult to + % disambiguate the sign of the beamforming delays based on the + % geometry of the transducer and foci alone. % % See also SEQUENCE.FOCUS SEQUENCE.C0 SAEQUENCE.NUMPULSE - type (1,1) string {mustBeMember(type, ["FSA", "PW", "VS"])} = 'FSA' + type (1,1) string {mustBeMember(type, ["FSA", "PW", "FC", "DV", "VS"])} = 'FSA' % FOCUS - Pulse sequence foci or focal vectors % % SEQUENCE.FOCUS specifies the foci of pulse sequence. @@ -50,15 +69,14 @@ % When the Sequence type is 'PW', the foci are unit normal vectors % specifying the propagation direction for each plane wave. % - % When the Sequence type is 'VS', the foci are the positions in - % space where all wavefronts must converge. + % When the Sequence type is 'FC', 'DV', or 'VS', the foci are the + % positions in space where each wavefront (virtually) converges. % % See also SEQUENCE.TYPE SEQUENCE.C0 focus (3,:) {mustBeNumeric} = zeros([3,1]); % C0 - Reference sound speed % - % SEQUENCE.C0 specifies the sound speed used for creating the - % delays. + % SEQUENCE.C0 specifies the sound speed used for computing delays. % % See also DELAYS SEQUENCE.FOCUS SEQUENCE.TYPE c0 (1,1) {mustBeNumeric} = 1540 @@ -178,8 +196,9 @@ % defines a plane wave (PW) sequence at the 1 x S array of % angles theta. The norm of the focus should always be 1. % - % seq = SEQUENCE('type', 'VS', 'focus', FOCI) defines a - % focused or diverging virtual source (VS) sequence with + % seq = SEQUENCE('type', 'FC', 'focus', FOCI) or + % seq = SEQUENCE('type', 'DV', 'focus', FOCI) defines a + % focused (FC) or diverging (DV) virtual source sequence with % focal point locations at the focal points FOCI. FOCI is a % 3 x S array of focal points. % @@ -191,7 +210,7 @@ % % See also SEQUENCERADIAL WAVEFORM arguments - kwargs.type (1,1) string {mustBeMember(kwargs.type, ["FSA", "PW", "VS"])} + kwargs.type (1,1) string {mustBeMember(kwargs.type, ["FSA", "PW", "VS", "FC", "DV"])} kwargs.focus (3,:) double kwargs.c0 (1,1) double kwargs.pulse (1,1) Waveform @@ -243,7 +262,7 @@ % Example: % % % Create a Sequence - % seq = Sequence('type', 'VS', 'c0', 1500, 'focus', [0;0;30e-3]); % m, s, Hz + % seq = Sequence('type', 'FC', 'c0', 1500, 'focus', [0;0;30e-3]); % m, s, Hz % % % convert from meters to millimeters, hertz to megahertz % seq = scale(seq, 'dist', 1e3, 'time', 1e6); % mm, us, MHz @@ -275,14 +294,14 @@ % conversion methods methods function sequence = QUPS2USTB(seq, xdc, t0) - % QUPS2USTB - Get a USTB/UFF uff.sequence object + % QUPS2USTB - Get a USTB compatible uff.wave object array % - % sequence = QUPS2USTB(seq, xdc, t0) creates a USTB - % compatible uff.wave array from the QUPS Sequence object seq - % where xdc is a QUPS transducer and t0 is the start time in - % the QUPS coordinate system. + % sequence = QUPS2USTB(seq, xdc, t0) creates a USTB compatible + % uff.wave array from the QUPS Sequence object seq where xdc is + % a Transducer and t0 is the start time in the QUPS coordinate + % system. % - % See also TRANSDUCER/QUPS2USTB CHANNELDATA/QUPS2USTB + % See also TRANSDUCER.QUPS2USTB CHANNELDATA.QUPS2USTB arguments seq (1,1) Sequence xdc (1,1) Transducer @@ -315,10 +334,18 @@ for n=1:N, sequence(n).source.xyz = p(:,n).'; end t0 = t0 + vecnorm(p,2,1) ./ seq.c0; % delay transform from element to origin for FSA - case {'VS'} + case {'VS', 'DV', 'FC'} % focused and diverging wave [sequence.wavefront] = deal(uff.wavefront.spherical); for n=1:N, sequence(n).source.xyz = seq.focus(:,n).'; end - t0 = t0 + vecnorm(seq.focus,2,1) ./ seq.c0; % transform for focal point to origin + switch seq.type % transform for focal point to origin + case 'DV', t0 = t0 - vecnorm(seq.focus,2,1) ./ seq.c0; + case 'FC', t0 = t0 + vecnorm(seq.focus,2,1) ./ seq.c0; + case 'VS', t0 = t0 + vecnorm(seq.focus,2,1) ./ seq.c0; + warning("QUPS:QUPS2USTB:ambiguousSequence", ... + "A Sequence of type 'VS' (virtual source) is ambiguous and will be treated as a focused transmit: " ... + + "set the type to 'FC' or 'DV' to avoid this warning." ... + ); + end end % set the start time @@ -329,9 +356,20 @@ methods(Static) function seq = UFF(sequence, c0) + % UFF - Create a Sequence from a uff.wave object array + % + % seq = UFF(sequence) creates a + % Sequence seq from the + % uff.wave object array sequence. + % + % seq = UFF(sequence, c0) additionally sets the + % Scan us.scan from the uff.scan uscan. + % + % See also SEQUENCE.UFF + arguments sequence (1,:) uff.wave - c0 (1,1) {mustBeReal} + c0 {mustBeReal, mustBeScalarOrEmpty} = uniquetol([sequence.sound_speed]) end wvt = unique([sequence.wavefront]); @@ -377,7 +415,7 @@ end switch type - case 'VS' + case {'FC','DV','VS'} seq = Sequence('type', type, 'c0', c0, 'focus', cat(1,p0.xyz)'); case 'FSA' seq = Sequence('type', type, 'c0', c0, 'numPulse', numel(sequence)); @@ -389,6 +427,199 @@ end end + + function [seq, t0] = Verasonics(TX, Trans, TW, kwargs) + % VERASONICS - Construct a Sequence from Verasonics structs + % + % seq = Sequence.Verasonics(TX, Trans) constructs a Sequence + % from the Verasonics 'TX' and 'Trans' structs. + % + % seq = Sequence.Verasonics(TX, Trans, TW) additionally imports + % the trilevel excitation waveform from the 'TW' struct as a + % Waveform. If omitted or empty, seq.pulse is a Waveform.Delta + % instead. + % + % [seq, t0] = Sequence.Verasonics(...) additionally returns an + % offset time array t0 between the transmit delay conventions + % used by QUPS and Verasonics. If the delays cannot be + % validated, t0 will be NaN. + % + % [...] = Sequence.Verasonics(..., 'tol', tol) sets the numeric + % threshold for verifying the parsed Verasonics delays are + % equivalent to the delays used by QUPS. The default is 1e-16. + % + % [...] = Sequence.Verasonics(..., 'c0', c0) sets the sound + % speed c0 in m/s. This should match the value of the Versonics + % variable 'Resource.Parameters.speedOfSound'. The default is + % 1540. + % + % [...] = Sequence.Verasonics(..., 'aperture', ap) explicitly + % provides the element to channel mapping. The default is + % 'Trans.HVMux.Aperture' if TX has an 'aperture' property or + % 'Trans.ConnectorES' otherwise. + % + % [...] = Sequence.Verasonics(..., 'xdc', xdc) provides a + % Transducer for verifying the parsed delays. This is helpful + % if you have a custom transducer or if Transducer.Verasonics + % fails to import the Trans struct properly. + % + % Example: + % + % % get the reference sound speed in meters / second + % c0 = Resource.Parameters.speedOfSound; + % + % % import the Sequence and delay offsets + % [seq, t0] = Sequence.Verasonics(TX, Trans, TW, 'c0', c0); + % + % See also TRANSDUCER.VERASONICS WAVEFORM.VERASONICS SCAN.VERASONICS + arguments + TX struct + Trans (1,1) struct + TW struct {mustBeScalarOrEmpty} = struct.empty + kwargs.c0 (1,1) {mustBeNumeric, mustBePositive, mustBeFloat} = 1540 + kwargs.tol (1,2) {mustBeNumeric, mustBePositive, mustBeFloat} = 1e-16 + kwargs.aperture (:,:) {mustBeNumeric, mustBeInteger, mustBePositive} + kwargs.xdc Transducer {mustBeScalarOrEmpty} = TransducerArray.empty + end + + % get channel mapping + ismux = isfield(TX, 'aperture'); % whether muxed or not + if isfield(kwargs, 'aperture') + ap = kwargs.aperture; % custom muxing + elseif ismux, ap = Trans.HVMux.Aperture; % mux + else, ap = Trans.ConnectorES; % no muxing + end + + % constants + tol = kwargs.tol; + c0 = kwargs.c0; + fc = 1e6*Trans.frequency; + lambda = c0 / fc; + + % tx params + apd = cat(1,TX.Apod ); % apodization + ang = cat(1,TX.Steer ); % angles + rf = cat(1,TX.focus );% .* lambda; % focal range (lambda) + pog = cat(1,TX.Origin);% .* lambda; % beam origin (lambda) + tau = cat(1,TX.Delay ) ./ fc; % tx delays (s) + + % build the full delay/apodization matrix + [apdtx, tautx] = deal(zeros(Trans.numelements, numel(TX))); % pre-allocate + for i = 1 : numel(TX) % for each transmit + if ismux, api = ap(:, TX(i).aperture); + else, api = ap(logical(ap)); + end % selected aperture + j = logical(api); % active elements + apdtx(j,i) = apd(i,:); % apodization + tautx(j,i) = tau(i,:); % delays + end + + % attempt to import the Transducer + xdc = kwargs.xdc; + if isempty(xdc) + try xdc = Transducer.Verasonics(Trans, c0); + catch, xdc = TransducerGeneric.empty; + end + end + + % virtual source ambiguous sequence type warning + [wid, wmsg] = deal( ... + "QUPS:Verasonics:ambiguousSequenceType", ... + "Cannot infer whether sequence is focused or diverging." ... + ); + + % create the corresponding Sequence + if isfield(TX, "FocalPt") % focal points -> VS + pf = cat(1, TX.FocalPt)' .* lambda; % focal points + % attempt to infer focused or diverging wave + if isa(class(xdc), "TransducerArray" ) ... + || isa(class(xdc), "TransducerMatrix") + if all(pf(3,:) < 0), styp = "DV"; + elseif all(pf(3,:) > 0), styp = "FC"; + end + elseif isa(class(xdc), "TransducerConvex") + r = vecnorm(pf - xdc.center,2,1); + if all(r < xdc.radius), styp = "DV"; + elseif all(r > xdc.radius), styp = "FC"; + end + end + if ~exist('styp', 'var') % fallback to virtual source + warning(wid, wmsg); % VS ambiguity warning + styp = "VS"; % default type + end + seq = Sequence("type",styp, "focus", pf); + + elseif ~any(tau,'all') % no delays -> FSA + seq = Sequence("type","FSA", "numPulse",numel(TX)); + + elseif all(all(pog == 0,2) & all(rf == 0,1) & any(ang,'all'),1) % PW + az = rad2deg(ang(:,1)'); % azimuth + el = rad2deg(ang(:,2)'); % elevation + if any(el) + seq = SequenceSpherical("type","PW","angles",[az; el]); + else + seq = SequenceRadial( "type","PW","angles", az ); + end + + elseif any(rf) + pf = pog + rf .* [ + sin(ang(:,1)) .* cos(ang(:,2)), ... + 1 .* sin(ang(:,2)), ... + cos(ang(:,1)) .* cos(ang(:,2)), ... + ]; % focal points + if all(rf > 0), styp = "FC"; % focused + elseif all(rf < 0), styp = "DV"; % diverging + else, styp = "VS"; % unclear + warning(wid, wmsg); % VS ambiguity warning + end + seq = Sequence("type",styp, "focus", lambda * pf.'); + + else + warning( ... + "QUPS:Verasonics:ambiguousSequenceType", ... + "Unable to infer transmit sequence type." ... + ); + seq = SequenceGeneric("apod",apdtx, "del",tautx, "numPulse",numel(TX)); + end + seq.c0 = c0; + + % validate the apodization and override if necessary + val = ~isempty(xdc) && isalmostn(apdtx, seq.apodization(xdc), tol(end)); + if ~val + warning(... + "QUPS:Verasonics:overrideSequenceApodization", ... + "Overriding QUPS apodization with Vantage defined values." ... + ); + seq.apodization_ = apdtx; + end + + % validate the delays + [t0, val] = deal(NaN, false); % no offset / unverified until proven successful + if ~isempty(xdc) % transducer successfully imported + act = logical(apdtx); % whether elements were active + tauq = seq.delays(xdc); % QUPS delays (N x S) + tauv = -tautx; % VSX delays (N x S) + if isequal(size(tauq), size(tauv)) % sizing matches (proceed) + [tauq(~act), tauv(~act)] = deal(nan); % set inactive delays to NaN + t0 = mean(tauv - tauq,1,'omitnan'); % offset time per transmit + val = isalmostn(tauv, tauq + t0, tol(1)); % verified with offset + end + end + + % override delays if they don't match + if ~val + warning(... + "QUPS:Verasonics:overrideSequenceDelays", ... + "Overriding QUPS delays with Vantage defined values." ... + ); + seq.delays_ = tautx; + end + + % import waveform + if isscalar(TW), seq.pulse = Waveform.Verasonics(TW, fc); + else, seq.pulse = Waveform.Delta(); + end + end end % temporal response methods @@ -405,9 +636,11 @@ % Sequence type. % % Type: - % 'VS' : t = 0 when a wave intersects the focus % 'PW' : t = 0 when a wave intersects the point [0;0;0] % 'FSA': t = 0 when a wave intersects the transmit element + % 'FC' : t = 0 when a wave intersects the focus + % 'DV' : t = 0 when a wave intersects the focus + % 'VS' : t = 0 when a wave intersects the focus % % If using the plane wave method, the focus is instead % interpreted as a normal unit vector. @@ -423,13 +656,15 @@ if isempty(self.delays_) switch self.type - case 'VS' - % TODO: use more robust logic for diverging wave test + case {'FC','DV','VS'} v = self.focus - p; % element to focus vector (3 x S x N) - s = ~all(sub(self.focus,3,1) > sub(p,3,1), 3); % whether in behind of the transducer (1 x S) tau = hypot(hypot(sub(v,1,1), sub(v,2,1)),sub(v,3,1)) ./ self.c0; % delay magnitude (1 x S x N) - tau = (-1).^s .* tau; % swap sign for diverging transmit - + switch self.type % get sign swap + case 'VS', s = (-1).^(~all(sub(self.focus,3,1) > sub(p,3,1), 3)); % whether behind the transducer (1 x S) + case 'FC', s = +1; % positive delays + case 'DV', s = -1; % negate delays + end + tau = tau .* s; % swap sign for diverging transmit case 'PW' % use inner product of plane-wave vector with the % positions to get plane-aligned distance @@ -485,7 +720,7 @@ % % % construct the Sequence % seq = Sequence(... - % 'type', 'VS', ... + % 'type', 'FC', ... % 'focus', [0;0;30e-3] + xf .* [1;0;0] ... % ); % @@ -521,15 +756,16 @@ % % t0 = t0Offset(seq) computes the start time offset t0 for the % Sequence seq. For FSA and PW sequences, t0 is always 0. For - % VS sequences, it can be used to shift the spatial location of - % t0 from the foci to the origin of the cooredinate system. + % virtual source sequences, it can be used to shift the spatial + % location of t0 from the foci to the origin of the coordinate + % system. % % Example: % % get a default system % us = UltrasoundSystem(); % % % create a focused Sequence and a scatterer at the focus - % us.seq = Sequence('type', 'VS', 'focus', [0,0,30e-3]', 'c0', 1500); + % us.seq = Sequence('type', 'FC', 'focus', [0,0,30e-3]', 'c0', 1500); % scat = Scatterers('pos', us.seq.focus,'c0',us.seq.c0); % % % get channel data for a scatterrer at the focus @@ -550,9 +786,12 @@ arguments, seq Sequence, end switch seq.type - case 'VS' % for virtual source, t0 is at the foci + case {'VS', 'FC'} % for virtual source, t0 is at the foci t0 = - vecnorm(seq.focus, 2,1) ./ seq.c0; % (1 x S) - otherwise % PW - t0 is at origin; FSA - t0 at the element + case {'DV'} % for virtual source, t0 is at the foci + warning("Untested: please verify this code."); + t0 = + vecnorm(seq.focus, 2,1) ./ seq.c0; % (1 x S) + case {'FSA', 'PW'} % PW - t0 is at origin; FSA - t0 at the element t0 = 0; % (1 x 1) end end diff --git a/src/SequenceGeneric.m b/src/SequenceGeneric.m index 5f2ae564..3e8a311a 100644 --- a/src/SequenceGeneric.m +++ b/src/SequenceGeneric.m @@ -206,7 +206,7 @@ for n=1:N, seq(n).source.xyz = p(:,n).'; end for n=1:N, seq(n).delay = p(:,n)/self.c0 + t0; end - case {'VS'} + case {'VS','DV','FC'} [seq.wavefront] = deal(uff.wavefront.spherical); for n=1:N, seq(n).source.xyz = self.focus(:,n).'; end [seq.delay] = deal(t0); diff --git a/src/SequenceRadial.m b/src/SequenceRadial.m index 96e58334..745f14be 100644 --- a/src/SequenceRadial.m +++ b/src/SequenceRadial.m @@ -50,7 +50,7 @@ arguments % Sequence arguments seq_args.?Sequence - seq_args.type (1,1) string {mustBeMember(seq_args.type, ["PW", "VS"])} = "PW" % restrict + seq_args.type (1,1) string {mustBeMember(seq_args.type, ["PW", "FC", "DV", "VS",])} = "PW" % restrict end arguments % SequenceRadial arguments seqr_args.apex (3,1) {mustBeNumeric} = [0;0;0] @@ -178,6 +178,7 @@ function moveApex(self, apex) end arguments quiver_args.?matlab.graphics.chart.primitive.Quiver + quiver_args.DisplayName = 'Sequence' end if numel(varargin) >= 1 && isa(varargin{1},'matlab.graphics.axis.Axes') @@ -190,10 +191,10 @@ function moveApex(self, apex) % pointing in the vector direction vecs = self.vectors() .* self.ranges; og = repmat(self.apex, [1,size(vecs,2)]); - [x, y] = deal(og(1,:), og(3,:)); - [u, v] = deal(vecs(1,:), vecs(3,:)); + [x, y, z] = deal( og(1,:), og(3,:), og(2,:)); + [u, v, w] = deal(vecs(1,:), vecs(3,:), vecs(2,:)); quiver_args = struct2nvpair(quiver_args); - h = quiver(hax, x, y, u, v, varargin{:}, quiver_args{:}); + h = quiver3(hax, x, y, z, u, v, w, varargin{:}, quiver_args{:}); end end diff --git a/src/Transducer.m b/src/Transducer.m index 3cf04404..3cc04065 100644 --- a/src/Transducer.m +++ b/src/Transducer.m @@ -16,7 +16,8 @@ width (1,1) double = 1.5e-4 % width of an element height (1,1) double = 6e-3 % height of an element numel (1,1) double = 128 % number of elements - offset (3,1) double = [0;0;0]% the offset from the origin + offset (3,1) double = [0;0;0] % the offset from the origin + rot (1,2) double = [0 0] % [azimuth, elevation] rotation after translation (deg) impulse Waveform {mustBeScalarOrEmpty} = Waveform.empty() % the impulse response function of the element end @@ -755,21 +756,21 @@ % build the rotation vectors % v = [cosd(phi); cosd(phi); sind(phi)] .* [cosd(th); sind(th); 1]; - rot = [phi; th; th] .* [1;1;0]; + elrot = [phi; th; th] .* [1;1;0]; % convert to kWaveGrid coorindates: axial x lateral x elevation switch dim case 2, % we only rotate in x-z plane - [p, rot] = deal(p([3,1 ],:), rot(2,:)); + [p, elrot] = deal(p([3,1 ],:), elrot(2,:)); [arr_off, arr_rot] = deal(-og(1:2), 0); case 3, - [p, rot] = deal(p([3,1,2],:), rot([3,1,2],:)); % I think this is right? + [p, elrot] = deal(p([3,1,2],:), elrot([3,1,2],:)); % I think this is right? [arr_off, arr_rot] = deal(-og(1:3), [0;0;0]); end % add each element to the array for i = 1:n - karray.addRectElement(p(:,i), xdc.width, xdc.height, rot(:,i)); + karray.addRectElement(p(:,i), xdc.width, xdc.height, elrot(:,i)); end karray.setArrayPosition(arr_off, arr_rot); @@ -980,12 +981,12 @@ % plot a patch: use depth as the color args = struct2nvpair(patch_args); - hp = patch(axs, 'XData', Xp, 'YData', Yp, 'ZData', Zp, 'CData', Zp, varargin{:}, args{:}); + hp = patch(axs, 'XData', Xp, 'YData', Zp, 'ZData', Yp, 'CData', Zp, varargin{:}, args{:}); % set default axis arguments xlabel(axs, 'x'); - ylabel(axs, 'y'); - zlabel(axs, 'z'); + ylabel(axs, 'z'); + zlabel(axs, 'y'); grid (axs, 'on'); % zlim (axs, [-1e-3, 1e-3] + [min(Zp(:)), max(Zp(:))]) axis (axs, 'equal') diff --git a/src/TransducerArray.m b/src/TransducerArray.m index fa305463..d3835d4a 100644 --- a/src/TransducerArray.m +++ b/src/TransducerArray.m @@ -88,34 +88,24 @@ end end end - - % define abstract methods - methods - function p = positions(self), p = findPositions(self); end - function [theta, phi, normal, width, height] = orientations(self) - [theta, phi, normal, width, height] = getOrientations(self); - end - end - - + % define position methods - methods(Hidden, Access=private) + methods(Access=public) % get methods - function p = findPositions(self) - % returns a 1 x N vector of the positions of the N elements with 0 - % at the center + function p = positions(self) array_width = (self.numel - 1) * self.pitch; x = linspace(-array_width/2, array_width/2, self.numel); - p = cat(1, x, zeros(2, numel(x))) + self.offset; + q = prod(quaternion([-self.rot(2),0,0;0,self.rot(1),0], 'rotvecd')); + p = rotatepoint(q, cat(1, x, zeros(2, numel(x)))')' + self.offset; end - function [theta, phi, normal, width, height] = getOrientations(self) - theta = zeros([1, self.numel]); - phi = zeros(size(theta)); + function [theta, phi, normal, width, height] = orientations(self) + theta = self.rot(1) + zeros([1, self.numel]); + phi = -self.rot(2) + zeros(size(theta)); ZERO = zeros(size(theta)); - normal = [cosd(phi).*sind(theta); sind(phi); cosd(phi).*cosd(theta)]; - width = [cosd(theta); sind(ZERO); -cosd(ZERO).*sind(theta)]; - height = [sind(phi).*sind(ZERO); cosd(phi); sind(phi).*cosd(ZERO)]; + normal = [cosd(phi ).*sind(theta); sind(phi ); cosd(phi ).*cosd(theta)]; + width = [cosd(theta); sind(ZERO); -cosd(ZERO).*sind(theta)]; + height = [sind(phi ).*sind(ZERO ); cosd(phi ); sind(phi ).*cosd(ZERO )]; end end diff --git a/src/TransducerConvex.m b/src/TransducerConvex.m index d502dca9..970f1fc1 100644 --- a/src/TransducerConvex.m +++ b/src/TransducerConvex.m @@ -49,8 +49,10 @@ % initialize the TransducerConvex for f = string(fieldnames(array_args))' + if f == "pitch", continue; end % set this last self.(f) = array_args.(f); end + if isfield(array_args, "pitch"), self.pitch = array_args.pitch; end % if kerf not set, default it to 0 if isempty(self.pitch), self.kerf = 0; end @@ -77,49 +79,27 @@ end end - % define abstract methods - methods - function p = positions(self), p = findPositions(self); end - function [theta, phi, normal, width, height] = orientations(self) - [theta, phi, normal, width, height] = getOrientations(self); - end - end - % define position methods - methods(Hidden,Access=private) + methods % get methods - function p = findPositions(self) - % returns a 3 x N vector of the positions of the N elements with - % the center element at the origin - + function p = positions(self) array_angular_width = (self.numel - 1)* self.angular_pitch; theta = linspace(-array_angular_width/2, array_angular_width/2, self.numel); z = self.radius * cosd(theta); x = self.radius * sind(theta); y = zeros(size(theta)); - p = cat(1, x, y, z) + self.center; + q = prod(quaternion([-self.rot(2),0,0;0,self.rot(1),0], 'rotvecd')); + p = rotatepoint(q, cat(1, x, y, z)')' + self.center; end - function [theta, phi, normal, width, height] = getOrientations(self) - % Outputs: - % - % - theta: the azimuthal angle in cylindrical coordinates - % - % - phi: the elevation angle in cylindrical coordinates - % - % - normal: a 3 x N array of the element normals - % - % - width: a 3 x N array of element width vectors - % - % - height: a 3 x N array of element height vectors - + function [theta, phi, normal, width, height] = orientations(self) array_angular_width = (self.numel - 1)* self.angular_pitch; - theta = linspace(-array_angular_width/2, array_angular_width/2, self.numel); - phi = zeros(size(theta)); + theta = self.rot(1) + linspace(-array_angular_width/2, array_angular_width/2, self.numel); + phi = -self.rot(2) + zeros(size(theta)); ZERO = zeros(size(theta)); - normal = [cosd(phi).*sind(theta); sind(phi); cosd(phi).*cosd(theta)]; - width = [cosd(theta); sind(ZERO); -cosd(ZERO).*sind(theta)]; - height = [sind(phi).*sind(ZERO); cosd(phi); sind(phi).*cosd(ZERO)]; + normal = [cosd(phi).*sind(theta); sind(phi ); cosd(phi ).*cosd(theta)]; + width = [cosd(theta); sind(ZERO); -cosd(ZERO).*sind(theta)]; + height = [sind(phi).*sind(ZERO ); cosd(phi ); sind(phi ).*cosd(ZERO )]; end end diff --git a/src/TransducerGeneric.m b/src/TransducerGeneric.m index f874ef74..2b09cad3 100644 --- a/src/TransducerGeneric.m +++ b/src/TransducerGeneric.m @@ -115,7 +115,6 @@ methods function p = getSIMUSParam(self) error("SIMUS does not support a TransducerGeneric."); - % TODO: error if origin not at 0. end end diff --git a/src/TransducerMatrix.m b/src/TransducerMatrix.m index 9a1a8f7c..06bc6d6d 100644 --- a/src/TransducerMatrix.m +++ b/src/TransducerMatrix.m @@ -124,18 +124,19 @@ y = linspace(-array_height/2, array_height/2, self.numd(end)); z = 0; [x,y,z] = ndgrid(x,y,z); - p = cat(2, x(:), y(:), z(:))' + self.offset + self.mux_offset; + q = prod(quaternion([-self.rot(2),0,0;0,self.rot(1),0], 'rotvecd')); + p = rotatepoint(q,cat(2, x(:), y(:), z(:)))' + self.offset + self.mux_offset; % returns a 1 x N vector of the positions of the N elements with 0 % at the center end function [theta, phi, normal, width, height] = orientations(self) - theta = zeros([1, self.numel]); - phi = zeros(size(theta)); + theta = self.rot(1) + zeros([1, self.numel]); + phi = -self.rot(2) + zeros(size(theta)); ZERO = zeros(size(theta)); - normal = [cosd(phi).*sind(theta); sind(phi); cosd(phi).*cosd(theta)]; + normal = [cosd(phi).*sind(theta); sind(phi ); cosd(phi ).*cosd(theta)]; width = [cosd(theta); sind(ZERO); -cosd(ZERO).*sind(theta)]; - height = [sind(phi).*sind(ZERO); cosd(phi); sind(phi).*cosd(ZERO)]; + height = [sind(phi).*sind(ZERO ); cosd(phi ); sind(phi ).*cosd(ZERO )]; end end diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index e1491b60..e37fc71d 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -1,26 +1,50 @@ -% ULTRASOUNDSYSTEM - Complete ultrasound system class +% ULTRASOUNDSYSTEM - Comprehensive ultrasound system definition class % % The ULTRASOUNDSYSTEM class is a synthesis class containing the properties -% describing a medical ultrasound system and providing methods to simulate -% channel data or beamform channel data. The complete system is described -% by the transmit and receive Transducer, the transmit Sequence, and the -% Scan defining the region for simulation or beamforming. +% describing a medical ultrasound system. Once a Transducer, Sequence, and +% Scan are defined, the methods are provided to use a Scatterers or Medium +% to simulate ChannelData, or beamform ChannelData into an image. % -% Multiple simulators are supported, but external simulators must be -% installed separately. They include: +% Multiple simulators are supported. Most simulators support the arbitrary +% Sequences and arbitrary Transducers allowed in QUPS. All simulators +% support parallel enviroments such as ProcessPools (multiple MATLAB +% instances) and parclusters (for compute clusters) with some supporting +% ThreadPools (multi-threading). External simulators must be installed +% separately. They include: % -% * greens via QUPS -% * simus via MUST -% * calc_scat_all, calc_scat_multi via FieldII -% * kspaceFirstOrder via K-wave -% * fullwaveSim via Fullwave +% * greens - simulate point scatterers via QUPS (GPU-enabled) +% * simus - simulate point scatterers via MUST +% * calc_scat_all - simulate point scatterers via FieldII, then synthesize transmits +% * calc_scat_multi - simulate point scatterers via FieldII, per transmit +% * kspaceFirstOrder - simulate a medium via K-wave (GPU-enabled) +% * fullwaveSim - simulate a medium via Fullwave (currently non-public) % -% Multiple beamformers are provided which include +% Multiple beamformers are provided, all of which are GPU enabled, either +% natively in MATLAB or via ptx binaries, or both. They include: % -% * bfDAS - a naive delay-and-sum beamformer -% * DAS - a more performant naive delay-and-sum beamformer -% * bfEikonal - a sound speed delay-and-sum beamformer using the eikonal equation -% * bfAdjoint - a frequency domain beamformer +% * bfDAS - a standard delay-and-sum beamformer +% * DAS - a restricted but more performant standard delay-and-sum beamformer +% * bfDASLUT - a delay-and-sum beamformer for custom delays +% * bfEikonal - a delay-and-sum beamformer using eikonal equation delays +% * bfAdjoint - a frequency domain matrix adjoint beamformer +% * bfMigration - a frequency domain stolt's f-k migration beamformer +% +% Most beamformers are pixel-based, so classical scanline-based processing +% must be emulated via apodization ND-arrays. The provided receive +% apodization generators include: +% +% * apScanline - emulate a scan-line beamformer (focal sequences) +% * apMultiline - emulate a multi-line beamformer (focal sequences) +% * apTranslatingAperture - emulate a translating transmit aperture +% * apApertureGrowth - receive apodization limiting the f# +% * apAcceptanceAngle - receive apodization limited by the element to pixel angle +% +% One can also synthesize new transmit sequences from full synthetic +% aperture (FSA) data or synthesizse FSA data from focused pulses. These +% utilities are provided by: +% +% * focusTx - synthesize a transmit sequence from FSA data +% * refocus - synthesize FSA data from a transmit sequence % % See also CHANNELDATA TRANSDUCER SEQUENCE SCAN SCATTERERS MEDIUM @@ -50,7 +74,7 @@ end properties(Dependent) - fc {mustBeNumeric} % central operating frequency(ies) (from the Transducer(s)) + fc {mustBeNumeric} % central operating frequency(ies) xdc Transducer % Transducer object (if receive and transmit are identical) pulse Waveform % Waveform object (from the Sequence) lambda {mustBeNumeric} % wavelength at the central frequency(ies) @@ -331,9 +355,11 @@ function delete(self) hrx = plot(self.rx, ax, 'r', 'DisplayName', 'Receive Elements', plargs{:}); % rx elements hxdc = [htx, hrx]; end + rq = max([range([hps.XData]), range([hps.YData]), range([hps.ZData])]); % scale quivers by range of the image + % rq = max(rq, range(xlim(ax))); % include x? switch self.seq.type % show the transmit sequence - case 'PW', hseq = plot(self.seq, ax, 3e-2, 'k.', 'DisplayName', 'Tx Sequence', plargs{:}); % scale the vectors for the plot - otherwise, hseq = plot(self.seq, ax, 'k.', 'DisplayName', 'Tx Sequence', plargs{:}); % plot focal points, if they exist + case 'PW', hseq = plot(self.seq, ax, rq/2, 'k.', 'DisplayName', 'Tx Sequence', plargs{:}); % scale the vectors for the plot + otherwise, hseq = plot(self.seq, ax, 'k.', 'DisplayName', 'Tx Sequence', plargs{:}); % plot focal points, if they exist end h = [hxdc, hps, hseq]; legend(ax, h); @@ -720,6 +746,19 @@ function delete(self) % USTB interop methods function [uchannel_data, uscan] = QUPS2USTB(us, chd, fmod) + % QUPS2USTB - Create a USTB channel data object + % + % uchannel_data = QUPS2USTB(us, chd) creates a USTB + % compatible uff.channel_data object from the UltrasoundSystem + % us and ChannelData chd. USTB must be on the path. + % + % uchannel_data = QUPS2USTB(..., fmod) sets the modulation + % frequency to fmod. The default is 0. + % + % [uchannel_data, uscan] = QUPS2USTB(...) additionally returns + % a USTB compatible uff.scan. + % + % See also ULTRASOUNDSYSTEM.UFF arguments us (1,1) UltrasoundSystem chd (1,1) ChannelData @@ -732,6 +771,17 @@ function delete(self) methods(Static) function [us, chd] = UFF(uchannel_data, uscan) + % UFF - Create an UltrasoundSystem and ChannelData from a uff.channel_data object + % + % [us, chd] = UFF(uchannel_data) creates an + % UltrasoundSystem us and ChannelData chd from the + % uff.channel_data object uchannel_data. + % + % [us, chd] = UFF(uchannel_data, uscan) additionally sets the + % Scan us.scan from the uff.scan uscan. + % + % See also ULTRASOUNDSYSTEM.UFF + arguments uchannel_data (1,1) uff.channel_data uscan (1,1) uff.scan @@ -884,7 +934,8 @@ function delete(self) % continuous resampling of the waveform case 'continuous', icmat = cell2mat(arrayfun(@(tau) {wv_tx.sample(t(:) - tau)}, tau_tx_pix)); case 'interpolate' % interpolate, upsampling by 10x in time first - t_up = wv_tx.getSampleTimes(10*fs_); + wv_tx.fs = 10*fs_; + t_up = wv_tx.time; icmat = interp1(t_up, wv_tx.sample(t_up), t(:) - tau_tx_pix, 'spline', 0); end @@ -2453,15 +2504,19 @@ function delete(self) dat_args = {chd.data, gather(chd.t0), gather(chd.fs), c0, 'device', kwargs.device, 'position-precision', kwargs.prec}; % data args if isfield(kwargs, 'interp'), interp_args = {'interp', kwargs.interp}; else, interp_args = {}; end ext_args = [interp_args, apod_args]; % extra args - + switch self.seq.type case 'FSA' - pos_args = {P_im, P_rx, self.tx.positions(), [0;0;1]}; + [~,~,nf] = self.tx.orientations(); % normal vectors + pos_args = {P_im, P_rx, self.tx.positions(), nf}; + ext_args{end+1} = 'diverging-waves'; %#ok case 'PW' pos_args = {P_im, P_rx, [0;0;0], self.seq.focus}; % TODO: use origin property in tx sequence ext_args{end+1} = 'plane-waves'; %#ok - case 'VS' - pos_args = {P_im, P_rx, self.seq.focus, [0;0;1]}; + case {'VS', 'FC', 'DV'} + nf = self.seq.focus - self.xdc.offset; % normal vector + pos_args = {P_im, P_rx, self.seq.focus, nf ./ norm(nf)}; + if self.seq.type == "DV", ext_args{end+1} = 'diverging-waves'; end %#ok end % request the CUDA kernel? @@ -2510,6 +2565,11 @@ function delete(self) % the data to the next power of 2. This accelerates the fft % computation, but uses more memory. % + % chd = FOCUSTX(..., 'bsize', B) uses a maximum block size of B + % output transmits at a time when vectorizing computations. A + % larger block size will run faster, but use more memory. The + % default is seq.numPulse. + % % Example: % % % Define the setup @@ -2520,8 +2580,8 @@ function delete(self) % us.seq = Sequence('type', 'FSA', 'c0', us.seq.c0, 'numPulse', us.xdc.numel); % chd = greens(us, scat); % compute the response % - % % Create plane-wave data by synthesizing the transmits with - % plane-wave delays + % % Create plane-wave data by synthesizing the transmits with plane-wave delays + % % seq_pw = SequenceRadial('type', 'PW', 'c0', us.seq.c0, ... % 'angles', -25:0.5:25, 'ranges', 1); % plane-wave sequence % chd_pw = focusTx(us, chd, seq_pw); % synthesize transmits @@ -2543,8 +2603,9 @@ function delete(self) chd (1,1) ChannelData seq (1,1) Sequence = self.seq; kwargs.interp (1,1) string {mustBeMember(kwargs.interp, ["linear", "nearest", "next", "previous", "spline", "pchip", "cubic", "makima", "freq", "lanczos3"])} = 'cubic' - kwargs.length string {mustBeScalarOrEmpty} = []; + kwargs.length string {mustBeScalarOrEmpty} = string.empty; kwargs.buffer (1,1) {mustBeNumeric, mustBeInteger} = 0 + kwargs.bsize (1,1) {mustBePositive, mustBeInteger} = seq.numPulse end % Copy semantics @@ -2585,14 +2646,19 @@ function delete(self) end % align dimensions - D = 1+max(3,ndims(chd.data)); % get a free dimension for M' + D = 1+max([3,ndims(chd.data), ndims(chd.t0)]); % get a free dimension for M' assert(D > chd.mdim, "Transmit must be in the first 3 dimensions (" + chd.mdim + ")."); tau = swapdim(tau ,[1 2],[chd.mdim D]); % move data apod = swapdim(apod,[1 2],[chd.mdim D]); % move data + id = num2cell((1:kwargs.bsize)' + (0:kwargs.bsize:seq.numPulse-1),1); % output sequence indices + id{end}(id{end} > seq.numPulse) = []; % delete OOB indices % sample and store - z = chd.sample(chd.time - tau, kwargs.interp, apod, chd.mdim); % sample (perm(T' x N x 1) x F x ... x M') - z = swapdim(z, chd.mdim, D); % replace transmit dimension (perm(T' x N x M') x F x ...) + for i = numel(id):-1:1 + z{i} = chd.sample(chd.time - sub(tau,id{i},D), kwargs.interp, sub(apod,id{i},D), chd.mdim); % sample ({perm(T' x N x 1) x F x ...} x M') + z{i} = swapdim(z{i}, chd.mdim, D); % replace transmit dimension (perm({T' x N} x M') x {F x ...}) + end + z = cat(chd.mdim, z{:}); % unpack transmit dimension (perm(T' x N x M') x F x ...) chd.data = z; % store output channel data % (perm(T' x N x M') x F x ...) end @@ -2641,9 +2707,12 @@ function delete(self) % % Example: % % Define the setup - make plane waves - % seqpw = SequenceRadial('type', 'PW', 'angles', -45:0.5:45); - % us = UltrasoundSystem(); - % scat = Scatterers('pos', [0;0;30e-3], 'c0', seqpw.c0); % define a point target + % c0 = 1500; + % us = UltrasoundSystem(); + % us.seq.c0 = c0; + % seqpw = SequenceRadial('type', 'PW', 'angles', -45:1.5:45, 'c0', c0); + % seqfc = SequenceRadial('type', 'VS', 'focus', [1;0;0].*(sub(us.xdc.positions,1,1)) + [0;0;20e-3], 'c0', c0); + % scat = Scatterers('pos', [5e-3;0;30e-3], 'c0', seqpw.c0); % define a point target % % % Compute the image % chd = greens(us, scat); % compute the response @@ -2654,37 +2723,45 @@ function delete(self) % uspw = copy(us); % uspw.seq = seqpw; % bpw = DAS(uspw, chdpw); - % - % % Refocus back to FSA, and beamform - % chdfsa = refocus(us, chdpw, seqpw); - % bfsa = DAS(us, chdfsa); % - % % Display the channel data + % % Refocus back to FSA, and beamform + % chdfsa1 = refocus(us, chdpw, seqpw); + % bfsa1 = DAS(us, chdfsa1); + % + % % Focus at focal points and beamform + % chdfc = focusTx(us, chd, seqfc); + % usfc = copy(us); + % usfc.seq = seqfc; + % bfc = DAS(usfc, chdfc); + % + % % Refocus back to FSA, and beamform + % chdfsa2 = refocus(us, chdpw, seqpw); + % bfsa2 = DAS(us, chdfsa2); + % + % %% Display the channel data % figure('Name', 'Channel Data'); - % tiledlayout('flow'); - % nexttile(); - % imagesc(chd); title('Original'); - % nexttile(); - % imagesc(chdpw); title('Plane-Wave'); - % nexttile(); - % imagesc(chdfsa); title('Refocused'); + % tiledlayout('flow'); + % chds = [chd, chd, chdpw, chdfsa1, chdfc, chdfsa2]; + % tnms = ["Original", "Original", "PW", "PW-REF", "FC", "FC-REF"]; + % for i = 1:numel(chds) + % himc = imagesc(chds(i),ceil(chds(i).M/2),nexttile()); + % title(tnms(i)); + % colorbar; colormap default; caxis(max(caxis) + [-50 0]); + % end + % linkaxes([himc.Parent]); % - % % Display the images - % bim = mod2db(b); % log-compression - % bimpw = mod2db(bpw); - % bimfsa = mod2db(bfsa); - % figure('Name', 'B-mode'); + % %% Display the images + % figure('Name', 'B-mode'); % tiledlayout('flow'); - % nexttile(); - % imagesc(us.scan, bim, [-80 0] + max(bim(:))); - % colormap gray; colorbar; title('Original'); - % nexttile(); - % imagesc(us.scan, bimpw, [-80 0] + max(bimpw(:))); - % colormap gray; colorbar; title('Plane-Wave'); - % nexttile(); - % imagesc(us.scan, bimfsa, [-80 0] + max(bimfsa(:))); - % colormap gray; colorbar; title('Refocused'); - % + % bs = cat(4, b, b, bpw, bfsa1, bfc, bfsa2); + % bpow = gather(mod2db(max(bs,[],1:2))); + % for i = 1:size(bs,4) + % himb(i) = imagesc(us.scan, sub(bs,i,4), nexttile(), bpow(i) + [-80 0]); + % colormap gray; colorbar; + % title(tnms(i)); + % end + % linkaxes([himb.Parent]); + % % See also FOCUSTX arguments self (1,1) UltrasoundSystem @@ -2694,12 +2771,19 @@ function delete(self) kwargs.method (1,1) string {mustBeMember(kwargs.method, ["tikhonov"])} = "tikhonov" end + % dispatch pagenorm function based on MATLAB version + if verLessThan('matlab', '9.13') + pagenorm2 = @(x) pagenorm(x,2); + else + pagenorm2 = @(x) max(pagesvd(x),[],1:2); + end + % get the apodization / delays from the sequence - tau = seq.delays(self.tx); % (M x V) - a = seq.apodization(self.tx); % (M x V) + tau = -seq.delays(self.tx); % (M x V) + a = seq.apodization(self.tx); % (M x V) % get the frequency vectors - f = chd.fftaxis; % perm(... x T x ...) + f = gather(chd.fftaxis); % perm(... x T x ...) % construct the encoding matrix (M x V x T) H = a .* exp(+2j*pi*shiftdim(f(:),-2).*tau); @@ -2711,16 +2795,16 @@ function delete(self) case "tikhonov" % TODO: option to use pinv, as it is (slightly) % different than matrix division - A = real(pagemtimes(H, 'ctranspose', H, 'none')) + (kwargs.gamma * eye(chd.M)); % A = (H'*H + gamma * I) + A = real(pagemtimes(H, 'ctranspose', H, 'none')) + (kwargs.gamma * pagenorm2(gather(H)).^2 .* eye(chd.M)); % A = (H'*H + gamma * I) Hi = pagetranspose(pagemrdivide(gather(H), gather(A))); % Hi = (A^-1 * H)' <=> (H / A)' Hi = cast(Hi, 'like', H); end - % move to matching data dimensions - D = max(ndims(chd.data)); + % move inverse matrix to matching data dimensions + D = max(ndims(chd.data), ndims(chd.t0)); ord = [chd.mdim, D+1, chd.tdim]; ord = [ord, setdiff(1:D, ord)]; % all dimensions - Hi = permute(Hi, ord); + Hi = ipermute(Hi, ord); % move data to the frequency domain x = chd.data; @@ -2779,8 +2863,8 @@ function delete(self) % modulation frequency fc. This undoes the effect of % demodulation/downmixing at the same frequency. % - % b = BFADJOINT(..., 'bsize', B) uses an block size of B when - % vectorizing computations. A larger block size will run + % b = BFADJOINT(..., 'bsize', B) uses a maximum block size of B + % when vectorizing computations. A larger block size will run % faster, but use more memory. The default is chosen % heuristically. % @@ -2828,14 +2912,14 @@ function delete(self) % % See also BFEIKONAL BFDAS DAS FOCUSTX - % TODO: test for tall types where receive diomension is tall - + % TODO: test for tall types where receive dimension is tall - % should work ... arguments self (1,1) UltrasoundSystem chd (1,1) ChannelData c0 (:,:,:,1,1) {mustBeNumeric} = self.seq.c0 - kwargs.fmod (1,1) {mustBeNumeric} = 0 % modulation frequency - kwargs.fthresh (1,1) {mustBeReal} = -Inf; % threshold for including frequencies + kwargs.fmod (1,1) {mustBeNumeric, mustBeFinite} = 0 % modulation frequency + kwargs.fthresh (1,1) {mustBeReal, mustBeNegative} = -Inf; % threshold for including frequencies kwargs.apod {mustBeNumericOrLogical} = 1; % apodization matrix (I1 x I2 x I3 x N x M) kwargs.Nfft (1,1) {mustBeInteger, mustBePositive} = chd.T; % FFT-length kwargs.keep_tx (1,1) logical = false % whether to preserve transmit dimension @@ -2858,7 +2942,7 @@ function delete(self) % validate sequence/transducer: doesn't work for % non-linear/focused - not sure why yet - seems like a % difficult to trace phase error bug. - if ~isa(self.tx, 'TransducerArray') && ~isa(self.rx, 'TransducerArray') && self.seq.type == "VS" + if ~isa(self.tx, 'TransducerArray') && ~isa(self.rx, 'TransducerArray') && any(self.seq.type == ["DV","FC","VS"]) warning('QUPS:bfAdjoint:UnsupportedSequence', ... 'This function is unsupported for focused transmits with non-linear transducers.' ... ); @@ -2898,7 +2982,7 @@ function delete(self) f_val = any(f_val, setdiff(1:ndims(x), chd.tdim)); % evaluate only freqs across aperture/frames that is above threshold % get the pixel positions - D = max(4, gather(ndims(chd.data))); % >= 4 + D = gather(max([4, ndims(chd.data), ndims(chd.t0)])); % >= 4 Pi = self.scan.positions(); Pi = swapdim(Pi, [1:4], [1, D+(1:3)]); % place I after data dims (3 x 1 x 1 x 1 x ... x [I]) c0 = shiftdim(c0, -D); % 1 x 1 x 1 x 1 x ... x [I] @@ -3060,6 +3144,10 @@ function delete(self) % method for interpolation. Support is provided by the % ChannelData/sample method. The default is 'cubic'. % + % b = BFEIKONAL(..., 'bsize', B) uses a maximum block size of B + % when vectorizing computations. A larger block size will run + % faster, but use more memory. The default is Inf. + % % [b, tau_rx, tau_tx] = BFEIKONAL(...) additionally returns the % receive and transmit time delays tau_rx and tau_tx are size % (I1 x I2 x I3 x N x 1) and (I1 x I2 x I3 x 1 x M), where @@ -3153,6 +3241,7 @@ function delete(self) kwargs.keep_rx (1,1) logical = false; kwargs.keep_tx (1,1) logical = false; kwargs.verbose (1,1) logical = true; + kwargs.bsize (1,1) {mustBePositive, mustBeInteger} = self.seq.numPulse; kwargs.delay_only (1,1) logical = isempty(chd); % compute only delays end @@ -3258,7 +3347,7 @@ function delete(self) if kwargs.delay_only, b = zeros([self.scan.size, 1+kwargs.keep_rx*(self.rx.numel-1), 1+kwargs.keep_tx*(self.tx.numel-1), 0]); return; end % extract relevant arguments - lut_args = ["apod", "fmod", "interp", "keep_tx", "keep_rx"]; + lut_args = ["apod", "fmod", "interp", "keep_tx", "keep_rx", "bsize"]; args = namedargs2cell(rmfield(kwargs, setdiff(fieldnames(kwargs), lut_args))); % beamform @@ -3295,6 +3384,10 @@ function delete(self) % interpolation. Support is provided by the % ChannelData/sample2sep method. The default is 'cubic'. % + % b = BFDAS(..., 'bsize', B) uses a maximum block size of B + % when vectorizing computations. A larger block size will run + % faster, but use more memory. The default is Inf. + % % [b, tau_rx, tau_tx] = BFDAS(...) additionally returns the % receive and transmit time delays tau_rx and tau_tx are size % (I1 x I2 x I3 x N x 1) and (I1 x I2 x I3 x 1 x M), where @@ -3332,6 +3425,7 @@ function delete(self) kwargs.keep_tx (1,1) logical = false kwargs.keep_rx (1,1) logical = false kwargs.delay_only (1,1) logical = isempty(chd); % compute only delays + kwargs.bsize (1,1) {mustBePositive, mustBeInteger} = self.seq.numPulse end % get image pixels, outside of range of data @@ -3342,9 +3436,9 @@ function delete(self) % get virtual source or plane wave geometries switch self.seq.type - case 'FSA', [Pv, Nv] = deal(self.tx.positions(), [0;0;1]); - case 'VS', [Pv, Nv] = deal(self.seq.focus, [0;0;1]); - case 'PW', [Pv, Nv] = deal([0;0;0], self.seq.focus); % TODO: use origin property in tx sequence + case 'FSA', [Pv, Nv] = deal(self.tx.positions(), argn(3, @()self.tx.orientations)); + case {'VS','FC','DV'}, [Pv, Nv] = deal(self.seq.focus, normalize(self.seq.focus - self.xdc.offset,1,"norm")); + case 'PW', [Pv, Nv] = deal([0;0;0], self.seq.focus); % TODO: use origin property in tx sequence end Pr = swapdim(Pr,2,5); % move N to dim 5 [Pv, Nv] = deal(swapdim(Pv,2,6), swapdim(Nv,2,6)); % move M to dim 6 @@ -3354,9 +3448,10 @@ function delete(self) % transmit sensing vector dv = Pi - Pv; % 3 x I1 x I2 x I3 x 1 x M - switch self.seq.type, - case {'VS', 'FSA'}, dv = vecnorm(dv, 2, 1) .* sign(sum(dv .* Nv,1)); - case {'PW'}, dv = sum(dv .* Nv, 1); + switch self.seq.type + case {'DV','FSA'}, dv = vecnorm(dv, 2, 1) ; + case {'VS','FC'}, dv = vecnorm(dv, 2, 1) .* sign(sum(dv .* Nv,1)); + case 'PW', dv = sum(dv .* Nv,1) ; end % 1 x I1 x I2 x I3 x 1 x M % bring to I1 x I2 x I3 x 1 x M @@ -3372,7 +3467,7 @@ function delete(self) if kwargs.delay_only, b = zeros([self.scan.size, 1+kwargs.keep_rx*(self.rx.numel-1), 1+kwargs.keep_tx*(self.tx.numel-1), 0]); return; end % extract relevant arguments - lut_args = ["apod", "fmod", "interp", "keep_tx", "keep_rx"]; + lut_args = ["apod", "fmod", "interp", "keep_tx", "keep_rx", "bsize"]; args = namedargs2cell(rmfield(kwargs, setdiff(fieldnames(kwargs), lut_args))); % beamform @@ -3419,6 +3514,10 @@ function delete(self) % interpolation. Support is provided by the % ChannelData/sample2sep method. The default is 'cubic'. % + % b = BFEIKONAL(..., 'bsize', B) uses a maximum block size of B + % when vectorizing computations. A larger block size will run + % faster, but use more memory. The default is self.seq.numPulse. + % % Example: % % % Define the setup @@ -3451,6 +3550,7 @@ function delete(self) kwargs.interp (1,1) string {mustBeMember(kwargs.interp, ["linear", "nearest", "next", "previous", "spline", "pchip", "cubic", "makima", "lanczos3"])} = 'cubic' kwargs.keep_tx (1,1) logical = false kwargs.keep_rx (1,1) logical = false + kwargs.bsize (1,1) {mustBePositive, mustBeInteger} = max(self.seq.numPulse, size(tau_tx,5), 'omitnan'); end % validate / parse receive table sizing @@ -3509,11 +3609,24 @@ function delete(self) if ~kwargs.keep_tx, sdim = [sdim, 5]; end % max dimension of data - D = max([3, ndims(chd), cellfun(@ndims, {chd.data})]); + D = max([3, ndims(chd), cellfun(@ndims, {chd.data}), cellfun(@ndims, {chd.t0})]); % sample, apodize, and sum over tx/rx if requested for i = numel(chd):-1:1 - bi = sample2sep(chd(i), tau_tx, tau_rx, kwargs.interp, kwargs.apod, sdim, kwargs.fmod, [4, 5]); % (I1 x I2 x I3 x [1|N] x [1|M] x [F x ... ]) + if isfinite(kwargs.bsize) + [chds, im] = splice(chd(i), chd.mdim, kwargs.bsize); % always splice tx + bi = {0}; % implicit preallocation + for m = numel(chds):-1:1 + tau_txm = sub(tau_tx, im(m), 5); + bim = sample2sep(chds(m), tau_txm, tau_rx, kwargs.interp, kwargs.apod, sdim, kwargs.fmod, [4, 5]); % (I1 x I2 x I3 x [1|N] x [1|M] x [F x ... ]) + if kwargs.keep_tx, bi{m} = bim; + else, bi{1} = bi{1} + bim; + end + end + bi = cat(5, bi{:}); + else + bi = sample2sep(chd(i), tau_tx, tau_rx, kwargs.interp, kwargs.apod, sdim, kwargs.fmod, [4, 5]); % (I1 x I2 x I3 x [1|N] x [1|M] x [F x ... ]) + end % move aperture dimension to end bi = swapdim(bi, 4:5, D+(3:4)); % (I1 x I2 x I3 x 1 x 1 x [F x ... ] x [1|N] x [1|M]) @@ -3625,21 +3738,26 @@ function delete(self) cs = c0/sqrt(2); % get the frequency domains' axes with negative frequencies - f = ((0 : F - 1)' - floor(F/2)) / F * chd.fs ; % T x 1 - temporal frequencies - kx = ((0 : K - 1)' - floor(K/2)) / K / self.xdc.pitch; % K x 1 - lateral spatial frequencies + f = ((0 : F - 1) - floor(F/2)) / F * chd.fs ; % 1 x T - temporal frequencies + kx = ((0 : K - 1) - floor(K/2)) / K / self.xdc.pitch; % 1 x K - lateral spatial frequencies % move to dimensions aligned to the data - f = shiftdim(f , 1-chd.tdim); - kx = shiftdim(kx, 1-chd.ndim); + f = swapdim(f , 2, chd.tdim); + kx = swapdim(kx, 2, chd.ndim); + + % get array elements + pn = self.xdc.positions; + x0 = pn(1,1); % element lateral start position % get transmit mapping in compatiable dimensions sq = copy(self.seq); sq.c0 = c0; + th0 = self.xdc.rot(1); % array orientation (azimuth) tau = sq.delays(self.xdc); % N x M ord = [chd.ndim, chd.mdim]; % send to these dimensions ord = [ord, setdiff(1:max(ord), ord)]; % account for all dimensions tau = ipermute(tau, ord); % permute to compatible dimensions - gamma = swapdim(sind(sq.angles) ./ (2 - cosd(sq.angles)), 2, chd.mdim); % lateral scaling + gamma = swapdim(sind(sq.angles-th0) ./ (2 - cosd(sq.angles-th0)), 2, chd.mdim); % lateral scaling % splice the data to operate per block of transmits [chds, ix] = splice(chd, chd.mdim, kwargs.bsize); % split into groups of data @@ -3690,7 +3808,7 @@ function delete(self) % get the spatial axes zax = sq.c0 ./ 2 .* tb; - xax = self.xdc.pitch .* (0 : K-1) + sub(self.xdc.positions,{1,1},[1,2]); + xax = self.xdc.pitch .* (0 : K-1) + x0; % align data laterally using Garcia's PWI mapping b = b .* exp(2j*pi*kx.*gamma{j}.*zax); @@ -3708,7 +3826,11 @@ function delete(self) if kwargs.keep_tx, b = cat(chd0.mdim, bm{:}); else, b = bm; end % create the corresponding scan - it aligns with our data - bscan = ScanCartesian('z', double(zax(1:chd0.T)), 'x', xax(1:chd0.N)); + bscan = ScanCartesian( ... + 'z', self.xdc.offset(3) + double(zax(1:chd0.T)), ... + 'x', xax(1:chd0.N), ... + 'y', self.xdc.offset(2) ... + ); % work-around: sometimes the numerical precision is % insufficient and interp2 is thrown off: ensure that the data @@ -3736,11 +3858,11 @@ function delete(self) % apodization functions methods function apod = apScanline(us, tol) - % APSCANLINE Create scanline apodization array + % APSCANLINE - Create scanline apodization array % - % apod = APSCANLINE(us) creates an ND-array - % to mask delayed data using the UltrasoundSystem self in order - % to form an image using scanlines. + % apod = APSCANLINE(us) creates an ND-array apod to mask + % data using the UltrasoundSystem us in order to form an image + % using scanlines. % % Scanline apodization is determined by accepting only % transmits and scanlines that are aligned across the transmit @@ -3786,36 +3908,33 @@ function delete(self) arguments us (1,1) UltrasoundSystem - tol {mustBePositive, mustBeScalarOrEmpty} = min(abs(diff(sub(us.seq.focus,1,1)))); % numerical tolerance + tol (1,1) {mustBePositive} = us.scan.("d"+scanlat(us.scan)); % numerical tolerance - e.g. us.scan.dx: assumes 2nd index the index of change end - % alias - sq = us.seq; - % soft validate the transmit sequence type: it should be focused - if sq.type ~= "VS", warning(... - "Expected sequence type to be VS but instead got " + sq.type + ": This may produce unexpected results."... + styps = ["VS", "FC", "DV"]; % valid sequence types + if all(us.seq.type ~= styps), warning(... + "Expected a Sequence of type " + join(styps, " or ") + ... + ", but instead got a Sequence of type " + us.seq.type + ": This may produce unexpected results." ... ); end % create a mask such that the transmit and pixel lateral % positions must 'match' if isa(us.scan, 'ScanCartesian') - xdim = find(us.scan.order == 'X'); % dimension of change in lateral - xi = shiftdim(us.scan.x(:), xdim-1); % lateral per pixel - xv = swapdim(sub(sq.focus,1,1), 2, 5); % 1 x 1 x 1 x 1 x M + xi = swapdim(us.scan.x, 2, us.scan.xdim); % lateral per pixel + xv = swapdim(sub(us.seq.focus,1,1), 2, 5); % 1 x 1 x 1 x 1 x M elseif isa(us.scan, 'ScanPolar') - xdim = find(us.scan.order == 'A'); % dimension of change in azimuth - xi = shiftdim(us.scan.a(:), xdim-1); % angle per pixel - xv = swapdim(sq.angles, 2, 5); % 1 x 1 x 1 x 1 x M + xi = swapdim(us.scan.a, us.scan.adim); % angle per pixel + xv = swapdim(us.seq.angles, 2, 5); % 1 x 1 x 1 x 1 x M end apod = abs(xi - xv) < tol; % create mask end function apod = apMultiline(us) - % APMULTILINE Create multi-line apodization array + % APMULTILINE - Create a multi-line apodization array % - % apod = APMULTILINE(us) creates an ND-array + % apod = APMULTILINE(us) creates an ND-array apod % to mask delayed data using the UltrasoundSystem us in order % to form an image using scanlines. % @@ -3861,26 +3980,24 @@ function delete(self) us (1,1) UltrasoundSystem end - % alias - sq = us.seq; - % soft validate the transmit sequence type: it should be focused - if sq.type ~= "VS", warning(... - "Expected sequence type to be VS but instead got " + sq.type + ": This may produce unexpected results."... + styps = ["VS", "FC", "DV"]; % valid sequence types + if all(us.seq.type ~= styps), warning(... + "Expected a Sequence of type " + join(styps, " or ") + ... + ", but instead got a Sequence of type " + us.seq.type + ": This may produce unexpected results." ... ); end % extract lateral or angle of transmit in order to compare if isa(us.scan, 'ScanCartesian') - xdim = find(us.scan.order == 'X'); % dimension of change in lateral - xi = shiftdim(us.scan.x(:), xdim-1); % lateral per pixel - xv = swapdim(sub(sq.focus,1,1), 2, 5); % 1 x 1 x 1 x 1 x M + xdim = us.scan.xdim; % dimension of change + xi = swapdim(us.scan.x, 2, us.scan.xdim); % lateral per pixel + xv = swapdim(sub(us.seq.focus,1,1), 2, 5); % 1 x 1 x 1 x 1 x M elseif isa(us.scan, 'ScanPolar') - xdim = find(us.scan.order == 'A'); % dimension of change in azimuth - xi = shiftdim(us.scan.a(:), xdim-1); % angle per pixel - xv = swapdim(sq.angles, 2, 5); % 1 x 1 x 1 x 1 x M + xdim = us.scan.adim; % dimension of change + xi = swapdim(us.scan.a, us.scan.adim); % angle per pixel + xv = swapdim(us.seq.angles, 2, 5); % 1 x 1 x 1 x 1 x M end - X = us.scan.size(xdim); % TODO: switch this to accept multiple transmits instead of % just left/right transmit @@ -3905,7 +4022,7 @@ function delete(self) [a_l(ind0), a_r(ind0)] = deal(1, 0); % set left to 1, right to 0 % build Tx x Rx apodization matrix - apod = zeros([X, sq.numPulse]); % build in reduced dimensions + apod = zeros([us.scan.size(xdim), us.seq.numPulse]); % build in reduced dimensions alind = sub2ind(size(apod), find(val), lind); % left matrix indices arind = sub2ind(size(apod), find(val), rind); % right matrix indices apod(alind) = apod(alind) + a_l; % add left apod @@ -3914,13 +4031,13 @@ function delete(self) end function apod = apTranslatingAperture(us, tol) - % APTRANSLATINGAPERTURE Create translating aperture apodization array - % - % apod = APTRANSLATINGAPERTURE(us, tol) - % creates an ND-array to mask delayed data from the - % UltrasoundSystem us with a translating aperture of size tol. - % The default is 1/4 the length of the aperture. + % APTRANSLATINGAPERTURE - Create translating aperture apodization array % + % apod = APTRANSLATINGAPERTURE(us) creates an ND-array apod to + % mask data from the UltrasoundSystem us in order to emulate a + % translating transmit aperture beamformer configuration. The + % transmit Sequence us.seq must be a focused. + % % If us.scan is a ScanCartesian, us.rx must be a % TransducerArray and the aperture is limited to receive % elements that are within tol of the focus laterally. @@ -3965,35 +4082,40 @@ function delete(self) arguments us (1,1) UltrasoundSystem - tol (1,1) {mustBePositive} = max(range(us.rx.bounds,2)) / 4; % defaults to 1/4 aperture + tol (1,2) {mustBePositive} = us.scan.("d"+scanlat(us.scan)); % numerical tolerance - e.g. us.scan.dx + end + + % soft validate the transmit sequence type: it should be focused + styps = ["VS", "FC", "DV"]; % valid sequence types + if all(us.seq.type ~= styps), warning(... + "Expected a Sequence of type " + join(styps, " or ") + ... + ", but instead got a Sequence of type " + us.seq.type + ": This may produce unexpected results." ... + ); end % extract lateral or angle of transmit in order to compare if isa(us.scan, 'ScanCartesian') - xdim = find(us.scan.order == 'X'); % dimension of change in lateral - xi = shiftdim(us.scan.x(:), xdim-1); % lateral per pixel - xv = swapdim(sub(us.seq.focus,1,1), 2, 5); % lateral per transmit 1 x 1 x 1 x 1 x M % soft error on transducer type - if isa(us.rx, 'TransducerArray'), else, warning( ... + if ~isa(us.rx, 'TransducerArray'), warning( ... "Expected a TransducerArray but instead got " + class(us.rx) + ": This may produce unexpected results."... ); end %#ok + xi = swapdim(us.scan.x, 2, us.scan.xdim); % lateral per pixel + xv = swapdim(sub(us.seq.focus,1,1), 2, 5); % lateral per transmit 1 x 1 x 1 x 1 x M xn = swapdim(sub(us.rx.positions,1,1), 2,4); % lateral per receiver elseif isa(us.scan, 'ScanPolar') - xdim = find(us.scan.order == 'A'); % dimension of change in azimuth - xi = shiftdim(us.scan.a(:), xdim-1); % angle per pixel - xv = swapdim(us.seq.angles, 2, 5); % angle per transmit 1 x 1 x 1 x 1 x M % soft error on transducer type - if isa(us.rx, 'TransducerConvex'), else, warning( ... - "Expected a TransducerArray but instead got " + class(us.rx) + ": This may produce unexpected results."... + if ~isa(us.rx, 'TransducerConvex'), warning( ... + "Expected a TransducerConvex but instead got " + class(us.rx) + ": This may produce unexpected results."... ); end %#ok - xn = swapdim(us.rx.orientations,2,4); % lateral per receiver + xi = swapdim(us.scan.a, us.scan.adim); % angle per pixel + xv = swapdim(us.seq.angles, 2, 5); % angle per transmit 1 x 1 x 1 x 1 x M + xn = swapdim(us.rx.orientations,2,4); % angle per receiver end apod = abs(xi - xv) <= tol(1) & abs(xi - xn) <= tol(end); % create mask - end function apod = apApertureGrowth(us, f, Dmax) - % APAPERTUREGROWTH Create an aperture growth aperture apodization array + % APAPERTUREGROWTH - Create an aperture growth aperture apodization array % % apod = APAPERTUREGROWTH(us) creates an % ND-array to mask delayed data using the transmit Sequence seq @@ -4065,8 +4187,8 @@ function delete(self) Pn = swapdim(us.rx.positions, 2, 5); % (3 x 1 x 1 x 1 x N) % get the pixel positions in the proper dimensions - Xi = swapdim(us.scan.x(:), 1, 1+us.scan.xdim); % (1 x I1 x I2 x I3) - Zi = swapdim(us.scan.z(:), 1, 1+us.scan.zdim); % (1 x I1 x I2 x I3) + Xi = swapdim(us.scan.x, 2, 1+us.scan.xdim); % (1 x 1 x I2 x 1) + Zi = swapdim(us.scan.z, 2, 1+us.scan.zdim); % (1 x I1 x 1 x 1) % get the equivalent aperture width (one-sided) and pixel depth % d = sub(Pn - Pv, 1, 1); % one-sided width where 0 is aligned with transmit @@ -4080,7 +4202,7 @@ function delete(self) end function apod = apAcceptanceAngle(us, theta) - % APACCEPTANCEANGLE Create an acceptance angle apodization array + % APACCEPTANCEANGLE - Create an acceptance angle apodization array % % apod = APACCEPTANCEANGLE(us) creates an ND-array to % mask delayed data from the UltrasoundSystem us which includes @@ -4090,9 +4212,9 @@ function delete(self) % apod = APACCEPTANCEANGLE(us, theta) uses an acceptance angle % of theta in degrees. The default is 45. % - % The output apod has dimensions I1 x I2 x I3 x N x M where + % The output apod has dimensions I1 x I2 x I3 x N x 1 where % I1 x I2 x I3 are the dimensions of the scan, N is the number - % of receive elements, and M is the number of transmits. + % of receive elements. % % Example: % @@ -4107,7 +4229,7 @@ function delete(self) % % Compute the image % chd = greens(us, scat); % compute the response % chd = hilbert(zeropad(singleT(chd), 0, max(0, chd.T - 2^9))); % precondition the data - % apod = apApertureGrowth(us, 2); % use a f# of 2 + % apod = apAcceptanceAngle(us, 25); % use a limit of 25 degrees % b0 = DAS(us, chd, 'apod', 1); % beamform the data w/o apodization % ba = DAS(us, chd, 'apod', apod); % beamform the data with apodization % @@ -4119,7 +4241,7 @@ function delete(self) % % bima = mod2db(ba); % log-compression % nexttile(); imagesc(us.scan, bima, [-80 0] + max(bima(:))); - % colormap gray; colorbar; title("Aperture growth apodization"); + % colormap gray; colorbar; title("Acceptance angle apodization"); % % See also ULTRASOUNDSYSTEM/APAPERTUREGROWTH @@ -4130,9 +4252,9 @@ function delete(self) end % get the receiver positions and orientations, N in dim 5 - Pn = swapdim(us.rx.positions , 2, 5); % (3 x 1 x 1 x 1 x N) % positions + Pn = swapdim(us.rx.positions, 2, 5); % (3 x 1 x 1 x 1 x N) % positions [~,~,n] = us.rx.orientations; - n = swapdim(n, 2, 5); % (3 x 1 x 1 x 1 x N) % normals + n = swapdim(n, 2, 5); % (3 x 1 x 1 x 1 x N) % element normals % get the image pixels Pi = us.scan.positions(); % (3 x I1 x I2 x I3) @@ -4147,22 +4269,9 @@ function delete(self) r = pagemtimes(n, 'transpose', r, 'none'); r = reshape(r, size(r,2:6)); % -> (I1 x I2 x I3 x N x 1) - % accept if greater than the cutoff + % accept if greater than the cutoff angle apod = r >= cosd(theta); - - % ----------------------- LEGACY --------------------- % - % get the receiver positions and orientations, N in dim 5 - % Pn2 = swapdim(us.rx.positions , 2, 4); % (3 x 1 x 1 x N) - % thn = swapdim(us.rx.orientations, 2, 4); % (3 x 1 x 1 x N) - - % get the points as a variance in depth and lateral - % [Xi, ~, Zi] = us.scan.getImagingGrid(); % (I1 x I2 x I3) - - % restrict to points where the angle is less than theta at the - % receiver - % thi = atan2d(Xi - sub(Pn2,1,1), Zi - sub(Pn2,3,1)); % angle at which the ray hits the element - % apod = abs(thi - thn) <= theta; % (I1 x I2 x I3 x N x M) - end + end end % dependent methods @@ -4361,7 +4470,8 @@ function delete(self) end % get the other sizes for beamform.m - VS = ~(self.seq.type == "PW"); % whither virtual source + VS = ~(self.seq.type == "PW"); % whether virtual source or plane-wave + DV = any(self.seq.type == ["DV", "FSA"]); % whether virtual source or plane-wave Isz = self.scan.size; % size of the scan N = self.rx.numel; % number of receiver elements M = self.seq.numPulse; % number of transmits @@ -4375,6 +4485,7 @@ function delete(self) def.DefinedMacros, ... keep the current defs "QUPS_" + {... prepend 'QUPS_' "VS="+VS,... virtual source model + "DV="+DV,... diverging wave model "N="+N,... elements "M="+M,... transmits "I1="+Isz(1),... pixel dim 1 @@ -4630,6 +4741,7 @@ function delete(self) end end +% defaults function tmp = mktempdir() tmp = tempname(); % new folder try @@ -4652,9 +4764,18 @@ function delete(self) end end +function lat_name = scanlat(scan) +arguments, scan (1,1) Scan, end +if isa(scan, 'ScanCartesian'), lat_name = "x"; +elseif isa(scan, 'ScanPolar'), lat_name = "a"; +elseif isa(scan, 'ScanGeneric'), lat_name = "v"; +elseif isa(scan, 'ScanSpherical'), lat_name = "a"; +end +end + % validator function mustBeArch(s) if ~startsWith(s, "compute_") error("Nvidia architectures must start with 'compute_'."); end -end \ No newline at end of file +end diff --git a/src/Waveform.m b/src/Waveform.m index cb21bbad..8d91a2e2 100644 --- a/src/Waveform.m +++ b/src/Waveform.m @@ -407,7 +407,7 @@ arguments this (1,1) Waveform - that (1,1) Waveform + that (1,1) Waveform = this fs double = max([this.fs, that.fs]) end @@ -448,6 +448,74 @@ % wv = Waveform('t0', 0, 'tend', 0, 'fun', @(t) t == 0); end + + function [wvtri, wvm1wy, wvm2wy] = Verasonics(TW, fc) + % Verasonics - Create a Waveform from a Verasonics TW struct + % + % wvtri = Waveform.Verasonics(TW, fc) creates a Waveform wvtri + % of the voltage excitation signal from the transmit waveform + % struct TW and the central frequency in Hz fc. + % + % wvtri = Waveform.Verasonics(TW) where TW represents all + % Parametric waveforms uses the frequency from the Parametric + % property. + % + % [wvtri, wvm1wy] = Waveform.Verasonics(...) additionally + % returns the 1-way waveform wvm1wy. + % + % [wvtri, wvm1wy, wvm2wy] = Waveform.Verasonics(...) + % additionally returns the 2-way waveform wvm2wy. + % + % Example: + % % Import the waveforms + % fc = 1e6*Trans.frequency; % reference frequency + % [wvt,~,wv2] = Waveform.Verasonics(TW, fc); + % + % % Display the waveforms + % figure; + % plot(wvt, '.-'); + % hold on; + % plot(wv2, '.-'); + % + % See also SCAN.VERASONICS SEQUENCE.VERASONICS + + arguments + TW struct + fc {mustBeNumeric, mustBePositive} = 1e6*arrayfun(@(t) t.Parameters(1), TW); + end + + % short-circuit on empty inputs + if isempty(TW) + [wvtri, wvm1wy, wvm2wy] = deal(reshape(Waveform.empty,size(TW))); + return; + end + + % identify tri-level field name + fld = "TriLvlWvfm" + ["", "_Sim"]; % potential field names + f = fld(isfield(TW, fld)); % find which one + if ~isscalar(f) % must have one or the other + error("QUPS:Verasonics:ambiguousProperty", ... + "TW's properties must include exactly 1 of '" + join(fld, "' or '") + "'." ... + ); + end + + % start time(s) + t02 = - [TW.peak] ./ (fc(:)'); + t01 = t02 ./ 2; + t0t = - cellfun(@(h) median(find(logical(h))) ./ 250e6, {TW.(f)}); + + % Sampled waveform constructor + wvfun = @(t0, T, w) Waveform( ... + "t0", t0, "fs", 250e6, "dt", 4e-9, "tend", t0 + (T-1)*4e-9, ... + "t", t0 + (0:T-1)*4e-9, "samples", w ... + ); + + % create Waveforms + Ts = {TW.numsamples}; % signal length + wvm1wy = reshape(cellfun(wvfun,num2cell(t01), Ts, {TW.Wvfm1Wy}), size(TW)); % one-way + wvm2wy = reshape(cellfun(wvfun,num2cell(t02), Ts, {TW.Wvfm2Wy}), size(TW)); % two-way + wvtri = reshape(cellfun(wvfun,num2cell(t0t), Ts, {TW.(f) }), size(TW)); % voltage + end end end diff --git a/src/bf.cu b/src/bf.cu index 36ad1959..d44551f6 100644 --- a/src/bf.cu +++ b/src/bf.cu @@ -99,7 +99,7 @@ void __device__ DAS_temp(U2 * __restrict__ y, rv = pi[i] - pvm; // (virtual) transmit to pixel vector dv = QUPS_VS ? // tx path length - copysign(length(rv), dot(rv, nv[m])) // virtual source + copysign(length(rv), (QUPS_DV ? 1.f : dot(rv, nv[m]))) // virtual source : dot(rv, nv[m]); // plane wave dr = length(pi[i] - pr[n]); // rx path length @@ -230,7 +230,7 @@ __global__ void delaysf(float * __restrict__ tau, rv = pi[i] - pv[m]; // (virtual) transmit to pixel vector dv = QUPS_VS ? // tx path length - copysign(length(rv), dot(rv, nv[m])) // virtual source + copysign(length(rv), (QUPS_DV ? 1.f : dot(rv, nv[m]))) // virtual source : dot(rv, nv[m]); // plane wave dr = length(pi[i] - pr[n]); // rx path length @@ -275,7 +275,7 @@ __global__ void delays(double * __restrict__ tau, rv = pi[i] - pv[m]; // (virtual) transmit to pixel vector dv = QUPS_VS ? // tx path length - copysign(length(rv), dot(rv, nv[m])) // virtual source + copysign(length(rv), (QUPS_DV ? 1.f : dot(rv, nv[m]))) // virtual source : dot(rv, nv[m]); // plane wave dr = length(pi[i] - pr[n]); // rx path length diff --git a/src/interpd.cu b/src/interpd.cu index 08c5e0e1..a94d8436 100644 --- a/src/interpd.cu +++ b/src/interpd.cu @@ -99,6 +99,7 @@ __device__ T2 cubic(const T2 * x, U tau, T2 no_v) { T2 s3 = x[ti + 2]; // Cubic Hermite interpolation (increased precision using fused multiply-adds) + // (Catmull-Rom) U a0 = 0 + u * (-1 + u * (+2 * u - 1)); U a1 = 2 + u * (+0 + u * (-5 * u + 3)); U a2 = 0 + u * (+1 + u * (+4 * u - 3)); @@ -220,7 +221,7 @@ inline __device__ half2 ui2h(const unsigned int i){ half2 h; } v; v.i = i; - return __halves2half2(__ushort_as_half(v.h.x), __ushort_as_half(v.h.y)); + return __halves2half2(v.h.x, v.h.y); } inline __device__ unsigned int h2ui(const half2 a){ @@ -269,15 +270,20 @@ __device__ size_t global_offset(size_t * dind, const size_t * sizes, const char // global index // init size_t dsz[3] = {1,1,1}; // {I,N,F} index cumulative sizes - size_t sz = 1, j = 0; + size_t str, j = 0; // stride, output index // find offset - for(size_t s = 0; s < QUPS_S; ++s){ - const char iflg = iflags[s]; // which label - dsz[iflg] *= sizes[s]; // increase size for this label - j += sz * (dind[iflg] % dsz[iflg]); // add offset - dind[iflg] /= dsz[iflg]; // fold index - sz *= sizes[s]; // increase indexing stride + # pragma unroll + for(char i = 0; i < 3; ++i){ // each label + str = 1; // reset stride + for(size_t s = 0; s < QUPS_S; ++s){ // for each data dimension + if(i == iflags[s]){ // matching label + const size_t k = (dind[i] / dsz[i]) % sizes[s]; // get sub-index + dsz[i] *= sizes[s]; // increase size for this label + j += str * k; // add offset + } + str *= sizes[s]; // increase indexing stride + } } return j; diff --git a/src/sizes.cu b/src/sizes.cu index 2c70c305..ce08978a 100644 --- a/src/sizes.cu +++ b/src/sizes.cu @@ -16,7 +16,10 @@ __constant__ size_t D; // number of points in the spatial integration // Beamforming transmit mode: focal point or plane-wave # ifndef QUPS_VS -__constant__ bool QUPS_VS; // whither virtual source mode +__constant__ bool QUPS_VS; // whether virtual source or plane wave mode +# endif +# ifndef QUPS_DV +__constant__ bool QUPS_DV; // whether diverging or focused wave mode # endif # ifndef QUPS_T diff --git a/utils/animate.m b/utils/animate.m index b935a81d..00129dde 100644 --- a/utils/animate.m +++ b/utils/animate.m @@ -1,13 +1,15 @@ -function [mvf, mvh] = animate(h, x, kwargs) +function [mvf, mvh] = animate(x, h, kwargs) % ANIMATE - Animate imagesc data % -% ANIMATE(h, x) animates the multi-dimensional data x by looping through +% ANIMATE(x, h) animates the multi-dimensional data x by looping through % the upper dimensions and iteratively updating the image handle h. The % data will be plotted until the figure is closed. % % If h is an array of image handles and x is a corresponding cell array of % image data, each image h(i) will be updated by the data x{i}. % +% If x or x{i} is complex, the magnitude in dB will be displayed. +% % ANIMATE(..., 'loop', false) plays through the animation once, rather than % looping until the figure is closed. % @@ -20,7 +22,7 @@ % [mvf, mvh] = ANIMATE(...) returns a cell matrix of movie frames for each % axes. This can be used to construct a movie or gif of each axes. % -% Note: MATLAB execution will be paused while the animation is playing. +% NOTE: MATLAB execution will continue indefinitely while the animation is playing. % Close the figure or press 'ctrl + c' in the command window to stop the % animation. % @@ -28,36 +30,34 @@ % % Simulate some data % us = UltrasoundSystem(); % get a default system % us.fs = single(us.fs); % use single precision for speed -% us.sequence = SequenceRadial('type', 'PW', 'angles', -21:0.5:21); -% scat = Scatterers('pos', [0;0;30e-3], 'c0', us.sequence.c0); % define a point target +% us.seq = SequenceRadial('type', 'PW', 'angles', -21:0.5:21); +% scat = Scatterers('pos', [0;0;30e-3], 'c0', us.seq.c0); % define a point target % chd = greens(us, scat); % simulate the ChannelData % % % Configure the image of the Channel Data % figure; -% chd_im = mod2db(chd); % nexttile(); -% h = imagesc(chd_im); % initialize the image +% h = imagesc(chd); % initialize the image % caxis(max(caxis) + [-60 0]); % 60 dB dynamic range % colorbar; % title('Channel Data per Transmit'); % % % Animate the data across transmits -% animate(h, chd_im.data, 'loop', false); % show once +% animate(chd.data, h, 'loop', false); % show once % % % Beamform the data -% b = DAS(us, chd, 'keep_tx', true); % B-mode image -% bim = mod2db(b); % log-compression / envelope detection +% b = DAS(us, chd, 'keep_tx', true); % B-mode images per tx % % % Initialize the B-mode image % nexttile(); -% h(2) = imagesc(us.scan, bim(:,:,1)); % show the first image +% h(2) = imagesc(us.scan, b); % show the center tx % colormap(h(2).Parent, 'gray'); % caxis(max(caxis) + [-60 0]); % 60 dB dynamic range % colorbar; % title('B-mode per Transmit'); % % % Animate both images across transmits -% mvf = animate(h, {chd_im.data, bim}, 'loop', false); % show once +% mvf = animate({chd.data, b}, h, 'loop', false); % show once % % % Create a movie % vobj = VideoWriter('tmp', 'Motion JPEG AVI'); @@ -68,8 +68,8 @@ % % See also IMAGESC FRAME2GIF arguments - h (1,:) matlab.graphics.primitive.Image - x {mustBeA(x, ["cell","gpuArray","double","single","logical","int64","int32","int16","int8","uint64","uint32","uint16","uint8"])} = 1 % data + x {mustBeA(x, ["cell","gpuArray","double","single","logical","int64","int32","int16","int8","uint64","uint32","uint16","uint8"])} % data + h (1,:) matlab.graphics.primitive.Image = inferHandles(x) kwargs.fs (1,1) {mustBePositive} = 20; % refresh rate (hertz) kwargs.loop (1,1) logical = true; % loop until cancelled end @@ -109,6 +109,7 @@ for m = 1:M if ~all(isvalid(h)), break; end for i = 1:I, if isreal(x{i}), h(i).CData(:) = x{i}(:,:,m); else, h(i).CData(:) = mod2db(x{i}(:,:,m)); end, end% update image + % if isa(h, 'matlab.graphics.chart.primitive.Surface'), h(i).ZData(:) = h(i).CData(:); end % TODO: integrate surfaces if m == 1, drawnow; getframe(); end % toss a frame to avoid bug where the first frame has a different size drawnow limitrate; tic; @@ -119,4 +120,42 @@ if ~kwargs.loop, break; end end -end \ No newline at end of file +function him = inferHandles(x) +% get the current figure +hf = gcf(); +if isempty(hf.Children) % new figure; no axes + % create images for this data + if isnumeric(x) || islogical(x), x = {x}; end % -> cell + + % use a tiledlayout by default + htl = tiledlayout(hf, 'flow'); + + % squeeze data into first 2 dims + x = cellfun(@squeeze, x, 'UniformOutput', false); + + % take modulos of complex data + val = cellfun(@isreal, x); + x(~val) = cellfun(@mod2db, x(~val), 'UniformOutput', false); + + % make images + him = cellfun(@(x) imagesc(nexttile(htl), x(:,:,1)), x); + + % done + return; + +elseif isa(hf.Children, 'matlab.graphics.layout.TiledChartLayout') + % parse the tree structure to extract axes handles + hax = hf.Children.Children; +elseif any(arrayfun(@(h) isa(h, 'matlab.graphics.axis.Axes'), hf.Children)) % (sub)plot(s) + hax = hf.Children; +else + error("Unable to infer plot handle; please explictly pass the handle.") +end + +% parse to grab image handles in same order as the data +hax = hax(arrayfun(@(hax)isa(hax, 'matlab.graphics.axis.Axes'), hax)); % axes only +him = {hax.Children}; % image and plot handles +him = cellfun(@(h) {h(arrayfun(@(h)isa(h, 'matlab.graphics.primitive.Image'),h))}, him); +him = flip([him{:}]); % image handle array, in order of creation + +% TODO: check sizing of data versus image handles? diff --git a/utils/struct2nvpair.m b/utils/struct2nvpair.m index 0e98c5df..f5588e82 100644 --- a/utils/struct2nvpair.m +++ b/utils/struct2nvpair.m @@ -12,5 +12,5 @@ % % See also STRUCT STRUCT2CELL FIELDNAMES function nv = struct2nvpair(s), arguments, s (1,1) struct, end -nv = cat(1, shiftdim(fieldnames(s),-1), shiftdim(struct2cell(s),-1)); +nv = reshape(namedargs2cell(s), 2, []); end \ No newline at end of file diff --git a/utils/sub.m b/utils/sub.m index 3d8de412..14e0d2df 100644 --- a/utils/sub.m +++ b/utils/sub.m @@ -3,7 +3,8 @@ % y = SUB(x, ind) returns x(ind,:,:,...,:) where x is any subscriptable % object. % -% y = SUB(x, ind, dim) slices x in dimension dim instead of dimension 1 +% y = SUB(x, ind, dim) slices x in dimension dim. The default is the first +% non-singleton dimension. % % y = SUB(x, vecind, vecdim) slices x indices in each specified dimension % in vecdim i.e. returns x(:,...,:,ind,:,...,:,ind,:,...,:). vecind must be @@ -20,8 +21,7 @@ % See also SEL SUBSREF SUBSASGN SUBSTRUCT function y = sub(x, ind, dim, expr) -% default to dim 1. TODO: default to 1st non-singleton -if nargin < 3, dim = 1; end +if nargin < 3, dim = max([1, find(size(x) ~= 1, 1, 'first')]); end if nargin < 4, expr = false; end % ensure indices placed in a cell