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 本章小结
二维平面校准法,说白了就是三步:
- 转圈采集数据——保持水平,均匀旋转
- 最小二乘法拟合圆——解线性方程组,提取圆心偏移
- 补偿输出——减去圆心偏移量,得到校准后的数据
这个方法简单、实用,能满足大部分二维航向应用的需求。但如果你发现校准后航向误差还是很大,别急着怀疑算法——先检查一下你的采集环境,是不是有干扰源?磁力计是不是没放平?
好,二维校准就讲到这里。代码你拿去就能用,但记得根据实际传感器的量程调整一下参数。
公众号:蓝海资料掘金营,微信deep3321