☰
MATLAB实现Zernike拟合:从单位圆归一化到最小二乘求解
2026/10/20 5:35:10 网站建设 项目流程

简介:Zernike拟合MATLAB程序是一份面向光学工程、精密制造及科研场景的实用工具包,用于对波前数据进行Zernike多项式拟合与误差量化,适合需要分析镜片表面质量、成像系统像差的研究人员或工程师。压缩包内共7个文件,以6个m脚本和函数为主,覆盖数据预处理、Zernike径向多项式计算、系数求解及可视化等核心环节,并附带1个测试数据mat文件,便于直接运行验证。资源包仅994KB,轻量易用。该资源已有11338人学习,说明在光学数据处理领域具有较高参考价值。通过梳理程序中的主函数与辅助模块,可掌握从波前解包裹到RMS误差评估的完整链路,并学习如何结合MATLAB线性代数工具实现自定义Zernike拟合流程,对理解光学系统性能评估方法有直接帮助。

1. Zernike拟合到底在做什么:先想清楚再写程序

搞光学的人迟早会遇到Zernike拟合。干涉仪测出来的波面、自适应光学的残余波前、眼底相机测出的像差数据,最后都要转成一组Zernike系数,方便后续优化、排名和跨系统比对。说白了,Zernike拟合就是拿若干个定义在单位圆上的正交多项式去逼近一个二维面形,得到每个多项式前面的系数。这个过程和傅里叶展开很像,只是把正弦余弦换成了Zernike多项式,把矩形区域换成了圆形区域。

很多人一开始以为MATLAB里有现成的zernike_fit命令,搜了一圈发现并没有。即便装了图像处理工具箱或者所谓的光学工具箱,官方也没有一个“输入相位图,输出Zernike系数”的现成接口。更多的现成代码散落在File Exchange和各种论文附件里,能用,但往往只适配某一种数据格式、某一种归一化约定。这也是我决定自己整理一份程序的原因:理解内部结构之后,才能在别人的约定和自己的数据之间自由切换。

这篇博文面向的读者比较明确:手里有波面或者相位数据,需要在MATLAB里做一次Zernike拟合,而且希望知道每一步在干什么、参数怎么设、结果怎么验证的工程人员和科研人员。后面所有代码我按“可复制、可运行、能自检”的标准写,尽量不依赖第三方工具箱。

2. 动手之前先定死的事:多项式定义和归一化约定

写代码之前,有一件事必须想清楚:你用的Zernike多项式,是哪个版本的定义。同一个名字,在Noll约定、Zemax Fringe约定、OSA/ANSI约定里,实际数值可能差出一个系数。

2.1 径向多项式与角向项的数学形式

Zernike多项式的标准写法是极坐标形式:

Z_n^m(rho, theta) = R_n^m(rho) * (cos(m*theta),当 m>=0;sin(|m|*theta),当 m<0)

其中径向部分 R_n^m(rho) 是 rho 的多项式:

R_n^m(rho) = sum_{s=0}^{(n-|m|)/2} (-1)^s (n-s)! / [s! ((n+|m|)/2 - s)! ((n-|m|)/2 - s)!] * rho^(n-2s)

n 是径向阶数,m 是角向频率,n 和 m 的奇偶性必须一致,且 |m| <= n。rho 是归一化半径,必须在 0 到 1 之间,所以才会有“单位圆”这个前提。

如果觉得这个公式看着烦,可以把它当做一个类似傅里叶级数的基底:n 控制径向变化的快慢,m 控制旋转对称性。m=0 的项都是旋转对称的,例如活塞项和离焦;m=1 的项是倾斜;m=2 的项是像散;m=3 的项是三叶草;更高阶依次类推。

2.2 不同归一化约定怎么统一

同一个 Zernike 多项式,在光学软件里的实际数值因约定不同可以差出 sqrt(2) 甚至更多。最常见的区分是:

约定径向多项式归一化系数常见使用场景
经典 / Zemax FringeR_n^m1镜头设计、Zemax
NollR_n^mm=0 时 sqrt(n+1),m≠0 时 sqrt(2(n+1))自适应光学
OSA / ANSI部分版本自带 sqrt 系数论文、人眼像差学术对比

这里关键的一点是:基底张成的空间不会变,所以拟合得到的残差、拟合面形都不会变,变的只是系数数值。换句话说,你在MATLAB里算出来的系数,到底要不要乘一个 sqrt(2) 或者 sqrt(n+1),完全取决于你要跟哪个软件对比。

2.3 约定选错的后果

我一直建议在程序开头就把 norm_flag 写死,再配一个注释。因为你处理的如果是Zygo干涉仪导出的数据,用的多半是 Fringe 约定;如果是自己搭建的自适应光学系统,通常用 Noll 约定。把 Noll 算出的系数直接拿来和 Fringe 的系数对比,前几项看起来很像,但在像散、彗差这些 m≠0 的项上会差 40% 以上。我自己有一次处理干涉仪数据时就吃过这个亏,最后逐项核对才找到原因。

3. 程序实现:基函数矩阵与最小二乘求解

下面给出一个不依赖任何工具箱的MATLAB实现,总共分三步:生成模式列表、计算基函数、解线性方程组。

3.1 模式列表的生成

先定义 n 从低到高、m 从 -n 到 n 步进 2 的排列顺序:

function modes = zernike_modes(max_n) modes = zeros(0, 2); for n = 0:max_n for m = -n:2:n modes(end+1, :) = [n, m]; end end end

比如 max_n=3 时,mode 列表长这样:

0,0 1,-1 1,1 2,-2 2,0 2,2 3,-3 3,-1 3,1 3,3

这个顺序不是Noll顺序,也不对应Zemax Fringe顺序,但它逻辑简单,后面只要记住 coeffs(第k项) 对应 modes 第 k 行的 (n,m) 即可。

3.2 径向多项式的向量化计算

径向多项式不要在像素点上套循环,直接把 rho 当矩阵算就行。公式里的求和项数只有 (n-m)/2+1 项,循环开销可以忽略。

function R = zernike_radial(n, m, rho) m = abs(m); if mod(n - m, 2) ~= 0 error('n 和 m 的奇偶性不一致'); end R = zeros(size(rho)); for s = 0:(n - m)/2 coef = (-1)^s * factorial(n - s) / ... (factorial(s) * factorial((n + m)/2 - s) * factorial((n - m)/2 - s)); R = R + coef * rho.^(n - 2*s); end end

3.3 组装完整的Zernike求值函数

把径向部分和角向部分拼起来,并带上归一化因子:

function Z = zernike_eval(n, m, X, Y, norm_flag) if nargin < 5 norm_flag = true; end rho = sqrt(X.^2 + Y.^2); theta = atan2(Y, X); if m >= 0 ang = cos(m * theta); else ang = sin(abs(m) * theta); end R = zernike_radial(n, m, rho); Z = R .* ang; if norm_flag if m == 0 Z = Z * sqrt(n + 1); else Z = Z * sqrt(2 * (n + 1)); end end end

这里注意 X 和 Y 必须是已经归一化到单位圆内的坐标,不能直接拿像素行列号去算。这是新手最容易忽略的一步。

3.4 组矩阵并用反斜杠求解

拟合的本质是解一个线性最小二乘问题:Ax=b,A 的每一列是一个 Zernike 基底在有效像素点上的取值,b 是相位值,x 就是系数。

function [coeffs, modes, fit_result, residual] = zernike_fit_phase(phase, X, Y, max_n, mask) if nargin < 5 || isempty(mask) mask = (X.^2 + Y.^2) <= 1; end idx = find(mask); x = X(idx(:)); y = Y(idx(:)); w = phase(idx(:)); modes = zernike_modes(max_n); A = zeros(numel(x), size(modes, 1)); for k = 1:size(modes, 1) A(:, k) = zernike_eval(modes(k, 1), modes(k, 2), x, y, true); end coeffs = A \ w; fit_result = nan(size(phase)); residual = nan(size(phase)); fit_result(idx) = A * coeffs; residual(idx) = w - A * coeffs; end

MATLAB 的反斜杠对满秩最小二乘问题足够稳定,不需要手写正规方程。只有当阶数取得特别高、或有效像素特别稀疏时才需要考虑正则化,后面我会提到一个简单办法。

4. 影响结果最隐蔽的一环:拟合圆域的处理

很多 Zernike 拟合程序跑出来结果对不上,问题往往不在多项式本身,而在圆心和半径没弄对。

4.1 圆心和半径怎么确定

Zernike 多项式严格定义在单位圆内,rho>1 的区域根本没有定义。所以拟合前必须先把离散坐标映射到单位圆坐标。如果数据本身就是干涉仪输出的圆形光阑图,圆心通常在测量图的几何中心附近,但不总是;如果光阑偏心或者有遮拦,就需要先做圆拟合。

圆拟合和 Zernike 拟合是两件事。圆拟合解决的问题是:给一堆边缘点,求最合适的圆心和半径。最常用的代数方法是拟合方程 x^2+y^2+Ax+By+C=0,解出 A、B、C 后圆心是 (-A/2,-B/2),半径是 sqrt((A/2)^2+(B/2)^2-C)。这个线性方程组在MATLAB里一次反斜杠就能解完,速度很快。

需要特别提醒的是,圆心和半径的精度直接影响归一化坐标,进而影响高阶 Zernike 系数。高阶项对 rho 的幂次很敏感,半径差一个像素,球差项系数可能就差好几个百分点。我的习惯是先做一次圆拟合,算一次低阶 Zernike 拟合,再用拟合残差来反复确认掩膜边界是否干净。

4.2 像素坐标到单位圆坐标的映射

假设你已经得到了圆域的圆心 (cx,cy) 和半径 r_pixel,归一化坐标就是:

xn = (X - cx) / r_pixel; yn = (Y - cy) / r_pixel; rho = sqrt(xn.^2 + yn.^2);

只有 rho <= 1 的像素参与拟合。很多从图片格式读进来的相位图会带着圆形遮罩,遮罩外是NaN或者0,这时候一定要先用 isnan 判断:

mask = ~isnan(phase) & (rho <= 1);

如果直接把整块矩形区域送进拟合矩阵,单位圆外那部分伪数据会严重污染系数,尤其低阶项会出现很奇怪的倾斜和离焦残差。这个问题我在刚接触干涉图处理时踩得很深,后来把掩膜打印出来看才恍然大悟。

4.3 带孔或者多区域的数据怎么办

中心遮挡是另一个常见情况。比如望远镜次镜遮挡,干涉图中心是一团NaN。这种情况不需要把内孔填零,只要把孔内像素排除在mask之外即可。Zernike 多项式原本是完整圆域上的正交系,去掉中心孔后,离散像素点上的基函数不再严格正交,直接反斜杠仍然能得到最小二乘解,但解出来的系数之间会有耦合。想得到比较干净的物理像差系数,建议采用正交化或者迭代方式处理,同时不要让遮挡面积占比过大,否则系数解释要谨慎。

5. 验证程序、评估拟合质量和我踩过的坑

代码写完不等于程序正确,我会先用合成数据做回归测试,再讲一讲几个经常被忽略的工程细节。

5.1 用已知系数做合成数据验证

合成数据的思路很简单:自己设定一组系数,按这组系数生成一张相位图,再加少量高斯噪声,然后跑拟合,看能否把系数恢复出来。

N = 400; x = linspace(-1, 1, N); [X, Y] = meshgrid(x, x); mask = (X.^2 + Y.^2) <= 1; true_coeffs = [0.2, -0.3, 0.8, -0.5, 0.35, 0.15]; modes = zernike_modes(2); phase = zeros(size(X)); for k = 1:length(true_coeffs) phase = phase + true_coeffs(k) * zernike_eval(modes(k,1), modes(k,2), X, Y, true); end phase(~mask) = NaN; [coeffs, fit_modes, fit_result, residual] = zernike_fit_phase(phase, X, Y, 2, mask); disp([true_coeffs', coeffs]);

如果程序没问题,输出的两列会非常接近,差异在 10 的负几次方量级。如果差异很大,先检查归一化约定和 mode 顺序。

有一点要强调:即使没有噪声,恢复的系数和真值也会有微小差异,因为离散网格上的基函数并不像连续域那样严格正交,反斜杠解的是带数值误差的最小二乘问题。阶数越高、网格越密,这个误差越小。如果出现差到不可接受的情况,把“反斜杠求解”换成摩尔-彭罗斯伪逆 pinv(A)*w 有时能改善稳定性。

5.2 残差RMS和阶数选择

拟合做完了,不要只看系数。残差图永远是最直观的诊断工具:

res_rms = sqrt(mean(residual(mask).^2, 'omitnan'));

残差RMS小于测量噪声水平,说明拟合能力已经够用;残差明显呈平滑条纹,说明低阶项没有拟合完,需要增大 max_n;残差整体呈随机颗粒,说明已经到底。判断阶数的时候有一个经验法则:干涉图的空间分辨率越高,可用的 Zernike 阶数也越高,但并不是越高越好。当阶数超过数据实际包含的信息量时,高阶系数开始拟合噪声,出现明显的振荡,这在波前重构里俗称叫“过拟合”,和机器学习里的过拟合是同一个道理。

5.3 我在实际项目中踩过的几个细节坑

第一个坑是模式顺序。同一个第4项,在Noll顺序里可能是离焦,在Fringe顺序里却对应另一个 (n,m)。在从外部文件读取系数时,一定要先确认对方的顺序表,而不是只看“第几个系数”。好在大多数干涉仪软件导出时都带说明,值得仔细读一遍说明文件。

第二个坑是相位包裹。干涉仪测出来的相位经常是包裹的,也就是在 [-pi,pi] 之间跳变。Zernike 拟合只能处理连续相位,必须先做解包裹。MATLAB 里有 unwrap 函数,可以对逐行或逐列处理,干涉图常用的是先对二维空间做解包裹,或者用 File Exchange 上成熟的二维解包裹工具。直接对包裹相位做拟合,跳变处会被当成巨大的面形突变,高阶系数立刻失真。

第三个坑是数据单位。相位图的单位如果是波长,那 Zernike 系数说成“某阶像差的RMS值是多少波”是顺的;但有些数据直接给的是纳米,有些给的是毫弧度。拟合程序本身不关心单位,但后面跟设计指标比对时,单位不一致会非常容易看错。我的习惯是在程序入口统一转成波长单位,并且在输出系数表的表头标注单位。

第四个坑是 A 矩阵的规模。当有效像素超过几十万、max_n 超过20项时,反斜杠的内存占用还是挺明显的,但还在可控范围内。如果确实遇到内存问题,可以不用一次性组 A 矩阵,而是逐项计算再叠加正规方程的 A'*A,或者用迭代法求解。平时几百乘几百的图像,直接用反斜杠完全没问题。

最后再分享一个我自己的小习惯:不管数据来自哪里,保存系数时我都会把 mode 顺序表、归一化约定、单位、圆心半径一起存入同一份CSV或者mat文件里。这样任何一个系数都能反查回原始坐标。这个习惯帮我省了不知道多少返工时间。如果你手头也有自己的 Zernike 拟合程序,建议现在就把这几行注释补上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询