简介:针对光学干涉计量、光谱分析和成像技术中的相位包裹问题,这份代码资源提供了基于最小二乘法的相位解包裹完整实现。压缩包内共2个m文件,主函数完整覆盖数据预处理、连续相位模型建立、误差平方和损失函数定义、优化求解及边缘后处理等步骤;配套示例脚本用于生成模拟包裹相位并调用主函数显示解包裹结果,便于对照分析相位恢复前后的变化。整个包仅1KB,体积小巧却涵盖核心算法逻辑,方便快速阅读与迁移使用。目前已有314人学习下载,适合光学工程、图像处理、信号处理等领域的研究者与学生,既可作为理解相位解包裹原理的入门示例,也可直接嵌入到相关测量或成像项目中解决实际相位恢复问题,具有较高的实用与参考价值。
1. 相位包裹与最小二乘法:不沿路径积分,直接解连续相位
干涉计量、数字全息和 InSAR 处理里,探测器只能记录强度,恢复出的相位被人为截断到(-π, π],也就是包裹相位。真正需要的是空间连续的真实相位,可一旦相邻像素差超过 π,就会生成一圈圈 2π 跳变。传统解包裹沿路径累加包裹相位差,在残差点附近会陷入路径依赖;最小二乘解包裹则没有路径概念,它把整个相位场当作未知变量,用二次型拟合所有相邻像素的包裹差分,一次全局求解得到连续相位。512×512 的干涉图在普通台式机上通常能秒级处理,这个速度优势来自把问题转成了泊松方程。
下面从相位包裹的数学结构出发,拆解phsunwrap.m与zuoye.m,从包裹差分的散度构造、泊松方程推导,到 DCT 频域求解,再到模拟数据验证。整个过程不需要lsqnonlin这类像素级优化器,适合手里有干涉条纹、波前图或者 InSAR 相位图,想把 2π 跳变稳定抹平的人。
2. 从包裹差分到泊松方程:最小二乘解包裹的数学选型
2.1 为什么选全局最小二乘而不是路径积分
设包裹相位为ψ(x,y),真实相位为φ(x,y),两者之间满足ψ = wrap(φ),也就是φ = ψ + 2πk(x,y),整数k随像素位置变化。解包裹的关键是求出每一处的k。传统方法沿一条积分路径逐点累加相邻包裹相位差,并让每次累加值落在(-π, π]。这个思路实现简单,但路径一旦经过残差点,不同方向积分会得到不同结果。残差点对应 2π 环绕误差,噪声、遮挡和不连续边界都会制造大量残差点,路径选择就成了麻烦事。
最小二乘法换了个角度:不显式求每个像素的k,而是找一个连续相位场φ^,使它的梯度在最小二乘意义下最接近包裹相位差。目标函数写为
E(φ) = Σ [ (Δxφ - Δxψ)^2 + (Δyφ - Δyψ)^2 ]
其中Δxψ = wrapToPi(ψ(i+1,j) - ψ(i,j)),Δyψ同理,wrapToPi把差值映射回主值区间,消除 2π 歧义。因为对误差平方求和,少数错误包裹差分会被整体平滑吸收,不会像路径积分那样沿路径一路放大。这个天然的抗噪特性,是它在光学干涉、InSAR 相位重建里被持续采用的原因。
2.2 包裹差分、散度与 Neumann 边界
对目标函数求导并令导数为零,方程组化简为离散泊松方程:
Δxxφ + Δyyφ = ρ
这里ρ是包裹差分的散度。用图像中心差分离散,ρ(i,j) = Δxψ(i,j) - Δxψ(i-1,j) + Δyψ(i,j) - Δyψ(i,j-1),边界处缺失项按零处理。也就是说,ρ完全来自相邻像素的相位跳变统计,有残差点的区域ρ自然非零。下面这段代码构造ρ,这是phsunwrap.m的第一步:
dx = zeros(M, N); dy = zeros(M, N); dx(:, 1:N-1) = wrapToPi(psi(:, 2:N) - psi(:, 1:N-1)); dy(1:M-1, :) = wrapToPi(psi(2:M, :) - psi(1:M-1, :)); rho = zeros(M, N); rho(1:M-1, :) = rho(1:M-1, :) + diff(dy, 1, 1); rho(:, 1:N-1) = rho(:, 1:N-1) + diff(dx, 1, 2);dx、dy分别保存横向和纵向相邻像素的包裹差分;diff(dy,1,1)沿第一维做差分,等价于dy(i,j) - dy(i-1,j),配合rho(1:M-1,:)的赋值,天然把i=1的上边界当作零。容易忽略的细节是:dx、dy最后一列和最后一行保留为零,而不是直接从矩阵里删掉,因为散度计算要求尺寸与原始相位一致,否则 DCT 维度对不上。
2.3 DCT 解法与特征值分母
离散泊松方程在 Neumann 边界条件下可以被二维 DCT-II 对角化。当相位场在边界满足零法向导数时,余弦基函数天然满足边界条件,拉普拉斯算子在余弦基下变成逐频率乘法。原本要解一个超大规模稀疏线性系统,现在只需要两次变换:
Φ = idct2( dct2(ρ) ./ Λ )
Λ的第(m,n)项是Λ(m,n) = 2(cos(πm/M) + cos(πn/N) - 2),m=0..M-1、n=0..N-1。波数越高分母绝对值越大,高频噪声会被自然抑制,这也是最小二乘解包裹比直接积分离散差分更平滑的原因之一。Λ(0,0)在m=n=0时为 0,对应整体常数偏置不可观测,稳妥做法是把它置为 1 避开除零。下面表格归纳了关键矩阵的含义:
| 变量/矩阵 | 构造方式 | 代表含义 |
|---|---|---|
dx | wrapToPi(横向前向差分) | 真实横向梯度的最小二乘估计源 |
dy | wrapToPi(纵向前向差分) | 真实纵向梯度的最小二乘估计源 |
rho | 对dx、dy做离散散度 | 包裹差分在局部的不平衡量 |
denom | 2(cos(πm/M)+cos(πn/N)-2) | 拉普拉斯特征值,零频除外 |
phi | idct2(dct2(rho)./denom) | 连续相位场,带任意常数偏置 |
DCT 解法总体复杂度是O(MN log(MN)),比逐像素优化快几个数量级。对 1024×1024 的干涉图,单次求解通常不需要等待;而直接调lsqnonlin求解同样规模的变量,至少面对 10^6 个决策变量,内存和迭代次数都不现实。下一章把整个算法落到phsunwrap.m里。
3. phsunwrap.m 实现解析:DCT 快速求解与边界处理
3.1 函数签名与参数约定
phsunwrap.m的典型入口是phi = phsunwrap(psi),输入psi是二维包裹相位矩阵,单位弧度。函数第一步要做psi = double(wrapToPi(psi)),这有两个作用:统一相位区间到(-π, π],以及把single类型转为double,避免dct2在某些环境下不接受非双精度输入。返回值phi是连续相位场,但整体偏置是任意的,因为零频不可观测。后续与参考相位比较时,必须先估计并去掉这个常数偏置。
光学里输入并不总是完整矩形,例如干涉图有坏点、遮挡或者无效区域,常见做法是填NaN。普通 DCT 解法要求稠密均匀网格,遇到NaN会直接输出NaN,所以这一章先处理无NaN的数据,带遮挡的加权方案放到第五章。
3.2 核心代码:差分、散度、DCT 解算
下面是一份完整可运行的phsunwrap.m等价实现。代码没有调用lsqnonlin,因为像素级优化器在这个问题上既慢又占内存;DCT 频域解法才是实际项目里的默认选项。
function phi = phsunwrap(psi) % 最小二乘相位解包裹,DCT 快速解法 % 输入: psi 包裹相位, M×N, rad % 输出: phi 连续相位, 相对值可比较, 带任意常数偏置 psi = double(wrapToPi(psi)); [M, N] = size(psi); % 1. 横向/纵向包裹相位差 dx = zeros(M, N); dy = zeros(M, N); dx(:, 1:N-1) = wrapToPi(psi(:, 2:N) - psi(:, 1:N-1)); dy(1:M-1, :) = wrapToPi(psi(2:M, :) - psi(1:M-1, :)); % 2. 散度,边界按零处理 rho = zeros(M, N); rho(1:M-1, :) = rho(1:M-1, :) + diff(dy, 1, 1); rho(:, 1:N-1) = rho(:, 1:N-1) + diff(dx, 1, 2); % 3. DCT 特征值分母,零频置 1 ky = (0:M-1).'; kx = 0:N-1; denom = 2 * (cos(pi * ky / M) + cos(pi * kx / N) - 2); denom(1, 1) = 1; % 4. 频域相除后逆 DCT F = dct2(rho) ./ denom; phi = idct2(F); % 5. 去掉均值,便于显示和误差比较 phi = phi - mean(phi(:)); end第一步用wrapToPi把相位差压缩到主值区间,相当于寻找每个相邻像素最合理的 2π 倍数。第二步用diff构造散度,注意写的是rho(1:M-1,:) = rho(1:M-1,:) + diff(dy,1,1),不能直接写rho = diff(dy,1,1),后者会让矩阵尺寸变成(M-1)×N,后续dct2就会错位。第三步的denom通过外积广播生成M×N矩阵,denom(1,1)必须改成 1,否则 0/0 产生NaN。
如果你的 MATLAB 版本低于 R2016b,第三步的隐式扩展可能不生效,需要改成:
denom = 2 * (bsxfun(@plus, cos(pi * ky / M), cos(pi * kx / N)) - 2);3.3 两个容易出错的边界细节
第一个坑是零填充。dx、dy在边界处补零,diff算出的散度在边界上只有一个方向的分量,这正好对应 Neumann 边界条件。如果换成gradient函数来构造散度,边界处理逻辑和 DCT 隐含延拓不一致,解出来的相位场边缘会出现扭曲。这个坑在图像拼接、多帧干涉序列里尤其隐蔽,因为单帧看起来“差不多”,但连续帧之间的相对误差会被放大。
第二个坑是常数偏置。denom(1,1) = 1不是数学近似,而是告诉求解器零频分量不参与约束。输出phi和真实相位可能差任意常数c,这个c需要靠已知控制点或者相位场均值归零来校正。加权最小二乘框架可以在目标函数里加入绝对相位参考,但那是工程优化层面的事,对基础算法理解来说,先接受这个常数偏置即可。
提示:
wrapToPi来自 Mapping Toolbox,没有该工具箱时可以用angle(exp(1i*psi))原地替换;dct2/idct2在 Image Processing Toolbox 中提供,没有的话可以用四次镜像延拓加fft2替代,文件交换区有不少现成实现。
不同求解路径的能力边界可以参考下表,我用复杂度而不是硬件实测来描述,因为具体速度依赖机器和版本:
| 求解方式 | 复杂度 | 内存占用 | 适用场景 |
|---|---|---|---|
| DCT 频域求解 | O(MN log(MN)) | 几个全矩阵 | 非加权、无NaN、密集规则网格 |
| 预条件共轭梯度 | 每轮O(MN log(MN)) | 稀疏矩阵加向量 | 加权、遮挡区域 |
lsqnonlin像素级优化 | 迭代加 Jacobian 组装 | (MN)^2量级 | 仅教学演示或极小规模 |
4. zuoye.m 实测脚本:仿真相位生成、调用解包裹与误差评估
4.1 生成可控的模拟包裹相位
zuoye.m是配套验证脚本,它体现的是完整测试工作流:构造已知真相位、模拟包裹和噪声、调用phsunwrap、统计误差。我常用高斯峰作为真相位场,原因是高斯峰梯度平缓且解析式简单,任意像素的真实相位都已知;边缘趋近常数,边界法向导数接近零,与phsunwrap.m隐含的 Neumann 边界条件匹配。用wrapToPi包裹后,峰值周围会形成一圈圈 2π 跳变环,能直观观察解包效果。
脚本开头可以这样写:
%% zuoye.m —— 最小二乘相位解包裹验证 N = 256; x = linspace(-2, 2, N); [X, Y] = meshgrid(x, x); truePhase = 4 * exp(-0.5 * (X.^2 + Y.^2)); sigma = 0.1; % 噪声标准差,单位 rad wrapped = wrapToPi(truePhase + sigma * randn(N, N));truePhase幅值取 4 rad,能让包裹后的跳变足够多,又不至于让相邻像素间的真实相位差超过 π。sigma控制相位噪声强度,这里直接加在相位域而不是强度域,是为了单独考察噪声对解包裹算法的影响。
4.2 运行解包裹并输出误差指标
调用phsunwrap后不能直接把输出和真相位相减,因为解包结果带一个任意常数偏置。习惯上先用中位数估计这个偏置,再计算包裹后的误差和 RMS,同时统计输入相位里的残差点数量:
phi_rec = phsunwrap(wrapped); % 消除常数偏置后统计误差 offset = median(wrapToPi(truePhase(:) - phi_rec(:))); err = wrapToPi(phi_rec - truePhase + offset); rms = sqrt(mean(err(:).^2)); % 统计残差点:环绕一周相位跳变和不为 0 dR = wrapToPi(wrapped(2:N, 1:N-1) - wrapped(1:N-1, 1:N-1)); dD = wrapToPi(wrapped(2:N, 2:N) - wrapped(2:N, 1:N-1)); dL = wrapToPi(wrapped(1:N-1, 2:N) - wrapped(2:N, 2:N)); dU = wrapToPi(wrapped(1:N-1, 1:N-1) - wrapped(1:N-1, 2:N)); residue = dR + dD + dL + dU; residueCount = sum(abs(residue) > 1e-6); fprintf('RMS=%.3f rad, residue=%d\n', rms, residueCount);残差点的计算本质是沿每个 2×2 像素块的四条边累加包裹相位差;没有噪声且真实相位梯度平滑时,这个环路和应该为零。噪声一旦让某条边包裹出错,环路和就是 ±2π,对应的像素块记为残差点。phsunwrap不会消除残差点,它只是让最终相位场在残差区域保持整体最小误差。
4.3 噪声水平与残差点数的影响
实际测量中相位噪声很难完全避免。把sigma从 0.05 提高到 0.2 rad,能看到两类变化:一是残差点数量快速增加,二是解包 RMS 上升。不同随机种子的具体数值会有差异,趋势大致如下:
sigma(rad) | 残差点数量级 | 解包 RMS (rad) |
|---|---|---|
| 0 | 0 | 1e-10 量级 |
| 0.05 | 几十个 | 0.02~0.04 |
| 0.1 | 数百个 | 0.05~0.08 |
| 0.2 | 数千个 | 0.10~0.20 |
当残差点密度超过每个 10×10 像素窗口约一个时,非加权最小二乘解出的相位场虽然整体光滑,局部却可能现出 2π 量级的裂痕,RMS 也会跳到 0.3 rad 以上。继续增大噪声会让残差区域连成片,纯 DCT 解法接近上限。
5. 残差点与加权最小二乘:在噪声场景下扩大适用范围
5.1 什么时候该上加权最小二乘
非加权最小二乘把所有像素的包裹差分放在同一权重上,残差点和坏像素带来的误差会被均匀摊平。如果干涉图只有少量孤立残差点,这个摊平是优点;如果有遮挡、阴影边界或者大面积低相干区,未加权的解会在这些区域产生明显的条纹泄漏。加权最小二乘把目标函数改成
E(φ) = Σ w_ij [ (Δxφ - Δxψ)^2 + (Δyφ - Δyψ)^2 ]
w_ij来自质量图,例如相位导数方差、残差点密度或相干系数。低质量区域权重调小,高质量区域主导求解,遮挡区域权重设为 0 等于完全跳过无效数据。此时泊松方程系数不再恒定,DCT 不能直接套用,常见做法是预条件共轭梯度,权重矩阵作为迭代输入。
一个简单的权重生成办法是用残差点密度做局部平滑。zuoye.m里已经算出residue,把二值化结果在 5×5 窗口卷积,就得到每个像素附近的残差密度,再设置阈值:
mask = conv2(double(abs(residue) > 1e-6), ones(5) / 25, 'same'); weight = 0.2 + 0.8 * (mask < 0.05);weight越接近 1 表示该区域质量越高;残差密集区权重会降到 0.2 左右,保留一定约束力但不主导求解。这个经验公式不是标准答案,实际处理时最好先看残差点分布再调阈值。
5.2 用回代梯度误差与残差点计数验证解包质量
无论用 DCT 还是迭代法,输出相位都必须经过验证才算闭环。我通常做三个检查:一是残差点计数,在包裹域计算,不会因解包而改变,反映输入数据质量;二是回代梯度误差,把解包相位重新做包裹差分,和输入差分比较,最大值远小于 π 说明没有新引入 2π 跳变;三是重构误差,有真相位或拟合相位时可观测 RMS。
下面这段检查代码可以直接加在zuoye.m末尾:
dx_est = wrapToPi(phi_rec(:, 2:N) - phi_rec(:, 1:N-1)); dx_in = wrapToPi(wrapped(:, 2:N) - wrapped(:, 1:N-1)); gradMax = max(abs(dx_est(:) - dx_in(:)), [], 'all'); fprintf('gradient check: %.3f rad, residue: %d\n', gradMax, residueCount);如果gradMax接近 π,说明解包相位在某处又跨过了包裹边界,需要回到sigma设置或权重掩膜上找原因。把这些检查常驻在实验脚本里,每次调整参数后就能快速判断是噪声本身变大,还是解包裹算法在某片区域失效。
本文还有配套的精品资源,点击获取