3. 坐标系统与光束偏转模型

做Galvo校准,第一个绕不开的就是坐标系统。说白了,激光打标不是简单的「指哪打哪」,而是要把数字世界的坐标,准确映射到物理世界的偏转角度上。

我记得刚入行时,遇到过一台振镜,标定总是差那么几十微米。换了好几块控制卡都没解决。最后发现,是我没搞懂它内部的坐标变换顺序。从那以后,我养成了一个习惯:拿到任何一套振镜系统,先画坐标变换流程图。

3.1 核心坐标系统一览

一套典型的双轴Galvo系统,至少涉及以下5个坐标空间:

序号 坐标系统 说明 典型单位
1 用户空间 (User Space) CAD/软件里看到的理想坐标,通常就是你要打的图案坐标 mm / inch
2 校正空间 (Corrected Space) 经过畸变、非线性补偿后的中间坐标 mm
3 扫描器空间 (Scanner Space) 控制卡内部的16位、18位等数字量 counts
4 偏转角空间 (Angle Space) X/Y检流计的实际机械转角 度 (°)
5 场镜像空间 (Field Image Space) 激光经过f-theta场镜后,在工件表面的实际光斑位置 mm

嗯,这里要注意:很多初学者以为用户坐标直接等于偏转角坐标,其实差得远。中间隔了至少三层变换:非线性补偿、振镜响应、场镜畸变。

3.2 光束偏转的数学模型

我习惯用一个简化模型来解释Galvo的偏转行为。对于理想f-theta场镜系统,偏转角θ与像高y的关系是:

y = f * θ

其中f是场镜焦距,θ是偏转角(弧度)。但实际情况远没那么简单——

为什么? 因为振镜轴的旋转中心不在场镜入瞳处。X镜和Y镜之间有物理间距,这个间距会导致「枕形畸变」和「梯形畸变」。我记得在给一台大光斑系统做标定时,忽略了这10mm的镜间距,结果角落的位置偏差直接到了0.5mm。

常用的模型分为三类:

  1. 线性模型 – 简单缩放,仅适合极小视场(< 50×50mm)
  2. 多项式模型 – 最常用,二阶或三阶多项式补偿畸变
  3. 物理光学模型 – 基于镜间距、反射定律、场镜公式的解析模型

我个人项目里,90%的情况用二阶或三阶多项式就够用了。除非你做的系统视场超过300×300mm或者对精度要求≤10μm,否则没必要上物理模型——调参时间太长。

3.3 经典变换流程

下面这张图是我每次做标定前必画的结构图。它帮你理清「用户坐标」到「振镜命令」到底经历了什么。

Galvo坐标变换流程 用户坐标 (x_user, y_user) / mm 校正坐标 (x_corr, y_corr) / mm 扫描器数字量 (dacX, dacY) / counts 偏转角 (θx, θy) / ° 振镜响应/校准 场镜像空间 (目标点) (x_out, y_out) / mm 场镜畸变模型 标定本质:建立逆映射 1. 实测已知点 (x_out, y_out) → 记录对应偏转角 (θx, θy) 2. 反向建立 (x_user, y_user) → (θx, θy) 的映射参数 3. 运行时正向应用:用户坐标 → 校正 → 数字量 → 偏转

从图上你能看到两个并行的通路。上面那条是「正向变换」:从用户坐标到最终的数字量输出。下面那条是「反馈路径」:用实际测量点来反向推演模型参数。标定的核心,就是让这两个通路互相收敛。

3.4 实际的变换步骤(代码级)

光说理论不写代码等于白讲。我平时在项目中用C++或者Python实现这个流程。给你看一段我常用的标定预处理函数——它负责把用户坐标转换到扫描器坐标系:

// 用户坐标 → 校正坐标 (使用多项式模型)
// params[]: 畸变补偿系数数组
void userToCorr(double xu, double yu, double *xc, double *yc, double params[][6]) {
    double r2 = xu*xu + yu*yu;  // 半径平方
    // 二阶径向畸变补偿
    *xc = xu * (1 + params[0][0] * r2 + params[0][1] * r2*r2);
    *yc = yu * (1 + params[1][0] * r2 + params[1][1] * r2*r2);
    // 加上切向畸变项(枕形/梯形)
    *xc += params[0][2] * (r2 + 2*xu*xu) + 2*params[0][3]*xu*yu;
    *yc += params[1][2] * (r2 + 2*yu*yu) + 2*params[1][3]*xu*yu;
}

// 校正坐标 → 扫描器数字量
double corrToDac(double xc, double scaleFactor, double offset, int bitRes) {
    double maxCount = pow(2, bitRes) - 1;
    double normalized = xc * scaleFactor + offset;
    return normalized * maxCount / 2.0 + maxCount / 2.0;
}

这段代码很简单,但有几个坑:

  • 参数顺序 – 径向项和切向项的顺序不同厂商的文档定义不同,我亲自踩过雷。
  • 符号约定 – X/Y镜的旋转方向决定了scaleFactor的正负,拿到新设备先空跑一个小方块验证方向。
  • 比特精度 – 16位DAC用±10V模式,实际输出范围不一定是0~65535,有的是±32768。

曾经我吃过的大亏:有一台振镜用的是±5V输出模式,但我按±10V去算offset,结果整个场偏移了20mm。当时打印了50个标定点全废了。后来我养成一个习惯:每次写新标定算法,先用「原点单点」验证坐标映射是否正确,再跑格。

3.5 坐标归一化与单位映射

实际工程中,用户输入可能是毫米、微米甚至像素。我建议所有内部计算统一为毫米,最后再根据场镜规格映射到DAC值。一个常用的转换表格:

用户单位 输入值 内部归一化 备注
mm 100, 50 100.0, 50.0 无需转换
μm 100000, 50000 100.0, 50.0 除以1000
像素(1024×768) 512, 384 根据DPI换算 需要场镜工作范围

嗯,这里我建议:

在控制卡驱动层之前,增加一个「单位适配层」。这样无论上游软件用啥单位,你都不用改标定核心代码。我习惯用一个结构体存储所有配置:

struct GalvoConfig {
    double fieldSize;       // 标定场幅 (mm)
    double focalLength;     // 焦距 (mm)
    double mirrorGap;       // 镜间距 (mm)
    double bitsResolution;  // DAC位数
    int dacRangeMV;         // DAC电压范围 (mV)
    // ... 其他参数
};

3.6 逆映射与迭代求解

实际标定时,我们往往已知目标光斑位置(实测得到),需要反推应该给振镜发什么数字量。这涉及到逆映射求解——因为畸变模型通常不是线性可逆的。

我常用的两种方法:

  1. 查表法 – 提前将整个场网格的「用户坐标⇔DAC值」关系存入查找表,运行时空闲插值。适合固定场幅,速度快。
  2. 牛顿迭代法 – 对于动态场幅或变焦系统,在线求解方程 y = f(θ) 的逆。收敛3~5次即可。

说实话,如果你做的是固定打标应用,查表法香得很。但要注意网格密度——我以前用21×21的网格做查表,拐弯处误差超了。后来改为41×41,这个问题才解决。

坐标系统总结要点:

  • 记住五层坐标空间:用户→校正→扫描器→偏转角→场镜像
  • 正向变换用多项式模型,反向变换用查表或迭代
  • 注意极性、镜间距、DAC位数的工程细节
  • 先验证原点,再跑格——省时间也省耗材

好了,这一章的内容就这些。把坐标系统吃透了,后面再做网格标定和误差补偿,你会发现思路特别清晰——因为你已经知道每个环节在「哪个空间」做事了。