4、二维平面校准法:最小二乘法拟合圆、参数提取与补偿公式、实战代码实现(Python)

好,咱们进入正题。

二维平面校准,说白了就是把磁力计在水平面上转一圈,采集数据,然后想办法把那个偏心圆拉回到圆心。这是最基础、最实用的校准方法。我个人习惯叫它“转圈法”,因为操作上就是这么回事。

4.1 为什么是圆?为什么是偏心?

先想一个问题:理想情况下,磁力计在水平面匀速旋转一圈,X轴和Y轴输出的数据点,在二维坐标系里应该是什么形状?

答案是——一个完美的圆,圆心在(0,0)。

但现实很骨感。硬铁干扰会让圆心偏移,软铁干扰会让圆变成椭圆。不过对于大多数消费级和工业级应用,我们主要处理硬铁干扰,也就是圆心偏移问题。所以二维平面校准的核心,就是把这个偏移的圆“拉”回原点。

核心思想:采集数据 → 拟合圆 → 提取圆心偏移量 → 补偿输出

4.2 最小二乘法拟合圆:数学原理

拟合圆的方法有很多,什么代数拟合法、几何拟合法。我个人最常用的是最小二乘法拟合圆。为什么?因为它简单、稳定、计算量小,在嵌入式上也能跑。

数学推导其实不复杂。假设我们采集了N个点 (xᵢ, yᵢ),要拟合的圆方程为:

(x - a)² + (y - b)² = R²

其中 (a, b) 是圆心,R 是半径。

展开后得到:

x² + y² - 2ax - 2by + (a² + b² - R²) = 0

令:

u = -2a
v = -2b
w = a² + b² - R²

则方程变为:

x² + y² + ux + vy + w = 0

你看,这就变成了一个线性最小二乘问题。我们只需要解出 u、v、w,就能反推出圆心 (a, b) 和半径 R。

我的经验:采集点数不要少于20个点。我曾经有个项目只采了8个点,拟合出来的圆偏差很大,后来改成36个点(每10度一个),效果就稳了。

4.3 参数提取与补偿公式

解出 u、v、w 之后,参数提取就很简单了:

a = -u / 2
b = -v / 2
R = sqrt(a² + b² - w)

然后补偿公式就是:

x_compensated = x_raw - a
y_compensated = y_raw - b

就这么简单?对,就这么简单。但要注意,这个补偿只适用于二维平面。如果你在三维空间里乱晃,那就要用三维校准了,那是后面章节的内容。

避坑指南:我曾经在一个项目中,采集数据时磁力计没有保持水平,结果拟合出来的圆是倾斜的,补偿后航向误差反而更大。所以二维校准的前提是——确保磁力计在水平面内旋转

4.4 实战代码实现(Python)

好了,理论讲完了,咱们直接上代码。这是我个人习惯的写法,注释写得比较详细,方便你移植到C语言或者MicroPython。

import numpy as np
import matplotlib.pyplot as plt

def fit_circle(x, y):
    """
    最小二乘法拟合圆
    输入:x, y - 采集的原始数据
    输出:圆心 (a, b),半径 R
    """
    # 构造矩阵 A 和向量 B
    A = np.column_stack([x, y, np.ones_like(x)])
    B = -(x**2 + y**2)
    
    # 解线性方程组
    uvw, _, _, _ = np.linalg.lstsq(A, B, rcond=None)
    u, v, w = uvw
    
    # 提取圆心和半径
    a = -u / 2
    b = -v / 2
    R = np.sqrt(a**2 + b**2 - w)
    
    return a, b, R

def compensate_magnetometer(x_raw, y_raw, a, b):
    """
    补偿磁力计数据
    输入:原始数据,圆心偏移量
    输出:补偿后的数据
    """
    x_comp = x_raw - a
    y_comp = y_raw - b
    return x_comp, y_comp

# ===== 模拟采集数据 =====
# 假设真实圆心在 (50, -30),半径 200
true_a, true_b, true_R = 50, -30, 200

# 生成角度
angles = np.linspace(0, 2*np.pi, 36)
x_true = true_R * np.cos(angles) + true_a
y_true = true_R * np.sin(angles) + true_b

# 添加一些噪声,模拟真实采集
np.random.seed(42)
x_noisy = x_true + np.random.normal(0, 5, len(angles))
y_noisy = y_true + np.random.normal(0, 5, len(angles))

# ===== 拟合圆 =====
a_fit, b_fit, R_fit = fit_circle(x_noisy, y_noisy)
print(f"拟合结果:圆心 ({a_fit:.2f}, {b_fit:.2f}),半径 {R_fit:.2f}")
print(f"真实值:  圆心 ({true_a}, {true_b}),半径 {true_R}")

# ===== 补偿数据 =====
x_comp, y_comp = compensate_magnetometer(x_noisy, y_noisy, a_fit, b_fit)

# ===== 可视化 =====
plt.figure(figsize=(10, 5))

# 原始数据
plt.subplot(1, 2, 1)
plt.scatter(x_noisy, y_noisy, label='原始数据', color='red')
circle_orig = plt.Circle((a_fit, b_fit), R_fit, fill=False, color='blue', linestyle='--', label='拟合圆')
plt.gca().add_patch(circle_orig)
plt.scatter(a_fit, b_fit, color='blue', marker='x', s=100, label='拟合圆心')
plt.axis('equal')
plt.grid(True)
plt.legend()
plt.title('原始数据与拟合圆')

# 补偿后数据
plt.subplot(1, 2, 2)
plt.scatter(x_comp, y_comp, label='补偿后数据', color='green')
circle_comp = plt.Circle((0, 0), R_fit, fill=False, color='orange', linestyle='--', label='理想圆')
plt.gca().add_patch(circle_comp)
plt.scatter(0, 0, color='orange', marker='x', s=100, label='原点')
plt.axis('equal')
plt.grid(True)
plt.legend()
plt.title('补偿后数据(圆心归零)')

plt.tight_layout()
plt.show()

运行这段代码,你会看到两幅图:左边是原始数据,圆心明显不在原点;右边是补偿后的数据,圆心被拉回到了(0,0)。

小技巧:在实际项目中,我通常会把拟合得到的圆心参数保存到EEPROM或者Flash里,每次上电时读取并补偿。这样一次校准,长期有效——除非你的设备结构发生了变化。

4.5 校准效果评估

校准完了,怎么知道效果好不好?我一般看两个指标:

指标 计算方法 合格标准
圆心偏移量 sqrt(a² + b²) 小于原始数据范围的5%
半径一致性 各点到圆心的距离标准差 小于平均半径的2%

嗯,这里要注意:半径一致性反映了软铁干扰的程度。如果标准差太大,说明你的数据点不是均匀分布在圆上,这时候二维校准可能不够用了,需要考虑三维校准或者椭圆拟合。

我曾经踩过的坑:有一次校准后航向精度还是不好,我反复检查代码都没问题。后来发现是采集数据时,磁力计附近有个大铁块在缓慢移动(一个叉车),导致数据一直在漂。所以校准环境一定要远离动态磁场干扰源

4.6 本章小结

二维平面校准法,说白了就是三步:

  1. 转圈采集数据——保持水平,均匀旋转
  2. 最小二乘法拟合圆——解线性方程组,提取圆心偏移
  3. 补偿输出——减去圆心偏移量,得到校准后的数据

这个方法简单、实用,能满足大部分二维航向应用的需求。但如果你发现校准后航向误差还是很大,别急着怀疑算法——先检查一下你的采集环境,是不是有干扰源?磁力计是不是没放平?

好,二维校准就讲到这里。代码你拿去就能用,但记得根据实际传感器的量程调整一下参数。


公众号:蓝海资料掘金营,微信deep3321