.1] = 0
445 | z = scipy.fft.idctn(z, axes=np.arange(1, len(z.shape)))
446 | return z
447 |
448 |
--------------------------------------------------------------------------------
/src/pymust/sptrack.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | import logging
3 | from . import utils, smoothn
4 | import numpy as np, scipy
5 |
6 |
7 | def sptrack(I: np.ndarray, param: utils.Param) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
8 | """
9 | %SPTRACK Speckle tracking using Fourier-based cross-correlation
10 | % [Di,Dj] = SPTRACK(I,PARAM) returns the motion field [Di,Dj] that occurs
11 | % from frame#k I(:,:,k) to frame#(k+1) I(:,:,k+1).
12 | %
13 | % I must be a 3-D array, with I(:,:,k) corresponding to image #k. I can
14 | % contain more than two images (i.e. size(I,3)>2). In such a case, an
15 | % ensemble correlation is used.
16 | %
17 | % >--- Try it: enter "sptrack" in the command window for an example ---<
18 | %
19 | % Di,Dj are the displacements (unit = pix) in the IMAGE coordinate system
20 | % (i.e. the "matrix" axes mode). The i-axis is vertical, with values
21 | % increasing from top to bottom. The j-axis is horizontal with values
22 | % increasing from left to right. The coordinate (1,1) corresponds to the
23 | % center of the upper left pixel.
24 | % To display the displacement field, you may use: quiver(Dj,Di), axis ij
25 | %
26 | % PARAM is a structure that contains the parameter values required for
27 | % speckle tracking (see below for details).
28 | %
29 | % [Di,Dj,id,jd] = SPTRACK(...) also returns the coordinates of the points
30 | % where the components of the displacement field are estimated.
31 | %
32 | %
33 | % PARAM is a structure that contains the following fields:
34 | % -------------------------------------------------------
35 | % 1) PARAM.winsize: Size of the interrogation windows (REQUIRED)
36 | % PARAM.winsize must be a 2-COLUMN array. If PARAM.winsize
37 | % contains several rows, a multi-grid, multiple-pass interro-
38 | % gation process is used.
39 | % Examples: a) If PARAM.winsize = [64 32], then a 64-by-32
40 | % (64 lines, 32 columns) interrogation window is used.
41 | % b) If PARAM.winsize = [64 64;32 32;16 16], a 64-by-64
42 | % interrogation window is first used. Then a 32-by-32
43 | % window, and finally a 16-by-16 window are used.
44 | % 2) PARAM.overlap: Overlap between the interrogation windows
45 | % (in %, default = 50)
46 | % 3) PARAM.iminc: Image increment (for ensemble correlation, default = 1)
47 | % The image #k is compared with image #(k+PARAM.iminc):
48 | % I(:,:,k) is compared with I(:,:,k+PARAM.iminc)
49 | % 5) PARAM.ROI: 2-D region of interest (default = the whole image).
50 | % PARAM.ROI must be a logical 2-D array with a size of
51 | % [size(I,1),size(I,2)]. The default is all(isfinite(I),3).
52 | %
53 | % NOTES:
54 | % -----
55 | % The displacement field is returned in PIXELS. Perform an appropriate
56 | % calibration to get physical units.
57 | %
58 | % SPTRACK is based on a multi-step cross-correlation method. The SMOOTHN
59 | % function (see Reference below) is used at each iterative step for the
60 | % validation and post-processing.
61 | %
62 | %
63 | % Example:
64 | % -------
65 | % I1 = conv2(rand(500,500),ones(10,10),'same'); % create a 500x500 image
66 | % I2 = imrotate(I1,-3,'bicubic','crop'); % clockwise rotation
67 | % param.winsize = [64 64;32 32];
68 | % [di,dj] = sptrack(cat(3,I1,I2),param);
69 | % quiver(dj(1:2:end,1:2:end),di(1:2:end,1:2:end))
70 | % axis equal ij
71 | %
72 | %
73 | % References for speckle tracking
74 | % -------------------------------
75 | % 1) Garcia D, Lantelme P, Saloux É. Introduction to speckle tracking in
76 | % cardiac ultrasound imaging. Handbook of speckle filtering and tracking
77 | % in cardiovascular ultrasound imaging and video. Institution of
78 | % Engineering and Technology. 2018.
79 | % PDF download
81 | % 2) Perrot V, Garcia D. Back to basics in ultrasound velocimetry:
82 | % tracking speckles by using a standard PIV algorithm. IEEE International
83 | % Ultrasonics Symposium (IUS). 2018
84 | % PDF download
86 | % 3) Joos P, Porée J, ..., Garcia D. High-frame-rate speckle tracking
87 | % echocardiography. IEEE Trans Ultrason Ferroelectr Freq Control. 2018.
88 | % PDF download
90 | %
91 | %
92 | % References for smoothing
93 | % -------------------------------
94 | % 1) Garcia D, Robust smoothing of gridded data in one and higher
95 | % dimensions with missing values. Computational Statistics & Data
96 | % Analysis, 2010.
97 | % PDF download
99 | % 2) Garcia D, A fast all-in-one method for automated post-processing of
100 | % PIV data. Experiments in Fluids, 2011.
101 | % PDF download
103 | %
104 | %
105 | % This function is part of MUST (Matlab UltraSound Toolbox).
106 | % MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later
107 | %
108 | % See also SMOOTHN
109 | %
110 | % -- Damien Garcia & Vincent Perrot -- 2013/02, last update: 2021/06/28
111 | % website: www.BiomeCardio.com
113 | """
114 |
115 |
116 | #%------------------------%
117 | #% CHECK THE INPUT SYNTAX %
118 | #%------------------------%
119 | I = I.astype(float).copy()
120 |
121 | # Image size
122 | assert len(I.shape) ==3,'I must be a 3-D array'
123 | M,N, P = I.shape
124 |
125 | if not utils.isfield(param, 'winsize'): # Sizes of the interrogation windows
126 | raise ValueError('Window size(s) (PARAM.winsize) must be specified in the field PARAM.')
127 | if isinstance(param.winsize, list):
128 | param.winsize = np.array(param.winsize)
129 | if isinstance(param.winsize, np.ndarray) and len(param.winsize.shape) == 1:
130 | param.winsize = param.winsize.reshape((1, -1), order = 'F')
131 |
132 | assert isinstance(param.winsize, np.ndarray) and len(param.winsize.shape) == 2 and param.winsize.shape[1] == 2 and 'PARAM.winsize must be a 2-column array.'
133 | tmp = np.diff(param.winsize,1,0)
134 | assert np.all(tmp<=0), 'The size of interrogation windows (PARAM.winsize) must decrease.'
135 |
136 | if not utils.isfield(param,'overlap'): # Overlap
137 | param.overlap = 50;
138 |
139 | assert np.isscalar(param.overlap) and param.overlap>=0 and param.overlap<100, 'PARAM.overlap (in %) must be a scalar in [0,100[.'
140 | overlap = param.overlap/100;
141 |
142 | if not utils.isfield(param,'ROI'): # Region of interest
143 | param.ROI = np.all(np.isfinite(I),2)
144 | ROI = param.ROI;
145 | assert isinstance(ROI, np.ndarray) and ROI.dtype == bool and np.allclose(ROI.shape,[M, N]), 'PARAM.ROI must be a binary image the same size as I[]:,:,0].'
146 |
147 |
148 | I[np.tile(np.logical_not(ROI)[..., None],[1,1, P])] = np.nan; # NaNing outside the ROI
149 |
150 | if not utils.isfield(param,'iminc'): # Step increment
151 | param.iminc = 1
152 |
153 | assert param.iminc>0 and isinstance(param.iminc, int), 'PARAM.iminc must be a positive integer.'
154 | assert param.iminc= 1:
176 | ic0= (2*i0+m)/2
177 | jc0 = (2*j0+n)/2;
178 | m0 = len(ic0)
179 | n0 = len(j0)
180 |
181 | # Size of the interrogation window
182 | m = param.winsize[kk,0];
183 | n = param.winsize[kk,1];
184 |
185 | # Positions (row,column) of the windows (left upper corner)
186 | inci = np.ceil(m*(1-overlap)); incj = np.ceil(n*(1-overlap));
187 | i_array= np.arange(0,M-m+1,inci, dtype=int)
188 | j_array =np.arange(0,N-n+1,incj, dtype=int)
189 |
190 | # Size of the displacement-field matrix
191 | siz = (len(j_array), len(i_array)) # j.shape
192 |
193 | # Window centers
194 | ic = (2*i_array+m)/2;
195 | jc = (2*j_array+n)/2;
196 | i0 = i_array.copy()
197 | j0 = j_array.copy()
198 |
199 | if kk>=1:
200 | #% Interpolation onto the new grid
201 | j_newgrid, i_newgrid = np.meshgrid(jc,ic)
202 | X_newgrid = np.stack([i_newgrid.flatten(), j_newgrid.flatten()], axis = 1)
203 | di = scipy.interpolate.interpn((ic0,jc0), di, X_newgrid,'cubic', bounds_error = False, fill_value = np.nan)
204 | dj = scipy.interpolate.interpn((ic0,jc0), dj, X_newgrid,'cubic', bounds_error = False, fill_value = np.nan)
205 | di = di.reshape(siz)
206 | dj = dj.reshape(siz)
207 | #% Extrapolation (remove NaNs)
208 | dj = rmnan(di+1j*dj,2)
209 | di = dj.real;
210 | dj = dj.imag;
211 | di = np.round(di); dj = np.round(dj);
212 | else:
213 | di = np.zeros(siz)
214 | dj = di.copy();
215 |
216 |
217 | #% Hanning window
218 | H = np.outer(scipy.signal.windows.hann(n+2)[1:-1], scipy.signal.windows.hann(m+2)[1:-1])
219 |
220 |
221 | C = np.zeros(siz); # will contain the correlation coefficients
222 |
223 | # Iterate over all centers
224 | for i, pixel_i in enumerate(i_array):
225 | for j, pixel_j in enumerate(j_array):
226 | #-- Split the images into small windows
227 | if pixel_i+di[i,j]>=0 and pixel_j+dj[i,j]>=0 and \
228 | pixel_i+di[i,j]+m0 and di00 and dj00 and pixel_j+dj[i,j]>0 and \
296 | pixel_i+di[i,j]+m-20 and di01 and dj0 tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
7 | """
8 | %SPTRACK Speckle tracking using Fourier-based cross-correlation
9 | % [Di,Dj] = SPTRACK(I,PARAM) returns the motion field [Di,Dj] that occurs
10 | % from frame#k I(:,:,k) to frame#(k+1) I(:,:,k+1).
11 | %
12 | % I must be a 3-D array, with I(:,:,k) corresponding to image #k. I can
13 | % contain more than two images (i.e. size(I,3)>2). In such a case, an
14 | % ensemble correlation is used.
15 | %
16 | % >--- Try it: enter "sptrack" in the command window for an example ---<
17 | %
18 | % Di,Dj are the displacements (unit = pix) in the IMAGE coordinate system
19 | % (i.e. the "matrix" axes mode). The i-axis is vertical, with values
20 | % increasing from top to bottom. The j-axis is horizontal with values
21 | % increasing from left to right. The coordinate (1,1) corresponds to the
22 | % center of the upper left pixel.
23 | % To display the displacement field, you may use: quiver(Dj,Di), axis ij
24 | %
25 | % PARAM is a structure that contains the parameter values required for
26 | % speckle tracking (see below for details).
27 | %
28 | % [Di,Dj,id,jd] = SPTRACK(...) also returns the coordinates of the points
29 | % where the components of the displacement field are estimated.
30 | %
31 | %
32 | % PARAM is a structure that contains the following fields:
33 | % -------------------------------------------------------
34 | % 1) PARAM.winsize: Size of the interrogation windows (REQUIRED)
35 | % PARAM.winsize must be a 2-COLUMN array. If PARAM.winsize
36 | % contains several rows, a multi-grid, multiple-pass interro-
37 | % gation process is used.
38 | % Examples: a) If PARAM.winsize = [64 32], then a 64-by-32
39 | % (64 lines, 32 columns) interrogation window is used.
40 | % b) If PARAM.winsize = [64 64;32 32;16 16], a 64-by-64
41 | % interrogation window is first used. Then a 32-by-32
42 | % window, and finally a 16-by-16 window are used.
43 | % 2) PARAM.overlap: Overlap between the interrogation windows
44 | % (in %, default = 50)
45 | % 3) PARAM.iminc: Image increment (for ensemble correlation, default = 1)
46 | % The image #k is compared with image #(k+PARAM.iminc):
47 | % I(:,:,k) is compared with I(:,:,k+PARAM.iminc)
48 | % 5) PARAM.ROI: 2-D region of interest (default = the whole image).
49 | % PARAM.ROI must be a logical 2-D array with a size of
50 | % [size(I,1),size(I,2)]. The default is all(isfinite(I),3).
51 | %
52 | % NOTES:
53 | % -----
54 | % The displacement field is returned in PIXELS. Perform an appropriate
55 | % calibration to get physical units.
56 | %
57 | % SPTRACK is based on a multi-step cross-correlation method. The SMOOTHN
58 | % function (see Reference below) is used at each iterative step for the
59 | % validation and post-processing.
60 | %
61 | %
62 | % Example:
63 | % -------
64 | % I1 = conv2(rand(500,500),ones(10,10),'same'); % create a 500x500 image
65 | % I2 = imrotate(I1,-3,'bicubic','crop'); % clockwise rotation
66 | % param.winsize = [64 64;32 32];
67 | % [di,dj] = sptrack(cat(3,I1,I2),param);
68 | % quiver(dj(1:2:end,1:2:end),di(1:2:end,1:2:end))
69 | % axis equal ij
70 | %
71 | %
72 | % References for speckle tracking
73 | % -------------------------------
74 | % 1) Garcia D, Lantelme P, Saloux É. Introduction to speckle tracking in
75 | % cardiac ultrasound imaging. Handbook of speckle filtering and tracking
76 | % in cardiovascular ultrasound imaging and video. Institution of
77 | % Engineering and Technology. 2018.
78 | % PDF download
80 | % 2) Perrot V, Garcia D. Back to basics in ultrasound velocimetry:
81 | % tracking speckles by using a standard PIV algorithm. IEEE International
82 | % Ultrasonics Symposium (IUS). 2018
83 | % PDF download
85 | % 3) Joos P, Porée J, ..., Garcia D. High-frame-rate speckle tracking
86 | % echocardiography. IEEE Trans Ultrason Ferroelectr Freq Control. 2018.
87 | % PDF download
89 | %
90 | %
91 | % References for smoothing
92 | % -------------------------------
93 | % 1) Garcia D, Robust smoothing of gridded data in one and higher
94 | % dimensions with missing values. Computational Statistics & Data
95 | % Analysis, 2010.
96 | % PDF download
98 | % 2) Garcia D, A fast all-in-one method for automated post-processing of
99 | % PIV data. Experiments in Fluids, 2011.
100 | % PDF download
102 | %
103 | %
104 | % This function is part of MUST (Matlab UltraSound Toolbox).
105 | % MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later
106 | %
107 | % See also SMOOTHN
108 | %
109 | % -- Damien Garcia & Vincent Perrot -- 2013/02, last update: 2021/06/28
110 | % website: www.BiomeCardio.com
112 | """
113 |
114 |
115 | #%------------------------%
116 | #% CHECK THE INPUT SYNTAX %
117 | #%------------------------%
118 |
119 | I = I.astype(float)
120 |
121 | # Image size
122 | assert len(I.shape) ==3,'I must be a 3-D array'
123 | M,N, P = I.shape
124 |
125 | if not utils.isfield(param, 'winsize'): # Sizes of the interrogation windows
126 | raise ValueError('Window size(s) (PARAM.winsize) must be specified in the field PARAM.')
127 | if isinstance(param.winsize, list):
128 | param.winsize = np.array(param.winsize)
129 | if isinstance(param.winsize, np.ndarray) and len(param.winsize.shape) == 1:
130 | param.winsize = param.winsize.reshape((1, -1), order = 'F')
131 |
132 | assert isinstance(param.winsize, np.ndarray) and len(param.winsize.shape) == 2 and param.winsize.shape[1] == 2 and 'PARAM.winsize must be a 2-column array.'
133 | tmp = np.diff(param.winsize,1,0)
134 | assert np.all(tmp<=0), 'The size of interrogation windows (PARAM.winsize) must decrease.'
135 |
136 | if not utils.isfield(param,'overlap'): # Overlap
137 | param.overlap = 50;
138 |
139 | assert np.isscalar(param.overlap) and param.overlap>=0 and param.overlap<100, 'PARAM.overlap (in %) must be a scalar in [0,100[.'
140 | overlap = param.overlap/100;
141 |
142 | if not utils.isfield(param,'ROI'): # Region of interest
143 | param.ROI = np.all(np.isfinite(I),2)
144 | ROI = param.ROI;
145 | assert isinstance(ROI, np.ndarray) and ROI.dtype == bool and np.allclose(ROI.shape,[M, N]), 'PARAM.ROI must be a binary image the same size as I[]:,:,0].'
146 |
147 |
148 | I[np.tile(np.logical_not(ROI)[..., None],[1,1, P])] = np.nan; # NaNing outside the ROI
149 |
150 | if not utils.isfield(param,'iminc'): # Step increment
151 | param.iminc = 1
152 |
153 | assert param.iminc>0 and isinstance(param.iminc, int), 'PARAM.iminc must be a positive integer.'
154 | assert param.iminc= 1:
176 | ic0 = (2*i+m)/2;
177 | ic0_array = (2*i0_array+m)/2
178 | jc0 = (2*j+n)/2;
179 | jc0_array = (2*j0_array+m)/2
180 | m0 = len(ic0_array)
181 | n0 = len(jc0_array)
182 |
183 | # Size of the interrogation window
184 | m = param.winsize[kk,0];
185 | n = param.winsize[kk,1];
186 |
187 | # Positions (row,column) of the windows (left upper corner)
188 | inci = np.ceil(m*(1-overlap)); incj = np.ceil(n*(1-overlap));
189 | i0_array= np.arange(0,M-m+1,inci, dtype=int)
190 | j0_array =np.arange(0,N-n+1,incj, dtype=int)
191 | j,i = np.meshgrid(j0_array,i0_array);
192 | # Size of the displacement-field matrix
193 | siz = (np.floor([(M-m)/inci, (N-n)/incj])+1).astype(int) # j.shape
194 | j = j.flatten(order = 'F'); i = i.flatten(order = 'F');
195 |
196 | # Window centers
197 | ic = (2*i+m)/2;
198 | ic_array = (2*i0_array+m)/2
199 | jc = (2*j+n)/2;
200 | jc_array = (2*j0_array+n)/2
201 |
202 | if kk>=1:
203 |
204 | #% Interpolation onto the new grid
205 | di = scipy.interpolate.interpn((jc0_array,ic0_array),di.reshape((m0,n0), order = 'F'),(jc,ic),'cubic', bounds_error = False, fill_value = np.nan)
206 | dj = scipy.interpolate.interpn((jc0_array,ic0_array),dj.reshape((m0,n0), order = 'F'),(jc,ic),'cubic', bounds_error = False, fill_value = np.nan)
207 |
208 | #% Extrapolation (remove NaNs)
209 | dj = rmnan(di+1j*dj,2)
210 | di = dj.real;
211 | dj = dj.imag;
212 | di = np.round(di); dj = np.round(dj);
213 | else:
214 | di = np.zeros(siz).flatten(order = 'F');
215 | dj = di.copy();
216 |
217 |
218 | #% Hanning window
219 | H = np.outer(scipy.signal.windows.hann(n+2)[1:-1], scipy.signal.windows.hann(m+2)[1:-1])
220 |
221 |
222 | C = np.zeros(siz).flatten(order = 'F'); # will contain the correlation coefficients
223 |
224 | for k,_ in enumerate(i):
225 | #-- Split the images into small windows
226 | if i[k]+di[k]>=0 and j[k]+dj[k]>=0 and \
227 | i[k]+di[k]+m0 and di00 and dj00 and j[k]+dj[k]>0 and \
296 | i[k]+di[k]+m-20 and di01 and dj0 tuple[np.ndarray, np.ndarray]:
6 | """
7 | %TGC Time-gain compensation for RF or IQ signals
8 | % TGC(RF) or TGC(IQ) performs a time-gain compensation of the RF or IQ
9 | % signals using a decreasing exponential law. Each column of the RF/IQ
10 | % array must correspond to a single RF/IQ signal over (fast-) time.
11 | %
12 | % [~,C] = TGC(RF) or [~,C] = TGC(IQ) also returns the coefficients used
13 | % for time-gain compensation (i.e. new_SIGNAL = C.*old_SIGNAL)
14 | %
15 | %
16 | % This function is part of MUST (Matlab UltraSound Toolbox).
17 | % MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later
18 | %
19 | % See also RF2IQ, DAS.
20 | %
21 | % -- Damien Garcia -- 2012/10, last update 2020/05
22 | % website: www.BiomeCardio.com
24 | """
25 |
26 | siz0 = S.shape
27 |
28 | if not utils.iscomplex(S): #% we have RF signals
29 | C = np.mean(np.abs(scipy.signal.hilbert(S, axis = 0)),1)
30 | #% C = median(abs(hilbert(S)),2);
31 | else: #% we have IQ signals
32 | C = np.mean(np.abs(S),1)
33 | # C = median(abs(S),2);
34 | n = len(C)
35 | n1 = int(np.ceil(n/10))
36 | n2 = int(np.floor(n*9/10))
37 | """
38 | % -- Robust linear fitting of log(C)
39 | % The intensity is assumed to decrease exponentially as distance increases.
40 | % A robust linear fitting is performed on log(C) to seek the TGC
41 | % exponential law.
42 | % --
43 | % See RLINFIT for details
44 | """
45 | N = 200# ; % a maximum of N points is used for the fitting
46 | p = min(N/(n2-n1)*100,100)
47 | slope,intercept = rlinfit(np.arange(n1,n2), np.log(C[n1:n2]),p)
48 |
49 | C = np.exp(intercept+slope*np.arange(n).reshape((-1,1)))
50 | C = C[0]/C
51 | S = S*C
52 |
53 | S = S.reshape(siz0)
54 | return S, C
55 |
56 | def rlinfit(x,y,p):
57 | """
58 | %RLINFIT Robust linear regression
59 | % See the original RLINFIT function for details
60 | """
61 | N = len(x)
62 | I = np.random.permutation(N)
63 | n = int(np.round(N*p/100))
64 | I = I[:n]
65 | x = x[I]
66 | y = y[I]
67 |
68 | #Not sure it is the best option, what about some regression with regularisation?
69 | if True:
70 | C = np.array( [ (i,j) for i,j in itertools.combinations(np.arange(n), 2)] )
71 | else:
72 | pass
73 | slope = np.median( (y[C[:,1]]-y[C[:,0]]) / (x[C[:,1]]-x[C[:,0]]) )
74 | intercept = np.median(y-slope*x)
75 | return slope, intercept
76 |
--------------------------------------------------------------------------------
/src/pymust/txdelay.py:
--------------------------------------------------------------------------------
1 | from . import utils
2 | import numpy as np
3 |
4 | def txdelayCircular(param: utils.Param, tilt: float, width: float) -> np.ndarray:
5 | return txdelay(param, tilt, width)
6 |
7 | def txdelayPlane(param: utils.Param, tilt: float) -> np.ndarray:
8 | return txdelay(param, tilt)
9 |
10 | def txdelayFocused(param: utils.Param, x: float, y: float) -> np.ndarray:
11 | return txdelay(x, y, param)
12 |
13 | def txdelay(*args):
14 | """
15 | GB Note: uses variable arguments as in matlab, but this is not very pythonic
16 | %TXDELAY Transmit delays for a linear or convex array
17 | % TXDELAY returns the transmit time delays for focused, plane or circular
18 | % beam patterns with a linear or convex array.
19 | %
20 | % DELAYS = TXDELAY(X0,Z0,PARAM) returns the transmit time delays which
21 | % must be used to generate a pressure field focused at the point
22 | % (x0,z0). Note: If z0 is negative, then the point (x0,z0) is a virtual
23 | % source. The properties of the medium and the array must be given in
24 | % the structure PARAM (see below).
25 | %
26 | % DELAYS = TXDELAY(PARAM,TILT) returns the transmit time delays which
27 | % must be used to get a tilted plane wave. TILT is the tilt angle about
28 | % the Y-axis. TILT equals zero radian when a plane wave is not tilted
29 | % (the delays are then = 0).
30 | %
31 | % DELAYS = TXDELAY(PARAM,TILT,WIDTH) yields the transmit time delays
32 | % necessary for creating a circular wave. The sector enclosed by the
33 | % circular waves is characterized by the angular width and tilt. TILT
34 | % represents the sector tilt angle about the Y-axis, WIDTH is the sector
35 | % width (both in radians). This option is not available for a convex
36 | % array.
37 | %
38 | % X0, Z0, TILT and WIDTH can be vectors. In that case, DELAYS is a matrix
39 | % whose rows contain the different delay laws.
40 | %
41 | % [DELAYS,PARAM] = TXDELAY(...) updates the PARAM structure parameters
42 | % including the default values. PARAM will also include PARAM.TXdelay
43 | % which is equal to DELAYS (in s).
44 | %
45 | % [...] = TXDELAY (no input parameter) runs an interactive example
46 | % simulating a focused pressure field generated by a 2.7 MHz phased
47 | % array. The user must choose the focus position.
48 | %
49 | % Units: X0,Z0 must be in m; TILT, WIDTH must be in rad. DELAYS are in s.
50 | %
51 | % PARAM is a structure that must contain the following fields:
52 | % ------------------------------------------------------------
53 | % 1) PARAM.pitch: pitch of the linear array (in m, REQUIRED)
54 | % 2) PARAM.Nelements: number of elements in the transducer array (REQUIRED)
55 | % 3) PARAM.radius: radius of curvature (in m, default = Inf)
56 | % 4) PARAM.c: longitudinal velocity (in m/s, default = 1540 m/s)
57 | %
58 | % ---
59 | % NOTE #1: X- and Z-axes
60 | % The X axis is PARALLEL to the transducer and points from the first
61 | % (leftmost) element to the last (rightmost) element (X = 0 at the CENTER
62 | % of the transducer). The Z axis is PERPENDICULAR to the transducer and
63 | % points downward (Z = 0 at the level of the transducer, Z increases as
64 | % depth increases).
65 | % ---
66 | % NOTE #2: TILT (in radians) describes the tilt angle in the
67 | % trigonometric direction. !! NOTE that there was an error in
68 | % the version older than 2022-11 !! Sorry about that!
69 | % ---
70 | %
71 | % Example #1:
72 | % ----------
73 | % %-- Generate a focused pressure field with a phased-array transducer
74 | % % Phased-array @ 2.7 MHz:
75 | % param = getparam('P4-2v');
76 | % % Focus position:
77 | % x0 = 2e-2; z0 = 5e-2;
78 | % % TX time delays:
79 | % dels = txdelay(x0,z0,param);
80 | % % Grid:
81 | % x = linspace(-4e-2,4e-2,200);
82 | % z = linspace(0,10e-2,200);
83 | % [x,z] = meshgrid(x,z);
84 | % % RMS pressure field:
85 | % P = pfield(x,z,dels,param);
86 | % imagesc(x(1,:)*1e2,z(:,1)*1e2,20*log10(P/max(P(:))))
87 | % hold on, plot(x0*1e2,z0*1e2,'k*'), hold off
88 | % colormap hot, axis equal tight ij
89 | % caxis([-20 0])
90 | % c = colorbar;
91 | % c.YTickLabel{end} = '0 dB';
92 | % xlabel('[cm]')
93 | %
94 | % Example #2:
95 | % ----------
96 | % %-- Generate a plane wave a convex transducer
97 | % % Convex array @ 3.6 MHz:
98 | % param = getparam('C5-2v');
99 | % % Tilt angle = 10 degrees:
100 | % tilt = pi/18; % in rad
101 | % % TX apodization
102 | % param.TXapodization = [zeros(1,28) ones(1,100) ];
103 | % % TX time delays:
104 | % dels = txdelay(param,tilt);
105 | % % 8cm-by-8cm grid:
106 | % [x,z] = impolgrid(100,8e-2,param);
107 | % % RMS pressure field:
108 | % P = pfield(x,z,dels,param);
109 | % pcolor(x*1e2,z*1e2,20*log10(P/max(P(:))))
110 | % shading interp
111 | % colormap hot, axis equal tight ij
112 | % caxis([-20 0])
113 | % c = colorbar;
114 | % c.YTickLabel{end} = '0 dB';
115 | % xlabel('[cm]')
116 | %
117 | %
118 | % This function is part of MUST (Matlab UltraSound Toolbox).
120 | % MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later
121 | %
122 | % See also PFIELD, SIMUS, DAS, DASMTX, GETPARAM.
123 | %
124 | % -- Damien Garcia -- 2015/03, last update: 2022/10/28
125 | % website: www.BiomeCardio.com
127 | """
128 |
129 | #%-- Check the input arguments
130 | if len(args) ==2: # % Plane wave: TXDELAY(param,tilt)
131 | param = args[0]
132 | option = 'Plane Wave'
133 | elif len(args) == 3:
134 | if isinstance(args[2], utils.Param): #% Origo: TXDELAY(x0,z0,param)
135 | param = args[2]
136 | option = 'Origo'
137 | else:# % Circular wave: TXDELAY(param,tilt,width)
138 | param = args[0]
139 | option = 'Circular Wave'
140 | else:
141 | ValueError('Wrong input arguments.')
142 |
143 | assert isinstance(param, utils.Param),'Wrong input arguments. PARAM must be a structure.'
144 |
145 | #%-- Number of elements
146 | if utils.isfield(param,'Nelements'):
147 | N = param.Nelements
148 | else:
149 | raise ValueError('The number of elements (PARAM.Nelements) is required.')
150 |
151 | #%-- Pitch (in m)
152 | if not utils.isfield(param,'pitch'):
153 | raise ValueError('A pitch value (PARAM.pitch) is required.')
154 |
155 | #%-- Longitudinal velocity (in m/s)
156 | if not utils.isfield(param,'c'):
157 | param.c = 1540
158 |
159 | c = param.c
160 |
161 | #%-- Radius of curvature (in m)
162 | #% for a convex array
163 | if not utils.isfield(param,'radius'):
164 | param.radius = np.inf # % default = linear array
165 |
166 | R = param.radius
167 | isLINEAR = np.isinf(R)
168 |
169 |
170 |
171 |
172 | #%-- Positions of the transducer elements
173 | x, z, THe, h= param.getElementPositions()
174 |
175 | if option == 'Plane Wave':
176 | tilt = np.array(args[1]).reshape((-1, 1)) # Check if it is not a vector
177 | assert np.all(np.abs(tilt)0, width 0 and width < pi'
212 | L = (N-1)*param.pitch
213 | #%-- Origo
214 | x0,z0 = angles2origo(L,tilt,width)
215 | #%--
216 | delays = np.sqrt((x-x0)**2 + z0**2)/c
217 | delays = -delays*np.sign(z0)
218 | delays = delays-np.min(delays,-1).reshape((-1, 1))
219 |
220 | param.TXdelay = delays
221 | return delays
222 |
223 | def angles2origo(L,tilt,width):
224 | #% Origo (virtual source) from the tilt and width angles
225 | tilt = np.mod(-tilt+np.pi/2,2*np.pi)-np.pi/2
226 | SignCorrection = np.ones(tilt.shape)
227 | idx = np.abs(tilt)>np.pi/2
228 | tilt[idx] = np.pi-tilt[idx]
229 | SignCorrection[idx] = -1
230 | z0 = SignCorrection*L/(np.tan(tilt-width/2)-np.tan(tilt+width/2))
231 | x0 = SignCorrection*z0*np.tan(width/2-tilt)+L/2
232 | return x0, z0
233 |
--------------------------------------------------------------------------------
/src/pymust/txdelay3.py:
--------------------------------------------------------------------------------
1 | from . import utils
2 | import numpy as np
3 | import scipy.optimize
4 |
5 | def txdelay3Plane(param: utils.Param, tiltx: float, tilty: float) -> np.ndarray:
6 | return txdelay3(param, tiltx, tilty)
7 |
8 | def txdelay3Diverging(param: utils.Param, tiltx: float, tilty: float, omega: float) -> np.ndarray:
9 | return txdelay3(param, tiltx, tilty, omega)
10 |
11 | def txdelay3Focused(param: utils.Param, x: float|np.ndarray, y: float|np.ndarray, z: float|np.ndarray) -> np.ndarray:
12 | return txdelay3(x, y, z, param)
13 |
14 | def txdelay3(*args):
15 | """
16 | TXDELAY3 Transmit delays for a matrix array
17 | TXDELAY3 returns the transmit time delays for focused, plane or
18 | diverging beam patterns with a matrix array.
19 |
20 | DELAYS = TXDELAY3(X0,Y0,Z0,PARAM) returns the transmit time delays for
21 | a pressure field focused at the point (x0,y0,z0). Note: if z0 is
22 | negative, then the point (x0,y0,z0) is a virtual source. The properties
23 | of the medium and the array must be given in the structure PARAM (see
24 | below).
25 |
26 | DELAYS = TXDELAY3([X01 X02],[Y01 Y02],[Z01 Z02],PARAM) returns the
27 | transmit time delays for a pressure field focused at the line specified
28 | by the two points (x01,y01,z01) and (x02,y02,z02).
29 |
30 | DELAYS = TXDELAY3(PARAM,TILTx,TILTy) returns the transmit time delays
31 | for a tilted plane wave. TILTx is the tilt angle about the X-axis.
32 | TILTy is the tilt angle about the Y-axis. If TILTx = TILTy = 0, then
33 | the delays are 0.
34 |
35 | DELAYS = TXDELAY3(PARAM,TILTx,TILTy,OMEGA) yields the transmit time
36 | delays for a diverging wave. The sector is characterized by the angular
37 | tilts and the solid angle OMEGA subtented by the rectangular aperture
38 | of the transducer. TILTx is the tilt angle about the X-axis. TILTy is
39 | the tilt angle about the Y-axis. OMEGA sets the amount of the field of
40 | view (in [0 2pi]). This syntax is for matrix arrays only, i.e. the
41 | element positions must form a plaid grid.
42 |
43 | [DELAYS,PARAM] = TXDELAY3(...) updates the PARAM structure parameters
44 | including the default values. PARAM will also include PARAM.TXdelay,
45 | which is equal to DELAYS (in s).
46 |
47 | [...] = TXDELAY3 (no input parameter) simulates a focused pressure
48 | field generated by a 3-MHz 32x32 matrix array (1st example below).
49 |
50 | Units: X0,Y0,Z0 must be in m; TILTx,TILTy must be in rad. OMEGA is in
51 | sr (steradian). DELAYS are in s.
52 |
53 | PARAM is a structure that must contain the following fields:
54 | ------------------------------------------------------------
55 | 1) PARAM.elements: x- and y-coordinates of the element centers
56 | (in m, REQUIRED). It MUST be a two-row matrix, with the 1st
57 | and 2nd rows containing the x and y coordinates, respectively.
58 | 2) PARAM.width: element width, in the x-direction
59 | (in m, required for diverging waves)
60 | 3) PARAM.height: element height, in the y-direction
61 | (in m, required for diverging waves)
62 | 4) PARAM.c: longitudinal velocity (in m/s, default = 1540 m/s)
63 |
64 | ---
65 | NOTE #1: X-, Y-, and Z-axes
66 | Conventional axes are used: For a linear array, the X-axis is PARALLEL
67 | to the transducer and points from the first (leftmost) element to the
68 | last (rightmost) element (X = 0 at the CENTER of the transducer). The
69 | Z-axis is PERPENDICULAR to the transducer and points downward (Z = 0 at
70 | the level of the transducer, Z increases as depth increases). The
71 | Y-axis is such that the coordinates are right-handed.
72 | ---
73 | NOTE #2: TILTx and TILTy (in radians) describe the tilt angles in the
74 | trigonometric direction.
75 | ---
76 |
77 | Example #1:
78 | ----------
79 | %-- Generate a focused pressure field with a matrix array
80 | % 3-MHz matrix array with 32x32 elements
81 | param.fc = 3e6;
82 | param.bandwidth = 70;
83 | param.width = 250e-6;
84 | param.height = 250e-6;
85 | % position of the elements (pitch = 300 microns)
86 | pitch = 300e-6;
87 | [xe,ye] = meshgrid(((1:32)-16.5)*pitch);
88 | param.elements = [xe(:).'; ye(:).'];
89 | % Focus position
90 | x0 = 0; y0 = -2e-3; z0 = 30e-3;
91 | % Transmit time delays:
92 | dels = txdelay3(x0,y0,z0,param);
93 | % 3-D grid
94 | n = 32;
95 | [xi,yi,zi] = meshgrid(linspace(-5e-3,5e-3,n),linspace(-5e-3,5e-3,n),...
96 | linspace(0,6e-2,4*n));
97 | % RMS pressure field
98 | RP = pfield3(xi,yi,zi,dels,param);
99 | % Display the pressure field
100 | slice(xi*1e2,yi*1e2,zi*1e2,20*log10(RP/max(RP(:))),...
101 | x0*1e2,y0*1e2,z0*1e2)
102 | shading flat
103 | colormap(hot), caxis([-20 0])
104 | set(gca,'zdir','reverse'), axis equal
105 | alpha color % some transparency
106 | c = colorbar; c.YTickLabel{end} = '0 dB';
107 | zlabel('[cm]')
108 |
109 | Example #2:
110 | ----------
111 | %-- Generate a pressure field focused on a line
112 | % 3-MHz matrix array with 32x32 elements
113 | param.fc = 3e6;
114 | param.bandwidth = 70;
115 | param.width = 250e-6;
116 | param.height = 250e-6;
117 | % position of the elements (pitch = 300 microns)
118 | pitch = 300e-6;
119 | [xe,ye] = meshgrid(((1:32)-16.5)*pitch);
120 | param.elements = [xe(:).'; ye(:).'];
121 | % Oblique focus-line @ z = 2.5cm
122 | x0 = [-1e-2 1e-2]; y0 = [-1e-2 1e-2]; z0 = [2.5e-2 2.5e-2];
123 | % Transmit time delays:
124 | dels = txdelay3(x0,y0,z0,param);
125 | % 3-D grid
126 | n = 32;
127 | [xi,yi,zi] = meshgrid(linspace(-5e-3,5e-3,n),linspace(-5e-3,5e-3,n),...
128 | linspace(0,6e-2,4*n));
129 | % RMS pressure field
130 | RP = pfield3(xi,yi,zi,dels,param);
131 | % Display the elements
132 | figure, plot3(xe*1e2,ye*1e2,0*xe,'b.')
133 | % Display the pressure field
134 | contourslice(xi*1e2,yi*1e2,zi*1e2,RP,[],[],.5:.5:6,15)
135 | set(gca,'zdir','reverse'), axis equal
136 | colormap(hot)
137 | zlabel('[cm]')
138 | view(-35,20), box on
139 |
140 |
141 | This function is part of MUST (Matlab UltraSound Toolbox).
143 | MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later
144 |
145 | See also PFIELD3, SIMUS3, DASMTX3, GETPARAM, TXDELAY.
146 |
147 | -- Damien Garcia -- 2022/10, last update: 2022/10/29
148 | website: www.BiomeCardio.com
150 | """
151 |
152 |
153 | #-- Check the input arguments
154 | if len(args) == 4:
155 | if isinstance(args[3], utils.Param):
156 | # Origo: TXDELAY3(x0,y0,z0,param)
157 | param = args[3]
158 | option = 'Origo'
159 | elif isinstance(args[0], utils.Param):
160 | # Diverging wave: TXDELAY3(param,TILTx,TILTx,Omega)
161 | param = args[0]
162 | option = 'Diverging Wave'
163 | else:
164 | ValueError('Wrong input arguments. PARAM must be a structure.')
165 | elif len(args) == 3: # Plane wave: TXDELAY3(param,TILTx,TILTy)
166 | param = args[0]
167 | option = 'Plane Wave'
168 | else:
169 | ValueError('Wrong input arguments.')
170 |
171 | assert isinstance(param, utils.Param),'Wrong input arguments. PARAM must be a structure.'
172 |
173 | #-- Coordinates of the transducer elements (xe,ye)
174 | assert utils.isfield(param,'elements'), 'PARAM.elements must contain the x- and y-locations of the transducer elements.'
175 | assert param.elements.shape[0]==2, 'PARAM.elements must have two rows that contain the x (1st row) and y (2nd row) coordinates of the transducer elements.'
176 | xe = param.elements[0,:]
177 | ye = param.elements[1,:]
178 |
179 | xe = xe.reshape((1, -1), order="F")
180 | ye = ye.reshape((1, -1), order="F")
181 |
182 | #-- Longitudinal velocity (in m/s)
183 | if not utils.isfield(param,'c'):
184 | param.c = 1540
185 |
186 | c = param.c
187 |
188 |
189 | #-- Positions of the transducer elements
190 | x, z, THe, h= param.getElementPositions()
191 |
192 | if option == 'Plane Wave':
193 | # DR : problems checking if it is not a vector, used as a number later, no need for casting into array
194 | tiltx = args[1] # tiltx = np.array(args[1]).reshape((-1, 1))
195 | tilty = args[2] # tilty = np.array(args[2]).reshape((-1, 1))
196 | assert np.isscalar(tiltx+tilty), 'TILTx and TILTy must be two scalars.'
197 | assert np.all(np.abs(np.array([tiltx,tilty]))=0, 'The solid angle must be nonnegative'
209 |
210 | # check if the elements are on a plaid grid
211 | uxe = np.unique(xe)
212 | uye = np.unique(ye)
213 | [xep,yep] = np.meshgrid(uxe,uye)
214 | test = np.all(np.sort(xep.flatten())==np.sort(xe)) & np.all(np.sort(yep.flatten())==np.sort(ye))
215 | assert test, 'The elements must be on a plaid grid with the "Diverging wave" option, i.e. the element positions must form a plaid grid.'
216 |
217 | # rotation of the point [0,0,-1]
218 | # TILTx about the x-axis, TILTy about the y-axis
219 | x = -np.sin(tilty)*np.cos(tiltx)
220 | y = np.sin(tiltx)
221 | z = -np.cos(tilty)*np.cos(tiltx)
222 |
223 | # corresponding azimuth and elevation
224 | # [az,el] = cart2sph(x,y,z);
225 | az = np.arctan2(y,x)
226 | el = np.arctan2(z,np.sqrt(x**2 + y**2))
227 |
228 | # dimensions of the matrix array
229 | l = np.max(xe)-np.min(xe)+param.width # width of the matrix array
230 | b = np.max(ye)-np.min(ye)+param.height # height of the matrix array
231 |
232 |
233 | # we know the azimuth and elevation of the virtual source
234 | # we need its radial position r for the given solid angle
235 | def myfun(r, omega=omega, l=l, b=b, az=az, el=el):
236 | return abs(solidAngle(r,l,b,az,el) - omega) # Define the function to minimize
237 | r = scipy.optimize.fminbound(myfun, 0, 2*np.pi, xtol=1e-6) # DR : function tolerance not being taken into account.
238 | # r = scipy.optimize.minimize_scalar(myfun, bounds=(0, 2*np.pi), method='bounded', options={'xatol': 1e-6, 'fatol': 1e-6}) # DR : alternative (not tried)
239 |
240 | # position of the virtual source, and TX delays
241 | cos_el = np.cos(el)
242 | x0 = r*cos_el*np.cos(az)
243 | y0 = r*cos_el*np.sin(az)
244 | z0 = r*np.sin(el)
245 | delays = txdelay3(x0,y0,z0,param)
246 |
247 | #-----
248 | elif option == 'Origo':
249 | x0 = np.array(args[0]).reshape((-1, 1), order="F")
250 | y0 = np.array(args[1]).reshape((-1, 1), order="F")
251 | z0 = np.array(args[2]).reshape((-1, 1), order="F")
252 | assert len(x0)==len(y0)==len(z0), 'X0, Y0, and Z0 must have the same length.'
253 | if len(x0)==1: # focus point
254 | d = np.sqrt((xe-x0)**2 + (ye-y0)**2 + z0**2)
255 | elif len(x0)==2: # focus line
256 | X = np.concatenate((xe, ye, np.zeros_like(xe)), axis=0)
257 | x1 = np.tile(np.concatenate((x0[0],y0[0],z0[0]), axis=0).reshape((-1,1), order="F"), (1, x0.shape[1]))
258 | x2 = np.tile(np.concatenate((x0[1],y0[1],z0[1]), axis=0).reshape((-1,1), order="F"), (1, x0.shape[1]))
259 | d = np.linalg.norm(np.cross(X-x1,X-x2, axis=0), axis=0)/np.linalg.norm(x2-x1)
260 | else:
261 | ValueError('X0, Y0, and Z0 must have 1 or 2 elements.')
262 | delays = -d/c*np.sign(z0)
263 |
264 | delays = delays-np.min(delays,-1).reshape((-1, 1))
265 |
266 | param.TXdelay = delays
267 | return delays
268 |
269 |
270 | def solidAngle(r, l, b, az, el):
271 | # Advanced Geometry: Mathematical Analysis of Unified Articles
272 | # by Harish Chandra Rajpoot
273 | # Publisher: Notion Press; First Edition (2014)
274 | # ISBN-10: 9383808152
275 | # ISBN-13: 978-9383808151
276 |
277 | # https://www.slideshare.net/hcr1991/solid-angle-subtended-by-a-rectangular-plane-at-any-point-in-the-space
278 |
279 | # Khadjavi, A.
280 | # "Calculation of solid angle subtended by rectangular apertures."
281 | # JOSA 58.10 (1968): 1417-1418.
282 |
283 | # notations from Harish Chandra Rajpoot
284 | L1 = l/2 + r*np.cos(el)*np.cos(az)
285 | L2 = -l/2 + r*np.cos(el)*np.cos(az)
286 | B1 = b/2 + r*np.cos(el)*np.sin(az)
287 | B2 = b/2 - r*np.cos(el)*np.sin(az)
288 | H = r*np.sin(el)
289 |
290 | # Solid angle calculation
291 | def w(l, b):
292 | return np.arcsin(l*b/np.hypot(l, H)/np.hypot(b, H))
293 |
294 | O = w(L1, B1) + w(L1, B2) - w(L2, B1) - w(L2, B2)
295 | return O
296 |
--------------------------------------------------------------------------------
/src/pymust/utils.py:
--------------------------------------------------------------------------------
1 | import numpy as np, scipy, scipy.interpolate, multiprocessing, multiprocessing.pool
2 | from abc import ABC
3 | import inspect, matplotlib, pickle, os, matplotlib.pyplot as plt, copy
4 | from collections import deque
5 |
6 |
7 | class dotdict(dict, ABC):
8 | """Copied from https://stackoverflow.com/questions/2352181/how-to-use-a-dot-to-access-members-of-dictionary"""
9 | """dot.notation access to dictionary attributes"""
10 | __getattr__ = dict.get
11 | __setattr__ = dict.__setitem__
12 | __delattr__ = dict.__delitem__
13 | def ignoreCaseInFieldNames(self):
14 | """Convert all field names to lower case"""
15 | names = self.names
16 | todelete =[]
17 | for k, v in self.items():
18 | if k.lower() in names and k in names:
19 | if k.lower() == k:
20 | continue
21 | elif names[k] in self:
22 | raise ValueError(f'Repeated key {k}')
23 | else:
24 | self[names[k]] = v
25 | todelete.append(k)
26 | for k in todelete:
27 | del self[k]
28 | return self
29 | def copy(self):
30 | return copy.deepcopy(self)
31 | def __getstate__(self):
32 | d = {k : v for k,v in self.items()}
33 | return d
34 | def __setstate__(self, d):
35 | for k, v in self.items():
36 | self[k] = v
37 |
38 | class Options(dotdict):
39 | default_Number_Workers = multiprocessing.cpu_count()
40 | @property
41 | def names(self):
42 | names = {'dBThresh','ElementSplitting',
43 | 'FullFrequencyDirectivity','FrequencyStep','ParPool',
44 | 'WaitBar'}
45 | return {n.lower(): n for n in names}
46 |
47 | def setParPool(self, workers, mode = 'process'):
48 | if mode not in ['process', 'thread']:
49 | raise ValueError('ParPoolMode must be either "process" or "thread"')
50 | self.ParPool_NumWorkers = workers
51 | self.ParPoolMode = mode
52 |
53 | def getParallelPool(self):
54 | workers = self.get('ParPool_NumWorkers', self.default_Number_Workers)
55 | mode = self.get('ParPoolMode', 'thread')
56 | if mode == 'process':
57 | pool = multiprocessing.Pool(workers)
58 | elif mode == 'thread':
59 | pool = multiprocessing.pool.ThreadPool(workers)
60 | else:
61 | raise ValueError('ParPoolMode must be either "process" or "thread"')
62 | return pool
63 |
64 | def getParallelSplitIndices(self, N,n_threads = None):
65 | if hasattr(N, '__len__'):
66 | N = len(N)
67 | assert isinstance(N, int), 'N must be an integer'
68 |
69 | n_threads = self.get('ParPool_NumWorkers', self.default_Number_Workers) if n_threads is None else n_threads
70 | #Create indices for parallel processing, split in workers
71 | idx = np.arange(0, N, N//n_threads)
72 |
73 | #Repeat along new axis
74 | idx = np.stack([idx, np.roll(idx, -1)], axis = 1)
75 | idx[-1, 1] = N
76 | return idx
77 |
78 | class Param(dotdict):
79 | @property
80 | def names(self):
81 | names = {'attenuation','baffle','bandwidth','c','fc',
82 | 'fnumber','focus','fs','height','kerf','movie','Nelements',
83 | 'passive','pitch','radius','RXangle','RXdelay'
84 | 'TXapodization','TXfreqsweep','TXnow','t0','width'}
85 | return {n.lower(): n for n in names}
86 |
87 | def getElementPositions(self):
88 | """
89 | Returns the position of each piezoelectrical element in the probe.
90 | """
91 | RadiusOfCurvature = self.radius
92 | NumberOfElements = self.Nelements
93 |
94 | if np.isinf(RadiusOfCurvature):
95 | #% Linear array
96 | xe = (np.arange(NumberOfElements)-(NumberOfElements-1)/2)*self.pitch
97 | ze = np.zeros((1,NumberOfElements))
98 | THe = np.zeros_like(ze)
99 | h = np.zeros_like(ze)
100 | else:
101 | #% Convex array
102 | chord = 2*RadiusOfCurvature*np.sin(np.arcsin(self.pitch/2/RadiusOfCurvature)*(NumberOfElements-1))
103 | h = np.sqrt(RadiusOfCurvature**2-chord**2/4); #% apothem
104 | #% https://en.wikipedia.org/wiki/Circular_segment
105 | #% THe = angle of the normal to element #e with respect to the z-axis
106 | THe = np.linspace(np.arctan2(-chord/2,h),np.arctan2(chord/2,h),NumberOfElements)
107 | ze = RadiusOfCurvature*np.cos(THe)
108 | xe = RadiusOfCurvature*np.sin(THe)
109 | ze = ze-h
110 | return xe.reshape((1,-1)), ze.reshape((1,-1)), THe.reshape((1,-1)), h.reshape((1,-1))
111 |
112 | def getPulseSpectrumFunction(self, FreqSweep = None):
113 | if 'TXnow' not in self:
114 | self.TXnow = 1
115 |
116 | #-- FREQUENCY SPECTRUM of the transmitted pulse
117 | if FreqSweep is None:
118 | # We want a windowed sine of width PARAM.TXnow
119 | T = self.TXnow /self.fc
120 | wc = 2 * np.pi * self.fc
121 | pulseSpectrum = lambda w = None: 1j * (mysinc(T * (w - wc) / 2) - mysinc(T * (w + wc) / 2))
122 | else:
123 | # We want a linear chirp of width PARAM.TXnow
124 | # (https://en.wikipedia.org/wiki/Chirp_spectrum#Linear_chirp)
125 | T = self.TXnow / self.fc
126 | wc = 2 * np.pi * self.fc
127 | dw = 2 * np.pi * FreqSweep
128 | s2 = lambda w = None: np.multiply(np.sqrt(np.pi * T / dw) * np.exp(- 1j * (w - wc) ** 2 * T / 2 / dw),(fresnelint((dw / 2 + w - wc) / np.sqrt(np.pi * dw / T)) + fresnelint((dw / 2 - w + wc) / np.sqrt(np.pi * dw / T))))
129 | pulseSpectrum = lambda w = None: (1j * s2(w) - 1j * s2(- w)) / T
130 | return pulseSpectrum
131 |
132 | def getProbeFunction(self):
133 | #%-- FREQUENCY RESPONSE of the ensemble PZT + probe
134 | #% We want a generalized normal window (6dB-bandwidth = PARAM.bandwidth)
135 | #% (https://en.wikipedia.org/wiki/Window_function#Generalized_normal_window)
136 | #-- FREQUENCY RESPONSE of the ensemble PZT + probe
137 | # We want a generalized normal window (6dB-bandwidth = PARAM.bandwidth)
138 | # (https://en.wikipedia.org/wiki/Window_function#Generalized_normal_window)
139 | wc = 2 * np.pi * self.fc
140 | wB = self.bandwidth * wc / 100
141 | p = np.log(126) / np.log(2 * wc / wB)
142 | probeSpectrum_sqr = lambda w: np.exp(- np.power(np.abs(w - wc) / (wB / 2 / np.power(np.log(2), 1 / p)), p))
143 | # The frequency response is a pulse-echo (transmit + receive) response. A
144 | # square root is thus required when calculating the pressure field:
145 | probeSpectrum = lambda w: np.sqrt(probeSpectrum_sqr(w))
146 | return probeSpectrum
147 |
148 | # To maintain same notation as matlab
149 | def interp1(y, xNew, kind):
150 | if kind == 'spline':
151 | kind = 'cubic' #3rd order spline
152 | interpolator = scipy.interpolate.interp1d(np.arange(len(y)), y, kind = kind)
153 | return interpolator(xNew)
154 |
155 | def isnumeric(x):
156 | return isinstance(x, np.ndarray) or isinstance(x, int) or isinstance(x, float) or isinstance(x, np.number)
157 |
158 | def iscomplex(x):
159 | return (isinstance(x, np.ndarray) and np.iscomplexobj(x)) or isinstance(x, complex)
160 |
161 | def islogical(v):
162 | return isinstance(v, bool)
163 |
164 | def isfield(d, k ):
165 | return k in d
166 |
167 | mysinc = lambda x = None: np.sinc(x / np.pi) # [note: In MATLAB/numpy, sinc is sin(pi*x)/(pi*x)]
168 |
169 |
170 | def shiftdim(array, n=None):
171 | """
172 | From stack overflow https://stackoverflow.com/questions/67584148/python-equivalent-of-matlab-shiftdim
173 | """
174 | if n is not None:
175 | if n >= 0:
176 | axes = tuple(range(len(array.shape)))
177 | new_axes = deque(axes)
178 | new_axes.rotate(n)
179 | return np.moveaxis(array, axes, tuple(new_axes))
180 | return np.expand_dims(array, axis=tuple(range(-n)))
181 | else:
182 | idx = 0
183 | for dim in array.shape:
184 | if dim == 1:
185 | idx += 1
186 | else:
187 | break
188 | axes = tuple(range(idx))
189 | # Note that this returns a tuple of 2 results
190 | return np.squeeze(array, axis=axes), len(axes)
191 |
192 | def isEmpty(x):
193 | return x is None or (isinstance(x, list) and len(x) == 0) or (isinstance(x, np.ndarray) and len(x) == 0)
194 |
195 | def emptyArrayIfNone(x):
196 | if isEmpty(x):
197 | x = np.array([])
198 | return x
199 |
200 | def eps(s = 'single'):
201 | if s == 'single':
202 | return 1.1921e-07
203 | else:
204 | raise ValueError()
205 |
206 | def nextpow2(n):
207 | i = 1
208 | while (1 << i) < n:
209 | i += 1
210 | return i
211 |
212 | def fresnelint(x):
213 | # FRESNELINT Fresnel integral.
214 |
215 | # J = FRESNELINT(X) returns the Fresnel integral J = C + 1i*S.
216 |
217 | # We use the approximation introduced by Mielenz in
218 | # Klaus D. Mielenz, Computation of Fresnel Integrals. II
219 | # J. Res. Natl. Inst. Stand. Technol. 105, 589 (2000), pp 589-590
220 |
221 | siz0 = x.shape
222 | x = x.flatten()
223 |
224 | issmall = np.abs(x) <= 1.6
225 | c = np.zeros(x.shape)
226 | s = np.zeros(x.shape)
227 | # When |x| < 1.6, a Taylor series is used (see Mielenz's paper)
228 | if np.any(issmall):
229 | n = np.arange(0,11)
230 | cn = np.concatenate([[1], np.cumprod(- np.pi ** 2 * (4 * n + 1) / (4 * (2 * n + 1) *(2 * n + 2)*(4 * n + 5)))])
231 | sn = np.concatenate([[1],np.cumprod(- np.pi ** 2 * (4 * n + 3) / (4 * (2 * n + 2)*(2 * n + 3)*(4 * n + 7)))]) * np.pi / 6
232 | n = np.concatenate([n,[11]]).reshape((1,-1))
233 | c[issmall] = np.sum(cn.reshape((1,-1))*x[issmall].reshape((-1, 1)) ** (4 * n + 1), 1)
234 | s[issmall] = np.sum(sn.reshape((1,-1))*x[issmall].reshape((-1, 1)) ** (4 * n + 3), 1)
235 |
236 | # When |x| > 1.6, we use the following:
237 | if not np.all(issmall ):
238 | n = np.arange(0,11+1)
239 | fn = np.array([0.318309844,9.34626e-08,- 0.09676631,0.000606222,0.325539361,0.325206461,- 7.450551455,32.20380908,- 78.8035274,118.5343352,- 102.4339798,39.06207702])
240 | fn = fn.reshape((1, fn.shape[0]))
241 | gn = np.array([0,0.101321519,- 4.07292e-05,- 0.152068115,- 0.046292605,1.622793598,- 5.199186089,7.477942354,- 0.695291507,- 15.10996796,22.28401942,- 10.89968491])
242 | gn = gn.reshape((1, gn.shape[0]))
243 |
244 | fx = np.sum(np.multiply(fn,x[not issmall ] ** (- 2 * n - 1)), 1)
245 | gx = np.sum(np.multiply(gn,x[not issmall ] ** (- 2 * n - 1)), 1)
246 | c[not issmall ] = 0.5 * np.sign(x[not issmall ]) + np.multiply(fx,np.sin(np.pi / 2 * x[not issmall ] ** 2)) - np.multiply(gx,np.cos(np.pi / 2 * x[not issmall ] ** 2))
247 | s[not issmall ] = 0.5 * np.sign(x[not issmall ]) - np.multiply(fx,np.cos(np.pi / 2 * x[not issmall ] ** 2)) - np.multiply(gx,np.sin(np.pi / 2 * x[not issmall ] ** 2))
248 |
249 | f = np.reshape(c, siz0) + 1j * np.reshape(s, siz0)
250 | return f
251 |
252 |
253 | # Plotting
254 | def polarplot(x, z, v, cmap = 'gray',background = 'black', probeUpward = True, **kwargs):
255 | plt.pcolormesh(x, z, v, cmap = cmap, shading='gouraud', **kwargs)
256 | plt.axis('equal')
257 | ax = plt.gca()
258 | ax.set_facecolor(background)
259 | if probeUpward:
260 | ax.invert_yaxis()
261 |
262 |
263 | def getDopplerColorMap():
264 | source_file_path = inspect.getfile(inspect.currentframe())
265 | with open( os.path.join(os.path.dirname(source_file_path), 'Data', 'colorMap.pkl'), 'rb') as f:
266 | dMap = pickle.load(f)
267 | new_cmap = matplotlib.colors.LinearSegmentedColormap('doppler', dMap)
268 | dopplerCM = matplotlib.cm.ScalarMappable(norm=matplotlib.colors.Normalize(),cmap=new_cmap)
269 | return dopplerCM
270 |
271 | def applyDasMTX(M, IQ, imageShape):
272 | return (M @ IQ.flatten(order = 'F')).reshape(imageShape, order = 'F')
273 |
--------------------------------------------------------------------------------
/src/pymust/wfilt.py:
--------------------------------------------------------------------------------
1 | import numpy as np,scipy, logging
2 | import typing
3 |
4 | def wfilt(SIG: np.ndarray, method: str, n: int) -> np.ndarray:
5 | """
6 | %WFILT Wall filtering (or clutter filtering)
7 | % fSIG = WFILT(SIG,METHOD,N) high-pass (wall) filters the RF or I/Q
8 | % signals stored in the 3-D array SIG for Doppler imaging.
9 | %
10 | % The first dimension of SIG (i.e. each column) corresponds to a single
11 | % RF or I/Q signal over (fast-) time, with the first column corresponding
12 | % to the first transducer element. The third dimension corresponds to the
13 | % slow-time axis.
14 | %
15 | % Three methods are available.
16 | % METHOD can be one of the following (case insensitive):
17 | %
18 | % 1) 'poly' - Least-squares (Nth degree) polynomial regression.
19 | % Orthogonal Legendre polynomials are used. The fitting
20 | % polynomial is removed from the original I/Q or RF data to
21 | % keep the high-frequency components. N (>=0) represents the
22 | % degree of the polynomials. The (slow-time) mean values are
23 | % removed if N = 0 (the polynomials are reduced to
24 | % constants).
25 | % 2) 'dct' - Truncated DCT (Discrete Cosine Transform).
26 | % Discrete cosine transforms (DCT) and inverse DCT are
27 | % performed along the slow-time dimension. The signals are
28 | % filtered by withdrawing the first N (>=1) components, i.e.
29 | % those corresponding to the N lowest frequencies (with
30 | % respect to slow-time).
31 | % 3) 'svd' - Truncated SVD (Singular Value Decomposition).
32 | % An SVD is carried out after a column arrangement of the
33 | % slow-time dimension. The signals are filtered by
34 | % withdrawing the top N singular vectors, i.e. those
35 | % corresponding to the N greatest singular values.
36 | %
37 | %
38 | % This function is part of MUST (Matlab UltraSound Toolbox).
39 | % MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later
40 | %
41 | % See also IQ2DOPPLER, RF2IQ.
42 | %
43 | % -- Damien Garcia -- 2014/06, last update 2023/05/12
44 | % website: www.BiomeCardio.com
46 | """
47 | logging.warning('NOTE GB: this code has not been tested!')
48 |
49 | #%-- Check the input arguments
50 |
51 | assert SIG.ndims ==3 and SIG.shape[2] >= 2,'SIG must be a 3-D array with SIG.shape[2]>=2';
52 | assert isinstance(n, int) and n >= 0, 'N must be a nonnegative integer.'
53 |
54 | siz0 = SIG.shape
55 | N = siz0[2]; # number of slow-time samples
56 | method = method.lower()
57 |
58 | if method == 'poly':
59 | #% ---------------------------------
60 | #% POLYNOMIAL REGRESSION WALL FILTER
61 | #% ---------------------------------
62 |
63 | assert N>n,'The packet length must be >N.'
64 |
65 | # If the degree is 0, the mean is removed.
66 | if n==0:
67 | return SIG-np.mean(SIG,2);
68 |
69 | # GB TODO: use Legendre Matrix instead (more numerically stable and efficient)
70 | V = np.vander(np.linspace(0,1,N), n+1) # Vandermonde matrix
71 | A = np.eye(N) - V @ np.linalg.pinv(V) # Projection matrix
72 | # Multiply along the slow-time dimension
73 | SIG = np.einsum('ij,nkj->nki', A, SIG)
74 |
75 |
76 | elif method == 'dct':
77 | #% -------------------------------------
78 | #% DISCRETE COSINE TRANSFORM WALL FILTER
79 | #% -------------------------------------
80 |
81 | assert n>0, 'N must be >0 with the "dct" method.'
82 | assert N>=n,'The packet length must be >=N.'
83 |
84 | #% If the degree is 0, the mean is removed.
85 | if n==1:
86 | return SIG-np.mean(SIG,2);
87 |
88 | D = scipy.fft.dct(np.eye(N), norm='ortho', axis=0)[n:, :] #DCT matrix, only high frequencies
89 | D= D.T@D # Create the projection matrix
90 | #Multiply along the slow-time dimension
91 | SIG = np.einsum('ij,nkj->nki', D, SIG)
92 |
93 | elif method == 'svd':
94 | #% ----------------------------------------
95 | #% SINGULAR VALUE DECOMPOSITION WALL FILTER
96 | #% ----------------------------------------
97 |
98 | assert n>0,'N must be >0 with the "svd" method.'
99 | assert N>=n,'The packet length must be >=N.'
100 |
101 | #% Each column represents a column-rearranged frame.
102 | SIG = SIG.reshape((-1, siz0[2]))
103 |
104 | U,S,V = scipy.svd(SIG,full_matrices = False); # SVD decomposition
105 | SIG = U[:,n:N] @ S[n:N,n:N] @V[:,n:N].T; # high-pass filtering
106 | SIG = SIG.reshape(siz0)
107 | else:
108 | raise ValueError('METHOD must be "poly", "dct", or "svd".')
109 |
110 | return SIG
--------------------------------------------------------------------------------
/tutorials/Figures/s1_ex1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/tutorials/Figures/s1_ex1.png
--------------------------------------------------------------------------------
/tutorials/Figures/s1_ex3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/tutorials/Figures/s1_ex3.png
--------------------------------------------------------------------------------
/tutorials/P1_P2.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/tutorials/P1_P2.zip
--------------------------------------------------------------------------------
/tutorials/export.py:
--------------------------------------------------------------------------------
1 | # To export the ipynb to text files (for submission)
2 | import json, os
3 | #Copied from https://stackoverflow.com/questions/37797709/convert-json-ipython-notebook-ipynb-to-py-file
4 | import argparse
5 |
6 | parser = argparse.ArgumentParser()
7 | parser.add_argument("-input", "-i", type = str, help="Input file/folder", default = '.')
8 |
9 | parser.add_argument("--noCode", help="Do not write the code",
10 | action="store_true")
11 | parser.add_argument("-output", "-o", type = str, help="Output", default = 'ExportedNB')
12 |
13 | args = parser.parse_args()
14 |
15 | writeCode = not args.noCode
16 | writeMarkdown = True
17 | writeAllMarkdown = True
18 |
19 | files = []
20 | if not os.path.exists(args.input):
21 | print("Input file/folder does not exist")
22 | exit()
23 | elif os.path.isfile(args.input):
24 | files.append(args.input)
25 | else:
26 | for file in os.listdir(args.input):
27 | if not file.endswith('.ipynb'):
28 | continue
29 | files.append(os.path.join(args.input, file))
30 | if not os.path.exists(args.output):
31 | os.makedirs(args.output)
32 |
33 | for file in files:
34 | code = json.load(open(file))
35 | py_file = open(f"{args.output}/{file.replace('ipynb', 'txt')}", "w+")
36 |
37 | for cell in code['cells']:
38 | if cell['cell_type'] == 'code' and writeCode:
39 | for line in cell['source']:
40 | py_file.write(line)
41 | py_file.write("\n")
42 | elif cell['cell_type'] == 'markdown' and writeMarkdown:
43 | py_file.write("\n")
44 | for line in cell['source']:
45 | if line and line[0] == "#" or writeAllMarkdown:
46 | py_file.write(line)
47 | py_file.write("\n")
48 |
49 | py_file.close()
--------------------------------------------------------------------------------