4、线性校准模型:两点校准法(Scale and Offset)的数学推导与实现
上一章我们聊了单点校准,说白了就是只调个偏移量。但实际项目中你会发现,单点校准根本不够用。为什么?因为触摸屏的物理特性和 ADC 采样电路,往往会让触摸坐标既偏了位置,又缩放了比例。
我记得有一次做一款 7 寸工业平板,单点校准后屏幕中心点倒是准了,但四个角偏差越来越大。当时我排查了半天,最后发现是 ADC 参考电压的温漂导致的增益误差。嗯,这就是典型的「既有偏移又有缩放」的场景。
两点校准法,就是专门解决这个问题的。它用一个线性模型,同时修正偏移(Offset)和缩放(Scale)。说白了,就是找到一条直线,把原始坐标映射到目标坐标上。
4.1 为什么需要两点?
先想一个问题:一条直线需要几个点来确定?两个。单点只能确定一个常数偏移,但无法确定斜率。两点校准的本质,就是通过两个已知的参考点,解出这条直线的斜率和截距。
你想想看,触摸屏的原始坐标 (Xraw, Yraw) 和实际物理坐标 (Xphys, Yphys) 之间,如果只考虑线性关系,那就是:
Xphys = a * Xraw + b
Yphys = c * Yraw + d
这里 a 和 c 是缩放因子(Scale),b 和 d 是偏移量(Offset)。四个未知数,需要四个方程。两个点,每个点提供 X 和 Y 两个方程,正好四个方程,解四个未知数。
核心思想:两点校准法假设触摸屏的误差是线性的——即原始坐标与实际坐标之间存在一个固定的缩放和偏移关系。这个假设在大多数电阻屏和部分电容屏上是成立的。
4.2 数学推导:从几何到代数
我们以 X 轴为例来推导。Y 轴完全同理。
假设我们在屏幕上点了两个参考点:
- 点 A:物理坐标 (X1, Y1),触摸原始坐标 (x1, y1)
- 点 B:物理坐标 (X2, Y2),触摸原始坐标 (x2, y2)
对于 X 轴,我们有:
X1 = a * x1 + b
X2 = a * x2 + b
两式相减消去 b:
X1 - X2 = a * (x1 - x2)
a = (X1 - X2) / (x1 - x2)
然后代入求 b:
b = X1 - a * x1
Y 轴同理:
c = (Y1 - Y2) / (y1 - y2)
d = Y1 - c * y1
就这么简单?嗯,数学上确实简单。但实际工程中,坑都在细节里。
我的经验:计算 a 和 c 时,分母 (x1 - x2) 和 (y1 - y2) 不能太小。如果两个参考点靠得太近,分母接近零,计算出的缩放因子会剧烈抖动。我一般要求两个参考点至少相距屏幕对角线长度的 60% 以上。
4.3 参考点的选择策略
参考点选在哪里,直接影响校准精度。我见过不少工程师随便选两个点,结果校准完边缘还是偏的。
下面是我总结的参考点选择原则:
| 原则 | 说明 | 我的建议 |
|---|---|---|
| 距离最大化 | 两个点尽量远离,覆盖屏幕对角区域 | 左上角和右下角,或左下角和右上角 |
| 避开边缘 | 不要选在屏幕最边缘,那里非线性失真大 | 距离边缘 10%-15% 屏幕尺寸的位置 |
| 对称分布 | 两个点尽量关于屏幕中心对称 | 这样能平均分配误差 |
| 可重复性 | 参考点位置要容易点击准确 | 使用十字准星或靶心图案 |
我个人习惯用左上和右下两个点。为什么?因为这样 X 和 Y 方向都能覆盖到最大范围,而且计算时分母最大,数值稳定性最好。
4.4 代码实现:C 语言版
下面是我在实际项目中用的一段代码。它接受两个参考点的物理坐标和原始坐标,计算出校准参数,然后应用校准。
#include <stdint.h>
typedef struct {
float scale_x; // X 轴缩放因子 a
float offset_x; // X 轴偏移 b
float scale_y; // Y 轴缩放因子 c
float offset_y; // Y 轴偏移 d
} calib_params_t;
typedef struct {
uint16_t x;
uint16_t y;
} point_t;
// 计算两点校准参数
// ref_phys: 物理坐标(参考点)
// ref_raw: 触摸原始坐标
// 返回值:校准参数结构体
calib_params_t calib_two_point(
const point_t ref_phys[2],
const point_t ref_raw[2])
{
calib_params_t params;
// 计算 X 轴参数
float dx_raw = (float)(ref_raw[1].x - ref_raw[0].x);
float dx_phys = (float)(ref_phys[1].x - ref_phys[0].x);
// 防止除零
if (dx_raw == 0.0f) {
dx_raw = 0.001f; // 极小值保护
}
params.scale_x = dx_phys / dx_raw;
params.offset_x = (float)ref_phys[0].x - params.scale_x * (float)ref_raw[0].x;
// 计算 Y 轴参数
float dy_raw = (float)(ref_raw[1].y - ref_raw[0].y);
float dy_phys = (float)(ref_phys[1].y - ref_phys[0].y);
if (dy_raw == 0.0f) {
dy_raw = 0.001f;
}
params.scale_y = dy_phys / dy_raw;
params.offset_y = (float)ref_phys[0].y - params.scale_y * (float)ref_raw[0].y;
return params;
}
// 应用校准
// raw: 原始触摸坐标
// params: 校准参数
// 返回值:校准后的物理坐标
point_t apply_calib(const point_t raw, const calib_params_t *params)
{
point_t phys;
phys.x = (uint16_t)(params->scale_x * (float)raw.x + params->offset_x + 0.5f);
phys.y = (uint16_t)(params->scale_y * (float)raw.y + params->offset_y + 0.5f);
return phys;
}
注意:上面的代码中,我加了 0.5f 做四舍五入。如果不加,直接截断取整,坐标会整体偏左偏上 1 个像素。这个细节我曾经忽略过,结果校准完总觉得光标位置差半个像素,排查了半天才发现是取整方式的问题。
4.5 两点校准的局限性
两点校准虽然比单点好很多,但它不是万能的。我遇到过一些情况,两点校准完全不够用:
- 屏幕有旋转误差:触摸屏安装时歪了,或者 LCD 和触摸层之间有角度偏差。两点校准只能处理平移和缩放,处理不了旋转。
- 非线性失真严重:有些廉价触摸屏,边缘区域的非线性误差很大。两点校准假设全屏线性,遇到这种情况边缘还是会偏。
- X 轴和 Y 轴耦合:有些触摸屏的 X 轴读数会受到 Y 轴位置的影响(比如某些老式电阻屏)。两点校准假设 X 和 Y 独立,无法处理耦合。
遇到这些情况怎么办?别急,后面章节会讲三点校准、多点校准,甚至非线性校准模型。两点校准只是起点,不是终点。
4.6 本章知识体系
下面这张图,把两点校准法的核心逻辑串起来了。从输入到输出,每一步都清晰可见。
4.7 工程实现中的避坑指南
代码写完了,数学推导也清楚了,但真正上机调试时,你可能会遇到下面这些问题。这些都是我踩过的坑,分享给你:
- 坐标系方向搞反:触摸屏的 X 轴方向和 LCD 的 X 轴方向可能相反。计算出的 scale 如果是负数,说明方向反了。我一般会在校准后先打印一下 scale 值,正数正常,负数就交换参考点的顺序。
- 浮点数精度问题:在资源受限的 MCU 上,float 运算可能很慢。如果 MCU 没有 FPU,建议把 scale 和 offset 转成定点数。比如 scale 放大 1024 倍存为整数,计算时用移位代替除法。
- 校准参数的持久化:校准参数算出来后,一定要存到非易失存储(如 EEPROM、Flash)里。不然每次重启都要重新校准,用户体验极差。
- 校准触发时机:不要每次触摸都重新计算校准参数。参数只在校准时计算一次,之后一直复用。除非用户主动触发重新校准。
一个小技巧:在校准界面,让用户点击参考点时,不要只取一次点击坐标。我一般会连续采样 5-10 次,去掉最大最小值,然后取平均。这样能有效抑制触摸噪声,提高校准精度。
好了,两点校准法就讲到这里。它的数学很简单,但工程实现中需要注意的细节不少。掌握了这个基础,后面讲三点校准和仿射变换时,你会理解得更快。