3、校准数学基础:线性变换与仿射变换、最小二乘法原理、坐标映射矩阵推导

做触摸校准,说白了就是解决一个问题:你点的是A点,系统却以为你点的是B点。为什么会这样?因为触摸屏的物理坐标和屏幕的显示坐标之间,存在一个固定的偏移和旋转关系。我们要做的,就是找到这个关系的数学表达。

这一章,我会带你从最基础的线性变换讲起,一直推到最终的坐标映射矩阵。别怕数学,我会用项目里的实际例子来讲。

3.1 线性变换与仿射变换

先问个问题:触摸屏的坐标和屏幕坐标之间,是什么关系?

我早年做第一个触摸项目时,天真地以为就是简单的比例缩放。结果发现,屏幕上的按钮怎么都对不准。后来才明白,这中间不仅有缩放,还有旋转、平移,甚至可能还有剪切变形。

这些变换,在数学上可以分为两类:

变换类型 特点 能做什么 不能做什么
线性变换 原点不变,直线仍是直线 缩放、旋转、剪切 平移(原点会动)
仿射变换 直线仍是直线,平行线仍是平行线 缩放、旋转、剪切、平移 透视变形

触摸校准用到的就是仿射变换。因为触摸屏的原点和屏幕的原点通常不重合,必须把平移也考虑进去。

仿射变换的数学形式是这样的:

x' = a*x + b*y + c
y' = d*x + e*y + f

其中 (x, y) 是触摸屏的原始坐标,(x', y') 是校准后的屏幕坐标。a、b、c、d、e、f 就是我们要找的六个参数。

核心理解:六个参数,对应六个自由度——水平缩放、垂直缩放、水平旋转、垂直旋转、水平平移、垂直平移。这就是为什么至少需要三个校准点(六个方程)才能解出它们。

3.2 最小二乘法原理

好,现在我们知道要解六个参数。但问题来了:实际采集的触摸点是有噪声的。

我记得有一次调试一款电容屏,明明点了校准十字的中心,采集到的坐标却总是抖动的。如果用三个点硬解方程,结果会非常敏感——稍微偏一点,校准就全歪了。

这时候就需要最小二乘法出场了。

它的核心思想很简单:不求每个点都完美匹配,而是让所有点的误差平方和最小。

举个例子。假设我们采集了 N 个校准点(N > 3),每个点都有一个理论坐标和实际采集坐标。对于第 i 个点,误差是:

error_x_i = (a*x_i + b*y_i + c) - x'_i
error_y_i = (d*x_i + e*y_i + f) - y'_i

我们要最小化的目标是:

S = Σ(error_x_i² + error_y_i²)

怎么最小化?对每个参数求偏导,令其等于零。这就是最小二乘法的标准解法。

我的经验:实际项目中,我一般采集 5 到 9 个校准点。点太少,抗噪声能力差;点太多,用户操作麻烦,而且边缘区域的点权重会被稀释。5 个点是个不错的折中。

3.3 坐标映射矩阵推导

有了前面的基础,我们来推导最终的矩阵形式。

把仿射变换写成矩阵:

[x']   [a  b  c]   [x]
[y'] = [d  e  f] * [y]
[1 ]   [0  0  1]   [1]

这个 3x3 矩阵就是坐标映射矩阵。注意最后一行是固定的 [0, 0, 1],这是齐次坐标的写法,方便做矩阵乘法。

现在,假设我们有 N 个校准点,可以构建超定方程组:

[x1  y1  1   0   0  0]   [a]   [x'1]
[0   0   0  x1  y1  1]   [b]   [y'1]
[x2  y2  1   0   0  0]   [c]   [x'2]
[0   0   0  x2  y2  1] * [d] = [y'2]
[...                   ]   [e]   [...]
[                        ]   [f]   [   ]

记作:A * P = B

其中 A 是 2N×6 的矩阵,P 是 6×1 的参数向量,B 是 2N×1 的目标向量。

最小二乘解为:

P = (Aᵀ * A)⁻¹ * Aᵀ * B

这就是传说中的伪逆解法。

曾经踩过的坑:有一次我在一个没有浮点运算单元的 MCU 上做校准,直接套用这个公式,结果矩阵求逆算到死机。后来我改用定点数 + 查表法做矩阵运算,才把时间压到 10ms 以内。如果你的平台资源紧张,记得做算法裁剪。

3.4 知识体系总览

下面这张图,把这一章的核心逻辑串起来了:

触摸校准数学基础 — 知识体系 触摸坐标偏移问题 线性变换(缩放/旋转/剪切) 仿射变换(线性 + 平移) 坐标映射矩阵 [x'] = [a b c] * [x] 三点硬解(不推荐) 最小二乘法(推荐) 伪逆求解 嵌入式平台算法裁剪与定点化 关键:至少3个校准点 → 推荐5~9个 → 构建超定方程 → 最小二乘 → 得到6个参数

3.5 代码实现要点

最后,给出一段伪代码,展示如何在嵌入式平台上实现最小二乘求解:

// 输入:cal_points[N] 结构体数组,包含 (touch_x, touch_y, screen_x, screen_y)
// 输出:矩阵参数 a, b, c, d, e, f

void calc_calibration_matrix(CalPoint *pts, int n, float *params) {
    // 1. 构建矩阵 A (2n x 6) 和向量 B (2n)
    float A[2*n][6], B[2*n];
    for (int i = 0; i < n; i++) {
        // 第 i 个点的 x 方程
        A[2*i][0] = pts[i].touch_x;
        A[2*i][1] = pts[i].touch_y;
        A[2*i][2] = 1.0f;
        A[2*i][3] = 0.0f;
        A[2*i][4] = 0.0f;
        A[2*i][5] = 0.0f;
        B[2*i] = pts[i].screen_x;
        
        // 第 i 个点的 y 方程
        A[2*i+1][0] = 0.0f;
        A[2*i+1][1] = 0.0f;
        A[2*i+1][2] = 0.0f;
        A[2*i+1][3] = pts[i].touch_x;
        A[2*i+1][4] = pts[i].touch_y;
        A[2*i+1][5] = 1.0f;
        B[2*i+1] = pts[i].screen_y;
    }
    
    // 2. 计算 Aᵀ * A 和 Aᵀ * B
    float ATA[6][6] = {0};
    float ATB[6] = {0};
    for (int i = 0; i < 2*n; i++) {
        for (int j = 0; j < 6; j++) {
            ATB[j] += A[i][j] * B[i];
            for (int k = 0; k < 6; k++) {
                ATA[j][k] += A[i][j] * A[i][k];
            }
        }
    }
    
    // 3. 求解 (AᵀA) * P = AᵀB,用高斯消元法
    gauss_elimination(ATA, ATB, params, 6);
}

工程建议:在实际项目中,我通常会把高斯消元法换成 LU 分解,数值稳定性更好。另外,如果 MCU 没有硬件浮点单元,记得把所有 float 换成定点数,用 Q15 或 Q31 格式做运算。

嗯,这一章的内容就到这里。数学基础打牢了,后面讲具体的校准算法和调试技巧时,你就能理解每一步为什么这么做了。