Simulate and use your own data
Here, we are going to simulate some data using the SIMUS simulator. As for this tutorial, let’s simulate a convex probe emitting ultrasound in a very simple medium with a single scatterer. The SIMUS simulator has its own detailed documentation, so we won’t go too much in detail here.
First, let’s build a medium, load a probe and define the emission sequence (five plane waves) :
1% Single scatterer medium, centered
2x = [0]; y = [0]; z = [25e-3];
3amp = ones(1, 1);
4
5% Simple convex probe (the C5-2v)
6param = getparam('C5-2v');
7
8% Compute the delays of the plane wave
9param.fs = 4*param.fc;
10angles = [-10, -5, 0, 5, 10];
11nb_a = size(angles, 2);
12delays = txdelay(param, deg2rad(angles));
To build the simulation, we need to create a container and to launch the simulation for each sequence :
1RF = cell(nb_a, 1);
2Nts = zeros(1, nb_a);
3for k = 1:nb_a
4 RF{k} = simus(x, y, z, amp, delays(k, :), param);
5 Nts(k) = size(RF{k}, 1);
6end
Since all the sequences will have a different number of time sample and different t0, we want to re-organize this in order to have a uniform number of time samples :
1nt = max(Nts);
2for angle = 1:nb_a
3 [nnt, nne] = size(RF{angle});
4 RF{angle} = cat(1, RF{angle}, zeros(nt - nnt, nne));
5end
6RF = cat(3, RF{:});
There we go! Now we have the data, of shape (nb_time_samples, nb_probe_element, nb_sequences), which is all we need, you can save them in a .mat file.
Let’s now see how ultraspy reads the data files. So far, three data file
extensions are implemented : .mat, .hdf5, and .rff256 (UlaOp format). You can
add your own in the _file_loaders/ directory as a python class, child of the
FileLoader class. Those should read the data file and store all the data as a
dictionary within self.data. The three examples should be enough if you
want to add your own extension so there is no need to detail much the process
here, but just don’t forget to add the new extension within the
_file_loaders/factory.py file.
Now that we can read any data format and store everything within a python
dictionary, we should define how to interpret them. From now on, the magic
happens in the reader.py file. The first thing is to define a identifier for
your data format, let’s call ours :code:’tuto_simu’. You should add it to the
list of the available systems, within the __extract_data
method, and then you can create your own __extract_tuto_simu_data.
Within __extract_tuto_simu_data(loaded_dict) we will have access to all
the data within loaded_dict. The first thing is to give the information
about the probe that we’ve used. There’s a few ways to build the probe.
1from ultraspy.probes.factory import build_probe
2
3# If we have the name of the probe and if the probe has been defined within
4# the probe class
5probe = get_probe('C5-2V')
6
7# If we don't, but have all the needed information
8probe = build_probe(geometry_type='convex',
9 nb_elements=128,
10 pitch=0.508e-3,
11 central_freq=3.57e6,
12 bandwidth=79,
13 radius=49.57e-3)
A good practice is to include the name of the probe within the data file, and then to add all the information as a Probe class so you are sure that you are always using the one from the config you’ve defined. You can also verify the geometry using :
1probe = probe.show()
Nice, now we need to load the data, which is expected to be of the shape (nb_frames, nb_transmissions, nb_probe_elements, nb_time_samples). So we need to reorganize the data to make it match. You also need to store the information about this data within a dedicated dictionary.
1# Change the shape of the data
2data = loaded_dict['RF'].transpose(2, 1, 0)[None, :]
3data_info = {
4 'data_shape': data.shape,
5 'data_type': data.dtype,
6 'is_iq': False,
7}
Next, something important is to provide the delays of the signals. Here, we’ve generated 5 sequences of 128 elements. Each sequence have distinct delays per element in order to emit tilted plane waves. Those were made to match five tilting of [-10, -5, 0, 5, 10] degrees. Let’s generate them :
1from ultraspy.transmit_delays_helpers import compute_pw_delays
2
3# Get the tilting angles
4angles = [-10, -5, 0, 5, 10]
5
6# Compute the delays
7delays = compute_pw_delays(np.radians(angles), probe,
8 speed_of_sound=1540,
9 transmission_mode='positives')
10
11# Visualize the results
12probe.show_delays(delays)
Now we have our delays, of shape (nb_cycles, nb_elements). We’ve generated them by ourselves, but the good way would have been to store them in the data file, since each echograph has its own way to deal with delays.
Finally, we also need to define which sequence has been emitted, meaning that, for each cycle, which elements have been triggered, both for emission and reception. Here, in a plane wave scenario, they’ve all been used for both emission and reception, so we can define :
1import numpy as np
2
3nb_transmissions = delays.shape[0]
4elements_indices = np.arange(probe.nb_elements)
5sequence = {
6 'emitted': np.tile(elements_indices, (nb_transmissions, 1)),
7 'received': np.tile(elements_indices, (nb_transmissions, 1)),
8}
Nice we are all set! The last thing to do is to store all these data within the four class attributes :
1self.data = data
2self.data_info = data_info
3self.acquisition_info = {
4 'sampling_freq': 4 * probe.central_freq,
5 't0': 0,
6 'prf': None,
7 'signal_duration': None,
8 'delays': delays,
9 'sound_speed': 1540,
10 'sequence_elements': sequence,
11}
12self.probe = probe
All good! Now we have all we need, the reader can now be safely used using :
1reader = Reader(my_data_file, 'tuto_simu')
The same procedure can be followed for data from Field II ; an example on how to generate 3D simulation is by the way available within resources/external_codes and the dedicated reader also exists (simu_3d). A version exists already for the Verasonics and the UlaOp data, but those are not maintained, and might only work on very specific situations.