![]() |
OpenCV 4.13.0
开源计算机视觉库 (Open Source Computer Vision)
|
上一篇教程: 使用 OpenCV 进行相机标定
下一篇教程: 交互式相机标定应用程序
| 原作者 | Edgar Riba |
| 兼容性 | OpenCV >= 3.0 |
如今,增强现实(AR)是计算机视觉和机器人领域最热门的研究课题之一。在增强现实中,最基础的问题是估计相机相对于物体的姿态。在计算机视觉领域,这是为了进行后续的 3D 渲染;在机器人领域,则是为了获取物体姿态以便进行抓取和操作。然而,这并不是一个容易解决的问题,因为图像处理中最常见的问题是:为了解决一个对人类来说基本且直观的问题,需要应用大量的算法或数学运算,从而产生巨大的计算开销。
本教程解释了如何构建一个实时应用程序,根据给定的 2D 图像及其 3D 纹理模型,以六个自由度跟踪有纹理的物体,并估计相机的姿态。
该程序将包含以下部分:
在计算机视觉中,通过 n 个 3D 到 2D 的点对应关系来估计相机姿态是一个基础且已被充分理解的问题。该问题最通用的版本需要估计姿态的六个自由度和五个标定参数:焦距、主点、纵横比和倾斜。使用著名的直接线性变换(DLT)算法,最少需要 6 组对应关系即可建立。不过,该问题存在多种简化形式,从而产生了一系列不同的算法来提高 DLT 的精度。
最常见的简化是假设标定参数已知,即所谓的 Perspective-n-Point(PnP)问题。
问题阐述: 给定一组在世界参考坐标系中表达的 3D 点 \(p_i\),及其在图像上的 2D 投影 \(u_i\),我们寻求找回相机相对于世界的姿态(\(R\) 和 \(t\))以及焦距 \(f\)。
OpenCV 提供了四种不同的方法来解决 PnP 问题并返回 \(R\) 和 \(t\)。然后,使用以下公式可以将 3D 点投影到图像平面上:
\[s\ \left [ \begin{matrix} u \\ v \\ 1 \end{matrix} \right ] = \left [ \begin{matrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{matrix} \right ] \left [ \begin{matrix} r_{11} & r_{12} & r_{13} & t_1 \\ r_{21} & r_{22} & r_{23} & t_2 \\ r_{31} & r_{32} & r_{33} & t_3 \end{matrix} \right ] \left [ \begin{matrix} X \\ Y \\ Z\\ 1 \end{matrix} \right ]\]
关于如何处理这些方程的完整文档位于 相机标定与 3D 重建。
您可以在 OpenCV 源码库的 samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/ 文件夹中找到本教程的源代码。
本教程由两个主要程序组成:
模型注册
此应用程序旨在供没有待检测物体 3D 纹理模型的用户使用。您可以使用此程序创建自己的纹理 3D 模型。此程序仅适用于平面物体,如果您想为形状复杂的物体建模,则应使用更专业的软件来创建。
该程序需要输入待注册物体的图像及其 3D 网格。我们还必须提供拍摄该输入图像的相机的内参。所有文件都需要使用绝对路径或相对于应用程序工作目录的相对路径来指定。如果未指定文件,程序将尝试打开提供的默认参数。
程序启动后,会从输入图像中提取 ORB 特征和描述符,然后结合网格使用 Möller–Trumbore 相交算法 计算找到的特征的 3D 坐标。最后,3D 点和描述符被存储在 YAML 格式文件的不同列表中,每一行代表一个不同的点。关于存储文件的技术背景可以在 使用 XML / YAML / JSON 文件进行文件输入和输出 教程中找到。
模型检测
此程序的目的是根据给定的 3D 纹理模型实时估计物体姿态。
程序启动后,加载 YAML 格式的 3D 纹理模型(其结构与模型注册程序中所述相同)。从场景中检测并提取 ORB 特征和描述符。然后,使用带有 cv::flann::GenericIndex 的 cv::FlannBasedMatcher 在场景描述符和模型描述符之间进行匹配。利用找到的匹配项结合 cv::solvePnPRansac 函数计算相机的 R 和 t。最后,应用卡尔曼滤波(KalmanFilter)以剔除不良姿态。
如果您在编译 OpenCV 时包含了示例(samples),可以在 opencv/build/bin/cpp-tutorial-pnp_detection 中找到它。然后您可以运行该应用程序并修改一些参数:
例如,您可以通过更改 pnp 方法来运行应用程序:
下面详细解释实时应用程序的代码:
读取 3D 纹理对象模型和对象网格(mesh)。
为了加载纹理模型,我实现了一个 Model 类,它具有 load() 函数,可以打开 YAML 文件并获取存储的 3D 点及其对应的描述符。您可以在 samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/Data/cookies_ORB.yml 中找到 3D 纹理模型的示例。
在主程序中,模型的加载方式如下:
为了读取对象网格,我实现了一个 Mesh 类,它具有 load() 函数,可以打开 .ply 文件并存储对象的 3D 点以及构成的三角形。您可以在 samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/Data/box.ply 中找到模型网格的示例。
在主程序中,网格的加载方式如下:
您也可以加载不同的模型和网格:
从相机或视频获取输入
为了进行检测,必须捕获视频。这可以通过传入机器中录制视频的绝对路径来加载。为了测试应用程序,您可以在 samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/Data/box.mp4 找到一个录制的视频。
然后算法逐帧计算:
您也可以加载不同的录制视频:
从场景中提取 ORB 特征和描述符
下一步是检测场景特征并提取其描述符。为此,我实现了一个 RobustMatcher 类,它具有关键点检测和特征提取功能。您可以在 samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/src/RobusMatcher.cpp 中找到它。在您的 RobustMatcher 对象中,可以使用 OpenCV 的任何 2D 特征检测器。在本例中,我使用了 cv::ORB 特征,因为它基于 cv::FAST 来检测关键点,并使用 cv::xfeatures2d::BriefDescriptorExtractor 来提取描述符,这意味着它速度快且对旋转具有鲁棒性。您可以在文档中找到有关 ORB 的更多详细信息。
以下代码展示了如何实例化并设置特征检测器和描述符提取器:
特征和描述符将由 RobustMatcher 在匹配函数内部计算。
使用 Flann 匹配器将场景描述符与模型描述符匹配
这是我们检测算法的第一步。主要思想是将场景描述符与我们的模型描述符进行匹配,以便知道当前场景中找到的特征的 3D 坐标。
首先,我们必须设置要使用的匹配器。在这种情况下,使用 cv::FlannBasedMatcher 匹配器,随着训练特征集的增加,其计算开销比 cv::BFMatcher 匹配器更低。然后,对于 FlannBased 匹配器,由于 ORB 描述符是二进制的,创建的索引是 Multi-Probe LSH: Efficient Indexing for High-Dimensional Similarity Search。
您可以调整 LSH 和搜索参数以提高匹配效率:
其次,我们必须通过使用 robustMatch() 或 fastRobustMatch() 函数来调用匹配器。使用这两个函数的区别在于它们的计算开销。第一个方法较慢,但在过滤良好匹配项方面更鲁棒,因为它使用了两次比例测试和一次对称性测试。相比之下,第二个方法较快,但鲁棒性较差,因为它只对匹配项应用了单次比例测试。
以下代码用于获取模型 3D 点及其描述符,然后在主程序中调用匹配器:
以下代码对应于 RobustMatcher 类中的 robustMatch() 函数。此函数使用给定的图像检测关键点并提取描述符,使用两个最近邻将提取的描述符与给定的模型描述符进行双向匹配。然后,对双向匹配应用比例测试,以删除第一和第二最佳匹配之间的距离比例大于给定阈值的匹配项。最后,应用对称性测试以删除非对称匹配。
匹配过滤后,我们必须使用获得的 DMatches 向量从场景关键点和 3D 模型中提取 2D 和 3D 对应关系。有关 cv::DMatch 的更多信息,请查看文档。
您还可以更改比例测试阈值、检测的关键点数量,以及是否使用鲁棒匹配器:
使用 PnP + Ransac 进行姿态估计
有了 2D 和 3D 对应关系后,我们必须应用 PnP 算法来估计相机姿态。我们必须使用 cv::solvePnPRansac 而不是 cv::solvePnP 的原因在于,匹配后的对应关系并不全是正确的,很可能存在错误的对应关系(即离群点 outliers)。随机采样一致性 (Ransac) 是一种非确定性的迭代方法,它从观测数据中估计数学模型的参数,随着迭代次数的增加产生近似结果。应用 Ransac 后,所有 outliers 将被剔除,从而以一定的概率估计出相机姿态的一个良好解。
对于相机姿态估计,我实现了一个 PnPProblem 类。该类有 4 个属性:给定的标定矩阵、旋转矩阵、平移矩阵和旋转-平移矩阵。用于估计姿态的相机的内参是必需的。为了获取参数,您可以参考 使用正方形棋盘格进行相机标定 和 使用 OpenCV 进行相机标定 教程。
以下代码展示了如何在主程序中声明 PnPProblem 类:
以下代码展示了 PnPProblem 类如何初始化其属性:
OpenCV 提供了四种 PnP 方法:ITERATIVE、EPNP、P3P 和 DLS。根据应用程序类型的不同,估计方法也会有所不同。在我们要制作实时应用程序的情况下,最合适的方法是 EPNP 和 P3P,因为它们在寻找最优解时比 ITERATIVE 和 DLS 更快。然而,EPNP 和 P3P 在面对平面时并不是特别鲁棒,有时姿态估计似乎具有镜像效应。因此,在本教程中使用了 ITERATIVE 方法,因为待检测物体具有平面部分。
OpenCV 的 RANSAC 实现需要您提供三个参数:1) 算法停止前的最大迭代次数,2) 观测点投影与计算点投影之间允许的最大距离(以将其视为内点),以及 3) 获得良好结果的置信度。您可以调整这些参数以提高算法性能。增加迭代次数将获得更精确的解,但需要更多时间。增加重投影误差会减少计算时间,但解将不准确。降低置信度会使算法更快,但获得的解也将不准确。
以下参数适用于此应用程序:
以下代码对应于 PnPProblem 类中的 estimatePoseRANSAC() 函数。该函数在给定 2D/3D 对应关系集、所需的 PnP 方法、输出内点容器和 Ransac 参数的情况下估计旋转和平移矩阵。
以下代码是主算法的第 3 步和第 4 步。首先,调用上述函数;其次,从 RANSAC 中获取输出内点向量,以获取用于绘图目的的 2D 场景点。如代码所示,如果我们有匹配项,则必须确保应用 RANSAC,否则由于 OpenCV 的某些 bug,函数 cv::solvePnPRansac 会崩溃。
最后,一旦相机姿态被估计出来,我们就可以使用 \(R\) 和 \(t\),结合“理论”部分展示的公式,计算在世界参考系中表达的给定 3D 点在图像上的 2D 投影。
以下代码对应于 PnPProblem 类中的 backproject3DPoint() 函数。该函数将世界参考系中表达的给定 3D 点反投影到 2D 图像上:
上述函数用于计算对象 Mesh 的所有 3D 点,以显示物体的姿态。
您还可以修改 RANSAC 参数和 PnP 方法:
使用线性卡尔曼滤波器剔除错误姿态
在计算机视觉或机器人领域,应用检测或跟踪技术后,由于某些传感器误差,通常会获得不良结果。为了避免这些错误检测,本教程解释了如何实现线性卡尔曼滤波器。卡尔曼滤波器将在检测到给定数量的内点后应用。
您可以找到有关 卡尔曼滤波 的更多信息。本教程使用了 OpenCV 实现的 cv::KalmanFilter,并基于 用于位置和方向跟踪的线性卡尔曼滤波器 来设置动力学和测量模型。
首先,我们必须定义状态向量,它将包含 18 个状态:位置数据 (x,y,z) 及其一阶和二阶导数(速度和加速度),然后以三个欧拉角(roll, pitch, yaw)的形式添加旋转及其一阶和二阶导数(角速度和角加速度):
\[X = (x,y,z,\dot x,\dot y,\dot z,\ddot x,\ddot y,\ddot z,\psi,\theta,\phi,\dot \psi,\dot \theta,\dot \phi,\ddot \psi,\ddot \theta,\ddot \phi)^T\]
其次,我们必须定义测量值的数量,共 6 个:从 \(R\) 和 \(t\) 中我们可以提取出 \((x,y,z)\) 和 \((\psi,\theta,\phi)\)。此外,我们必须定义应用于系统的控制动作数量,在本例中为 零。最后,我们必须定义测量之间的时间差,在本例中为 \(1/T\),其中 T 是视频的帧率。
以下代码对应于 卡尔曼滤波器 的初始化。首先设置过程噪声、测量噪声和误差协方差矩阵。其次,设置转移矩阵(即动力学模型),最后设置测量矩阵(即测量模型)。
您可以调整过程噪声和测量噪声以提高 卡尔曼滤波器 的性能。测量噪声越小,收敛速度越快,但算法对错误测量也会变得敏感。
以下代码是主算法的第 5 步。当 Ransac 获得的内点数超过阈值时,填充测量矩阵,然后更新 卡尔曼滤波器:
以下代码对应于 fillMeasurements() 函数,它将测得的 旋转矩阵转换为欧拉角,并与测得的平移向量一起填充到测量矩阵中:
以下代码对应于 updateKalmanFilter() 函数,该函数更新卡尔曼滤波器并设置估计的旋转矩阵和平移向量。估计的旋转矩阵来自于将估计的 欧拉角转换为旋转矩阵:
第 6 步是设置估计的旋转-平移矩阵:
最后一步(可选)是绘制找到的姿态。为此,我实现了一个绘制所有网格 3D 点和额外参考轴的函数:
您还可以修改更新卡尔曼滤波所需的最小内点数:
以下视频是使用所述检测算法进行实时姿态估计的结果,使用了以下参数:
您可以点击此处在 YouTube 上观看实时姿态估计展示。