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。
常用的模型分为三类:
- 线性模型 – 简单缩放,仅适合极小视场(< 50×50mm)
- 多项式模型 – 最常用,二阶或三阶多项式补偿畸变
- 物理光学模型 – 基于镜间距、反射定律、场镜公式的解析模型
我个人项目里,90%的情况用二阶或三阶多项式就够用了。除非你做的系统视场超过300×300mm或者对精度要求≤10μm,否则没必要上物理模型——调参时间太长。
3.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 逆映射与迭代求解
实际标定时,我们往往已知目标光斑位置(实测得到),需要反推应该给振镜发什么数字量。这涉及到逆映射求解——因为畸变模型通常不是线性可逆的。
我常用的两种方法:
- 查表法 – 提前将整个场网格的「用户坐标⇔DAC值」关系存入查找表,运行时空闲插值。适合固定场幅,速度快。
- 牛顿迭代法 – 对于动态场幅或变焦系统,在线求解方程 y = f(θ) 的逆。收敛3~5次即可。
说实话,如果你做的是固定打标应用,查表法香得很。但要注意网格密度——我以前用21×21的网格做查表,拐弯处误差超了。后来改为41×41,这个问题才解决。
坐标系统总结要点:
- 记住五层坐标空间:用户→校正→扫描器→偏转角→场镜像
- 正向变换用多项式模型,反向变换用查表或迭代
- 注意极性、镜间距、DAC位数的工程细节
- 先验证原点,再跑格——省时间也省耗材
好了,这一章的内容就这些。把坐标系统吃透了,后面再做网格标定和误差补偿,你会发现思路特别清晰——因为你已经知道每个环节在「哪个空间」做事了。