本人讲解关于slam一系列文章汇总链接:史上最全slam从零开始,针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下:
(02)Cartographer源码无死角解析- (00)目录_最新无死角讲解:https://blog.csdn.net/weixin_43013761/article/details/127350885
文末正下方中心提供了本人 联系方式, 点击本人照片即可显示 W X → 官方认证 {\color{blue}{文末正下方中心}提供了本人 \color{red} 联系方式,\color{blue}点击本人照片即可显示WX→官方认证} 文末正下方中心提供了本人联系方式,点击本人照片即可显示WX→官方认证
在上一篇博客中,对 src/cartographer_ros/cartographer_ros/cartographer_ros/node_options.cc 文件的 LoadOptions() 函数进行了整体上的分析,但是没有进行很细致的讲解,其主要的核心都是围绕这 cartographer::common::ConfigurationFileResolver 的类对象 file_resolver 展开,该类的实现于 src/cartographer/cartographer/common/configuration_file_resolver.cc 文件中。其继承自 FileResolver:
// Resolves file paths and file content for the Lua 'read' and 'include'
// functions. Use this to configure where those functions load other files from.
class FileResolver {
public:
virtual ~FileResolver() {}
virtual std::string GetFullPathOrDie(const std::string& basename) = 0;
virtual std::string GetFileContentOrDie(const std::string& basename) = 0;
};
该类十分简单,仅仅定义了一些接口而已,只有一个私有成员变量 configuration_files_directories_。那么下面我们再回到 ConfigurationFileResolver 进行细致讲解。
configuration_file_resolver.h 文件中定义 ConfigurationFileResolver 的构造函数时,使用到了explicit关键字→作用就是防止类构造函数的隐式自动转换。比如在一个构造函数中,其期待传入的数据类型并非 int 类型,而用户误传入了一个 int 10 进去,其就有可能发生隐式转换,比如编译器把 int 10 封装成一个类对象传入。 构造函数的实现如下:
/**
* @brief Construct a new Configuration File Resolver:: Configuration File Resolver object
*
* @param[in] configuration_files_directories 配置文件目录
*/
ConfigurationFileResolver::ConfigurationFileResolver(
const std::vector<std::string>& configuration_files_directories)
: configuration_files_directories_(configuration_files_directories) {
configuration_files_directories_.push_back(kConfigurationFilesDirectory);
}
传入的参数 configuration_files_directories(配置文件目录) 直接赋值给 configuration_files_directories_,然后还会把 kConfigurationFilesDirectory 这个变量追加到 configuration_files_directories_ 之中,该变量可以在 build_isolated/cartographer/install/cartographer/common/config.h 中找到,
namespace cartographer {
namespace common {
constexpr char kConfigurationFilesDirectory[] =
"/my_work/ROS/work/cartographer_detailed_comments_ws/install_isolated/share/cartographer/configuration_files";
constexpr char kSourceDirectory[] = "/my_work/ROS/work/cartographer_detailed_comments_ws/src/cartographer";
} // namespace common
} // namespace cartographer
其是一个编译生成的文件,那么其是如何生成的呢?又在哪里配置? 来看 src/cartographer/cartographer/common/config.h.cmake 文件:
namespace cartographer {
namespace common {
constexpr char kConfigurationFilesDirectory[] =
"@CARTOGRAPHER_CONFIGURATION_FILES_DIRECTORY@";
constexpr char kSourceDirectory[] = "@PROJECT_SOURCE_DIR@";
} // namespace common
} // namespace cartographer
其仅仅配置了两个变量,分别为 kConfigurationFilesDirectory,kSourceDirectory。
了解构造函数之后,再来看 ConfigurationFileResolver::GetFileContentOrDie() 与 ConfigurationFileResolver::GetFullPathOrDie()函数:
/**
* @brief 在所有的配置文件目录中 根据给定配置文件的名字 搜索 配置文件
*
* @param[in] basename 给定配置文件的名字
* @return std::string 如果搜索成功, 返回配置文件的全路径名
*/
std::string ConfigurationFileResolver::GetFullPathOrDie(
const std::string& basename) {
for (const auto& path : configuration_files_directories_) {
const std::string filename = path + "/" + basename;
std::ifstream stream(filename.c_str());
// 只要找到就退出
if (stream.good()) {
LOG(INFO) << "Found '" << filename << "' for '" << basename << "'.";
return filename;
}
}
// 如果找不到配置文件就退出整个程序
LOG(FATAL) << "File '" << basename << "' was not found.";
}
/**
* @brief 读取配置文件内容
*
* @param[in] basename 文件名
* @return std::string 文件内容的数据流
*/
std::string ConfigurationFileResolver::GetFileContentOrDie(
const std::string& basename) {
CHECK(!basename.empty()) << "File basename cannot be empty." << basename;
// 根据文件名查找是否在给定文件夹中存在
const std::string filename = GetFullPathOrDie(basename);
std::ifstream stream(filename.c_str());
// 读取配置文件内容并返回
return std::string((std::istreambuf_iterator<char>(stream)),
std::istreambuf_iterator<char>());
}
} // namespace common
} // namespace cartographer
GetFullPathOrDie () 函数中是一个循环,其会对 configuration_files_directories_ 中的所有目录进行查找,查找 basename(.lua) 文件,找到,或者没有找到都会打印相应的信息。最后GetFileContentOrDie() 函数从找到的文件中读出内容返回。
LoadOptions() 函数中,在其调用:
const std::string code =file_resolver->GetFileContentOrDie(configuration_basename);
之后,其进一步会执行:
// 根据给定的字符串, 生成一个lua字典
cartographer::common::LuaParameterDictionary lua_parameter_dictionary(
code, std::move(file_resolver));
LuaParameterDictionary 于 src/cartographer/cartographer/common/lua_parameter_dictionary.cc 文件中实现,先来看看其头文件 lua_parameter_dictionary.h,可以看到如下:
#include "cartographer/common/lua.h"
#include "cartographer/common/port.h"
#include "glog/logging.h"
其上的 lua.h 内容为:
#ifndef CARTOGRAPHER_COMMON_LUA_H_
#define CARTOGRAPHER_COMMON_LUA_H_
// 这个头文件包含了使用lua所需的所有头文件
#include
#endif // CARTOGRAPHER_COMMON_LUA_H_
可以看到这里的使用了 lua 这个标准库,那么再来看 LuaParameterDictionary 的构造函数:
LuaParameterDictionary::LuaParameterDictionary(
const std::string& code, std::unique_ptr<FileResolver> file_resolver)
: LuaParameterDictionary(code, ReferenceCount::YES,
std::move(file_resolver)) {}
LuaParameterDictionary::LuaParameterDictionary(
const std::string& code, ReferenceCount reference_count,
std::unique_ptr<FileResolver> file_resolver)
: L_(luaL_newstate()),
index_into_reference_table_(-1),
file_resolver_(std::move(file_resolver)),
reference_count_(reference_count) {
......
}
可以看到其有两个重载函数,在初始化 LuaParameterDictionary 的时候,传入的为两个参数 code、file_resolver。所以其是调用第一个重载函数,其使用初始化列表对进行初始化的时候,会调用第二个重载函数,多了一个传入的参数为 ReferenceCount::YES。ReferenceCount 是枚举类型: enum class ReferenceCount { YES, NO };
lua相关的这个库还是比较复杂的,更多Lua的学习可以参考:https://www.cnblogs.com/chevin/p/5884657.html,
太细节的东西这里就不进行讲解了。在第二个重载函数中,其中有如下代码(有兴趣的朋友可以深入了解一下):
// 将choose、read、LuaInclude注册为Lua的全局函数变量,使得Lua可以调用C函数
lua_register(L_, "choose", LuaChoose);
lua_register(L_, "include", LuaInclude);
lua_register(L_, "read", LuaRead);
个人感觉对于 LuaParameterDictionary 只需要会使用就行了,对于其使用无法就是 LoadOptions() 中调用的四句代码,另外本人在debug的时候,也并没有看到其中具体内容,如下:
其实也没有太多的信息可用,也就是可以看看其中的包含的 include 文件名而已,不过其并不影响对其的使用,如 src/cartographer/cartographer/common/lua_parameter_dictionary.cc 文件中的如下函数:
std::string GetString(const std::string& key);
double GetDouble(const std::string& key);
int GetInt(const std::string& key);
bool GetBool(const std::string& key);
int GetNonNegativeInt(const std::string& key);
std::vector<double> GetArrayValuesAsDoubles();
......
都是从 LuaParameterDictionary 字典获取值是使用的,即传入一个 key,返回其对应的 value,同时还会根据调用的函数不同,对数据进行校验。其实我们会配置,更改参数就行了,不用理解得那么细。平常情况下,我们使用 yml 文件就可以了,并没有这么复杂。
通过前面的学习,已经大概知道 Cartographer 是如何加载配置文件的了,在回到 node_main.cc 文件:
// 根据Lua配置文件中的内容, 为node_options, trajectory_options 赋值
std::tie(node_options, trajectory_options) =
LoadOptions(FLAGS_configuration_directory, FLAGS_configuration_basename);
// MapBuilder类是完整的SLAM算法类
// 包含前端(TrajectoryBuilders,scan to submap) 与 后端(用于查找回环的PoseGraph)
auto map_builder =
cartographer::mapping::CreateMapBuilder(node_options.map_builder_options);
// Node类的初始化, 将ROS的topic传入SLAM, 也就是MapBuilder
Node node(node_options, std::move(map_builder), &tf_buffer,
FLAGS_collect_metrics);
后续将会围绕 CreateMapBuilder 与 Node 展开,进行详细讲解。