3. 误差可视化:散点图与向量场图

数据不会骗人,但数据有时候会「藏」东西。

你算了一堆重投影误差的均值、标准差,看着 RMS 0.3 像素觉得挺满意。但真的把误差画出来,你可能会吓一跳——原来有一半的角点误差在 0.1 像素,另一半却在 0.6 像素。均值 0.3 像素,其实是个假象。

这就是我为什么坚持:标定结果好不好,先看图,再看数。

核心观点: 数值指标只能告诉你「有没有问题」,可视化能告诉你「问题在哪」。

3.1 散点图:看误差的「长相」

散点图是最直接的误差可视化方式。把每个角点的重投影误差 (dx, dy) 画在二维平面上,你一眼就能看出很多信息。

3.1.1 基础散点图

横轴是 x 方向误差,纵轴是 y 方向误差。理想情况下,所有点应该均匀分布在原点附近,形成一个圆形云团。

import matplotlib.pyplot as plt
import numpy as np

# 假设 errors 是 (N, 2) 的数组,每行是 (dx, dy)
errors = np.array([
    [0.12, -0.08],
    [-0.05, 0.15],
    [0.22, -0.18],
    # ... 实际数据会很多
])

plt.figure(figsize=(8, 6))
plt.scatter(errors[:, 0], errors[:, 1], 
            alpha=0.5, s=10, c='#3498db')
plt.axhline(0, color='gray', linestyle='--', linewidth=0.5)
plt.axvline(0, color='gray', linestyle='--', linewidth=0.5)
plt.xlabel('dx (pixels)')
plt.ylabel('dy (pixels)')
plt.title('重投影误差散点分布')
plt.axis('equal')
plt.grid(True, alpha=0.3)
plt.show()
我的习惯: 一定要加 axis('equal')。否则 x 和 y 轴比例不一致,圆形云团会被拉成椭圆,容易误判。

3.1.2 散点图能告诉你什么?

分布形态 可能原因 我的建议
圆形云团,中心在原点 标定质量好,误差随机 可以放心往下走
云团偏离原点 主点估计有偏差 检查主点初值,重新优化
云团被拉长(椭圆形) 某个方向误差偏大 检查该方向上的畸变模型
出现离群点(远离主云团) 角点检测错误或标定板不平 剔除这些点,重新标定
云团有「尾巴」或「卫星」 部分图像质量差 检查对应图像,考虑剔除

我曾经遇到过一个案例:散点图看起来是圆形,但仔细一看,中心点不在 (0,0),而是偏到了 (0.3, -0.2)。查了半天,发现是标定板的主点给错了。嗯,这种系统性偏差,只看 RMS 根本发现不了。

3.2 向量场图:看误差的「方向」

散点图告诉你误差有多大,但没告诉你误差发生在图像的哪个位置。向量场图就是干这个的。

3.2.1 什么是向量场图?

在图像的每个角点位置,画一个箭头。箭头的起点是检测到的角点位置,终点是重投影后的理论位置。箭头的方向和长度,直观展示了该点的误差。

def plot_vector_field(image_points, reprojected_points, img_shape):
    """
    image_points: (N, 2) 检测到的角点
    reprojected_points: (N, 2) 重投影的角点
    """
    plt.figure(figsize=(10, 8))
    
    # 计算误差向量
    vectors = reprojected_points - image_points
    
    # 画箭头
    plt.quiver(image_points[:, 0], image_points[:, 1],
               vectors[:, 0], vectors[:, 1],
               angles='xy', scale_units='xy', scale=1,
               color='#e74c3c', alpha=0.6, width=0.003)
    
    plt.xlim(0, img_shape[1])
    plt.ylim(img_shape[0], 0)  # 图像坐标系 y 轴向下
    plt.xlabel('u (pixels)')
    plt.ylabel('v (pixels)')
    plt.title('重投影误差向量场')
    plt.gca().set_aspect('equal')
    plt.show()

3.2.2 向量场图的解读技巧

说实话,向量场图比散点图难读一些,但信息量也大得多。我一般会按以下步骤来看:

  1. 看整体趋势:箭头是否都指向同一个方向?如果是,说明有系统性偏差。
  2. 看边缘 vs 中心:边缘的箭头是不是比中心大很多?如果是,畸变模型可能不够。
  3. 看局部异常:某个区域箭头突然变大或方向突变?那里可能有标定板缺陷。
  4. 看对称性:左右对称位置的箭头是否对称?不对称可能意味着镜头装配有问题。
注意: 向量场图对图像边缘的误差特别敏感。如果边缘误差很大,中心的箭头可能小到看不见。我一般会单独画一张边缘区域的放大图。

3.3 误差分布的知识图谱

下面这张图总结了误差可视化的核心逻辑。说白了,就是「先看整体,再看局部,最后定位问题」。

误差可视化分析流程 重投影误差数据 (dx, dy) 两种视图 散点图 误差分布形态 向量场图 误差空间分布 散点图关注点 • 是否偏离原点 • 是否呈椭圆形 • 是否有离群点 向量场图关注点 • 整体趋势方向 • 边缘 vs 中心 • 局部异常区域 定位问题,优化标定

3.4 进阶技巧:误差热力图

除了散点图和向量场图,我偶尔还会用热力图。把图像划分成网格,每个网格内计算平均误差大小,然后用颜色表示。

def plot_error_heatmap(errors, grid_size=(10, 10), img_shape=(1080, 1920)):
    """
    errors: (N, 3) 每行是 (u, v, error_magnitude)
    """
    h, w = img_shape
    grid_h, grid_w = grid_size
    
    # 初始化网格
    heatmap = np.zeros((grid_h, grid_w))
    count = np.zeros((grid_h, grid_w))
    
    # 累加误差
    for u, v, err in errors:
        i = int(v / h * grid_h)
        j = int(u / w * grid_w)
        i = min(i, grid_h - 1)
        j = min(j, grid_w - 1)
        heatmap[i, j] += err
        count[i, j] += 1
    
    # 平均
    mask = count > 0
    heatmap[mask] /= count[mask]
    
    plt.figure(figsize=(10, 6))
    plt.imshow(heatmap, cmap='hot', interpolation='nearest')
    plt.colorbar(label='平均误差 (pixels)')
    plt.xlabel('网格列')
    plt.ylabel('网格行')
    plt.title('重投影误差热力图')
    plt.show()
一个小技巧: 热力图的网格大小很关键。网格太大,细节丢失;网格太小,每个格子里的点太少,统计不稳定。我一般用 10×10 到 20×20 的网格。

3.5 避坑指南

做误差可视化这么多年,踩过的坑不少。挑几个典型的说说:

  • 我曾经把坐标轴搞反了:图像坐标系和笛卡尔坐标系的 y 轴方向相反。画向量场图时忘了反转 y 轴,结果所有箭头都指向了错误的方向。查了半天,最后发现是这个小问题。
  • 我曾经被离群点误导过:有一次散点图里有个点离得很远,我以为是角点检测错误。结果仔细一看,是标定板上的一个污点。嗯,擦掉污点重新拍就好了。
  • 我曾经忽略了边缘区域:只看整体散点图,觉得误差分布不错。后来画了向量场图才发现,图像四个角的误差比中心大了 5 倍。这就是典型的畸变模型不够用。
重要提醒: 可视化只是工具,不是目的。不要为了画图而画图。每次画完图,都要问自己一个问题:「这张图告诉我要怎么改进标定?」 如果回答不了,说明你还没看懂这张图。