【Windows + VSCode】DBoW2 从零下载、编译到运行 Demo 完整复现教程

【Windows + VSCode】DBoW2 从零下载、编译到运行 Demo 完整复现教程

目标是在一台新的 Windows 主机上,从下载安装到最终运行 demo.exe,完整复现 DBoW2 工程。


文章目录

  • [【Windows + VSCode】DBoW2 从零下载、编译到运行 Demo 完整复现教程](#【Windows + VSCode】DBoW2 从零下载、编译到运行 Demo 完整复现教程)
  • 前言
  • 安装基础编译环境
    • [安装 MSVC x64 编译器](#安装 MSVC x64 编译器)
    • [安装 CMake](#安装 CMake)
    • [安装 Git](#安装 Git)
    • [安装 VSCode 和插件](#安装 VSCode 和插件)
  • [下载第三方库 OpenCV 和官方源码 DBoW2](#下载第三方库 OpenCV 和官方源码 DBoW2)
    • [下载第三方库 OpenCV](#下载第三方库 OpenCV)
    • [下载官方源码 DBoW2](#下载官方源码 DBoW2)
  • 命令行编译和运行

前言

在视觉 SLAM、图像检索和回环检测任务中,经常需要判断"当前看到的画面,是否和历史上某一帧很像"。如果直接拿两张图片逐像素比较,不仅计算量大,而且对视角、光照、局部遮挡都很敏感。因此,实际系统通常会先从图像中提取局部特征,例如 ORB、BRIEF、SIFT 等,再把这些局部特征转换成更适合快速检索的图像表示。

DBoW2,全称可以理解为 Dynamic Bag of Words 2,是一个经典的 C++ 视觉词袋库。它的核心思想是把图像中的局部描述子映射到一棵视觉词汇树上,把一张图像表示成一个 Bag-of-Words 向量。这样,图像之间的相似度比较就可以从大量局部特征匹配,简化为稀疏向量之间的快速打分。

在 ORB-SLAM、ORB-SLAM2、ORB-SLAM3 等视觉 SLAM 系统中,DBoW2 常被用于回环检测和重定位。简单来说,它可以帮助系统快速回答:

text 复制代码
当前帧是否和历史关键帧相似?
当前相机是否回到了以前到过的位置?
跟踪丢失后,能不能通过图像检索找回当前位置?

DBoW2 本身不是完整的 SLAM 系统,也不负责相机位姿优化。它更像是 SLAM 系统中的"图像检索模块"或"视觉词袋数据库模块"。它负责把图像特征变成词袋向量,建立图像数据库,并根据相似度返回候选匹配结果。后续是否接受这些候选结果,还需要几何验证、PnP、RANSAC、位姿图优化等模块继续判断。

本文的目标不是讲解 DBoW2 的全部算法细节,而是先解决一个更基础也更容易卡住的问题:如何在 Windows + VSCode 环境下,从零下载依赖、配置 CMake、编译工程,并成功运行 DBoW2 自带 demo。只要能把 demo 跑通,后续再阅读源码、接入 ORB-SLAM、修改词袋模型或做实验,就有了一个可靠的起点。

推荐运行环境:

注意事项 说明
操作系统 Windows 10 / Windows 11
编译器 Visual Studio 2022 的 MSVC x64 编译器
构建工具 CMake
编辑器 VSCode
第三方库 OpenCV Windows 预编译包
示例根目录 D:\ORBSLAM
DBoW2 示例路径 D:\ORBSLAM\DBoW2
OpenCV 示例路径 D:\ORBSLAM\opencv\build

如果你的工程放在其他盘,例如 E:\ORBSLAMC:\dev\ORBSLAM,后文所有 D:\ORBSLAM 都需要替换成你的真实路径。

安装基础编译环境

软件下载 说明
Visual Studio 2022 Community 包含了 MSVC 编译器工具链,同时还包含完整的 IDE(编辑器、调试器、设计器等)
Visual Studio 2022 Build Tools (推荐) 包含了 MSVC 编译器工具链,但只提供命令行环境,不含 IDE 图形界面
CMake 跨平台构建工具,现代 C++ 项目必备
Git for Windows 用于 git clone 下载源码
VSCode 编辑器

安装 MSVC x64 编译器

VSCode 只是编辑器,不自带 C++ 编译器。DBoW2 的 .cpp 文件最终需要 Visual Studio 提供的 MSVC 编译器编译。(博主这里选择安装 Visual Studio 2022 Build Tools)

安装 Visual Studio Community 或 Build Tools 时,必须勾选:

text 复制代码
Desktop development with C++

建议确认包含:

  • MSVC C++ x64/x86 build tools
  • Windows 10 SDK 或 Windows 11 SDK
  • C++ CMake tools for Windows

安装完成后,打开开始菜单中的:

text 复制代码
x64 Native Tools Command Prompt for VS 2022

输入:

bat 复制代码
cl

如果看到 Microsoft C/C++ 编译器版本信息,说明 MSVC 可用。若提示 'cl' 不是内部或外部命令,通常是没有安装 C++ workload,或者打开的是普通 CMD/PowerShell。

安装 CMake

推荐 Windows x64 Installer(.msi 安装包):双击安装自动配好环境变量,还带图形界面。

安装 CMake 时注意勾选:将 CMake 的 bin 文件夹路径添加到系统环境变量中。

text 复制代码
Add CMake to the PATH environment variable

安装完成后,关闭旧终端,重新打开 PowerShell:

powershell 复制代码
cmake --version

能显示版本号即安装成功。

注意:必须安装 CMake,后续 VSCode 的 CMake Tools 插件不能代替 CMake 本体,插件只是帮你调用 cmake.exe

安装 Git

直接选择"Standalone Installer"下的"Git for Windows/x64 Setup":这是最标准、最推荐普通用户的安装方式(.exe 安装包)。

安装 Git 后,重新打开 PowerShell:

powershell 复制代码
git --version

能显示版本号即可。若不想安装 Git,也可以从 GitHub 下载 ZIP 源码包。

安装 VSCode 和插件

先安装 VSCode 本体:

安装完成后打开 VSCode,快捷键Ctrl + Shift + X在左侧扩展栏中搜索并安装插件:

text 复制代码
C/C++
CMake Tools

下载第三方库 OpenCV 和官方源码 DBoW2

代码下载 说明
OpenCV 4.8.0 Windows 包 使用的 OpenCV 版本(Windows 自解压安装包)
DBoW2 官方仓库 DBoW2(回环检测词袋模型)原始开源仓库

建议把 OpenCV 和 DBoW2 放在同一个父目录下:

text 复制代码
D:\ORBSLAM
├── DBoW2
│   ├── CMakeLists.txt
│   ├── include
│   ├── src
│   ├── demo
│   ├── docs
│   └── README.md
└── opencv
    └── build
        ├── OpenCVConfig.cmake
        ├── x64\vc16\bin
        └── x64\vc16\lib

下载第三方库 OpenCV

下载

text 复制代码
opencv-4.8.0-windows.exe

双击运行,它本质上是自解压包。建议解压到:

text 复制代码
D:\ORBSLAM

解压后应得到:

text 复制代码
D:\ORBSLAM\opencv\build

用 PowerShell 检查关键文件:

powershell 复制代码
Test-Path D:\ORBSLAM\opencv\build\OpenCVConfig.cmake
Test-Path D:\ORBSLAM\opencv\build\x64\vc16\bin\opencv_world480.dll

两个都输出 True,说明 OpenCV 位置正确。

下载官方源码 DBoW2

  1. 使用 Git 下载(推荐):

    powershell 复制代码
    cd D:\ORBSLAM
    # 从 GitHub 官方源克隆 DBoW2 
    git clone https://github.com/dorian3d/DBoW2.git DBoW2
  2. 如果不用 Git,使用 ZIP 下载:打开 https://github.com/dorian3d/DBoW2点击 Code选择 Download ZIP下载,解压后把文件夹改名为 DBoW2放到 D:\ORBSLAM\DBoW2

    检查源码结构

powershell 复制代码
cd D:\ORBSLAM\DBoW2
dir

至少应看到:

text 复制代码
CMakeLists.txt
README.md
include
src
demo

命令行编译和运行

Visual Studio 2022 编译

打开:

text 复制代码
Developer PowerShell for VS 2022

执行:

powershell 复制代码
cd D:\ORBSLAM\DBoW2

# 配置 CMake
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DOpenCV_DIR=D:/ORBSLAM/opencv/build -DBUILD_Demo=ON "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" -DOpenCV_RUNTIME=vc16 -DOpenCV_ARCH=x64

# 编译
cmake --build build --config Release 

参数说明

参数 含义
-S . 源码目录是当前目录
-B build 构建目录是 build
-G "Visual Studio 17 2022" 使用 VS2022 生成器
-A x64 使用 64 位编译
-DOpenCV_DIR=... 指定 OpenCV 的 CMake 配置目录
-DBUILD_Demo=ON 编译 demo 程序
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 绕过新版 CMake 对 cmake_minimum_required(VERSION 3.0) 的兼容性检查
-DOpenCV_RUNTIME=vc16 -DOpenCV_ARCH=x64 手动指定 OpenCV 运行时版本和架构,当 MSVC 版本超出 OpenCVConfig 匹配范围时必须使用
--config Release 编译 Release 版本

-DCMAKE_POLICY_VERSION_MINIMUM=3.5 解决新版 CMake 兼容性,-DOpenCV_RUNTIME=vc16 -DOpenCV_ARCH=x64 解决新版 MSVC 兼容性。

运行 demo

编译成功后会生成:

运行 demo:

powershell 复制代码
cd D:\ORBSLAM\DBoW2\build
$env:PATH = "D:\ORBSLAM\opencv\build\x64\vc16\bin;" + $env:PATH
.\Release\demo.exe

注意必须在 build 目录运行,因为 demo 读取图片时使用相对路径,程序中途出现:

text 复制代码
Press enter to continue

按回车继续,成功运行后会生成:

text 复制代码
# 视觉词汇表(YAML + gzip 压缩),包含视觉单词及 TF-IDF 权重
D:\ORBSLAM\DBoW2\build\small_voc.yml.gz
# 图像数据库,包含词汇表 + 图的倒排索引,加载后可直接查询
D:\ORBSLAM\DBoW2\build\small_db.yml.gz
文件 存了什么 干什么用
small_voc.yml.gz 词汇树结构 + 单词的 IDF 权重 特征 → 单词:新图像的特征沿树往下走,每层找最近的聚类中心,最终落到叶子节点得到单词 ID
small_db.yml.gz 词汇表 + 倒排索引 单词 → 图像:每个单词下挂着包含它的图像列表,查询时只比较共同单词的图像,快速返回 Top-N

独立工具:训练与查询分离

原始的 demo.cpp 把训练和测试写在一起。在实际使用中,训练词汇表、建库、查新图往往是分开的步骤------词汇表可以用多样化的数据集训练一次,然后对不同的场景独立建库和查询。为此新增了三个独立工具:

文件 用途
demo/train.cpp 传入图片文件夹,训练词汇表,保存 small_voc.yml.gz
demo/build_db.cpp 加载词汇表,传入图片文件夹建库,保存 small_db.yml.gz
demo/query.cpp 加载词汇表 + 数据库,查任意新图

train.cpp 文件

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <filesystem>
#include <algorithm>
#include "DBoW2.h"
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/features2d.hpp>

using namespace DBoW2;
using namespace std;
namespace fs = std::filesystem;

static void extractFeatures(const vector<string> &paths, vector<vector<cv::Mat> > &features)
{
  cv::Ptr<cv::ORB> orb = cv::ORB::create();
  for(size_t i = 0; i < paths.size(); i++) {
    cv::Mat img = cv::imread(paths[i], 0);
    if(img.empty()) { cerr << "Cannot read " << paths[i] << endl; continue; }
    vector<cv::KeyPoint> kps; cv::Mat desc;
    orb->detectAndCompute(img, cv::Mat(), kps, desc);
    vector<cv::Mat> f(desc.rows);
    for(int j = 0; j < desc.rows; j++) f[j] = desc.row(j);
    features.push_back(f);
    cout << "  [" << (i+1) << "/" << paths.size() << "] " << paths[i] << ": " << desc.rows << " features" << endl;
  }
}

int main(int argc, char **argv)
{
  if(argc < 2) { cerr << "Usage: " << argv[0] << " <image_dir>" << endl; return 1; }

  vector<string> images;
  for(auto &entry : fs::directory_iterator(argv[1])) {
    string ext = entry.path().extension().string();
    if(ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp" || ext == ".pgm")
      images.push_back(entry.path().string());
  }
  if(images.empty()) { cerr << "No images found in " << argv[1] << endl; return 1; }
  sort(images.begin(), images.end());
  cout << "Found " << images.size() << " images" << endl;

  cout << "Extracting features ..." << endl;
  vector<vector<cv::Mat> > features;
  extractFeatures(images, features);

  cout << "Training vocabulary (k=9, L=3, TF-IDF, L1) ..." << endl;
  OrbVocabulary voc(9, 3, TF_IDF, L1_NORM);
  voc.create(features);
  cout << "  " << voc << endl;
  voc.save("small_voc.yml.gz");
  cout << "Saved small_voc.yml.gz" << endl;
  return 0;
}

build_db.cpp

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <filesystem>
#include <algorithm>
#include <fstream>
#include "DBoW2.h"
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/features2d.hpp>
using namespace DBoW2;
using namespace std;
namespace fs = std::filesystem;
static void extractFeatures(const vector<string> &paths, vector<vector<cv::Mat> > &features)
{
  cv::Ptr<cv::ORB> orb = cv::ORB::create();
  for(size_t i = 0; i < paths.size(); i++) {
    cv::Mat img = cv::imread(paths[i], 0);
    if(img.empty()) { cerr << "Cannot read " << paths[i] << endl; continue; }
    vector<cv::KeyPoint> kps; cv::Mat desc;
    orb->detectAndCompute(img, cv::Mat(), kps, desc);
    vector<cv::Mat> f(desc.rows);
    for(int j = 0; j < desc.rows; j++) f[j] = desc.row(j);
    features.push_back(f);
    cout << "  [" << (i+1) << "/" << paths.size() << "] " << paths[i] << ": " << desc.rows << " features" << endl;
  }
}
int main(int argc, char **argv)
{
  if(argc < 2) { cerr << "Usage: " << argv[0] << " <image_dir>" << endl; return 1; }
  cout << "Loading vocabulary from small_voc.yml.gz ..." << endl;
  OrbVocabulary voc("small_voc.yml.gz");
  cout << "  " << voc << endl;
  vector<string> images;
  for(auto &entry : fs::directory_iterator(argv[1])) {
    string ext = entry.path().extension().string();
    if(ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp" || ext == ".pgm")
      images.push_back(entry.path().string());
  }
  if(images.empty()) { cerr << "No images found in " << argv[1] << endl; return 1; }
  sort(images.begin(), images.end());
  cout << "Found " << images.size() << " images" << endl;
  cout << "Extracting features ..." << endl;
  vector<vector<cv::Mat> > features;
  extractFeatures(images, features);
  cout << "Building database ..." << endl;
  OrbDatabase db(voc, false, 0);
  for(size_t i = 0; i < features.size(); i++) db.add(features[i]);
  cout << "  " << db << endl;
  db.save("small_db.yml.gz");
  cout << "Saved small_db.yml.gz" << endl;
  // Save filename index
  ofstream idx("db_index.txt");
  for(size_t i = 0; i < images.size(); i++) idx << images[i] << endl;
  cout << "Saved db_index.txt (" << images.size() << " entries)" << endl;
  return 0;
}

query.cpp

cpp 复制代码
#include <iostream>
#include <iomanip>
#include <vector>
#include <string>
#include <fstream>
#include "DBoW2.h"
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/features2d.hpp>
using namespace DBoW2;
using namespace std;
static void extractFeatures(const string &path, vector<cv::Mat> &features)
{
  cv::Mat img = cv::imread(path, 0);
  if(img.empty()) return;
  cv::Ptr<cv::ORB> orb = cv::ORB::create();
  vector<cv::KeyPoint> kps; cv::Mat desc;
  orb->detectAndCompute(img, cv::Mat(), kps, desc);
  features.resize(desc.rows);
  for(int i = 0; i < desc.rows; i++) features[i] = desc.row(i);
}
int main(int argc, char **argv)
{
  if(argc < 2) { cerr << "Usage: " << argv[0] << " <image_path>" << endl; return 1; }
  string img_path = argv[1];
  cout << "Loading vocabulary: small_voc.yml.gz ..." << endl;
  OrbVocabulary voc("small_voc.yml.gz");
  cout << "  " << voc << endl;
  cout << "Loading database: small_db.yml.gz ..." << endl;
  OrbDatabase db("small_db.yml.gz");
  cout << "  " << db << endl;
  // Load filename index
  vector<string> idx;
  ifstream inf("db_index.txt");
  string line;
  while(getline(inf, line)) idx.push_back(line);
  cout << "  Index: " << idx.size() << " filenames loaded" << endl;
  cout << "Extracting features: " << img_path << endl;
  vector<cv::Mat> features;
  extractFeatures(img_path, features);
  if(features.empty()) { cerr << "Error: no features extracted" << endl; return 1; }
  cout << "  " << features.size() << " features" << endl;
  cout << endl << "Query results:" << endl;
  QueryResults ret;
  db.query(features, ret, min((int)db.size(), 20));
  for(unsigned int i = 0; i < ret.size(); i++) {
    int eid = ret[i].Id;
    string fname = (eid < (int)idx.size()) ? idx[eid] : "unknown";
    cout << "  #" << (i+1) << " Score=" << fixed << setprecision(4) << ret[i].Score
         << "  " << fname << endl;
  }
  return 0;
}

要编译这两个工具,在 CMakeLists.txtif(BUILD_Demo) 块中找到 demoadd_executable,在它后面添加:

cmake 复制代码
if(BUILD_Demo)
  add_executable(demo demo/demo.cpp)                    # 原始 demo(训练+测试一体)
  target_link_libraries(demo ${PROJECT_NAME} ${OpenCV_LIBS})
  set_target_properties(demo PROPERTIES CXX_STANDARD 11)
  file(COPY demo/images DESTINATION ${CMAKE_BINARY_DIR}/)

  add_executable(train demo/train.cpp)                  # 新增:训练词汇表
  target_link_libraries(train ${PROJECT_NAME} ${OpenCV_LIBS})
  set_target_properties(train PROPERTIES CXX_STANDARD 17)

  add_executable(build_db demo/build_db.cpp)            # 新增:加载词汇表+建库
  target_link_libraries(build_db ${PROJECT_NAME} ${OpenCV_LIBS})
  set_target_properties(build_db PROPERTIES CXX_STANDARD 17)

  add_executable(query demo/query.cpp)                  # 新增:查询新图
  target_link_libraries(query ${PROJECT_NAME} ${OpenCV_LIBS})
  set_target_properties(query PROPERTIES CXX_STANDARD 11)
endif(BUILD_Demo)

然后重新 cmake configure + build。

打开:

text 复制代码
Developer PowerShell for VS 2022

执行:

powershell 复制代码
cd D:\ORBSLAM\DBoW2

# 配置 CMake
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DOpenCV_DIR=D:/ORBSLAM/opencv/build -DBUILD_Demo=ON "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" -DOpenCV_RUNTIME=vc16 -DOpenCV_ARCH=x64

# 编译
cmake --build build --config Release 

典型使用流程: LoopDB 是专为回环检测设计的公开数据集【数据集地址:LoopDB Dataset (Google Drive)】,包含 1000+ 张室内外图像。

博主把数据放到放到DBoW2工程目录下,并且将原数据拆分出三个数据集:

  • 训练集(left_train,600 张):用来训练词汇表。提取每张图的 ORB 特征,做层次化 K-means 聚类,生成 729 个视觉单词及 IDF 权重。这个词汇表相当于"词典",把图像特征映射到单词 ID。
  • 建库集(left_db,420 张):用来构建图像数据库。加载词汇表,把每张图的特征转成 BoW 向量,建立倒排索引。这张表记录了"哪个单词出现在哪张图里"。
  • 测试集(left_test,20 张):用来验证检索效果。从建库集中随机抽取,确保这些图原本属于建库集的场景,但不加入数据库。拿它们去查询数据库,看能否找回同场景的相似图。

拆分原因:

  • 训练集和建库集分开:词汇表一旦训练好可以重复使用。这样换一组新图建库时不用重新训练,省时间。
  • 测试集从建库集里抽:如果测试图跟数据库里的图是不同场景的,查不到是正常的,没法验证效果。从建库集里随机抽出几张,保证数据库里有同场景的"邻居",这样查询分数高的结果才是真正有效。

下面用 LoopDB 演示三个工具的完整使用。

powershell 复制代码
cd D:\ORBSLAM\DBoW2\build
$env:PATH = "D:\ORBSLAM\opencv\build\x64\vc16\bin;$env:PATH"
# 1. 训练词汇表
.\Release\train.exe D:\ORBSLAM\DBoW2\left_train
# 生成 small_voc.yml.gz
powershell 复制代码
# 2. 建库
.\Release\build_db.exe D:\ORBSLAM\DBoW2\left_db
# 加载 small_voc.yml.gz → 提取 left_db 特征 → 生成 small_db.yml.gz

build_db.exe 建库时会额外保存 db_index.txt(EntryId → 文件名映射),query.exe 加载后显示文件名,方便确认检索结果。

powershell 复制代码
# 3. 查询测试(用 left_test 中的图,在 left_db 中找最相似的)
.\Release\query.exe D:\ORBSLAM\DBoW2\left_test\IMG_20240702_124457.jpg

查询的图片:

返回的对应数据库的图片: