zea 0.0.7__py3-none-any.whl → 0.0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zea/__init__.py +3 -3
- zea/agent/masks.py +2 -2
- zea/agent/selection.py +3 -3
- zea/backend/__init__.py +1 -1
- zea/backend/tensorflow/dataloader.py +1 -5
- zea/beamform/beamformer.py +4 -2
- zea/beamform/pfield.py +2 -2
- zea/beamform/pixelgrid.py +1 -1
- zea/data/__init__.py +0 -9
- zea/data/augmentations.py +222 -29
- zea/data/convert/__init__.py +1 -6
- zea/data/convert/__main__.py +164 -0
- zea/data/convert/camus.py +106 -40
- zea/data/convert/echonet.py +184 -83
- zea/data/convert/echonetlvh/README.md +2 -3
- zea/data/convert/echonetlvh/{convert_raw_to_usbmd.py → __init__.py} +174 -103
- zea/data/convert/echonetlvh/manual_rejections.txt +73 -0
- zea/data/convert/echonetlvh/precompute_crop.py +43 -64
- zea/data/convert/picmus.py +37 -40
- zea/data/convert/utils.py +86 -0
- zea/data/convert/verasonics.py +1247 -0
- zea/data/data_format.py +124 -6
- zea/data/dataloader.py +12 -7
- zea/data/datasets.py +109 -70
- zea/data/file.py +119 -82
- zea/data/file_operations.py +496 -0
- zea/data/preset_utils.py +2 -2
- zea/display.py +8 -9
- zea/doppler.py +5 -5
- zea/func/__init__.py +109 -0
- zea/{tensor_ops.py → func/tensor.py} +113 -69
- zea/func/ultrasound.py +500 -0
- zea/internal/_generate_keras_ops.py +5 -5
- zea/internal/checks.py +6 -12
- zea/internal/operators.py +4 -0
- zea/io_lib.py +108 -160
- zea/metrics.py +6 -5
- zea/models/__init__.py +1 -1
- zea/models/diffusion.py +63 -12
- zea/models/echonetlvh.py +1 -1
- zea/models/gmm.py +1 -1
- zea/models/lv_segmentation.py +2 -0
- zea/ops/__init__.py +188 -0
- zea/ops/base.py +442 -0
- zea/{keras_ops.py → ops/keras_ops.py} +2 -2
- zea/ops/pipeline.py +1472 -0
- zea/ops/tensor.py +356 -0
- zea/ops/ultrasound.py +890 -0
- zea/probes.py +2 -10
- zea/scan.py +35 -28
- zea/tools/fit_scan_cone.py +90 -160
- zea/tools/selection_tool.py +1 -1
- zea/tracking/__init__.py +16 -0
- zea/tracking/base.py +94 -0
- zea/tracking/lucas_kanade.py +474 -0
- zea/tracking/segmentation.py +110 -0
- zea/utils.py +11 -2
- {zea-0.0.7.dist-info → zea-0.0.9.dist-info}/METADATA +5 -1
- {zea-0.0.7.dist-info → zea-0.0.9.dist-info}/RECORD +62 -48
- zea/data/convert/matlab.py +0 -1237
- zea/ops.py +0 -3294
- {zea-0.0.7.dist-info → zea-0.0.9.dist-info}/WHEEL +0 -0
- {zea-0.0.7.dist-info → zea-0.0.9.dist-info}/entry_points.txt +0 -0
- {zea-0.0.7.dist-info → zea-0.0.9.dist-info}/licenses/LICENSE +0 -0
zea/data/convert/matlab.py
DELETED
|
@@ -1,1237 +0,0 @@
|
|
|
1
|
-
"""Functionality to convert Verasonics matlab raw files to the zea format.
|
|
2
|
-
|
|
3
|
-
Example (MATLAB):
|
|
4
|
-
|
|
5
|
-
.. code-block:: matlab
|
|
6
|
-
|
|
7
|
-
>> setup_script;
|
|
8
|
-
>> VSX;
|
|
9
|
-
>> save_raw('C:/path/to/raw_data.mat');
|
|
10
|
-
|
|
11
|
-
Then in python:
|
|
12
|
-
|
|
13
|
-
.. code-block:: python
|
|
14
|
-
|
|
15
|
-
from zea.data_format.zea_from_matlab_raw import zea_from_matlab_raw
|
|
16
|
-
|
|
17
|
-
zea_from_matlab_raw("C:/path/to/raw_data.mat", "C:/path/to/output.hdf5")
|
|
18
|
-
|
|
19
|
-
Or alternatively, use the script below to convert all .mat files in a directory:
|
|
20
|
-
|
|
21
|
-
.. code-block:: bash
|
|
22
|
-
|
|
23
|
-
python zea/data/convert/matlab.py "C:/path/to/directory"
|
|
24
|
-
|
|
25
|
-
or without the directory argument, the script will prompt you to select a directory
|
|
26
|
-
using a file dialog.
|
|
27
|
-
|
|
28
|
-
Event structure
|
|
29
|
-
---------------
|
|
30
|
-
|
|
31
|
-
By default the zea dataformat saves all the data to an hdf5 file with the following structure:
|
|
32
|
-
|
|
33
|
-
.. code-block:: text
|
|
34
|
-
|
|
35
|
-
regular_zea_dataset.hdf5
|
|
36
|
-
├── data
|
|
37
|
-
└── scan
|
|
38
|
-
└── center_frequency: 1MHz
|
|
39
|
-
|
|
40
|
-
The data is stored in the ``data`` group and the scan parameters are stored in the ``scan``.
|
|
41
|
-
However, when we do an adaptive acquisition, some scanning parameters might change. These
|
|
42
|
-
blocks of data with consistent scanning parameters we call events. In the case we have multiple
|
|
43
|
-
events, we store the data in the following structure:
|
|
44
|
-
|
|
45
|
-
.. code-block:: text
|
|
46
|
-
|
|
47
|
-
zea_dataset.hdf5
|
|
48
|
-
├── event_0
|
|
49
|
-
│ ├── data
|
|
50
|
-
│ └── scan
|
|
51
|
-
│ └── center_frequency: 1MHz
|
|
52
|
-
├── event_1
|
|
53
|
-
│ ├── data
|
|
54
|
-
│ └── scan
|
|
55
|
-
│ └── center_frequency: 2MHz
|
|
56
|
-
├── event_2
|
|
57
|
-
│ ├── data
|
|
58
|
-
│ └── scan
|
|
59
|
-
└── event_3
|
|
60
|
-
├── data
|
|
61
|
-
└── scan
|
|
62
|
-
|
|
63
|
-
This structure is supported by the zea toolbox. The way we can save the data in this structure
|
|
64
|
-
from the Verasonics, is by changing the setup script to keep track of the TX struct at each event.
|
|
65
|
-
|
|
66
|
-
The way this is done is still in development, an example of such an acquisition script that is
|
|
67
|
-
compatible with saving event structures is found here:
|
|
68
|
-
`setup_agent.m <https://github.com/tue-bmd/needle-tracking/blob/ius2024-demo-nc/verasonics/setup_agent.m>`_
|
|
69
|
-
|
|
70
|
-
Adding additional elements
|
|
71
|
-
--------------------------
|
|
72
|
-
|
|
73
|
-
You can add additional elements to the dataset by defining a function that reads the
|
|
74
|
-
data from the file and returns a ``DatasetElement``. Then pass the function to the
|
|
75
|
-
``zea_from_matlab_raw`` function as a list.
|
|
76
|
-
|
|
77
|
-
.. code-block:: python
|
|
78
|
-
|
|
79
|
-
def read_max_high_voltage(file):
|
|
80
|
-
lens_correction = file["Trans"]["lensCorrection"][0, 0].item()
|
|
81
|
-
return lens_correction
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def read_high_voltage_func(file):
|
|
85
|
-
return DatasetElement(
|
|
86
|
-
group_name="scan",
|
|
87
|
-
dataset_name="max_high_voltage",
|
|
88
|
-
data=read_max_high_voltage(file),
|
|
89
|
-
description="The maximum high voltage used by the Verasonics system.",
|
|
90
|
-
unit="V",
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
zea_from_matlab_raw(
|
|
95
|
-
"C:/path/to/raw_data.mat",
|
|
96
|
-
"C:/path/to/output.hdf5",
|
|
97
|
-
[read_high_voltage_func],
|
|
98
|
-
)
|
|
99
|
-
""" # noqa: E501
|
|
100
|
-
|
|
101
|
-
import argparse
|
|
102
|
-
import os
|
|
103
|
-
import sys
|
|
104
|
-
import traceback
|
|
105
|
-
from pathlib import Path
|
|
106
|
-
|
|
107
|
-
import h5py
|
|
108
|
-
import numpy as np
|
|
109
|
-
|
|
110
|
-
from zea import log
|
|
111
|
-
from zea.data.data_format import DatasetElement, generate_zea_dataset
|
|
112
|
-
from zea.ops import LogCompress, Normalize
|
|
113
|
-
from zea.utils import strtobool
|
|
114
|
-
|
|
115
|
-
_VERASONICS_TO_ZEA_PROBE_NAMES = {
|
|
116
|
-
"L11-4v": "verasonics_l11_4v",
|
|
117
|
-
"L11-5v": "verasonics_l11_5v",
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def dereference_index(file, dataset, index, event=None, subindex=None):
|
|
122
|
-
"""Get the element at the given index from the dataset, dereferencing it if
|
|
123
|
-
necessary.
|
|
124
|
-
|
|
125
|
-
MATLAB stores items in struct array differently depending on the size. If the size
|
|
126
|
-
is 1, the item is stored as a regular dataset. If the size is larger, the item is
|
|
127
|
-
stored as a dataset of references to the actual data.
|
|
128
|
-
|
|
129
|
-
This function dereferences the dataset if it is a reference. Otherwise, it returns
|
|
130
|
-
the dataset.
|
|
131
|
-
|
|
132
|
-
Args:
|
|
133
|
-
file (h5py.File): The file to read the dataset from.
|
|
134
|
-
dataset (h5py.Dataset): The dataset to read the element from.
|
|
135
|
-
index (int): The index of the element to read.
|
|
136
|
-
event (int, optional): The event index. Usually we store each event in the
|
|
137
|
-
second dimension of the dataset. Defaults to None in this case we assume
|
|
138
|
-
that there is only a single event.
|
|
139
|
-
subindex (slice, optional): The subindex of the element to read after
|
|
140
|
-
referencing the actual data. Defaults to None. In this case, all the data
|
|
141
|
-
is returned.
|
|
142
|
-
"""
|
|
143
|
-
if isinstance(dataset.fillvalue, h5py.h5r.Reference):
|
|
144
|
-
if event is not None:
|
|
145
|
-
reference = dataset[index, event]
|
|
146
|
-
else:
|
|
147
|
-
reference = dataset[index, 0]
|
|
148
|
-
if subindex is None:
|
|
149
|
-
return file[reference][:]
|
|
150
|
-
else:
|
|
151
|
-
return file[reference][subindex]
|
|
152
|
-
else:
|
|
153
|
-
if index > 0:
|
|
154
|
-
log.warning(
|
|
155
|
-
f"index {index} is not a reference. You are probably "
|
|
156
|
-
"incorrectly indexing a dataset."
|
|
157
|
-
)
|
|
158
|
-
return dataset
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def get_reference_size(dataset):
|
|
162
|
-
"""Get the size of a reference dataset."""
|
|
163
|
-
if isinstance(dataset.fillvalue, h5py.h5r.Reference):
|
|
164
|
-
return len(dataset)
|
|
165
|
-
else:
|
|
166
|
-
return 1
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def decode_string(dataset):
|
|
170
|
-
"""Decode a string dataset."""
|
|
171
|
-
return "".join([chr(c) for c in dataset.squeeze()])
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def read_probe_geometry(file):
|
|
175
|
-
"""
|
|
176
|
-
Read the probe geometry from the file.
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
file (h5py.File): The file to read the probe geometry from. (The file should
|
|
180
|
-
be opened in read mode.)
|
|
181
|
-
|
|
182
|
-
Returns:
|
|
183
|
-
probe_geometry (np.ndarray): The probe geometry of shape (n_el, 3).
|
|
184
|
-
"""
|
|
185
|
-
# Read the probe geometry from the file
|
|
186
|
-
probe_geometry = file["Trans"]["ElementPos"][:3, :]
|
|
187
|
-
|
|
188
|
-
# Transpose the probe geometry to have the shape (n_el, 3)
|
|
189
|
-
probe_geometry = probe_geometry.T
|
|
190
|
-
|
|
191
|
-
# Read the unit
|
|
192
|
-
unit = decode_string(file["Trans"]["units"][:])
|
|
193
|
-
|
|
194
|
-
# Convert the probe geometry to meters
|
|
195
|
-
if unit == "mm":
|
|
196
|
-
probe_geometry = probe_geometry / 1000
|
|
197
|
-
else:
|
|
198
|
-
wavelength = read_wavelength(file)
|
|
199
|
-
probe_geometry = probe_geometry * wavelength
|
|
200
|
-
|
|
201
|
-
return probe_geometry
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def read_wavelength(file):
|
|
205
|
-
"""Reads the wavelength from the file.
|
|
206
|
-
|
|
207
|
-
Args:
|
|
208
|
-
`file` (`h5py.File`): The file to read the wavelength from. (The file should be
|
|
209
|
-
opened in read mode.)
|
|
210
|
-
|
|
211
|
-
Returns:
|
|
212
|
-
`wavelength` (`float`): The wavelength of the probe.
|
|
213
|
-
"""
|
|
214
|
-
center_frequency = read_probe_center_frequency(file)
|
|
215
|
-
sound_speed = read_sound_speed(file)
|
|
216
|
-
wavelength = sound_speed / center_frequency
|
|
217
|
-
return wavelength
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def read_transmit_events(file, event=None, frames="all"):
|
|
221
|
-
"""Read the events from the file and finds the order in which transmits and receives
|
|
222
|
-
appear in the events.
|
|
223
|
-
|
|
224
|
-
Args:
|
|
225
|
-
file (h5py.File): The file to read the events from.
|
|
226
|
-
The file should be opened in read mode.
|
|
227
|
-
event (int, optional): The event index. Defaults to None.
|
|
228
|
-
frames (str or list, optional): The frames to read. Defaults to "all".
|
|
229
|
-
|
|
230
|
-
Returns:
|
|
231
|
-
tuple: (tx_order, rcv_order, time_to_next_acq)
|
|
232
|
-
tx_order (list): The order in which the transmits appear in the events.
|
|
233
|
-
rcv_order (list): The order in which the receives appear in the events.
|
|
234
|
-
time_to_next_acq (np.ndarray): The time to next acquisition of shape (n_acq, n_tx).
|
|
235
|
-
"""
|
|
236
|
-
|
|
237
|
-
num_events = file["Event"]["info"].shape[0]
|
|
238
|
-
|
|
239
|
-
# In the Verasonics the transmits may not be in order in the TX structure and a
|
|
240
|
-
# transmit might be reused. Therefore, we need to keep track of the order in which
|
|
241
|
-
# the transmits appear in the Events.
|
|
242
|
-
tx_order = []
|
|
243
|
-
rcv_order = []
|
|
244
|
-
time_to_next_acq = []
|
|
245
|
-
|
|
246
|
-
frame_indices = get_frame_indices(file, frames)
|
|
247
|
-
|
|
248
|
-
for i in range(num_events):
|
|
249
|
-
# Get the tx
|
|
250
|
-
event_tx = dereference_index(file, file["Event"]["tx"], i)
|
|
251
|
-
event_tx = int(event_tx.item())
|
|
252
|
-
|
|
253
|
-
# Get the rcv
|
|
254
|
-
event_rcv = dereference_index(file, file["Event"]["rcv"], i)
|
|
255
|
-
event_rcv = int(event_rcv.item())
|
|
256
|
-
|
|
257
|
-
if not bool(event_tx) == bool(event_rcv):
|
|
258
|
-
log.warning(
|
|
259
|
-
"Events should have both a transmit and a receive or neither. "
|
|
260
|
-
f"Event {i} has a transmit but no receive or vice versa."
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
if not event_tx:
|
|
264
|
-
continue
|
|
265
|
-
|
|
266
|
-
# Subtract one to make the indices 0-based
|
|
267
|
-
event_tx -= 1
|
|
268
|
-
event_rcv -= 1
|
|
269
|
-
|
|
270
|
-
# Check in the Receive structure if this is still the first frame
|
|
271
|
-
framenum_ref = file["Receive"]["framenum"][event_rcv, 0]
|
|
272
|
-
framenum = file[framenum_ref][:].item()
|
|
273
|
-
|
|
274
|
-
# Only add the event to the list if it is the first frame since we assume
|
|
275
|
-
# that all frames have the same transmits and receives
|
|
276
|
-
if framenum == 1:
|
|
277
|
-
# Add the event to the list
|
|
278
|
-
tx_order.append(event_tx)
|
|
279
|
-
rcv_order.append(event_rcv)
|
|
280
|
-
|
|
281
|
-
# Read the time_to_next_acq
|
|
282
|
-
seq_control_indices = dereference_index(file, file["Event"]["seqControl"], i)
|
|
283
|
-
|
|
284
|
-
for seq_control_index in seq_control_indices:
|
|
285
|
-
seq_control_index = int(seq_control_index.item() - 1)
|
|
286
|
-
seq_control = dereference_index(file, file["SeqControl"]["command"], seq_control_index)
|
|
287
|
-
# Decode the seq_control int array into a string
|
|
288
|
-
seq_control = decode_string(seq_control)
|
|
289
|
-
if seq_control == "timeToNextAcq":
|
|
290
|
-
value = dereference_index(
|
|
291
|
-
file, file["SeqControl"]["argument"], seq_control_index
|
|
292
|
-
).item()
|
|
293
|
-
value = value * 1e-6
|
|
294
|
-
time_to_next_acq.append(value)
|
|
295
|
-
|
|
296
|
-
n_tx = len(tx_order)
|
|
297
|
-
time_to_next_acq = np.array(time_to_next_acq)
|
|
298
|
-
time_to_next_acq = np.reshape(time_to_next_acq, (-1, n_tx))
|
|
299
|
-
|
|
300
|
-
if event is not None:
|
|
301
|
-
time_to_next_acq = time_to_next_acq[event]
|
|
302
|
-
time_to_next_acq = np.expand_dims(time_to_next_acq, axis=0)
|
|
303
|
-
|
|
304
|
-
time_to_next_acq = time_to_next_acq[frame_indices]
|
|
305
|
-
|
|
306
|
-
return tx_order, rcv_order, time_to_next_acq
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def read_t0_delays_apod(file, tx_order, event=None):
|
|
310
|
-
"""
|
|
311
|
-
Read the t0 delays and apodization from the file.
|
|
312
|
-
|
|
313
|
-
Args:
|
|
314
|
-
file (h5py.File): The file to read the t0 delays from. (The file should be
|
|
315
|
-
opened in read mode.)
|
|
316
|
-
|
|
317
|
-
Returns:
|
|
318
|
-
t0_delays (np.ndarray): The t0 delays of shape (n_tx, n_el).
|
|
319
|
-
apod (np.ndarray): The apodization of shape (n_el,).
|
|
320
|
-
"""
|
|
321
|
-
|
|
322
|
-
t0_delays_list = []
|
|
323
|
-
tx_apodizations_list = []
|
|
324
|
-
|
|
325
|
-
wavelength = read_wavelength(file)
|
|
326
|
-
sound_speed = read_sound_speed(file)
|
|
327
|
-
|
|
328
|
-
for n in tx_order:
|
|
329
|
-
# Get column vector of t0_delays
|
|
330
|
-
if event is None:
|
|
331
|
-
t0_delays = dereference_index(file, file["TX"]["Delay"], n)
|
|
332
|
-
else:
|
|
333
|
-
t0_delays = dereference_index(file, file["TX_Agent"]["Delay"], n, event)
|
|
334
|
-
# Turn into 1d array
|
|
335
|
-
t0_delays = t0_delays[:, 0]
|
|
336
|
-
|
|
337
|
-
t0_delays_list.append(t0_delays)
|
|
338
|
-
|
|
339
|
-
# Get column vector of apodizations
|
|
340
|
-
if event is None:
|
|
341
|
-
tx_apodizations = dereference_index(file, file["TX"]["Apod"], n)
|
|
342
|
-
else:
|
|
343
|
-
tx_apodizations = dereference_index(file, file["TX_Agent"]["Apod"], n, event)
|
|
344
|
-
# Turn into 1d array
|
|
345
|
-
tx_apodizations = tx_apodizations[:, 0]
|
|
346
|
-
tx_apodizations_list.append(tx_apodizations)
|
|
347
|
-
|
|
348
|
-
t0_delays = np.stack(t0_delays_list, axis=0)
|
|
349
|
-
apodizations = np.stack(tx_apodizations_list, axis=0)
|
|
350
|
-
|
|
351
|
-
# Convert the t0_delays to meters
|
|
352
|
-
t0_delays = t0_delays * wavelength / sound_speed
|
|
353
|
-
|
|
354
|
-
return t0_delays, apodizations
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
def read_sampling_frequency(file):
|
|
358
|
-
"""
|
|
359
|
-
Read the sampling frequency from the file.
|
|
360
|
-
|
|
361
|
-
Args:
|
|
362
|
-
file (h5py.File): The file to read the sampling frequency from. (The file
|
|
363
|
-
should be opened in read mode.)
|
|
364
|
-
|
|
365
|
-
Returns:
|
|
366
|
-
sampling_frequency (float): The sampling frequency.
|
|
367
|
-
"""
|
|
368
|
-
# Read the sampling frequency from the file
|
|
369
|
-
adc_rate = dereference_index(file, file["Receive"]["decimSampleRate"], 0)
|
|
370
|
-
|
|
371
|
-
# The Vantage NXT has renamed this field to sampleSkip
|
|
372
|
-
if "quadDecim" in file["Receive"]:
|
|
373
|
-
quaddecim = dereference_index(file, file["Receive"]["quadDecim"], 0)
|
|
374
|
-
else:
|
|
375
|
-
quaddecim = 1.0
|
|
376
|
-
|
|
377
|
-
sampling_frequency = adc_rate / quaddecim * 1e6
|
|
378
|
-
|
|
379
|
-
return sampling_frequency[0, 0]
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def read_waveforms(file, tx_order, event=None):
|
|
383
|
-
"""
|
|
384
|
-
Read the waveforms from the file.
|
|
385
|
-
|
|
386
|
-
Args:
|
|
387
|
-
file (h5py.File): The file to read the waveforms from. (The file should be
|
|
388
|
-
opened in read mode.)
|
|
389
|
-
|
|
390
|
-
Returns:
|
|
391
|
-
waveforms (np.ndarray): The waveforms of shape (n_tx, n_samples).
|
|
392
|
-
"""
|
|
393
|
-
waveforms_one_way_list = []
|
|
394
|
-
waveforms_two_way_list = []
|
|
395
|
-
|
|
396
|
-
# Read all the waveforms from the file
|
|
397
|
-
n_waveforms = get_reference_size(file["TW"]["Wvfm1Wy"])
|
|
398
|
-
for n in range(n_waveforms):
|
|
399
|
-
# Get the row vector of the 1-way waveform
|
|
400
|
-
waveform_one_way = dereference_index(file, file["TW"]["Wvfm1Wy"], n)[:]
|
|
401
|
-
# Turn into 1d array
|
|
402
|
-
waveform_one_way = waveform_one_way[0, :]
|
|
403
|
-
|
|
404
|
-
# Get the row vector of the 2-way waveform
|
|
405
|
-
waveform_two_way = dereference_index(file, file["TW"]["Wvfm2Wy"], n)[:]
|
|
406
|
-
# Turn into 1d array
|
|
407
|
-
waveform_two_way = waveform_two_way[0, :]
|
|
408
|
-
|
|
409
|
-
waveforms_one_way_list.append(waveform_one_way)
|
|
410
|
-
waveforms_two_way_list.append(waveform_two_way)
|
|
411
|
-
|
|
412
|
-
tx_waveform_indices = []
|
|
413
|
-
|
|
414
|
-
for n in tx_order:
|
|
415
|
-
# Read the waveform
|
|
416
|
-
if event is None:
|
|
417
|
-
waveform_index = dereference_index(file, file["TX"]["waveform"], n)[:]
|
|
418
|
-
else:
|
|
419
|
-
waveform_index = dereference_index(file, file["TX_Agent"]["waveform"], n, event)[:]
|
|
420
|
-
# Subtract one to make the indices 0-based
|
|
421
|
-
waveform_index -= 1
|
|
422
|
-
# Turn into integer
|
|
423
|
-
waveform_index = int(waveform_index.item())
|
|
424
|
-
tx_waveform_indices.append(waveform_index)
|
|
425
|
-
|
|
426
|
-
return tx_waveform_indices, waveforms_one_way_list, waveforms_two_way_list
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
def read_polar_angles(file, tx_order, event=None):
|
|
430
|
-
"""
|
|
431
|
-
Read the polar angles from the file.
|
|
432
|
-
|
|
433
|
-
Args:
|
|
434
|
-
file (h5py.File): The file to read the polar angles from. (The file should
|
|
435
|
-
be opened in read mode.)
|
|
436
|
-
|
|
437
|
-
Returns:
|
|
438
|
-
polar_angles (np.ndarray): The polar angles of shape (n_tx,).
|
|
439
|
-
"""
|
|
440
|
-
polar_angles_list = []
|
|
441
|
-
|
|
442
|
-
for n in tx_order:
|
|
443
|
-
# Read the polar angle
|
|
444
|
-
if event is None:
|
|
445
|
-
polar_angle = dereference_index(file, file["TX"]["Steer"], n)[:]
|
|
446
|
-
else:
|
|
447
|
-
polar_angle = dereference_index(file, file["TX_Agent"]["Steer"], n, event)[:]
|
|
448
|
-
# Turn into 1d array
|
|
449
|
-
polar_angle = polar_angle[0, 0]
|
|
450
|
-
|
|
451
|
-
polar_angles_list.append(polar_angle)
|
|
452
|
-
|
|
453
|
-
polar_angles = np.stack(polar_angles_list, axis=0)
|
|
454
|
-
|
|
455
|
-
return polar_angles
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
def read_azimuth_angles(file, tx_order, event=None):
|
|
459
|
-
"""
|
|
460
|
-
Read the azimuth angles from the file.
|
|
461
|
-
|
|
462
|
-
Args:
|
|
463
|
-
file (h5py.File): The file to read the azimuth angles from. (The file should
|
|
464
|
-
be opened in read mode.)
|
|
465
|
-
|
|
466
|
-
Returns:
|
|
467
|
-
azimuth_angles (np.ndarray): The azimuth angles of shape (n_tx,).
|
|
468
|
-
"""
|
|
469
|
-
azimuth_angles_list = []
|
|
470
|
-
|
|
471
|
-
for n in tx_order:
|
|
472
|
-
# Read the azimuth angle
|
|
473
|
-
if event is None:
|
|
474
|
-
azimuth_angle = dereference_index(file, file["TX"]["Steer"], n)[:]
|
|
475
|
-
else:
|
|
476
|
-
azimuth_angle = dereference_index(file, file["TX_Agent"]["Steer"], n, event)[:]
|
|
477
|
-
# Turn into 1d array
|
|
478
|
-
azimuth_angle = azimuth_angle[1, 0]
|
|
479
|
-
|
|
480
|
-
azimuth_angles_list.append(azimuth_angle)
|
|
481
|
-
|
|
482
|
-
azimuth_angles = np.stack(azimuth_angles_list, axis=0)
|
|
483
|
-
|
|
484
|
-
return azimuth_angles
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
def read_raw_data(file, event=None, frames="all"):
|
|
488
|
-
"""
|
|
489
|
-
Read the raw data from the file.
|
|
490
|
-
|
|
491
|
-
Args:
|
|
492
|
-
file (h5py.File): The file to read the raw data from. (The file should be
|
|
493
|
-
opened in read mode.)
|
|
494
|
-
|
|
495
|
-
Returns:
|
|
496
|
-
raw_data (np.ndarray): The raw data of shape (n_rcv, n_samples).
|
|
497
|
-
"""
|
|
498
|
-
|
|
499
|
-
# Get the number of axial samples
|
|
500
|
-
start_sample = dereference_index(file, file["Receive"]["startSample"], 0).item()
|
|
501
|
-
end_sample = dereference_index(file, file["Receive"]["endSample"], 0).item()
|
|
502
|
-
n_ax = int(end_sample - start_sample + 1)
|
|
503
|
-
|
|
504
|
-
# Obtain the number of transmit events per frame
|
|
505
|
-
tx_order, _, _ = read_transmit_events(file)
|
|
506
|
-
n_tx = len(tx_order)
|
|
507
|
-
|
|
508
|
-
# Read the raw data from the file
|
|
509
|
-
if event is None:
|
|
510
|
-
raw_data = dereference_index(file, file["RcvData"], 0)
|
|
511
|
-
else:
|
|
512
|
-
# for now we only index frames as events
|
|
513
|
-
raw_data = dereference_index(file, file["RcvData"], 0, subindex=event)
|
|
514
|
-
raw_data = np.expand_dims(raw_data, axis=0)
|
|
515
|
-
|
|
516
|
-
frame_indices = get_frame_indices(file, frames)
|
|
517
|
-
|
|
518
|
-
raw_data = raw_data[frame_indices]
|
|
519
|
-
|
|
520
|
-
raw_data = raw_data[:, :, : n_ax * n_tx]
|
|
521
|
-
|
|
522
|
-
raw_data = raw_data.reshape((raw_data.shape[0], raw_data.shape[1], n_tx, -1))
|
|
523
|
-
|
|
524
|
-
raw_data = np.transpose(raw_data, (0, 2, 3, 1))
|
|
525
|
-
|
|
526
|
-
# Add channel dimension
|
|
527
|
-
raw_data = raw_data[..., None]
|
|
528
|
-
|
|
529
|
-
return raw_data
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
def read_probe_center_frequency(file):
|
|
533
|
-
"""Reads the center frequency of the probe from the file.
|
|
534
|
-
|
|
535
|
-
Args:
|
|
536
|
-
file (h5py.File): The file to read the center frequency from. (The file
|
|
537
|
-
should be opened in read mode.)
|
|
538
|
-
|
|
539
|
-
Returns:
|
|
540
|
-
center_frequency (float): The center frequency of the probe.
|
|
541
|
-
"""
|
|
542
|
-
center_frequency = file["Trans"]["frequency"][0, 0] * 1e6
|
|
543
|
-
return center_frequency
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
def read_sound_speed(file):
|
|
547
|
-
"""Reads the speed of sound from the file.
|
|
548
|
-
|
|
549
|
-
Args:
|
|
550
|
-
file (h5py.File): The file to read the speed of sound from. (The file
|
|
551
|
-
should be opened in read mode.)
|
|
552
|
-
|
|
553
|
-
Returns:
|
|
554
|
-
sound_speed (float): The speed of sound.
|
|
555
|
-
"""
|
|
556
|
-
|
|
557
|
-
sound_speed = file["Resource"]["Parameters"]["speedOfSound"][0, 0].item()
|
|
558
|
-
return sound_speed
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
def read_initial_times(file, rcv_order, sound_speed):
|
|
562
|
-
"""Reads the initial times from the file.
|
|
563
|
-
|
|
564
|
-
Args:
|
|
565
|
-
file (h5py.File): The file to read the initial times from. (The file should
|
|
566
|
-
be opened in read mode.)
|
|
567
|
-
rcv_order (list): The order in which the receives appear in the events.
|
|
568
|
-
wavelength (float): The wavelength of the probe.
|
|
569
|
-
sound_speed (float): The speed of sound.
|
|
570
|
-
|
|
571
|
-
Returns:
|
|
572
|
-
initial_times (np.ndarray): The initial times of shape (n_rcv,).
|
|
573
|
-
"""
|
|
574
|
-
wavelength = read_wavelength(file)
|
|
575
|
-
initial_times = []
|
|
576
|
-
for n in rcv_order:
|
|
577
|
-
start_depth = dereference_index(file, file["Receive"]["startDepth"], n).item()
|
|
578
|
-
|
|
579
|
-
initial_times.append(2 * start_depth * wavelength / sound_speed)
|
|
580
|
-
|
|
581
|
-
return np.array(initial_times).astype(np.float32)
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
def read_probe_name(file):
|
|
585
|
-
"""Reads the name of the probe from the file.
|
|
586
|
-
|
|
587
|
-
Args:
|
|
588
|
-
file (h5py.File): The file to read the name of the probe from. (The file
|
|
589
|
-
should be opened in read mode.)
|
|
590
|
-
|
|
591
|
-
Returns:
|
|
592
|
-
probe_name (str): The name of the probe.
|
|
593
|
-
"""
|
|
594
|
-
probe_name = file["Trans"]["name"][:]
|
|
595
|
-
probe_name = decode_string(probe_name)
|
|
596
|
-
# Translates between verasonics probe names and zea probe names
|
|
597
|
-
if probe_name in _VERASONICS_TO_ZEA_PROBE_NAMES:
|
|
598
|
-
probe_name = _VERASONICS_TO_ZEA_PROBE_NAMES[probe_name]
|
|
599
|
-
else:
|
|
600
|
-
log.warning(
|
|
601
|
-
f"Probe name {probe_name} is not in the list of known probes. "
|
|
602
|
-
"Please add it to the _VERASONICS_TO_ZEA_PROBE_NAMES dictionary. "
|
|
603
|
-
"Falling back to generic probe."
|
|
604
|
-
)
|
|
605
|
-
probe_name = "generic"
|
|
606
|
-
|
|
607
|
-
return probe_name
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
def read_focus_distances(file, tx_order, event=None):
|
|
611
|
-
"""Reads the focus distances from the file.
|
|
612
|
-
|
|
613
|
-
Args:
|
|
614
|
-
file (h5py.File): The file to read the focus distances from. (The file
|
|
615
|
-
should be opened in read mode.)
|
|
616
|
-
tx_order (list): The order in which the transmits appear in the events.
|
|
617
|
-
|
|
618
|
-
Returns:
|
|
619
|
-
focus_distances (list): The focus distances.
|
|
620
|
-
"""
|
|
621
|
-
focus_distances = []
|
|
622
|
-
for n in tx_order:
|
|
623
|
-
if event is None:
|
|
624
|
-
focus_distance = dereference_index(file, file["TX"]["focus"], n)[0, 0]
|
|
625
|
-
else:
|
|
626
|
-
focus_distance = dereference_index(file, file["TX_Agent"]["focus"], n, event)[0, 0]
|
|
627
|
-
focus_distances.append(focus_distance)
|
|
628
|
-
return np.array(focus_distances)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
def _probe_geometry_is_ordered_ula(probe_geometry):
|
|
632
|
-
"""Checks if the probe geometry is ordered as a uniform linear array (ULA)."""
|
|
633
|
-
diff_vec = probe_geometry[1:] - probe_geometry[:-1]
|
|
634
|
-
return np.isclose(diff_vec, diff_vec[0]).all()
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
def planewave_focal_distance_to_inf(focus_distances, t0_delays, tx_apodizations, probe_geometry):
|
|
638
|
-
"""Detects plane wave transmits and sets the focus distance to infinity.
|
|
639
|
-
|
|
640
|
-
Args:
|
|
641
|
-
focus_distances (np.ndarray): The focus distances of shape (n_tx,).
|
|
642
|
-
t0_delays (np.ndarray): The t0 delays of shape (n_tx, n_el).
|
|
643
|
-
tx_apodizations (np.ndarray): The apodization of shape (n_tx, n_el).
|
|
644
|
-
|
|
645
|
-
Returns:
|
|
646
|
-
focus_distances (np.ndarray): The focus distances of shape (n_tx,).
|
|
647
|
-
|
|
648
|
-
Note:
|
|
649
|
-
This function assumes that the probe_geometry is a 1d uniform linear array.
|
|
650
|
-
If not it will warn and return.
|
|
651
|
-
"""
|
|
652
|
-
if not _probe_geometry_is_ordered_ula(probe_geometry):
|
|
653
|
-
log.warning(
|
|
654
|
-
"The probe geometry is not ordered as a uniform linear array. "
|
|
655
|
-
"Focal distances are not set to infinity for plane waves."
|
|
656
|
-
)
|
|
657
|
-
return focus_distances
|
|
658
|
-
|
|
659
|
-
for tx in range(focus_distances.size):
|
|
660
|
-
mask_active = np.abs(tx_apodizations[tx]) > 0
|
|
661
|
-
if np.sum(mask_active) < 2:
|
|
662
|
-
continue
|
|
663
|
-
t0_delays_active = t0_delays[tx][mask_active]
|
|
664
|
-
|
|
665
|
-
# If the t0_delays all have the same offset, we assume it is a plane wave
|
|
666
|
-
if np.std(np.diff(t0_delays_active)) < 1e-16:
|
|
667
|
-
focus_distances[tx] = np.inf
|
|
668
|
-
|
|
669
|
-
return focus_distances
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
def read_bandwidth_percent(file):
|
|
673
|
-
"""Reads the bandwidth percent from the file.
|
|
674
|
-
|
|
675
|
-
Args:
|
|
676
|
-
file (h5py.File): The file to read the bandwidth percent from. (The file
|
|
677
|
-
should be opened in read mode.)
|
|
678
|
-
|
|
679
|
-
Returns:
|
|
680
|
-
bandwidth_percent (int): The bandwidth percent.
|
|
681
|
-
"""
|
|
682
|
-
bandwidth_percent = dereference_index(file, file["Receive"]["sampleMode"], 0)
|
|
683
|
-
bandwidth_percent = decode_string(bandwidth_percent)
|
|
684
|
-
bandwidth_percent = int(bandwidth_percent[2:-2])
|
|
685
|
-
return bandwidth_percent
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
def read_lens_correction(file):
|
|
689
|
-
"""Reads the lens correction from the file.
|
|
690
|
-
|
|
691
|
-
Args:
|
|
692
|
-
`file` (`h5py.File`): The file to read the lens correction from. (The file
|
|
693
|
-
should be opened in read mode.)
|
|
694
|
-
|
|
695
|
-
Returns:
|
|
696
|
-
`lens_correction` (`np.ndarray`): The lens correction.
|
|
697
|
-
"""
|
|
698
|
-
lens_correction = file["Trans"]["lensCorrection"][0, 0].item()
|
|
699
|
-
return lens_correction
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
def read_tgc_gain_curve(file):
|
|
703
|
-
"""Reads the TGC gain curve from the file.
|
|
704
|
-
|
|
705
|
-
Parameters
|
|
706
|
-
----------
|
|
707
|
-
file : h5py.File
|
|
708
|
-
The file to read the TGC gain curve from. (The file should be opened in read
|
|
709
|
-
mode.)
|
|
710
|
-
|
|
711
|
-
Returns
|
|
712
|
-
-------
|
|
713
|
-
np.ndarray
|
|
714
|
-
The TGC gain curve of shape `(n_ax,)`.
|
|
715
|
-
"""
|
|
716
|
-
|
|
717
|
-
gain_curve = file["TGC"]["Waveform"][:][:, 0]
|
|
718
|
-
|
|
719
|
-
# Normalize the gain_curve to [0, 40]dB
|
|
720
|
-
gain_curve = gain_curve / 1023 * 40
|
|
721
|
-
|
|
722
|
-
# The gain curve is sampled at 800ns (See Verasonics documentation for details.
|
|
723
|
-
# Specifically the tutorial sequence programming)
|
|
724
|
-
gain_curve_sampling_period = 800e-9
|
|
725
|
-
|
|
726
|
-
# Define the time axis for the gain curve
|
|
727
|
-
t_gain_curve = np.arange(gain_curve.size) * gain_curve_sampling_period
|
|
728
|
-
|
|
729
|
-
# Read the number of axial samples
|
|
730
|
-
start_sample = dereference_index(file, file["Receive"]["startSample"], 0).item()
|
|
731
|
-
end_sample = dereference_index(file, file["Receive"]["endSample"], 0).item()
|
|
732
|
-
n_ax = int(end_sample - start_sample + 1)
|
|
733
|
-
|
|
734
|
-
# Read the sampling frequency
|
|
735
|
-
sampling_frequency = read_sampling_frequency(file)
|
|
736
|
-
|
|
737
|
-
# Define the time axis for the axial samples
|
|
738
|
-
t_samples = np.arange(n_ax) / sampling_frequency
|
|
739
|
-
|
|
740
|
-
# Interpolate the gain_curve to the number of axial samples
|
|
741
|
-
gain_curve = np.interp(t_samples, t_gain_curve, gain_curve)
|
|
742
|
-
|
|
743
|
-
# The gain_curve gains are in dB, so we need to convert them to linear scale
|
|
744
|
-
gain_curve = 10 ** (gain_curve / 20)
|
|
745
|
-
|
|
746
|
-
return gain_curve
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
def read_image_data_p(file, event=None, frames="all"):
|
|
750
|
-
"""Reads the image data from the file.
|
|
751
|
-
|
|
752
|
-
Args:
|
|
753
|
-
`file` (`h5py.File`): The file to read the image data from. (The file should be
|
|
754
|
-
opened in read mode.)
|
|
755
|
-
|
|
756
|
-
Returns:
|
|
757
|
-
`image_data` (`np.ndarray`): The image data.
|
|
758
|
-
"""
|
|
759
|
-
# Check if the file contains image data
|
|
760
|
-
if "ImgDataP" not in file:
|
|
761
|
-
return None
|
|
762
|
-
|
|
763
|
-
frame_indices = get_frame_indices(file, frames)
|
|
764
|
-
|
|
765
|
-
# Get the dataset reference
|
|
766
|
-
image_data_ref = file["ImgDataP"][0, 0]
|
|
767
|
-
# Dereference the dataset
|
|
768
|
-
if event is None:
|
|
769
|
-
image_data = file[image_data_ref][:]
|
|
770
|
-
else:
|
|
771
|
-
image_data = file[image_data_ref][event]
|
|
772
|
-
image_data = np.expand_dims(image_data, axis=0)
|
|
773
|
-
|
|
774
|
-
# Get the relevant dimensions
|
|
775
|
-
image_data = image_data[:, 0, :, :]
|
|
776
|
-
|
|
777
|
-
# Convert to [-60, 0] dB range based on min and max values
|
|
778
|
-
normalize = Normalize(output_range=(0, 1), input_range=None)
|
|
779
|
-
log_compress = LogCompress()
|
|
780
|
-
|
|
781
|
-
image_data = normalize(data=image_data)["data"]
|
|
782
|
-
image_data = log_compress(data=image_data, dynamic_range=(-np.inf, 0))["data"]
|
|
783
|
-
|
|
784
|
-
# Reshape so that [n_frames, n_samples, n_lines]
|
|
785
|
-
image_data = np.transpose(image_data, (0, 2, 1))
|
|
786
|
-
|
|
787
|
-
image_data = image_data[frame_indices]
|
|
788
|
-
|
|
789
|
-
return image_data
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
def read_probe_element_width(file):
|
|
793
|
-
"""Reads the element width from the file.
|
|
794
|
-
|
|
795
|
-
Args:
|
|
796
|
-
file (h5py.File): The file to read the element width from.
|
|
797
|
-
The file should be opened in read mode.
|
|
798
|
-
|
|
799
|
-
Returns:
|
|
800
|
-
float: The element width.
|
|
801
|
-
"""
|
|
802
|
-
element_width = file["Trans"]["elementWidth"][:][0, 0]
|
|
803
|
-
|
|
804
|
-
# Read the unit
|
|
805
|
-
unit = decode_string(file["Trans"]["units"][:])
|
|
806
|
-
|
|
807
|
-
# Convert the probe element width to meters
|
|
808
|
-
if unit == "mm":
|
|
809
|
-
element_width = element_width / 1000
|
|
810
|
-
else:
|
|
811
|
-
wavelength = read_wavelength(file)
|
|
812
|
-
element_width = element_width * wavelength
|
|
813
|
-
|
|
814
|
-
return element_width
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
def read_verasonics_file(file, event=None, additional_functions=None, frames="all"):
|
|
818
|
-
"""Reads data from a .mat Verasonics output file.
|
|
819
|
-
|
|
820
|
-
Args:
|
|
821
|
-
file (h5py.File): The file to read the data from. (The file should be opened in
|
|
822
|
-
read mode.)
|
|
823
|
-
event (int, optional): The event index. Defaults to None in this case we assume
|
|
824
|
-
the data file is stored without event structure.
|
|
825
|
-
additional_functions (list, optional): A list of functions that read additional
|
|
826
|
-
data from the file. Each function should take the file as input and return a
|
|
827
|
-
`DatasetElement`. Defaults to None.
|
|
828
|
-
"""
|
|
829
|
-
|
|
830
|
-
probe_geometry = read_probe_geometry(file)
|
|
831
|
-
|
|
832
|
-
# same for all events
|
|
833
|
-
tx_order, rcv_order, time_to_next_transmit = read_transmit_events(file, frames=frames)
|
|
834
|
-
sampling_frequency = read_sampling_frequency(file)
|
|
835
|
-
bandwidth_percent = read_bandwidth_percent(file)
|
|
836
|
-
center_frequency = read_probe_center_frequency(file)
|
|
837
|
-
sound_speed = read_sound_speed(file)
|
|
838
|
-
initial_times = read_initial_times(file, rcv_order, sound_speed)
|
|
839
|
-
probe_name = read_probe_name(file)
|
|
840
|
-
tgc_gain_curve = read_tgc_gain_curve(file)
|
|
841
|
-
element_width = read_probe_element_width(file)
|
|
842
|
-
|
|
843
|
-
# these are capable of handling multiple events
|
|
844
|
-
raw_data = read_raw_data(file, event, frames=frames)
|
|
845
|
-
image = read_image_data_p(file, event, frames=frames)
|
|
846
|
-
|
|
847
|
-
polar_angles = read_polar_angles(file, tx_order, event)
|
|
848
|
-
azimuth_angles = read_azimuth_angles(file, tx_order, event)
|
|
849
|
-
t0_delays, tx_apodizations = read_t0_delays_apod(file, tx_order, event)
|
|
850
|
-
focus_distances = read_focus_distances(file, tx_order, event)
|
|
851
|
-
|
|
852
|
-
tx_waveform_indices, waveforms_one_way_list, waveforms_two_way_list = read_waveforms(
|
|
853
|
-
file, tx_order, event
|
|
854
|
-
)
|
|
855
|
-
focus_distances = planewave_focal_distance_to_inf(
|
|
856
|
-
focus_distances, t0_delays, tx_apodizations, probe_geometry
|
|
857
|
-
)
|
|
858
|
-
|
|
859
|
-
# If the data is captured in BS100BW mode or BS50BW mode, the data is stored in
|
|
860
|
-
# as complex IQ data and the sampling frequency is halved.
|
|
861
|
-
if bandwidth_percent in (50, 100):
|
|
862
|
-
raw_data = np.concatenate(
|
|
863
|
-
(
|
|
864
|
-
raw_data[:, :, 0::2, :, :],
|
|
865
|
-
-raw_data[:, :, 1::2, :, :],
|
|
866
|
-
),
|
|
867
|
-
axis=-1,
|
|
868
|
-
)
|
|
869
|
-
# Two sequential samples are interpreted as a single complex sample
|
|
870
|
-
# Therefore, we need to halve the sampling frequency
|
|
871
|
-
sampling_frequency = sampling_frequency / 2
|
|
872
|
-
|
|
873
|
-
# We have halved the number of samples, so we need to halve the number
|
|
874
|
-
# of samples in the gain curve as well
|
|
875
|
-
tgc_gain_curve = tgc_gain_curve[0::2]
|
|
876
|
-
|
|
877
|
-
lens_correction = read_lens_correction(file)
|
|
878
|
-
if event is None:
|
|
879
|
-
group_name = "scan"
|
|
880
|
-
else:
|
|
881
|
-
group_name = f"event_{event}/scan"
|
|
882
|
-
|
|
883
|
-
el_lens_correction = DatasetElement(
|
|
884
|
-
group_name=group_name,
|
|
885
|
-
dataset_name="lens_correction",
|
|
886
|
-
data=lens_correction,
|
|
887
|
-
description=(
|
|
888
|
-
"The lens correction value used by Verasonics. This value is the "
|
|
889
|
-
"additional path length in wavelength that the lens introduces. "
|
|
890
|
-
"(This disregards refraction.)"
|
|
891
|
-
),
|
|
892
|
-
unit="wavelengths",
|
|
893
|
-
)
|
|
894
|
-
|
|
895
|
-
additional_elements = []
|
|
896
|
-
if additional_functions is not None:
|
|
897
|
-
for additional_function in additional_functions:
|
|
898
|
-
additional_elements.append(additional_function(file))
|
|
899
|
-
|
|
900
|
-
data = {
|
|
901
|
-
"probe_geometry": probe_geometry,
|
|
902
|
-
"time_to_next_transmit": time_to_next_transmit,
|
|
903
|
-
"t0_delays": t0_delays,
|
|
904
|
-
"tx_apodizations": tx_apodizations,
|
|
905
|
-
"sampling_frequency": sampling_frequency,
|
|
906
|
-
"polar_angles": polar_angles,
|
|
907
|
-
"azimuth_angles": azimuth_angles,
|
|
908
|
-
"bandwidth_percent": bandwidth_percent,
|
|
909
|
-
"raw_data": raw_data,
|
|
910
|
-
"image": image,
|
|
911
|
-
"center_frequency": center_frequency,
|
|
912
|
-
"sound_speed": sound_speed,
|
|
913
|
-
"initial_times": initial_times,
|
|
914
|
-
"probe_name": probe_name,
|
|
915
|
-
"focus_distances": focus_distances,
|
|
916
|
-
"tx_waveform_indices": tx_waveform_indices,
|
|
917
|
-
"waveforms_one_way": waveforms_one_way_list,
|
|
918
|
-
"waveforms_two_way": waveforms_two_way_list,
|
|
919
|
-
"tgc_gain_curve": tgc_gain_curve,
|
|
920
|
-
"element_width": element_width,
|
|
921
|
-
"additional_elements": [el_lens_correction, *additional_elements],
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
return data
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
def get_frame_indices(file, frames):
|
|
928
|
-
"""Creates a numpy array of frame indices from the file and the frames argument.
|
|
929
|
-
|
|
930
|
-
Args:
|
|
931
|
-
file (h5py.File): The file to read the frame indices from.
|
|
932
|
-
frames (str): The frames argument. This can be "all" or a list of frame indices.
|
|
933
|
-
|
|
934
|
-
Returns:
|
|
935
|
-
frame_indices (np.ndarray): The frame indices.
|
|
936
|
-
"""
|
|
937
|
-
# Read the number of frames from the file
|
|
938
|
-
n_frames = int(file["Resource"]["RcvBuffer"]["numFrames"][0][0])
|
|
939
|
-
|
|
940
|
-
if isinstance(frames, str) and frames == "all":
|
|
941
|
-
# Create an array of all frame-indices
|
|
942
|
-
frame_indices = np.arange(n_frames)
|
|
943
|
-
else:
|
|
944
|
-
frame_indices = np.array(frames)
|
|
945
|
-
frame_indices.sort()
|
|
946
|
-
|
|
947
|
-
if np.any(frame_indices >= n_frames):
|
|
948
|
-
log.error(
|
|
949
|
-
f"Frame indices {frame_indices} are out of bounds. "
|
|
950
|
-
f"The file contains {n_frames} frames. "
|
|
951
|
-
f"Using only the indices that are within bounds."
|
|
952
|
-
)
|
|
953
|
-
# Remove out of bounds indices
|
|
954
|
-
frame_indices = frame_indices[frame_indices < n_frames]
|
|
955
|
-
|
|
956
|
-
return frame_indices
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
def zea_from_matlab_raw(input_path, output_path, additional_functions=None, frames="all"):
|
|
960
|
-
"""Converts a Verasonics matlab raw file to the zea format. The MATLAB file
|
|
961
|
-
should be created using the `save_raw` function and be stored in "v7.3" format.
|
|
962
|
-
|
|
963
|
-
Args:
|
|
964
|
-
input_path (str): The path to the input file (.mat file).
|
|
965
|
-
output_path (str): The path to the output file (.hdf5 file).
|
|
966
|
-
additional_functions (list, optional): A list of functions that read additional
|
|
967
|
-
data from the file. Each function should take the file as input and return a
|
|
968
|
-
`DatasetElement`. Defaults to None.
|
|
969
|
-
frames (str or list of int, optional): The frames to add to the file. This can be
|
|
970
|
-
a list of integers, a range of integers (e.g. 4-8), or 'all'. Defaults to
|
|
971
|
-
'all'.
|
|
972
|
-
"""
|
|
973
|
-
# Create the output directory if it does not exist
|
|
974
|
-
input_path = Path(input_path)
|
|
975
|
-
output_path = Path(output_path)
|
|
976
|
-
|
|
977
|
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
978
|
-
|
|
979
|
-
assert input_path.is_file(), log.error(f"Input file {log.yellow(input_path)} does not exist.")
|
|
980
|
-
|
|
981
|
-
# Load the data
|
|
982
|
-
with h5py.File(input_path, "r") as file:
|
|
983
|
-
if "TX_Agent" in file:
|
|
984
|
-
active_keys = file["TX_Agent"].keys()
|
|
985
|
-
log.info(
|
|
986
|
-
f"Found active imaging data with {len(active_keys)} events. "
|
|
987
|
-
"Will convert and save all parameters for each event separately."
|
|
988
|
-
)
|
|
989
|
-
num_events = set(file["TX_Agent"][key].shape[-1] for key in active_keys)
|
|
990
|
-
assert len(num_events) == 1, (
|
|
991
|
-
"All TX_Agent entries should have the same number of events."
|
|
992
|
-
)
|
|
993
|
-
num_events = num_events.pop()
|
|
994
|
-
|
|
995
|
-
# loop over TX_Agent entries and overwrite TX each time
|
|
996
|
-
data = {}
|
|
997
|
-
for event in range(num_events):
|
|
998
|
-
data[event] = read_verasonics_file(
|
|
999
|
-
file,
|
|
1000
|
-
event=event,
|
|
1001
|
-
additional_functions=additional_functions,
|
|
1002
|
-
)
|
|
1003
|
-
|
|
1004
|
-
# convert dict of events to dict of lists
|
|
1005
|
-
data = {key: [data[event][key] for event in data] for key in data[0]}
|
|
1006
|
-
description = ["Verasonics data with multiple events"] * num_events
|
|
1007
|
-
# Generate the zea dataset
|
|
1008
|
-
generate_zea_dataset(
|
|
1009
|
-
path=output_path,
|
|
1010
|
-
**data,
|
|
1011
|
-
event_structure=True,
|
|
1012
|
-
description=description,
|
|
1013
|
-
)
|
|
1014
|
-
|
|
1015
|
-
else:
|
|
1016
|
-
# Here we call al the functions to read the data from the file
|
|
1017
|
-
data = read_verasonics_file(
|
|
1018
|
-
file, additional_functions=additional_functions, frames=frames
|
|
1019
|
-
)
|
|
1020
|
-
|
|
1021
|
-
# Generate the zea dataset
|
|
1022
|
-
generate_zea_dataset(path=output_path, **data, description="Verasonics data")
|
|
1023
|
-
|
|
1024
|
-
log.success(f"Converted {log.yellow(input_path)} to {log.yellow(output_path)}")
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
def parse_args():
|
|
1028
|
-
"""Parse command line arguments."""
|
|
1029
|
-
parser = argparse.ArgumentParser(
|
|
1030
|
-
description="Convert Verasonics matlab raw files to the zea format."
|
|
1031
|
-
"Example usage: python zea/data/convert/matlab.py raw_file.mat output.hdf5 --frames 1-5 7"
|
|
1032
|
-
)
|
|
1033
|
-
parser.add_argument(
|
|
1034
|
-
"input_path",
|
|
1035
|
-
default=None,
|
|
1036
|
-
type=str,
|
|
1037
|
-
nargs="?",
|
|
1038
|
-
help="The path to a file or directory containing raw Verasonics data.",
|
|
1039
|
-
)
|
|
1040
|
-
|
|
1041
|
-
parser.add_argument(
|
|
1042
|
-
"output_path",
|
|
1043
|
-
default=None,
|
|
1044
|
-
type=str,
|
|
1045
|
-
nargs="?",
|
|
1046
|
-
help="The path to the output file or directory.",
|
|
1047
|
-
)
|
|
1048
|
-
|
|
1049
|
-
parser.add_argument(
|
|
1050
|
-
"--frames",
|
|
1051
|
-
default=["all"],
|
|
1052
|
-
type=str,
|
|
1053
|
-
nargs="+",
|
|
1054
|
-
help="The frames to add to the file. This can be a list of integers, a range "
|
|
1055
|
-
"of integers (e.g. 4-8), or 'all'.",
|
|
1056
|
-
)
|
|
1057
|
-
|
|
1058
|
-
return parser.parse_args()
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
def get_answer(prompt, additional_options=None):
|
|
1062
|
-
"""Get a yes or no answer from the user. There is also the option to provide
|
|
1063
|
-
additional options. In case yes or no is selected, the function returns a boolean.
|
|
1064
|
-
In case an additional option is selected, the function returns the selected option
|
|
1065
|
-
as a string.
|
|
1066
|
-
|
|
1067
|
-
Args:
|
|
1068
|
-
prompt (str): The prompt to show the user.
|
|
1069
|
-
additional_options (list, optional): Additional options to show the user.
|
|
1070
|
-
Defaults to None.
|
|
1071
|
-
|
|
1072
|
-
Returns:
|
|
1073
|
-
str: The user's answer.
|
|
1074
|
-
"""
|
|
1075
|
-
while True:
|
|
1076
|
-
answer = input(prompt)
|
|
1077
|
-
try:
|
|
1078
|
-
bool_answer = strtobool(answer)
|
|
1079
|
-
return bool_answer
|
|
1080
|
-
except ValueError:
|
|
1081
|
-
if additional_options is not None and answer in additional_options:
|
|
1082
|
-
return answer
|
|
1083
|
-
log.warning("Invalid input.")
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
if __name__ == "__main__":
|
|
1087
|
-
args = parse_args()
|
|
1088
|
-
|
|
1089
|
-
# Variable to indicate what to do with existing files.
|
|
1090
|
-
# Is set by the user in case these are found.
|
|
1091
|
-
existing_file_policy = None
|
|
1092
|
-
|
|
1093
|
-
if args.input_path is None:
|
|
1094
|
-
log.info("Select a directory containing Verasonics matlab raw files.")
|
|
1095
|
-
# Create a Tkinter root window
|
|
1096
|
-
try:
|
|
1097
|
-
import tkinter as tk
|
|
1098
|
-
from tkinter import filedialog
|
|
1099
|
-
|
|
1100
|
-
root = tk.Tk()
|
|
1101
|
-
root.withdraw()
|
|
1102
|
-
# Prompt the user to select a file or directory
|
|
1103
|
-
selected_path = filedialog.askdirectory()
|
|
1104
|
-
except ImportError as e:
|
|
1105
|
-
raise ImportError(
|
|
1106
|
-
log.error(
|
|
1107
|
-
"tkinter is not installed. Please install it with 'apt install python3-tk'."
|
|
1108
|
-
)
|
|
1109
|
-
) from e
|
|
1110
|
-
except Exception as e:
|
|
1111
|
-
raise ValueError(
|
|
1112
|
-
log.error(
|
|
1113
|
-
"Failed to open a file dialog (possibly in headless state). "
|
|
1114
|
-
"Please provide a path as an argument. "
|
|
1115
|
-
)
|
|
1116
|
-
) from e
|
|
1117
|
-
else:
|
|
1118
|
-
selected_path = args.input_path
|
|
1119
|
-
|
|
1120
|
-
# Exit when no path is selected
|
|
1121
|
-
if not selected_path:
|
|
1122
|
-
log.error("No path selected.")
|
|
1123
|
-
sys.exit()
|
|
1124
|
-
else:
|
|
1125
|
-
selected_path = Path(selected_path)
|
|
1126
|
-
|
|
1127
|
-
selected_path_is_directory = os.path.isdir(selected_path)
|
|
1128
|
-
|
|
1129
|
-
# Set the output path to be next to the input directory with _zea appended
|
|
1130
|
-
# to the name
|
|
1131
|
-
if args.output_path is None:
|
|
1132
|
-
if selected_path_is_directory:
|
|
1133
|
-
output_path = selected_path.parent / (Path(selected_path).name + "_zea")
|
|
1134
|
-
else:
|
|
1135
|
-
output_path = str(selected_path.with_suffix("")) + "_zea.hdf5"
|
|
1136
|
-
output_path = Path(output_path)
|
|
1137
|
-
else:
|
|
1138
|
-
output_path = Path(args.output_path)
|
|
1139
|
-
if selected_path.is_file() and output_path.suffix not in (".hdf5", ".h5"):
|
|
1140
|
-
log.error(
|
|
1141
|
-
"When converting a single file, the output path should have the .hdf5 "
|
|
1142
|
-
"or .h5 extension."
|
|
1143
|
-
)
|
|
1144
|
-
sys.exit()
|
|
1145
|
-
elif selected_path.is_dir() and output_path.is_file():
|
|
1146
|
-
log.error("When converting a directory, the output path should be a directory.")
|
|
1147
|
-
sys.exit()
|
|
1148
|
-
#
|
|
1149
|
-
if output_path.is_dir() and not selected_path_is_directory:
|
|
1150
|
-
output_path = output_path / (selected_path.name + "_zea.hdf5")
|
|
1151
|
-
|
|
1152
|
-
log.info(f"Selected path: {log.yellow(selected_path)}")
|
|
1153
|
-
|
|
1154
|
-
# Parse frames
|
|
1155
|
-
frames = args.frames
|
|
1156
|
-
if frames[0] == "all":
|
|
1157
|
-
frames = "all"
|
|
1158
|
-
else:
|
|
1159
|
-
indices = set()
|
|
1160
|
-
for frame in frames:
|
|
1161
|
-
if "-" in frame:
|
|
1162
|
-
start, end = frame.split("-")
|
|
1163
|
-
indices.update(range(int(start), int(end) + 1))
|
|
1164
|
-
else:
|
|
1165
|
-
indices.add(int(frame))
|
|
1166
|
-
frames = list(indices)
|
|
1167
|
-
frames.sort()
|
|
1168
|
-
# Do the conversion of a single file
|
|
1169
|
-
if not selected_path_is_directory:
|
|
1170
|
-
if output_path.is_file():
|
|
1171
|
-
answer = get_answer(
|
|
1172
|
-
f"File {log.yellow(output_path)} exists. Overwrite?"
|
|
1173
|
-
"\n\ty\t - Overwrite"
|
|
1174
|
-
"\n\tn\t - Skip"
|
|
1175
|
-
"\nAnswer: "
|
|
1176
|
-
)
|
|
1177
|
-
if answer is True:
|
|
1178
|
-
log.warning(f"{selected_path} exists. Deleting...")
|
|
1179
|
-
output_path.unlink(missing_ok=False)
|
|
1180
|
-
else:
|
|
1181
|
-
log.info("Aborting...")
|
|
1182
|
-
sys.exit()
|
|
1183
|
-
zea_from_matlab_raw(selected_path, output_path, frames=frames)
|
|
1184
|
-
else:
|
|
1185
|
-
# Continue with the rest of your code...
|
|
1186
|
-
for root, dirs, files in os.walk(selected_path):
|
|
1187
|
-
for mat_file in files:
|
|
1188
|
-
# Skip non-mat files
|
|
1189
|
-
if not mat_file.endswith(".mat"):
|
|
1190
|
-
continue
|
|
1191
|
-
|
|
1192
|
-
log.info(f"Found raw data file {log.yellow(mat_file)}")
|
|
1193
|
-
|
|
1194
|
-
# Convert the file to a Path object
|
|
1195
|
-
mat_file = Path(mat_file)
|
|
1196
|
-
|
|
1197
|
-
# Construct the output path
|
|
1198
|
-
relative_path = (Path(root) / Path(mat_file)).relative_to(selected_path)
|
|
1199
|
-
file_output_path = output_path / (relative_path.with_suffix(".hdf5"))
|
|
1200
|
-
|
|
1201
|
-
full_path = selected_path / relative_path
|
|
1202
|
-
|
|
1203
|
-
# Handle existing files
|
|
1204
|
-
if file_output_path.is_file():
|
|
1205
|
-
if existing_file_policy is None:
|
|
1206
|
-
answer = get_answer(
|
|
1207
|
-
f"File {log.yellow(file_output_path)} exists. Overwrite?"
|
|
1208
|
-
"\n\ty\t - Overwrite"
|
|
1209
|
-
"\n\tn\t - Skip"
|
|
1210
|
-
"\n\tya\t - Overwrite all existing files"
|
|
1211
|
-
"\n\tna\t - Skip all existing files"
|
|
1212
|
-
"\nAnswer: ",
|
|
1213
|
-
additional_options=("ya", "na"),
|
|
1214
|
-
)
|
|
1215
|
-
if answer == "ya":
|
|
1216
|
-
existing_file_policy = "overwrite"
|
|
1217
|
-
elif answer == "na":
|
|
1218
|
-
existing_file_policy = "skip"
|
|
1219
|
-
continue
|
|
1220
|
-
|
|
1221
|
-
if existing_file_policy == "skip" or answer is False:
|
|
1222
|
-
log.info("Skipping...")
|
|
1223
|
-
continue
|
|
1224
|
-
|
|
1225
|
-
if existing_file_policy == "overwrite" or answer is True:
|
|
1226
|
-
log.warning(f"{log.yellow(full_path)} exists. Deleting...")
|
|
1227
|
-
file_output_path.unlink(missing_ok=False)
|
|
1228
|
-
|
|
1229
|
-
try:
|
|
1230
|
-
zea_from_matlab_raw(full_path, file_output_path, frames=frames)
|
|
1231
|
-
except Exception:
|
|
1232
|
-
# Print error message without raising it
|
|
1233
|
-
log.error(f"Failed to convert {mat_file}")
|
|
1234
|
-
# Print stacktrace
|
|
1235
|
-
traceback.print_exc()
|
|
1236
|
-
|
|
1237
|
-
continue
|