欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

karto探秘之open_karto 第四章 --- 回环检测与后端优化

程序员文章站 2024-03-25 09:26:40
...

1 Mapper::Process()

主函数中与后端优化和回环检测相关的代码如下所示:

  kt_bool Mapper::Process(LocalizedRangeScan* pScan)
  {
      // 省略...

      if (m_pUseScanMatching->GetValue())
      {
        // add to graph 
        m_pGraph->AddVertex(pScan);

        //边是具有约束关系的两个顶点,在 AddEdges中的操作有: 寻找可以连接的两个帧,计算scanMatch,同时保存scanMatch的结果到Graph之中,然后添加到SPA里面
        m_pGraph->AddEdges(pScan, covariance);

        m_pMapperSensorManager->AddRunningScan(pScan);  // 这里面有RunningScan的维护,即删除不合适的头

        //进行回环检测的步骤
        if (m_pDoLoopClosing->GetValue())
        {
          std::vector<Name> deviceNames = m_pMapperSensorManager->GetSensorNames();
          const_forEach(std::vector<Name>, &deviceNames)
          {
            m_pGraph->TryCloseLoop(pScan, *iter);
          }
        }
      }
      m_pMapperSensorManager->SetLastScan(pScan);   //每一次的 LastScan都是一个正常的pScan,而不是空的
      return true;
    }
    return false;
  }

进行完扫描匹配之后,得到了当前scan的最优位姿. 之后向图结构中添加顶点和边, 再之后是进行回环检测.

2 MapperGraph::AddVertex()

之后,进行后端的第一步,将当前scan作为节点添加到图结构中.

这里的图结构有2个,
一个是 karto里的Graph类, 用于查找回环时,在这个图上做广度优先搜索.

这里的Graph::AddVertex() ,指的是调用MapperGraph的基类Graph 的AddVertex()函数,进行节点的保存.

第二个就是稀疏位姿图spa中的图结构了, 用于做全局位姿优化.

  /**
   * Adds a vertex representing the given scan to the graph
   * @param pScan
   */
  void MapperGraph::AddVertex(LocalizedRangeScan* pScan)
  {
    assert(pScan);

    if (pScan != NULL)
    {
      // 将当前scan转换成 vertex
      Vertex<LocalizedRangeScan>* pVertex = new Vertex<LocalizedRangeScan>(pScan);

      // 将vertex加入Graph图结构中 
      Graph<LocalizedRangeScan>::AddVertex(pScan->GetSensorName(), pVertex);

      if (m_pMapper->m_pScanOptimizer != NULL)
      {
        //使用SPA进行优化,将节点添加进 <sparse_bundle_adjustment/spa2d.h> 的求解类型中
        m_pMapper->m_pScanOptimizer->AddNode(pVertex);
      }
    }
  }


// spa_solver.cpp
void SpaSolver::AddNode(karto::Vertex<karto::LocalizedRangeScan>* pVertex)
{
  karto::Pose2 pose = pVertex->GetObject()->GetCorrectedPose();
  Eigen::Vector3d vector(pose.GetX(), pose.GetY(), pose.GetHeading());
  m_Spa.addNode(vector, pVertex->GetObject()->GetUniqueId());
}

3 MapperGraph::AddEdges()

// 向图中添加边结构
void MapperGraph::AddEdges(LocalizedRangeScan *pScan, const Matrix3 &rCovariance)
{
    MapperSensorManager *pSensorManager = m_pMapper->m_pMapperSensorManager;

    const Name &rSensorName = pScan->GetSensorName();

    // link to previous scan
    kt_int32s previousScanNum = pScan->GetStateId() - 1;
    if (pSensorManager->GetLastScan(rSensorName) != NULL)
    {
        assert(previousScanNum >= 0);
        // 添加边的第一种方式,总共有三种方式
        // 第一种 将前一帧雷达数据与当前雷达数据连接,添加边约束,添加1条边
        LinkScans(pSensorManager->GetScan(rSensorName, previousScanNum), pScan, pScan->GetSensorPose(), rCovariance);
    }

    Pose2Vector means;
    std::vector<Matrix3> covariances;

    // first scan (link to first scan of other robots)
    // 如果是第一帧雷达数据
    if (pSensorManager->GetLastScan(rSensorName) == NULL)
    {
        assert(pSensorManager->GetScans(rSensorName).size() == 1);

        std::vector<Name> deviceNames = pSensorManager->GetSensorNames();
        // 看看是不是还有其他的设备在传输 scan,对于单一的雷达,进不去循环
        forEach(std::vector<Name>, &deviceNames)
        {
            const Name &rCandidateSensorName = *iter;

            // skip if candidate device is the same or other device has no scans
            if ((rCandidateSensorName == rSensorName) || (pSensorManager->GetScans(rCandidateSensorName).empty()))
            {
                continue;
            }

            Pose2 bestPose;
            Matrix3 covariance;
            kt_double response = m_pMapper->m_pSequentialScanMatcher->MatchScan(pScan,
                                                                                pSensorManager->GetScans(rCandidateSensorName),
                                                                                bestPose, covariance);
            LinkScans(pSensorManager->GetScan(rCandidateSensorName, 0), pScan, bestPose, covariance);

            // only add to means and covariances if response was high "enough"
            if (response > m_pMapper->m_pLinkMatchMinimumResponseFine->GetValue())
            {
                means.push_back(bestPose);
                covariances.push_back(covariance);
            }
        }
    }
    else
    {
        // link to running scans
        // 第二种 将当前scan 与 running scans 添加边,running scans 就是最近的70个scan,所有的running scans只添加1条边
        Pose2 scanPose = pScan->GetSensorPose();
        means.push_back(scanPose);
        covariances.push_back(rCovariance);
        LinkChainToScan(pSensorManager->GetRunningScans(rSensorName), pScan, scanPose, rCovariance);
    }

    // link to other near chains (chains that include new scan are invalid)
    // 第三种 将当前scan 与 NearChains scans 添加边
    LinkNearChains(pScan, means, covariances);

    if (!means.empty())
    {
        pScan->SetSensorPose(ComputeWeightedMean(means, covariances));
    }
}

向位姿图中添加边一共存在三种方式:

第一种: LinkScans()

将前一帧雷达数据与当前雷达数据连接,添加边约束. 调用一次SpaSolver::AddConstraint().

第二种 LinkChainToScan()

karto中将 一定范围内的 多个连续的 scan 称为一条chain, 中文不好翻译…就直接这么叫吧.

running scans 是在当前scan之前的连续的20m范围内的scan.

将running scans 作为一条chain, 将当前scan与这个chain添加一条边, 在所有的running scans中找到距离当前scan最近的一个scan, 再调用 LinkScans() 将这两个scan添加一条边.

第三种 LinkNearChains()

首先Graph图结构中搜索与当前scan的距离在一定阈值范围内的所有的scan, 这些scan被karto称为near scan .

然后根据near scan的index, 为每一个near scan 扩充成为一条chain, 以index增加 和 index减小 两个方向将 near scan 扩充成为一条chain. 同时, 这条chain 不可以包含当前的scan, 这种chain被成为 near chain.

3.1 将当前scan与前一帧scan添加边

3.1.1 MapperGraph::LinkScans()

// 将前一帧雷达数据与当前雷达数据连接,添加边约束,添加1条边
void MapperGraph::LinkScans(LocalizedRangeScan *pFromScan, LocalizedRangeScan *pToScan,
                            const Pose2 &rMean, const Matrix3 &rCovariance)
{
    kt_bool isNewEdge = true;
    // 将source与target连接起来,或者说将pFromScan与pToScan连接起来
    Edge<LocalizedRangeScan> *pEdge = AddEdge(pFromScan, pToScan, isNewEdge);

    // only attach link information if the edge is new
    if (isNewEdge == true)
    {
        pEdge->SetLabel(new LinkInfo(pFromScan->GetSensorPose(), rMean, rCovariance));
        if (m_pMapper->m_pScanOptimizer != NULL)
        {
            m_pMapper->m_pScanOptimizer->AddConstraint(pEdge);
        }
    }
}

3.1.2 SpaSolver::AddConstraint()

void SpaSolver::AddConstraint(karto::Edge<karto::LocalizedRangeScan> *pEdge)
{
    karto::LocalizedRangeScan *pSource = pEdge->GetSource()->GetObject();
    karto::LocalizedRangeScan *pTarget = pEdge->GetTarget()->GetObject();
    karto::LinkInfo *pLinkInfo = (karto::LinkInfo *)(pEdge->GetLabel());

    // 两个位姿间的坐标变换
    karto::Pose2 diff = pLinkInfo->GetPoseDifference();
    Eigen::Vector3d mean(diff.GetX(), diff.GetY(), diff.GetHeading());

    // 信息矩阵, 协方差矩阵的逆
    karto::Matrix3 precisionMatrix = pLinkInfo->GetCovariance().Inverse();
    Eigen::Matrix<double, 3, 3> m;
    m(0, 0) = precisionMatrix(0, 0);
    m(0, 1) = m(1, 0) = precisionMatrix(0, 1);
    m(0, 2) = m(2, 0) = precisionMatrix(0, 2);
    m(1, 1) = precisionMatrix(1, 1);
    m(1, 2) = m(2, 1) = precisionMatrix(1, 2);
    m(2, 2) = precisionMatrix(2, 2);

    m_Spa.addConstraint(pSource->GetUniqueId(), pTarget->GetUniqueId(), mean, m);
}

3.2 将当前scan与 running scans 添加边:MapperGraph::LinkChainToScan()

// 将当前scan 与 scanchain 添加边,在所有的数据中找到和当前scan距离最近的一个scan,与当前scan添加一条边
void MapperGraph::LinkChainToScan(const LocalizedRangeScanVector &rChain, LocalizedRangeScan *pScan,
                                  const Pose2 &rMean, const Matrix3 &rCovariance)
{
    Pose2 pose = pScan->GetReferencePose(m_pMapper->m_pUseScanBarycenter->GetValue());

    LocalizedRangeScan *pClosestScan = GetClosestScanToPose(rChain, pose);
    assert(pClosestScan != NULL);

    Pose2 closestScanPose = pClosestScan->GetReferencePose(m_pMapper->m_pUseScanBarycenter->GetValue());

    kt_double squaredDistance = pose.GetPosition().SquaredDistance(closestScanPose.GetPosition());
    if (squaredDistance < math::Square(m_pMapper->m_pLinkScanMaximumDistance->GetValue()) + KT_TOLERANCE)
    {
        LinkScans(pClosestScan, pScan, rMean, rCovariance);
    }
}

3.3 将当前scan与 near chains 添加边:MapperGraph::LinkNearChains()

3.3.1 LinkNearChains()

LinkNearChains() 函数首先调用 FindNearChains() 获取 near chain .

之后再对每一个有效的near chain 与 当前scan 添加边, 每一个near chain 会调用一次 LinkChainToScan().

这其实也算是个回环检测, 只不过这个搜索的范围会小一些.

// 将满足要求的near chain在图结构中添加一条边
void MapperGraph::LinkNearChains(LocalizedRangeScan *pScan, Pose2Vector &rMeans, std::vector<Matrix3> &rCovariances)
{
    // 找到满足要求的 near chain
    const std::vector<LocalizedRangeScanVector> nearChains = FindNearChains(pScan);
    const_forEach(std::vector<LocalizedRangeScanVector>, &nearChains)
    {
        // chain中scan的个数要满足要求
        if (iter->size() < m_pMapper->m_pLoopMatchMinimumChainSize->GetValue())
        {
            continue;
        }

        Pose2 mean;
        Matrix3 covariance;
        // match scan against "near" chain
        // 将当前scan 对 所有的chain 进行匹配,匹配得分大于阈值就将这条chain与scan添加一个边
        kt_double response = m_pMapper->m_pSequentialScanMatcher->MatchScan(pScan, *iter, mean, covariance, false);
        if (response > m_pMapper->m_pLinkMatchMinimumResponseFine->GetValue() - KT_TOLERANCE)
        {
            rMeans.push_back(mean);
            rCovariances.push_back(covariance);
            // 得分满足要求就在图结构中 将这个chain添加一条边
            LinkChainToScan(*iter, pScan, mean, covariance);
        }
    }
}

3.3.2 FindNearChains()

FindNearChains() 函数调用FindNearLinkedScans() 来获取 near scan, 再对所有的near scan 扩充成为 near chain , 就会得到很多个 near chain , 之后通过将当前scan与这个chain进行扫描匹配, 计算下匹配得分, 如果匹配得分高于阈值就说明是个有效的 near chain.

// 找到不包含当前scan的一系列near chain
std::vector<LocalizedRangeScanVector> MapperGraph::FindNearChains(LocalizedRangeScan *pScan)
{
    std::vector<LocalizedRangeScanVector> nearChains;

    Pose2 scanPose = pScan->GetReferencePose(m_pMapper->m_pUseScanBarycenter->GetValue());

    // to keep track of which scans have been added to a chain
    LocalizedRangeScanVector processed;

    // 在pscan周围查找可以链接的scan
    // m_pMapper->m_pLinkScanMaximumDistance 为1.5m
    const LocalizedRangeScanVector nearLinkedScans = FindNearLinkedScans(pScan,
                                                                         m_pMapper->m_pLinkScanMaximumDistance->GetValue());
    // 利用每一个 LinkedScans 来扩充成一个chain, chain要满足不包含pscan的条件
    const_forEach(LocalizedRangeScanVector, &nearLinkedScans)
    {
        LocalizedRangeScan *pNearScan = *iter;

        if (pNearScan == pScan)
        {
            continue;
        }

        // scan has already been processed, skip
        // scan已经被添加过了就跳过
        if (find(processed.begin(), processed.end(), pNearScan) != processed.end())
        {
            continue;
        }

        processed.push_back(pNearScan);

        // build up chain
        kt_bool isValidChain = true;
        std::list<LocalizedRangeScan *> chain;

        // add scans before current scan being processed
        // 从当前scan的index-1开始遍历,直到index=0
        for (kt_int32s candidateScanNum = pNearScan->GetStateId() - 1; candidateScanNum >= 0; candidateScanNum--)
        {
            LocalizedRangeScan *pCandidateScan = m_pMapper->m_pMapperSensorManager->GetScan(pNearScan->GetSensorName(),
                                                                                            candidateScanNum);

            // chain is invalid--contains scan being added
            // 包含当前scan的chain不能用
            if (pCandidateScan == pScan)
            {
                isValidChain = false;
            }

            Pose2 candidatePose = pCandidateScan->GetReferencePose(m_pMapper->m_pUseScanBarycenter->GetValue());
            kt_double squaredDistance = scanPose.GetPosition().SquaredDistance(candidatePose.GetPosition());

            if (squaredDistance < math::Square(m_pMapper->m_pLinkScanMaximumDistance->GetValue()) + KT_TOLERANCE)
            {
                chain.push_front(pCandidateScan);
                processed.push_back(pCandidateScan);
            }
            else
            {
                break;
            }
        }

        // 将当前scan加入到chain中
        chain.push_back(pNearScan);

        // add scans after current scan being processed
        // 从当前scan的index+1开始遍历,直到index=end
        kt_int32u end = static_cast<kt_int32u>(m_pMapper->m_pMapperSensorManager->GetScans(pNearScan->GetSensorName()).size());
        for (kt_int32u candidateScanNum = pNearScan->GetStateId() + 1; candidateScanNum < end; candidateScanNum++)
        {
            LocalizedRangeScan *pCandidateScan = m_pMapper->m_pMapperSensorManager->GetScan(pNearScan->GetSensorName(),
                                                                                            candidateScanNum);
            // 包含当前scan的chain不能用
            if (pCandidateScan == pScan)
            {
                isValidChain = false;
            }

            Pose2 candidatePose = pCandidateScan->GetReferencePose(m_pMapper->m_pUseScanBarycenter->GetValue());
            kt_double squaredDistance = scanPose.GetPosition().SquaredDistance(candidatePose.GetPosition());

            if (squaredDistance < math::Square(m_pMapper->m_pLinkScanMaximumDistance->GetValue()) + KT_TOLERANCE)
            {
                chain.push_back(pCandidateScan);
                processed.push_back(pCandidateScan);
            }
            else
            {
                break;
            }
        }

        if (isValidChain)
        {
            // change list to vector
            LocalizedRangeScanVector tempChain;
            std::copy(chain.begin(), chain.end(), std::inserter(tempChain, tempChain.begin()));
            // add chain to collection
            nearChains.push_back(tempChain);
        }
    }

    return nearChains;
}

3.3.3 FindNearLinkedScans()


// 根据传入不同的搜索距离,在一定范围内来寻找 NearLinkedScans
LocalizedRangeScanVector MapperGraph::FindNearLinkedScans(LocalizedRangeScan *pScan, kt_double maxDistance)
{
    // 在pScan周围寻找 NearLinkedScans
    NearScanVisitor *pVisitor = new NearScanVisitor(pScan, maxDistance, m_pMapper->m_pUseScanBarycenter->GetValue());
    LocalizedRangeScanVector nearLinkedScans = m_pTraversal->Traverse(GetVertex(pScan), pVisitor);
    delete pVisitor;

    return nearLinkedScans;
}

3.3.4 广度优先搜索算法 Traverse()

添加边时的LinkNearChains()函数 与 回环检测时的 FindPossibleLoopClosure()函数 都会调用 FindNearLinkedScans() 函数.

只不过前者是使用 FindNearLinkedScans() 的结果, 后者是将 FindNearLinkedScans() 的结果排除掉.

FindNearLinkedScans() 函数 是使用广度优先搜索算法, 在图结构中找到与当前节点距离在一定范围内的所有节点.

经常刷 leetcode 的同学可能会对这个算法很熟悉, 但是在slam中挺少遇到的, 所以这里将这一块拿出来着重讲一下.

所以, 同学们, 刷 leetcode 不是毫无作用的, 那里的算法是真正可以用到实际项目中的. slam 里就用到了.

    /**
     * Traverse the graph starting with the given vertex; applies the visitor to visited nodes
     * 广度优先搜索算法,查找给定范围内的所有的节点
     * @param pStartVertex
     * @param pVisitor
     * @return visited vertices
     */
    virtual std::vector<T *> Traverse(Vertex<T> *pStartVertex, Visitor<T> *pVisitor)
    {
        std::queue<Vertex<T> *> toVisit;
        std::set<Vertex<T> *> seenVertices;
        std::vector<Vertex<T> *> validVertices;

        toVisit.push(pStartVertex);
        seenVertices.insert(pStartVertex);

        do
        {
            Vertex<T> *pNext = toVisit.front();
            toVisit.pop();
            // 距离小于阈值就加入到validVertices中
            if (pVisitor->Visit(pNext))
            {
                // vertex is valid, explore neighbors
                validVertices.push_back(pNext);

                // 获取与这个节点相连的所有节点
                std::vector<Vertex<T> *> adjacentVertices = pNext->GetAdjacentVertices();
                forEach(typename std::vector<Vertex<T> *>, &adjacentVertices)
                {
                    Vertex<T> *pAdjacent = *iter;

                    // adjacent vertex has not yet been seen, add to queue for processing
                    if (seenVertices.find(pAdjacent) == seenVertices.end())
                    {
                        // 如果没有使用过,就加入到toVisit中,同时加入seenVertices以防止重复使用
                        toVisit.push(pAdjacent);
                        seenVertices.insert(pAdjacent);
                    }
                }
            }
        } while (toVisit.empty() == false);

        // 将结果保存成vector
        std::vector<T *> objects;
        forEach(typename std::vector<Vertex<T> *>, &validVertices)
        {
            objects.push_back((*iter)->GetObject());
        }

        return objects;
    }

3.3.6 GetAdjacentVertices()

    /**
     * Gets a vector of the vertices adjacent to this vertex
     * @return adjacent vertices
     */
    std::vector<Vertex<T> *> GetAdjacentVertices() const
    {
        std::vector<Vertex<T> *> vertices;

        const_forEach(typename std::vector<Edge<T> *>, &m_Edges)
        {
            Edge<T> *pEdge = *iter;

            // check both source and target because we have a undirected graph
            if (pEdge->GetSource() != this)
            {
                vertices.push_back(pEdge->GetSource());
            }

            if (pEdge->GetTarget() != this)
            {
                vertices.push_back(pEdge->GetTarget());
            }
        }

        return vertices;
    }

4 回环检测与全局优化

4.1 TryCloseLoop()

TryCloseLoop() 函数首先调用 FindPossibleLoopClosure() 获取可能是回环的 chain , 然后使用当前scan与这个chain进行粗扫描匹配,如果响应值大于阈值,协方差小于阈值,再进行精匹配,如果精匹配的得分(响应值)大于阈值,则找到了回环

如果得分大于阈值, 就认为找到了一个回环, 调用LinkChainToScan() 函数添加一条边, 并紧接着调用 CorrectPoses() 函数执行全局优化,重新计算所有位姿.

之后再调用FindPossibleLoopClosure() 函数, 按照上次的index继续进行回环检测与匹配, 如果有回环了再进行扫描匹配. 直到index为最后一个scan的索引为止.


// 进行回环检测,找到回环后再进行全局优化
kt_bool MapperGraph::TryCloseLoop(LocalizedRangeScan *pScan, const Name &rSensorName)
{
    kt_bool loopClosed = false;

    kt_int32u scanIndex = 0;
    
    // 通过FindPossibleLoopClosure遍历所有之前的scan,计算其与当前pscan的距离
    // 找到 连续的几帧和当前帧的距离小于阈值,同时不属于 Near Linked Scans的 几个scan,也是一条chain
    LocalizedRangeScanVector candidateChain = FindPossibleLoopClosure(pScan, rSensorName, scanIndex);

    while (!candidateChain.empty())
    {
        Pose2 bestPose;
        Matrix3 covariance;
        // 粗匹配
        kt_double coarseResponse = m_pLoopScanMatcher->MatchScan(pScan, candidateChain,bestPose, covariance, false, false);

        if ((coarseResponse > m_pMapper->m_pLoopMatchMinimumResponseCoarse->GetValue()) &&
            (covariance(0, 0) < m_pMapper->m_pLoopMatchMaximumVarianceCoarse->GetValue()) &&
            (covariance(1, 1) < m_pMapper->m_pLoopMatchMaximumVarianceCoarse->GetValue()))
        {
            LocalizedRangeScan tmpScan(pScan->GetSensorName(), pScan->GetRangeReadingsVector());
            tmpScan.SetUniqueId(pScan->GetUniqueId());
            tmpScan.SetTime(pScan->GetTime());
            tmpScan.SetStateId(pScan->GetStateId());
            tmpScan.SetCorrectedPose(pScan->GetCorrectedPose());
            tmpScan.SetSensorPose(bestPose); // This also updates OdometricPose.

            // 精匹配 这里的这句和上面 不同之处在于 MatchScan的最后一个参数, 这里使用了缺省值 true,因此进行细匹配
            kt_double fineResponse = m_pMapper->m_pSequentialScanMatcher->MatchScan(&tmpScan, candidateChain,bestPose, covariance, false);

            if (fineResponse < m_pMapper->m_pLoopMatchMinimumResponseFine->GetValue())
            {
                // m_pMapper->FireLoopClosureCheck("REJECTED!");
            }
            else
            {
                // 精匹配得分大于阈值,认为找到回环了
                pScan->SetSensorPose(bestPose);

                // 将chain添加边约束,1个candidateChain添加1个边约束,等会要用来优化
                LinkChainToScan(candidateChain, pScan, bestPose, covariance);

                // 找到回环了立刻执行一下全局优化,重新计算所有位姿
                CorrectPoses();
                loopClosed = true;
            }
        }
        // 计算下一个candidateChain, scanIndex会继续增加
        candidateChain = FindPossibleLoopClosure(pScan, rSensorName, scanIndex);
    }

    return loopClosed;
}

4.3 FindPossibleLoopClosure()

FindPossibleLoopClosure() 首先调用 FindNearLinkedScans() 获取当前节点的 near scan, 这些near scan 不可以作为回环.

这块我没搞懂, 可能是因为 near scan 已经在上边的程序中使用过了. 但是这块的搜索距离又和上边的不一样… 不理解.

之后, 对所有之前保存的scan做依次遍历, 计算与当前scan的距离.

如果距离小于m_pLoopSearchMaximumDistance, 那说明这附近可能是个存在回环的区域, 在这个区域范围内时向chain中添加数据.

当距离已经大于 m_pLoopSearchMaximumDistance , 说明已经出了回环可能存在的区域, 当出了这个区域时停止向chain添加scan,并判断这个chain是否是个有效的chain

有效的chain 要满足几个条件:

  • 要满足连续的几个候选解都在这个范围内才能是一个有效的chain.
  • 如果 pCandidateScan 在 nearLinkedScans 中, 就将这个chain删掉.
  • chain中的scan个数大于阈值m_pLoopMatchMinimumChainSize

如果找到了一个有效的chain, 程序就先退出, 并将当前处理到的scan的index返回.

// 遍历之前保存的每个scan,计算与当前pscan的距离,如果能够满足连续几个scan与pscan的距离都在阈值范围内,则认为找到了一个可能的回环
LocalizedRangeScanVector MapperGraph::FindPossibleLoopClosure(LocalizedRangeScan *pScan,
                                                              const Name &rSensorName,
                                                              kt_int32u &rStartNum)
{
    LocalizedRangeScanVector chain; // return value
    // 得到 当前帧的扫描点世界坐标的平均值
    Pose2 pose = pScan->GetReferencePose(m_pMapper->m_pUseScanBarycenter->GetValue());

    // possible loop closure chain should not include close scans that have a
    // path of links to the scan of interest
    
    // m_pMapper->m_pLoopSearchMaximumDistance可以设置为15m

    // 找到可以作为near linked 的scan,这些scan不可作回环
    const LocalizedRangeScanVector nearLinkedScans =
        FindNearLinkedScans(pScan, m_pMapper->m_pLoopSearchMaximumDistance->GetValue());

    kt_int32u nScans = static_cast<kt_int32u>(m_pMapper->m_pMapperSensorManager->GetScans(rSensorName).size());
    for (; rStartNum < nScans; rStartNum++)
    {
        // 遍历每个scan, 每个scan都是可能是回环的候选项
        LocalizedRangeScan *pCandidateScan = m_pMapper->m_pMapperSensorManager->GetScan(rSensorName, rStartNum);
        // 获取候选项的位姿
        Pose2 candidateScanPose = pCandidateScan->GetReferencePose(m_pMapper->m_pUseScanBarycenter->GetValue());
        // 计算当前scan与候选项的距离的平方
        kt_double squaredDistance = candidateScanPose.GetPosition().SquaredDistance(pose.GetPosition());
        // 如果距离小于m_pLoopSearchMaximumDistance, 那说明这附近可能是个存在回环的区域
        // 在这个区域范围内时 向chain中添加数据, 要满足连续的几个候选解都在这个范围内才能是一个有效的chain
        if (squaredDistance < math::Square(m_pMapper->m_pLoopSearchMaximumDistance->GetValue()) + KT_TOLERANCE)
        {
            // a linked scan cannot be in the chain
            // 如果 pCandidateScan 在 nearLinkedScans 中,就将这个chain删掉
            if (find(nearLinkedScans.begin(), nearLinkedScans.end(), pCandidateScan) != nearLinkedScans.end())
            {
                chain.clear();
            }
            // 在这个区域范围内时 向chain中添加数据
            else
            {
                chain.push_back(pCandidateScan);
            }
        }
        // 当距离已经大于 m_pLoopSearchMaximumDistance , 说明已经出了回环可能存在的区域
        // 当出了这个区域时停止向chain添加scan,并判断这个chain是否是个有效的chain
        else
        {
            // return chain if it is long "enough"
            // 如果chain中的scan个数大于阈值,说明是个有效的回环chain,提前返回
            if (chain.size() >= m_pMapper->m_pLoopMatchMinimumChainSize->GetValue())
            {
                return chain;
            }
            // 个数小于阈值,是个无效的回环chain,删掉
            else
            {
                chain.clear();
            }
        }
    }

    return chain;
}

4.4 执行位姿图优化

4.4.1 MapperGraph::CorrectPoses()

CorrectPoses() 是进行位姿图优化求解的地方。将这个函数放在了 回环检测的里边,也就是说,只有在回环检测通过(2帧scan匹配得分大于阈值时)的情况下,才进行一次图优化求解。

// 执行后端优化,校正所有的位姿
void MapperGraph::CorrectPoses()
{
    // optimize scans!
    ScanSolver *pSolver = m_pMapper->m_pScanOptimizer;
    if (pSolver != NULL)
    {
        // 执行优化操作
        pSolver->Compute();

        const_forEach(ScanSolver::IdPoseVector, &pSolver->GetCorrections())
        {
            // 保存优化后的所有位姿
            m_pMapper->m_pMapperSensorManager->GetScan(iter->first)->SetSensorPose(iter->second);
        }

        pSolver->Clear();
    }
}

4.4.2 SpaSolver::Compute()

// 进行全局优化
void SpaSolver::Compute()
{
    corrections.clear();

    typedef std::vector<sba::Node2d, Eigen::aligned_allocator<sba::Node2d>> NodeVector;

    ROS_INFO("Calling doSPA for loop closure");
    // 进行优化求解
    m_Spa.doSPA(40);
    ROS_INFO("Finished doSPA for loop closure");

    // 将优化后的位姿转换成karto的格式
    NodeVector nodes = m_Spa.getNodes();
    forEach(NodeVector, &nodes)
    {
        karto::Pose2 pose(iter->trans(0), iter->trans(1), iter->arot);
        corrections.push_back(std::make_pair(iter->nodeId, pose));
    }
}

图优化求解的结果:

corrections 存储的是图结构 所有的 顶点id 与 顶点
图优化的结果是 更新了所有顶点(也就是有效scan)的坐标值

总结

  1. karto的后端优化是和回环检测结合在一起的,首先向SPA中添加顶点,再通过3种方式向SPA中添加边结构,然后试着找回环。

  2. 找回环:首先通过坐标距离小于阈值, 并且形成的chain中scan的个数大于阈值,才算是一个候选的回环chain。然后使用当前scan与这个chain进行粗扫描匹配,如果响应值大于阈值,协方差小于阈值,再进行精匹配,如果精匹配的得分(响应值)大于阈值,则找到了回环。

  3. 找到了回环则将回环约束作为边结构添加到SPA中,然后进行图优化求解,更新所有scan的坐标。

  4. 通过 扫描匹配 与 后端优化环节可知,整个open_karto没有维护地图, 每次扫描匹配的地图都是通过一系列的scan新生成的, 是非常省资源的。
    最后的栅格地图是通过所有优化后的scan进行生成的,在slam_karto进行进行调用,不用维护整个栅格地图,非常的省cpu与内存。

REFERENCES

加入了详细中文解释的open_karto地址: https://github.com/kadn/open_karto

Karto_slam框架与代码解析 https://blog.csdn.net/qq_24893115/article/details/52965410

Karto SLAM之open_karto代码学习笔记(二) https://blog.csdn.net/wphkadn/article/details/90247146

SLAM_Karto 学习(四) 深入理解 ScanMatch 过程 https://blog.csdn.net/Fourier_Legend/article/details/81558886?utm_source=blogxgwz0#process%E5%87%BD%E6%95%B0uml