《视觉SLAM十四讲》ch13 设计SLAM系统 相机轨迹实现

news2025/3/25 10:52:34

前言


        相信大家在slam学习中,一定会遇到slam系统的性能评估问题。虽然有EVO这样的开源评估工具,我们也需要自己了解系统生成的trajectory.txt的含义,方便我们更好的理解相机的运行跟踪过程。

项目配置如下:

 数据解读:


        这是KITTI数据集中的轨迹 (trajectory.txt),通常用于视觉里程计(VO)或SLAM的评估。文件格式通常是 12个浮点数,表示一个 4×4 变换矩阵(刚体变换),具体来说,每一行表示一个位姿(相机的姿态和位置),格式如下:

解析每列数据:

  1. 前3×3部分(列1到列9):旋转矩阵 RRR(描述相机的旋转)
  2. 列10到列12:平移向量 ttt(描述相机的位移)

项目解读:

run_kitti_stereo.cpp的vo->Init()初始化前段可视化模块viewer_,后端backend_,地图map_等slam子系统。

进入主线程的特征追踪:

void VisualOdometry::Run() {
    while (1) {
        LOG(INFO) << "VO is running";
        if (Step() == false) {
            // 退出,并保存轨迹
            frontend_->SaveTrajectory("/home/xulei/下载/slambook2/ch13/trajectory.txt");
            break;
        }
    }

    backend_->Stop();
    viewer_->Close();

    LOG(INFO) << "VO exit";
}
bool VisualOdometry::Step() {
    if (dataset_->current_image_index_ > 3681) {  //判断避免异常退出,数据集大小。
        return false;
    }
    Frame::Ptr new_frame = dataset_->NextFrame();
    if (new_frame == nullptr) return false;

    auto t1 = std::chrono::steady_clock::now();
    bool success = frontend_->AddFrame(new_frame);
    auto t2 = std::chrono::steady_clock::now();
    auto time_used =
        std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
    LOG(INFO) << "VO cost time: " << time_used.count() << " seconds.";
    return success;

}

}  // namespace myslam

 其中,nextframe()方法是读取数据集,并进行当前帧的创建。

  Frame::Ptr new_frame = dataset_->NextFrame();
AddFrame(new_frame)方法是进入特征提取与追踪线程。
  bool success = frontend_->AddFrame(new_frame);

其中,很重要的一点是跟踪的质量评估:

bool Frontend::AddFrame(myslam::Frame::Ptr frame) {
    current_frame_ = frame;

    switch (status_) {
        case FrontendStatus::INITING:
            StereoInit();
            break;
        case FrontendStatus::TRACKING_GOOD:
        case FrontendStatus::TRACKING_BAD:
            Track();
            break;
        case FrontendStatus::LOST:
            Reset();
            break;
    }
    // 让线程暂停 0.5 秒
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

    last_frame_ = current_frame_;
    return true;
}

前端的工作大多在Frontend类中完成。

 该部分也是我们代码改进,并进一步实现轨迹的保存工作。

注意,我们这里不考虑关键帧的处理,即后端的优化的实时的影响,直接在位姿计算完成就推算世界坐标下的轨迹。

首先,在frontend.h类中,加入方法,与轨迹变量。

void SaveTrajectory(const std::string &filename);  // 轨迹保存
std::vector<Eigen::Matrix<double, 3, 4>> trajectory_;

具体在.cpp文件实现。

 找到StereoInit(),Track()函数,将函数替换为:(这里代码比较冗余,也可以封装成一个方法调用)

bool Frontend::StereoInit() {

    // **转换到世界坐标系,并存储 3×4 变换矩阵**
    SE3 T_c_w = current_frame_->Pose();   // 获取相机位姿 T_c_w
    SE3 T_w_c = T_c_w.inverse();          // 计算世界坐标系到相机的变换 T_w_c
    Eigen::Matrix3d R_w_c = T_w_c.rotationMatrix().matrix();  // 获取旋转矩阵 (3×3)
    Eigen::Vector3d t_w_c = T_w_c.translation();              // 获取平移向量 (3×1)

    // 记录 3×4 矩阵
    Eigen::Matrix<double, 3, 4> world_pose;
    world_pose.block<3,3>(0,0) = R_w_c;  // 旋转部分
    world_pose.block<3,1>(0,3) = t_w_c;  // 平移部分

    // **记录轨迹**
    trajectory_.push_back(world_pose);


    int num_features_left = DetectFeatures();
    int num_coor_features = FindFeaturesInRight();
    if (num_coor_features < num_features_init_) {
        return false;
    }

    bool build_map_success = BuildInitMap();
    if (build_map_success) {
        status_ = FrontendStatus::TRACKING_GOOD;
        if (viewer_) {
            viewer_->AddCurrentFrame(current_frame_);
            viewer_->UpdateMap();
        }
        return true;
    }
    return false;
}
ool Frontend::Track() {
    if (last_frame_) {
        current_frame_->SetPose(relative_motion_ * last_frame_->Pose());
    }

    int num_track_last = TrackLastFrame();
    tracking_inliers_ = EstimateCurrentPose();
    printf("num_track_last = %d", tracking_inliers_);
    if (tracking_inliers_ > num_features_tracking_) {
        status_ = FrontendStatus::TRACKING_GOOD;
    } else if (tracking_inliers_ > num_features_tracking_bad_) {
        status_ = FrontendStatus::TRACKING_BAD;
    } else {
        status_ = FrontendStatus::LOST;
    }

    InsertKeyframe();
    relative_motion_ = current_frame_->Pose() * last_frame_->Pose().inverse();

    // **转换到世界坐标系,并存储 3×4 变换矩阵**
    SE3 T_c_w = current_frame_->Pose();   // 获取相机位姿 T_c_w
    SE3 T_w_c = T_c_w.inverse();          // 计算世界坐标系到相机的变换 T_w_c
    Eigen::Matrix3d R_w_c = T_w_c.rotationMatrix().matrix();  // 获取旋转矩阵 (3×3)
    Eigen::Vector3d t_w_c = T_w_c.translation();              // 获取平移向量 (3×1)

    // 记录 3×4 矩阵
    Eigen::Matrix<double, 3, 4> world_pose;
    world_pose.block<3,3>(0,0) = R_w_c;  // 旋转部分
    world_pose.block<3,1>(0,3) = t_w_c;  // 平移部分

    // **记录轨迹**
    trajectory_.push_back(world_pose);

    // // **调试输出**
    // std::cout << std::fixed << std::setprecision(9);
    // std::cout << "Frame " << trajectory_.size()
    //           << " | R_w_c: \n" << R_w_c
    //           << "\n t_w_c: " << t_w_c.transpose() << std::endl;

    if (viewer_) viewer_->AddCurrentFrame(current_frame_);
    return true;
}

 最后实现其SaveTrajector()函数,保存.txt文件:

void Frontend::SaveTrajectory(const std::string &filename) {
    std::ofstream file(filename);
    if (!file.is_open()) {
        std::cerr << "Error opening trajectory file: " << filename << std::endl;
        return;
    }

    file << std::fixed << std::setprecision(9);  // 设定输出精度

    for (const auto &pose : trajectory_) {
        // 提取旋转和平移部分(假设 trajectory_ 存储的是 3×4 矩阵)
        Eigen::Matrix3d R_w_c = pose.block<3,3>(0,0);
        Eigen::Vector3d t_w_c = pose.block<3,1>(0,3);

        // 写入文件
        file << R_w_c(0,0) << " " << R_w_c(0,1) << " " << R_w_c(0,2) << " " << t_w_c(0) << " "
             << R_w_c(1,0) << " " << R_w_c(1,1) << " " << R_w_c(1,2) << " " << t_w_c(1) << " "
             << R_w_c(2,0) << " " << R_w_c(2,1) << " " << R_w_c(2,2) << " " << t_w_c(2) << std::endl;
    }

    file.close();
    std::cout << "Trajectory saved to " << filename << std::endl;
}

重新编译代码,运行:


最后得到,格式为:

0.999990331 -0.002488019 -0.003625839 0.001951625 0.002480646 0.999994849 -0.002036517 -0.007563952 0.003630888 0.002027503 0.999991353 0.681278434
0.999971701 -0.001201005 -0.007426599 -0.014921545 0.001173216 0.999992299 -0.003745129 -0.014889465 0.007431040 0.003736310 0.999965409 1.369789353
0.999938793 -0.000961412 -0.011022030 -0.041936008 0.000902450 0.999985264 -0.005353181 -0.016047572 0.011027014 0.005342906 0.999924926 2.069745033
0.999877013 0.000796693 -0.015662799 -0.064529202 -0.000890743 0.999981611 -0.005998610 -0.025888373 0.015657732 0.006011824 0.999859337 2.795177413
0.999790482 -0.000408300 -0.020465240 -0.073989152 0.000286856 0.999982336 -0.005936741 -0.036340712 0.020467303 0.005929627 0.999772939 3.531565772
0.999620212 0.011536263 -0.025026921 -0.084931933 -0.011736120 0.999900288 -0.007853525 -0.060457271 0.024933825 0.008144261 0.999655928 4.288284960
0.999448852 0.014043954 -0.030079214 -0.119917101 -0.014297214 0.999863991 -0.008221300 -0.075260961 0.029959663 0.008646818 0.999513707 5.057364575
0.999297794 0.014574258 -0.034518265 -0.151109538 -0.014807489 0.999869166 -0.006510771 -0.087394672 0.034418859 0.007017328 0.999382859 5.840754611
0.999154885 0.014253942 -0.038553105 -0.200260214 -0.014383673 0.999891776 -0.003089717 -0.114111359 0.038504892 0.003641641 0.999251776 6.608571213
0.999029138 0.013669141 -0.041880029 -0.238672274 -0.013693447 0.999906197 -0.000293535 -0.124407472 0.041872088 0.000866732 0.999122604 7.415410187
0.998883339 0.015144070 -0.044751887 -0.279161319 -0.015184780 0.999884542 -0.000569862 -0.138168200 0.044738090 0.001248774 0.998997970 8.233449862
0.998772936 0.014903271 -0.047228336 -0.316118436 -0.014963441 0.999887618 -0.000920710 -0.143100781 0.047209307 0.001626278 0.998883695 9.053884639
0.998375407 0.030955991 -0.047835896 -0.362929685 -0.031132074 0.999510955 -0.002940145 -0.168459719 0.047721487 0.004424599 0.998850881 9.881833862
0.998340888 0.030504872 -0.048835694 -0.420649005 -0.030656826 0.999527170 -0.002365388 -0.167085051 0.048740448 0.003858611 0.998804025


进一步用evo评估该系统:

evo_traj kitti  trajectory.txt  --ref=02.txt -p   --plot_mode xz

平面误差:

 相机的xyz轴的误差:

 

 相机的旋转:

 

 总结

这样实现的方法可能比较取巧,没有考虑后续的BA优化,后续也会持续的优化该系统,希望能给大家提供一些项目的改进灵感与slam的相机运动理解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2319049.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在类Unix终端中如何实现快速进入新建目录

&#x1f6aa; 前言 相信喜欢使用终端工作的小伙伴或多或少会被一个小地方给膈应&#xff0c;那就是每次想要新建一个文件夹并且进入之&#xff0c;那么就需要两条指令&#xff1a;mkdir DIR和cd DIR&#xff0c;有些人可能要杠了&#xff0c;我一条指令也能&#xff0c;mkdir…

TG电报群管理机器人定制开发的重要性

在Telegram&#xff08;电报&#xff09;用户突破20亿、中文社群规模持续扩张的背景下&#xff0c;定制化群管理机器人的开发已成为社群运营的战略刚需。这种技术工具不仅解决了海量用户管理的效率难题&#xff0c;更通过智能化功能重构了数字社群的治理范式。本文从管理效能、…

VNA操作使用学习-01 界面说明

以我手里面的liteVNA为例。也可以参考其他的nanoVNA的操作说明。我先了解一下具体的菜单意思。 今天我想做一个天调&#xff0c;居然发现我连一颗基本的50欧姆插件电阻和50欧姆的smt电阻的幅频特性都没有去测试过&#xff0c;那买来这个nva有什么用途呢&#xff0c;束之高阁求…

耘想Docker版Linux NAS的安装说明

耘想LinNAS&#xff08;Linux NAS&#xff09;可以通过Docker部署&#xff0c;支持x86和arm64两种硬件架构。下面讲解LinNAS的部署过程。 1. 安装Docker CentOS系统&#xff1a;yum install docker –y Ubuntu系统&#xff1a;apt install docker.io –y 2. 下载LinNas镜像…

OpenCV图像拼接(4)图像拼接模块的一个匹配器类cv::detail::BestOf2NearestRangeMatcher

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::detail::BestOf2NearestRangeMatcher 是 OpenCV 库中用于图像拼接模块的一个匹配器类&#xff0c;专门用于寻找两幅图像之间的最佳特征点匹配…

不用 Tomcat?SpringBoot 项目用啥代替?

在SpringBoot框架中&#xff0c;我们使用最多的是Tomcat&#xff0c;这是SpringBoot默认的容器技术&#xff0c;而且是内嵌式的Tomcat。 同时&#xff0c;SpringBoot也支持Undertow容器&#xff0c;我们可以很方便的用Undertow替换Tomcat&#xff0c;而Undertow的性能和内存使…

Zabbix安装(保姆级教程)

Zabbix 是一款开源的企业级监控解决方案&#xff0c;能够监控网络的多个参数以及服务器、虚拟机、应用程序、服务、数据库、网站和云的健康状况和完整性。它提供了灵活的通知机制&#xff0c;允许用户为几乎任何事件配置基于电子邮件的告警&#xff0c;从而能够快速响应服务器问…

鸿蒙开发真机调试:无线调试和USB调试

前言 在鸿蒙开发的旅程中&#xff0c;真机调试堪称至关重要的环节&#xff0c;其意义不容小觑。虽说模拟器能够为我们提供初步的测试环境&#xff0c;方便我们在开发过程中快速预览应用的基本效果&#xff0c;但它与真机环境相比&#xff0c;仍存在诸多差异。就好比在模拟器中…

工厂函数详解:概念、目的与作用

一、什么是工厂函数&#xff1f; 工厂函数&#xff08;Factory Function&#xff09;是一种设计模式&#xff0c;其核心是通过一个函数来 创建并返回对象&#xff0c;而不是直接使用 new 或构造函数实例化对象。它封装了对象的创建过程&#xff0c;使代码更灵活、可维护。 二、…

Python简单爬虫实践案例

学习目标 能够知道Web开发流程 能够掌握FastAPI实现访问多个指定网页 知道通过requests模块爬取图片 知道通过requests模块爬取GDP数据 能够用pyecharts实现饼图 能够知道logging日志的使用 一、基于FastAPI之Web站点开发 1、基于FastAPI搭建Web服务器 # 导入FastAPI模…

基于springboot的房产销售系统(016)

摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于房产销售系统当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了房产销售系统&#xff0c;它彻底改变了过去传统的…

云盘搭建笔记

报错问题&#xff1a; No input file specified. 伪静态 location / {if (!-e $request_filename) { rewrite ^(.*)$ /index.php/$1 last;break;} } location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php/$1 last; break; } } 设…

《从深海到卫浴:Relax Max如何用军工科技重塑生活仪式》​

《从深海到卫浴&#xff1a;Relax Max如何用军工科技重塑生活仪式》​ 当瑞士联邦理工学院的一纸专利授权书揭开帷幕&#xff0c;卫浴行业终于意识到&#xff1a;Relax Max的「军工科技民用化」绝非营销噱头。这支由前潜艇工程师和航天材料学家组成的团队&#xff0c;将核潜艇…

【vulhub/wordpress靶场】------获取webshell

1.进入靶场环境&#xff1a; 输入&#xff1a;cd / vulhub / wordpress / pwnscriptum 修改版本号&#xff1a; vim docker-compose.yml version: 3 保存退出 开启靶场环境&#xff1a; docker - compose up - d 开启成功&#xff0c;docker ps查看端口 靶场环境80…

人工智能助力家庭机器人:从清洁到陪伴的智能转型

引言&#xff1a;家庭机器人进入智能时代 过去&#xff0c;家庭机器人只是简单的“工具”&#xff0c;主要用于扫地、拖地、擦窗等单一任务。然而&#xff0c;随着人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;家庭机器人正经历从“机械助手”向“智能管家”甚…

【第14节】windows sdk编程:进程与线程介绍

目录 一、进程与线程概述 1.1 进程查看 1.2 何为进程 1.3 进程的创建 1.4 进程创建实例 1.5 线程查看 1.6 何为线程 1.7 线程的创建 1.8 线程函数 1.9 线程实例 二、内核对象 2.1 何为内核对象 2.2 内核对象的公共特点 2.3 内核对象句柄 2.4 内核对象的跨进程访…

STM32U575RIT6单片机(四)

作业: 使用I2C获取SHT20传感器温湿度 使用I2C获取AP3216C三合一传感器: 光照, 接近, 红外 三个功能 合并的传感器 #ifndef SHT20_H #define SHT20_H#include "stdint.h" #include "i2c.h" #include "stdio.h" //1、确定从机的设备地址(代码不…

EMQX安装与配置

EMQX安装与配置 EMQX安装与配置 https://www.emqx.com/zh/downloads-and-install/broker?osUbuntucd /usr/local/srcwget https://www.emqx.com/zh/downloads/broker/4.4.19/emqx-4.4.19-otp24.3.4.2-1-ubuntu16.04-amd64.deb sudo apt install ./emqx-4.4.19-otp24.3.4.2-1…

JVM逃逸分析作用和原理

JVM逃逸分析作用和原理 在JVM的性能优化中&#xff0c;我们通常会关注内存分配、垃圾回收等问题。而逃逸分析&#xff08;Escape Analysis&#xff09;是JVM中一种精妙的优化技术&#xff0c;它可以在对象分配时判断该对象是否会在方法或线程之外被访问&#xff0c;从而影响其…

拓展 Coco AI 功能 - 智能检索 Hexo 博客

在之前的文章中&#xff0c;我们成功让 Coco AI 检索 Hugo 博客&#xff0c;这对于博客作者来说是一大福音。然而&#xff0c;从 Hexo 迁移到 Hugo 的成本不容小觑&#xff0c;毕竟大多数开发者对 Node.js 更熟悉&#xff0c;而 Golang 相对陌生。那么&#xff0c;既然 Coco AI…