首页 > 编程语言 >ORB-SLAM3源码学习: KeyFrameDatabase.cc: KeyFrameDatabase::DetectNBestCandidates找到N个融合候选关键帧和闭环候选关键帧

ORB-SLAM3源码学习: KeyFrameDatabase.cc: KeyFrameDatabase::DetectNBestCandidates找到N个融合候选关键帧和闭环候选关键帧

时间:2025-01-04 23:34:36浏览次数:3  
标签:候选 关键帧 pKFi lit ++ KeyFrameDatabase 单词

前言

ORB-SLAM3支持多地图系统,因此引进了地图融合线程。

地图融合线程会检测活跃地图和整个地图集是否存在共同区域,如果检测到共同区域发生在当前关键帧和活跃地图中,则执行闭环操作,如果检测到共同区域发生在当前帧和非活跃地图中,则执行地图融合操作。如果同时检测到闭环和地图融合,则忽略闭环执行地图融合操作。具体定义在KeyFrameDatabase.cc中。

1.函数声明

void KeyFrameDatabase::DetectNBestCandidates(KeyFrame *pKF, vector<KeyFrame *> &vpLoopCand, vector<KeyFrame *> &vpMergeCand, int nNumCandidates)

2.函数定义

下面的流程已经总结的很到位了! 

具体流程如下:

1.统计与当前帧具有共同单词的关键帧,但是排除掉了与当前关键帧共视的关键帧,之后遍历这些共同单词,有该单词的关键帧进行标记(pKFi->mnPlaceRecognitionQuery = pKF->mnId;)将这个关键帧加入到列表中(lKFsSharingWords.push_back(pKFi);)记录有这个共同单词的关键帧的数量(pKFi->mnPlaceRecognitionWords++;)。该部分在一个锁中。若是最后没有统计到与当前关键帧具有公共单词的关键帧则直接返回。

排除了共视关键帧:目的是防止直接将共视的关键帧当作闭环关键帧。 

 // Step 1统计与当前关键帧有相同单词的关键帧
    list<KeyFrame *> lKFsSharingWords;
    // set<KeyFrame*> spInsertedKFsSharing;
    //  当前关键帧的共视关键帧(避免将当前关键帧的共视关键帧加入回环检测)
    set<KeyFrame *> spConnectedKF;

    // Search all keyframes that share a word with current frame
    {
        unique_lock<mutex> lock(mMutex);
        // 拿到当前关键帧的共视关键帧
        spConnectedKF = pKF->GetConnectedKeyFrames();

        // 遍历当前关键帧bow向量的每个单词
        for (DBoW2::BowVector::const_iterator vit = pKF->mBowVec.begin(), vend = pKF->mBowVec.end(); vit != vend; vit++)
        { // 拿到当前单词的逆向索引(所有有当前单词的关键帧)
            list<KeyFrame *> &lKFs = mvInvertedFile[vit->first];
            // 遍历每个有该单词的关键帧
            for (list<KeyFrame *>::iterator lit = lKFs.begin(), lend = lKFs.end(); lit != lend; lit++)
            {
                KeyFrame *pKFi = *lit;
                /*if(spConnectedKF.find(pKFi) != spConnectedKF.end())
                {
                    continue;
                }*/
                // 如果此关键帧没有被当前关键帧访问过(防止重复添加)
                if (pKFi->mnPlaceRecognitionQuery != pKF->mnId)
                {
                    // 初始化公共单词数为0
                    pKFi->mnPlaceRecognitionWords = 0;
                    // 如果该关键帧不是当前关键帧的共视关键帧
                    if (!spConnectedKF.count(pKFi))
                    {
                        // 标记该关键帧被当前关键帧访问到(也就是有公共单词)
                        pKFi->mnPlaceRecognitionQuery = pKF->mnId;
                        // 把当前关键帧添加到有公共单词的关键帧列表中
                        lKFsSharingWords.push_back(pKFi);
                    }
                }
                // 递增该关键帧与当前关键帧的公共单词数
                pKFi->mnPlaceRecognitionWords++;
                /*if(spInsertedKFsSharing.find(pKFi) == spInsertedKFsSharing.end())
                {
                    lKFsSharingWords.push_back(pKFi);
                    spInsertedKFsSharing.insert(pKFi);
                }*/
            }
        }
    }
    // 如果没有有公共单词的关键帧,直接返回
    if (lKFsSharingWords.empty())
        return;

 2.不断遍历前边筛选好的具有共同单词数的关键帧,逐一比较得到一个最大的共同的单词数(maxCommonWords),用最大单词数的0.8倍作为最小单词数用于后续的筛选,之后对满足大于最小单词数的关键帧进行相似度的计算,并加入到创建的新列表中(lScoreAndMatch.push_back(make_pair(si, pKFi));),如果经过筛选并没有满足条件的关键帧则返回。

// Only compare against those keyframes that share enough words
    // Step 2 统计所有候选帧中与当前关键帧的公共单词数最多的单词数maxCommonWords,并筛选
    int maxCommonWords = 0;
    for (list<KeyFrame *>::iterator lit = lKFsSharingWords.begin(), lend = lKFsSharingWords.end(); lit != lend; lit++)
    {
        if ((*lit)->mnPlaceRecognitionWords > maxCommonWords)
            maxCommonWords = (*lit)->mnPlaceRecognitionWords;
    }
    // 取0.8倍为阀值
    int minCommonWords = maxCommonWords * 0.8f;
    // 这里的pair是 <相似度,候选帧的指针> : 记录所有大于minCommonWords的候选帧与当前关键帧的相似度
    list<pair<float, KeyFrame *>> lScoreAndMatch;
    // 只是个统计变量,貌似没有用到
    int nscores = 0;

    // Compute similarity score.
    // 对所有大于minCommonWords的候选帧计算相似度

    // 遍历所有有公共单词的候选帧
    for (list<KeyFrame *>::iterator lit = lKFsSharingWords.begin(), lend = lKFsSharingWords.end(); lit != lend; lit++)
    {
        KeyFrame *pKFi = *lit;
        // 如果当前帧的公共单词数大于minCommonWords
        if (pKFi->mnPlaceRecognitionWords > minCommonWords)
        {
            nscores++; //未使用
            // 计算相似度
            float si = mpVoc->score(pKF->mBowVec, pKFi->mBowVec);
            // 记录该候选帧与当前帧的相似度
            pKFi->mPlaceRecognitionScore = si;
            // 记录到容器里, 每个元素是<相似度,候选帧的指针>
            lScoreAndMatch.push_back(make_pair(si, pKFi));
        }
    }
    // 如果为空,直接返回,表示没有符合上述条件的关键帧
    if (lScoreAndMatch.empty())
        return;

3. 遍历上述已经筛选过得到的列表(lScoreAndMatch),获取列表中的关键帧的最好的10个共视关键帧(这些关键帧必须还得是之前被访问过的),统计一些变量(accScore、pBestKF、bestScore),并将其记录到变量lAccScoreAndMatch中。按照相似度对这个列表进行排序,遍历这个列表当遍历未越界以及(闭环候选容器的尺寸或者融合候选容器的尺寸不大于指定数量)就会已知循环。如果没有被添加过就要判断到底应该添加到哪个候选关键帧列表(回环候选关键帧和融合候选关键帧列表)中,已经添加过的就进行标记。

    // Step 3 : 用小组得分排序得到top3总分里最高分的关键帧,作为候选帧
    // 统计以组为单位的累计相似度和组内相似度最高的关键帧, 每个pair为<小组总相似度,组内相似度最高的关键帧指针>
    list<pair<float, KeyFrame *>> lAccScoreAndMatch;
    float bestAccScore = 0;

    // Lets now accumulate score by covisibility
    // 变量所有被lScoreAndMatch记录的pair <相似度,候选关键帧>
    for (list<pair<float, KeyFrame *>>::iterator it = lScoreAndMatch.begin(), itend = lScoreAndMatch.end(); it != itend; it++)
    {
        // 候选关键帧
        KeyFrame *pKFi = it->second;
        // 与候选关键帧共视关系最好的10个关键帧
        vector<KeyFrame *> vpNeighs = pKFi->GetBestCovisibilityKeyFrames(10);
        // 初始化最大相似度为该候选关键帧自己的相似度
        float bestScore = it->first;
        // 初始化小组累计得分为改候选关键帧自己的相似度
        float accScore = bestScore;
        // 初始化组内相似度最高的帧为该候选关键帧本身
        KeyFrame *pBestKF = pKFi;
        // 遍历与当前关键帧共视关系最好的10帧
        for (vector<KeyFrame *>::iterator vit = vpNeighs.begin(), vend = vpNeighs.end(); vit != vend; vit++)
        {
            KeyFrame *pKF2 = *vit;
            // 如果该关键帧没有被当前关键帧访问过(也就是没有公共单词)则跳过
            if (pKF2->mnPlaceRecognitionQuery != pKF->mnId)
                continue;
            // 累加小组总分
            accScore += pKF2->mPlaceRecognitionScore;
            // 如果大于组内最高分,则更新当前最高分记录
            if (pKF2->mPlaceRecognitionScore > bestScore)
            {
                pBestKF = pKF2;
                bestScore = pKF2->mPlaceRecognitionScore;
            }
        }
        // 统计以组为单位的累计相似度和组内相似度最高的关键帧, 每个pair为<小组总相似度,组内相似度最高的关键帧指针>
        lAccScoreAndMatch.push_back(make_pair(accScore, pBestKF));
        // 统计最高得分, 这个bestAccSocre没有用到
        if (accScore > bestAccScore)
            bestAccScore = accScore;
    }

    // cout << "Amount of candidates: " << lAccScoreAndMatch.size() << endl;
    //  按相似度从大到小排序
    lAccScoreAndMatch.sort(compFirst);
    // 最后返回的变量, 记录回环的候选帧
    vpLoopCand.reserve(nNumCandidates);
    // 最后返回的变量, 记录融合候选帧
    vpMergeCand.reserve(nNumCandidates);
    // 避免重复添加
    set<KeyFrame *> spAlreadyAddedKF;
    // cout << "Candidates in score order " << endl;
    // for(list<pair<float,KeyFrame*> >::iterator it=lAccScoreAndMatch.begin(), itend=lAccScoreAndMatch.end(); it!=itend; it++)
    int i = 0;
    list<pair<float, KeyFrame *>>::iterator it = lAccScoreAndMatch.begin();
    // 遍历lAccScoreAndMatch中所有的pair, 每个pair为<小组总相似度,组内相似度最高的关键帧指针>,nNumCandidates默认为3
    while (i < lAccScoreAndMatch.size() && (vpLoopCand.size() < nNumCandidates || vpMergeCand.size() < nNumCandidates))
    {
        // cout << "Accum score: " << it->first << endl;
        //  拿到候选关键帧的指针
        KeyFrame *pKFi = it->second;
        if (pKFi->isBad())
            continue;

        // 如果没有被重复添加
        if (!spAlreadyAddedKF.count(pKFi))
        {
            // 如果候选帧与当前关键帧在同一个地图里,且候选者数量还不足够
            if (pKF->GetMap() == pKFi->GetMap() && vpLoopCand.size() < nNumCandidates)
            {
                // 添加到回环候选帧里
                vpLoopCand.push_back(pKFi);
            }
            // 如果候选者与当前关键帧不再同一个地图里, 且候选者数量还不足够, 且候选者所在地图不是bad
            else if (pKF->GetMap() != pKFi->GetMap() && vpMergeCand.size() < nNumCandidates && !pKFi->GetMap()->IsBad())
            {
                // 添加到融合候选帧里
                vpMergeCand.push_back(pKFi);
            }
            // 防止重复添加
            spAlreadyAddedKF.insert(pKFi);
        }
        i++;
        it++;
    }
}

3.完整的代码解析

/*
 找到N个融合候选N个回环候选
 pKF 当前关键帧(我们要寻找这个关键帧的回环候选帧和融合候选帧)
 vpLoopCand 记录找到的回环候选关键帧
 vpMergeCand 记录找到的融合候选关键帧
 nNumCandidates 期望的候选数目,即回环和候选分别应该有多少个
 */
void KeyFrameDatabase::DetectNBestCandidates(KeyFrame *pKF, vector<KeyFrame *> &vpLoopCand, vector<KeyFrame *> &vpMergeCand, int nNumCandidates)
{
    // Step 1统计与当前关键帧有相同单词的关键帧
    list<KeyFrame *> lKFsSharingWords;
    // set<KeyFrame*> spInsertedKFsSharing;
    //  当前关键帧的共视关键帧(避免将当前关键帧的共视关键帧加入回环检测)
    set<KeyFrame *> spConnectedKF;

    // Search all keyframes that share a word with current frame
    {
        unique_lock<mutex> lock(mMutex);
        // 拿到当前关键帧的共视关键帧
        spConnectedKF = pKF->GetConnectedKeyFrames();

        // 遍历当前关键帧bow向量的每个单词
        for (DBoW2::BowVector::const_iterator vit = pKF->mBowVec.begin(), vend = pKF->mBowVec.end(); vit != vend; vit++)
        { // 拿到当前单词的逆向索引(所有有当前单词的关键帧)
            list<KeyFrame *> &lKFs = mvInvertedFile[vit->first];
            // 遍历每个有该单词的关键帧
            for (list<KeyFrame *>::iterator lit = lKFs.begin(), lend = lKFs.end(); lit != lend; lit++)
            {
                KeyFrame *pKFi = *lit;
                /*if(spConnectedKF.find(pKFi) != spConnectedKF.end())
                {
                    continue;
                }*/
                // 如果此关键帧没有被当前关键帧访问过(防止重复添加)
                if (pKFi->mnPlaceRecognitionQuery != pKF->mnId)
                {
                    // 初始化公共单词数为0
                    pKFi->mnPlaceRecognitionWords = 0;
                    // 如果该关键帧不是当前关键帧的共视关键帧
                    if (!spConnectedKF.count(pKFi))
                    {
                        // 标记该关键帧被当前关键帧访问到(也就是有公共单词)
                        pKFi->mnPlaceRecognitionQuery = pKF->mnId;
                        // 把当前关键帧添加到有公共单词的关键帧列表中
                        lKFsSharingWords.push_back(pKFi);
                    }
                }
                // 递增该关键帧与当前关键帧的公共单词数
                pKFi->mnPlaceRecognitionWords++;
                /*if(spInsertedKFsSharing.find(pKFi) == spInsertedKFsSharing.end())
                {
                    lKFsSharingWords.push_back(pKFi);
                    spInsertedKFsSharing.insert(pKFi);
                }*/
            }
        }
    }
    // 如果没有有公共单词的关键帧,直接返回
    if (lKFsSharingWords.empty())
        return;

    // Only compare against those keyframes that share enough words
    // Step 2 统计所有候选帧中与当前关键帧的公共单词数最多的单词数maxCommonWords,并筛选
    int maxCommonWords = 0;
    for (list<KeyFrame *>::iterator lit = lKFsSharingWords.begin(), lend = lKFsSharingWords.end(); lit != lend; lit++)
    {
        if ((*lit)->mnPlaceRecognitionWords > maxCommonWords)
            maxCommonWords = (*lit)->mnPlaceRecognitionWords;
    }
    // 取0.8倍为阀值
    int minCommonWords = maxCommonWords * 0.8f;
    // 这里的pair是 <相似度,候选帧的指针> : 记录所有大于minCommonWords的候选帧与当前关键帧的相似度
    list<pair<float, KeyFrame *>> lScoreAndMatch;
    // 只是个统计变量,貌似没有用到
    int nscores = 0;

    // Compute similarity score.
    // 对所有大于minCommonWords的候选帧计算相似度

    // 遍历所有有公共单词的候选帧
    for (list<KeyFrame *>::iterator lit = lKFsSharingWords.begin(), lend = lKFsSharingWords.end(); lit != lend; lit++)
    {
        KeyFrame *pKFi = *lit;
        // 如果当前帧的公共单词数大于minCommonWords
        if (pKFi->mnPlaceRecognitionWords > minCommonWords)
        {
            nscores++; //未使用
            // 计算相似度
            float si = mpVoc->score(pKF->mBowVec, pKFi->mBowVec);
            // 记录该候选帧与当前帧的相似度
            pKFi->mPlaceRecognitionScore = si;
            // 记录到容器里, 每个元素是<相似度,候选帧的指针>
            lScoreAndMatch.push_back(make_pair(si, pKFi));
        }
    }
    // 如果为空,直接返回,表示没有符合上述条件的关键帧
    if (lScoreAndMatch.empty())
        return;
    // Step 3 : 用小组得分排序得到top3总分里最高分的关键帧,作为候选帧
    // 统计以组为单位的累计相似度和组内相似度最高的关键帧, 每个pair为<小组总相似度,组内相似度最高的关键帧指针>
    list<pair<float, KeyFrame *>> lAccScoreAndMatch;
    float bestAccScore = 0;

    // Lets now accumulate score by covisibility
    // 变量所有被lScoreAndMatch记录的pair <相似度,候选关键帧>
    for (list<pair<float, KeyFrame *>>::iterator it = lScoreAndMatch.begin(), itend = lScoreAndMatch.end(); it != itend; it++)
    {
        // 候选关键帧
        KeyFrame *pKFi = it->second;
        // 与候选关键帧共视关系最好的10个关键帧
        vector<KeyFrame *> vpNeighs = pKFi->GetBestCovisibilityKeyFrames(10);
        // 初始化最大相似度为该候选关键帧自己的相似度
        float bestScore = it->first;
        // 初始化小组累计得分为改候选关键帧自己的相似度
        float accScore = bestScore;
        // 初始化组内相似度最高的帧为该候选关键帧本身
        KeyFrame *pBestKF = pKFi;
        // 遍历与当前关键帧共视关系最好的10帧
        for (vector<KeyFrame *>::iterator vit = vpNeighs.begin(), vend = vpNeighs.end(); vit != vend; vit++)
        {
            KeyFrame *pKF2 = *vit;
            // 如果该关键帧没有被当前关键帧访问过(也就是没有公共单词)则跳过
            if (pKF2->mnPlaceRecognitionQuery != pKF->mnId)
                continue;
            // 累加小组总分
            accScore += pKF2->mPlaceRecognitionScore;
            // 如果大于组内最高分,则更新当前最高分记录
            if (pKF2->mPlaceRecognitionScore > bestScore)
            {
                pBestKF = pKF2;
                bestScore = pKF2->mPlaceRecognitionScore;
            }
        }
        // 统计以组为单位的累计相似度和组内相似度最高的关键帧, 每个pair为<小组总相似度,组内相似度最高的关键帧指针>
        lAccScoreAndMatch.push_back(make_pair(accScore, pBestKF));
        // 统计最高得分, 这个bestAccSocre没有用到
        if (accScore > bestAccScore)
            bestAccScore = accScore;
    }

    // cout << "Amount of candidates: " << lAccScoreAndMatch.size() << endl;
    //  按相似度从大到小排序
    lAccScoreAndMatch.sort(compFirst);
    // 最后返回的变量, 记录回环的候选帧
    vpLoopCand.reserve(nNumCandidates);
    // 最后返回的变量, 记录融合候选帧
    vpMergeCand.reserve(nNumCandidates);
    // 避免重复添加
    set<KeyFrame *> spAlreadyAddedKF;
    // cout << "Candidates in score order " << endl;
    // for(list<pair<float,KeyFrame*> >::iterator it=lAccScoreAndMatch.begin(), itend=lAccScoreAndMatch.end(); it!=itend; it++)
    int i = 0;
    list<pair<float, KeyFrame *>>::iterator it = lAccScoreAndMatch.begin();
    // 遍历lAccScoreAndMatch中所有的pair, 每个pair为<小组总相似度,组内相似度最高的关键帧指针>,nNumCandidates默认为3
    while (i < lAccScoreAndMatch.size() && (vpLoopCand.size() < nNumCandidates || vpMergeCand.size() < nNumCandidates))
    {
        // cout << "Accum score: " << it->first << endl;
        //  拿到候选关键帧的指针
        KeyFrame *pKFi = it->second;
        if (pKFi->isBad())
            continue;

        // 如果没有被重复添加
        if (!spAlreadyAddedKF.count(pKFi))
        {
            // 如果候选帧与当前关键帧在同一个地图里,且候选者数量还不足够
            if (pKF->GetMap() == pKFi->GetMap() && vpLoopCand.size() < nNumCandidates)
            {
                // 添加到回环候选帧里
                vpLoopCand.push_back(pKFi);
            }
            // 如果候选者与当前关键帧不再同一个地图里, 且候选者数量还不足够, 且候选者所在地图不是bad
            else if (pKF->GetMap() != pKFi->GetMap() && vpMergeCand.size() < nNumCandidates && !pKFi->GetMap()->IsBad())
            {
                // 添加到融合候选帧里
                vpMergeCand.push_back(pKFi);
            }
            // 防止重复添加
            spAlreadyAddedKF.insert(pKFi);
        }
        i++;
        it++;
    }
}

结束语

以上就是我学习到的内容,如果对您有帮助请多多支持我,如果哪里有问题欢迎大家在评论区积极讨论,我看到会及时回复。  

标签:候选,关键帧,pKFi,lit,++,KeyFrameDatabase,单词
From: https://blog.csdn.net/2301_76831056/article/details/144911365

相关文章

  • 【WPF学习】第五十四章 关键帧动画
    【WPF学习】第五十四章关键帧动画 到目前为止,看到的所有动画都使用线性插值从起点到终点。但如果需要创建具有多个分段的动画和不规则移动的动画。例如,可能希望创建一个动画,快速地将一个元素滑入到视图中,然后慢慢地将它移到正确位置。可通过创建两个连续的动画,并使用Beg......
  • video-analyzer:开源视频分析工具,支持提取视频关键帧、音频转录,自动生成视频详细描述
    ❤️如果你也关注AI的发展现状,且对AI应用开发非常感兴趣,我会每日跟你分享最新的AI资讯和开源应用,也会不定期分享自己的想法和开源实例,欢迎关注我哦!......
  • 【CSS in Depth 2 精译_097】第 17 章:CSS 动画特效概述 + 17.1:关键帧的概念及用法 + 1
    当前内容所在位置(可进入专栏查看其他译好的章节内容)第五部分添加动效✔️【第17章动画】✔️17.1关键帧✔️17.23D变换下的动画设置✔️17.2.1添加动画前页面布局的构建✔️17.2.2为布局添加动画✔️17.3动画延迟与填充模式文章目录17动画Animat......
  • 【优化】-审批任务候选人提取超时
    背景低代码审批流在创建审批任务时需要为审批任务分配审批人,在配置审批人的时候,可以选择不同维度的身份,如用户、岗位、角色、组织,可以同时配置多个维度,每个维度的结果取交集。在这个基础上还可以根据运行时环境不同选择不同组织下的用户,例如当前登录组织的上n级或者下n级,也可以......
  • C题目:有3个候选人,每个选民只能投票一个人,要求编一个统计选票的程序,先后输出候选人的名
    C题目:有3个候选人,每个选民只能投票一个人,要求编一个统计选票的程序,先输出候选人的名字,最后输出各个人所的票结果。代码:#include<stdio.h>#include<string.h>structPerson{charname[20];intcount;}leader[3]={{"li",0},{"zhang",0},{"sun",......
  • 作为HR如何做候选人的背景调查
    如果HR想更进一步解决背景调查难题,可以先建立一套规范的调查流程,这样能够大大节约时间。在这套流程中要包括明确调查的内容,比如个人学历,工作经历,资质证书等等,只要是和岗位招聘相关的信息都需要全面覆盖。再就是要与求职者充分沟通,征得对方同意之后再深入了解其背景调查情况,最......
  • 昇腾 - AscendCL C++应用开发 目标检测中的非极大值抑制NMS和计算候选边界框之间的交
    昇腾-AscendCLC++应用开发目标检测中的非极大值抑制(NMS,Non-MaximumSuppression)涉及计算候选边界框之间的交并比(IOU,IntersectionoverUnion)flyfish结构体BBox:定义了一个边界框的数据结构,包含中心坐标、宽高、置信度分数、类别索引和输出索引。函数IOU:计算两个......
  • 对候选人得票的统计程序
            一个结构体变量中可以存放一组数据(如一个学生的学号、姓名、成绩等数据)。如果有10个学生的数据需要参加运算,显然应该用数组,这就是结构体数组。结构体数组与以前介绍过的数值型数组不同之处在于:每个数组元素都是一个结构体类型的数据,它们都分别包括各个成员项。......
  • 初识C语言~~查找票数最高候选人
    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录前言一、题目描述二、解题思路三、编写代码总结前言提示:这里可以添加本文要记录的大概内容:大家好又见面喽!!今天是刷题,二话不说开干。提示:以下是本篇文章正文内容,下面案例可供参考一、题目......
  • 更改Windows11/10自带的微软拼音输入法的“候选词字体”
    候选字体开启半全角切换快捷键自定义短语自定义短语符号名称符号快捷短语引号「」yh单引号『』yh全角空格×2  kk叉(乘号)×cha六角括号〔〕ljkh超级管理员默认用户名administratoradmin......