ORB-SLAM2的稠密重建实战(1) — 流程与函数功能说明

news2024/11/23 15:13:18

目录

0 系统整体流程

1 输出信息

2 初始化文件

3 初始化并运行追踪线程Tracking

Step1:地图初始化

Step2:初始化成功(mbOnlyTracking)

Step3:局部地图跟踪TrackLocalMap()

Step4:跟踪成功

Step5:跟踪失败

Step6:记录位姿信息

4 初始化并运行局部建图线程LocalMapping

Step1:局部建图线程时设置不接受关键帧

Step2:处理列表中的关键帧

Step3:根据地图点的观测情况剔除质量不好的地图点

Step4:当前关键帧与相邻关键帧通过三角化产生新的地图点

Step5:检查并融合当前关键帧与相邻关键帧帧(两级相邻)中重复的地图点

Step6:进行局部地图的BA并剔除当前帧相邻的关键帧中冗余的关键帧

5 初始化并运行回环检测线程LoopClosing

Step1:查询闭环检测队列是否有新关键帧

Step2:闭环检测

Step3:计算Sim3()变换ComputeSim3()

Step4:闭环矫正

Step5:全局BA


0 系统整体流程

首先加载图片LoadImages,加载SLAM系统,运行System::System,System::System为该系统的运行函数,后面1-5点细讲,处理完后SLAM.Shutdown(),最后保存TUM格式的相机轨迹。

#include<iostream>
#include<algorithm>
#include<fstream>
#include<chrono>
#include<opencv2/core/core.hpp>
#include<System.h>
#include<unistd.h>
using namespace std;

/**
 * @brief 获取图像文件的信息
 * @param[in]  strImagePath     图像文件存放路径
 * @param[in]  strPathTimes     时间戳文件的存放路径
 * @param[out] vstrImages       图像文件名数组
 * @param[out] vTimeStamps      时间戳数组
 */
void LoadImages(const string &strImagePath, const string &strPathTimes,
                vector<string> &vstrImages, vector<double> &vTimeStamps);

int main(int argc, char **argv)
{
    // step 0 检查输入参数个数是否足够
    if(argc != 5)
    {
        cerr << endl << "Usage: ./mono_tum path_to_vocabulary path_to_settings path_to_image_folder path_to_times_file" << endl;
        return 1;
    }

    // step 1 加载图像
    // Retrieve paths to images
    // 图像序列的文件名字符串序列
    vector<string> vstrImageFilenames;
    // 时间戳
    vector<double> vTimestamps;
    LoadImages(string(argv[3]),             // path_to_image_folder
               string(argv[4]),             // path_to_times_file
               vstrImageFilenames,          // 读取到的图像名称数组
               vTimestamps);                // 时间戳数组

    // 当前图像序列中的图像数目
    int nImages = vstrImageFilenames.size();

    if(nImages<=0)
    {
        cerr << "ERROR: Failed to load images" << endl;
        return 1;
    }

    // step 2 加载SLAM系统
    // Create SLAM system. It initializes all system threads and gets ready to process frames.
    ORB_SLAM2::System SLAM(
        argv[1],                            // path_to_vocabulary
        argv[2],                            // path_to_settings
        ORB_SLAM2::System::MONOCULAR,       // 单目模式
        true);                              // 启用可视化查看器

    // step 3 运行前准备
    // Vector for tracking time statistics
    // 统计追踪一帧耗时 (仅Tracker线程)
    vector<float> vTimesTrack;
    vTimesTrack.resize(nImages);

    cout << endl << "-------" << endl;
    cout << "Start processing sequence ..." << endl;
    cout << "Images in the sequence: " << nImages << endl << endl;

    // Main loop
    // step 4 依次追踪序列中的每一张图像
    cv::Mat im;
    for(int ni=0; ni<nImages; ni++)
    {
        // Read image from file
        // step 4.1 读根据前面获得的图像文件名读取图像,读取过程中不改变图像的格式 
        im = cv::imread(vstrImageFilenames[ni],CV_LOAD_IMAGE_UNCHANGED);
        double tframe = vTimestamps[ni];

        // step 4.2 图像的合法性检查
        if(im.empty())
        {
            cerr << endl << "Failed to load image at: "
                 <<  vstrImageFilenames[ni] << endl;
            return 1;
        }

        // step 4.3 开始计时
#ifdef COMPILEDWITHC11
        std::chrono::steady_clock::time_point t1 = std::chrono::steady_clock::now();
#else
        std::chrono::monotonic_clock::time_point t1 = std::chrono::monotonic_clock::now();
#endif

        // Pass the image to the SLAM system
        // step 4.4 追踪当前图像
        SLAM.TrackMonocular(im,tframe);

        // step 4.5 追踪完成,停止当前帧的图像计时, 并计算追踪耗时

#ifdef COMPILEDWITHC11
        std::chrono::steady_clock::time_point t2 = std::chrono::steady_clock::now();
#else
        std::chrono::monotonic_clock::time_point t2 = std::chrono::monotonic_clock::now();
#endif

        double ttrack= std::chrono::duration_cast<std::chrono::duration<double> >(t2 - t1).count();

        vTimesTrack[ni]=ttrack;

        // Wait to load the next frame
        // step 4.6 根据图像时间戳中记录的两张图像之间的时间和现在追踪当前图像所耗费的时间,继续等待指定的时间以使得下一张图像能够
        // 按照时间戳被送入到SLAM系统中进行跟踪
        double T=0;
        if(ni<nImages-1)
            T = vTimestamps[ni+1]-tframe;
        else if(ni>0)
            T = tframe-vTimestamps[ni-1];

        if(ttrack<T)
            usleep((T-ttrack)*1e6);
    }

    // step 5 如果所有的图像都预测完了,那么终止当前的SLAM系统
    // Stop all threads
    SLAM.Shutdown();

    // Tracking time statistics
    // step 6 计算平均耗时
    sort(vTimesTrack.begin(),vTimesTrack.end());
    float totaltime = 0;
    for(int ni=0; ni<nImages; ni++)
    {
        totaltime+=vTimesTrack[ni];
    }
    cout << "-------" << endl << endl;
    cout << "median tracking time: " << vTimesTrack[nImages/2] << endl;
    cout << "mean tracking time: " << totaltime/nImages << endl;

    // Save camera trajectory
    // step 7 保存TUM格式的相机轨迹
    // 估计是单目时有尺度漂移, 而LGA GBA都只能优化关键帧使尺度漂移最小, 普通帧所产生的轨迹漂移这里无能为力, 我猜作者这样就只
    // 保存了关键帧的位姿,从而避免普通帧带有尺度漂移的位姿对最终误差计算的影响
    SLAM.SaveKeyFrameTrajectoryTUM("KeyFrameTrajectory.txt");

    return 0;
}

// 从文件中加载图像序列中每一张图像的文件路径和时间戳
void LoadImages(const string &strImagePath, const string &strPathTimes,
                vector<string> &vstrImages, vector<double> &vTimeStamps)
{
    // 打开文件
    ifstream fTimes;
    fTimes.open(strPathTimes.c_str());
    vTimeStamps.reserve(5000);
    vstrImages.reserve(5000);
    // 遍历文件
    while(!fTimes.eof())
    {
        string s;
        getline(fTimes,s);
        // 只有在当前行不为空的时候执行
        if(!s.empty())
        {
            stringstream ss;
            ss << s;
            // 生成当前行所指出的RGB图像的文件名称
            vstrImages.push_back(strImagePath + "/" + ss.str() + ".png");
            double t;
            ss >> t;
            // 记录该图像的时间戳
            vTimeStamps.push_back(t/1e9);

        }
    }
}

System::System的框架图如下:

ORB-SLAM2总共有三个线程: Tracking , Local Mapping ,Loop Closing线程(实际上其实还多了个Viewer线程,用于显示界面)。
输入:有三种模式可以选择:单目模式、双目模式和RGB-D模式。
跟踪:Tracking 线程主要负责定位相机以及决定是否插入一个新的关键帧。将输入的每一个帧进行提取ORB Features,并且以此估计相机位姿。
局部建图:Local Mapping 线程主要负责处理新的关键帧以及执行局部BA以此来优化相机位姿。此外,负责新的 Map Points 的创建和剔除掉冗余的KeyFrames。
闭环:Loop Closing 线程主要负责回环检测和全局地图优化,减少位姿累计误差漂移,将误差均匀化,使得总体误差最小。
全局BA:优化所有的关键帧及其地图点。
位置识别:需要导入离线训练好的字典,这个字典是由视觉词袋模型构建的。新输入的图像帧需要先在线转化为词袋向量,主要应用于特征匹配、重定位、闭环。
地图:地图主要由地图点和关键帧组成。关键帧之间根据共视地图点数目组成了共视图,根据父子关系组成了生成树。

1 输出信息

  • 输出欢迎信息
  • 输出当前传感器类型:ORB-SLAM2支持单目、双目、RGB-D相机
  • 检查配置文件,打开失败输出调试信息
    // 1 输出信息
    // 输出欢迎信息
    cout << endl <<
    "ORB-SLAM2 Copyright (C) 2014-2016 Raul Mur-Artal, University of Zaragoza." << endl <<
    "This program comes with ABSOLUTELY NO WARRANTY;" << endl  <<
    "This is free software, and you are welcome to redistribute it" << endl <<
    "under certain conditions. See LICENSE.txt." << endl << endl;

    // 输出当前传感器类型
    cout << "Input sensor was set to: ";

    if(mSensor==MONOCULAR)
        cout << "Monocular" << endl;
    else if(mSensor==STEREO)
        cout << "Stereo" << endl;
    else if(mSensor==RGBD)
        cout << "RGB-D" << endl;

    // 检查配置文件
    cv::FileStorage fsSettings(strSettingsFile.c_str(), 	//将配置文件名转换成为字符串
    						   cv::FileStorage::READ);		//只读
    //如果打开失败,就输出调试信息
    if(!fsSettings.isOpened())
    {
       cerr << "Failed to open settings file at: " << strSettingsFile << endl;
       //然后退出
       exit(-1);
    }

    // 加载ORB字典
    cout << endl << "Loading ORB Vocabulary. This could take a while..." << endl;
    
    // 建立一个新的ORB字典
    mpVocabulary = new ORBVocabulary();
    // 获取字典加载状态
    bool bVocLoad = mpVocabulary->loadFromTextFile(strVocFile);
    // 如果加载失败,就输出调试信息
    if(!bVocLoad)
    {
        cerr << "Wrong path to vocabulary. " << endl;
        cerr << "Falied to open at: " << strVocFile << endl;
        //然后退出
        exit(-1);
    }
    // 否则则说明加载成功
    cout << "Vocabulary loaded!" << endl << endl;

2 初始化文件

  • 创建ORB字典
  • 创建KeyFrameDatabase(*mpVocabulary)
  • 帧绘制器FrameDrawer(mpMap)
  • 地图绘制器MapDrawer(mpMap, strSettingsFile)
	2 初始化文件
	// 加载ORB字典
    cout << endl << "Loading ORB Vocabulary. This could take a while..." << endl;
    
    // 建立一个新的ORB字典
    mpVocabulary = new ORBVocabulary();
    // 获取字典加载状态
    bool bVocLoad = mpVocabulary->loadFromTextFile(strVocFile);
    // 如果加载失败,就输出调试信息
    if(!bVocLoad)
    {
        cerr << "Wrong path to vocabulary. " << endl;
        cerr << "Falied to open at: " << strVocFile << endl;
        //然后退出
        exit(-1);
    }
    // 否则则说明加载成功
    cout << "Vocabulary loaded!" << endl << endl;

    // 检查关键帧数据库
    mpKeyFrameDatabase = new KeyFrameDatabase(*mpVocabulary);

    // 创建地图
    mpMap = new Map();

    //这里的帧绘制器和地图绘制器将会被可视化的Viewer所使用
    mpFrameDrawer = new FrameDrawer(mpMap);
    mpMapDrawer = new MapDrawer(mpMap, strSettingsFile);

3 初始化并运行追踪线程Tracking

void Tracking::Track()
{
    // track包含两部分:估计运动、跟踪局部地图
    ...
}

Step1:地图初始化

双目和RGBD相机共用一个初始化函数StereoInitialization(),单目初始化利用MonocularInitialization()

StereoInitialization()

(1)初始化要求当前帧的特征点超过500:初始化要求当前帧的特征点超过500,满足要求开始初始化。

(2)设置当前帧的状态:设置当前帧的位姿,同时将当前帧为初始关键帧pKFini,向地图mpMap中添加该初始关键帧。

(3)为每个特征点构造MapPoint:为每个特征点构造MapPoint,地图点的深度必须>0。通过反投影得到该特征点的世界坐标系下3D坐标。为该MapPoint添加属性:观测到该MapPoint的关键帧,该MapPoint的描述子,该MapPoint的平均观测方向和深度范围。

(4)在地图中添加该MapPoint并更新信息:在地图中添加该MapPoint,设置哪些特征点可以观测到该MapPoint,并将MapPoint添加到当前帧的mvpMapPoints中,建立特征点和MapPoint之间的联系。

(5)更新局部地图和其他与之相关的信息:在局部地图中更新该初始关键帧的信息,同时更新其他与之相关的信息。

(6)初始化成功

MonocularInitialization()

(1)判断单目初始器是否被创建,没有则创建,得到第一帧

(2)如果单目初始化器已经被创建,判断当前帧特征点数大于100,得到第一帧

(3)在第一帧mInitialFrame与第二帧mCurrentFrame中找匹配的特征点对

(4)如果两帧之间的匹配点太少,重新初始化

(5)通过H模型或F模型进行单目初始化,得到两帧间相对运动、初始MapPoints

(6)将三角化得到的3D点包装成MapPoints

Step2:初始化成功(mbOnlyTracking)

如果初始化成功,利用mbOnlyTracking判断跟踪模式是正常SLAM模式(定位+地图更新)还是仅定位模式;正常SLAM模式分为参考关键帧跟踪TrackReferenceKeyFrame()、恒速模型跟踪TrackWithMotionModel()、重定位跟踪Relocalization()

参考关键帧跟踪TrackReferenceKeyFrame()

(1)将当前普通帧的描述子转化为BoW向量:词袋BoW加速当前帧与参考帧之间的特征点匹配

(2)当前帧与参考帧之间的特征点匹配:匹配数目小于设定值,认为跟踪失败

(3)初始化当前帧的位姿:将上一帧的位姿态作为当前帧位姿的初始值,通过优化3D-2D的重投影误差来获得位姿

(4)剔除优化后的匹配点中的外点

(5)判断是否跟踪成功:判断匹配的特征点是否满足设定值,满足则成功

恒速模型跟踪TrackWithMotionModel()

恒速模型跟踪和参考关键帧跟踪流程类似,区别在于恒速模型跟踪有上一帧的位姿。

(1)更新上一帧的位姿:对于双目或RGB-D相机,还会根据深度值生成临时地图点

(2)根据上一帧特征点对应地图点进行投影匹配

(3)优化当前帧位姿

(4)剔除地图点中外点

(5)判断是否跟踪成功:判断匹配的特征点是否满足设定值,满足则成功

重定位跟踪Relocalization()

恒速模型跟踪和参考关键帧跟踪都失效后,开启重定位跟踪

(1)计算当前帧特征点的词袋向量

(2)找到与当前帧相似的候选关键帧:通过BoW进行匹配

(3)通过EPnP算法估计姿态

(4)通过PoseOptimization对姿态进行优化求解:只优化位姿,不优化地图点的坐标,返回的是内点的数量

(5)判断是否求解成功:防止误匹配,需要经过一系列的判断,直到找到能够匹配上的关键帧,如果内点较少,则通过投影的方式对之前未匹配的点进行匹配,再进行优化求解。

Step3:局部地图跟踪TrackLocalMap()

局部地图跟踪用于对位姿进一步优化

(1)更新局部关键帧和局部地图点:局部关键帧mvpLocalKeyFrames包含一些列的共视高级证,局部地图点mvpLocalMapPoints是由是所有局部关键帧的地图点。

(2)在局部地图中查找与当前帧匹配的MapPoints:其实也就是对局部地图点进行跟踪,得到更多的匹配关系

(3)更新局部所有MapPoints后对位姿再次优化:前面新增了更多的匹配关系,BA优化得到更准确的位姿

(4)统计跟踪局部地图后匹配数目:更新当前帧的MapPoints被观测程度,并统计跟踪局部地图的效果

(5)决定是否跟踪成功:通过判断匹配点是数目看是否成功跟踪

Step4:跟踪成功

如果跟踪成功,更新速度、位姿、地图点等,并插入新关键帧

Step5:跟踪失败

如果跟踪失败,只能重新Reset

Step6:记录位姿信息

用于最后保存所有的轨迹

4 初始化并运行局部建图线程LocalMapping

void LocalMapping::Run()
{
    // 标记状态,表示当前run函数正在运行,尚未结束
    mbFinished = false;
    // 主循环
    while(1)
    {
        ...
    }
}
  

Step1:局部建图线程时设置不接受关键帧

Step2:处理列表中的关键帧

包括计算BoW、更新观测、描述子、共视图,插入到地图等

(1)从缓冲队列中取出一帧关键帧

(2)计算该关键帧特征点的词袋向量

(3)处理关键帧中有效的地图点信息:更新normal,描述子等信息,对当前处理的这个关键帧中的所有的地图点展开遍历

(4)更新关键帧间的连接关系

(5)将该关键帧插入到地图中

Step3:根据地图点的观测情况剔除质量不好的地图点

(1)根据相机类型设置不同的观测阈值

(2)遍历检查新添加的地图点:已经是坏点的地图点仅从队列中删除,跟踪到该地图点的帧数相比预计可观测到该地图点的帧数的比例小于25%,从地图中删除,从该点建立开始,到现在已经过了不小于2个关键帧,从建立该点开始,已经过了3个关键帧而没有被剔除,则认为是质量高的点

Step4:当前关键帧与相邻关键帧通过三角化产生新的地图点

增加新的地图点使得跟踪更稳

(1)在当前关键帧的共视关键帧中找到共视程度最高的nn帧相邻关键帧:nn表示搜索最佳共视关键帧的数目,不同传感器设定的值不同。

(2)遍历相邻关键帧实现三角化:搜索匹配并用极线约束剔除误匹配,最终实现三角化

(3)判断相机运动的基线是不是足够长:因为太短的基线下能够恢复的地图点不稳定,基线太短恢复3D点不准,那么跳过当前邻接的关键帧,不生成3D点。

(4)根据两个关键帧的位姿计算它们之间的基础矩阵

(5)生成新的匹配点对:通过词袋对两关键帧的未匹配的特征点快速匹配,用极线约束抑制离群点,生成新的匹配点对

(6)对每对匹配通过三角化生成3D点:进行一系列的检测才将新产生的点放入检测队列中

Step5:检查并融合当前关键帧与相邻关键帧帧(两级相邻)中重复的地图点

(1)获得当前关键帧在共视图中权重排名前nn的邻接关键帧

(2)存储一级相邻关键帧及其二级相邻关键帧

(3)将当前帧的地图点分别投影到两级相邻关键帧进行地图点融合:寻找匹配点对应的地图点进行融合,称为正向投影融合

(4)将两级相邻关键帧地图点分别投影到当前关键帧进行地图点融合:寻找匹配点对应的地图点进行融合,称为反向投影融合,和第(3)步相反

(5)更新当前帧地图点的描述子、深度、平均观测方向等属性

(6)更新当前帧与其它帧的共视连接关系

Step6:进行局部地图的BA并剔除当前帧相邻的关键帧中冗余的关键帧

(1)根据共视图提取当前关键帧的所有共视关键帧

(2)判断共视关键帧的地图点是否为冗余地图点:遍历该共视关键帧的所有地图点,其中能被其它至少3个关键帧观测到的地图点为冗余地图点

(3)判断共视关键帧是否为冗余关键帧:如果该关键帧90%以上的有效地图点被判断为冗余的,则认为该关键帧是冗余的,需要删除该关键帧

Step7:将当前帧加入到闭环检测队列中

5 初始化并运行回环检测线程LoopClosing

// 回环线程主函数
void LoopClosing::Run()
{
    mbFinished =false;

    // 线程主循环
    while(1)
    {
        // Check if there are keyframes in the queue
        // Loopclosing中的关键帧是LocalMapping发送过来的,LocalMapping是Tracking中发过来的
        // 在LocalMapping中通过 InsertKeyFrame 将关键帧插入闭环检测队列mlpLoopKeyFrameQueue
        // Step 1 查看闭环检测队列mlpLoopKeyFrameQueue中有没有关键帧进来
        if(CheckNewKeyFrames())
        {
            if(DetectLoop())
            {
               if(ComputeSim3())
               {
                   CorrectLoop();
               }
            }
        }
        // 查看是否有外部线程请求复位当前线程
        ResetIfRequested();

        // 查看外部线程是否有终止当前线程的请求,如果有的话就跳出这个线程的主函数的主循环
        if(CheckFinish())
            break;

        //usleep(5000);
		std::this_thread::sleep_for(std::chrono::milliseconds(5));

	}
    // 运行到这里说明有外部线程请求终止当前线程,在这个函数中执行终止当前线程的一些操作
    SetFinish();
}

Step1:查询闭环检测队列是否有新关键帧

Step2:闭环检测

从队列中取出一个关键帧作为当前检测闭环关键帧:从队列头开始取,也就是先取早进来的关键帧

(1)判断是否达到闭环检测的条件:如果距离上次闭环没多久(小于10帧),或者map中关键帧总共还没有10帧,则不进行闭环检测

(2)计算当前关键帧与每个共视关键的最低相似度得分minScore:遍历当前回环关键帧所有连接(>15个共视地图点)关键帧,计算当前关键帧与每个共视关键的bow相似度得分,并得到最低得分minScore

(3)在所有关键帧中找出闭环候选帧:和当前关键帧具有回环关系的关键帧不应该低于最低的相似度minScore,如果没有闭环候选帧,返回false

(4)在候选帧中检测具有连续性的候选帧:检测具有连续性的候选帧较为复杂, 遍历得到的初级的候选关键帧

(5)当前闭环检测的关键帧添加到关键帧数据库中

Step3:计算Sim3()变换ComputeSim3()

(1)遍历闭环候选帧集并构造一个Sim3Solver:筛选出与当前帧的匹配特征点数大于20的候选帧集合,并为每一个候选帧构造一个Sim3Solver

(2)对每一个候选帧进行 Sim3Solver 迭代匹配:直到有一个候选帧匹配成功,或者全部失败

(3)取出闭环匹配上关键帧的相连关键帧的地图点:得到它们的地图点放入 mvpLoopMapPoints

(4)将取出的地图点投影到当前关键帧进行投影匹配:根据投影查找更多的匹配(成功的闭环匹配需要满足足够多的匹配特征点数)

(5)统计当前帧与闭环关键帧的匹配地图点数目:判断当前帧与检测出的所有闭环关键帧是否有足够多的地图点匹配,超过设定值个说明成功闭环,否则失败

Step4:闭环矫正

(1)结束局部地图线程和全局BA:如果有全局BA在运行,终止掉,迎接新的全局BA,为闭环矫正做准备

(2)根据共视关系更新当前帧与其它关键帧之间的连接:因为之前闭环检测、计算Sim3中改变了该关键帧的地图点,所以需要更新

(3)得到Sim3优化后与当前帧相连的关键帧的位姿和MapPoints:通过位姿传播,得到Sim3优化后,与当前帧相连的关键帧的位姿,以及它们的MapPoints

(4)检查当前帧的和闭环匹配帧的MapPoints是否存在冲突:检查当前帧的MapPoints与闭环匹配帧的MapPoints是否存在冲突,对冲突的MapPoints进行替换或填补

(5)进行MapPoints检查与替换:通过将闭环时相连关键帧的mvpLoopMapPoints投影到这些关键帧中,进行MapPoints检查与替换

(6)更新当前关键帧之间的共视相连关系:得到因闭环时MapPoints融合而新得到的连接关系

(7)进行EssentialGraph优化:LoopConnections是形成闭环后新生成的连接关系

(8)添加当前帧与闭环匹配帧之间的边

Step5:全局BA

(1)执行全局BA优化所有的关键帧位姿和地图中地图点

(2)遍历并更新全局地图中的所有spanning tree中的关键帧

(3)遍历每一个地图点并用更新的关键帧位姿来更新地图点位置

  

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

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

相关文章

【EKF】卡尔曼滤波的二维应用实例

前言 在上期&#xff0c;使用一个简单的一维应用实例来加深了卡尔曼滤波的印象后&#xff0c;使用一个二维的例子来看一下卡尔曼的效果。使用一个自由落体的例子来说明&#xff0c;假设一个物体在重力作用下&#xff0c;速度由0开始做自由落体运动&#xff0c;有观测装置对该物…

自动化部署编译部署【.net core】

自动化部署编译部署【.net core】 github 自动化编译部署 .NET 程序&#xff0c;程序有两个服务&#xff0c;一个是api&#xff0c;一个是admin. 需要部署到两台机器上(测试和正式)&#xff0c;所以采用两个Action来处理 项目目录结构 root ├── Config │ ├── deploy …

【Linux】进程间通信 —— 共享内存

文章目录 &#x1f4d5; 共享内存的原理&#x1f4d5; 代码实现 & 深入理解共享内存shmget() 函数shmctl() 、shmdt()、shmat()特点 &#x1f4d5; 源代码comm.hppserver.ccclient.cc &#x1f4d5; 共享内存的原理 我们知道&#xff0c;如果想实现进程间通信&#xff0c;…

Linux Shell 实现一键部署subversion

subversion SVN是subversion的缩写&#xff0c;是一个开放源代码的版本控制系统&#xff0c;通过采用分支管理系统的高效管理&#xff0c;简而言之就是用于多个人共同开发同一个项目&#xff0c;实现共享资源&#xff0c;实现最终集中式的管理。 TortoiseSVN TortoiseSVN 是…

C语言函数大全-- t 开头的函数

C语言函数大全 本篇介绍C语言函数大全-- t 开头的函数 1. tan&#xff0c;tanf&#xff0c;tanl 1.1 函数说明 函数声明函数功能double tan(double x)计算 以弧度 x 为单位的角度的正切值&#xff08;double&#xff09;float tanf(float x)计算 以弧度 x 为单位的角度的正…

Spring Boot项目创建和使用

一、Spring Boot简介 1.概念 Spring Boot 就是 Spring 框架的脚⼿架&#xff0c;它就是为了快速开发 Spring 框架⽽诞⽣的。 2.优点 有快速集成框架&#xff0c;可以快速添加外部jar包内置web框架&#xff0c;可以直接运行可以快速部署&#xff0c;不依赖任何外部的web容器…

【牛客刷题专栏】0x26:JZ25 合并两个排序的链表(C语言编程题)

前言 个人推荐在牛客网刷题(点击可以跳转)&#xff0c;它登陆后会保存刷题记录进度&#xff0c;重新登录时写过的题目代码不会丢失。个人刷题练习系列专栏&#xff1a;个人CSDN牛客刷题专栏。 题目来自&#xff1a;牛客/题库 / 在线编程 / 剑指offer&#xff1a; 目录 前言问…

MATLAB 之 数值数据,矩阵的表示和变量及其操作

文章目录 一、数值数据1. 数值数据类型的分类1.1 整型1.2 浮点型1.3 复型 2. 数据的输出格式 二、矩阵的表示1. 矩阵的建立1.1 直接输入法建立矩阵1.2 已建好的矩阵建立更大的矩阵 2. 冒号表达式3. 矩阵元素的引用3.1 矩阵元素的引用方式3.2 利用冒号表达式获得子矩阵3.3 利用空…

Spring执行流程Bean生命周期

Spring执行流程 说明&#xff1a;这里只是说的大概流程&#xff0c;不是严格按照源码上一步一步说的。 简单来说&#xff0c;分为下边四个步骤&#xff1a; 启动Spring容器实例化Bean&#xff08;分配内存空间&#xff09;将Bean注册到Spring当中&#xff08;存操作&#xf…

【常用 Linux 命令的基本使用】总结篇(附必要的 Ubuntu 截图)

本文目录 1. 常用 Linux 命令的基本使用1.1 学习 Linux 终端命令的原因1.2 常用 Linux 命令的基本使用1.3 自动补全 2. Linux 终端命令格式2.1 终端命令格式2.2 查阅命令帮助信息&#xff08;了解&#xff09; 3. 文件和目录常用命令3.1 查看目录内容3.1.1 终端实用技巧3.1.2 l…

shell数组

目录 一&#xff1a;数组定义方法 1、方法一 ​ 2、方法二 ​3、方法三 ​4、方法四 5、判断数组是否完整 &#xff08;1&#xff09;方法一 &#xff08;2&#xff09;方法二&#xff1a;通过脚本 二&#xff1a;获取数组值 1、获取数组长度 2、获取数组数据列表 3、…

本地部署 Stable Diffusion web UI

本地部署 ChatGLM-6B 0. 什么是 Stable Diffusion1. 什么是 Stable Diffusion web UI2. Github 地址3. 安装 Miniconda34. 创建虚拟环境5. 安装 Stable Diffusion web UI6. 启动 Stable Diffusion web UI7. 访问 Stable Diffusion web UI8. 其他 0. 什么是 Stable Diffusion S…

UE4 面试题整理

1、new与malloc的区别 new&#xff1a; new首先会去调用operator new函数&#xff0c;申请足够的内存&#xff08;大多数底层用malloc实现&#xff09;&#xff0c;然后调用类型的构造函数来初始化变量&#xff0c;最后返回自定义类型的指针&#xff0c;delete先调用析构函数&…

IDEA配置使用Git

使用IDEA2018.2.8版本下操作 文章目录 一、安装Git1.GitHub与Git的关系2.Git的安装 二、在IDEA中配置Git三、创建远程仓库和本地仓库1.在gitee上创建远程仓库2.在IDEA上创建本地仓库 四、分支操作1.创建分支2.删除分支3.比较分支4.合并分支 一、安装Git 1.GitHub与Git的关系 …

位运算(算法)

目录 一、位运算的知识点二、位运算的应用1. x & 1一个数补码表示中的第k位数字求补码中含有1的个数偶数位与奇数位分开输出 2. x & (-x)返回x的最后一位1及之后的数字判断一个数是否是2的n次方求补码中含有1的个数 3. x & (x - 1)求补码中含有1的个数&#xff08;…

Transformer模型各模块详解及代码实现

Transformer 前言1、Transformer模型整体架构2、Embeeding2.1 词向量2.1.1 独热编码2.1.2 Word Embedding2.1.3 总结 2.2 代码实现 3、Positional Encoding3.1 位置编码简介3.2 代码讲解 4、Multi-Head Attention5、Layer Norm6、Positionwise Feed Forward7、Encoder and Deco…

Qt Creator 的使用技巧

1、Qt Creator 的快捷键 在 Qt Creator 里&#xff0c;假若自己不知道某些功能按钮的快捷键是什么&#xff0c;可以将鼠标移至该按钮 上面就可以知道它的快捷键了。如下图&#xff0c;想知道运行的快捷键是什么&#xff0c;那么我们将鼠标移至 Qt Creator 的左下角的运行…

日常开发中代码技巧(个人使用)

日常开发中代码技巧 String&#xff0c;StringBuffer判断非空 String判断非空 参考&#xff1a;https://blog.csdn.net/Echo_width/article/details/79653704 首先&#xff0c;区分空串和null串 1 空串""是长度为0的字符串&#xff0c;它有自己的串长度&#xff08;…

Python每日一练:小艺读书醉酒的狱卒非降序数组(详解快排)

文章目录 前言一、小艺读书二、醉酒的狱卒三、非降序数组总结 前言 今天这个非降序数组&#xff0c;阅读解理小学水平&#xff0c;说起来都是泪啊。我折腾了一天都没搞定&#xff0c;从冒泡写到快速排序。换了几种都还不行&#xff0c;我又给快排加上插入排序。结果还是不能全…

MySQL--复合查询--0422

注&#xff1a;为了方便查看 mysql语句会有突然的换行&#xff0c;书写时请勿模仿。 目录 1.单表查询回顾 显示工资最高的员工的名字和工作岗位 显示工资高于平均工资的员工信息 2.多表查询 比如需要查询雇员名字和所在部门编号及部门名字。 显示部门号为10的部门名&…