4、校准算法入门:线性校准模型、三点校准法、最小二乘法拟合
触摸屏的校准,说白了就是解决一个数学问题:你点下去的那个物理坐标,和屏幕显示的逻辑坐标之间,到底差了多少?
我刚开始做触摸驱动的时候,总觉得校准不就是加减乘除嘛,有什么难的?结果第一次把触摸屏焊到嵌入式板子上,点左上角,光标却跑到了中间偏右的位置。嗯,那会儿我才意识到——没有校准,触摸就是废的。
这一章,我们就从最简单的线性模型讲起,再到经典的三点校准法,最后聊聊更精确的最小二乘法拟合。你不需要精通线性代数,但得理解背后的直觉。
4.1 线性校准模型:从“点歪了”到“点准了”
先问一个问题:为什么触摸屏需要校准?
因为物理层和逻辑层之间存在偏移、旋转、缩放。电阻屏的电压读数会漂,电容屏的感应矩阵也有公差。你想想看,一个 1024×600 的屏幕,触摸控制器读回来的原始坐标可能是 (512, 300),但实际手指按下去的位置,对应的逻辑坐标可能是 (500, 290)。
这个映射关系,在大多数情况下是线性的。也就是说,我们可以用一个简单的公式来描述:
X_logic = a * X_raw + b * Y_raw + c
Y_logic = d * X_raw + e * Y_raw + f
这里 a, b, c, d, e, f 就是我们要找的校准参数。一旦确定了这六个参数,任何原始坐标都能被映射到正确的逻辑位置。
核心要点:线性校准模型假设触摸屏的物理坐标与逻辑坐标之间是仿射变换关系。这个假设在大多数消费级触摸屏上成立,除非你的屏幕本身有严重的非线性畸变。
我个人习惯把这种模型叫做“六参数模型”。为什么是六个?因为二维平面上的仿射变换,正好有六个自由度——两个方向的平移、两个方向的缩放、一个旋转、一个剪切。你不需要记住这些术语,只需要知道:六个参数,足够描述大多数触摸屏的偏差。
4.2 三点校准法:最经典的工程实践
既然有六个未知数,那理论上我们只需要三个已知点就能解出来。为什么?因为每个点提供两个方程(X 和 Y),三个点就是六个方程,刚好解六个未知数。
这就是三点校准法的数学基础。我在项目中遇到过很多次,工程师为了省事只做两点校准,结果屏幕边缘的误差大到无法接受。三点校准,是性价比最高的方案。
4.2.1 校准步骤
- 在屏幕上显示三个校准点——通常选在左上、右下、中间,或者左上、右上、左下。
- 用户依次点击这三个点,系统记录下每次点击的原始物理坐标 (X_raw, Y_raw)。
- 已知这三个点的逻辑坐标(比如 (50, 50), (974, 550), (512, 300))。
- 代入线性方程组,求解六个参数。
具体怎么解?我们来看一个简化版的 Python 实现:
import numpy as np
def three_point_calibration(raw_points, logic_points):
"""
raw_points: 三个原始坐标点,格式 [(x1,y1), (x2,y2), (x3,y3)]
logic_points: 对应的三个逻辑坐标点,格式 [(x1,y1), (x2,y2), (x3,y3)]
返回校准参数 [a, b, c, d, e, f]
"""
# 构建矩阵 A (6x6)
A = []
B = []
for (rx, ry), (lx, ly) in zip(raw_points, logic_points):
A.append([rx, ry, 1, 0, 0, 0])
A.append([0, 0, 0, rx, ry, 1])
B.append(lx)
B.append(ly)
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
# 解线性方程组 A * params = B
params = np.linalg.solve(A, B)
return params.tolist()
# 示例用法
raw = [(100, 100), (900, 100), (500, 500)]
logic = [(50, 50), (970, 50), (512, 300)]
params = three_point_calibration(raw, logic)
print(f"校准参数: {params}")
工程小技巧:三点校准的精度高度依赖于校准点的位置分布。我建议三个点尽量分散,不要选在一条直线上。如果三个点共线,矩阵会奇异,解出来的参数完全不可用。
4.2.2 三点校准的局限性
三点校准虽然简单,但它有一个隐含假设:用户点击的误差为零。你想想看,这现实吗?手指按下去的时候,总会有几像素的抖动。如果只用三个点,任何一个点的点击误差都会直接污染全部六个参数。
我曾经在一个量产项目中吃过这个亏——产线上用三点校准,结果 10% 的机器校准后边缘误差超过 5 个像素。排查了半天,发现是操作员点击校准点时手抖了一下。从那以后,我对三点校准的态度就是:快速验证可以用,量产慎用。
4.3 最小二乘法拟合:用冗余数据换精度
既然三个点不够稳,那我们就用更多的点。比如五个点、九个点,甚至十六个点。每个点都提供一组方程,但点多了,方程组就变成超定的了——方程数多于未知数。
超定方程组没有精确解,但我们可以找一个误差最小的近似解。这就是最小二乘法干的事。
4.3.1 数学直觉
最小二乘法的核心思想很简单:让所有校准点的预测误差的平方和最小。
假设我们有 N 个校准点(N > 3),每个点都有一个预测坐标和实际坐标。我们定义误差为:
error = Σ (预测X - 实际X)² + Σ (预测Y - 实际Y)²
然后我们调整六个参数,让这个 error 尽可能小。数学上,这可以通过正规方程或奇异值分解来求解。
4.3.2 Python 实现
import numpy as np
def least_squares_calibration(raw_points, logic_points):
"""
使用最小二乘法求解校准参数
raw_points: N个原始坐标点,格式 [(x1,y1), ..., (xN,yN)]
logic_points: N个对应的逻辑坐标点
"""
N = len(raw_points)
# 构建矩阵 A (2N x 6)
A = np.zeros((2*N, 6))
B = np.zeros(2*N)
for i, (rx, ry), (lx, ly) in enumerate(zip(raw_points, logic_points)):
A[2*i] = [rx, ry, 1, 0, 0, 0]
A[2*i+1] = [0, 0, 0, rx, ry, 1]
B[2*i] = lx
B[2*i+1] = ly
# 使用最小二乘法求解 (A^T * A) * params = A^T * B
params, residuals, rank, s = np.linalg.lstsq(A, B, rcond=None)
return params.tolist()
# 示例:使用9个校准点
raw_9 = [(50,50), (512,50), (974,50),
(50,300), (512,300), (974,300),
(50,550), (512,550), (974,550)]
logic_9 = [(50,50), (512,50), (974,50),
(50,300), (512,300), (974,300),
(50,550), (512,550), (974,550)]
params_ls = least_squares_calibration(raw_9, logic_9)
print(f"最小二乘校准参数: {params_ls}")
注意:最小二乘法虽然精度高,但计算量比三点校准大。在资源受限的 MCU 上,建议在 PC 端或上位机完成参数计算,然后把参数烧录到设备中。运行时只需要做一次矩阵乘法,开销很小。
4.4 三种方法对比
我把这三种方法放在一起对比一下,方便你根据实际场景选择:
| 方法 | 所需点数 | 精度 | 抗干扰能力 | 计算复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 线性模型(理论) | 至少 3 点 | 取决于点分布 | 低 | 低 | 数学推导 |
| 三点校准法 | 3 点 | 中等 | 低(单点误差影响大) | 极低 | 快速验证、原型调试 |
| 最小二乘法 | ≥ 4 点(推荐 9 点) | 高 | 高(多点平均抵消误差) | 中等 | 量产校准、高精度设备 |
我个人在量产项目中,几乎只用最小二乘法。虽然计算参数时多花几毫秒,但换来的是整批产品的一致性。三点校准更适合你在开发板上快速验证触摸是否能用。
4.5 知识体系总览
下面这张图,把本章的核心逻辑串起来了:
我的建议:如果你刚开始做触摸校准,先用三点校准法跑通流程,感受一下参数变化对触摸精度的影响。等你对整个链路熟悉了,再切换到最小二乘法。别一上来就追求完美,先让触摸能动起来,再让它动得准。