简介:一份以超声成像为核心的Matlab/C语言学习资料包,面向生物医学工程、信号处理学习者及医疗设备研发入门者,系统演示从超声回波数据采集、滤波预处理、多普勒回波分析到傅里叶图像重建与后处理的完整流程,可帮助读者理解实时聚焦波束(RFB)等成像模式。压缩包内共664个文件,总大小约65.34MB,主要包含Matlab脚本与函数(.m/.mat)、C/C++源码(.c/.cpp/.h)、可执行程序及Visual Studio工程文件(.vcxproj/.sln),另有html文档、bmp/png图像和dat数据文件,分别覆盖源码、文档、测试数据与编译构建等不同用途。该资源已有2566人学习,说明其内容对超声成像入门与实际编码具有较高参考价值。通过资源附带的可执行程序、示例图像和完整工程,学习者既能直观对比Matlab原型与C语言实现的效果差异,也能在真实数据上复现滤波、FFT和图像重建步骤,为后续开发高实时性成像系统打下基础。
1. 超声成像例子:为什么从仿真入手是最稳的入门姿势
超声成像例子这个词,看起来像是一个代码仓库或者教程合集,但真正做过一两个超声项目的人会明白,它代表的是一整条从声场仿真、射频数据生成、波束合成到最终B超图像显示的落地链路。很多想接触超声成像的开发者,第一步就卡在“没有超声探头、没有标准体模、没有采集板卡”上,而仿真恰恰能把这些硬件依赖全部架空,让你在一台普通电脑上就把成像流程跑通。
我自己的经验是,超声成像算法本身并不玄学,卡住人的通常是这四件事:声场怎么算、射频回波怎么得到、延时应怎么补偿、最后的灰度图怎么映射。这篇笔记就按照我自己趟过一遍的顺序来写——先说清楚例子背后的数据流,再分别用两种最常见的仿真工具把代码跑起来,最后把最常翻车的参数和坑点一次性列出来。适合正在选方向、准备做毕业设计或者想快速验证波束合成算法的开发者,新手能照着走,熟手可以直接挑参数参考。
2. 超声成像例子到底在模拟什么:一条完整的数据链路
2.1 从声波发射到灰度图:超声成像的最小知识链路
超声成像的物理链条其实很短:探头里的压电阵元受电信号激励,产生超声脉冲向组织内传播;声波遇到不同声阻抗的组织界面会产生反射回波;回波再被同一批阵元接收,变成电信号;系统对每个阵元的接收信号做延时补偿和加权求和,得到一个聚焦后的射频(RF)信号;最后经过包络检测、对数压缩和灰度映射,才变成我们熟悉的超声图像。
理解这条链路的价值在于,你后续写的每一段代码都在复现其中的某一步。比如用 Field II 计算声场,实际上是在算“发射脉冲经过介质后在某个空间点形成的压力波幅度”;而波束合成中的延迟叠加(DAS)算法,则是这条链路的数字信号处理核心。超声成像例子之所以值得反复琢磨,正是因为它把抽象的物理和信号处理知识压缩进了可运行的代码里。
2.2 仿真工具选型:Field II 和 k-Wave 到底该用哪个
我最早接触超声仿真时,最常被问到的问题就是“该学 Field II 还是 k-Wave”。这里直接说结论:两者解决的问题不一样,正常流程是都用到。
Field II 属于“解析/半解析”仿真器,基于空间脉冲响应法,计算精度高、速度快,特别适合模拟线性阵列探头的发射和接收声场,也是生成 RF 数据做波束合成算法验证的事实标准。k-Wave 则是基于 k-space 伪谱法的全波仿真工具箱,能够模拟声波在非均匀介质中的传播、散射和吸收,适合研究组织中的声场分布、温度升高或者更复杂的物理过程。
下面是我的选型习惯:
| 需求 | 首选工具 | 原因 |
|---|---|---|
| 生成 RF 回波数据,验证 DAS 波束合成 | Field II | 计算快,API 成熟,输出直接对应阵元通道数据 |
| 观察声场传播过程、散射体响应 | k-Wave | 能可视化声压随时间的变化,物理过程直观 |
| 模拟非均匀组织、声速变化 | k-Wave | 内置声速/密度/吸收参数,可逐点设置 |
| 大批量生成训练数据 | Field II | 单次仿真开销小,参数化方便 |
需要说明的是,Field II 在 MATLAB 环境下运行时,最核心的概念是“计算换能器激励产生的声场或接收响应”,它本身不直接给出最终的 B 超图像,你需要自己写波束合成和后处理。这也是“超声成像例子”通常包含两条线的原因:一条是声场仿真,一条是成像信号处理。
2.3 一个例子足够了吗:从仿真到算法评估你需要准备什么
很多人拿到一个示例代码,跑通后就以为已经搞定了超声成像。我建议再往前推一步:一个完整的仿真实验环境,至少应该包含三个部分。
第一部分是散射体模型,也就是模拟组织中的点散射目标。第二部分是探头参数,包括阵元数、阵元间距、中心频率、采样频率,这些直接决定 RF 数据的格式和后续信号处理的参数选择。第三部分是成像算法脚本,把 RF 数据处理成图像,并计算分辨率、对比度等指标。
我一般会把这三部分拆成三个文件夹:scatter(散射体定义)、probe(探头参数)、processing(成像算法)。这样每次换参数,只需要改对应的脚本,不用把代码揉成一团。后面两章的示例也会按这个习惯组织。
3. 用 Field II 生成第一份仿真超声数据:最小脚本与四个必调参数
3.1 安装与初始化:让 Field II 第一次跑起来
Field II 的安装步骤并不复杂,但有一个顺序上的坑:必须先运行 field_init.m 完成库初始化,再创建换能器。这里给出一段最小可用脚本,用来生成一个 64 阵元线性阵列探头的仿真数据。
% 最小可用脚本:64阵元线阵,单个散射点,生成RF数据 clear; close all; clc; % 1. 初始化 Field II field_init(0); % 参数0表示不显示进度条,节省输出开销 % 2. 定义探头参数 f0 = 5e6; % 中心频率:5 MHz c = 1540; % 声速:软组织典型值 1540 m/s lambda = c / f0; % 波长 pitch = lambda; % 阵元间距,通常取一个波长 n_elements = 64; % 阵元数量 width = 0.9 * lambda; % 阵元宽度 height = 5e-3; % 阵元高度(仰角方向) kerf = pitch - width; % 阵元间隙 % 3. 创建线性阵列探头 xdc = xdc_linear_array(n_elements, width, height, kerf); % 4. 设置激励脉冲和冲激响应 impulse_response = sin(2*pi*f0*(0:1/f0*4:1/f0*4*1/f0*100)); % 简单正弦脉冲 % 实际推荐用 gauspuls 生成高斯包络脉冲,更接近真实探头 fs = 100e6; % 采样率:100 MHz t = 0:1/fs:1e-5; % 时间轴 excitation = gauspuls(t, f0, 0.5); % 高斯包络正弦脉冲 xdc_impulse(xdc, impulse_response); xdc_excitation(xdc, excitation); % 5. 设置采样频率 set_sampling(fs);这段代码的关键点有两个。第一,field_init(0)里的参数控制是否输出仿真进度,机器性能一般时建议设为 0,否则控制台会被刷屏。第二,xdc_linear_array的参数顺序是阵元数、宽度、高度、间隙,不要记反;kerf为 0 表示阵元无缝拼接,实际探头一般留有间隙。
关于激励脉冲,示例里我给了一段正弦波,但真正仿真时建议用gauspuls。为什么?因为正弦脉冲的频谱很窄,仿真的 RF 信号会和真实探头的宽带脉冲差距较大,导致后续包络检测的波形不够真实。
3.2 定义散射体并计算 RF 数据
探头创建好之后,下一步就是定义散射体并计算接收信号。这一步的输出就是所谓的“RF 数据”,它是波束合成算法的输入。
% 定义一个点散射体:位于探头下方 30mm 处 scat_pos = [0, 0, 30e-3]; % x, y, z 坐标,z 为深度方向 scat_amp = 1; % 散射强度 % 计算接收信号 [rf_data, start_time] = calc_scat(xdc, scat_pos, scat_amp); % 可视化看一下单阵元的 RF 信号 figure; plot((0:length(rf_data)-1)/fs + start_time, rf_data(:, 1)); xlabel('时间 (s)'); ylabel('幅度'); title('第一个阵元的RF回波信号');calc_scat是 Field II 里计算点散射体回波的核心函数。返回值rf_data是二维矩阵,行对应采样时间点,列对应阵元通道;start_time是回波到达第一个阵元的起始时间,用来在成像时对齐时间轴。
注意scat_pos的 z 坐标用了 30mm,这个值要和后面成像时的时间窗对应上。声速 1540 m/s 时,30mm 深度往返时间是 39 微秒左右,所以采样点数是 100e6 * 39e-6 ≈ 3900 点。如果散射体放得特别深,rf_data会变长,内存占用会明显上升。
3.3 四个必调参数和它们的作用
用 Field II 做仿真,绕不开这四个参数:
中心频率(f0)决定分辨率上限。理论上轴向分辨率约等于波长的一半,5MHz 对应的波长约 0.3mm,这就是你后续图像能分清的最小间距。
采样率(fs)决定 RF 数据的时间精度。采样率太低,延时补偿的最小步长会变大,波束合成的聚焦质量会明显变差。通常取中心频率的 10~20 倍,100MHz 对 5MHz 来说是一个安全值。
阵元数量(n_elements)决定成像孔径和横向分辨率。阵元数越多,孔径越大,聚焦后的主瓣越窄,但运算量也会线性增加。在入门阶段,64 阵元是一个性价比很高的配置。
阵元间距(pitch)决定是否出现栅瓣。间距大于波长时,波束中会出现明显的栅瓣,图像上表现为伪目标。因此 pitch 应控制在 0.5~1 个波长,我做参数实验时通常从 0.5 个波长开始往上涨。
4. 用 k-Wave 做声场可视化:换一种方式理解波束合成
4.1 k-Wave 建模:网格、介质与传感器的三步走
k-Wave 的建模思路和 Field II 完全不同:它不是在计算“某个阵元到某点的响应”,而是直接在一个二维或三维网格上数值求解声波方程。你可能需要一段时间来适应它的思维方式,但它对理解超声传播的物理过程非常有帮助。
一个典型的 k-Wave 仿真包含三步:定义网格,定义介质参数,定义声源和传感器。下面的代码建立了一个二维声场传播模型,观察一个单阵元发射的声波在均匀介质中的传播过程。
% 二维声场传播仿真:观察点声源的波前传播 clear; close all; clc; % 1. 网格定义 dx = 0.1e-3; % 网格步长 0.1mm Nx = 256; Ny = 256; % 网格点数 kgrid = kWaveGrid(Nx, dx, Ny, dx); % 2. 介质参数(均匀) medium.sound_speed = 1540; % 声速 m/s medium.density = 1000; % 密度 kg/m^3 % 3. 声源:一个位于网格中心的点源 source.p_mask = zeros(Nx, Ny); source.p_mask(128, 128) = 1; source.p = toneBurst(1/(dx/medium.sound_speed*2), 5e6, 5); % 5MHz, 5周期 % 4. 传感器:记录整个网格的声压(用于动画) sensor.mask = ones(Nx, Ny); % 记录所有网格点的声压这里source.p_mask用来标记哪些网格点是声源,toneBurst函数生成一个有限周期的正弦脉冲。注意toneBurst的第一个参数是采样频率,在 k-Wave 里,时间步长一般由网格步长和声速决定,采样频率要按这个时间步长的倒数来设定,写错的话声源波形会失真。
sensor.mask 设为全 1 矩阵,是为了记录每个时刻所有网格点的声压值,方便后续做动画。这样做的代价是存储量很大,如果你只是想看点源到某一点的声压,应该只设置一个点或一条线。
4.2 运行仿真并可视化声场传播
% 运行仿真 sensor_data = kspaceFirstOrder2D(kgrid, medium, source, sensor, 'PlotSim', true); % 仿真结束后,选取几个时间帧绘制声场快照 figure; for t = 1:round(size(sensor_data.p,1)/10):size(sensor_data.p,1) imagesc(squeeze(sensor_data.p(t,:,:))); colormap(flipud(gray)); axis image; title(sprintf('t = %d', t)); drawnow; pause(0.05); endkspaceFirstOrder2D是 k-Wave 的主求解函数,名称里的 2D 指的是二维仿真。'PlotSim', true会在仿真过程中实时显示声场传播动画,对理解波的衍射和反射非常有帮助。
这段代码里我用sensor_data.p的三维矩阵来画快照:第一维是时间帧,第二、三维是网格空间的 x 和 y 坐标。实际操作中,我建议把PlotSim设为 true 完整看一遍传播过程,这比事后画静态图更能建立物理直觉——你会看到声波从点源扩散成圆弧,遇到介质边界产生反射波,这些现象在后续生成 RF 数据时同样存在。
4.3 把 k-Wave 的结果用到成像例子里:一个务实的结合方式
k-Wave 直接做完整 B 超成像并不高效,因为它要模拟全波传播,计算量比 Field II 大得多。我见过不少人把 k-Wave 跑出来的声压数据当作 RF 信号去成像,结果发现时间成本完全不可接受。务实的方式是分工:用 Field II 高质量地生成大量 RF 数据做算法验证,用 k-Wave 做单点或多点场景的物理过程可视化,帮助解释算法为什么在某些场景失效。
比如研究反射杂波对图像的影响时,我会先构造一个包含强反射界面的 k-Wave 模型,观察反射波如何被阵元接收,形成多条“伪影”,再去 Field II 中复现同样的设置,量化伪影的强度。这种“先看物理过程,再做算法验证”的组合,能让每个例子的技术价值都发挥出来。
5. 超声成像例子里最常踩的 5 个坑:现象、原因与对策
5.1 阵元间距超过一个波长,图像里出现“幽灵目标”
现象:成像结果中,散射点旁边出现一个亮度近乎一样的假目标,位置左右对称。
原因:阵元间距大于波长时,波束方向图上会出现栅瓣。栅瓣方向上的回波被当成主瓣方向处理,导致假目标。这是阵列信号处理中的经典问题,我在第一次做 128 阵元仿真时也翻过车。
解决:把pitch控制在 0.5 到 1 个波长之间。如果你必须用大间距(比如模拟稀疏阵列),就改用随机稀疏阵列,让栅瓣的能量分散到旁瓣里,而不是形成一个明显的假目标。
5.2 使用正弦脉冲做激励,图像分辨率明显偏粗
现象:换用gauspuls前,图像上的点目标在轴向方向上拉出了很长的拖尾。
原因:正弦脉冲的频带很窄,等效于时域上的长信号,回波的持续时间长,导致径向分辨率变差。
解决:做仿真时不要随便用一个 sine 函数拼出激励信号,用gauspuls(t, f0, 0.5)生成高斯包络脉冲,带宽系数取 0.5~1.0。带宽越大,轴向分辨率越好,但信噪比会略降。
5.3 采样率太低,波束合成的聚焦效果完全体现不出来
现象:延时补偿的“延迟”量永远只能凑到整数个采样点,聚焦后的点目标宽度远大于理论值。
原因:波束合成的延时计算需要亚采样精度。采样率只有 20MHz,中心频率 5MHz 时,一个周期只有 4 个采样点,延时量化误差太大。
解决:采样率设为中心频率的 10 倍以上,最好是 20 倍。5MHz 探头配 100MHz 采样是安全组合。如果你的电脑内存扛不住高采样率产生的数据量,至少做到 10 倍。
5.4 散射体离探头太近,回波和激励信号混叠
现象:RF 数据前几个采样点的值异常大,后面真正的回波被淹没。
原因:calc_scat计算的回波包含了从阵元到散射体再返回的完整路径,但如果散射体在近场区,回波到达时间极短,激励信号本身还没衰减完,两者混在一起。
解决:将散射体放在距探头至少 20 个波长以上的深度(5MHz 时为 6mm 以上),或者在计算后去掉激励信号持续时间段的数据。实际做体模仿真时,把目标放在 20mm 到 60mm 深度区间最省心。
5.5 组织的声速设为 1540,但实际声速不是这个值
现象:图像上目标的位置比真实位置偏深或偏浅,看起来像是空间发生了偏移。
原因:波束合成是用固定声速计算延时的。如果组织的真实声速与假设声速不一致,聚焦点就会错位。
解决:先用 1540 跑通流程,再做声速误差实验。实际上,可以对比 1500、1540、1580 三组声速参数下的图像偏移量,这也能帮你理解超声设备中声速校正的作用。把这个误差分析写进例子里,会让你的工作内容比单纯跑通例子扎实很多。
6. 把仿真例子变成自己的工具箱:一个管用的参数习惯
到这一步,你已经有能力用 Field II 生成 RF 数据、用 k-Wave 观察声场传播、用 DAS 做波束合成并显示图像。但如果每次都手工改参数,效率还是太低。我的习惯是把参数集中放在一个结构体里,再写一个循环批量跑实验。
% 批量实验:扫描阵元数量对分辨率的影响 params.f0 = 5e6; params.c = 1540; params.fs = 100e6; params.pitch_factor = 0.5; % 阵元间距 / 波长 n_elements_list = [32, 64, 128, 256]; for n = n_elements_list params.n_elements = n; % 调用自己封装好的仿真函数 rf_data = sim_rf_data(params); bmode_img = das_beamform(rf_data, params); % 计算点扩散函数宽度并记录 width = measure_psf(bmode_img); fprintf('n=%d, PSF=%.2f mm\n', n, width); end这个批处理框架的价值在于,你不需要每次都想“参数在哪里改”,而是把所有可调变量固定成结构体字段,批量实验时只需要写一个 for 循环。我习惯把三个函数拆开:sim_rf_data负责调用 Field II 生成数据,das_beamform负责延迟叠加与包络检测,measure_psf负责从输出图像中量测点扩散函数宽度。这样每一层的输入输出都清晰,排查问题的时候能快速定位是仿真问题还是算法问题。
一个值得做的进阶验证是沿着深度方向放置一排等间距的散射点,成像后看 PSF 随深度的变化规律。你很快会发现,波束合成在近场和远场的表现差异非常大,近场的 PSF 窄、对比度高,远场的主瓣变宽且旁瓣变高。这个现象直接关系到实际临床中为什么探头要贴近皮肤、为什么深部病灶分辨率差。
最后说一个我自己的习惯:每次改参数前,先另存一份带日期后缀的脚本,或者在结果里记录当前参数组的哈希值。超声仿真里“改了一行参数,结果全变了”的时刻太多了,如果没有留痕,你会花大量的时间在复现旧结果上。
做超声成像例子,跑通只是起点,真正的收获是你能解释每一次改动为什么让图像变好或变差,希望这篇笔记能帮你把这一步走稳。
本文还有配套的精品资源,点击获取