orbslam2代码解读(2):tracking跟踪线程

书接上回,mpTracker->GrabImageMonocular(im,timestamp)函数处理过程:

  1. 如果图像是彩色图,就转成灰度图
  2. 如果当前帧是初始化的帧,那么在构建Frame的时候,提取orb特征点数量为正常的两倍(目的就是能够在初始化的时候有更多匹配点对),如果是普通帧,就正常构建Frame。
  3. 接着就是调用tracking线程中的Track()函数。
  4. 返回当前图像帧的位姿估计结果。

上一篇文章已经讲完了构造Frame并且进行图像数据处理的过程,这篇主要解读tracking跟踪线程。

MonocularInitialization()单目初始化

step1

记录第一帧图像的关键点数量,只有关键点数量大于100才认为是有效的初始帧。

step2

记录第二帧图像的关键点数量,也是第二帧特征点数量大于100,就进行初始化。简而言之,要有连续两帧图像的特征点数量都大于100,才进行后续初始化操作

step3

构建一个ORBmatcher对象,很关键的一个对象,主要是用来搜索两帧图像之间的特征点匹配关系。

  ORBmatcher matcher(
      0.9,        //最佳的和次佳特征点评分的比值阈值,这里是比较宽松的,跟踪时一般是0.7
      true);      //检查特征点的方向

然后就是通过matcher.SearchForInitialization函数找到两帧图像之间的特征点匹配关系
这个函数的步骤如下:

  1. 构建旋转直方图。就是通过大多数特征点的角度一致性来过滤外点。
  2. 遍历第一帧的所有特征点,然后根据半径窗口搜索在第二帧的所有候选匹配特征点,过程如下图所示:(图中红色的圆圈就是搜索半径)
    在这里插入图片描述
  3. 接着遍历步骤2中第二帧的所有候选匹配特征点,计算它们的描述子距离(因为是二进制存储,所以用的是汉明距离,即不同位数的个数),找到最优的和次优的。
  4. 对最优和次优的结果进行检查,当最优的距离小于阈值且最优距离小于次优距离的百分之90(比较宽松的阈值),这个是初始化ORBmatcher对象对象的参数。都通过检查后记录最佳的特征点匹配关系。
  5. 然后对这个特征点匹配的结果计算两个特征之间的角度差(这个角度是通过前面灰度质心法算出来的),加入到旋转直方图中。
  6. 结束遍历第一帧的所有特征点的大循环。筛选旋转直方图中不符合大多数旋转方向的特征点匹配关系(旋转直方图过滤)。

step4

如果第一帧和第二帧的匹配点对太少(代码中阈值是匹配点对小于100),那么就需要重新选初始化的图像帧

step5

通过本质矩阵F或者通过单应矩阵H来得到两帧图像的相对运动,得到相对运动后将平移t进行归一化(即将t的模取为1),通过这个R和t的关系,进行三角化恢复出三维地图点(这个时候单目slam系统的尺度就固定了,后续的slam尺度都是以初始化时侯的尺度为基准,尺度漂移也是在这个初始化尺度上去漂移)。

主要是通过mpInitializer->Initialize函数实现,步骤如下:

  1. 初始化一个用于RANSAC的二维数组,里面存的是有匹配关系的特征点索引。为了方便理解画了个简易表格去说明,最后就是在这个二维数组中用随机数去填充,类似随机选取8个点的过程。
    在这里插入图片描述

  2. 为了并行计算F矩阵和H矩阵,代码中是分别开了两个线程去计算F矩阵和H矩阵。(F矩阵和H矩阵的秩和性质后续开独立的文章去说明)
    计算H矩阵的过程如下步骤:
    2.1.1 将当前帧和参考帧中的特征点坐标进行归一化,归一化的目的可以参考SLAM入门之视觉里程计(4):基础矩阵的估计,总的来说就是可以减少噪声的干扰,大大提升8点法的精度。(PS其实对这里的归一化没有深刻的认识,所以后续还需要学习这部分
    2.1.2 根据设定的RANSAC迭代次数(200),用对应的8个特征点计算H矩阵,其实计算H矩阵也是如视觉slam14讲上的类似,根据8对点构造出一个线性方程,然后就是用SVD分解来求解这个超定的齐次线性方程,求解的推导可以参考这篇文章:SVD在求解超定齐次线性方程组Ax=0中的应用。最优解就是 V T V^T VT的第9个奇异向量。
    在这里插入图片描述
    2.1.3 计算完H矩阵就利用重投影误差为当次RANSAC的结果评分,评分的规则其实就是卡方分布,因为重投影的形式与卡方分布检验的形式很像。得分的代码如下:

        // Step 2.3 用阈值标记离群点,内点的话累加得分
        if(chiSquare1>th)
            bIn = false;    
        else
            // 误差越大,得分越低
            score += th - chiSquare1;
    

    th就是卡方分布的阈值。重投影误差有双向投影(第一帧图像往第二帧图像投影得到一个分数,第二帧图像往第一帧图像投影得到又一个分数),分数都加到score中,最终就得到这8个点构成H矩阵的得分(得分越高,误差越小)。

    2.1.4 完成RANSAC迭代次数(200)后,选择得分最高的一组点计算出的H矩阵作为结果输出。

    计算F矩阵的过程如下步骤:
    2.2.1 一样需要将特征点归一化,跟上面计算H矩阵的一样。
    2.2.2 根据设定的RANSAC迭代次数(200),用对应的8个特征点计算F矩阵。只是构建的线性方程有区别。但是求解方式都是一样的都是基于SVD分解来求解一个最优解。
    在这里插入图片描述
    2.2.3 计算完F矩阵就利用点到直线距离(与重投影误差差不多)为当次RANSAC的结果评分,评分的规则其实就是卡方分布。
    2.2.4 完成RANSAC迭代次数(200)后,选择得分最高的一组点计算出的F矩阵作为结果输出。

  3. 计算完H矩阵和F矩阵,根据得分情况,来选择H矩阵来恢复R,t和三角化的点。程序中更偏向用H矩阵来恢复。

通过H矩阵恢复R,t和三角化的点

程序参考的是Motion and structure from motion in a piecewise plannar environment这篇文章。

  1. 首先将H矩阵进行奇异值分解,得到H矩阵分解出来的8组解
  2. 对8组解进行验证,选择在相机前方最多3D点的解作为最优解。
  3. 最终最优解需要满足:
    (1)最优解的3D点个数明显大于次优解的3D点
    (2)视差角大于规定的阈值
    (3)最优解的3D点个数大于规定的最小的被三角化的点数量
    (4)最优解的3D点个数要足够多,达到总数的90%以上

通过F矩阵恢复R,t和三角化的点

过程序上面差不多,只是F矩阵恢复出来的只有四组解,所以只需要对4组解进行验证。

step6

初始化成功了,就把初始化的参考帧设置为第一个关键帧,第二帧设置为第二个关键帧。这两个关键帧都分别计算Bow(词袋后续讲解)。最后将三角化得到的三维点加入到全局地图中,为了后续pnp求解两帧图像运动。

StereoInitialization()双目或者RGBD的初始化

这个过程很简单,因为在前面预处理的时候都得到了当前图像的3D点了,只要特征点的数量大于500个,直接就以当前帧作为原点,直接把特征点对应的3D点加入到全局地图中即可。

TrackReferenceKeyFrame()。跟踪参考关键帧

这个函数的使用时机:

  1. 当运动模型为空,即刚刚完成初始化开始时的几帧,并没办法得到运动模型,所以只能选最近的参考关键帧来跟踪。
  2. 当刚刚完成重定位,也是需要依靠参考关键帧来跟踪。
    在这里插入图片描述

step1

将当前帧的描述子转化为BoW向量。这里详细展开说一下词袋模型。

视觉词袋模型

本质上词袋模型(bag of words),指的就是用“图像上有哪几种特征”来描述一幅图像。在图像上words代表着已经提取了的特征点,然后把这些words组成一个bag就可以通过这个词袋去搜索相似的图像从而构成回环。
在这里插入图片描述
词袋模型的使用分为两个过程:

  1. 我们通过某种方式得到了一本字典(orbslam2通过离线训练的方式,用了很多图像,都提取了ORB特征点,将描述子通过k-means进行聚类,根据设定的树分支数和深度,从叶子节点开始聚类,一直到根节点,最后得到一个非常大的vocabulary tree)。
  2. 在线图像生成BoW向量。当我们在线跑SLAM的时候,就是根据当前帧的图像中提取的ORB特征点,遍历每个ORB特征点,从离线创建好的vocabulary tree中开始找自己的位置,从根节点开始,用该描述子和每个节点的描述子计算汉明距离,选择汉明距离最小的作为自己所在的节点,一直遍历到叶子节点。
    在这里插入图片描述
理解词袋向量BowVector

它内部实际存储的是这个std::map<WordId, WordValue>,其中 WordId 和 WordValue 表示Word在所有叶子中距离最近的叶子的id 和权重(后面解释)。同一个Word id 的权重是累加更新的。

理解特征向量FeatureVector

内部实际是个std::map<NodeId, std::vector<unsigned int>>

  1. 其中NodeId 并不是该叶子节点直接的父节点id,而是距离叶子节点深度为level up对应的node 的id,对应上面 vocabulary tree 图示里的 Word’s node id。为什么不直接设置为父节点?因为后面搜索该Word 的匹配点的时候是在和它具有同样node id下面所有子节点中的Word 进行匹配,搜索区域见图示中的 Word’s search region。所以搜索范围大小是根据level up来确定的,level up 值越大,搜索范围越广,速度越慢;level up 值越小,搜索范围越小,速度越快,但能够匹配的特征就越少。
  2. 另外 std::vector 中实际存的是NodeId 下所有特征点在图像中的索引。FeatureVector主要用于不同图像特征点快速匹配,加速几何关系验证,比如ORBmatcher::SearchByBoW 中就是通过NodeId 来进行快速匹配。
词袋模型相似度计算

这个部分我主要参考视觉slam14将来描述。正如前面介绍的词袋向量和特征向量,我们可以清楚特征向量主要是用于两个图像之间快速找到特征点匹配关系的,而相似度计算是通过词袋向量。
因为不一定每个单词重要性都是一样的(很多常出现的单词往往不是很重要),为了对单词的重要性或区分性加以评估,一般给单词不同的权值来得到更好的效果。
TF的意思是某单词在一幅图像上经常出现,它的区分度就高,而IDF的思想,是某个单词在字典中出现的频率越低,分类图像时,区分度越高。
IDF在建立字典的时候就计算:统计某个叶子节点 w i w_i wi中特征数量相对于所有特征数量的比例,作为IDF的值。假设所有特征数量为n, w i w_i wi数量为 n i n_i ni,那么这个单词的IDF就是:
I D F i = l o g ( n / n i ) {IDF}_i=log(n/n_i) IDFi=log(n/ni)
TF就是指某个特征出现在一张图像中出现频率。假设图像A中单词 w i w_i wi出现了 n i n_i ni次,而一共出现的单词次数是n,那么TF的值为:
T F i = n i / n TF_i = n_i/n TFi=ni/n
所以最终叶子节点 w i w_i wi的权重就是 I D F i {IDF}_i IDFi T F i TF_i TFi的积。
η i = T F i × I D F i \eta_i=TF_i × {IDF}_i ηi=TFi×IDFi
所以这时候每个叶子节点都会有一个权重,如果这时候给出两个图像A、B,它们的词袋向量可以表示为:
A = { ( w 1 , η 1 ) , ( w 2 , η 2 ) , … , ( w N , η N ) } =  def  v A A=\left\{\left(w_1, \eta_1\right),\left(w_2, \eta_2\right), \ldots,\left(w_N, \eta_N\right)\right\} \stackrel{\text { def }}{=} \boldsymbol{v}_A A={(w1,η1),(w2,η2),,(wN,ηN)}= def vA
B = { ( w 1 , η 1 ) , ( w 2 , η 2 ) , … , ( w N , η N ) } =  def  v B B=\left\{\left(w_1, \eta_1\right),\left(w_2, \eta_2\right), \ldots,\left(w_N, \eta_N\right)\right\} \stackrel{\text { def }}{=} \boldsymbol{v}_B B={(w1,η1),(w2,η2),,(wN,ηN)}= def vB
slam14讲中,给出的一种计算得分的公式:
s ( v A − v B ) = 2 ∑ i = 1 N ∣ v A i ∣ + ∣ v B i ∣ − ∣ v A i − v B i ∣ s\left(\boldsymbol{v}_A-\boldsymbol{v}_B\right)=2 \sum_{i=1}^N\left|\boldsymbol{v}_{A i}\right|+\left|\boldsymbol{v}_{B i}\right|-\left|\boldsymbol{v}_{A i}-\boldsymbol{v}_{B i}\right| s(vAvB)=2i=1NvAi+vBivAivBi
在orbslam的代码中,当两个图像之间某个 WordId 相同时,那么就将它们的权重进行运算,在orbslam中实现了很多的得分形式,有L1,L2和卡方得分等等。反正都是得分越高,那么这两帧图像的相似度越高,越容易构建回环。

orbslam中的直接索引表和倒排索引表

直接索引表是以图像为对象,存储图像中的特征点与字典中某个节点(node)的关联关系。直接索引表的作用就是给定了两个Frame对象或者KeyFrame对象,直接根据这个特征向量FeatureVector,找到属于同一个节点(node)下的特征点,然后遍历这些特征点计算描述子的相似得分即可快速找到匹配点对,而不是直接去遍历所有的特征点。

倒排索引表是以单词(word)为对象,存的是word的权重和这个word在哪个图像出现。倒排索引表的作用就是检测回环的时候使用,能够方便对比哪些图像有共同的word。
在这里插入图片描述

step2

通过词袋BoW加速当前帧与参考帧之间的特征点匹配。所以上面讲词袋模型的时候,特征向量FeatureVector就是用来快速得到匹配点对的。记录特征匹配成功后当前帧每个特征点对饮的MapPoint(来自参考帧),最后通过旋转直方图取出误匹配的点对。

step3

用上一帧位姿当作当前帧位姿的初始值。这样做的目的:因为根据参考关键帧跟踪的情况只在刚刚完成初始化或者重定位,没有匀速运动模型,所以就用上一帧位姿来当作这一帧预测位姿的初始值。

step4

通过3D-2D的重投影误差(pnp)来求解当前帧到参考帧之间的位姿。代码中就是通过g2o图优化的方式来实现(只优化位姿):

  1. 定义顶点g2o::VertexSE3Expmap,只有一个顶点就是当前帧的 T c w T_{cw} Tcw
  2. 定义边,如果是单目就是g2o::EdgeSE3ProjectXYZOnlyPose,如果是双目或者rgbd就是g2o::EdgeStereoSE3ProjectXYZOnlyPose,都是一元边。后续就是把边的各种属性:空间点位置,相机内参,观测,鲁棒核等信息
  3. 最后就是迭代优化,分四次迭代,每次迭代10次。期间需要根据卡方分布来去除外点,最终优化出当前帧的位姿。

step5

根据优化期间得到的外点标记,去除当前帧的地图点mCurrentFrame.mvpMapPoints中的外点。

TrackWithMotionModel()。匀速模型跟踪

这个函数是跟踪线程使用最频繁的函数,因为这个函数是用于两个普通帧之间跟踪,输出当前帧的位姿。

step1

更新上一帧的位姿,就是将上一帧在世界坐标系下的位姿计算出来:
l a s t T w o r l d = l a s t T r e f ∗ r e f T w o r l d ^{last}T_{world} = ^{last}T_{ref} * ^{ref}T_{world} lastTworld=lastTrefrefTworld
如果是双目或者rgbd的情况,会根据上一帧有深度的特征点创建临时的地图点,这些地图点只是用来跟踪,不会加入到全局地图中,所以跟踪成功后会删除。

step2

根据上一帧估计的速度,用恒速运动模型,预测当前帧的位姿。本质上就是假设相邻的位姿变化率相同
在这里插入图片描述

step3

用上一帧的地图点进行投影匹配。将上一帧的地图点根据预测的当前帧位姿,投影到当前帧的图像上,根据设定的搜索半径去搜索匹配点对。
通过matcher.SearchByProjection函数来找匹配关系
这个函数的步骤如下:

  1. 构建旋转直方图。就是通过大多数特征点的角度一致性来过滤外点。
  2. 遍历第一帧的所有特征点,然后根据半径窗口搜索在第二帧的所有候选匹配特征点,过程如下图所示:(图中红色的圆圈就是搜索半径)
    在这里插入图片描述
  3. 接着遍历步骤2中第二帧的所有候选匹配特征点,计算它们的描述子距离(因为是二进制存储,所以用的是汉明距离,即不同位数的个数),找到最优的和次优的。
  4. 对最优和次优的结果进行检查,当最优的距离小于阈值且最优距离小于次优距离的百分之90(比较宽松的阈值),这个是初始化ORBmatcher对象对象的参数。都通过检查后记录最佳的特征点匹配关系。
  5. 然后对这个特征点匹配的结果计算两个特征之间的角度差(这个角度是通过前面灰度质心法算出来的),加入到旋转直方图中。
  6. 结束遍历第一帧的所有特征点的大循环。筛选旋转直方图中不符合大多数旋转方向的特征点匹配关系(旋转直方图过滤)。

大部分的过程都跟单目初始化的时候很相似。都是通过半径搜索的方式去找匹配点对。如果匹配点太少,orbslam中的程序还会加大半径范围去搜索匹配点对。

step4

通过3D-2D的重投影误差(pnp)来求解当前帧到参考帧之间的位姿。代码中就是通过g2o图优化的方式来实现(只优化位姿)。过程是跟TrackReferenceKeyFrame()跟踪参考关键帧的step4是一样的,因为都是调用同一个函数且只优化当前帧位姿,这里不再赘述。

step5

根据g20优化过程中知道的外点关系,直接剔除mCurrentFrame.mvpMapPoints中的外点。

Relocalization()。重定位

这个函数主要是当跟踪跟丢了的时候,能够让当前帧重新与之前的地图重新连接上的方法。也可以在基于先建地图定位时,初始化定位信息的时候用。

step1

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

step2

用词袋模型找到与当前帧相似的候选关键帧。通过mpKeyFrameDB->DetectRelocalizationCandidates函数来找到与当前帧相似的候选关键帧,这个过程跟找回环关键帧是类似的。
这个函数的步骤如下:

  1. 根据前面介绍的orbslam2中的倒排索引表,就是找出和当前帧有公共word的所有关键帧。
  2. 统计上述关键帧中与当前帧具有单词最多的单词数,用来设定阈值1筛选(最小公共单词数为最大公共单词数的0.8倍)
  3. 再次遍历上述关键帧,并且挑出符合阈值的关键帧并且计算它们与当前帧的相似得分(通过词袋向量来计算)
  4. 单单计算当前帧和某一关键帧的相似性是不够的,这里将与关键帧共视程度最高的前十个关键帧归为一组,计算组的累计得分,并且用于设定阈值2来筛选关键帧。(最高得分的0.75倍)
  5. 再次遍历候选关键帧,找到最终符合要求的候选关键帧。
    请添加图片描述

step3

遍历从上面过程得到的候选关键帧,通过词袋模型进行快速搜索匹配点对,如果匹配点对数量足够就初始化EPnP求解器。如果EPnP没有超过迭代次数上限,那么说明能够得到一个比较好的初值,再继续用g2o优化当前帧的位姿,内点数如果也满足要求那么就证明重定位成功,把优化后位姿设置为当前帧位姿,并且将当前帧的地图点进行外点去除。
除了这个重定位成功的过程,程序还有很长的代码是面对一些不理想的情景,比如内点数不符合要求,那么就用投影的方式找新的匹配点对再去做位姿优化,尽最大努力完成初始化的过程,这个过程大致都说过,所以这里就不赘述了。

TrackLocalMap()。局部地图跟踪

局部地图跟踪是跟踪过程的最后一步了,本质上这个过程就是根据关键帧的共视关系,进一步提高当前帧的定位精度,其实跟激光SLAM的局部地图匹配是一样的,激光SLAM中帧与帧之间的匹配精度有限(因为一帧激光点云的不完整性),而帧与局部地图的匹配的精度是更高的(局部地图更加完整)。而orbslam2是通过关键帧的共视关系来构建局部地图

step1

更新局部关键帧mvpLocalKeyFrames和局部地图点mvpLocalMapPoints
更新局部关键帧的过程:

  1. 遍历当前帧的地图点,记录所有能观测到当前帧地图点的关键帧。
  2. 更新局部关键帧,添加的局部关键帧有三种类型:
    2.1能观测到当前帧地图点的关键帧,称为一级共视关键帧
    2.2一级共视关键帧的共视关键帧,成为二级共视关键帧
    2.3一级共视关键帧的子关键帧、父关键帧
  3. 更新当前帧的参考关键帧,选择与自己共视程度最高的关键帧作为参考关键帧
  4. 先清空局部地图点,然后根据局部关键帧中的地图点添加到局部地图点中。
    在这里插入图片描述

step2

筛选局部地图中新增的在视野范围内的地图点,投影到当前帧搜索匹配,得到更多的匹配关系。其实就是把局部地图点根据前面跟踪出来的位姿,转换到当前帧的相机坐标系上(并且会筛选这些局部地图点确保是在当前帧的视野内,比如z值为负的地图点肯定需要去除)。最后就是经典的matcher.SearchByProjection通过投影再用半径搜索去找匹配点对。

step3

通过3D-2D的重投影误差(pnp)来求解当前帧到参考帧之间的位姿。又是经典的g2o优化(仅优化当前帧位姿)。不赘述

step4

更新当前帧的地图点被观测程度。注意:地图点的被观测状态有两种:

  1. 一个IncreaseVisible,这个其实是指这个地图点在构建局部地图的时候,在当前帧视野范围内(通过Frame::isInFrustum()函数判断是否在视野内)。
  2. 另一个IncreaseFound,这个就是指这个地图点与当前帧特征点匹配上的意思。意义比IncreaseVisible更强,因为在视野范围内也有可能因为一些遮挡而导致没有特征点与之匹配。

并统计跟踪局部地图后匹配数目,如果匹配数目大于等于30就说明这个局部地图跟踪是成功的。

CreateNewKeyFrame()。产生新的关键帧

跟踪线程中的跟踪函数基本上都讲完了,实际SLAM的过程就是根据当前的跟踪的质量来选择不同的跟踪函数。最后再通过局部地图的跟踪,我们已经得到一个比较好的当前帧位姿了,所以我们就需要判断需不需要创建新的关键帧。
常见的关键帧生成方式:

  1. 根据运动距离或者转动角度的阈值来产生关键帧。
  2. 根据时间来产生关键帧。比如每10个雷达帧为一个关键帧
  3. orbslam2则是通过前面跟踪的状态来决定是否应该产生关键帧

orbslam2产生关键帧的判断条件

  1. 在参考关键帧中,统计观测次数大于3的地图点数量nRefMatches。单目的情况下,如果当前帧的内点数小于nRefMatches0.9倍就要加新的关键帧(因为单目的跟踪依赖关键帧之间三角化的地图点来通过pnp求解相机运动,所以插入关键帧的频率要高),如果是双目或rgbd的话,就是当前帧的内点数小于nRefMatches0.75倍就要加新的关键帧(因为在跟踪的过程中,双目或rgbd会加入一些距离可信的点来提高跟踪效果)。
  2. 除了根据跟踪状态来,orbslam2还会设置mMaxFramesmMinFrames参数来根据时间且结合跟踪效果决定是否产生关键帧。

创建新的关键帧

  1. 将当前帧构造成关键帧。
  2. 将当前关键帧设置为当前帧的参考关键帧
  3. 如果相机是双目或rgbd相机,则为当前帧生成新的地图点,单目不需要。(就是直接把当前帧深度可信的3d点分别加入到当前关键帧中和全局地图中)。
  4. 插入关键帧,就是把新创建的关键帧插入到mpLocalMapper这个线程的mlNewKeyFrames容器中。
  5. tracking线程中新的关键帧mpLastKeyFrame设置为新创建的关键帧。

总结

这篇文章主要就是整理了tracking跟踪线程中的主要函数:

  1. 单目初始化
  2. 双目或rgbd初始化
  3. 参考关键帧的跟踪
  4. 匀速模型跟踪
  5. 重定位(与回环有点像,主要是跟丢后重新找到在地图中的定位)
  6. 局部地图跟踪(提高定位信息在局部的准确性)
  7. 判断产生关键帧的时机

其实整个Tracking::Track()函数很大,逻辑也很多,但是本质上通过一系列逻辑将上述函数组合,而实现跟踪的效果。我们学习的目的应该是聚焦在函数实现上,过度关注这些逻辑有点浪费时间,因为我认为这些逻辑可能每个人的思考都不同,在需要的时候参考参考即可。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android JobService启动系统源码分析

以下就JobService的执行流程,系统层实现进行详解 入口点在JobScheduler.scheduler 系统层JobScheduler是个抽象类,它的实现类是JobScheduler mBinder,一看就知道这里面肯定是跨进程了。它的服务端在JobSchedulerService里面,具体 为什么请看系统服务器启动流程相关文章,…

【传知代码】上下位关系自动检测方法(论文复现)

前言&#xff1a;在信息爆炸的时代&#xff0c;我们每天都沉浸在海量的数据和信息中。随着互联网技术的飞速发展&#xff0c;如何从这些信息中准确、高效地提取出有用的知识&#xff0c;成为了当下研究的热点。其中&#xff0c;上下位关系&#xff08;也称为层级关系或种属关系…

康姿百德集团公司官网床垫价格透明,品质睡眠触手可及

选择康姿百德床垫&#xff0c;价格透明品质靠谱&#xff0c;让你拥有美梦连连 在当今社会&#xff0c;良好的睡眠质量被越来越多的人所重视。睡眠不仅关系到我们第二天的精力状态&#xff0c;更长远地影响着我们的身体健康。因此&#xff0c;选择一款合适的床垫对于获得优质睡…

Android Studio Jellyfish版本修改project使用特定jdk版本的步骤

android studio总是把这些东西改来改去让人十分恼火&#xff0c;IDE本身改来改去就让人无法上手就立即工作&#xff0c;很多时间浪费在IDE和gradle的配置和奇奇怪怪现象的斗智斗勇上&#xff0c;搞Android是真的有点浪费生命。一入此坑深不见底 jellyfish版安卓studio已经无法通…

Python模块导入,别out了,看看这些高级玩法!

目录 1、基础导入&#xff1a;import语句 &#x1f4da; 1.1 直接导入模块 1.2 导入模块别名 1.3 从模块导入特定属性 2、高级导入&#xff1a;from...import &#x1f9f0; 2.1 选择性导入模块成员 2.2 嵌套模块导入 2.3 避免命名冲突策略 3、动态导入&#xff1a;imp…

Mysql基础-多表查询

Mysql基础-多表查询 文章目录 Mysql基础-多表查询1 多表关系1.1 一对多1.2 多对多1.3 一对一 2 多表查询概述2.1 多表查询分类 3 内连接4 外连接5 自连接6 联合查询-union union all7 子查询7.1 标量子查询7.2 列子查询7.3 行子查询7.4 表子查询 1 多表关系 项目开发中&#xf…

Jenkins构建打包部署前端Vue项目至Nginx

一. 安装jenkins 基于DockerJenkins实现自动部署SpringBootMaven项目-CSDN博客 二. 安装NodeJs插件并配置 显示上面两行则表示安装成功, 然后回到首页, 点击’系统管理’->‘全局工具配置’: 配置node.js 三. 创建jenkins项目 1、创建项目 2、配置gitee 3、配置源码 4、…

网络学了点socket,写个聊天室,还得改进

目录 第一版: common 服务端: 客户端 第一版问题总结: 第二版 服务端: 客户端: 改进: Windows客户端 一些小问题 还可以进行的改进 这篇文章我就先不讲网络基础的东西了,我讲讲在我进行制作我这个拉跨聊天室中遇到的问题,并写了三版代码. 第一版: common #pragm…

MyBatis映射器:实现动态SQL语句

大家好&#xff0c;我是王有志&#xff0c;一个分享硬核 Java 技术的金融摸鱼侠&#xff0c;欢迎大家加入 Java 人自己的交流群“共同富裕的 Java 人”。 上一篇文章中&#xff0c;我们已经学习了如何在 MyBatis 的映射器中通过简单的 SQL 语句实现增删改查&#xff0c;今天我…

关闭windows11磁盘地址栏上的历史记录

关闭windows11的磁盘地址栏上的历史记录 windows11打开磁盘后访问某一个磁盘路径后会记录这个磁盘路径&#xff0c;而且有时候会卡住这个地址栏&#xff08;关都关不掉&#xff09;&#xff0c;非常麻烦。 如下图所示&#xff1a; 关闭地址栏历史记录 按下windows键打开开…

C++面试八股文:static和const的关键字有哪些用法?

100编程书屋_孔夫子旧书网 某日二师兄参加XXX科技公司的C工程师开发岗位第7面&#xff1a; 面试官&#xff1a;C中&#xff0c;static和const的关键字有哪些用法? 二师兄&#xff1a;satic关键字主要用在以下三个方面&#xff1a;1.用在全局作用域&#xff0c;修饰的变量或者…

Adobe Premiere Pro 2024下载安装(视频剪辑软件Pr2024)

百度网盘下载地址&#xff08;含PR教学课程&#xff08;PR从入门到精通108节课程&#xff09;&#xff09;https://pan.baidu.com/s/1WKYZENoMzTcKhbgMgbEPGQ?pwdSIMS 一、Pr简介 Pr全称Premiere&#xff0c;是Adobe公司开发的一款功能强大的视频剪辑软件&#xff0c;目前被…

Java实现物候相机和植被分析导出相对指数成果图

一、基础概念 植被分析是利用地理信息系统&#xff08;GIS&#xff09;、遥感技术、生态学、环境科学等多学科交叉手段&#xff0c;对植被的分布、类型、结构、组成、动态变化、生产力、生态功能进行量化评估的过程。植被分析对于生态保护、生物多样性研究、资源管理、环境监测…

Mysql基础 - 事务

Mysql基础 - 事务 文章目录 Mysql基础 - 事务1 事务简介2 事务操作2.1 控制事务一2.2 控制事务二 3 事务四大特性4 并发事务问题5 事务隔离级别 1 事务简介 事务是一组操作的集合&#xff0c;他是一个不可分割的工作单位&#xff0c;事务会把所有操作作为一个整体一起向系统提…

[chisel]马上要火的硬件语言,快来了解一下优缺点

Chisel是什么&#xff1f; Chisel的全称为Constructing Hardware In a Scala Embedded Language&#xff0c;是一个基于Scala的DSL&#xff08;Domain Specific Language&#xff0c;特定领域专用语言&#xff09;。2012年&#xff0c;加州大学伯克利分校&#xff08;UC Berkel…

【Rd-03E】使用CH340给Rd03_E雷达模块烧录固件

Rd03_E 指导手册 安信可新品雷达模组Rd-03搭配STM32制作简易人体感应雷达灯教程 http://t.csdnimg.cn/mqhkE 测距指导手册网址&#xff1a; https://docs.ai-thinker.com/_media/rd-03e%E7%B2%BE%E5%87%86%E6%B5%8B%E8%B7%9D%E7%94%A8%E6%88%B7%E6%89%8B%E5%86%8C%E4%B8%AD%…

02-JAVA面向对象编程

一、面向对象编程 1、面向过程编程思想&#xff08;Process Oritented Programming&#xff09; 将实现一个功能的一组指令组合在一起&#xff0c;成为一个函数。这个函数就能实现这一个功能&#xff0c;是对功能实现的一种抽象。通过这种抽象方式&#xff0c;将代码实现复用。…

软件游戏steam_api.dll丢失的解决方法,总结5种有效的方法

在玩电脑游戏时&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“游戏缺少steam_api.dll”。这个问题可能让很多玩家感到困惑和烦恼。那么&#xff0c;究竟是什么原因导致游戏缺少steam_api.dll呢&#xff1f;又该如何解决这个问题呢&#xff1f;本文将为大家…

pyqt opengl 小黑块

目录 OpenGLWidget_g初始化函数&#xff1a; 解决方法&#xff1a;把初始化函数的parent去掉 pyqt opengl 小黑块 原因&#xff1a; 创建OpenGLWidget_g的时候把main_window作为父类&#xff08;self&#xff09;传进去了&#xff0c; self.opengl_widget OpenGLWidget_g(…

SpringBoot+Vue网上超市系统(前后端分离)

技术栈 JavaSpringBootMavenMySQLMyBatisVueShiroElement-UI 系统角色对应功能 用户管理员 系统功能截图