近场声全息(NAH)仿真实现:从阵列实值信号到波数域重建

上一篇文章《近场声全息(NAH)简介:从测量面声场到源面重建-CSDN博客》中,我主要介绍了近场声全息的基本原理:在测量面获取声场分布后,通过二维傅里叶变换将其转换到波数域,再利用声场在波数域中的传播关系,将测量面声场反推到靠近声源的重建面,从而恢复重建面上的声场分布。

这篇文章我将仿真实现该算法,很多入门示例会直接在测量面人为构造一张复声压分布,然后直接做波数域反推,但和真实阵列测量之间还隔着一步。因为在实际工程中,麦克风阵列首先采到的是实值时域信号,而不是直接得到复声压矩阵。只有在对这些时域信号做频域分析之后,我们才能在目标频率上提取出复声压,并将其作为 NAH 的输入。

因此,这一篇文章采用一种更接近实际过程的思路:

首先构造两个单频点声源,然后让 8×8 麦克风阵列接收实值时域信号;接着对每一路信号做 FFT,在目标频率处提取复频谱值;然后把这些复频谱值按阵列位置重新排列,形成测量面上的复声压矩阵;最后基于近场声全息的波数域传播关系,将测量面声场反推到重建面。

一、仿真的总体思路

首先,在空间中设置两个单频点声源,它们位于重建面之后。阵列位于测量面上,共有 8×8 个麦克风。每一个麦克风接收到的都是若干个声源叠加后的实值时域信号。然后对每一路时域信号做 FFT,提取目标频率下的复声压值。这样,就得到了 NAH 所需的测量面输入数据。随后,再对测量面复声压做二维傅里叶变换,将其转到波数域,并利用传播算子反推到重建面,最终通过二维逆傅里叶变换恢复空间域声场。

整个仿真流程可以概括为:

声源建模→阵列采样(实值时域信号)→FFT提取目标频点复声压→二维傅里叶变换→波数域传播补偿→二维逆傅里叶变换→重建面声场

二、仿真基本参数

Matlab 复制代码
%% =========================================================
%  1. 基本参数
% =========================================================
c = 343;                    % 声速 (m/s)
rho0 = 1.21;                % 空气密度
f0 = 5000;                  % 目标频率 (Hz)
omega = 2*pi*f0;
k = omega / c;

fs = 51200;                 % 采样率 (Hz)
T = 0.08;                   % 采样时长 (s)
t = 0:1/fs:T-1/fs;
Nt = length(t);

z_meas  = 0.02;             % 测量面 z = 0.02 m
z_recon = 0.00;             % 重建面 z = 0 m

SNR = 10;                   % 加噪声信噪比 (dB)

三、8×8 麦克风阵列的建立

阵列采用一个 8×8 的规则平面阵列,总共 64 个通道。阵列在 x 和 y 方向上的孔径都取为 0.2 m。

Matlab 复制代码
%% =========================================================
%  2. 8×8 麦克风阵列
% =========================================================
Nx = 8;
Ny = 8;
Lx = 0.2;                  % 阵列孔径 x方向 (m)
Ly = 0.2;                  % 阵列孔径 y方向 (m)

x_mic = linspace(-Lx/2, Lx/2, Nx);
y_mic = linspace(-Ly/2, Ly/2, Ny);
[Xm, Ym] = meshgrid(x_mic, y_mic);

dx = x_mic(2) - x_mic(1);
dy = y_mic(2) - y_mic(1);

这一步虽然简单,但它决定了后面二维傅里叶变换时的空间采样基础。

四、声源建模:两个单频点声源

为了得到一个较为直观的近场声场,本例中构造了两个单频点声源,均位于重建面之后,即 z<0 的位置。它们的存在使得测量面上的声场表现为两个声源共同叠加的结果,而不是单一球面波。

在本文中,点声源复声压采用:

这里的 r 是场点到声源的传播距离。这个相位表达式和上一篇中讨论的波数域传播写法是一整套统一的约定,因此后面无论是生成时域信号,还是构造参考真值,都会沿用这一套符号体系。

两个声源在程序中分别具有各自的位置、幅值和初始相位。初始相位这里都取 0,也就是说两个声源之间的相位差主要来自传播路径差。

Matlab 复制代码
%% =========================================================
%  3. 构造真实声源
%  两个单频点声源,位置放在 z < 0
% =========================================================
zs1 = 0.015;
zs2 = 0.020;

src(1).x   = -0.05;
src(1).y   =  0.03;
src(1).z   = -zs1;
src(1).A   = 1.0;
src(1).phi = 0;

src(2).x   =  0.060;
src(2).y   = -0.04;
src(2).z   = -zs2;
src(2).A   = 0.9;
src(2).phi = 0;

Ns = numel(src);

五、从点声源到阵列:生成实值时域信号

在真实测量中,阵列首先拿到的不是复声压,而是一段段实值时域波形。所以程序里先要模拟每个麦克风的时域输出。

对于阵列中任意一个麦克风,设其坐标为 ,第 s 个声源到该麦克风的距离为:

由于本文整体采用的复声压形式为 ,因此每个麦克风接收到的实值时域信号写成:

其中:

  • 表示第 s 个声源的幅值;
  • rs 是该声源到当前麦克风的传播距离;
  • ϕs 是该声源的初始相位;
  • Ns 是声源个数。

从这个公式也可以看出,程序里每一路麦克风信号本质上都是若干个单频余弦信号叠加而成,只不过每个分量的幅值由 衰减决定,相位由传播距离决定。

Matlab 复制代码
%% =========================================================
%  4. 生成 64 路麦克风的实值时域信号
% =========================================================
sig = zeros(Ny, Nx, Nt);

for iy = 1:Ny
    for ix = 1:Nx
        x0 = Xm(iy, ix);
        y0 = Ym(iy, ix);
        z0 = z_meas;

        temp = zeros(1, Nt);

        for s = 1:Ns
            rs = sqrt((x0 - src(s).x)^2 + (y0 - src(s).y)^2 + (z0 - src(s).z)^2);
            temp = temp + (src(s).A / rs) .* cos(2*pi*f0*t + k*rs + src(s).phi);
        end

        sig(iy, ix, :) = temp;
    end
end

%% =========================================================
%  5. 加入白噪声
% =========================================================
sig_noisy = zeros(size(sig));
for iy = 1:Ny
    for ix = 1:Nx
        s0 = squeeze(sig(iy, ix, :));
        s_rms = rms(s0);
        n_rms = s_rms / (10^(SNR/20));
        noise = n_rms * randn(size(s0));
        sig_noisy(iy, ix, :) = s0 + noise;
    end
end

六、从实值时域信号到目标频点复声压

近场声全息在单频分析时,真正使用的不是整段时域信号,而是某一个目标频率下的复声压值。因此,在得到每个麦克风的实值时域信号之后,下一步就是对每一路信号做 FFT,并在目标频率 f0处提取对应的频谱值。

Matlab 复制代码
%% =========================================================
%  6. 对每路实值信号做 FFT,提取目标频点复声压
% =========================================================
Nfft = Nt;
f_axis = (0:Nfft-1) * fs / Nfft;
[~, idx_f0] = min(abs(f_axis - f0));

P_meas_8x8 = zeros(Ny, Nx);

for iy = 1:Ny
    for ix = 1:Nx
        s0 = squeeze(sig_noisy(iy, ix, :));
        S0 = fft(s0, Nfft);
        P_meas_8x8(iy, ix) = S0(idx_f0);
    end
end

% 归一化,仅用于显示,不影响相对分布
P_meas_8x8 = P_meas_8x8 / max(abs(P_meas_8x8(:)));

七、测量面声场的二维傅里叶变换

得到测量面上的复声压矩阵之后,就进入标准的 NAH 处理流程。

首先,对测量面上的复声压分布做二维傅里叶变换,得到其在波数域中的表示:

这里的 是横向波数坐标,表示声场在 x 和 y 方向上的空间频率成分。

在离散计算中,两个方向上的波数采样间隔分别为:

其中 dx 和 dy 分别是阵列在 x 和 y 方向上的阵元间距。于是离散波数坐标可以按等间隔建立起来,从而形成二维波数网格 (KX,KY)。

这一步的意义在上一篇已经讲过:空间域中的二维声场分布,在经过二维傅里叶变换后,被分解为许多不同横向波数的平面波分量。接下来,只需要分别处理这些平面波分量在 z 方向上的传播,就可以大大简化平面到平面的声场反推问题。

八、传播波与倏逝波

在三维空间中,波数关系满足:

因此,kz 需要根据 kx​ 和 ky​ 的大小分情况讨论。

Matlab 复制代码
%% =========================================================
%  7. 基于 8×8 复声压做波数域 NAH
% =========================================================
% 7.1 建立波数域坐标
dkx = 2*pi / (Nx*dx);
dky = 2*pi / (Ny*dy);

kx = (-floor(Nx/2):ceil(Nx/2)-1) * dkx;
ky = (-floor(Ny/2):ceil(Ny/2)-1) * dky;
[KX, KY] = meshgrid(kx, ky);

Kt2 = KX.^2 + KY.^2;

kz = zeros(size(KX));
prop_mask = (Kt2 <= k^2);
evan_mask = (Kt2 >  k^2);

kz(prop_mask) = sqrt(k^2 - Kt2(prop_mask));
kz(evan_mask) = 1j * sqrt(Kt2(evan_mask) - k^2);

% 7.2 测量面 -> 波数域
P_meas_k = fftshift(fft2(ifftshift(P_meas_8x8)));

这一点很关键,因为它意味着:

  • 对传播波而言,主要体现为相位传播;
  • 对倏逝波而言,主要体现为随传播距离增加而快速衰减。

倏逝波虽然不能远距离传播,但它包含近场中的高空间频率信息,因此与声源附近的细节恢复密切相关。这也正是 NAH 能够优于远场成像方法的重要原因之一。

九、测量面到重建面的波数域反推

从测量面 反推到重建面 ​ 时,可以写为:

也就是说,程序中使用的反向传播算子为:

于是重建面上的波数谱为:

最后,再通过二维逆傅里叶变换回到空间域,即可得到重建面复声压:

从物理上理解,这一步的含义其实就是:先把测量面声场分解成一组横向波数分量,然后分别把每个分量沿 z 方向"传回去",最后再把这些分量重新叠加起来,恢复出目标平面上的声场分布。

十、为什么还需要正则化:简单波数截断

理论上,只要把测量面波数谱乘上反向传播算子,就可以得到重建面波数谱。但实际计算中,这一步往往会遇到不稳定问题,尤其是倏逝波部分。原因在于,倏逝波在前向传播过程中会衰减得很快,而在反推时则会被重新补偿,这会导致高波数噪声也被一并放大。

因此,NAH 在实际计算中通常需要引入某种正则化或滤波策略。

在这份代码中,采用的是最简单的一种方式:波数截断。即只保留满足下式的波数分量:

其中截止波数取为:

于是滤波掩膜可以表示为:

最终反推公式变为:

这种处理虽然简单,但这里只是演示 NAH 的基本计算流程。

Matlab 复制代码
% 简单正则化:波数截断
kc = 1.1 * k;
filter_mask = sqrt(Kt2) <= kc;

% 7.3 测量面 -> 重建面(反向传播)
% P(z_recon) = P(z_meas) * exp[-j*kz*(z_meas-z_recon)]
H_backward = exp(-1j * kz * (z_meas - z_recon));

P_recon_k = P_meas_k .* H_backward .* filter_mask;

% 7.4 回到空间域
p_recon_8x8 = fftshift(ifft2(ifftshift(P_recon_k)));

十一、构造参考真值

在真实实验中,我们通常并不知道重建面上的真实声场,因此很难严格判断重建结果到底准不准确。但在仿真中不同,因为声源位置和参数都是我们自己设定的,所以完全可以根据已知声源,直接计算出重建面上的理论参考场。

当前参考场写成:

这里的 rs 表示重建面上任一点到第 s 个声源的距离。

需要说明的是,这个参考场并不参与 NAH 反推本身,它只是为了在仿真完成后,与重建结果做对比,从而判断当前重建效果究竟如何。

Matlab 复制代码
%% =========================================================
%  8. 构造"真实重建面"参考场(用于对比)
% =========================================================
Nshow = 640;
x_show = linspace(-Lx/2, Lx/2, Nshow);
y_show = linspace(-Ly/2, Ly/2, Nshow);
[Xshow, Yshow] = meshgrid(x_show, y_show);

p_true_show = zeros(Nshow, Nshow);

for s = 1:Ns
    rs = sqrt((Xshow - src(s).x).^2 + (Yshow - src(s).y).^2 + (z_recon - src(s).z).^2);
    p_true_show = p_true_show + (src(s).A ./ rs) .* exp(1j * (k*rs + src(s).phi));
end

p_true_show = p_true_show / max(abs(p_true_show(:)));

十二、显示插值

程序中还把 8×8 的结果插值显示到了 640×640 的网格上。插值显示的目的,仅仅是为了让图像更平滑、更连续,便于观察热点位置和空间分布趋势。

也就是说,真正的测量数据依然只有 64 个点;插值只是利用这 64 个点之间的变化趋势,在显示层面上把图画得更细而已。

Matlab 复制代码
%% =========================================================
%  9. 8×8 结果插值到 640×640
% =========================================================
% 9.1 测量面幅值显示插值
P_meas_abs_show = interp2(Xm, Ym, abs(P_meas_8x8), Xshow, Yshow, 'cubic');

% 9.2 重建面复声压插值:实部/虚部分别插值
re_show = interp2(Xm, Ym, real(p_recon_8x8), Xshow, Yshow, 'cubic');
im_show = interp2(Xm, Ym, imag(p_recon_8x8), Xshow, Yshow, 'cubic');
p_recon_show = re_show + 1j * im_show;

% 归一化显示
P_meas_abs_show = P_meas_abs_show / max(P_meas_abs_show(:));
p_recon_show = p_recon_show / max(abs(p_recon_show(:)));

十三、误差评估:怎么判断重建效果

为了对重建效果做一个简单量化,程序中采用了幅值相对误差指标:

其中:

  • 是重建结果;
  • 是理论参考场。

这个指标反映的是重建幅值分布相对于参考真值的整体偏差大小。

Matlab 复制代码
%% =========================================================
%  10. 误差评估
% =========================================================
err_amp = norm(abs(p_recon_show(:)) - abs(p_true_show(:))) / norm(abs(p_true_show(:)));
fprintf('重建面幅值相对误差 = %.4f\n', err_amp);

十四、仿真结果

十五、总结

本文在上一篇近场声全息原理介绍的基础上,进一步给出了一个更贴近真实测量过程的仿真实现思路。与直接在测量面人为构造复声压不同,这里是从两个单频点声源出发,先生成 8×8 麦克风阵列的实值时域信号,再通过 FFT 提取目标频率处的复声压,随后进入波数域,通过传播算子将测量面声场反推到重建面,并最终恢复出重建面上的声场分布。

从结果上看,这段代码不仅演示了近场声全息的完整处理流程,也清楚地说明了几个重要事实:阵列测到的是实值时域信号,单频 NAH 实际使用的是目标频点复声压;插值显示只是视觉增强,不代表真实采样分辨率提高;而倏逝波虽然包含近场细节信息,但同时也会带来数值不稳定,因此必须配合适当的正则化策略。

后续我也会继续围绕阵列信号处理和工程应用展开整理,欢迎持续关注与交流。

相关推荐
汀、人工智能2 小时前
16 - 高级特性
数据结构·算法·数据库架构·图论·16 - 高级特性
大熊背3 小时前
利用ISP离线模式进行分块LSC校正的方法
人工智能·算法·机器学习
XWalnut3 小时前
LeetCode刷题 day4
算法·leetcode·职场和发展
蒸汽求职3 小时前
机器人软件工程(Robotics SDE):特斯拉Optimus落地引发的嵌入式C++与感知算法人才抢夺战
大数据·c++·算法·职场和发展·机器人·求职招聘·ai-native
AI成长日志4 小时前
【笔面试算法学习专栏】双指针专题·简单难度两题精讲:167.两数之和II、283.移动零
学习·算法·面试
旖-旎4 小时前
分治(库存管理|||)(4)
c++·算法·leetcode·排序算法·快速选择算法
青稞社区.4 小时前
ICLR‘26 Oral | 当 LLM Agent 在多轮推理中迷失时:T3 如何让强化学习重新学会主动推理
人工智能·算法·agi
春花秋月夏海冬雪4 小时前
代码随想录刷题 - 贪心Part1
java·算法·贪心·代码随想录
环黄金线HHJX.4 小时前
Tuan符号系统重塑智能开发
开发语言·人工智能·算法·编辑器