From fbb226b1cea565f4e56ad413c02dd1af0886c549 Mon Sep 17 00:00:00 2001 From: Thurston Date: Wed, 12 Jul 2023 12:33:41 -0700 Subject: [PATCH 01/50] Update documentation. --- src/ChannelData.m | 17 ++++++++--------- src/Sequence.m | 25 ++++++++++++++++++------- src/UltrasoundSystem.m | 36 ++++++++++++++++++++++++++++++------ 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/ChannelData.m b/src/ChannelData.m index fc38d783..bcc2cd13 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 diff --git a/src/Sequence.m b/src/Sequence.m index 3636b36c..e52155ad 100644 --- a/src/Sequence.m +++ b/src/Sequence.m @@ -275,14 +275,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 @@ -329,9 +329,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]); diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index bb386029..dc37d638 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -720,6 +720,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 +745,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 @@ -4102,9 +4126,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: % @@ -4119,7 +4143,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 % @@ -4131,7 +4155,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 @@ -4142,9 +4166,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) From 9d5056f6eef120c86ccf6bc06019bfbf6de38882 Mon Sep 17 00:00:00 2001 From: Thurston Date: Wed, 12 Jul 2023 12:35:27 -0700 Subject: [PATCH 02/50] interpd.cu : half type atomicAdd bug fix. --- src/interpd.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interpd.cu b/src/interpd.cu index 08c5e0e1..cd8e3ef7 100644 --- a/src/interpd.cu +++ b/src/interpd.cu @@ -220,7 +220,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){ From c4ab158adc35839d201d333c36fd6a5b59c4a0fc Mon Sep 17 00:00:00 2001 From: Thurston Date: Thu, 13 Jul 2023 11:30:01 -0700 Subject: [PATCH 03/50] UltrasoundSystem.ap*() apodization generator functions refactored, changed defaults, and updated documentation. --- src/UltrasoundSystem.m | 127 +++++++++++++++++++---------------------- 1 file changed, 59 insertions(+), 68 deletions(-) diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index dc37d638..04cc9309 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -3772,11 +3772,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 @@ -3822,36 +3822,31 @@ 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."... + if us.seq.type ~= "VS", warning(... + "Expected sequence type to be VS but instead got " + 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. % @@ -3897,26 +3892,22 @@ 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."... + if us.seq.type ~= "VS", warning(... + "Expected sequence type to be VS but instead got " + 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 @@ -3941,7 +3932,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 @@ -3950,13 +3941,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. @@ -4001,35 +3992,38 @@ 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 + if us.seq.type ~= "VS", warning(... + "Expected sequence type to be VS but instead got " + 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 @@ -4101,8 +4095,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 @@ -4116,7 +4110,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 @@ -4183,22 +4177,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 @@ -4666,6 +4647,7 @@ function delete(self) end end +% defaults function tmp = mktempdir() tmp = tempname(); % new folder try @@ -4688,6 +4670,15 @@ 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_") From 246c23fc412e2f17bad7b3d7abe5f81596eebb9d Mon Sep 17 00:00:00 2001 From: Thurston Date: Thu, 13 Jul 2023 11:30:48 -0700 Subject: [PATCH 04/50] Scatterers.plot() : updated default marker to '.' --- src/Scatterers.m | 3 +++ 1 file changed, 3 insertions(+) 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{:}); From 428951419b8117e3d4f413d16e668f73e7271979 Mon Sep 17 00:00:00 2001 From: Thurston Date: Thu, 13 Jul 2023 11:31:20 -0700 Subject: [PATCH 05/50] UltrasoundSystem, Sequence : Updated documentation. --- src/Sequence.m | 27 ++++++++++++------- src/UltrasoundSystem.m | 60 +++++++++++++++++++++++++++++------------- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/Sequence.m b/src/Sequence.m index e52155ad..d4885bb0 100644 --- a/src/Sequence.m +++ b/src/Sequence.m @@ -4,18 +4,25 @@ % 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 '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. % +% 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 diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index 04cc9309..07ba15b9 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) From 3256bedfa0c5b36f8c544089f83070530a47d8cf Mon Sep 17 00:00:00 2001 From: Thurston Date: Sun, 16 Jul 2023 15:36:19 -0700 Subject: [PATCH 06/50] animate() : changed function signature; infers or creates image handles optimistically. --- utils/animate.m | 65 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/utils/animate.m b/utils/animate.m index b935a81d..aa9cacb4 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,38 @@ 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); + + % 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 isa(hf.Children, 'matlab.graphics.axis.Axes') % (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(isa(h, 'matlab.graphics.primitive.Image'))}, him); +him = flip([him{:}]); % image handle array, in order of creation + +% TODO: check sizing of data versus image handles? From eab55db25ec6d6a6e7a20c5ee739d9399bb7854e Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Wed, 2 Aug 2023 13:10:45 -0700 Subject: [PATCH 07/50] ChannelData.hilbert() : uses native function when possible. --- src/ChannelData.m | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ChannelData.m b/src/ChannelData.m index bcc2cd13..cd6e59e2 100644 --- a/src/ChannelData.m +++ b/src/ChannelData.m @@ -495,15 +495,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 From 89ee404d38200c4ee35b181e1fa8646f6b05dd4d Mon Sep 17 00:00:00 2001 From: Thurston Date: Wed, 2 Aug 2023 13:17:38 -0700 Subject: [PATCH 08/50] ChannelData.join() : bug fix - .order property preserved. --- src/ChannelData.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ChannelData.m b/src/ChannelData.m index cd6e59e2..63dc26d3 100644 --- a/src/ChannelData.m +++ b/src/ChannelData.m @@ -1164,7 +1164,7 @@ function gif(chd, filename, h, varargin) 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 From b5a2a33fd3e83488a7da190a2dc9593e66932831 Mon Sep 17 00:00:00 2001 From: Thurston Date: Wed, 2 Aug 2023 13:18:58 -0700 Subject: [PATCH 09/50] struct2nvpair() : calls native function for performance. --- utils/struct2nvpair.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 04911d02c5c85d4bed9f1f5de982ef2f2bcbbb8f Mon Sep 17 00:00:00 2001 From: Thurston Date: Tue, 8 Aug 2023 23:14:45 -0700 Subject: [PATCH 10/50] UltrasoundSystem.refocus() : added frequency normalization; sign flip bug fix. --- src/UltrasoundSystem.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index 07ba15b9..3dab1c11 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -2755,8 +2755,8 @@ function delete(self) 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 ...) @@ -2771,7 +2771,7 @@ 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 * pagenorm(gather(H),2).^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 From 7bca4eff5a638825161d50e3b3acf2536c0b808d Mon Sep 17 00:00:00 2001 From: Thurston Date: Tue, 8 Aug 2023 23:16:04 -0700 Subject: [PATCH 11/50] UltrasoundSystem.fullwaveConf() : updated syntax. --- src/UltrasoundSystem.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index 3dab1c11..4e1dfa44 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -932,7 +932,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 From 551a5fb77758330de9736b3a82408ca5d39a3e73 Mon Sep 17 00:00:00 2001 From: Thurston Date: Wed, 9 Aug 2023 10:59:04 -0700 Subject: [PATCH 12/50] UltrasoundSystem.refocus() : delay sign flip bug fix; added normalization. --- src/UltrasoundSystem.m | 86 +++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index 4e1dfa44..c2535140 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -2702,9 +2702,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 @@ -2715,37 +2718,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 @@ -2755,12 +2766,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) % 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); @@ -2772,16 +2790,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 * pagenorm(gather(H),2).^2 .* 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 + % move inverse matrix to matching data dimensions D = max(ndims(chd.data)); 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; From 086cf3b335f8766e54beea253b20511136c50376 Mon Sep 17 00:00:00 2001 From: Thurston Date: Mon, 18 Sep 2023 15:46:31 -0700 Subject: [PATCH 13/50] Adding Sequence.Verasonics constructor. --- src/Sequence.m | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/Sequence.m b/src/Sequence.m index d4885bb0..3d8d2ba3 100644 --- a/src/Sequence.m +++ b/src/Sequence.m @@ -407,6 +407,62 @@ end end + + function seq = Verasonics(TX, Trans, Resource, TW) + ismux = isfield(TX, 'aperture'); % whether muxed or not + if ismux, ap = Trans.HVMux.ApertureES; % mux + else, ap = 1:Resource.Parameters.numTransmit; % no muxing + end + + % constants + c0 = Resource.Parameters.speedOfSound; + 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 + pog = cat(1,TX.Origin) .* lambda; % beam origin + tau = cat(1,TX.Delay ) ./ fc; % tx delays + + % create the corresponding Sequence + if isfield(TX, "FocalPt") % focal points -> VS + pf = cat(1, TX.FocalPt)' .* lambda; % focal points + seq = Sequence("type","VS", "focus", pf); + elseif ~any(tau,'all') % no delays -> FSA + M = numel(TX); + seq = Sequence("type","FSA", "numPulse",M); + elseif all(pog == 0) && all(rf == 0) && any(ang) % 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 + seq = Sequence("type","VS", "focus",pf); + else + error("Unable to infer focal sequence type."); + end + seq.c0 = c0; + + % import waveform + % TODO: validate + if nargin >= 4 + t = (0:TW.numsamples-1)' ./ 250e6; + t0 = - TW.peak ./ fc; + flds = "TriLvlWvfm" + ["", "_Sim"]; + f = flds(isfield(TW, flds)); + seq.pulse = Waveform('t', t + t0, 'samples', TW.(f), 'fs', 250e6); + end + end end % temporal response methods From 8fa2843aac1fe75dee7cfaba212d3bfb26eb91d6 Mon Sep 17 00:00:00 2001 From: Thurston Date: Mon, 18 Sep 2023 15:53:29 -0700 Subject: [PATCH 14/50] Adding Waveform.Verasonics constructor. --- src/Waveform.m | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Waveform.m b/src/Waveform.m index cb21bbad..17baa4d5 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,40 @@ % wv = Waveform('t0', 0, 'tend', 0, 'fun', @(t) t == 0); end + + function [wvtri, wvm1wy, wvm2wy] = Verasonics(TW, fc) + if nargin < 2 + if isfield(TW, 'Parameters') + warning("Inferring transducer frequency from pulse frequency. Use the second input to avoid this warning."); + fc = TW.Parameters(1); + else + error("Unable to infer pulse frequency."); + end + end + + % identify tri-leve field name + fld = "TriLvlWvfm" + ["", "_Sim"]; % potential field names + f = fld(isfield(TW, fld)); + + % start time(s) + t02 = - [TW.peak] ./ fc*1e-6 ; + t01 = t02 ./ 2; + t0t = - cellfun(@(h) median(find(logical(h))) ./ 250e6, {TW.(f)}); + + % signal length + Ts = {TW.numsamples}; + + % 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 + wvm1wy = reshape(cellfun(wvfun,num2cell(t01), Ts, {TW.Wvfm1Wy}), size(TW)); + wvm2wy = reshape(cellfun(wvfun,num2cell(t02), Ts, {TW.Wvfm2Wy}), size(TW)); + wvtri = reshape(cellfun(wvfun,num2cell(t0t), Ts, {TW.(f) }), size(TW)); + end end end From 42be477250f1fd4430567f13ef3a84f65772d6c8 Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Mon, 18 Sep 2023 17:11:36 -0700 Subject: [PATCH 15/50] Sequence.Verasonics() : bug fix for VS TX-array. --- src/Sequence.m | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Sequence.m b/src/Sequence.m index 3d8d2ba3..0d97111e 100644 --- a/src/Sequence.m +++ b/src/Sequence.m @@ -422,9 +422,9 @@ % tx params apd = cat(1,TX.Apod ); % apodization ang = cat(1,TX.Steer ); % angles - rf = cat(1,TX.focus ) .* lambda; % focal range - pog = cat(1,TX.Origin) .* lambda; % beam origin - tau = cat(1,TX.Delay ) ./ fc; % tx delays + rf = cat(1,TX.focus );% .* lambda; % focal range + pog = cat(1,TX.Origin);% .* lambda; % beam origin + tau = cat(1,TX.Delay );% ./ fc; % tx delays % create the corresponding Sequence if isfield(TX, "FocalPt") % focal points -> VS @@ -433,7 +433,7 @@ elseif ~any(tau,'all') % no delays -> FSA M = numel(TX); seq = Sequence("type","FSA", "numPulse",M); - elseif all(pog == 0) && all(rf == 0) && any(ang) % PW + elseif all(all(pog == 0,2) & all(rf == 0,1) & any(ang,2),1) % PW az = rad2deg(ang(:,1)'); % azimuth el = rad2deg(ang(:,2)'); % elevation if any(el) @@ -442,12 +442,12 @@ 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)), ... + pf = pog + rf .* [ + sin(ang(:,1)) .* cos(ang(:,2)), ... + 1 .* sin(ang(:,2)), ... + cos(ang(:,1)) .* cos(ang(:,2)), ... ]; % focal points - seq = Sequence("type","VS", "focus",pf); + seq = Sequence("type","VS", "focus", lambda * pf.'); else error("Unable to infer focal sequence type."); end From 4e128b7f3cdee62a000b111bad9829706265c48b Mon Sep 17 00:00:00 2001 From: Thurston Date: Tue, 3 Oct 2023 14:14:39 -0700 Subject: [PATCH 16/50] interp.cu() : bug fix for 3D+ fused indexing with non-uniform sizing. --- src/interpd.cu | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/interpd.cu b/src/interpd.cu index cd8e3ef7..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)); @@ -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; From 13b0ae140a66371be5bcc444d7aeb2c7088c159c Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 18:44:16 -0700 Subject: [PATCH 17/50] UltrasoundSystem.focusTx() : added block size argument. --- src/UltrasoundSystem.m | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index c2535140..b05c0def 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -2571,6 +2571,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 @@ -2581,8 +2586,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 @@ -2604,8 +2609,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 @@ -2646,14 +2652,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 From fb81eae57ede873948a65daa337b63bb1185b742 Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 18:48:26 -0700 Subject: [PATCH 18/50] Added Transducer rotation property. --- src/Transducer.m | 11 ++++++----- src/TransducerArray.m | 32 +++++++++++-------------------- src/TransducerConvex.m | 42 ++++++++++------------------------------- src/TransducerGeneric.m | 1 - src/TransducerMatrix.m | 11 ++++++----- 5 files changed, 33 insertions(+), 64 deletions(-) diff --git a/src/Transducer.m b/src/Transducer.m index 3cf04404..aaee47dc 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); 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..360cfcb0 100644 --- a/src/TransducerConvex.m +++ b/src/TransducerConvex.m @@ -77,49 +77,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 From 9d50c7eff142258284117a77da02e1aa8a86ed6b Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 18:50:00 -0700 Subject: [PATCH 19/50] ScanGeneric() : bug fix. --- src/ScanGeneric.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From 15cc14be562bafa641095a28d76cbe9a513c8db3 Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 20:24:57 -0700 Subject: [PATCH 20/50] UltrasoundSystem() : modified beamformers to handle rotation, offset - bfAdjoint still not robust for non-identity TX matrix. --- src/UltrasoundSystem.m | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index b05c0def..77173b85 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -2514,15 +2514,17 @@ 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}; 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]}; + nf = self.seq.focus - self.xdc.offset; % normal vector + pos_args = {P_im, P_rx, self.seq.focus, nf ./ norm(nf)}; end % request the CUDA kernel? @@ -3432,8 +3434,8 @@ 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 'FSA', [Pv, Nv] = deal(self.tx.positions(), argn(3, @()self.tx.orientations)); + case 'VS', [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 @@ -3715,21 +3717,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 @@ -3780,7 +3787,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); @@ -3798,7 +3805,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 From 54a5ec0ccd266b8c3a3709f2e750cbf6739e7662 Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 20:26:42 -0700 Subject: [PATCH 21/50] UltrasoundSystem() : added 'bsize' arguments for beamforming methods to control memory usage trade-off. --- src/UltrasoundSystem.m | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index 77173b85..592cb840 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -2871,8 +2871,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. % @@ -3152,6 +3152,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 @@ -3245,6 +3249,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 @@ -3350,7 +3355,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 @@ -3387,6 +3392,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 @@ -3424,6 +3433,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 @@ -3464,7 +3474,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 @@ -3511,6 +3521,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 @@ -3543,6 +3557,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 @@ -3605,7 +3620,20 @@ function delete(self) % 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]) From dada8a510d666cd07098ca469576f9bf173704e7 Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 20:27:12 -0700 Subject: [PATCH 22/50] ChannelData() : update help. --- src/ChannelData.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ChannelData.m b/src/ChannelData.m index 63dc26d3..0910cbd6 100644 --- a/src/ChannelData.m +++ b/src/ChannelData.m @@ -994,7 +994,7 @@ % See also IMAGESC arguments chd ChannelData - m {mustBeInteger} = floor((chd.M+1)/2); + m {mustBeInteger} = ceil(chd.M/2); end arguments(Repeating) varargin @@ -1180,6 +1180,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 From 5463fc1db2ed6a483f4dd67bccae4e1e0f86079a Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 20:29:25 -0700 Subject: [PATCH 23/50] UltrasoundSystem.bfDAS() : better diverging wave sequence detection. --- src/UltrasoundSystem.m | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index 592cb840..c8bfdff4 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -3456,9 +3456,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 'FSA' , dv = vecnorm(dv, 2, 1); + case {'VS'}, 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 From e30b5a98cb8ced0195fd67f95956914d79f0f94f Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 20:30:36 -0700 Subject: [PATCH 24/50] UltrasoundSystem() : ChannelData data size includes t0 dimensions. --- src/UltrasoundSystem.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index c8bfdff4..f7514604 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -2809,7 +2809,7 @@ function delete(self) end % move inverse matrix to matching data dimensions - D = max(ndims(chd.data)); + D = max(ndims(chd.data), ndims(chd.t0)); ord = [chd.mdim, D+1, chd.tdim]; ord = [ord, setdiff(1:D, ord)]; % all dimensions Hi = ipermute(Hi, ord); @@ -2990,7 +2990,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] @@ -3617,7 +3617,7 @@ 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 From d0e5e02307ad94cc4c15318249250dad995faee9 Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 20:31:35 -0700 Subject: [PATCH 25/50] UltrasoundSystem.bfAdjoint() : update arguments block. --- src/UltrasoundSystem.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index f7514604..7a08ccc1 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -2920,14 +2920,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 From 2a5d7365216e5c300f19313a47464352f75584d9 Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 20:32:10 -0700 Subject: [PATCH 26/50] TransducerConvex() : bug fix. --- src/TransducerConvex.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/TransducerConvex.m b/src/TransducerConvex.m index 360cfcb0..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 From d90b739ed3d1b2bb35c2ccadc6a9b1e0f96ea49b Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 21:16:19 -0700 Subject: [PATCH 27/50] Transducer.plot() : plotting dimensions compatible with 2D plots. --- src/Transducer.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Transducer.m b/src/Transducer.m index aaee47dc..3cc04065 100644 --- a/src/Transducer.m +++ b/src/Transducer.m @@ -981,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') From 71b0e69c3783398bd58af00e9698b1eb187d3954 Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 21:38:26 -0700 Subject: [PATCH 28/50] Updated class plotting defaults. --- src/Scan.m | 1 + src/SequenceRadial.m | 7 ++++--- src/UltrasoundSystem.m | 8 +++++--- 3 files changed, 10 insertions(+), 6 deletions(-) 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/SequenceRadial.m b/src/SequenceRadial.m index 96e58334..f27a11f8 100644 --- a/src/SequenceRadial.m +++ b/src/SequenceRadial.m @@ -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/UltrasoundSystem.m b/src/UltrasoundSystem.m index 7a08ccc1..a263efd1 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -355,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); @@ -4778,4 +4780,4 @@ function mustBeArch(s) if ~startsWith(s, "compute_") error("Nvidia architectures must start with 'compute_'."); end -end \ No newline at end of file +end From de16a932b9c5edaf5616ddb89072c720e82d293d Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 21:38:57 -0700 Subject: [PATCH 29/50] Cosmetic. --- kern/wsinterpd2.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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); From 40ab69537e3157429c910b96037519e0a1b8e9c9 Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 21:39:20 -0700 Subject: [PATCH 30/50] beamform() : bug fix - missing else statement. --- kern/beamform.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kern/beamform.m b/kern/beamform.m index b1eac0f8..12da70b8 100644 --- a/kern/beamform.m +++ b/kern/beamform.m @@ -91,6 +91,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'; From fc4d47eb445530f657b4421c37f218bc24034d1a Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 21:46:06 -0700 Subject: [PATCH 31/50] animate() : improved handle inference. --- utils/animate.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/animate.m b/utils/animate.m index aa9cacb4..5f3b90b2 100644 --- a/utils/animate.m +++ b/utils/animate.m @@ -142,7 +142,7 @@ elseif isa(hf.Children, 'matlab.graphics.layout.TiledChartLayout') % parse the tree structure to extract axes handles hax = hf.Children.Children; -elseif isa(hf.Children, 'matlab.graphics.axis.Axes') % (sub)plot(s) +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.") @@ -151,7 +151,7 @@ % 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(isa(h, 'matlab.graphics.primitive.Image'))}, him); +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? From 0234e1050dfa2b30b471b833b6bc7b2c25e146e5 Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 13 Oct 2023 22:15:12 -0700 Subject: [PATCH 32/50] dmas() : bug fix. --- kern/dmas.m | 1 - 1 file changed, 1 deletion(-) diff --git a/kern/dmas.m b/kern/dmas.m index 1e66daae..1a5f2d49 100644 --- a/kern/dmas.m +++ b/kern/dmas.m @@ -64,7 +64,6 @@ N = size(bn, dim); % aperture length for i = 1:N-1 % 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) From 35802cfe4f7b9a476c8f6f882c045d6d03bf3bcc Mon Sep 17 00:00:00 2001 From: Thurston Date: Sun, 15 Oct 2023 11:24:35 -0700 Subject: [PATCH 33/50] Updated example script; added TransducerMatrix type. --- example.mlx | Bin 11667 -> 11672 bytes example_.m | 59 ++++++++++++++++++++++------------------------------ 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/example.mlx b/example.mlx index 6575311b7852d5786407dd1a9ff11863c2ab7612..1ec9d10c5a9e817f60014e78ca175f8a5c188782 100644 GIT binary patch delta 9613 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 BMe+au diff --git a/example_.m b/example_.m index 3cdae865..7d15e27c 100644 --- a/example_.m +++ b/example_.m @@ -25,6 +25,8 @@ % 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 ! @@ -69,14 +71,19 @@ 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 +end % make 3D region % 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 @@ -134,11 +141,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 +152,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 +164,10 @@ % make a scatterer, if not diffuse if scat.numScat < 500 - s_rad = max([tscan.dx, tscan.dz]); % scatterer radius + s_rad = max([tscan.dx,tscan.dy,tscan.dz],[],'omitnan'); % 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]}}); + 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,7 +179,8 @@ % 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'); @@ -190,35 +189,26 @@ 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 +% 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 transducer -plot(xdc, 'r+', 'DisplayName', 'Elements'); % elements - -% plot the imaging region (Scan) -hps = plot(scan, 'w.', 'MarkerSize', 0.5, 'DisplayName', 'Image'); % the imaging points - -% 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 +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; @@ -237,7 +227,7 @@ % 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 @@ -279,6 +269,7 @@ switch class(xdc) case 'TransducerArray' , scale = xdc.pitch; % Definitions in elements case 'TransducerConvex', scale = xdc.angular_pitch; % Definitions in degrees + case 'TransducerMatrix', scale = min(xdc.pitch); % Definitions in elements end % Choose the apodization (beamforming weights) @@ -298,8 +289,8 @@ % 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 case "DAS" From 9401ea6c7129ada59899a5dc438af4920d0e2a6b Mon Sep 17 00:00:00 2001 From: Thurston Date: Sun, 15 Oct 2023 11:25:13 -0700 Subject: [PATCH 34/50] Updated cheat sheet. --- cheat_sheet.m | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/cheat_sheet.m b/cheat_sheet.m index a0fba8dc..ebc37d3a 100644 --- a/cheat_sheet.m +++ b/cheat_sheet.m @@ -171,10 +171,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 +187,8 @@ b = bfEikonal(us, chd, med, cscan); % frequency-domain adjoint Green's function beamformer -% (poor performance on 'VS' sequences) +% (poor performance on '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 +196,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 +218,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 +337,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 +357,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 +384,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 +432,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 +460,7 @@ % ----------- Signal processing ------------ % % sample the waveform -tau = zeros([1024, 1]); +tau = (0 : 1023) .* 0.1e-6; y = wv.sample(tau); % convolve waveforms From 226443eccfe01f38d685f1b496ff44403c348bfa Mon Sep 17 00:00:00 2001 From: Thurston Date: Sun, 15 Oct 2023 11:27:25 -0700 Subject: [PATCH 35/50] slsc() : bug fix; updated example. --- kern/slsc.m | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/kern/slsc.m b/kern/slsc.m index ec87a120..e0bd51da 100644 --- a/kern/slsc.m +++ b/kern/slsc.m @@ -62,18 +62,8 @@ % 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); @@ -113,7 +103,7 @@ 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 @@ -121,12 +111,8 @@ % 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) / K); % get weighting / filter across receiver pairs W = S ./ (A - H) / 2 / L; % weights per pair (debiased, pos & neg averaged, multi-correlation-averaged) @@ -159,8 +145,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 - From 1722a74a1ce2692e79a91ed6a34fea164251c645 Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 20 Oct 2023 07:27:35 -0700 Subject: [PATCH 36/50] dmas() : added lag argument to select aperture pairs. --- kern/dmas.m | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/kern/dmas.m b/kern/dmas.m index 1a5f2d49..27c71e5a 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); @@ -58,11 +67,13 @@ 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); end From 7b277a50df6056d40d408c83072efdc205f3769c Mon Sep 17 00:00:00 2001 From: Thurston Date: Fri, 20 Oct 2023 07:33:52 -0700 Subject: [PATCH 37/50] slsc() : bug fix (scaling with a time kernel using the average method); updated example. --- kern/slsc.m | 45 +++++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/kern/slsc.m b/kern/slsc.m index e0bd51da..948fbbaa 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.'); @@ -69,19 +69,41 @@ % 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 % defaults arguments @@ -106,13 +128,12 @@ 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) with 0/0 -> 0 - x = nan2zero(x ./ vecnorm(x,2,kdim) / K); + 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) From c7049f591b276d0f010162fe860484d1986af486 Mon Sep 17 00:00:00 2001 From: Thurston Date: Wed, 25 Oct 2023 09:30:45 -0700 Subject: [PATCH 38/50] Adding pcf() - phase coherence factor; updated documentation. --- kern/cohfac.m | 2 +- kern/dmas.m | 2 +- kern/pcf.m | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++ kern/slsc.m | 2 +- 4 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 kern/pcf.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 27c71e5a..8f967d35 100644 --- a/kern/dmas.m +++ b/kern/dmas.m @@ -62,7 +62,7 @@ % 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} 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 948fbbaa..d0039f9e 100644 --- a/kern/slsc.m +++ b/kern/slsc.m @@ -103,7 +103,7 @@ % imagesc(us.scan, real(zk)); % colorbar; % -% See also DMAS COHFAC +% See also DMAS COHFAC PCF % defaults arguments From 284dbd1bacf1ec2a44d8b017759089758657db93 Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Tue, 5 Dec 2023 12:12:52 -0800 Subject: [PATCH 39/50] Sequence type 'VS' switched to 'FC' and 'DV' to disambiguate focused versus diverging transmits. Legacy support for 'VS' maintained. --- cheat_sheet.m | 16 +- .../pulse-sequence-design/sequence_types.m | 15 +- kern/beamform.m | 21 ++- src/ChannelData.m | 3 + src/ScanCartesian.m | 8 +- src/Sequence.m | 140 ++++++++++++------ src/SequenceGeneric.m | 2 +- src/SequenceRadial.m | 2 +- src/UltrasoundSystem.m | 40 +++-- src/bf.cu | 6 +- src/sizes.cu | 5 +- 11 files changed, 172 insertions(+), 86 deletions(-) diff --git a/cheat_sheet.m b/cheat_sheet.m index ebc37d3a..c0ca5b9a 100644 --- a/cheat_sheet.m +++ b/cheat_sheet.m @@ -64,13 +64,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 +83,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 +101,7 @@ end rfocal = 60e-3; %% focal range seq = SequenceRadial( ... - 'type','VS', ... + 'type','FC', ... 'angles',tha, ... 'ranges',norm(xdc.center) + rfocal, ... 'apex',xdc.center ... @@ -187,8 +187,8 @@ b = bfEikonal(us, chd, med, cscan); % frequency-domain adjoint Green's function beamformer -% (poor performance on 'VS' sequences, convex arrays, or rotated/offset -% transducers) +% 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) 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/kern/beamform.m b/kern/beamform.m index 12da70b8..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); @@ -115,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}; @@ -277,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; @@ -356,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/src/ChannelData.m b/src/ChannelData.m index 0910cbd6..db5a1813 100644 --- a/src/ChannelData.m +++ b/src/ChannelData.m @@ -161,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("Untested code: please validate."); + t0 = t0 + vecnorm(seq.focus, 2,1) ./ seq.c0; % transform for focal point to origin case 'PW' % no action necessary 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/Sequence.m b/src/Sequence.m index 0d97111e..f846f53c 100644 --- a/src/Sequence.m +++ b/src/Sequence.m @@ -9,15 +9,16 @@ % 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 '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 '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 type 'VS', the foci are spatial positions and time 0 is when the -% wavefront passes through the foci. +% For types 'FC' (focused), 'DV' (diverging), 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. -% % 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. @@ -29,25 +30,36 @@ % 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 '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 '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 '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. @@ -57,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 @@ -185,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. % @@ -198,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 @@ -250,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 @@ -322,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 @@ -395,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)); @@ -429,7 +449,29 @@ % create the corresponding Sequence if isfield(TX, "FocalPt") % focal points -> VS pf = cat(1, TX.FocalPt)' .* lambda; % focal points - seq = Sequence("type","VS", "focus", pf); + try % attempt to infer focused or diverging wave + xdc = Transducer.Verasonics(Trans, c0); % get transducer + 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 + else + warning("QUPS:Verasonics:ambiguousSequence", ... + "Cannot infer whether sequence is focused or diverging."); + styp = "VS"; % default type + end + catch + warning("QUPS:Verasonics:ambiguousSequence", ... + "Cannot infer whether sequence is focused or diverging."); + styp = "VS"; % default type + end + seq = Sequence("type",styp, "focus", pf); elseif ~any(tau,'all') % no delays -> FSA M = numel(TX); seq = Sequence("type","FSA", "numPulse",M); @@ -447,7 +489,11 @@ 1 .* sin(ang(:,2)), ... cos(ang(:,1)) .* cos(ang(:,2)), ... ]; % focal points - seq = Sequence("type","VS", "focus", lambda * pf.'); + if all(rf > 0), styp = "FC"; % focused + elseif all(rf < 0), styp = "DV"; % diverging + else, styp = "VS"; % unclear + end + seq = Sequence("type",styp, "focus", lambda * pf.'); else error("Unable to infer focal sequence type."); end @@ -479,9 +525,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. @@ -497,13 +545,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 @@ -559,7 +609,7 @@ % % % construct the Sequence % seq = Sequence(... - % 'type', 'VS', ... + % 'type', 'FC', ... % 'focus', [0;0;30e-3] + xf .* [1;0;0] ... % ); % @@ -595,15 +645,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 @@ -624,9 +675,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 f27a11f8..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] diff --git a/src/UltrasoundSystem.m b/src/UltrasoundSystem.m index a263efd1..c42cf96b 100644 --- a/src/UltrasoundSystem.m +++ b/src/UltrasoundSystem.m @@ -2521,12 +2521,14 @@ function delete(self) case 'FSA' [~,~,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' + 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? @@ -2952,7 +2954,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.' ... ); @@ -3446,9 +3448,9 @@ function delete(self) % get virtual source or plane wave geometries switch self.seq.type - case 'FSA', [Pv, Nv] = deal(self.tx.positions(), argn(3, @()self.tx.orientations)); - case 'VS', [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 + 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 @@ -3459,9 +3461,9 @@ 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 'FSA' , dv = vecnorm(dv, 2, 1); - case {'VS'}, dv = vecnorm(dv, 2, 1) .* sign(sum(dv .* Nv,1)); - case {'PW'}, dv = sum(dv .* Nv, 1); + 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 @@ -3922,8 +3924,10 @@ function delete(self) end % soft validate the transmit sequence type: it should be focused - if us.seq.type ~= "VS", warning(... - "Expected sequence type to be VS but instead got " + us.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 @@ -3989,8 +3993,10 @@ function delete(self) end % soft validate the transmit sequence type: it should be focused - if us.seq.type ~= "VS", warning(... - "Expected sequence type to be VS but instead got " + us.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 @@ -4092,8 +4098,10 @@ function delete(self) end % soft validate the transmit sequence type: it should be focused - if us.seq.type ~= "VS", warning(... - "Expected sequence type to be VS but instead got " + us.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 @@ -4474,7 +4482,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 @@ -4488,6 +4497,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 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/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 From 0ddb52ce9833e02bfb0a0afa7d9596fee8a5166a Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Wed, 13 Dec 2023 12:01:39 -0800 Subject: [PATCH 40/50] animate() : accepts complex data when creating new image. --- utils/animate.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utils/animate.m b/utils/animate.m index 5f3b90b2..00129dde 100644 --- a/utils/animate.m +++ b/utils/animate.m @@ -133,6 +133,10 @@ % 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); From 514d0f25c66c010d2da776bee5a83b100a8499aa Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Wed, 13 Dec 2023 12:04:08 -0800 Subject: [PATCH 41/50] sub() : dimension defaults to first non-singleton. --- utils/sub.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From 2de580ec905f64b949f4892d563ad8a87bfa60d2 Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Thu, 14 Dec 2023 09:17:14 -0800 Subject: [PATCH 42/50] Sequence.Verasonics() : added validation, documentation, and output offset time. --- src/Sequence.m | 184 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 147 insertions(+), 37 deletions(-) diff --git a/src/Sequence.m b/src/Sequence.m index f846f53c..cbb80220 100644 --- a/src/Sequence.m +++ b/src/Sequence.m @@ -428,61 +428,138 @@ end - function seq = Verasonics(TX, Trans, Resource, TW) + 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, 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 ismux, ap = Trans.HVMux.ApertureES; % mux - else, ap = 1:Resource.Parameters.numTransmit; % no muxing + if isfield(kwargs, 'aperture') + ap = kwargs.aperture; % custom muxing + elseif ismux, ap = Trans.HVMux.Aperture; % mux + else, ap = Trans.ConnectorES; % no muxing end % constants - c0 = Resource.Parameters.speedOfSound; + 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 - pog = cat(1,TX.Origin);% .* lambda; % beam origin - tau = cat(1,TX.Delay );% ./ fc; % tx delays + 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 - try % attempt to infer focused or diverging wave - xdc = Transducer.Verasonics(Trans, c0); % get transducer - if isa(class(xdc), "TransducerArray") ... + % 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 - else - warning("QUPS:Verasonics:ambiguousSequence", ... - "Cannot infer whether sequence is focused or diverging."); - styp = "VS"; % default type + if all(pf(3,:) < 0), styp = "DV"; + elseif all(pf(3,:) > 0), styp = "FC"; end - catch - warning("QUPS:Verasonics:ambiguousSequence", ... - "Cannot infer whether sequence is focused or diverging."); + 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 - M = numel(TX); - seq = Sequence("type","FSA", "numPulse",M); + seq = Sequence("type","FSA", "numPulse",numel(TX)); + elseif all(all(pog == 0,2) & all(rf == 0,1) & any(ang,2),1) % PW az = rad2deg(ang(:,1)'); % azimuth el = rad2deg(ang(:,2)'); % elevation if any(el) - seq = SequenceSpherical("type","PW", "angles", [az; el]); + seq = SequenceSpherical("type","PW","angles",[az; el]); else - seq = SequenceRadial("type","PW", "angles",az); + seq = SequenceRadial( "type","PW","angles", az ); end + elseif any(rf) pf = pog + rf .* [ sin(ang(:,1)) .* cos(ang(:,2)), ... @@ -492,21 +569,54 @@ 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 - error("Unable to infer focal sequence type."); + 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 - % TODO: validate - if nargin >= 4 - t = (0:TW.numsamples-1)' ./ 250e6; - t0 = - TW.peak ./ fc; - flds = "TriLvlWvfm" + ["", "_Sim"]; - f = flds(isfield(TW, flds)); - seq.pulse = Waveform('t', t + t0, 'samples', TW.(f), 'fs', 250e6); + if isscalar(TW), seq.pulse = Waveform.Verasonics(TW, fc); + else, seq.pulse = Waveform.Delta(); end end end From ed89568df9cf807dfbcd9af34eeb95ca0d3d8463 Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Thu, 14 Dec 2023 09:17:22 -0800 Subject: [PATCH 43/50] Adding ChannelData.Verasonics() to import data to a (time x acq x [channel | elem] x frame) array. --- src/ChannelData.m | 137 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/src/ChannelData.m b/src/ChannelData.m index db5a1813..5e91eb1f 100644 --- a/src/ChannelData.m +++ b/src/ChannelData.m @@ -178,6 +178,143 @@ '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'). + % + % 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]) + 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 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 + + % 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 From b0b25794d87707692f1b215d9409d64bb9f40f72 Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Thu, 14 Dec 2023 10:03:32 -0800 Subject: [PATCH 44/50] ChannelData.Verasonic() : added 0-insertion for alternate sampling modes. --- src/ChannelData.m | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/ChannelData.m b/src/ChannelData.m index 5e91eb1f..fce473d4 100644 --- a/src/ChannelData.m +++ b/src/ChannelData.m @@ -208,6 +208,11 @@ % 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)); @@ -220,6 +225,7 @@ 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 @@ -243,6 +249,15 @@ 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 @@ -307,6 +322,21 @@ 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 From cf3aeda835415e90d1e5a20fd8254ee6b99ac72c Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Thu, 14 Dec 2023 12:22:49 -0800 Subject: [PATCH 45/50] Sequence.Verasonics() : bug fix - identifying PW sequences including a 0 degree transmit angle. --- src/Sequence.m | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Sequence.m b/src/Sequence.m index cbb80220..237fe803 100644 --- a/src/Sequence.m +++ b/src/Sequence.m @@ -436,7 +436,8 @@ % % seq = Sequence.Verasonics(TX, Trans, TW) additionally imports % the trilevel excitation waveform from the 'TW' struct as a - % Waveform. If omitted, seq.pulse is a Waveform.Delta instead. + % 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 @@ -551,7 +552,7 @@ 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,2),1) % PW + 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) From b14b989e4ed11366d44f480e32f9c2bf9c21b05d Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Thu, 14 Dec 2023 12:29:29 -0800 Subject: [PATCH 46/50] Adding Verasonic import example script w/o data. --- examples/import/import_verasonics_data.m | 83 ++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 examples/import/import_verasonics_data.m 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 + + + + + + + + From e3d10b3df0b8f18b99c571ebf01ebd33c1768c2d Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Thu, 14 Dec 2023 14:09:49 -0800 Subject: [PATCH 47/50] ChannelData.join() : bug fix - handles empty input. --- src/ChannelData.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ChannelData.m b/src/ChannelData.m index fce473d4..859b4cb1 100644 --- a/src/ChannelData.m +++ b/src/ChannelData.m @@ -1328,7 +1328,8 @@ 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 From a0de4360615fd22ad9d792432f4895f6380094cb Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Thu, 14 Dec 2023 14:10:13 -0800 Subject: [PATCH 48/50] ChannelData.Verasonics() : warns on unvalidated code path. --- src/ChannelData.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ChannelData.m b/src/ChannelData.m index 859b4cb1..865c4ef6 100644 --- a/src/ChannelData.m +++ b/src/ChannelData.m @@ -162,7 +162,7 @@ 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("Untested code: please validate."); + 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 From a2294d8a697d3a559d92596f78d6c0f158692d3a Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Thu, 14 Dec 2023 14:16:06 -0800 Subject: [PATCH 49/50] Waveform.Verasonics() : added documentation, error checking; handles ND-array inputs. --- src/Waveform.m | 66 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/src/Waveform.m b/src/Waveform.m index 17baa4d5..8d91a2e2 100644 --- a/src/Waveform.m +++ b/src/Waveform.m @@ -450,27 +450,60 @@ end function [wvtri, wvm1wy, wvm2wy] = Verasonics(TW, fc) - if nargin < 2 - if isfield(TW, 'Parameters') - warning("Inferring transducer frequency from pulse frequency. Use the second input to avoid this warning."); - fc = TW.Parameters(1); - else - error("Unable to infer pulse frequency."); - end + % 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-leve field name + % identify tri-level field name fld = "TriLvlWvfm" + ["", "_Sim"]; % potential field names - f = fld(isfield(TW, fld)); + 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*1e-6 ; + t02 = - [TW.peak] ./ (fc(:)'); t01 = t02 ./ 2; t0t = - cellfun(@(h) median(find(logical(h))) ./ 250e6, {TW.(f)}); - % signal length - Ts = {TW.numsamples}; - % Sampled waveform constructor wvfun = @(t0, T, w) Waveform( ... "t0", t0, "fs", 250e6, "dt", 4e-9, "tend", t0 + (T-1)*4e-9, ... @@ -478,9 +511,10 @@ ); % create Waveforms - wvm1wy = reshape(cellfun(wvfun,num2cell(t01), Ts, {TW.Wvfm1Wy}), size(TW)); - wvm2wy = reshape(cellfun(wvfun,num2cell(t02), Ts, {TW.Wvfm2Wy}), size(TW)); - wvtri = reshape(cellfun(wvfun,num2cell(t0t), Ts, {TW.(f) }), size(TW)); + 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 From ed13743909ec742d26671ac21a386486cd18067e Mon Sep 17 00:00:00 2001 From: thorstone25 Date: Thu, 14 Dec 2023 14:19:51 -0800 Subject: [PATCH 50/50] Updated example scripts. --- cheat_sheet.m | 6 ++ example.mlx | Bin 11672 -> 11413 bytes example_.m | 79 +++++++++++++------------ examples/simulation/multilayer_media.m | 2 +- 4 files changed, 48 insertions(+), 39 deletions(-) diff --git a/cheat_sheet.m b/cheat_sheet.m index c0ca5b9a..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; diff --git a/example.mlx b/example.mlx index 1ec9d10c5a9e817f60014e78ca175f8a5c188782..cdf6e9a408506fa401adf6d6e61cd48fc809e996 100644 GIT binary patch delta 10108 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 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. @@ -61,26 +62,28 @@ 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 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 + 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 -end % make 3D region + tscan.y = 0; % 2D grid +end % point per wavelength - aim for >2 for a simulation ppw = scat.c0 / xdc.fc / min([tscan.dx, tscan.dy, tscan.dz], [], 'omitnan'); @@ -104,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' %% @@ -115,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 ... @@ -165,7 +168,6 @@ % make a scatterer, if not diffuse if scat.numScat < 500 s_rad = max([tscan.dx,tscan.dy,tscan.dz],[],'omitnan'); % scatterer radius - nextdim = @(p) ndims(p) + 1; % helper function 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 @@ -185,15 +187,14 @@ 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 +imagesc(med, tscan, 'props', "c"); colorbar; % show the background medium for the simulation/imaging region + % 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 system +hold on; hs = plot(us); % plot the point scatterers @@ -214,11 +215,12 @@ 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 @@ -231,8 +233,8 @@ 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 @@ -240,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 @@ -261,15 +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 'TransducerMatrix', scale = min(xdc.pitch); % Definitions in elements + 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) @@ -281,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 @@ -292,14 +295,14 @@ 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/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