├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── AbstractIUS ├── Abstract_IUS_pymust.pdf ├── abstractFigure.jpg ├── abstractFigure.png ├── abstractFigure.pptx ├── abstractFigure.tiff ├── carotid.jpg ├── pymust.docx └── script4Gab.m ├── LICENSE ├── Matlab examples ├── rotatePoints.m ├── rotatingDisk.m ├── s.m └── testPfield.m ├── README.md ├── examples ├── DW_echo_demo.ipynb ├── data │ ├── PWI_disk.mat │ ├── RFsignal@20MHz.mat │ ├── bmode_PWI_disk.mat │ └── heart.jpg ├── multiline_transmit_demo.ipynb ├── pfield.ipynb ├── pfield3.ipynb ├── quickstart_demo.ipynb ├── rotatingDiskVelocitySynthetic.ipynb ├── rotatingDiskVelocitySyntheticML.ipynb ├── rotatingDisk_real.ipynb ├── testMkMovie.ipynb └── testSmoothn.ipynb ├── pyproject.toml ├── setup.py ├── src ├── pymust.egg-info │ └── dependency_links.txt └── pymust │ ├── Data │ └── colorMap.pkl │ ├── __init__.py │ ├── bmode.py │ ├── dasmtx.py │ ├── dasmtx3.py │ ├── genscat.py │ ├── getparam.py │ ├── getpulse.py │ ├── impolgrid.py │ ├── iq2doppler.py │ ├── mkmovie.py │ ├── pfield.py │ ├── pfield3.py │ ├── rf2iq.py │ ├── simus.py │ ├── simus3.py │ ├── smoothn.py │ ├── sptrack.py │ ├── sptrack_old.py │ ├── tgc.py │ ├── txdelay.py │ ├── txdelay3.py │ ├── utils.py │ └── wfilt.py └── tutorials ├── Figures ├── s1_ex1.png └── s1_ex3.png ├── P1_P2.zip ├── P1_radiofrequency.ipynb ├── P2_several_elements.ipynb ├── P2_several_elements_new.ipynb ├── P3_bmode.ipynb ├── P4_different_probes.ipynb ├── P5_Doppler.ipynb ├── P6_spectralDoppler.ipynb ├── basic_example.ipynb └── export.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package to PyPI when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | workflow_dispatch: 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | release-build: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - uses: actions/setup-python@v5 27 | with: 28 | python-version: "3.x" 29 | 30 | - name: Build release distributions 31 | run: | 32 | # NOTE: put your own distribution build steps here. 33 | python -m pip install build 34 | python -m build 35 | 36 | - name: Upload distributions 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: release-dists 40 | path: dist/ 41 | 42 | pypi-publish: 43 | runs-on: ubuntu-latest 44 | needs: 45 | - release-build 46 | permissions: 47 | # IMPORTANT: this permission is mandatory for trusted publishing 48 | id-token: write 49 | 50 | # Dedicated environments with protections for publishing are strongly recommended. 51 | # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules 52 | environment: 53 | name: pypi 54 | # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: 55 | #url: https://pypi.org/project/PyMUST/ 56 | # 57 | # ALTERNATIVE: if your GitHub Release name is the PyPI project version string 58 | # ALTERNATIVE: exactly, uncomment the following line instead: 59 | url: https://pypi.org/project/PyMUST/${{ github.event.release.name }} 60 | 61 | steps: 62 | - name: Retrieve release distributions 63 | uses: actions/download-artifact@v4 64 | with: 65 | name: release-dists 66 | path: dist/ 67 | 68 | - name: Publish release distributions to PyPI 69 | uses: pypa/gh-action-pypi-publish@release/v1 70 | with: 71 | packages-dir: dist/ 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | src/pymust.egg-info/* 3 | .DS_Store 4 | */.DS_Store 5 | *.lprof 6 | *.prof 7 | *.gif 8 | /tutorials/ExportedNB 9 | /tutorials/slides 10 | -------------------------------------------------------------------------------- /AbstractIUS/Abstract_IUS_pymust.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/AbstractIUS/Abstract_IUS_pymust.pdf -------------------------------------------------------------------------------- /AbstractIUS/abstractFigure.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/AbstractIUS/abstractFigure.jpg -------------------------------------------------------------------------------- /AbstractIUS/abstractFigure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/AbstractIUS/abstractFigure.png -------------------------------------------------------------------------------- /AbstractIUS/abstractFigure.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/AbstractIUS/abstractFigure.pptx -------------------------------------------------------------------------------- /AbstractIUS/abstractFigure.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/AbstractIUS/abstractFigure.tiff -------------------------------------------------------------------------------- /AbstractIUS/carotid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/AbstractIUS/carotid.jpg -------------------------------------------------------------------------------- /AbstractIUS/pymust.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/AbstractIUS/pymust.docx -------------------------------------------------------------------------------- /AbstractIUS/script4Gab.m: -------------------------------------------------------------------------------- 1 | 2 | I = rgb2gray(imread('carotid.jpg')); 3 | I = smoothn(double(I),100); 4 | I = I-min(I(:)); 5 | %% 6 | param = getparam('L11-5v'); 7 | param.attenuation = 0.5; 8 | L = (param.Nelements-1)*param.pitch; 9 | 10 | DistanceFactor = 1; 11 | [xs,~,zs,RC] = genscat([5e-2 NaN],1540/param.fc*DistanceFactor,I,.4); 12 | 13 | DR = 50; 14 | dels = zeros(1,param.Nelements); 15 | %[xs,zs,RC] = rmscat(xs,zs,RC,dels,param,DR); 16 | [RF,param] = simus(xs,zs,RC,dels,param); 17 | 18 | IQ = rf2iq(RF,param.fs,param.fc); 19 | [xi,zi] = meshgrid(linspace(-L/2,L/2,256),linspace(eps,3e-2,round(256/L*3e-2))); 20 | 21 | param.fnumber = []; 22 | IQb = das(IQ,xi,zi,zeros(1,size(IQ,2)),param); 23 | B = bmode(IQb); 24 | imshow(B) -------------------------------------------------------------------------------- /Matlab examples/rotatePoints.m: -------------------------------------------------------------------------------- 1 | function [xs_rot, ys_rot] = rotatePoints(x, y, x0, y0, theta) 2 | % Rotate the points an angle of theta along the central point (x0, y0) 3 | x = x - x0; 4 | y = y - y0; 5 | xs_rot = x .* cos(theta) - y .* sin(theta) + x0; 6 | ys_rot = x .* sin(theta) + y .*cos(theta) + y0; 7 | end -------------------------------------------------------------------------------- /Matlab examples/rotatingDisk.m: -------------------------------------------------------------------------------- 1 | addpath('~/Downloads/MUST') 2 | param = getparam('P4-2v'); 3 | nPoints = 200000; 4 | xs = rand(1,nPoints)*12e-2-6e-2; 5 | zs =rand(1,nPoints)*12e-2; 6 | 7 | centerDisk = 0.05; 8 | idx = hypot(xs,zs-centerDisk)<2e-2; % First disk 9 | idx2 = hypot(xs,zs-.035)< 5e-3; % Second disk 10 | 11 | RC = rand(size(xs)); % reflection coefficients 12 | 13 | % Add reflectiion to both spheres 14 | RC(idx) = RC(idx) + 1; 15 | RC(idx2) = RC(idx2)+ 2; 16 | clear IQ; 17 | %%-- 18 | %% 19 | 20 | % Rotating disk velocity 21 | rotation_frequency = .5; 22 | w = 2 * pi * rotation_frequency; %1 Hz = 2 pi rads 23 | nreps = 5; 24 | param.PRP = 1e-3; 25 | tic 26 | for i = 1:nreps 27 | options.dBThresh = -6; 28 | options.ParPool = true; 29 | 30 | [xs_rot, zs_rot] = rotatePoints(xs(idx), zs(idx), 0, centerDisk, w * param.PRP); 31 | xs(idx) = xs_rot; 32 | zs(idx) = zs_rot; 33 | width = 60/180*pi; %width angle in rad 34 | txdel = txdelay(param,0,width); % in s 35 | [RF, param, RF_spectrum] = simus(xs,zs,RC,txdel,param, options); 36 | param.fs = 4 * param.fc; 37 | IQ(:, :, i) = rf2iq(RF,param); 38 | end 39 | toc 40 | 41 | % Beamforming 42 | [x, z] = impolgrid(128,10e-2,pi/2,param); 43 | M = dasmtx(IQ(:,:,1),x, z,txdel, param); 44 | IQ_r = reshape(IQ, [size(M, 2), size(IQ, 3)]); 45 | IQb = M*IQ_r; 46 | IQb = reshape(IQb, [size(x,1), size(x,2), size(IQ, 3)]); 47 | [v, var] = iq2doppler(IQb, param); -------------------------------------------------------------------------------- /Matlab examples/s.m: -------------------------------------------------------------------------------- 1 | xs = [1.7, 1.3, 0.7, 0, -0.7, -1.3, -1.7, 0, -1, 1]*1e-2; 2 | zs = [2.8, 3.2, 3.5, 3.6, 3.5, 3.2, 2.8, 2, 0.8, 0.8]*1e-2; 3 | RC = ones(size(xs)); 4 | 5 | param = getparam('L11-5v'); 6 | 7 | param.attenuation = 0.; 8 | txdel_0 = zeros(1,param.Nelements); 9 | tic 10 | [F,info, param] = mkmovie(xs, zs, RC,txdel_0,param, 'noAttenuation.gif'); 11 | toc -------------------------------------------------------------------------------- /Matlab examples/testPfield.m: -------------------------------------------------------------------------------- 1 | p = gcp(); % If no pool, do not create new one. 2 | if isempty(p) 3 | poolsize = 0; 4 | else 5 | poolsize = p.NumWorkers 6 | end 7 | %% 8 | I = rgb2gray(imread('heart.jpg')); 9 | % Pseudorandom distribution of scatterers (depth is 15 cm) 10 | fc = 2e6; 11 | [xs,y,zs,RC] = genscat([nan, 15e-2],1540/fc,I); 12 | 13 | 14 | param.fc = 2.7e6; 15 | param.bandwidth = 76; 16 | 17 | param.Nelements = 1; 18 | %% 19 | tic 20 | param = getparam('L11-5v'); 21 | param.attenuation = 0.5; 22 | OPTIONS.ParPool = true; 23 | param.fs = 4 * param.fc; 24 | 25 | 26 | L = (param.Nelements-1)*param.pitch; 27 | param.TXdelay = txdelay(param,deg2rad(10)); 28 | RF= simus(xs,zs,RC,param.TXdelay,param, OPTIONS); 29 | RF = tgc(RF); 30 | IQ = rf2iq(RF, param.fs,param.fc); 31 | 32 | param.fnumber = []; 33 | [xi_linear,zi_linear] = impolgrid([100, 100],15e-2,deg2rad(120),param); 34 | M = dasmtx(IQ,xi_linear,zi_linear,param); 35 | IQb = M*reshape(IQ,[], 1); 36 | IQb = reshape(IQb,size(xi_linear)); 37 | 38 | B_linear = bmode(IQb); 39 | toc 40 | tic 41 | P_linear = pfield(xi_linear, [], zi_linear, param.TXdelay, param); 42 | toc 43 | 44 | %% 45 | tic 46 | param = getparam('P6-3'); 47 | param.attenuation = 0.5; 48 | OPTIONS.ParPool = true; 49 | param.fs = 4 * param.fc; 50 | 51 | 52 | L = (param.Nelements-1)*param.pitch; 53 | param.TXdelay = txdelay(param, -pi/12, pi/3); 54 | RF= simus(xs,zs,RC,param.TXdelay,param, OPTIONS); 55 | RF = tgc(RF); 56 | IQ = rf2iq(RF, param.fs,param.fc); 57 | 58 | param.fnumber = []; 59 | [xi_linear,zi_linear] = impolgrid([200, 200],15e-2,deg2rad(120),param); 60 | M = dasmtx(IQ,xi_linear,zi_linear,param); 61 | IQb = M*reshape(IQ,[], 1); 62 | IQb = reshape(IQb,size(xi_linear)); 63 | 64 | B_linear = bmode(IQb); 65 | toc 66 | tic 67 | P_linear = pfield(xi_linear, [], zi_linear, param.TXdelay, param); 68 | toc 69 | %% 70 | tic 71 | param = getparam('C5-2V'); 72 | param.attenuation = 0.5; 73 | OPTIONS.ParPool = true; 74 | param.fs = 4 * param.fc; 75 | 76 | 77 | L = (param.Nelements-1)*param.pitch; 78 | param.TXdelay = txdelay(param, 0); 79 | RF= simus(xs,zs,RC,param.TXdelay,param, OPTIONS); 80 | RF = tgc(RF); 81 | IQ = rf2iq(RF, param.fs,param.fc); 82 | 83 | param.fnumber = []; 84 | [xi_linear,zi_linear] = impolgrid([200, 100],15e-2, param); 85 | M = dasmtx(IQ,xi_linear,zi_linear,param); 86 | IQb = M*reshape(IQ,[], 1); 87 | IQb = reshape(IQb,size(xi_linear)); 88 | 89 | B_linear = bmode(IQb); 90 | toc 91 | tic 92 | P_linear = pfield(xi_linear, [], zi_linear, param.TXdelay, param); 93 | toc 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyMUST 2 | This is a Python reimplementation of the MUST ultrasound toolbox for synthetic image generation and reconstruction (https://www.biomecardio.com/MUST/). 3 | 4 | *Notice:* this is still under development, and might have bugs and errors. Also, even if results should be the same with Matlab version, small numerical differences are expected. If you find any bug/unconsistency with the matlab version, please open a github issue, or send an email to ({damien.garcia@creatis.insa-lyon.fr, gabriel.bernardino@upf.edu}). 5 | 6 | As a design decision, we have tried to keep syntax as close as possible with the matlab version, specially regarding the way functions are called. This has resulted in non-pythonic arguments (i.e., overuse of variable number of positional arguments). This allows to make use of Must documentation (https://www.biomecardio.com/MUST/documentation.html). Keep in mind that, since Python does not allow a changing number of returns, each function will output the maximum number of variables of the matlab version. 7 | 8 | ## Installation 9 | ### Install from pip 10 | > pip install pymust 11 | 12 | ### Download from github 13 | To install a local version of pymust with its dependencies (matplotlib, scipy, numpy), download it, go to the main folder and then run: 14 | The package works in OsX, Linux and Windows (but parallelism might not be available on Windows). We recommend installing it in a separate conda environment. 15 | 16 | To install pymust with its dependencies (matplotlib, scipy, numpy), you can directly install from pip: 17 | > pip install git+https://github.com/creatis-ULTIM/PyMUST.git 18 | 19 | Alternatively, you can install from the test pypi using the following instruction: 20 | > python3 -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ PyMUST 21 | 22 | ## Main functions 23 | Please refer to the Matlab documentation or examples for a full description of the functions involved 24 | - Transducer definition (getparam) 25 | - Element delays (txdelay) 26 | - Simulation (simus, pfield) 27 | - Bmode and Doppler image beamforming from radiofrequencies (tgc, rf2iq, das, bmode, iq2doppler) 28 | 29 | ## Examples 30 | In the folder "examples", you have python notebooks ilustrating the main functionalities of PyMUST. They are the same as the ones available in the Matlab version. 31 | 32 | ## Next steps 33 | If there is a functionality that you would like to see, please open an issue. 34 | - Update function documentation. 35 | - Find computational bottlenecks, and optimise (possibly with C extensions). 36 | - GPU acceleration 37 | - Differentiable rendering. 38 | 39 | ## Citation 40 | If you use this library for your research, please cite: 41 | - [G. Bernardino, D. Garcia "PyMUST: an open-Source Python Library for the Simulation and Analysis of Ultrasound." 2024 IEEE Ultrasonics, Ferroelectrics, and Frequency Control Joint Symposium doi:10.1109/uffc-js60046.2024.10793881](https://ieeexplore.ieee.org/document/10793881) 42 | - [D. Garcia, "Make the most of MUST, an open-source MATLAB UltraSound Toolbox", 2021 IEEE International Ultrasonics Symposium (IUS), 2021, pp. 1-4, doi: 10.1109/IUS52206.2021.9593605](https://www.biomecardio.com/publis/ius21.pdf) 43 | - [D. Garcia, "SIMUS: an open-source simulator for medical ultrasound imaging. Part I: theory & examples", Computer Methods and Programs in Biomedicine, 218, 2022, p. 106726, doi: 10.1016/j.cmpb.2022.106726](https://www.biomecardio.com/publis/cmpb22.pdf) 44 | - [A. Cigier, F. Varray and D. Garcia "SIMUS: an open-source simulator for medical ultrasound imaging. Part II: comparison with four simulators," Computer Methods and Programs in Biomedicine, 218, 2022, p. 106726, doi: 10.1016/j.cmpb.2022.106726.](www.biomecardio.com/publis/cmpb22a.pdf) 45 | 46 | If you use the speckle tracking: 47 | - [V. Perrot and D. Garcia, "Back to basics in ultrasound velocimetry: tracking speckles by using a standard PIV algorithm", 2018 IEEE International Ultrasonics Symposium (IUS), 2018, pp. 206-212, doi: 10.1109/ULTSYM.2018.8579665](https://www.biomecardio.com/publis/ius18.pdf) 48 | 49 | If you use beamforming: 50 | - [V. Perrot, M. Polichetti, F. Varray and D. Garcia, "So you think you can DAS? A viewpoint on delay-and-sum beamforming," 'Ultrasonics, 111, 2021, p. 106309, doi 10.1016/j.ultras.2020.106309](https://www.biomecardio.com/publis/ultrasonics21.pdf) 51 | 52 | If you use vector flow: 53 | - [C. Madiena, J. Faurie, J. Porée and D. Garcia "Color and vector flow imaging in parallel ultrasound with sub-Nyquist sampling," IEEE Transactions on Ultrasonics, Ferroelectrics, and Frequency Control 65, 2018, pp. 795-802 10.1109/TUFFC.2018.2817885](https://hal.science/hal-01988025/) 54 | 55 | ## Acknowledgements 56 | This work has been patially funded by Grant #RYC2022-035960-I funded by MICIU/AEI/ 10.13039/501100011033 and by the FSE+ 57 | ![image](https://github.com/user-attachments/assets/31c21398-2c34-421c-a3a0-1e628ab5d0cd) 58 | 59 | 60 | -------------------------------------------------------------------------------- /examples/data/PWI_disk.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/examples/data/PWI_disk.mat -------------------------------------------------------------------------------- /examples/data/RFsignal@20MHz.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/examples/data/RFsignal@20MHz.mat -------------------------------------------------------------------------------- /examples/data/bmode_PWI_disk.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/examples/data/bmode_PWI_disk.mat -------------------------------------------------------------------------------- /examples/data/heart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/examples/data/heart.jpg -------------------------------------------------------------------------------- /examples/testMkMovie.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pymust, pymust.mkmovie\n", 10 | "import importlib\n", 11 | "import numpy as np\n", 12 | "import matplotlib.pyplot as plt" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 2, 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "param = pymust.getparam('P4-2v');" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "xf = 0e-2\n", 31 | "zf = 2e-2 # focus position (in m)\n", 32 | "txdel = pymust.txdelay(xf,zf,param);" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 4, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "param.movie = np.array([3, 6, 100])" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 6, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "param.attenuation = 0.0\n", 51 | "F,info, param = pymust.mkmovie(txdel,param, 'noAttenuation.gif');" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "param.attenuation = 0.5\n", 61 | "F,info, param = pymust.mkmovie(txdel,param, 'meow.gif');" 62 | ] 63 | } 64 | ], 65 | "metadata": { 66 | "kernelspec": { 67 | "display_name": "pymust", 68 | "language": "python", 69 | "name": "python3" 70 | }, 71 | "language_info": { 72 | "codemirror_mode": { 73 | "name": "ipython", 74 | "version": 3 75 | }, 76 | "file_extension": ".py", 77 | "mimetype": "text/x-python", 78 | "name": "python", 79 | "nbconvert_exporter": "python", 80 | "pygments_lexer": "ipython3", 81 | "version": "3.11.4" 82 | }, 83 | "orig_nbformat": 4 84 | }, 85 | "nbformat": 4, 86 | "nbformat_minor": 2 87 | } 88 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "setuptools-scm>=8"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | 7 | [project] 8 | name = "PyMUST" 9 | authors = [ 10 | {name="Damien Garcia", email="damien.garcia@creatis.insa-lyon.fr"}, 11 | {name = "Gabriel Bernardino", email = "gabriel.bernardino@upf.edu"}, 12 | ] 13 | description = "Python port of the MUST toolbox for ultrasound signal processing and generation of simulated images" 14 | readme = "README.md" 15 | requires-python = ">=3.8" 16 | classifiers = [ 17 | "Programming Language :: Python :: 3", 18 | "Operating System :: OS Independent", 19 | ] 20 | dependencies = [ 21 | "matplotlib", 22 | "numpy", 23 | "scipy", 24 | ] 25 | dynamic = ["license", "version"] 26 | 27 | [project.urls] 28 | Homepage = "https://www.biomecardio.com/MUST" 29 | Repository = "https://github.com/creatis-ULTIM/PyMUST" 30 | Issues = "https://github.com/creatis-ULTIM/PyMUST/issues" 31 | 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | import subprocess 5 | 6 | def _get_version_hash(): 7 | """Talk to git and find out the tag/hash of our latest commit""" 8 | try: 9 | p = subprocess.Popen(["git", "describe", 10 | "--tags", "--dirty", "--always"], 11 | stdout=subprocess.PIPE) 12 | except EnvironmentError: 13 | print("Couldn't run git to get a version number for setup.py") 14 | return 15 | ver = p.communicate()[0] 16 | if isinstance(ver, bytes): 17 | ver = ver.decode('ascii') 18 | return ver.strip() 19 | 20 | setup(name='pymust', 21 | description='Python port of the MUST toolbox for ultrasound signal processing and generation of simulated images.', 22 | author='Gabriel Bernardino (Python port), Damien Garcia (original matlab code)', 23 | author_email='gabriel.bernardino1@gmail.com', 24 | url='https://www.biomecardio.com/en//', 25 | #version= '0.1.3', 26 | packages=['pymust'], 27 | license = 'GNU Lesser General Public License v3.0 (LGPL v3)', 28 | package_dir={'':'src'} 29 | ) 30 | -------------------------------------------------------------------------------- /src/pymust.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/pymust/Data/colorMap.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatis-ULTIM/PyMUST/50f56c51c0d3a0f4fcedd93c102bce6e50fd4581/src/pymust/Data/colorMap.pkl -------------------------------------------------------------------------------- /src/pymust/__init__.py: -------------------------------------------------------------------------------- 1 | interactiveDevelopment = False # Put it to True if you are developing pymust, in order to allow easier reloading of the modules 2 | if not interactiveDevelopment: 3 | from pymust.bmode import bmode 4 | from pymust.dasmtx import dasmtx 5 | from pymust.dasmtx3 import dasmtx3 6 | from pymust.getparam import getparam 7 | from pymust.impolgrid import impolgrid 8 | from pymust.iq2doppler import iq2doppler, getNyquistVelocity 9 | from pymust.pfield import pfield 10 | from pymust.pfield3 import pfield3 11 | from pymust.rf2iq import rf2iq 12 | from pymust.simus import simus 13 | from pymust.simus3 import simus3 14 | from pymust.tgc import tgc 15 | from pymust.txdelay import txdelay, txdelayCircular, txdelayPlane, txdelayFocused 16 | from pymust.txdelay3 import txdelay3, txdelay3Plane, txdelay3Diverging, txdelay3Focused 17 | from pymust.utils import getDopplerColorMap 18 | from pymust.genscat import genscat 19 | from pymust.genscat import genscat 20 | from pymust.mkmovie import mkmovie 21 | from pymust.getpulse import getpulse 22 | from pymust.smoothn import smoothn 23 | from pymust.sptrack import sptrack 24 | # Missing functions: genscat, speckletracking, cite + visualisation 25 | # Visualisation: pcolor, Doppler color map + transparency, radiofrequency data -------------------------------------------------------------------------------- /src/pymust/bmode.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from . import utils 3 | def bmode(IQ: np.ndarray, DR: float = 40) -> np.ndarray: 4 | 5 | """ 6 | %BMODE B-mode image from I/Q signals 7 | % BMODE(IQ,DR) converts the I/Q signals (in IQ) to 8-bit log-compressed 8 | % ultrasound images with a dynamic range DR (in dB). IQ is a complex 9 | % whose real (imaginary) part contains the inphase (quadrature) 10 | % component. 11 | % 12 | % BMODE(IQ) uses DR = 40 dB; 13 | % 14 | % 15 | % Example: 16 | % ------- 17 | % #-- Analyze undersampled RF signals and generate B-mode images 18 | % # Download experimental data (128-element linear array + rotating disk) 19 | % load('PWI_disk.mat') 20 | % # Demodulate the RF signals with RF2IQ. 21 | % IQ = rf2iq(RF,param); 22 | % % Create a 2.5-cm-by-2.5-cm image grid. 23 | % dx = 1e-4; % grid x-step (in m) 24 | % dz = 1e-4; % grid z-step (in m) 25 | % [x,z] = meshgrid(-1.25e-2:dx:1.25e-2,1e-2:dz:3.5e-2); 26 | % % Create a Delay-And-Sum DAS matri x with DASMTX. 27 | % param.fnumber = []; % an f-number will be determined by DASMTX 28 | % M = dasmtx(1i*size(IQ),x,z,param,'nearest'); 29 | % % Beamform the I/Q signals. 30 | % IQb = M*reshape(IQ,[],32); 31 | % IQb = reshape(IQb,[size(x) 32]); 32 | % % Create the B-mode images with a -30dB range. 33 | % I = bmode(IQb,30); 34 | % % Display four B-mode images. 35 | % for k = 1:4 36 | % subplot(2,2,k) 37 | % imshow(I(:,:,10*k-9)) 38 | % axis off 39 | % title(['frame #' int2str(10*k-9)]) 40 | % end 41 | % colormap gray 42 | % 43 | % 44 | % This function is part of MUST (Matlab UltraSound Toolbox). 45 | % MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later 46 | % 47 | % See also RF2IQ, TGC, SPTRACK. 48 | % 49 | % -- Damien Garcia -- 2020/06 50 | % website: www.BiomeCardio.com 52 | """ 53 | assert utils.iscomplex(IQ),'IQ must be a complex array' 54 | 55 | I = np.abs(IQ) # real envelope 56 | 57 | if (DR >= 1): 58 | I = 20*np.log10(I/np.max(I))+DR 59 | I = (255*I/DR) #.astype(np.uint8) # 8-bit log-compressed image 60 | else: 61 | I = np.power(I / np.max(I), DR) 62 | I *= 255 63 | 64 | I[I<0] = 0 65 | I[I>255] = 255 66 | 67 | return I.astype(np.uint8) -------------------------------------------------------------------------------- /src/pymust/genscat.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import scipy, scipy.interpolate 3 | from typing import Union 4 | from . import utils 5 | import numpy as np 6 | 7 | def genscat(roidim: np.ndarray, meandist: np.ndarray ,I: Union[np.ndarray, None] = None, g: Union[np.ndarray, float] = None) -> tuple[np.ndarray, np.ndarray, np.ndarray]: 8 | """ 9 | %GENSCAT Generate a distribution of scatterers 10 | % [XS,YS,ZS] = GENSCAT([WIDTH HEIGHT],MEANDIST) generates a 2-D 11 | % pseudorandom distribution of scatterers such that the mean distance 12 | % between a scatterer and its nearest neighbor is approximately MEANDIST. 13 | % The vector [WIDTH HEIGHT] (unit = m) specifies the width and height of 14 | % the rectangular ROI to which the scatterers belong. The middle of its 15 | % lower edge (in the x-z coordinate system) is located at (0,0). In this 16 | % 2-D syntax, YS is a vector of zeros. 17 | % 18 | % [XS,YS,ZS] = GENSCAT([WIDTH HEIGHT DEPTH],MEANDIST) generates a 3-D 19 | % pseudorandom distribution of scatterers such that the mean distance 20 | % between a scatterer and its nearest neighbor is approximately MEANDIST. 21 | % The vector [WIDTH HEIGHT DEPTH] (unit = m) specifies the width 22 | % (x-direction), height (z-direction), and depth (y-direction) of the ROI 23 | % box to which the scatterers belong. The center of its lower face (in 24 | % the x-y-z coordinate system) is located at (0,0,0). 25 | % 26 | % [XS,YS,ZS,RC] = GENSCAT([...],MEANDIST,I) also returns the reflection 27 | % coefficients RC of the scatterers. The RC values follow Rayleigh 28 | % distributions whose means are calculated from the image I. I must be a 29 | % 2-D or 3-D image whose size is given by [WIDTH HEIGHT] (2-D) or 30 | % [WIDTH HEIGHT DEPTH] (3-D). 31 | % 32 | % If a 2-D image I is given as an input, you can use [WIDTH NaN] or [Nan 33 | % HEIGHT]. The missing value is calculated from the pixel-based size of 34 | % the image I while preserving the aspect ratio. Similarly, [WIDTH Nan 35 | % NaN], [Nan HEIGHT NaN], or [NaN NaN DEPTH] can be used with a 3-D 36 | % image. If I is empty or omitted, then I is assumed to be an image of 37 | % ones. 38 | % 39 | % [...] = GENSCAT(...,G) assumes that the dynamic range of the input 40 | % image I is G dB (defaut value = 40 dB). If G<=1, it is assumed that the 41 | % image I is gamma-compressed, with gamma = G. 42 | % 43 | % Note on MEANDIST: 44 | % ---------------- 45 | % We recommend a MEANDIST value less than or equal to the minimum 46 | % wavelength (at -6 dB). For a speed of sound c, from the PARAM structure 47 | % generated by GETPARAM, you may use 48 | % MEANDIST = PARAM.c/(PARAM.fc*(1+PARAM.bandwidth/200)), 49 | % or a smaller value. This value is used automatically if you input the 50 | % PARAM structure instead of MEANDIST: 51 | % [...] = GENSCAT([...],PARAM,I) 52 | % Note: by default, PARAM.c = 1540 [m/s] and PARAM.bandwidth = 75 [%], 53 | % as in PFIELD. 54 | % 55 | % 56 | % Example: cardiac scatterers: 57 | % --------------------------- 58 | % %-- Read the heart image and make it gray 59 | % I = imread('heart.jpg'); 60 | % I = rgb2gray(I); 61 | % [nl,nc] = size(I); 62 | % %-- Parameters of the cardiac phased array 63 | % param = getparam('P4-2v'); 64 | % %-- Pseudorandom distribution of scatterers (depth is 15 cm) 65 | % [xs,~,zs,RC] = genscat([NaN 15e-2],param,I); 66 | % %-- Display the scatterers in a dB scale 67 | % scatter(xs*1e2,zs*1e2,2,20*log10(RC/max(RC(:))),'filled') 68 | % caxis([-40 0]) 69 | % colormap hot 70 | % axis equal ij tight 71 | % set(gca,'XColor','none','box','off') 72 | % title([int2str(numel(RC)) ' cardiac scatterers']) 73 | % ylabel('[cm]') 74 | % 75 | % 76 | % This function is part of MUST (Matlab UltraSound Toolbox). 77 | % MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later 78 | % 79 | % See also SIMUS, GETPARAM. 80 | % 81 | % -- Damien Garcia -- 2021/12, last update 2022/05/10 82 | % website: www.BiomeCardio.com 84 | """ 85 | 86 | #%-- Check the input arguments 87 | if isinstance(roidim, list): 88 | roidim = np.array(roidim) 89 | 90 | assert utils.isnumeric(roidim) and isinstance(roidim, np.ndarray), 'The 1st argument must be a numeric vector.' 91 | assert len(roidim)==2 or len(roidim)==3, 'The 1st argument must be a vector of length 2 or 3.' 92 | if isinstance(meandist, utils.Param): 93 | #%-- The input is PARAM 94 | #% Check the PARAM structure and calculate the default MEANDIST 95 | #% (= minimal wavelength at -6 dB) 96 | param = meandist 97 | #%-- 98 | #% 1) Center frequency (in Hz) 99 | assert utils.isfield(param,'fc'), 'A center frequency value (PARAM.fc) is required.' 100 | #%-- 101 | #% 2) Fractional bandwidth at -6dB (in %) 102 | if not utils.isfield(param,'bandwidth'): 103 | param.bandwidth = 75 104 | 105 | assert param.bandwidth>0 and param.bandwidth<200, 'The fractional bandwidth at -6 dB (PARAM.bandwidth, in %) must be in ]0,200[' 106 | 107 | #%-- 108 | #% 3) Longitudinal velocity (in m/s) 109 | if not utils.isfield(param,'c'): 110 | param.c = 1540 # % default value 111 | 112 | #%-- 113 | meandist = param.c/(param.fc*(1+param.bandwidth/200)) 114 | else: 115 | assert isinstance(meandist, float) and meandist>0, 'MEANDIST must be a positive scalar.' 116 | 117 | if I is not None: 118 | assert len(I.shape) in [2, 3] and np.all(I>=0), 'I must be 2-D or 3-D with non-negative elements.' 119 | assert len(roidim)== len(I.shape), 'The number of dimensions of I does not match the length of the 1st argument.' 120 | else: 121 | assert np.all(np.isfinite(roidim)), 'The 1st argument must contain only finite elements if I is not given.' 122 | assert g is None, 'The 4th argument g cannot be used if I is not given.' #Note GB: actually a warning should be enough 123 | 124 | ##%-- Calculate xmin, xmax, ymin, ymax, and zmax (we have zmin = 0) 125 | width,height = roidim 126 | #% 127 | if len(roidim)==2: #% 2-D 128 | assert np.any((np.isfinite(roidim))), 'The vector [WIDTH HEIGHT] must contain at least one finite element.' 129 | if I is not None: 130 | m,n = I.shape 131 | if not np.isfinite(width): 132 | width = n*height/m 133 | if not np.isfinite(height): 134 | height = m*width/n 135 | else: # 3-D 136 | tmp = np.sum(np.isfinite(roidim)) 137 | assert tmp==1 or tmp==3, 'The vector [WIDTH HEIGHT DEPTH] must contain one or three finite elements.' 138 | depth = roidim[2] 139 | if I is not None: 140 | m,n,p = I.shape 141 | if not np.all(np.isfinite(roidim)): #% [WIDTH HEIGHT DEPTH] contains NaN or Inf 142 | if np.isfinite(height): 143 | width = n*height/m 144 | depth = p*height/m 145 | elif np.isfinite(width): 146 | height = m*width/n 147 | depth = p*width/n 148 | else: 149 | width = n*depth/p 150 | height = m*depth/p 151 | ymin = -depth/2 152 | ymax = depth/2 153 | 154 | 155 | xmin = -width/2 156 | xmax = width/2 157 | zmin = 0 158 | zmax = height 159 | 160 | 161 | #%-- Pseudorandom distribution of the scatterers 162 | if len(roidim)==2: #% 2-D 163 | #%-- 2-D pseudorandom distribution -- 164 | 165 | xz_inc = meandist/np.sqrt(2/5) 166 | #% note: sqrt(2/5) was determined numerically (theory = ?...) 167 | 168 | xs,zs =np.meshgrid( np.arange(xmin, xmax, xz_inc), np.arange(zmin, zmax, xz_inc)) 169 | xs = xs + np.random.rand(*xs.shape)*xz_inc-xz_inc/2 170 | zs = zs + np.random.rand(*zs.shape)*xz_inc-xz_inc/2 171 | 172 | idx = np.logical_and(np.logical_and(xs>xmin, xszmin, zsxmin, xsymin, yszmin, zs1: 252 | #% log compression 253 | RC = np.power(10,(g/20*(RC-1))) 254 | else: 255 | #% gamma compression 256 | RC = np.power(RC, 1/g) 257 | 258 | #% add some randomness in the reflection coefficients 259 | #% RC = RC.*raylrnd(1,1,length(xs))'/sqrt(pi/2); 260 | RC = RC*np.hypot(np.random.rand(*xs.shape),np.random.rand(*xs.shape))/np.sqrt(np.pi/2) 261 | 262 | return xs,ys,zs,RC -------------------------------------------------------------------------------- /src/pymust/getparam.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from . import utils 3 | 4 | 5 | def getparam(probe: str) -> utils.Param: 6 | #GETPARAM Get parameters of a uniform linear or convex array 7 | # PARAM = GETPARAM opens a dialog box which allows you to select a 8 | # transducer whose parameters are returned in PARAM. 9 | 10 | # PARAM = GETPARAM(PROBE), where PROBE is a string, returns the prameters 11 | # of the transducer given by PROBE. 12 | 13 | # The structure PARAM is used in several functions of MUST (Matlab 14 | # UltraSound Toolbox). The structure returned by GETPARAM contains only 15 | # the fields that describe a transducer. Other fields may be required in 16 | # some MUST functions. 17 | 18 | # PROBE can be one of the following: 19 | # --------------------------------- 20 | # 1) 'L11-5v' (128-element, 7.6-MHz linear array) 21 | # 2) 'L12-3v' (192-element, 7.5-MHz linear array) 22 | # 3) 'C5-2v' (128-element, 3.6-MHz convex array) 23 | # 4) 'P4-2v' (64-element, 2.7-MHz phased array) 24 | 25 | # These are the Verasonics' transducers. 27 | # Feel free to complete this list for your own use. 28 | 29 | # PARAM is a structure that contains the following fields: 30 | # -------------------------------------------------------- 31 | # 1) PARAM.Nelements: number of elements in the transducer array 32 | # 2) PARAM.fc: center frequency (in Hz) 33 | # 3) PARAM.pitch: element pitch (in m) 34 | # 4) PARAM.width: element width (in m) 35 | # 5) PARAM.kerf: kerf width (in m) 36 | # 6) PARAM.bandwidth: 6-dB fractional bandwidth (in #) 37 | # 7) PARAM.radius: radius of curvature (in m, Inf for a linear array) 38 | # 8) PARAM.focus: elevation focus (in m) 39 | # 9) PARAM.height: element height (in m) 40 | 41 | 42 | # Example: 43 | # ------- 44 | # #-- Generate a focused pressure field with a phased-array transducer 45 | # # Phased-array @ 2.7 MHz: 46 | # param = getparam('P4-2v'); 47 | # # Focus position: 48 | # x0 = 2e-2; z0 = 5e-2; 49 | # # TX time delays: 50 | # dels = txdelay(x0,z0,param); 51 | # # Grid: 52 | # x = linspace(-4e-2,4e-2,200); 53 | # z = linspace(param.pitch,10e-2,200); 54 | # [x,z] = meshgrid(x,z); 55 | # y = zeros(size(x)); 56 | # # RMS pressure field: 57 | # P = pfield(x,y,z,dels,param); 58 | # imagesc(x(1,:)*1e2,z(:,1)*1e2,20*log10(P/max(P(:)))) 59 | # hold on, plot(x0*1e2,z0*1e2,'k*'), hold off 60 | # colormap hot, axis equal tight 61 | # caxis([-20 0]) 62 | # c = colorbar; 63 | # c.YTickLabel{end} = '0 dB'; 64 | # xlabel('[cm]') 65 | 66 | 67 | # This function is part of MUST (Matlab UltraSound Toolbox). 69 | # MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later 70 | 71 | # See also TXDELAY, PFIELD, SIMUS, GETPULSE. 72 | 73 | # -- Damien Garcia -- 2015/03, last update: 2020/07 74 | # website: www.BiomeCardio.com 76 | param = utils.Param() 77 | probe = probe.upper() 78 | 79 | 80 | # from computeTrans.m (Verasonics, version post Aug 2019) 81 | if 'L11-5V' == probe: 82 | # --- L11-5v (Verasonics) --- 83 | param.fc = 7600000.0 84 | param.kerf = 3e-05 85 | param.width = 0.00027 86 | param.pitch = 0.0003 87 | param.Nelements = 128 88 | param.bandwidth = 77 89 | param.radius = np.inf 90 | param.height = 0.005 91 | param.focus = 0.018 92 | elif 'L12-3V' == probe: 93 | # --- L12-3v (Verasonics) --- 94 | param.fc = 7540000.0 95 | param.kerf = 3e-05 96 | param.width = 0.00017 97 | param.pitch = 0.0002 98 | param.Nelements = 192 99 | param.bandwidth = 93 100 | param.radius = np.inf 101 | param.height = 0.005 102 | param.focus = 0.02 103 | elif 'C5-2V' == probe: 104 | # --- C5-2v (Verasonics) --- 105 | param.fc = 3570000.0 106 | param.kerf = 4.8e-05 107 | param.width = 0.00046 108 | param.pitch = 0.000508 109 | param.Nelements = 128 110 | param.bandwidth = 79 111 | param.radius = 0.04957 112 | param.height = 0.0135 113 | param.focus = 0.06 114 | elif 'P4-2V' == probe: 115 | # --- P4-2v (Verasonics) --- 116 | param.fc = 2720000.0 117 | param.kerf = 5e-05 118 | param.width = 0.00025 119 | param.pitch = 0.0003 120 | param.Nelements = 64 121 | param.bandwidth = 74 122 | param.radius = np.inf 123 | param.height = 0.014 124 | param.focus = 0.06 125 | #--- From the OLD version of GETPARAM: ---# 126 | elif 'PA4-2/20' == probe: 127 | # --- PA4-2/20 --- 128 | param.fc = 2500000.0 129 | param.kerf = 5e-05 130 | param.pitch = 0.0003 131 | param.height = 0.014 132 | param.Nelements = 64 133 | param.bandwidth = 60 134 | elif 'L9-4/38' == (probe): 135 | # --- L9-4/38 --- 136 | param.fc = 5000000.0 137 | param.kerf = 3.5e-05 138 | param.pitch = 0.0003048 139 | param.height = 0.006 140 | param.Nelements = 128 141 | param.bandwidth = 65 142 | elif 'LA530' == (probe): 143 | # --- LA530 --- 144 | param.fc = 3000000.0 145 | width = 0.215 / 1000 146 | param.kerf = 0.03 / 1000 147 | param.pitch = width + param.kerf 148 | # element_height = 6/1000; # Height of element [m] 149 | param.Nelements = 192 150 | elif 'L14-5/38' == (probe): 151 | # --- L14-5/38 --- 152 | param.fc = 7200000.0 153 | param.kerf = 2.5e-05 154 | param.pitch = 0.0003048 155 | # height = 4e-3; # Height of element [m] 156 | param.Nelements = 128 157 | param.bandwidth = 70 158 | elif 'L14-5W/60' == (probe): 159 | # --- L14-5W/60 --- 160 | param.fc = 7500000.0 161 | param.kerf = 2.5e-05 162 | param.pitch = 0.000472 163 | # height = 4e-3; # Height of element [m] 164 | param.Nelements = 128 165 | param.bandwidth = 65 166 | elif 'P6-3' == (probe): 167 | # --- P6-3 --- 168 | param.fc = 4500000.0 169 | param.kerf = 2.5e-05 170 | param.pitch = 0.000218 171 | param.Nelements = 64 172 | param.bandwidth = 2 / 3 * 100 173 | else: 174 | raise Exception(np.array(['The probe ',probe,' is unknown. Should be one of [L11-5V, L12-3V, C5-2V, P4-2V, PA4-2/20, L9-4/38, LA530, L14-5/38, L14-5W/60, P6-3]'])) 175 | 176 | return param 177 | -------------------------------------------------------------------------------- /src/pymust/getpulse.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import numpy as np 3 | from . import utils 4 | 5 | def getpulse(param: utils.Param, way: int = 2, PreVel: str = 'pressure', dt: float = 1e-09) -> tuple[np.ndarray, np.ndarray]: 6 | #GETPULSE Get the transmit pulse 7 | # PULSE = GETPULSE(PARAM,WAY) returns the one-way or two-way transmit 8 | # pulse with a time sampling of 1 nanosecond. Use WAY = 1 to get the 9 | # one-way pulse, or WAY = 2 to obtain the two-way (pulse-echo) pulse. 10 | 11 | # PULSE = GETPULSE(PARAM,WAY,PRESVEL) returns the pulse in terms of 12 | # Pressure or Velocity. PRESVEL can be: 13 | # 'pressure', which is the default 14 | # 'velocity3D' or 'velocity2D' 15 | 16 | # PULSE = GETPULSE(PARAM) uses WAY = 1 and PRESVEL = 'pressure'. 17 | 18 | # [PULSE,t] = GETPULSE(...) also returns the time vector. 19 | 20 | # PARAM is a structure which must contain the following fields: 21 | # ------------------------------------------------------------ 22 | # 1) PARAM.fc: central frequency (in Hz, REQUIRED) 23 | # 2) PARAM.bandwidth: pulse-echo 6dB fractional bandwidth (in #) 24 | # The default is 75#. 25 | # 3) PARAM.TXnow: number of wavelengths of the TX pulse (default: 1) 26 | # 4) PARAM.TXfreqsweep: frequency sweep for a linear chirp (default: []) 27 | # To be used to simulate a linear TX chirp. 28 | 29 | # Example #1: 30 | # ---------- 31 | # #-- Get the one-way pulse of a phased-array probe 32 | # # Phased-array @ 2.7 MHz: 33 | # param = getparam('P4-2v'); 34 | # # One-way transmit pulse 35 | # [pulse,t] = getpulse(param); 36 | # # Plot the pulse 37 | # plot(t*1e6,pulse) 38 | # xlabel('{\mu}s') 39 | # axis tight 40 | 41 | # Example #2: 42 | # ---------- 43 | # #-- Check the pulse with a linear chirp 44 | # # Linear array: 45 | # param = getparam('L11-5v'); 46 | # # Modify the fractional bandwidth: 47 | # param.bandwidth = 120; 48 | # # Define the properties of the chirp 49 | # param.TXnow = 20; 50 | # param.TXfreqsweep = 10e6; 51 | # # One-way transmit pulse 52 | # [pulse,t] = getpulse(param); 53 | # # Plot the pulse 54 | # plot(t*1e6,pulse) 55 | # xlabel('{\mu}s') 56 | # axis tight 57 | 58 | 59 | # This function is part of MUST (Matlab UltraSound Toolbox). 61 | # MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later 62 | 63 | # See also PFIELD, SIMUS, GETPARAM. 64 | 65 | # -- Damien Garcia -- 2020/12, last update: 2021/05/26 66 | # website: www.BiomeCardio.com 68 | 69 | 70 | assert way == 1 or way == 2,'WAY must be 1 (one-way) or 2 (two-way)' 71 | #-- Check PreVel 72 | PreVelValid = ['pressure','pres','velocity2d','velocity3d','vel2d','vel3d'] 73 | assert PreVel.lower() in PreVelValid, 'PRESVEL must be one of: ' + ', '.joiN(PreVelValid) 74 | #-- Center frequency (in Hz) 75 | assert 'fc' in param,'A center frequency value (PARAM.fc) is required.' 76 | fc = param.fc 77 | 78 | #-- Fractional bandwidth at -6dB (in #) 79 | if not 'bandwidth' in param : 80 | param.bandwidth = 75 81 | 82 | assert param.bandwidth > 0 and param.bandwidth < 200,'The fractional bandwidth at -6 dB (PARAM.bandwidth, in %) must be in ]0,200[' 83 | 84 | #-- TX pulse: Number of wavelengths 85 | if 'TXnow' not in param : 86 | param.TXnow = 1 87 | 88 | NoW = param.TXnow 89 | 90 | assert np.isscalar(NoW) and utils.isnumeric(NoW) and NoW > 0,'PARAM.TXnow must be a positive scalar.' 91 | 92 | #-- TX pulse: Frequency sweep for a linear chirp 93 | if not 'TXfreqsweep' in param or param.TXfreqsweep is None or np.isinf(param.TXfreqsweep): 94 | param.TXfreqsweep = None 95 | 96 | FreqSweep = param.TXfreqsweep 97 | assert FreqSweep is None or (np.isscalar(FreqSweep) and utils.isnumeric(FreqSweep) and FreqSweep > 0),'PARAM.TXfreqsweep must be None (windowed sine) or a positive scalar (linear chirp).' 98 | 99 | pulseSpectrum = param.getPulseSpectrumFunction(FreqSweep) 100 | 101 | #-- FREQUENCY RESPONSE of the ensemble PZT + probe 102 | probeSpectrum = param.getProbeFunction() 103 | # Note: The spectrum of the pulse (pulseSpectrum) will be then multiplied 104 | # by the frequency-domain tapering window of the transducer (probeSpectrum) 105 | 106 | #-- frequency samples 107 | eps = 1e-9 108 | 109 | df = param.fc / param.TXnow / 32 110 | p = utils.nextpow2(1 / dt / 2 / df) 111 | Nf = 2 ** p 112 | f = np.linspace(0,1 / dt / 2,Nf) 113 | #-- spectrum of the pulse 114 | F = np.multiply(pulseSpectrum(2 * np.pi * f),probeSpectrum(2 * np.pi * f) ** way) 115 | if PreVel.lower() in ['vel2d','velocity2d']: 116 | F = F / (np.sqrt(f) + eps) 117 | elif PreVel.lower() in ['vel3d','velocity3d']: 118 | F = F / (f + eps) 119 | 120 | # Corrected frequencies % (added on April 24, 2023 ; removed on Nov 8, 2023) on MUST 121 | # P = np.abs(F)**2 122 | # Fc = np.trapz(f*P) / np.trapz(P) 123 | # f = f + Fc - fc 124 | 125 | F = np.multiply(pulseSpectrum(2 * np.pi * f),probeSpectrum(2 * np.pi * f) ** way) 126 | 127 | #-- pulse in the temporal domain (step = 1 ns) 128 | pulse = np.fft.fftshift(np.fft.irfft(F)) 129 | pulse = pulse / np.max(np.abs(pulse)) 130 | #-- keep the significant magnitudes 131 | idx, = np.where(pulse > (1 / 1023)) 132 | idx1 = idx[0] 133 | idx2 = idx[-1] 134 | idx = min(idx1 + 1, 2 * Nf - 1 - idx2-1) 135 | #pulse = pulse[np.arange(end() - idx + 1,idx+- 1,- 1) 136 | pulse = pulse[-idx: idx-2:-1] 137 | #-- time vector 138 | t = np.arange(len(pulse)) *dt 139 | return pulse,t 140 | 141 | 142 | -------------------------------------------------------------------------------- /src/pymust/impolgrid.py: -------------------------------------------------------------------------------- 1 | import numpy as np, logging, typing 2 | from . import utils 3 | def impolgrid(siz: typing.Union[int, np.ndarray, list], zmax: float, width: float, param: utils.Param = None): 4 | """ 5 | %IMPOLGRID Polar-type grid for ultrasound images 6 | % IMPOLGRID returns a polar-type (fan-type) grid expressed in Cartesian 7 | % coordinates. This is a "natural" grid (before scan-conversion) used 8 | % when beamforming signals obtained with a cardiac phased array or a 9 | % convex array. 10 | % 11 | % [X,Z] = IMPOLGRID(SIZ,ZMAX,WIDTH,PARAM) returns the X,Z coordinates of 12 | % the fan-type grid of size SIZ and angular width WIDTH (in rad) for a 13 | % phased array described by PARAM. The maximal Z (maximal depth) is ZMAX. 14 | % 15 | % [X,Z] = IMPOLGRID(SIZ,ZMAX,PARAM) returns the X,Z coordinates of 16 | % the fan-type grid of size SIZ and angular width WIDTH (in rad) for a 17 | % convex array described by PARAM. For a convex array, PARAM.radius is 18 | % not Inf. The maximal Z (maximal depth) is ZMAX. 19 | % 20 | % If SIZ is a scalar M, then the size of the grid is [M,M]. 21 | % 22 | % [X,Z,Z0] = IMPOLGRID(...) also returns the z-coordinate of the grid 23 | % origin. Note that X0 = 0. 24 | % 25 | % Units: X,Z,Z0 are in m. WIDTH must be in rad. 26 | % 27 | % PARAM is a structure which must contain the following fields: 28 | % ------------------------------------------------------------ 29 | % 1) PARAM.pitch: pitch of the array (in m, REQUIRED) 30 | % 2) PARAM.Nelements: number of elements in the transducer array (REQUIRED) 31 | % 3) PARAM.radius: radius of curvature (in m, default = Inf, linear array) 32 | % 33 | % 34 | % Examples: 35 | % -------- 36 | % %-- Generate a focused pressure field with a phased-array transducer 37 | % % Phased-array @ 2.7 MHz: 38 | % param = getparam('P4-2v'); 39 | % % Focus position: 40 | % xf = 2e-2; zf = 5e-2; 41 | % % TX time delays: 42 | % dels = txdelay(xf,zf,param); 43 | % % 60-degrees wide grid: 44 | % [x,z] = impolgrid([100 50],10e-2,pi/3,param); 45 | % % RMS pressure field: 46 | % P = pfield(x,z,dels,param); 47 | % % Scatter plot of the pressure field: 48 | % figure 49 | % scatter(x(:)*1e2,z(:)*1e2,5,20*log10(P(:)/max(P(:))),'filled') 50 | % colormap jet, axis equal ij tight 51 | % xlabel('cm'), ylabel('cm') 52 | % caxis([-20 0]) 53 | % c = colorbar; 54 | % c.YTickLabel{end} = '0 dB'; 55 | % % Image of the pressure field: 56 | % figure 57 | % pcolor(x*1e2,z*1e2,20*log10(P/max(P(:)))) 58 | % shading interp 59 | % colormap hot, axis equal ij tight 60 | % xlabel('[cm]'), ylabel('[cm]') 61 | % caxis([-20 0]) 62 | % c = colorbar; 63 | % c.YTickLabel{end} = '0 dB'; 64 | % 65 | % 66 | % This function is part of MUST (Matlab UltraSound Toolbox). 67 | % MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later 68 | % 69 | % See also DAS, DASMTX, PFIELD. 70 | % 71 | % -- Damien Garcia -- 2020/05, last update: 2022/03/30 72 | % website: www.BiomeCardio.com 74 | """ 75 | noWidth = False 76 | 77 | #GB: Change the arguments names... this is nonpythonic, but keeping consistent with matlab implementation 78 | if param is None: 79 | param = width 80 | noWidth = True 81 | 82 | 83 | assert isinstance(siz, int) or len(siz)==1 or len(siz)==2,'SIZ must be [M,N] or M.' 84 | if isinstance(siz, int): 85 | siz = np.array([siz, siz]) 86 | 87 | assert np.all(siz>0) and np.issubdtype(siz.dtype, np.integer), 'SIZ components must be positive integers.' 88 | 89 | assert np.isscalar(zmax) and zmax>0, 'ZMAX must be a positive scalar.' 90 | 91 | assert isinstance(param, utils.Param),'PARAM must be a structure.' 92 | 93 | #%-- Pitch (in m) 94 | if not utils.isfield(param,'pitch'): 95 | raise ValueError('A pitch value (PARAM.pitch) is required.') 96 | p = param.pitch 97 | 98 | #%-- Number of elements 99 | if utils.isfield(param,'Nelements'): 100 | N = param.Nelements 101 | else: 102 | raise ValueError('The number of elements (PARAM.Nelements) is required.') 103 | 104 | 105 | #%-- Radius of curvature (in m) 106 | #% for a convex array 107 | if not utils.isfield(param,'radius'): 108 | param.radius = np.inf #% default = linear array 109 | 110 | R = param.radius 111 | isLINEAR = np.isinf(R) 112 | 113 | if not isLINEAR and not noWidth: 114 | logging.warning('MUST:impolgrid', 'The parameter WIDTH is ignored with a convex array.') 115 | 116 | #%-- Origo (x0,z0) 117 | #% x0 = 0; 118 | if isLINEAR: 119 | L = (N-1)*p# % array width 120 | #% z0 = -L/2*(1+cos(width))/sin(width); % (old version) 121 | z0 = 0 122 | else: 123 | L = 2*R*np.sin(np.arcsin(p/2/R)*(N-1)) # % chord length 124 | d = np.sqrt(R**2-L**2/4) # % apothem 125 | #% https://en.wikipedia.org/wiki/Circular_segment 126 | z0 = -d 127 | 128 | 129 | #%-- Image polar grid 130 | if isLINEAR: 131 | R = np.hypot(L/2,z0) 132 | th,r = np.meshgrid( 133 | np.linspace(width/2,-width/2,siz[1])+np.pi/2, 134 | np.linspace(R+p,-z0+zmax,siz[0])) 135 | x,z = pol2cart(th,r) 136 | else: 137 | th,r = np.meshgrid( 138 | np.linspace(np.arctan2(L/2,d),np.arctan2(-L/2,d),siz[1])+np.pi/2, 139 | np.linspace(R+p,-z0+zmax,siz[0])) 140 | x,z = pol2cart(th,r) 141 | 142 | z = z+z0 143 | return x, z 144 | 145 | def pol2cart(th, r): 146 | return r*np.cos(th), r*np.sin(th) 147 | -------------------------------------------------------------------------------- /src/pymust/iq2doppler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import numpy as np,scipy, scipy.signal, typing 3 | from . import utils 4 | def iq2doppler(IQ: np.ndarray, param: utils.Param, M: typing.Union[int,np.ndarray] = 1, lag: int = 1) -> tuple[np.ndarray, np.ndarray]: 5 | """ 6 | %IQ2DOPPLER Convert I/Q data to color Doppler 7 | % VD = IQ2DOPPLER(IQ,PARAM) returns the Doppler velocities from the I/Q 8 | % time series using a slow-time autocorrelator. 9 | % 10 | % PARAM is a structure that must contain the following fields: 11 | % a) PARAM.fc: center frequency (in Hz, REQUIRED) 12 | % b) PARAM.c: longitudinal velocity (in m/s, default = 1540 m/s) 13 | % c) PARAM.PRF (in Hz) or PARAM.PRP (in s): 14 | % pulse repetition frequency or period (REQUIRED) 15 | % 16 | % VD = IQ2DOPPLER(IQ,PARAM,M): 17 | % - If M is of a two-component vector [M(1) M(2)], the output Doppler 18 | % velocity is estimated from the M(1)-by-M(2) neighborhood around the 19 | % corresponding pixel. 20 | % - If M is a scalar, then an M-by-M neighborhood is used. 21 | % - If M is empty, then M = 1. 22 | % 23 | % VD = IQ2DOPPLER(IQ,PARAM,M,LAG) uses a lag of value LAG in the 24 | % autocorrelator. By default, LAG = 1. 25 | % 26 | % [VD,VarD] = IQ2DOPPLER(...) also returns an estimated Doppler variance. 27 | % 28 | % Important note: 29 | % -------------- 30 | % IQ must be a 3-D complex array, where the real and imaginary parts 31 | % correspond to the in-phase and quadrature components, respectively. The 32 | % 3rd dimension corresponds to the slow-time axis. IQ2DOPPLER uses a full 33 | % ensemble length to perform the auto-correlation, i.e. ensemble length 34 | % (or packet size) = size(IQ,3). 35 | % 36 | % 37 | % REFERENCE 38 | % --------- 39 | % If you find this function useful, you can cite the following paper. 40 | % Key references are included in the text of the function. 41 | % 42 | % 1) Madiena C, Faurie J, Porée J, Garcia D, Color and vector flow 43 | % imaging in parallel ultrasound with sub-Nyquist sampling. IEEE Trans 44 | % Ultrason Ferroelectr Freq Control, 2018;65:795-802. 45 | % download PDF 47 | % 48 | % 49 | % This function is part of MUST (Matlab UltraSound Toolbox). 50 | % MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later 51 | % 52 | % See also RF2IQ, WFILT. 53 | % 54 | % -- Damien Garcia & Jonathan Porée -- 2015/01, last update: 2020/06/24 55 | % website: www.BiomeCardio.com 57 | """ 58 | 59 | if isinstance(M, int): 60 | M = M *np.ones(2, dtype = int) 61 | 62 | assert np.all(M>0) and M.dtype == int, 'M must contain integers >0' 63 | #%- 64 | if len(IQ.shape)==4: 65 | raise ValueError('IQ is a 4-D array: use IQ2DOPPLER3.') 66 | 67 | assert len(IQ.shape)==3,'IQ must be a 3-D array' 68 | #%- 69 | 70 | assert isinstance(lag,int) and lag>0, 'The 4th input parameter LAG must be a positive integer' 71 | 72 | #%----- Input parameters in PARAM ----- 73 | #%-- 1) Speed of sound 74 | assert isinstance(param, utils.Param), 'param should be a Param class' 75 | if not utils. isfield(param,'c'): 76 | param.c = 1540 # % longitudinal velocity in m/s 77 | 78 | c = param.c 79 | #%-- 2) Center frequency 80 | if utils.isfield(param,'fc'): 81 | fc = param.fc 82 | else: 83 | raise ValueError('A center frequency (fc) must be specified in the structure PARAM: PARAM.fc') 84 | 85 | #%-- 3) Pulse repetition frequency or period (PRF or PRP) 86 | if utils.isfield(param,'PRF'): 87 | PRF = param.PRF 88 | elif utils.isfield(param,'PRP'): 89 | PRF = 1./param.PRP 90 | else: 91 | raise ValueError('A pulse repetition frequency or period must be specified in the structure PARAM: PARAM.PRF or PARAM.PRP') 92 | 93 | if utils.isfield(param,'PRP') and utils.isfield(param,'PRF'): 94 | assert abs(param.PRF-1./param.PRP)= 0. 68 | % See "Note on BAFFLE property" in PFIELD for details 69 | % 70 | % *** MEDIUM PARAMETERS *** 71 | % 7) PARAM.c: longitudinal velocity (in m/s, default = 1540 m/s) 72 | % 8) PARAM.attenuation: attenuation coefficient (dB/cm/MHz, default: 0) 73 | % Notes: A linear frequency-dependence is assumed. 74 | % A typical value for soft tissues is ~0.5 dB/cm/MHz. 75 | % 76 | % *** TRANSMIT PARAMETERS *** 77 | % 9) PARAM.TXapodization: transmision apodization (default: no apodization) 78 | % 10) PARAM.TXnow: pulse length in number of wavelengths (default: 1) 79 | % Use PARAM.TXnow = Inf for a mono-harmonic signal. 80 | % 11) PARAM.TXfreqsweep: frequency sweep for a linear chirp (default: []) 81 | % To be used to simulate a linear TX chirp. 82 | % See "Note on CHIRP signals" in PFIELD for details 83 | % 84 | % Other syntaxes: 85 | % -------------- 86 | % F = MKMOVIE(X,Z,RC,DELAYS,PARAM) also simulates backscattered echoes. 87 | % The scatterers are characterized by their coordinates (X,Z) and 88 | % reflection coefficients RC. X, Z and RC must be of same size. 89 | % 90 | % [F,INFO] = MKMOVIE(...) returns image information in the structure 91 | % INFO. INFO.Xgrid and INFO.Zgrid are the x- and z-coordinates of the 92 | % image. INFO.TimeStep is the time step between two consecutive frames. 93 | % 94 | % [F,INFO,PARAM] = MKMOVIE(...) updates the fields of the PARAM 95 | % structure. 96 | % 97 | % 98 | % OPTIONS: 99 | % ------- 100 | % %-- FREQUENCY SAMPLES --% 101 | % 1) Only frequency components of the transmitted signal in the range 102 | % [0,2fc] with significant amplitude are considered. The default 103 | % relative amplitude is -60 dB in MKMOVIE. You can change this value 104 | % by using the following: 105 | % [...] = MKMOVIE(...,OPTIONS), 106 | % where OPTIONS.dBThresh is the threshold in dB (default = -60). 107 | % --- 108 | % %-- FULL-FREQUENCY DIRECTIVITY --% 109 | % 2) By default, the directivity of the elements depends only on the 110 | % center frequency. This makes the calculation faster. To make the 111 | % directivities fully frequency-dependent, use: 112 | % [...] = MKMOVIE(...,OPTIONS), 113 | % with OPTIONS.FullFrequencyDirectivity = true (default = false). 114 | % --- 115 | % %-- ELEMENT SPLITTING --% 116 | % 3) Each transducer element of the array is split into small segments. 117 | % The length of these small segments must be small enough to ensure 118 | % that the far-field model is accurate. By default, the elements are 119 | % split into M segments, with M being defined by: 120 | % M = ceil(element_width/smallest_wavelength); 121 | % To modify the number M of subelements by splitting, you may adjust 122 | % OPTIONS.ElementSplitting. For example, OPTIONS.ElementSplitting = 1 123 | % --- 124 | % %-- WAIT BAR --% 125 | % 4) If OPTIONS.WaitBar is true, a wait bar appears (only if the number 126 | % of frequency samples >10). Default is true. 127 | % --- 128 | % 129 | % CREATE an animated GIF: 130 | % ---------------------- 131 | % [...] = MKMOVIE(...,FILENAME) creates a 10-fps animated GIF to the file 132 | % specified by FILENAME. The duration of the animated GIF is ~15 seconds. 133 | % You can modify the duration and fps by using PARAM.movie (see above). 134 | % 135 | % Example #1: a diverging wave from a phased-array transducer 136 | % ---------- 137 | % % Phased-array @ 2.7 MHz: 138 | % param = getparam('P4-2v'); 139 | % % TX time delays for a 90-degree wide diverging wave: 140 | % dels = txdelay(param,0,pi/2); 141 | % % Scatterers' position: 142 | % n = 20; 143 | % x = rand(n,1)*8e-2-4e-2; 144 | % z = rand(n,1)*10e-2; 145 | % % Backscattering coefficient 146 | % RC = (rand(n,1)+1)/2; 147 | % % Image size (in cm) 148 | % param.movie = [8 10]; 149 | % % Movie frames 150 | % [F,info] = mkmovie(x,z,RC,dels,param); 151 | % % Check the movie frames 152 | % figure 153 | % colormap([1-hot(128); hot(128)]); 154 | % for k = 1:size(F,3) 155 | % image(info.Xgrid,info.Zgrid,F(:,:,k)) 156 | % hold on 157 | % scatter(x,z,5,'w','filled') 158 | % hold off 159 | % axis equal off 160 | % title([int2str(info.TimeStep*k*1e6) ' \mus']) 161 | % drawnow 162 | % end 163 | % 164 | % Example #2: an animated GIF of a focused wave 165 | % ---------- 166 | % % Phased-array @ 2.7 MHz 167 | % param = getparam('P4-2v'); 168 | % % Focus location at xf = 0 cm, zf = 3 cm 169 | % xf = 0; zf = 3e-2; % focus position (in m) 170 | % % Transmit time delays (in s) 171 | % txdel = txdelay(xf,zf,param); % in s 172 | % % Define the image size (in cm) and its resolution (in pix/cm) 173 | % param.movie = [3 6 100]; 174 | % % Create an animated GIF 175 | % mkmovie(txdel,param,'focused_wave.gif'); 176 | % % Open the GIF in your browser 177 | % web('focused_wave.gif') 178 | % 179 | % This function is part of MUST (Matlab UltraSound Toolbox). 181 | % MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later 182 | % 183 | % See also PFIELD, SIMUS, TXDELAY. 184 | % 185 | % -- Damien Garcia -- 2017/10, last update 2023/02/01 186 | % website: www.BiomeCardio.com 188 | """ 189 | nargin = len(varargin) 190 | assert 2 <= nargin <= 7, "Wrong number of input arguments." 191 | 192 | if isinstance(varargin[-1],str): 193 | gifname = varargin[-1]; 194 | isGIF = True; 195 | Nargin = nargin-1; 196 | else: 197 | isGIF = False; 198 | Nargin = nargin; 199 | 200 | if Nargin == 2:# %#mkmovie(delaysTX,param) 201 | delaysTX = varargin[0] 202 | param = varargin[1] 203 | x = []; 204 | z = []; 205 | RC = []; 206 | options = utils.Options() 207 | elif Nargin == 3: #% mkmovie(delaysTX,param,options) 208 | delaysTX = varargin[0] 209 | param = varargin[1] 210 | options = varargin[2] 211 | x = []; 212 | z = []; 213 | RC = []; 214 | elif Nargin == 5: #mkmovie(x,z,RC,delaysTX,param) 215 | x = varargin[0] 216 | z = varargin[1] 217 | RC = varargin[2] 218 | delaysTX = varargin[3] 219 | param = varargin[4] 220 | options = utils.Options() 221 | elif Nargin == 6: #% mkmovie(x,z,RC,delaysTX,param,options) 222 | x = varargin[0] 223 | z = varargin[1] 224 | RC = varargin[2] 225 | delaysTX = varargin[3] 226 | param = varargin[4] 227 | options = varargin[5] 228 | else: 229 | raise ValueError('Wrong input arguments') 230 | 231 | param = param.ignoreCaseInFieldNames() 232 | options = options.ignoreCaseInFieldNames() 233 | options.CallFun = 'mkmovie' 234 | 235 | 236 | #%------------------------% 237 | #% CHECK THE INPUT SYNTAX % 238 | #%------------------------% 239 | 240 | assert isinstance(param, utils.Param),'The structure PARAM is required.' 241 | if not utils.isEmpty(x) or not utils.isEmpty(z) or not utils.isEmpty(RC): 242 | assert np.array_equal(x.shape, z.shape) and np.array_equal(x.shape, RC.shape),'X, Z and RC must be of same size.' 243 | 244 | #%-- Check if syntax errors may appear when using PFIELD 245 | opt = options.copy() 246 | pfield(None, None, None, delaysTX,param,opt) 247 | 248 | #%-- Movie properties 249 | NumberOfElements = param.Nelements; # number of array elements 250 | L = param.pitch*(NumberOfElements-1) 251 | if not 'movie' in param: 252 | # NOTE: width and height are in cm 253 | param.movie = np.array([200*L, 200*L, 50, 15, 10]) # default 254 | else: 255 | assert utils.isnumeric(param.movie) and len(param.movie)>1 and len(param.movie)<6 and np.all(param.movie>0), 'PARAM.movie must contain two to five positive parameters.' 256 | 257 | 258 | #% default resolution = 50 pix/cm, default duration = 15 s, default fps = 10 259 | paramMOVdefault = np.array([np.nan,np.nan,50, 15, 10]) 260 | n = len(param.movie) 261 | param.movie = np.concatenate([param.movie, paramMOVdefault[n:]] ) 262 | 263 | 264 | #%-- dB threshold (i.e. faster computation if lower) 265 | if not utils.isfield(options,'dBThresh') or not utils.isEmpty(options.dBThresh): 266 | options.dBThresh = -60 # default is -60dB in MKMOVIE 267 | assert np.isscalar(options.dBThresh) and utils.isnumeric(options.dBThresh) and options.dBThresh<0,'OPTIONS.dBThresh must be a negative scalar.' 268 | 269 | #-- Frequency step (scaling factor) 270 | # The frequency step is determined automatically. It is tuned to avoid 271 | # aliasing in the temporal domain. The frequency step can be adjusted by 272 | # using a scaling factor. For a smoother result, you may use a scaling 273 | # factor<1. 274 | if not utils.isfield(options,'FrequencyStep'): 275 | options.FrequencyStep = 1 276 | assert np.isscalar(options.FrequencyStep) and \ 277 | utils.isnumeric(options.FrequencyStep) and options.FrequencyStep>0, \ 278 | 'OPTIONS.FrequencyStep must be a positive scalar.' 279 | 280 | if options.FrequencyStep>1: 281 | logging.warning('MUST:FrequencyStep \n OPTIONS.FrequencyStep is >1: aliasing may be present!') 282 | 283 | 284 | #%-------------------------------% 285 | #% end of CHECK THE INPUT SYNTAX % 286 | #%-------------------------------% 287 | 288 | 289 | #%-- Image grid 290 | ROIwidth = param.movie[0]*1e-2; # image width (in m) 291 | ROIheight = param.movie[1]*1e-2; # image height (in m) 292 | pixsize = 1/param.movie[2]*1e-2; #% pixel size (in m) 293 | xi = np.arange(pixsize/2,ROIwidth + pixsize/2, pixsize); 294 | zi = np.arange(pixsize/2,ROIheight + pixsize/2, pixsize); 295 | xi, zi = np.meshgrid(xi-np.mean(xi), zi); 296 | 297 | 298 | #%-- Frequency sampling 299 | maxD = np.hypot((ROIwidth+L)/2,ROIheight); # maximum travel distance 300 | df = 1/2/(maxD/param.c); 301 | df = df*options.FrequencyStep; 302 | Nf = int(2*np.ceil(param.fc/df)+1); #% number of frequency samples 303 | 304 | 305 | #%-- Run PFIELD to calculate the RF spectra 306 | SPECT = np.zeros([Nf,np.prod(xi.shape)], dtype = np.complex64); # will contain the RF spectra 307 | options.FrequencyStep = df; 308 | options.ElementSplitting = 1; 309 | options.RC = RC; 310 | options.x = x; 311 | options.z = z; 312 | #%- 313 | #% we need idx... 314 | opt = options.copy(); 315 | opt.x = []; 316 | opt.z = []; 317 | opt.RC = []; 318 | opt.computeIndices = True 319 | _,_,idx = pfield([],[], [], delaysTX,param,opt); 320 | #- 321 | _, SPECT[idx,:], _ = pfield(xi, [], zi,delaysTX,param,options); 322 | 323 | #-- IFFT to recover the time-resolved signals 324 | #% 325 | #F = SPECT #; clear SPECT 326 | F = SPECT 327 | F = F.reshape([Nf, xi.shape[0], xi.shape[1]]) #reshape(F,Nf,size(xi,1),size(xi,2)); 328 | F = utils.shiftdim(F,1) 329 | 330 | #F = np.concatenate([F, 331 | # np.conj(np.flip(F[:,:,2:-1],3)) 332 | # ], 333 | # axis = 2 334 | # ); 335 | 336 | 337 | #% 338 | F = np.fft.irfft(F, axis = 2); # Note GB: not Sure if you need the expanded version covering positive and negative, or the single spectra is enoguh 339 | #% 340 | 341 | #% 342 | F = np.flip(F,2) 343 | F = F[:,:,:int(np.round(F.shape[2]//2))]; 344 | F = F/np.max(np.abs(F)) 345 | 346 | 347 | if utils.isfield(param,'gamma') and param.gamma!=1: # % gamma compression 348 | F = np.power(np.abs(F), param.gamma*np.sign(F)) 349 | F = ((F+1)/2*255).astype(np.uint8); 350 | #% 351 | 352 | #%-- some information about the movie 353 | info = utils.dotdict() 354 | info.Xgrid = xi[0, :] # % in m 355 | info.Zgrid = zi[:, 0] # % in m 356 | info.TimeStep = maxD/param.c/F.shape[2] #% in s 357 | 358 | #%-- animated GIF 359 | if isGIF: 360 | plotScatters = options.get('plotScatterers', False) 361 | # Cretae the colormap, see https://matplotlib.org/3.1.0/tutorials/colors/colormap-manipulation.html 362 | N = 256 363 | vals = np.ones((N, 4)) 364 | vals[128:, :] = [plt.cm.hot(0+i * 2) for i in range(128) ] 365 | vals[:128, :] = [plt.cm.hot(1 + i * 2) for i in range(128) ] 366 | vals[:128, :3] = 1 - vals[:128, :3] 367 | newcmp = matplotlib.colors.ListedColormap(vals) 368 | #%- the matrix f contains the scatterers 369 | f = np.zeros(F.shape[:2]) 370 | if not utils.isEmpty(x): 371 | maxRC = 1; # max(RC(:)); 372 | for k,_ in enumerate(x): 373 | i = np.argmin(np.abs(zi[:,0]-z[k])) 374 | j = np.argmin(np.abs(xi[:,0]-x[k])) 375 | f[i,j] = RC[k]/maxRC 376 | 377 | n = int(2*np.round(param.movie[2]/10)+1) 378 | window1d = np.abs(np.blackman(n)) 379 | window2d = np.sqrt(np.outer(window1d,window1d)) 380 | f = scipy.signal.convolve2d(f,window2d,'same') 381 | 382 | f = (f*128).astype(np.uint8) 383 | 384 | # NOTE GB: put it later somehow 385 | #%- add the signature 386 | #% Please do not remove it 387 | #if f.shape>37 && size(f,2)>147 388 | # f(end-36:end-5,6:147) = Signature/2; 389 | #else 390 | # f = 0; 391 | #end 392 | 393 | #map = np.flipud([1-hot(128); hot(128)]); 394 | 395 | #%- create the GIF movie 396 | 397 | # Define the colormap 398 | vals = np.array([matplotlib.cm.hot(2*i) for i in range(127)] + [matplotlib.cm.hot(2*i) for i in range(128)]) 399 | vals[:127, :3] = 1 - vals[:127, :3] 400 | cm2 = matplotlib.colors.LinearSegmentedColormap.from_list('hot2',vals) 401 | 402 | 403 | 404 | Tmov = param.movie[3]# % movie duration in s 405 | fps = param.movie[4] # % frame per second 406 | 407 | nk = int(np.round(F.shape[2]/(Tmov*fps))) #; % increment 408 | ks = np.arange(0,F.shape[2],nk) 409 | 410 | interactive = matplotlib.is_interactive() 411 | if interactive: 412 | plt.ioff() 413 | 414 | fig,ax = plt.subplots() 415 | 416 | def animate(i): 417 | ax.clear() 418 | im = ax.imshow(F[:, :,ks[i]], cmap = cm2, extent=[info.Xgrid[0]*1e2,info.Xgrid[-1]*1e2,info.Zgrid[-1]*1e2,info.Zgrid[0]*1e2]) 419 | if plotScatters: 420 | dx = info.Xgrid[1] - info.Xgrid[0] 421 | dz = info.Zgrid[1] - info.Zgrid[0] 422 | ax.scatter(x/dx,z/dz, s = 5, c = 'w', marker = 'o', facecolors = 'none') 423 | 424 | im.set_clim(0,255) 425 | plt.xlabel('x (cm)') 426 | plt.ylabel('z (cm)') 427 | return im, 428 | 429 | ani = FuncAnimation(fig, animate, blit=True, repeat=False, frames=len(ks)) 430 | ani.save(gifname, dpi=300, writer=PillowWriter(fps=fps)) 431 | plt.close(fig) 432 | if interactive: 433 | plt.ion() 434 | 435 | return F,info,param -------------------------------------------------------------------------------- /src/pymust/rf2iq.py: -------------------------------------------------------------------------------- 1 | import numpy as np, scipy, scipy.signal, logging 2 | from . import utils 3 | from typing import Union 4 | 5 | def rf2iq(RF: np.ndarray, Fs: Union[float, utils.Param], Fc: float = None, B: float = None) -> np.ndarray: 6 | """ 7 | %RF2IQ I/Q demodulation of RF data 8 | % IQ = RF2IQ(RF,Fs,Fc) demodulates the radiofrequency (RF) bandpass 9 | % signals and returns the Inphase/Quadrature (I/Q) components. IQ is a 10 | % complex whose real (imaginary) part contains the inphase (quadrature) 11 | % component. 12 | % 1) Fs is the sampling frequency of the RF signals (in Hz), 13 | % 2) Fc represents the center frequency (in Hz). 14 | % 15 | % IQ = RF2IQ(RF,Fs) or IQ = RF2IQ(RF,Fs,[],...) calculates the carrier 16 | % frequency. 17 | % IMPORTANT: Fc must be given if the RF signal is undersampled (as in 18 | % bandpass sampling). 19 | % 20 | % [IQ,Fc] = RF2IQ(...) also returns the carrier frequency (in Hz). 21 | % 22 | % RF2IQ uses a downmixing process followed by low-pass filtering. The 23 | % low-pass filter is determined by the normalized cut-off frequency Wn. 24 | % By default Wn = min(2*Fc/Fs,0.5). The cut-off frequency Wn can be 25 | % adjusted if the relative bandwidth (in %) is given: 26 | % 27 | % IQ = RF2IQ(RF,Fs,Fc,B) 28 | % 29 | % The bandwidth in % is defined by: 30 | % B = Bandwidth_in_% = Bandwidth_in_Hz*(100/Fc). 31 | % When B is an input variable, the cut-off frequency is 32 | % Wn = Bandwidth_in_Hz/Fs, i.e: 33 | % Wn = B*(Fc/100)/Fs. 34 | % 35 | % If there is a time offset, use PARAM.t0, as explained below. 36 | % 37 | % An alternative syntax for RF2IQ is the following: 38 | % IQ = RF2IQ(RF,PARAM), where the structure PARAM must contain the 39 | % required parameters: 40 | % 1) PARAM.fs: sampling frequency (in Hz, REQUIRED) 41 | % 2) PARAM.fc: center frequency (in Hz, OPTIONAL, required for 42 | % undersampled RF signals) 43 | % 3) PARAM.bandwidth: fractional bandwidth (in %, OPTIONAL) 44 | % 4) PARAM.t0: time offset (in s, OPTIONAL, default = 0) 45 | % 46 | % Notes on Undersampling (sub-Nyquist sampling) 47 | % ---------------------- 48 | % If the RF signal is undersampled, the carrier frequency Fc must be 49 | % specified. If a fractional bandwidth (B or PARAM.bandwidth) is given, a 50 | % warning message appears if harmful aliasing is suspected. 51 | % 52 | % Notes: 53 | % ----- 54 | % RF2IQ treats the data along the first non-singleton dimension as 55 | % vectors, i.e. RF2IQ demodulates along columns for 2-D and 3-D RF data. 56 | % Each column corresponds to a single RF signal over (fast-) time. 57 | % Use IQ2RF to recover the RF signals. 58 | % 59 | % Method: 60 | % ------ 61 | % RF2IQ multiplies RF by a phasor of frequency Fc (down-mixing) and 62 | % applies a fifth-order Butterworth lowpass filter using FILTFILT: 63 | % IQ = RF.*exp(-1i*2*pi*Fc*t); 64 | % [b,a] = butter(5,2*Fc/Fs); 65 | % IQ = filtfilt(b,a,IQ)*2; 66 | % 67 | % 68 | % Example #1: Envelope of an RF signal 69 | % ---------- 70 | % % Load an RF signal sampled at 20 MHz 71 | % load RFsignal@20MHz.mat 72 | % % I/Q demodulation 73 | % IQ = rf2iq(RF,20e6); 74 | % % RF signal and its envelope 75 | % plot(RF), hold on 76 | % plot(abs(IQ),'Linewidth',1.5), hold off 77 | % legend({'RF signal','I/Q amplitude'}) 78 | % 79 | % Example #2: Demodulation of an undersampled RF signal 80 | % ---------- 81 | % % Load an RF signal sampled at 20 MHz 82 | % % (Center frequency = 5 MHz / Bandwidth = 2 MHz) 83 | % load RFsignal@20MHz.mat 84 | % % I/Q demodulation of the original RF signal 85 | % Fs = 20e6; 86 | % IQ = rf2iq(RF,Fs); 87 | % % Create an undersampled RF signal (sampling at Fs/5 = 4 MHz) 88 | % bpsRF = RF(1:5:end); 89 | % subplot(211), plot(1:1000,RF,1:5:1000,bpsRF,'.-') 90 | % title('RF signal (5 MHz array)') 91 | % legend({'sampled @ 20 MHz','bandpass sampled @ 4 MHz'}) 92 | % % I/Q demodulation of the undersampled RF signal 93 | % Fs = 4e6; Fc = 5e6; 94 | % iq = rf2iq(bpsRF,Fs,Fc); 95 | % % Display the IQ signals 96 | % subplot(212), plot(1:1000,abs(IQ),1:5:1000,abs(iq),'.-') 97 | % title('I/Q amplitude') 98 | % legend({'sampled @ 20 MHz','bandpass sampled @ 4 MHz'}) 99 | % 100 | % 101 | % REFERENCE 102 | % --------- 103 | % If you find this function useful, you can cite the following paper. 104 | % 105 | % 1) Madiena C, Faurie J, Porée J, Garcia D, Color and vector flow 106 | % imaging in parallel ultrasound with sub-Nyquist sampling. IEEE Trans 107 | % Ultrason Ferroelectr Freq Control, 2018;65:795-802. 108 | % download PDF 110 | % 111 | % 112 | % This function is part of MUST (Matlab UltraSound Toolbox). 113 | % MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later 114 | % 115 | % See also IQ2DOPPLER, IQ2RF, BMODE, WFILT. 116 | % 117 | % -- Damien Garcia -- 2012/01, last update: 2020/05 118 | % website: www.BiomeCardio.com 120 | """ 121 | 122 | #%-- Check input arguments 123 | assert np.issubdtype(RF.dtype, np.floating),'RF must contain real RF signals.' 124 | t0 = 0; #% default value for time offset 125 | if isinstance(Fs, utils.Param): 126 | param = Fs 127 | param.ignoreCaseInFieldNames() 128 | assert utils.isfield(param,'fs'), 'A sampling frequency (PARAM.fs) is required.' 129 | Fs = param.fs 130 | B = param.get('bandwidth', None) 131 | Fc = param.get('fc', None) 132 | t0 = param.get('t0', np.zeros((1))) 133 | 134 | 135 | assert np.isscalar(Fs), 'The sampling frequency (Fs or PARAM.fs) must be a scalar.' 136 | assert Fc is None or np.isscalar(Fc), 'The center frequency (Fc or PARAM.fc) must be None or a scalar.' 137 | 138 | #%-- Convert to column vector (if RF is a row vector) 139 | 140 | #%-- Time vector 141 | nl = RF.shape[0] 142 | t = np.arange(nl)/Fs 143 | if isinstance(t0, float): 144 | t0 = np.ones((1))*t0 145 | assert utils.isnumeric(t0) and np.isscalar(t0) or isinstance(t0, np.ndarray) and (len(t0)==1 or len(t0)==nl), 'PARAM.t0 must be a numeric scalar or vector of size = size(RF,1).' 146 | t = t+t0 147 | 148 | #%-- Seek the carrier frequency (if required) 149 | if Fc is None: 150 | #% Keep a maximum of 100 randomly selected scanlines 151 | Nc = RF.shape[1] 152 | if Nc<100: 153 | idx = np.arange(Nc) 154 | else: 155 | idx = np.random.permutation(Nc)[:100] 156 | #% Power Spectrum 157 | P = np.linalg.norm(np.fft.rfft(RF[:,idx], axis = 0),axis =1) 158 | freqs = np.fft.rfftfreq(RF.shape[0],1/Fs) 159 | #% Carrier frequency 160 | Fc = np.sum(freqs*P)/np.sum(P) 161 | 162 | #%-- Normalized cut-off frequency 163 | if B is None: 164 | Wn = min(2*Fc/Fs,0.5) 165 | else: 166 | assert np.isscalar(B), 'The signal bandwidth (B or PARAM.bandwidth) must be a scalar.' 167 | assert B>0 and B<200, 'The signal bandwidth (B or PARAM.bandwidth, in %) must be within the interval of ]0,200[.' 168 | B = Fc*B/100 #; % bandwidth in Hz 169 | Wn = B/Fs 170 | 171 | assert Wn>0 and Wn<=1,'The normalized cutoff frequency is not within the interval of (0,1). Check the input parameters!' 172 | 173 | #%-- Down-mixing of the RF signals 174 | exponential = np.exp(-1j*2*np.pi*Fc*t) 175 | exponential = exponential.reshape( [-1] + [1 for _ in range(RF.ndim-1)]) 176 | IQ =exponential*RF 177 | 178 | 179 | # %-- Low-pass filter 180 | b,a = scipy.signal.butter(5,Wn) 181 | IQ = scipy.signal.filtfilt(b,a,IQ, axis = 0)*2; #% factor 2: to preserve the envelope amplitude 182 | 183 | #%-- Recover the initial size (if was a vector row) 184 | #if wasrow: 185 | # IQ = IQ.T # end 186 | 187 | #%-- Display a warning message if harmful aliasing is suspected 188 | if B is not None and Fs<(2*Fc+B): #% the RF signal is undersampled 189 | fL = Fc-B/2; fH = Fc+B/2; #% lower and higher frequencies of the bandpass signal 190 | n = int(np.floor(fH/(fH-fL))) 191 | harmlessAliasing = np.any(np.logical_and(2*fH/np.arange(1,n+1) <=Fs, Fs<=2*fL/(np.arange(n) +1e-10))) 192 | if not harmlessAliasing: 193 | logging.warning('RF2IQ:harmfulAliasing: Harmful aliasing is present: the aliases are not mutually exclusive!') 194 | return IQ -------------------------------------------------------------------------------- /src/pymust/simus.py: -------------------------------------------------------------------------------- 1 | from . import utils, pfield, getpulse 2 | import logging, copy, multiprocessing, functools 3 | import numpy as np 4 | 5 | # pfield wrapper so it is compatible with multiprocessing. Needs to be defined in a global scope 6 | def pfieldParallel(x: np.ndarray, y: np.ndarray, z: np.ndarray, RC: np.ndarray, delaysTX: np.ndarray, param: utils.Param, options: utils.Options): 7 | options = options.copy() 8 | options.ParPool = False # No parallel within the parallel 9 | options.RC = RC 10 | _, RFsp, idx = pfield(x, y, z, delaysTX, param, options) 11 | return RFsp, idx 12 | 13 | 14 | def simus(*varargin): 15 | """ 16 | %SIMUS Simulation of ultrasound RF signals for a linear or convex array 17 | % RF = SIMUS(X,Y,Z,RC,DELAYS,PARAM) simulates ultrasound RF radio- 18 | % frequency signals generated by an ultrasound uniform LINEAR or CONVEX 19 | % array insonifying a medium of scatterers. 20 | % The scatterers are characterized by their coordinates (X,Y,Z) and 21 | % reflection coefficients RC. 22 | % 23 | % X, Y, Z and RC must be of same size. The elements of the ULA are 24 | % excited at different time delays, given by the vector DELAYS. The 25 | % transmission and reception characteristics must be given in the 26 | % structure PARAM (see below for details). 27 | % 28 | % RF = SIMUS(X,Z,RC,DELAYS,PARAM) or RF = SIMUS(X,[],Z,RC,DELAYS,PARAM) 29 | % disregards elevation focusing (PARAM.focus is ignored) and assumes that 30 | % Y=0 (2-D space). The computation is faster in 2-D. 31 | % 32 | % >--- Try it: enter "simus" in the command window for an example ---< 33 | % 34 | % The RF output matrix contains Number_of_Elements columns. Each column 35 | % therefore represents an RF signal. The number of rows depends on the 36 | % depth (estimated from max(Z)) and the sampling frequency PARAM.fs (see 37 | % below). By default, the sampling frequency is four times the center 38 | % frequency. 39 | % 40 | % Units: X,Y,Z must be in m; DELAYS must be in s; RC has no unit. 41 | % 42 | % DELAYS can also be a matrix. This alternative can be used to simulate 43 | % MLT (multi-line transmit) sequences. In this case, each ROW represents 44 | % a series of delays. For example, to create a 4-MLT sequence with a 45 | % 64-element phased array, DELAYS matrix must have 4 rows and 64 columns 46 | % (size = [4 64]). 47 | % 48 | % SIMUS uses PFIELD during transmission and reception. The parameters 49 | % that must be included in the structure PARAM are similar as those in 50 | % PFIELD. Additional parameters are also required (see below). 51 | % 52 | % --- 53 | % NOTE #1: X-, Y-, and Z-axes 54 | % Conventional axes are used: 55 | % i) For a LINEAR array, the X-axis is PARALLEL to the transducer and 56 | % points from the first (leftmost) element to the last (rightmost) 57 | % element (X = 0 at the CENTER of the transducer). The Z-axis is 58 | % PERPENDICULAR to the transducer and points downward (Z = 0 at the 59 | % level of the transducer, Z increases as depth increases). The 60 | % Y-axis is such that the coordinates are right-handed. 61 | % ii) For a CONVEX array, the X-axis is parallel to the chord and Z = 0 62 | % at the level of the chord. 63 | % --- 64 | % NOTE #2: Simplified method: Directivity 65 | % By default, the calculation is made faster by assuming that the 66 | % directivity of the elements is dependent only on the central frequency. 67 | % This simplification very little affects the pressure field in most 68 | % situations (except near the array). To turn off this option, use 69 | % OPTIONS.FullFrequencyDirectivity = true. 70 | % (see ADVANCED OPTIONS below). 71 | % --- 72 | % 73 | % PARAM is a structure that contains the following fields: 74 | % ------------------------------------------------------- 75 | % *** TRANSDUCER PROPERTIES *** 76 | % 1) PARAM.fc: central frequency (in Hz, REQUIRED) 77 | % 2) PARAM.pitch: pitch of the array (in m, REQUIRED) 78 | % 3) PARAM.width: element width (in m, REQUIRED) 79 | % or PARAM.kerf: kerf width (in m, REQUIRED) 80 | % note: width = pitch-kerf 81 | % 4) PARAM.focus: elevation focus (in m, ignored if Y is not given) 82 | % The default is Inf (no elevation focusing) 83 | % 5) PARAM.height: element height (in m, ignored if Y is not given) 84 | % The default is Inf (no elevation focusing) 85 | % 6) PARAM.radius: radius of curvature (in m) 86 | % The default is Inf (rectilinear array) 87 | % 7) PARAM.bandwidth: pulse-echo 6dB fractional bandwidth (in %) 88 | % The default is 75%. 89 | % 8) PARAM.baffle: property of the baffle: 90 | % 'soft' (default), 'rigid' or a scalar > 0. 91 | % See "Note on BAFFLE properties" in "help pfield" 92 | % 93 | % *** MEDIUM PARAMETERS *** 94 | % 9) PARAM.c: longitudinal velocity (in m/s, default = 1540 m/s) 95 | % 10) PARAM.attenuation: attenuation coefficient (dB/cm/MHz, default: 0) 96 | % Notes: A linear frequency-dependence is assumed. 97 | % A typical value for soft tissues is ~0.5 dB/cm/MHz. 98 | % 99 | % *** TRANSMIT PARAMETERS *** 100 | % 11) PARAM.TXapodization: transmit apodization (default: no apodization) 101 | % 12) PARAM.TXnow: number of wavelengths of the TX pulse (default: 1) 102 | % 13) PARAM.TXfreqsweep: frequency sweep for a linear chirp (default: []) 103 | % To be used to simulate a linear TX chirp. 104 | % 105 | % *** RECEIVE PARAMETERS *** (not in PFIELD) 106 | % 14) PARAM.fs: sampling frequency (in Hz, default = 4*param.fc) 107 | % 15) PARAM.RXdelay: reception law delays (in s, default = 0) 108 | % 109 | % Other syntaxes: 110 | % -------------- 111 | % i} [RF,PARAM] = SIMUS(...) updates the fields of the PARAM structure. 112 | % ii} [...] = SIMUS without any input argument provides an interactive 113 | % example designed to produce RF signals from a focused ultrasound 114 | % beam using a 2.7 MHz phased-array transducer (without elevation 115 | % focusing). 116 | % 117 | % PARALLEL COMPUTING: 118 | % ------------------ 119 | % SIMUS calls the function PFIELD. If you have the Parallel Computing 120 | % Toolbox, SIMUS can execute several PFIELDs in parallel. If this option 121 | % is activated, a parallel pool is created on the default cluster. All 122 | % workers in the pool are used. The X and Z are splitted into NW chunks, 123 | % NW being the number of workers. To execute parallel computing, use: 124 | % [...] = SIMUS(...,OPTIONS), 125 | % with OPTIONS.ParPool = true (default = false). 126 | % 127 | % 128 | % OTHER OPTIONS: 129 | % ------------- 130 | % %-- FREQUENCY STEP & FREQUENCY SAMPLES --% 131 | % 1a) Only frequency components of the transmitted signal in the range 132 | % [0,2fc] with significant amplitude are considered. The default 133 | % relative amplitude is -100 dB. You can change this value by using 134 | % the following: 135 | % [...] = SIMUS(...,OPTIONS), 136 | % where OPTIONS.dBThresh is the threshold in dB (default = -100). 137 | % 1b) The frequency step is determined automatically to avoid aliasing in 138 | % the time domain. This step can be adjusted with a scaling factor 139 | % OPTIONS.FrequencyStep (default = 1). It is not recommended to 140 | % modify this scaling factor in SIMUS. 141 | % --- 142 | % %-- FULL-FREQUENCY DIRECTIVITY --% 143 | % 2) By default, the directivity of the elements depends only on the 144 | % center frequency. This makes the calculation faster. To make the 145 | % directivities fully frequency-dependent, use: 146 | % [...] = SIMUS(...,OPTIONS), 147 | % with OPTIONS.FullFrequencyDirectivity = true (default = false). 148 | % --- 149 | % %-- ELEMENT SPLITTING --% 150 | % 3) Each transducer element of the array is split into small segments. 151 | % The length of these small segments must be small enough to ensure 152 | % that the far-field model is accurate. By default, the elements are 153 | % split into M segments, with M being defined by: 154 | % M = ceil(element_width/smallest_wavelength); 155 | % To modify the number M of subelements by splitting, you may adjust 156 | % OPTIONS.ElementSplitting. For example, OPTIONS.ElementSplitting = 1 157 | % --- 158 | % %-- WAIT BAR --% 159 | % 4) If OPTIONS.WaitBar is true, a wait bar appears (only if the number 160 | % of frequency samples >10). Default is true. 161 | % --- 162 | % 163 | % 164 | % Notes regarding the model & REFERENCES: 165 | % -------------------------------------- 166 | % 1) SIMUS calls the function PFIELD. It works for uniform linear or 167 | % convex arrays. A uniform array has identical elements along a 168 | % rectilinear or curved line in space with uniform spacing. Each 169 | % element is split into small segments (if required). The radiation 170 | % patterns in the x-z plane are derived by using a Fraunhofer 171 | % (far-field) approximation. Those in the x-y elevational plane are 172 | % derived by using a Fresnel (paraxial) approximation. 173 | % 2) The paper that describes the first 2-D version of SIMUS is: 174 | % SHAHRIARI S, GARCIA D. Meshfree simulations of ultrasound vector 175 | % flow imaging using smoothed particle hydrodynamics. Phys Med Biol, 176 | % 2018;63:205011. PDF here 178 | % 3) The paper that describes the theory of the full (2-D + 3-D) version 179 | % of SIMUS will be submitted by February 2021. 180 | % 181 | % 182 | % A simple EXAMPLE: 183 | % ---------------- 184 | % %-- Generate RF signals using a phased-array transducer 185 | % % Phased-array @ 2.7 MHz: 186 | % param = getparam('P4-2v'); 187 | % % TX time delays: 188 | % x0 = 0; z0 = 3e-2; % focus location 189 | % dels = txdelay(x0,z0,param); 190 | % % Six scatterers: 191 | % x = zeros(1,6); y = zeros(1,6); 192 | % z = linspace(1,10,6)*1e-2; 193 | % % Reflectivity coefficients: 194 | % RC = ones(1,6); 195 | % % RF signals: 196 | % param.fs = 20*param.fc; % sampling frequency 197 | % RF = simus(x,y,z,RC,dels,param); 198 | % % Plot the RF signals 199 | % plot(bsxfun(@plus,RF(:,1:7:64)/max(RF(:)),(1:10))',... 200 | % (0:size(RF,1)-1)/param.fs*1e6,'k') 201 | % set(gca,'XTick',1:10,'XTickLabel',int2str((1:7:64)')) 202 | % title('RF signals') 203 | % xlabel('Element number'), ylabel('time (\mus)') 204 | % xlim([0 11]), axis ij 205 | % 206 | % 207 | % This function is part of MUST (Matlab UltraSound Toolbox). 209 | % MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later 210 | % 211 | % See also PFIELD, TXDELAY, MKMOVIE, GETPARAM, GETPULSE. 212 | % 213 | % -- Damien Garcia -- 2017/10, last update 2022/05/14 214 | % website: www.BiomeCardio.com 216 | """ 217 | 218 | returnTime = False #NoteGB: Set to True if you want to return the time, but quite a mess right now with the matlab style arguments 219 | 220 | nargin = len(varargin) 221 | if nargin<= 3 or nargin > 7: 222 | raise ValueError("Wrong number of input arguments.") 223 | #%-- Input variables: X,Y,Z,DELAYS,PARAM,OPTIONS 224 | x = varargin[0] 225 | 226 | if nargin ==5: # simus(X,Z,RC,DELAYS,PARAM) 227 | y = None 228 | z = varargin[1] 229 | RC = varargin[2] 230 | delaysTX = varargin[3] 231 | param = varargin[4] 232 | options = utils.Options() 233 | elif nargin == 6: # simus(X,Z,RC,DELAYS,PARAM,OPTIONS) 234 | if isinstance(varargin[4], utils.Param): #% simus(X,Z,RC,DELAYS,PARAM,OPTIONS) 235 | y = None 236 | z = varargin[1] 237 | RC = varargin[2] 238 | delaysTX = varargin[3] 239 | param = varargin[4] 240 | options = copy.deepcopy(varargin[5]) 241 | else: # % simus(X,Y,Z,RC,DELAYS,PARAM) 242 | y = varargin[1] 243 | z = varargin[2] 244 | RC = varargin[3] 245 | delaysTX = varargin[4] 246 | param = varargin[5] 247 | options = utils.Options() 248 | else: # simus(X,Y,Z,RC,DELAYS,PARAM,OPTIONS) 249 | y = varargin[1] 250 | z = varargin[2] 251 | RC = varargin[3] 252 | delaysTX = varargin[4] 253 | param = varargin[5] 254 | options = copy.deepcopy(varargin[6]) 255 | assert isinstance(param, utils.Param),'PARAM must be a structure.' 256 | 257 | #%-- Elevation focusing and X,Y,Z size 258 | if utils.isEmpty(y): 259 | ElevationFocusing = False 260 | assert x.shape == z.shape and x.shape == RC.shape, 'X, Z, and RC must be of same size.' 261 | else: 262 | ElevationFocusing = True 263 | assert x.shape == z.shape and x.shape == RC.shape and y.shape == x.shape, 'X, Y, Z, and RC must be of same size.' 264 | 265 | if len(x.shape) ==0: 266 | return np.array([]), np.array([]) 267 | 268 | 269 | #%------------------------% 270 | #% CHECK THE INPUT SYNTAX % 271 | #%------------------------% 272 | 273 | 274 | param = param.ignoreCaseInFieldNames() 275 | options = options.ignoreCaseInFieldNames() 276 | options.CallFun = 'simus' 277 | 278 | # GB TODO: wait bar + parallelisation 279 | #%-- Wait bar 280 | #if ~isfield(options,'WaitBar') 281 | # options.WaitBar = true; 282 | #end 283 | #assert(isscalar(options.WaitBar) && islogical(options.WaitBar),... 284 | # 'OPTIONS.WaitBar must be a logical scalar (true or false).') 285 | 286 | #%-- Parallel pool 287 | #if ~isfield(options,'ParPool') 288 | # options.ParPool = False 289 | #end 290 | 291 | #%-- Check if syntax errors may appear when using PFIELD 292 | #try: 293 | # opt = options 294 | # opt.ParPool = false; 295 | # opt.WaitBar = false; 296 | # [~,param] = pfield([],[],delaysTX,param,opt); 297 | #catch ME 298 | # throw(ME) 299 | #end 300 | 301 | #%-- Sampling frequency (in Hz) 302 | if not utils.isfield(param,'fs'): 303 | param.fs = 4*param.fc; #% default 304 | 305 | assert param.fs>=4*param.fc,'PARAM.fs must be >= 4*PARAM.fc.' 306 | 307 | NumberOfElements = param.Nelements # % number of array elements 308 | 309 | #%-- Receive delays (in s) 310 | if not utils.isfield(param,'RXdelay'): 311 | param.RXdelay = np.zeros((1,NumberOfElements), dtype = np.float32) 312 | else: 313 | assert isinstance(param.RXdelay, np.ndarray) and utils.isnumeric(param.RXdelay), 'PARAM.RXdelay must be a vector' 314 | assert param.RXdelay.shape[1] ==NumberOfElements, 'PARAM.RXdelay must be of length = (number of elements)' 315 | param.RXdelay = param.RXdelay.reshape((1,NumberOfElements)) 316 | 317 | #%-- dB threshold (in dB: faster computation if lower value) 318 | if not utils.isfield(options,'dBThresh'): 319 | options.dBThresh = -100; # % default is -100dB in SIMUS 320 | 321 | assert np.isscalar(options.dBThresh) and utils.isnumeric(options.dBThresh) and options.dBThresh<0,'OPTIONS.dBThresh must be a negative scalar.' 322 | 323 | #%-- Frequency step (scaling factor) 324 | #% The frequency step is determined automatically. It is tuned to avoid 325 | #% aliasing in the temporal domain. The frequency step can be adjusted by 326 | #% using a scaling factor. For a smoother result, you may use a scaling 327 | #% factor<1. 328 | if not utils.isfield(options,'FrequencyStep'): 329 | options.FrequencyStep = 1 330 | 331 | assert np.isscalar(options.FrequencyStep) and utils.isnumeric(options.FrequencyStep) and options.FrequencyStep>0, 'OPTIONS.FrequencyStep must be a positive scalar.' 332 | 333 | if options.FrequencyStep>1: 334 | logging.warning('MUST:FrequencyStep', 'OPTIONS.FrequencyStep is >1: aliasing may be present!') 335 | 336 | if not utils.isfield(param, 'c'): 337 | param.c = 1540 #default sound speed in soft tissue 338 | 339 | 340 | 341 | #%-------------------------------% 342 | # % end of CHECK THE INPUT SYNTAX % 343 | # %-------------------------------% 344 | 345 | #GB NOTE: same as in pfield, put in param ? 346 | #%-- Centers of the tranducer elements (x- and z-coordinates) 347 | xe, ze, THe, h= param.getElementPositions() 348 | 349 | #%-- Maximum distance 350 | d2 = (x.reshape((-1,1))-xe)**2+(z.reshape((-1,1))-ze)**2 351 | maxD = np.sqrt(np.max(d2)) #% maximum element-scatterer distance 352 | _, tp = getpulse.getpulse(param, 2) 353 | maxD = maxD + tp[-1] * param.c #add pulse length 354 | 355 | #%-- FREQUENCY SAMPLES 356 | valid_tx_delays = np.array([e for e in delaysTX.flatten() if not np.isnan(e)]) 357 | df = 1/2/(2*maxD/param.c + np.max(np.concat((valid_tx_delays,param.RXdelay.flatten())))) # % to avoid aliasing in the time domain 358 | # df = 1/2/(2*maxD/param.c + np.max(delaysTX.flatten() + param.RXdelay.flatten())) # % to avoid aliasing in the time domain 359 | df = df*options.FrequencyStep 360 | Nf = 2*int(np.ceil(param.fc/df))+1 # % number of frequency samples 361 | #%-- Run PFIELD to calculate the RF spectra 362 | RFspectrum = np.zeros((Nf,NumberOfElements), dtype = np.complex64)# % will contain the RF spectra 363 | options.FrequencyStep = df 364 | 365 | #%- run PFIELD in a parallel pool (NW workers) 366 | if options.get('ParPool', False): 367 | 368 | with options.getParallelPool() as pool: 369 | idx = options.getParallelSplitIndices(x.shape[1]) 370 | 371 | RS = pool.starmap(functools.partial(pfieldParallel, delaysTX = delaysTX, param = param, options = options), 372 | [ ( x[:,i:j], 373 | y[:,i:j] if not utils.isEmpty(y) else None, 374 | z[:,i:j], 375 | RC[:,i:j]) for i,j in idx ]) 376 | 377 | 378 | for (RFsp, idx_spectrum) in RS: 379 | RFspectrum[idx_spectrum, :] += RFsp 380 | 381 | # end 382 | else: 383 | #%- no parallel pool 384 | options.RC = RC 385 | _, RFsp,idx = pfield(x,y,z,delaysTX,param,options) 386 | RFspectrum[idx,:] = RFsp 387 | 388 | #%-- RF signals (in the time domain) 389 | nf = int(np.ceil(param.fs/2/param.fc*(Nf-1))) 390 | RF = np.fft.irfft(np.conj(RFspectrum),nf, axis = 0) 391 | RF = RF[:(nf + 1)//2] #*param.fs/4/param.fc 392 | 393 | #%-- Zeroing the very small values 394 | RelThresh = 1e-5#; % -100 dB 395 | tmp2= lambda RelRF: 0.5*(1+np.tanh((RelRF-RelThresh)/(RelThresh/10))) 396 | tmp = lambda RelRF: np.round(tmp2(RelRF)/(RelThresh/10))*(RelThresh/10) 397 | RF = RF*tmp(np.abs(RF)/np.max(np.abs(RF))) 398 | if returnTime: 399 | return RF,RFspectrum, np.arange(RF.shape[0])/param.fs 400 | else: 401 | return RF,RFspectrum 402 | 403 | -------------------------------------------------------------------------------- /src/pymust/simus3.py: -------------------------------------------------------------------------------- 1 | from . import utils, pfield3, getpulse 2 | import logging, copy, multiprocessing, functools 3 | import numpy as np 4 | 5 | # pfield wrapper so it is compatible with multiprocessing. Needs to be defined in a global scope 6 | def pfieldParallel3(x: np.ndarray, y: np.ndarray, z: np.ndarray, RC: np.ndarray, delaysTX: np.ndarray, param: utils.Param, options: utils.Options): 7 | options = options.copy() 8 | options.ParPool = False # No parallel within the parallel 9 | options.RC = RC 10 | _, RFsp, idx = pfield3(x, y, z, delaysTX, param, options) 11 | return RFsp, idx 12 | 13 | 14 | def simus3(*varargin): 15 | """ 16 | SIMUS3 Simulation of ultrasound RF signals for a a planar 2-D array 17 | RF = SIMUS3(X,Y,Z,RC,DELAYS,PARAM) simulates ultrasound RF radio- 18 | frequency signals generated by an ultrasound planar 2-D array 19 | insonifying a medium of scatterers. 20 | The scatterers are characterized by their coordinates (X,Y,Z) and 21 | reflection coefficients RC. 22 | 23 | Use SIMUS for uniform linear or convex arrays. 24 | 25 | X, Y, Z and RC must be of same size. The elements of the 2-D array are 26 | excited at different time delays, given by the vector DELAYS. The 27 | transmission and reception characteristics must be given in the 28 | structure PARAM (see below for details). 29 | 30 | >--- Try it: enter "simus3" in the command window for an example ---< 31 | 32 | The RF output matrix contains Number_of_Elements columns. Each column 33 | therefore represents an RF signal. The number of rows depends on the 34 | depth and the sampling frequency PARAM.fs (see below). By default, the 35 | sampling frequency is four times the center frequency. 36 | 37 | Units: X,Y,Z must be in m; DELAYS must be in s; RC has no unit. 38 | 39 | DELAYS can be a matrix. This syntax can be used to simulate MPT 40 | (multi-plane transmit) sequences. In this case, each ROW represents a 41 | delay series. For example, to create a 4-MPT sequence with a 42 | 1024-element matrix array, the DELAYS matrix must have 4 rows and 1024 43 | columns (size = [4 1024]). 44 | Note: Use TXDELAY3 to create standard delays (focus point, focus line, 45 | plane waves, diverging waves) with a matrix array. 46 | 47 | SIMUS3 uses PFIELD3 during transmission and reception. The parameters 48 | that must be included in the structure PARAM are similar as those in 49 | PFIELD3. Additional parameters are also required (see below). 50 | 51 | --- 52 | NOTE #1: X-, Y-, and Z-axes 53 | Conventional axes are used: 54 | The X-axis is PARALLEL to the transducer and points from the first 55 | (leftmost) element to the last (rightmost) element (X = 0 at the CENTER 56 | of the transducer). The Z-axis is PERPENDICULAR to the transducer and 57 | points downward (Z = 0 at the level of the transducer, Z increases as 58 | depth increases). The Y-axis is such that the coordinates are 59 | right-handed. 60 | --- 61 | NOTE #2: Simplified method: Directivity 62 | By default, the calculation is made faster by assuming that the 63 | directivity of the elements is dependent only on the central frequency. 64 | This simplification very little affects the pressure field in most 65 | situations (except near the array). To turn off this option, use 66 | OPTIONS.FullFrequencyDirectivity = true. 67 | (see ADVANCED OPTIONS below). 68 | --- 69 | 70 | PARAM is a structure that contains the following fields: 71 | ------------------------------------------------------- 72 | *** TRANSDUCER PROPERTIES *** 73 | 1) PARAM.fc: central frequency (in Hz, REQUIRED) 74 | 2) PARAM.elements: x- and y-coordinates of the element centers 75 | (in m, REQUIRED). It MUST be a two-row matrix, with the 1st 76 | and 2nd rows containing the x and y coordinates, respectively. 77 | 3) PARAM.width: element width, in the x-direction (in m, REQUIRED) 78 | 4) PARAM.height: element height, in the y-direction (in m, REQUIRED) 79 | 5) PARAM.bandwidth: pulse-echo 6dB fractional bandwidth (in %) 80 | The default is 75%. 81 | 6) PARAM.baffle: property of the baffle: 82 | 'soft' (default), 'rigid', or a scalar > 0. 83 | See "Note on BAFFLE properties" below for details 84 | 85 | *** MEDIUM PARAMETERS *** 86 | 7) PARAM.c: longitudinal velocity (in m/s, default = 1540 m/s) 87 | 8) PARAM.attenuation: attenuation coefficient (dB/cm/MHz, default: 0) 88 | Notes: A linear frequency-dependence is assumed. 89 | A typical value for soft tissues is ~0.5 dB/cm/MHz. 90 | 91 | *** TRANSMIT PARAMETERS *** 92 | 9) PARAM.TXapodization: transmit apodization (default: no apodization) 93 | 10) PARAM.TXnow: number of wavelengths of the TX pulse (default: 1) 94 | 11) PARAM.TXfreqsweep: frequency sweep for a linear chirp (default: []) 95 | To be used to simulate a linear TX down-chirp. 96 | 97 | *** RECEIVE PARAMETERS *** (not in PFIELD3) 98 | 14) PARAM.fs: sampling frequency (in Hz, default = 4*param.fc) 99 | 15) PARAM.RXdelay: reception law delays (in s, default = 0) 100 | 101 | Other syntaxes: 102 | -------------- 103 | i} [RF,PARAM] = SIMUS3(...) updates the fields of the PARAM structure. 104 | ii} [...] = SIMUS3 without any input argument provides an example 105 | designed to produce RF signals from a focused ultrasound beam 106 | using a 3 MHz matrix array transducer. 107 | 108 | PARALLEL COMPUTING: 109 | ------------------ 110 | SIMUS3 calls the function PFIELD3. If you have the Parallel Computing 111 | Toolbox, SIMUS3 can execute several PFIELD3 in parallel. If this option 112 | is activated, a parallel pool is created on the default cluster. All 113 | workers in the pool are used. The X,Y,Z are splitted into NW chunks, NW 114 | being the number of workers. To execute parallel computing, use: 115 | [...] = SIMUS3(...,OPTIONS), 116 | with OPTIONS.ParPool = true (default = false). 117 | 118 | 119 | OTHER OPTIONS: 120 | ------------- 121 | %-- FREQUENCY STEP & FREQUENCY SAMPLES --% 122 | 1a) Only frequency components of the transmitted signal in the range 123 | [0,2fc] with significant amplitude are considered. The default 124 | relative amplitude is -100 dB. You can change this value by using 125 | the following: 126 | [...] = SIMUS3(...,OPTIONS), 127 | where OPTIONS.dBThresh is the threshold in dB (default = -100). 128 | 1b) The frequency step is determined automatically to avoid aliasing in 129 | the time domain. This step can be adjusted with a scaling factor 130 | OPTIONS.FrequencyStep (default = 1). It is not recommended to 131 | modify this scaling factor in SIMUS3. 132 | --- 133 | %-- FULL-FREQUENCY DIRECTIVITY --% 134 | 2) By default, the directivity of the elements depends only on the 135 | center frequency. This makes the calculation faster. To make the 136 | directivities fully frequency-dependent, use: 137 | [...] = SIMUS3(...,OPTIONS), 138 | with OPTIONS.FullFrequencyDirectivity = true (default = false). 139 | --- 140 | %-- ELEMENT SPLITTING --% 141 | 3) Each transducer element of the array is split into small rectangles. 142 | The width and height and of these small rectangles must be small 143 | enough to ensure that the far-field model is accurate. By default, 144 | the elements are split into M-by-N rectangles, with M and N being 145 | defined by: 146 | M = ceil(element_width/smallest_wavelength); 147 | N = ceil(element_height/smallest_wavelength); 148 | To modify the number MN of subelements by splitting, you may adjust 149 | OPTIONS.ElementSplitting, which must contain two elements. For 150 | example, OPTIONS.ElementSplitting = [1 3]. 151 | --- 152 | %-- WAIT BAR --% 153 | 4) If OPTIONS.WaitBar is true, a wait bar appears (only if the number 154 | of frequency samples >10). Default is true. 155 | --- 156 | 157 | 158 | Notes regarding the model & REFERENCES: 159 | -------------------------------------- 160 | 1) SIMUS3 calls the function PFIELD3. It works for planar 2-D arrays. 161 | It considers arrays that have identical rectangular elements on the 162 | z=0 plane. Each element is split into small rectangles (if 163 | required). As the sub-elements are small enough, the 164 | three-dimensional radiation patterns are derived by using Fraunhofer 165 | (far-field) equations. 166 | 2) The paper that describes the first 2-D version of SIMUS is: 167 | SHAHRIARI S, GARCIA D. Meshfree simulations of ultrasound vector 168 | flow imaging using smoothed particle hydrodynamics. Phys Med Biol, 169 | 2018;63:205011. PDF here 171 | 3) The papers that describe the theory and validation of the 2-D + 3-D 172 | versions of PFIELD and SIMUS are: 173 | i) GARCIA D. SIMUS: an open-source simulator for medical ultrasound 174 | imaging. Part I: theory & examples. Comput Methods Programs 175 | Biomed, 2022;218:106726. PDF here 177 | ii) CIGIER A, VARRAY F, GARCIA D. SIMUS: an open-source simulator 178 | for medical ultrasound imaging. Part II:comparison with four 179 | simulators. Comput Methods Programs Biomed, 2022;220:106774. 180 | PDF here 181 | 4) Use the fonction CITE to guide you in citations. 182 | 5) There is yet no publication for SIMUS3 (it is planned for 2023-24). 183 | 184 | This function is part of MUST (Matlab UltraSound Toolbox). 186 | MUST (c) 2020 Damien Garcia, LGPL-3.0-or-later 187 | 188 | See also SIMUS, PFIELD3, TXDELAY3, GETPARAM, GETPULSE, CITE. 189 | 190 | -- Damien Garcia -- 2022/10, last update 2022/12/01 191 | website: www.BiomeCardio.com 193 | """ 194 | 195 | returnTime = False #NoteGB: Set to True if you want to return the time, but quite a mess right now with the matlab style arguments 196 | 197 | nargin = len(varargin) 198 | if nargin<= 3 or nargin > 7: # DR : should nargin<= 5 ? Only valid 6 or 7 199 | raise ValueError("Wrong number of input arguments.") 200 | #-- Input variables: X,Y,Z,DELAYS,PARAM,OPTIONS 201 | x = varargin[0] 202 | 203 | if nargin == 6: # simus3(X,Y,Z,RC,DELAYS,PARAM) 204 | y = varargin[1] 205 | z = varargin[2] 206 | RC = varargin[3] 207 | delaysTX = varargin[4] 208 | param = varargin[5] 209 | options = utils.Options() 210 | else: # simus3(X,Y,Z,RC,DELAYS,PARAM,OPTIONS) 211 | y = varargin[1] 212 | z = varargin[2] 213 | RC = varargin[3] 214 | delaysTX = varargin[4] 215 | param = varargin[5] 216 | options = copy.deepcopy(varargin[6]) 217 | assert isinstance(param, utils.Param),'PARAM must be a structure.' 218 | 219 | #-- X,Y,Z size 220 | assert x.shape == z.shape and x.shape == RC.shape and y.shape == x.shape, 'X, Y, Z, and RC must be of same size.' 221 | 222 | if len(x.shape) == 0: 223 | return np.array([]), np.array([]) 224 | 225 | 226 | #%------------------------% 227 | #% CHECK THE INPUT SYNTAX % 228 | #%------------------------% 229 | 230 | 231 | param = param.ignoreCaseInFieldNames() 232 | options = options.ignoreCaseInFieldNames() 233 | options.CallFun = 'simus3' 234 | 235 | # GB TODO: wait bar + parallelisation 236 | #%-- Wait bar 237 | #if ~isfield(options,'WaitBar') 238 | # options.WaitBar = true; 239 | #end 240 | #assert(isscalar(options.WaitBar) && islogical(options.WaitBar),... 241 | # 'OPTIONS.WaitBar must be a logical scalar (true or false).') 242 | 243 | #%-- Parallel pool 244 | #if ~isfield(options,'ParPool') 245 | # options.ParPool = False 246 | #end 247 | 248 | #%-- Check if syntax errors may appear when using PFIELD3 249 | #try: 250 | # opt = options 251 | # opt.ParPool = false; 252 | # opt.WaitBar = false; 253 | # [~,param] = pfield([],,[],[],delaysTX,param,opt); 254 | #catch ME 255 | # throw(ME) 256 | #end 257 | 258 | #-- Sampling frequency (in Hz) 259 | if not utils.isfield(param,'fs'): 260 | param.fs = 4*param.fc # default 261 | 262 | assert param.fs>=4*param.fc,'PARAM.fs must be >= 4*PARAM.fc.' 263 | 264 | NumberOfElements = param.elements.shape[1] # number of array elements 265 | 266 | #-- Receive delays (in s) 267 | if not utils.isfield(param,'RXdelay'): 268 | param.RXdelay = np.zeros((1,NumberOfElements), dtype = np.float32) 269 | else: 270 | assert isinstance(param.RXdelay, np.ndarray) and utils.isnumeric(param.RXdelay), 'PARAM.RXdelay must be a vector' 271 | assert param.RXdelay.shape[1] ==NumberOfElements, 'PARAM.RXdelay must be of length = (number of elements)' 272 | param.RXdelay = param.RXdelay.reshape((1,NumberOfElements), order='F') 273 | 274 | #-- dB threshold (in dB: faster computation if lower value) 275 | if not utils.isfield(options,'dBThresh'): 276 | options.dBThresh = -100; # default is -100dB in SIMUS3 277 | 278 | assert np.isscalar(options.dBThresh) and utils.isnumeric(options.dBThresh) and options.dBThresh<0,'OPTIONS.dBThresh must be a negative scalar.' 279 | 280 | #-- Frequency step (scaling factor) 281 | # The frequency step is determined automatically. It is tuned to avoid 282 | # aliasing in the temporal domain. The frequency step can be adjusted by 283 | # using a scaling factor. For a smoother result, you may use a scaling 284 | # factor<1. 285 | if not utils.isfield(options,'FrequencyStep'): 286 | options.FrequencyStep = 1 287 | 288 | assert np.isscalar(options.FrequencyStep) and utils.isnumeric(options.FrequencyStep) and options.FrequencyStep>0, 'OPTIONS.FrequencyStep must be a positive scalar.' 289 | 290 | if options.FrequencyStep>1: 291 | logging.warning('MUST:FrequencyStep', 'OPTIONS.FrequencyStep is >1: aliasing may be present!') 292 | 293 | if not utils.isfield(param, 'c'): 294 | param.c = 1540 # default sound speed in soft tissue 295 | 296 | 297 | #%-------------------------------% 298 | #% end of CHECK THE INPUT SYNTAX % 299 | #%-------------------------------% 300 | 301 | #GB NOTE: same as in pfield, put in param ? 302 | #-- Centers of the tranducer elements (x- and z-coordinates) 303 | xe = param.elements[0,:] 304 | ye = param.elements[1,:] 305 | 306 | #-- Maximum distance 307 | d2 = (x.reshape((-1,1),order='F')-xe)**2+(y.reshape((-1,1),order='F')-ye)**2 + z.reshape((-1,1),order='F')**2 308 | maxD = np.sqrt(np.max(d2)) # maximum element-scatterer distance 309 | _, tp = getpulse.getpulse(param, 2) 310 | maxD = maxD + tp[-1] * param.c # add pulse length 311 | 312 | #-- FREQUENCY SAMPLES 313 | df = 1/2/(2*maxD/param.c + np.max(delaysTX.flatten(order='F') + param.RXdelay.flatten(order='F'))) # to avoid aliasing in the time domain 314 | df = df*options.FrequencyStep 315 | Nf = 2*int(np.ceil(param.fc/df))+1 # number of frequency samples 316 | #-- Run PFIELD3 to calculate the RF spectra 317 | RFspectrum = np.zeros((Nf,NumberOfElements), dtype = np.complex64) # will contain the RF spectra 318 | options.FrequencyStep = df 319 | 320 | #-- run PFIELD3 in a parallel pool (NW workers) 321 | if options.get('ParPool', False): 322 | 323 | with options.getParallelPool() as pool: 324 | idx = options.getParallelSplitIndices(x.shape[1]) 325 | 326 | RS = pool.starmap(functools.partial(pfieldParallel3, delaysTX = delaysTX, param = param, options = options), 327 | [ ( x[:,i:j], 328 | y[:,i:j], 329 | z[:,i:j], 330 | RC[:,i:j]) for i,j in idx]) 331 | 332 | 333 | for (RFsp, idx_spectrum) in RS: 334 | RFspectrum[idx_spectrum, :] += RFsp 335 | 336 | 337 | else: 338 | #- no parallel pool 339 | options.RC = RC 340 | _, RFsp,idx = pfield3(x,y,z,delaysTX,param,options) 341 | RFspectrum[idx,:] = RFsp 342 | 343 | #-- RF signals (in the time domain) 344 | nf = int(np.ceil(param.fs/2/param.fc*(Nf-1))) 345 | RF = np.fft.irfft(np.conj(RFspectrum),nf, axis = 0) 346 | RF = RF[:(nf + 1)//2] #*param.fs/4/param.fc 347 | 348 | #-- Zeroing the very small values 349 | RelThresh = 1e-5# -100 dB 350 | tmp2= lambda RelRF: 0.5*(1+np.tanh((RelRF-RelThresh)/(RelThresh/10))) 351 | tmp = lambda RelRF: np.round(tmp2(RelRF)/(RelThresh/10))*(RelThresh/10) 352 | RF = RF*tmp(np.abs(RF)/np.max(np.abs(RF))) # DR: MUST has + eps in the denominator 353 | if returnTime: 354 | return RF,RFspectrum, np.arange(RF.shape[0])/param.fs 355 | else: 356 | return RF,RFspectrum 357 | -------------------------------------------------------------------------------- /src/pymust/smoothn.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import scipy, numpy as np, typing, logging 3 | from . import utils 4 | 5 | 6 | def smoothn(y: np.ndarray, 7 | W: typing.Optional[np.ndarray] = None, 8 | S: typing.Optional[float] = None, 9 | axis: typing.Optional[np.ndarray] = None, # Use this to specify the axis indicating multicomponent data 10 | TolZ: typing.Optional[float] = 1e-3, 11 | MaxIter: typing.Optional[int] = 100, 12 | Initial: typing.Optional[np.ndarray] = None, 13 | Spacing: typing.Optional[np.ndarray] = None, 14 | Order: typing.Optional[int] = 2, 15 | Weight: str = 'bisquare', 16 | isrobust: bool = False ) -> tuple[np.ndarray, float, bool]: 17 | """ 18 | %SMOOTHN Robust spline smoothing for 1-D to N-D data. 19 | % SMOOTHN provides a fast, automatized and robust discretized spline 20 | % smoothing for data of arbitrary dimension. 21 | % 22 | % Z = SMOOTHN(Y) automatically smoothes the uniformly-sampled array Y. Y 23 | % can be any N-D noisy array (time series, images, 3D data,...). Non 24 | % finite data (NaN or Inf) are treated as missing values. 25 | % 26 | % Z = SMOOTHN(Y,S) smoothes the array Y using the smoothing parameter S. 27 | % S must be a real positive scalar. The larger S is, the smoother the 28 | % output will be. If the smoothing parameter S is omitted (see previous 29 | % option) or empty (i.e. S = []), it is automatically determined by 30 | % minimizing the generalized cross-validation (GCV) score. 31 | % 32 | % Z = SMOOTHN(Y,W) or Z = SMOOTHN(Y,W,S) smoothes Y using a weighting 33 | % array W of positive values, which must have the same size as Y. Note 34 | % that a nil weight corresponds to a missing value. 35 | % 36 | % If you want to smooth a vector field or multicomponent data, Y must be 37 | % a cell array. For example, if you need to smooth a 3-D vectorial flow 38 | % (Vx,Vy,Vz), use Y = {Vx,Vy,Vz}. The output Z is also a cell array which 39 | % contains the smoothed components. See examples 5 to 8 below. 40 | % 41 | % [Z,S] = SMOOTHN(...) also returns the calculated value for the 42 | % smoothness parameter S. 43 | % 44 | % 45 | % 1) ROBUST smoothing 46 | % ------------------- 47 | % Z = SMOOTHN(...,'robust') carries out a robust smoothing that minimizes 48 | % the influence of outlying data. 49 | % 50 | % An iteration process is used with the 'ROBUST' option, or in the 51 | % presence of weighted and/or missing values. Z = SMOOTHN(...,OPTIONS) 52 | % smoothes with the termination parameters specified in the structure 53 | % OPTIONS. OPTIONS is a structure of optional parameters that change the 54 | % default smoothing properties. It must be the last input argument. 55 | % --- 56 | % The structure OPTIONS can contain the following fields: 57 | % ----------------- 58 | % OPTIONS.TolZ: Termination tolerance on Z (default = 1e-3), 59 | % OPTIONS.TolZ must be in ]0,1[ 60 | % OPTIONS.MaxIter: Maximum number of iterations allowed 61 | % (default = 100) 62 | % OPTIONS.Initial: Initial value for the iterative process 63 | % (default = original data, Y) 64 | % OPTIONS.Weight: Weight function for robust smoothing: 65 | % 'bisquare' (default), 'talworth' or 'cauchy' 66 | % ----------------- 67 | % 68 | % [Z,S,EXITFLAG] = SMOOTHN(...) returns a boolean value EXITFLAG that 69 | % describes the exit condition of SMOOTHN: 70 | % 1 SMOOTHN converged. 71 | % 0 Maximum number of iterations was reached. 72 | % 73 | % 74 | % 2) Different spacing increments 75 | % ------------------------------- 76 | % SMOOTHN, by default, assumes that the spacing increments are constant 77 | % and equal in all the directions (i.e. dx = dy = dz = ...). This means 78 | % that the smoothness parameter is also similar for each direction. If 79 | % the increments differ from one direction to the other, it can be useful 80 | % to adapt these smoothness parameters. You can thus use the following 81 | % field in OPTIONS: 82 | % OPTIONS.Spacing = [d1 d2 d3...], 83 | % where dI represents the spacing between points in the Ith dimension. 84 | % 85 | % Important note: d1 is the spacing increment for the first 86 | % non-singleton dimension (i.e. the vertical direction for matrices). 87 | % 88 | % 89 | % 3) REFERENCES (please refer to the two following papers) 90 | % ------------- 91 | % 1) Garcia D, Robust smoothing of gridded data in one and higher 92 | % dimensions with missing values. Computational Statistics & Data 93 | % Analysis, 2010;54:1167-1178. 94 | % download PDF 96 | % 2) Garcia D, A fast all-in-one method for automated post-processing of 97 | % PIV data. Exp Fluids, 2011;50:1247-1259. 98 | % download PDF 100 | % 101 | % 102 | """ 103 | ##%% Check input arguments 104 | 105 | # Test & prepare the variables 106 | #--- 107 | # y = array to be smoothed 108 | if isinstance(y, list) or isinstance(y, tuple): 109 | if not axis is None: 110 | raise ValueError('If Y is a list or tuple, it is considered a multivariate array and the axis parameter must be None') 111 | y = np.array(y) 112 | axis = [0] 113 | 114 | origShapeY = y.shape 115 | # If the axis is None, then it is a single array 116 | if axis is None: 117 | axis = [0] 118 | y =y[np.newaxis, :] 119 | 120 | 121 | 122 | # Axis must be a list 123 | if not np.allclose(axis, np.arange(len(axis))): 124 | raise NotImplemented('Now, multivariate arrays must be in the first dimensions (ordered)') 125 | else: 126 | # Merge all the indices being multivariate array in the first component. 127 | y = y.reshape((-1, *[d for i, d in enumerate(y.shape) if i not in axis])) 128 | sizy = y[0].shape 129 | ny = y.shape[0]; # number of y components 130 | for i, yi in enumerate(y): 131 | assert np.all(np.equal(sizy,yi.shape)), 'Data arrays in Y must have the same size.' 132 | 133 | 134 | noe = np.prod(sizy)# % number of elements 135 | if noe==1: 136 | return y.reshape(origShapeY), [], True 137 | 138 | #--- 139 | # Smoothness parameter and weights 140 | if W is None: 141 | W = np.ones(sizy) 142 | if S is None: 143 | isauto = True 144 | else: 145 | isauto = False 146 | assert (np.isscalar(S) and S>0), 'The smoothing parameter S must be a scalar >0' 147 | assert utils.isnumeric(S),'S must be a numeric scalar' 148 | 149 | assert utils.isnumeric(W),'W must be a numeric array' 150 | assert np.all(np.equal(W.shape,sizy)), 'Arrays for data and weights (Y and W) must have same size.' 151 | 152 | #--- 153 | # Field names in the structure OPTIONS 154 | 155 | assert utils.isnumeric(MaxIter) and np.isscalar(MaxIter) and MaxIter>=1 and MaxIter==np.round(MaxIter), 'OPTIONS.MaxIter must be an integer >=1' 156 | MaxIter = int(MaxIter) 157 | 158 | assert utils.isnumeric(TolZ) and np.isscalar(TolZ) and TolZ>0 and TolZ<1,'OPTIONS.TolZ must be in ]0,1[' 159 | #--- 160 | # "Initial Guess" criterion 161 | if Initial is None: 162 | isinitial = False 163 | else: 164 | isInitial = True 165 | z0 = Initial; 166 | z = z.reshape((-1, *[d for i, d in enumerate(origShapeY) if i not in axis])) 167 | assert np.all_equal(z.shape, y.shape), 'OPTIONS.Initial must contain a valid initial guess for Z' 168 | 169 | 170 | #%--- 171 | # "Weight function" criterion (for robust smoothing) 172 | Weight = Weight.lower() 173 | assert Weight in ['bisquare','talworth','cauchy'], 'The weight function must be "bisquare", "cauchy" or " talworth".' 174 | 175 | #--- 176 | # "Order" criterion (by default m = 2) 177 | # Note: m = 0 is of course not recommended! 178 | 179 | m = Order 180 | assert m in [0,1,2], 'The order (OPTIONS.order) must be 0, 1 or 2.' 181 | 182 | #--- 183 | # "Spacing" criterion 184 | d = len(y[0].shape) 185 | if Spacing is None: 186 | dI = np.ones(d) # 187 | else: 188 | dI = Spacing; 189 | assert utils.isnumeric(dI) and len(dI) == d, 'A valid spacing (OPTIONS.Spacing) must be chosen' 190 | 191 | dI = dI/max(dI); 192 | #--- 193 | # Weights. Zero weights are assigned to not finite values (Inf or NaN), 194 | # (Inf/NaN values = missing data). 195 | IsFinite = np.all(np.isfinite(y), axis = 0) 196 | nof = np.count_nonzero(IsFinite); #% number of finite elements 197 | W = W*IsFinite; 198 | assert np.all(W>=0,) & np.all(np.isfinite(W)), 'Weights must all be finite and >=0' 199 | # W = W/max(W(:)); 200 | #--- 201 | # Weighted or missing data? 202 | isweighted = np.any(W !=1) 203 | 204 | #--- 205 | # Automatic smoothing? 206 | 207 | 208 | #% Create the Lambda tensor 209 | #--- 210 | # Lambda contains the eingenvalues of the difference matrix used in this 211 | # penalized least squares process (see CSDA paper for details) 212 | Lambda = np.zeros(sizy); 213 | for i in range(d): 214 | siz0 = np.ones((1,d)) 215 | siz0[0,i] = sizy[i] 216 | Lambda = Lambda + (2-2*np.cos(np.pi*(np.arange(sizy[i],dtype = float).reshape([1 if i != j else sizy[i] for j in range(d)]))/sizy[i]))/dI[i]**2 217 | 218 | if not isauto: 219 | Gamma = 1/(1+S*Lambda**m) 220 | #% Upper and lower bound for the smoothness parameter 221 | # The average leverage (h) is by definition in [0 1]. Weak smoothing occurs 222 | # if h is close to 1, while over-smoothing appears when h is near 0. Upper 223 | # and lower bounds for h are given to avoid under- or over-smoothing. See 224 | # equation relating h to the smoothness parameter for m = 2 (Equation #12 225 | # in the referenced CSDA paper). 226 | N = np.sum(sizy!=1) # tensor rank of the y-array 227 | hMin = 1e-6 228 | hMax = 0.99 229 | if m==0: # Not recommended. For mathematical purpose only. 230 | sMinBnd = 1/hMax**(1/N)-1; 231 | sMaxBnd = 1/hMin**(1/N)-1; 232 | elif m==1: 233 | sMinBnd = (1/hMax**(2/N)-1)/4; 234 | sMaxBnd = (1/hMin**(2/N)-1)/4; 235 | elif m==2: 236 | sMinBnd = ((1+np.sqrt(1+8*hMax**(2/N)))/4/hMax**(2/N)**2-1)/16; 237 | sMaxBnd = (((1+np.sqrt(1+8*hMin**(2/N)))/4/hMin**(2/N))**2-1)/16; 238 | 239 | 240 | #% Initialize before iterating 241 | #--- 242 | Wtot = W; 243 | #--- Initial conditions for z 244 | if isweighted: 245 | #--- With weighted/missing data 246 | # An initial guess is provided to ensure faster convergence. For that 247 | # purpose, a nearest neighbor interpolation followed by a coarse 248 | # smoothing are performed. 249 | #--- 250 | if isinitial:#% an initial guess (z0) has been already given 251 | z = z0; 252 | else: 253 | z = InitialGuess(y,IsFinite) 254 | 255 | else: 256 | z = np.zeros_like(y) 257 | #--- 258 | z0 = z; 259 | for i, _ in enumerate(y): 260 | y[i][np.logical_not(IsFinite)] = 0 #arbitrary values for missing y-data 261 | 262 | 263 | #--- 264 | tol = 1; 265 | RobustIterativeProcess = True; 266 | RobustStep = 1; 267 | nit = 0; 268 | DCTy = np.zeros_like(y); 269 | #--- Error on p. Smoothness parameter s = 10^p 270 | errp = 0.1; 271 | 272 | #--- Relaxation factor RF: to speedup convergence 273 | RF = 1 + 0.75*isweighted; 274 | 275 | #%% Main iterative process 276 | #%--- 277 | while RobustIterativeProcess: 278 | #--- "amount" of weights (see the function GCVscore) 279 | aow = np.sum(Wtot)/np.max(W)/noe; # 0 < aow <= 1 280 | #--- 281 | while tol> TolZ and nit tol=0 (no iteration) 305 | tol = isweighted*np.linalg.norm( z0 - z)/np.linalg.norm(z0) 306 | z0 = z.copy() # re-initialization 307 | 308 | exitflag = nit< MaxIter 309 | if isrobust: #-- Robust Smoothing: iteratively re-weighted process 310 | ##--- average leverage 311 | h = 1; 312 | for k in range(N): 313 | if m==0: ## not recommended - only for numerical purpose 314 | h0 = 1/(1+S/dI[k]**(2**m)); 315 | elif m==1: 316 | h0 = 1/np.sqrt(1+4*S/dI[k]*(2**m)); 317 | elif m==2: 318 | h0 = np.sqrt(1+16*S/dI[k]**(2**m)); 319 | h0 = np.sqrt(1+h0)/np.sqrt(2)/h0; 320 | h = h*h0; 321 | #%--- take robust weights into account 322 | Wtot = W*RobustWeights(y,z,IsFinite,h, Weight); 323 | 324 | #%--- re-initialize for another iterative weighted process 325 | isweighted = True; 326 | tol = 1; 327 | nit = 0; 328 | #%--- 329 | RobustStep = RobustStep+1 330 | RobustIterativeProcess = RobustStep<4 # 3 robust steps are enough. 331 | else: 332 | RobustIterativeProcess = False; # stop the whole process 333 | 334 | 335 | 336 | #% Warning messages 337 | #%--- 338 | if isauto: 339 | if abs(np.log10(S)-np.log10(sMinBnd))0.95: # aow = 1 means that all of the data are equally weighted 362 | # very much faster: does not require any inverse DCT 363 | for kk in range(ny): 364 | RSS = RSS + np.linalg.norm(DCTy[kk]*(Gamma-1))**2; 365 | else: 366 | # take account of the weights to calculate RSS: 367 | for kk in range(ny): 368 | yhat = scipy.fft.idctn(Gamma*DCTy[kk]); 369 | RSS = RSS + np.linalg.norm(np.sqrt(Wtot[IsFinite])* (y[kk][IsFinite]-yhat[IsFinite]))**2; 370 | #%--- 371 | TrH = np.sum(Gamma) 372 | GCVscore = RSS/nof/(1-TrH/noe)**2 373 | return GCVscore 374 | 375 | 376 | 377 | def RobustWeights(y,z,I,h,wstr): 378 | # One seeks the weights for robust smoothing... 379 | 380 | r = y- z 381 | r = r.reshape((r.shape[0], -1)) 382 | rI = r[:, I.flatten()] 383 | MMED = np.median(rI); # marginal median 384 | AD = np.linalg.norm(rI-MMED, axis = 0); # absolute deviation 385 | MAD = np.median(AD); # median absolute deviation 386 | 387 | #%-- Studentized residuals 388 | u = np.linalg.norm(r, axis = 0)/(1.4826*MAD)/np.sqrt(1-h); 389 | u = u.reshape(I.shape) 390 | #u = u.reshape(I.shape) 391 | if wstr == 'cauchy': 392 | c = 2.385 393 | W = 1/(1+(u/c)**2); #% Cauchy weights 394 | elif wstr == 'talworth': 395 | c = 2.795 396 | W = u .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() --------------------------------------------------------------------------------