VINS-Mono中的DBoW2关键代码注释

2023-05-16

VINS-Mono中的DBoW2关键代码注释


在阅读VINS-Mono源码时对DBoW2中代码顺手做的注释,怕以后会忘记,在这里记录一下,注释有不当之处,望各位大神看到后多多指点。理论参考高翔的《视觉SLAM十四讲》第12章的内容。这里找回环的方法是图像与数据库的比较,ORBSLAM中采用的方法是 图像与图像直接比较。
在pose_graph包里的pose_graph_node.cpp文件中,有这么一句代码,

posegraph.loadVocabulary(vocabulary_file);

这句代码包含的信息量巨大,其实现的功能就是将support_files文件夹下的brief_k10L6.bin文件构造为词袋模型的词典。这个词典的内容以树形结构存储,大意可以用下图表示,节点存储的权重和描述子是字典的核心内容。
在这里插入图片描述

转到pose_graph.cpp文件中,

void PoseGraph::loadVocabulary(std::string voc_path)
{
    //构造词典,核心代码在这里
    voc = new BriefVocabulary(voc_path);
    //利用构造的词典,初始化图像数据库,它在计算图像相似度时发挥重要作用
    db.setVocabulary(*voc, false, 0);
}
/// BRIEF Vocabulary
//BriefVocabulary类其实是这个类的另外一个名字
typedef DBoW2::TemplatedVocabulary<DBoW2::FBrief::TDescriptor, DBoW2::FBrief> 
  BriefVocabulary;

/// BRIEF Database
//BriefDatabase类其实是这个类的另外一个名字
typedef DBoW2::TemplatedDatabase<DBoW2::FBrief::TDescriptor, DBoW2::FBrief> 
  BriefDatabase;

上面的代码在DBoW文件夹下的DBoW2.h文件中,下面的代码在TemplatedVocabulary.h文件中,

template<class TDescriptor, class F>
TemplatedVocabulary<TDescriptor,F>::TemplatedVocabulary
  (const std::string &filename): m_scoring_object(NULL)
{
    //从brief_k10L6.bin文件中加载词典
    loadBin(filename);
}
//节点m_nodes包括叶子m_words,即叶子一定是节点,节点不一定是叶子。叶子就是单词
template<class TDescriptor, class F>
void TemplatedVocabulary<TDescriptor,F>::loadBin(const std::string &filename) { 

  m_words.clear();//单词(树形结构的叶子)
  m_nodes.clear();//树形结构的各层节点,包括叶子
  //printf("loop load bin\n");
  std::ifstream ifStream(filename);//得到文件流
  VINSLoop::Vocabulary voc;
  voc.deserialize(ifStream);//从文件流中得到voc,这里不懂具体怎么实现的
  ifStream.close();
  
  m_k = voc.k;//树形结构各节点的子节点数的最大值
  m_L = voc.L;//树形结构的深度,即层数
  m_scoring = (ScoringType)voc.scoringType;//评分的标准选哪一个
  m_weighting = (WeightingType)voc.weightingType;//权重的标准选哪一个
 
  createScoringObject(); //根据评分标准得到m_scoring_object
  //节点数重置
  m_nodes.resize(voc.nNodes + 1); // +1 to include root
  m_nodes[0].id = 0;//根节点id

  for(unsigned int i = 0; i < voc.nNodes; ++i)
  {
    NodeId nid = voc.nodes[i].nodeId;//节点id
    NodeId pid = voc.nodes[i].parentId;//节点的父id
    WordValue weight = voc.nodes[i].weight;//节点权重
    //构造节点m_nodes 
    m_nodes[nid].id = nid;
    m_nodes[nid].parent = pid;
    m_nodes[nid].weight = weight;
    m_nodes[pid].children.push_back(nid);
      
    // Sorry to break template here
    //节点描述子
    m_nodes[nid].descriptor = boost::dynamic_bitset<>(voc.nodes[i].descriptor, voc.nodes[i].descriptor + 4);
    //这段代码可忽略
    if (i < 5) {
      std::string test;
      boost::to_string(m_nodes[nid].descriptor, test);
      //cout << "descriptor[" << i << "] = " << test << endl;
    }
  }
  
  // words
  m_words.resize(voc.nWords);//叶子数重置

  for(unsigned int i = 0; i < voc.nWords; ++i)
  {
    NodeId wid = (int)voc.words[i].wordId;//叶子id
    NodeId nid = (int)voc.words[i].nodeId;//叶子作为节点身份的id
    //节点m_nodes的参数补充, 叶子m_words的构造
    m_nodes[nid].word_id = wid;
    m_words[wid] = &m_nodes[nid];
  }
}

词典构造工作完成。

下面的代码在TemplatedDatabase.h文件中

template<class TDescriptor, class F>
template<class T>
inline void TemplatedDatabase<TDescriptor, F>::setVocabulary
  (const T& voc, bool use_di, int di_levels)
{
  m_use_di = use_di;//false
  m_dilevels = di_levels;//0
  delete m_voc;
  m_voc = new T(voc);//词袋模型的词典
  clear();//一些变量的初始化
}
template<class TDescriptor, class F>
inline void TemplatedDatabase<TDescriptor, F>::clear()
{
  // resize vectors
  m_ifile.resize(0);//这个变量很重要,要特别注意
  m_ifile.resize(m_voc->size());
  m_dfile.resize(0);
  m_dBowfile.resize(0);
  m_nentries = 0;
}

返回pose_graph.cpp文件中,

//添加关键帧的描述子到图像数据库
db.add(keyframe->brief_descriptors);

下面代码在TemplatedDatabase.h文件中

template<class TDescriptor, class F>
EntryId TemplatedDatabase<TDescriptor, F>::add(
  const std::vector<TDescriptor> &features,
  BowVector *bowvec, FeatureVector *fvec)
{
  BowVector aux;
  BowVector& v = (bowvec ? *bowvec : aux);
  
  ......
  else//执行这里的代码
  {
    //这段代码的功能就是把描述子features转为BowVector(叶子id,叶子在此帧的权重)的形式
    m_voc->transform(features, v); // with features
    return add(v);
  }
}

转到TemplatedVocabulary.h文件中

template<class TDescriptor, class F>
void TemplatedVocabulary<TDescriptor,F>::transform(
  const std::vector<TDescriptor>& features, BowVector &v) const
{
  v.clear();//先清空
  
  if(empty())
  {
    return;
  }

  // normalize 
  LNorm norm;
  bool must = m_scoring_object->mustNormalize(norm);

  typename std::vector<TDescriptor>::const_iterator fit;

  if(m_weighting == TF || m_weighting == TF_IDF)
  {
    for(fit = features.begin(); fit < features.end(); ++fit)
    {
      WordId id;
      WordValue w; 
      // w is the idf value if TF_IDF, 1 if TF
      //在词典中得到,与此描述子最为匹配的叶子的id及权重
      transform(*fit, id, w);
      
      // not stopped
      // 累积该叶节点的idf权重,v(id).weight += w
	  // 最后v(id).weight实际上等于M*idf,M为插入该叶节点的特征描述的个数
      if(w > 0) v.addWeight(id, w);
    }
    //这段不执行
    if(!v.empty() && !must)
    {
      // unnecessary when normalizing
      const double nd = v.size();
      for(BowVector::iterator vit = v.begin(); vit != v.end(); vit++) 
        vit->second /= nd;
    }
    
  }
  else // IDF || BINARY
  {
    for(fit = features.begin(); fit < features.end(); ++fit)
    {
      WordId id;
      WordValue w;
      // w is idf if IDF, or 1 if BINARY
      
      transform(*fit, id, w);
      
      // not stopped
      if(w > 0) v.addIfNotExist(id, w);
      
    } // if add_features
  } // if m_weighting == ...
  
  if(must) v.normalize(norm);
}
template<class TDescriptor, class F>
void TemplatedVocabulary<TDescriptor,F>::transform(const TDescriptor &feature, 
  WordId &word_id, WordValue &weight, NodeId *nid, int levelsup) const
{ 
  // propagate the feature down the tree
  std::vector<NodeId> nodes;//表示的是某个节点的所有子节点
  typename std::vector<NodeId>::const_iterator nit;//节点迭代器

  // level at which the node must be stored in nid, if given
  const int nid_level = m_L - levelsup;//levelsup=0
  if(nid_level <= 0 && nid != NULL) *nid = 0; // root,nid =NULL

  NodeId final_id = 0; // root
  int current_level = 0;
 // 当前描述子feature逐层匹配,直到叶子层
  do
  {
    ++current_level;
    nodes = m_nodes[final_id].children;
    final_id = nodes[0];
    // 计算该描述子与本层节点描述子的距离,选取距离最小的节点,记录下它的id
    double best_d = F::distance(feature, m_nodes[final_id].descriptor);
    for(nit = nodes.begin() + 1; nit != nodes.end(); ++nit)
    {
      NodeId id = *nit;
      double d = F::distance(feature, m_nodes[id].descriptor);
      if(d < best_d)
      {
        best_d = d;
        final_id = id;
      }
    }
    
    if(nid != NULL && current_level == nid_level)
      *nid = final_id;
    
  } while( !m_nodes[final_id].isLeaf() );

  // turn node id into word id
  // 取出叶子对应的word id(所有叶子集合内的编号)和权重
  word_id = m_nodes[final_id].word_id;
  weight = m_nodes[final_id].weight;
}

回到TemplatedDatabase.h文件中,

template<class TDescriptor, class F>
EntryId TemplatedDatabase<TDescriptor, F>::add(const BowVector &v,
  const FeatureVector &fv)
{
  EntryId entry_id = m_nentries++;//数据库中图像帧id

  BowVector::const_iterator vit;
  std::vector<unsigned int>::const_iterator iit;

  if(m_use_di)//false
  {
    ......
  }
  
  // update inverted file
  for(vit = v.begin(); vit != v.end(); ++vit)
  {
    const WordId& word_id = vit->first;//叶子id
    const WordValue& word_weight = vit->second;//叶子在该图像中的权重
    //下面两句代码的意思: 叶子word_id在图像entry_id中的权重为word_weight
    IFRow& ifrow = m_ifile[word_id];//获取m_ifile[word_id]的地址
    ifrow.push_back(IFPair(entry_id, word_weight));//添加记录
  }
  
  return entry_id;
}

转到pose_graph.cpp文件中,

//在图像数据库中查找与当前帧相似度最高的4帧图像
db.query(keyframe->brief_descriptors, ret, 4, frame_index - 50);

转到TemplatedDatabase.h文件中,

template<class TDescriptor, class F>
void TemplatedDatabase<TDescriptor, F>::query(
  const std::vector<TDescriptor> &features,
  QueryResults &ret, int max_results, int max_id) const
{
  BowVector vec;
  //将当前帧的描述子转为BowVector向量
  m_voc->transform(features, vec);
  //利用当前帧的BowVector向量,在图像数据库中查找与当前帧相似度最高的几帧图像
  query(vec, ret, max_results, max_id);
}

template<class TDescriptor, class F>
void TemplatedDatabase<TDescriptor, F>::query(
  const BowVector &vec, 
  QueryResults &ret, int max_results, int max_id) const
{
  ret.resize(0);
  
  switch(m_voc->getScoringType())
  {
    case L1_NORM:
      queryL1(vec, ret, max_results, max_id);
      break; 
      ......
      break;
  }
}

queryL1()函数为例,

template<class TDescriptor, class F>
void TemplatedDatabase<TDescriptor, F>::queryL1(const BowVector &vec, 
  QueryResults &ret, int max_results, int max_id) const
{
  BowVector::const_iterator vit;
  typename IFRow::const_iterator rit;
  //pairs中存储着当前图像与数据库中图像的相似度value  
  std::map<EntryId, double> pairs;
  std::map<EntryId, double>::iterator pit;
  //遍历当前图像的所有单词
  for(vit = vec.begin(); vit != vec.end(); ++vit)
  {
    const WordId word_id = vit->first;//叶子(单词)id
    const WordValue& qvalue = vit->second;//单词word_id在当前图像中的权值
    // 注意单词word_id在数据库中各个图像的权值是不同的    
    const IFRow& row = m_ifile[word_id];
    
    // IFRows are sorted in ascending entry_id order
    // 计算当前图像与数据库中各个图像entry_id的相似度value
    for(rit = row.begin(); rit != row.end(); ++rit)
    {
      const EntryId entry_id = rit->entry_id;//图像id
      const WordValue& dvalue = rit->word_weight;//单词word_id在图像entry_id中的权值
      //if判断的第2和第3个条件是什么意思?
      if((int)entry_id < max_id || max_id == -1 || (int)entry_id == m_nentries - 1)
      {
        double value = fabs(qvalue - dvalue) - fabs(qvalue) - fabs(dvalue);
        
        pit = pairs.lower_bound(entry_id);
        if(pit != pairs.end() && !(pairs.key_comp()(entry_id, pit->first)))
        {
          pit->second += value;
        }
        else
        {
          pairs.insert(pit, 
            std::map<EntryId, double>::value_type(entry_id, value));
        }
      }
      
    } // for each inverted row
  } // for each query word
	
  // move to vector
  ret.reserve(pairs.size());
  for(pit = pairs.begin(); pit != pairs.end(); ++pit)
  {
    ret.push_back(Result(pit->first, pit->second));
  }
	
  // 升序排列,前面计算的value是负值,所以value值越小相似度越高
  std::sort(ret.begin(), ret.end());

  if(max_results > 0 && (int)ret.size() > max_results)
    ret.resize(max_results);//取最相似的max_results个结果
  QueryResults::iterator qit;
  for(qit = ret.begin(); qit != ret.end(); qit++) 
    qit->Score = -qit->Score/2.0;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

VINS-Mono中的DBoW2关键代码注释 的相关文章

  • matlab cody学习笔记 day23 判断输入的是否是向量

    好久没更新了 xff0c 今天刷一道 1 Problem 605 Whether the input is vector Given the input x return 1 if x is vector or else 0 我本来想的是获
  • 串口通信校验方式(even,odd,space,mark)

    无校验 xff08 no parity xff09 奇校验 xff08 odd parity xff09 xff1a 如果字符数据位中 34 1 34 的数目是偶数 xff0c 校验位为 34 1 34 xff0c 如果 34 1 34 的
  • Eigen介绍及简单使用

    Eigen是可以用来进行线性代数 矩阵 向量操作等运算的C 43 43 库 xff0c 它里面包含了很多算法 它的License是MPL2 它支持多平台 Eigen采用源码的方式提供给用户使用 xff0c 在使用时只需要包含Eigen的头文
  • APM最新固件(20181220)

    ardupilot Makefile MAKEFILE LISTWAF BINARY 61 modules waf waf lightWAF 61 python WAF BINARY WAF FLAGSEXPLICIT COMMANDS 6
  • Java考试复习

    java考试复习 1 判断题 单选题 填空题看网上测试 xff1b 注意 xff01 xff01 xff01 xff08 单选题答案里面的粗黑的有分号是代表同时满足 xff1b 填空题答案里面的粗黑的有分号是代表其中一个就满足 xff09
  • 计算机网络之第4章 网络层

    计算机网络 第4章 网络层 网络层概述 以下属于网络层范畴的是 IP地址在因特网使用的TCP IP协议栈中 xff0c 核心协议是 IP 网络层提供的两种服务 TCP IP参考模型的网络层提供的是 无连接不可靠的数据报服务 IPv4地址概述
  • 中标麒麟Linux服务器5.0(mips64el)安装QT开发环境

    中标麒麟服务器5 0 xff08 mips64el xff09 上QT开发需要用到can口 xff0c 原版自带的QT不包含相关模块 xff0c 故重新下载一个带有can模块的qt版本进行安装 该系统架构是mips64el的 xff0c 所
  • mavros永远连接失败

    之前在做无人机使用px4源码避障的实物实验 xff0c 已经有了无人机 xff0c 想按照惯例先在nvidia NX上位机上跑一下仿真实验测试一下 xff0c 结果mavros用了一万种方法 xff0c 就是连不上 xff0c 仿真跑不了
  • 成功解决mingw下载太慢的问题

    MinGW w64 for 32 and 64 bit Windows Browse Files at SourceForge net 1 在此页面下滑找到你要下载的文件 2 点击Problems Downloadings 3 选择一个合适
  • PyQt5学习笔记9_使用setStyle和setStyleSheet进行换肤

    通过QStyleFactory keys 可获取用于setStyle的有效值 xff0c 本例程中包含 Windows xff0c WindowsXP xff0c WindowsVista xff0c Fusion 四种风格 xff0c 此
  • mkdir 创建目录

    参数选项 参数说明 p 连续创建目录 mkdir data 创建目录data 或 cd mkdir data 或 cd mkdir data 注 xff1a 是将两条命令分隔开 mkdir p data b c 连续创建目录 data b
  • CC, TBD, EOD都是什么鬼?拯救一写英文邮件就发慌

    职场新人在工作中经常听到这样的对话 xff1a 给客户的邮件记得CC我 xff0c BCC给财务 xff0c 告诉客户合同签订时间还TBD But CC BCC TBD到底是什么鬼 xff1f 马上来恶补一下职场英文缩写 xff0c 拯救一
  • Apache Openmeetings安装介绍

    翻译自Apache OpenMeetings 更新时间 xff1a 2017 01 11 目录 目录Openmeetings安装端口NAT设置自定义硬件需求Debian链接更新日志VoIP提示和技巧 Openmeetings安装 从过往版本
  • Could not transfer artifact xxx from/to xxx解决方案

    在做Openmeetings二次开发的时候install时出现了如下错误 INFO Parent project loaded span class hljs keyword from span repository org apache
  • MavenInvocationException解决方案

    在编译Openmeetings的时候出现了这样的错误信息 xff1a MavenInvocationException Error configuring command line Reason Maven executable not f
  • 生成生命周期介绍

    翻译自http maven apache org guides introduction introduction to the lifecycle html 目录 目录生成生命周期基础 生成生命周期由阶段组成通用命令行调用一个生成阶段是由
  • Crypto++库在VS 2013中的使用 + 基于操作模式AES加密

    一 下载Crypto 43 43 Library Crypto 43 43 Library的官方网 xff1a http www cryptopp com 二 建立自己使用的Crypto 43 43 Library 由于从官方网下载的Cry
  • MATLAB工具箱路径缓存

    关于MATLAB工具箱路径缓存 出于性能考虑 xff0c MATLAB将跨会话缓存工具箱文件夹信息 缓存特性对您来说是透明的 但是 xff0c 如果MATLAB没有找到您的最新版本的MATLAB代码文件 xff0c 或者如果您收到有关工具箱
  • MySQL语法

    初识MySQL 为什么学习数据库 1 岗位技能需求 2 现在的世界 得数据者得天下 3 存储数据的方法 4 程序 网站中 大量数据如何长久保存 5 数据库是几乎软件体系中最核心的一个存在 什么是数据库 数据库 DataBase 简称DB 概
  • HBase Configuration过程

    HBase客户端API中 xff0c 我们看到对HBase的任何操作都需要首先创建HBaseConfiguration类的实例 为HBaseConfiguration类继承自Configuration类 xff0c 而Configurati

随机推荐

  • 中国版的 Github:gitee.com、coding.net

    https gitee com 码云 社区版 主要功能代码托管 xff1a 对个人开发者提供免费的云端 Git 仓库 xff0c 可创建最多 1000 个项目 xff0c 不限公有或私有 xff0c 支持SSH HTTP SVN xff1b
  • winScp 连接 FilEZillA报(由于目标计算机积极拒绝,无法连接)

    场景 xff1a 服务器一台 xff1b 本地台式机一台 xff0c 为了文件传输方便 xff0c 在服务器上使用FilEZillA搭建了FTP xff0c 在本地使用WinScp进行连接 问题 xff1a 首先FTP搭建没问题 xff0c
  • 关于 Raspberry Pi3 使用 Intel® RealSense™ D400 cameras的简单介绍

    Raspberry Pi Raspberry pi 可以称为个人微型电脑 xff0c 虽然它的性能无法与普通电脑相比 xff0c 但是它在很多方面都给我们带来了惊喜 xff0c 它的特点是便于携带 xff0c 功能基本和普通电脑一样 xff
  • 安装好后 实例启动出现问题

    错误如上正在排错中 File 34 usr lib python2 7 site packages nova conductor manager py 34 line 671 in build instances request spec
  • gazebo仿真之plugin系列一

    官网教程 xff1a http gazebosim org tutorials tut 61 plugins hello world amp cat 61 write plugin 本次内容涉及五个方面 xff1a plugin的基本介绍与
  • gazebo官网教程之快速开始

    英文教程 xff1a http gazebosim org tutorials tut 61 quick start amp cat 61 get started 一 运行gazebo 打开有默认环境的gazebo只需要三步 xff1a 1
  • 基于unity无人机3D仿真《一》

    基于unity无人机3D仿真 一 实现无人机的模型的制作 运动学关系 姿态角等 xff1b 实现无人机各种姿态运动 一 目前的效果 二 无人机模型 制作软件 xff1a maya 模型结构 xff1a 三 开发平台 unity2017 43
  • 比特、字节转换

    1bite xff08 比特 xff09 61 1字节 数字 xff1a 1字符 61 1字节 英文 xff1a 1字符 61 1字节 汉字 xff1a 1字符 61 2字节 在ASCII码中 xff0c 一个英文字母 xff08 不分大小
  • Unity无人机仿真github项目

    本人本科生有幸得到导师的指导 xff0c 对Unity这个平台学习已有一段时间 该平台在搭建自主仿真平台方面确实有很大优势 下面是在学习过程中收集到的一些多旋翼无人机仿真的github项目 xff0c 可供需要的快速学习 xff08 推荐先
  • matlab2020a中使用TrueTime工具

    环境 xff1a matlab版本 xff1a 2020a 参考文章 网络控制系统仿真 xff1a Truetime2 0工具箱安装 xff08 win10 43 matlab R2017b xff09 目标 xff1a 在matlab20
  • ros的init机制续篇

    这篇博客主要探讨init的实现过程 ros span class token double colon punctuation span span class token function init span span class toke
  • 基于Unity构建机器人的数字孪生平台系列1—介绍

    1 0 简介 本系列博客将开源近两年结合Unity和多旋翼无人机的相关工作 xff0c 涵盖仿真 建模 全局云端通信网络 本地局部通信网络 ROS 43 Unity VR等方面内容 该工作完整构建以虚控实 xff0c 沉浸式VR交互 xff
  • 基于Unity构建机器人的数字孪生平台系列2—四旋翼无人机三维模型

    系列2的主要内容是探讨如何自己构建一个模型并且导入Unity 1 简介 3D仿真与其他类型仿真的一大区别是三维场景和三维模型 为了实现对某个对象的仿真 xff0c 模型是必须的 当然 xff0c 针对不同的仿真任务 xff0c 需要描述对象
  • 模式识别实现之人脸识别(matlab)

    描述 用有监督学习机制设计并实现模式识别方法 xff0c 用于进行人脸面部特征识别 xff0c 如性别 xff08 男性 女性 xff09 年龄 xff08 儿童 青少年 成年 老年 xff09 佩戴眼镜 xff08 是 否 xff09 戴
  • 无人机自动驾驶GAAS学习一

    building GAAS environment 基本依赖项 pip install pandas jinja2 pyserial cerberus pyulog numpy toml pyquaternion Q1 程序 pip 尚未安
  • 无人机学习之launch文件的学习

    官网教程 xff1a http wiki ros org roslaunch XML xff08 1 xff09 ros系统launch文件出现的原因 xff1a 一个功能的实现包括比较多的节点的运行 xff0c 并且每个节点的启动是有顺序
  • 配置最基础的linux系统——Centos6.x版本

    自然是用到虚拟机了 xff0c Vmware是我常用的 xff0c 这里建立一个虚拟的裸机很简单 xff0c 有两点是要说明的 1 最大磁盘大小 xff0c 这个默认的是最小大小 xff0c 不能设的别它还小了 xff0c 否则启动不了Ce
  • matlab实现画散点图(一个x对应多个y)

    1 具体实现是 xff0c 首选导入数据 aray 61 importdata 位置 xff1b m n 61 size array 2 x轴间距设置 x 61 1 1 m 3 处理数组数据 figure 1 for i 61 1 1 n
  • VINS-Mono源码分析7— pose_graph2(四自由度位姿图优化)

    VINS Mono源码分析7 pose graph2 在上一篇博文中 xff0c 大概分析了一下VINS Mono回环检测和重定位的代码实现 xff0c 这里主要分析 四自由度的位姿图优化 关于这部分的原理可以参考VINS Mono论文第8
  • VINS-Mono中的DBoW2关键代码注释

    VINS Mono中的DBoW2关键代码注释 在阅读VINS Mono源码时对DBoW2中代码顺手做的注释 xff0c 怕以后会忘记 xff0c 在这里记录一下 xff0c 注释有不当之处 xff0c 望各位大神看到后多多指点 理论参考高翔