KeyFrame(关键帧)是 ORB_SLAM2 系统中非常核心的一个概念和数据结构。它不是每一帧都创建,而是从普通的 Frame 对象中选取具有代表性的帧来创建。关键帧构成了地图的基础,用于跟踪、局部建图和回环检测。

以下是对 KeyFrame 类的详细解析:

1. 关键帧的角色与创建

  • 作用:关键帧是地图的基本单元,存储了比普通帧更持久和重要的信息,用于优化相机轨迹和构建环境地图。它们之间通过共视图(Covisibility Graph)和生成树(Spanning Tree)等结构连接起来。
  • 创建:关键帧由一个 Frame 对象、一个指向地图 Map 的指针和一个指向关键帧数据库 KeyFrameDatabase 的指针来构造。构造时,它会从父 Frame 复制大量信息,如时间戳、相机参数、特征点、描述子、地图点关联等,并被赋予一个唯一的 mnId

2. 核心数据成员(属性)

KeyFrame 类包含了众多成员变量来存储其状态和信息,主要分为几类:

  • 标识与时间戳
    • mnId: 关键帧的唯一 ID。
    • mnFrameId: 创建此关键帧的原始 Frame 的 ID。
    • mTimeStamp: 时间戳。
  • 位姿信息
    • Tcw: 从世界坐标系到当前相机坐标系的变换矩阵。
    • Twc: 从相机坐标系到世界坐标系的变换矩阵(Tcw 的逆)。
    • Ow: 相机光心在世界坐标系下的坐标。
    • 这些位姿信息通过 mMutexPose 互斥锁保护,需要通过 SetPose, GetPose, GetPoseInverse, GetCameraCenter 等函数访问。
  • 相机与图像信息
    • 相机内参 (fx, fy, cx, cy, invfx, invfy, mK)。
    • 双目/深度信息 (mbf, mb, mThDepth)。
    • 图像边界 (mnMinX, mnMinY, mnMaxX, mnMaxY)。
    • 图像网格 (mGrid):用于加速特征点匹配,存储每个网格单元内的特征点索引。
  • 特征点与描述子
    • N: 特征点数量。
    • mvKeys: 原始特征点 (cv::KeyPoint 格式)。
    • mvKeysUn: 去畸变后的归一化特征点坐标。
    • mDescriptors: 特征点的 ORB 描述子。
    • mvuRight / mvDepth: 双目或 RGB-D 下特征点的右图像坐标或深度信息。
  • 地图点关联
    • mvpMapPoints: 一个 std::vector<MapPoint*>,存储与该关键帧中特征点关联的地图点指针。如果某个特征点没有关联地图点,或关联的地图点被删除,则对应指针为 NULL。该变量由 mMutexFeatures 保护。
  • 词袋模型 (BoW)
    • mBowVec: DBoW2 格式的词袋向量,用于快速进行图像识别(回环检测、重定位)。
    • mFeatVec: DBoW2 格式的特征向量,记录了特征点到词典内部节点的映射关系。
    • 通过 ComputeBoW() 函数计算生成。
  • 图结构信息(由 mMutexConnections 保护):
    • 共视图 (Covisibility Graph)
      • mConnectedKeyFrameWeights: 存储与其他关键帧的连接及其权重(共视地图点数量)。
      • mvpOrderedConnectedKeyFrames, mvOrderedWeights: 按权重排序后的共视关键帧列表及其权重。
    • 生成树 (Spanning Tree)
      • mpParent: 指向父关键帧(通常是共视程度最高的那个)。
      • mspChildrens: 指向子关键帧的集合。
    • 回环边 (Loop Edges)
      • mspLoopEdges: 存储与当前关键帧形成闭环关系的关键帧集合。
  • 状态标志
    • mbBad: 标记该关键帧是否已被标记为“坏点”,准备或已经被删除。
    • mbNotErase, mbToBeErased: 用于控制删除逻辑,例如在进行回环优化时暂时阻止删除。
  • 指针
    • mpMap: 指向所属的 Map 对象。
    • mpKeyFrameDB: 指向 KeyFrameDatabase
    • mpORBvocabulary: 指向 ORB 词典。

3. 核心功能(成员函数)

  • 位姿管理SetPose 设置位姿并计算相关变量 (Ow, Twc),GetPose 等函数提供线程安全的位姿访问。
  • BoW 计算ComputeBoW 使用 ORB 词典计算关键帧的词袋表示。
  • 共视图管理
    • AddConnection: 添加与其他关键帧的共视连接。
    • EraseConnection: 删除共视连接。
    • UpdateConnections: 核心函数,根据共享的地图点重新计算并更新与其他关键帧的共视权重和连接关系。
    • UpdateBestCovisibles: 对共视关键帧按权重排序。
    • GetConnectedKeyFrames, GetVectorCovisibleKeyFrames, GetBestCovisibilityKeyFrames, GetCovisiblesByWeight, GetWeight: 提供不同方式查询共视关键帧。
  • 生成树管理AddChild, EraseChild, ChangeParent, GetChilds, GetParent, hasChild 用于维护生成树结构。
  • 回环边管理AddLoopEdge, GetLoopEdges 用于记录和查询回环信息。
  • 地图点管理
    • AddMapPoint: 关联一个地图点到指定的特征点索引。
    • EraseMapPointMatch: 断开与某个地图点的关联(将指针设为 NULL)。
    • ReplaceMapPointMatch: 替换某个索引处的地图点关联。
    • GetMapPoints, GetMapPointMatches, GetMapPoint: 获取关联的地图点信息。
    • TrackedMapPoints: 统计被有效跟踪(观测次数达标)的地图点数量。
  • 特征点相关操作
    • GetFeaturesInArea: 在指定区域内快速查找特征点。
    • UnprojectStereo: 将双目或 RGB-D 特征点反投影到三维空间。
    • IsInImage: 判断某个二维点是否在图像范围内。
  • 删除管理
    • SetNotErase, SetErase: 控制是否允许删除该关键帧。
    • SetBadFlag: 标记关键帧为坏点,并执行复杂的清理逻辑,包括:断开与其他关键帧的连接、通知地图点移除观测、更新生成树结构、从地图和关键帧数据库中移除自身。这是一个非常关键的操作,确保地图结构的完整性。
    • isBad: 查询关键帧是否已被标记为坏点。
  • 其他
    • ComputeSceneMedianDepth: 估计场景深度(主要用于单目初始化)。

4. 线程安全

由于 ORB_SLAM2 是多线程系统(Tracking, Local Mapping, Loop Closing),KeyFrame 的很多数据会被多个线程同时访问。因此,关键数据(位姿、图连接、地图点关联)都使用了 std::mutex 进行保护,以确保线程安全。

总结

KeyFrame 类是 ORB_SLAM2 中地图表示的核心,它不仅存储了自身的详细信息(位姿、特征、BoW),更重要的是维护了与其他关键帧(通过共视图、生成树、回环边)和地图点之间的复杂连接关系。这些关系是 SLAM 系统进行优化和保持一致性的基础。对 KeyFrame 的管理(创建、更新连接、删除)是 Local Mapping 和 Loop Closing 线程的关键任务。

何时插入关键帧

  1. 距离上一个关键帧足够远: 确保自从上一次插入关键帧后,已经过了一定的时间或者处理了足够数量的帧,以防止关键帧插入过于密集。
  2. 跟踪质量下降: 当前帧能够稳定跟踪到的地图点数量显著减少。这通常通过以下两种方式之一来判断:
    • 当前帧跟踪到的地图点总数低于某个下限阈值。
    • 当前帧跟踪到的地图点数量,相比于其参考关键帧(通常是距离最近的关键帧)所能看到的地图点数量,低于一个特定的比例(例如90%)。这表明视角或场景发生了显著变化。
  3. 局部建图线程准备就绪: 负责处理新关键帧和进行局部优化的“局部建图”(Local Mapping)线程当前不是非常繁忙,并且允许接受新的关键帧。如果它正在进行耗时的局部BA(Bundle Adjustment)或者全局BA,或者被其他原因阻塞,则会暂停插入。
  4. 满足最小跟踪点数: 即使满足了与参考关键帧的比例条件,通常也要求当前帧跟踪到的点数不能过少(要高于另一个更低的阈值),以保证关键帧的质量。

简单来说,插入关键帧是为了在视角变化足够大(跟踪点减少)、距离上一关键帧有一定间隔、且后端处理单元有能力处理时,向地图中添加新的、带有足够信息的节点。

追踪线程中的参考关键帧的变换

  1. 对于双目/rgbd来说第一帧就是追踪线程的参考关键帧
  2. 在后面进来的帧中,首先将追踪线程参考关键帧作为当前帧的参考关键帧
  3. 然后在局部跟踪步骤中将共视程度最高的帧作为当前帧的参考关键帧,并且也作为追踪线程的参考关键帧,也就是下一帧过来的帧会被初始赋予的参考关键帧
  4. 并不是总是将最近的一个关键帧作为当前帧的参考关键帧(我一直误会是这样)