从零实现视觉SLAM:现代C++架构、多线程优化与工程实践详解

📅 2026/8/2 10:00:51 👁️ 阅读次数 📝 编程学习
从零实现视觉SLAM:现代C++架构、多线程优化与工程实践详解

1. 项目概述:从理论到实践的视觉SLAM跨越

视觉SLAM(Simultaneous Localization and Mapping,即时定位与地图构建)这个名字,对于机器人、自动驾驶和增强现实领域的开发者来说,早已不再陌生。它就像一个机器人的“眼睛”和“大脑”,让机器在未知环境中,一边移动一边构建周围环境的地图,同时确定自己在这幅地图中的精确位置。市面上不乏优秀的开源框架,如ORB-SLAM、VINS-Mono等,它们像封装好的“黑盒”,提供了强大的功能。但作为一名深耕C++后端与算法多年的工程师,我始终认为,仅仅调用API无法真正理解其精妙之处,也无法应对那些需要深度定制和极致性能的场景。

因此,我决定抛开现成的轮子,动手从零开始,用现代C++实现一套视觉SLAM系统。这个项目的核心目标不是复刻某个特定框架,而是深入算法腹地,亲手搭建每一个关键模块——从图像特征点的提取与匹配,到相机运动与场景结构的联合优化,再到最终地图的生成与维护。我希望通过这个过程,不仅掌握SLAM的数学本质和工程实现细节,更能锤炼出面对复杂系统时,那种从架构设计到代码调试的全局掌控力。这不仅仅是一个算法实现,更是一次对C++工程能力、数学功底和系统思维的全面考验。无论你是希望深入理解SLAM原理的学生,还是需要在产品中集成或优化SLAM模块的工程师,相信这次从理论到代码的完整穿越,都能带来实实在在的收获。

2. 核心架构设计与模块拆解

一套完整的视觉SLAM系统是一个复杂的流水线。在动手写代码之前,清晰的架构设计是成功的基石。我的实现主要遵循经典的前端-后端-闭环检测-建图范式,但在模块划分和数据结构设计上,融入了许多现代C++的工程实践,旨在保证性能的同时,提升代码的可读性和可维护性。

2.1 前端视觉里程计:系统的“感官”与“直觉”

前端视觉里程计(Visual Odometry, VO)是SLAM系统的“眼睛”,负责处理连续的图像输入,并实时估算出相机在两帧之间的运动。这是整个系统实时性的关键,也是算法密集度最高的部分之一。

我的前端实现主要包含以下几个核心子模块:

  1. 特征提取与描述:这是所有视觉处理的基础。我实现了多种特征点,包括经典的FAST角点、ORB特征,以及用于验证的SIFT特征(虽然速度较慢)。关键在于,我设计了一个统一的特征基类Feature,并使用策略模式(Strategy Pattern)来动态选择不同的提取器和描述子。例如,ORBExtractorSIFTExtractor都继承自FeatureExtractor接口。这样,在系统初始化时,可以通过配置文件轻松切换特征类型,而无需改动核心流程代码。ORB特征因其计算速度快和旋转、尺度不变性良好,被设为默认选项。

  2. 特征匹配:提取特征后,需要在连续帧间找到对应关系。我实现了暴力匹配(Brute-Force)和快速近似最近邻(FLANN)两种匹配器。对于ORB特征,由于其描述子是二进制向量,我特别优化了汉明距离(Hamming Distance)的计算,利用CPU的位运算指令进行加速。匹配后的结果会经过一系列滤除步骤:首先根据描述子距离进行初步筛选,然后使用交叉验证(Cross-check)剔除不一致的匹配,最后利用基础矩阵(Fundamental Matrix)或单应性矩阵(Homography)的RANSAC算法,进一步剔除外点(Outliers),得到一组高质量的匹配对。

  3. 运动估计:有了匹配点对,就可以估计相机运动。对于单目相机,这是一个从2D到2D的运动估计问题。我实现了对极几何下的本质矩阵(Essential Matrix)分解和单应性矩阵分解两种方法,并根据场景平面程度自动选择更合适的模型。分解得到的相机运动(旋转矩阵R和平移向量t)通常具有尺度不确定性,这是单目SLAM的固有特性,其尺度需要在后续的优化和闭环中逐步确定。

注意:前端VO的稳定性直接决定了整个SLAM系统的鲁棒性。在实际编码中,要特别注意异常处理。例如,当一帧图像提取的特征点过少,或RANSAC后内点比例太低时,应果断将本帧标记为“跟踪丢失”,并触发重定位逻辑,而不是强行使用不可靠的运动估计,导致后续状态完全发散。

2.2 后端非线性优化:系统的“大脑”与“纠偏”

前端VO提供的运动估计是增量式的,不可避免会累积误差,导致长期运行后轨迹漂移和地图变形。后端优化就是用来解决这个问题的“大脑”。它维护一个包含所有关键帧位姿和地图点位置的概率图模型,并利用所有观测数据(特征匹配)来联合优化这些变量,得到全局一致的最优估计。

我选择使用图优化(Graph Optimization)作为后端框架,具体实现了基于g2o(General Graph Optimization)库的优化器。之所以选择图优化而非传统的滤波器方法(如EKF),是因为图优化能更自然地利用所有历史信息,进行全局批量优化,精度更高,且更易于理解和扩展。

  1. 图模型的构建

    • 顶点(Vertex):代表待优化的变量。主要有两类:VertexPose(关键帧的位姿,即旋转和平移)和VertexPointXYZ(三维地图点的位置)。
    • 边(Edge):代表约束,即观测方程。我实现了EdgeProjection,它连接一个位姿顶点和一个地图点顶点,边的值是该地图点在该关键帧图像上的二维像素坐标观测值,误差函数定义为重投影误差(观测坐标减去由当前位姿和地图点位置计算出的投影坐标)。
  2. 优化流程:当新的关键帧插入时,将其位姿作为顶点加入图中,并将其观测到的地图点(如果已是顶点则建立边,如果是新点则创建顶点并建立边)关联起来。当累积一定数量的关键帧或检测到闭环时,触发一次局部或全局优化。优化过程就是调整图中所有顶点的值,使得所有边的误差平方和最小化,这是一个标准的非线性最小二乘问题,使用列文伯格-马夸尔特(Levenberg-Marquardt)算法求解。

  3. 关键帧管理:并非每一帧都作为关键帧加入后端优化,那样计算量会爆炸。我设计了一套关键帧选择策略:与上一关键帧的时间间隔超过一定阈值、跟踪到的地图点数量变化显著、或相机运动基线足够大时,才将当前帧提升为关键帧。这有效控制了后端图的规模,保证了优化效率。

2.3 闭环检测与建图:系统的“记忆”与“全局观”

闭环检测是消除累积误差的终极手段。当机器人回到曾经到过的地点时,系统需要识别出这种场景重现,并在后端图中添加一个强大的位姿约束,从而将漂移的轨迹“拉回”正确的位置。

  1. 闭环检测:我采用基于词袋模型(Bag of Words, BoW)的方法。首先,使用DBoW2或FBoW库,用所有关键帧的特征描述子离线训练一个视觉词典。在线运行时,将每一关键帧转化为一个词袋向量。当新的关键帧产生时,在词典中快速检索与其最相似的若干历史关键帧。为了排除误匹配,在词袋匹配之后,还需要进行几何验证:即尝试用当前帧与候选闭环帧进行特征匹配和运动估计,如果能够解算出有效的变换关系且支持的内点足够多,则确认闭环成功。

  2. 闭环校正:一旦确认闭环,就在后端优化图中添加一条连接当前关键帧和闭环候选帧的边,这条边的约束值就是通过几何验证计算出的相对位姿变换。然后触发一次全局优化,整个地图和轨迹都会被调整到一致的状态,累积误差得以大幅消除。

  3. 地图构建与维护:地图是SLAM的产出之一。我维护了一个全局地图点云和关键帧数据库。地图点由三角化生成,并随着后端优化不断调整其位置。为了控制内存增长,需要定期剔除质量差的地图点(如被观测次数少、重投影误差大)。最终,我们可以将优化后的地图点云和关键帧轨迹保存下来,用于导航、重建等上层应用。

3. 核心C++实现细节与工程实践

用C++实现这样一个算法密集的系统,远不止是把数学公式翻译成代码。它涉及到内存管理、数据结构设计、并发控制、性能优化等一系列工程挑战。下面分享几个关键的实现细节和我的工程选择。

3.1 现代C++特性与设计模式的应用

为了构建一个清晰、灵活且高效的系统,我广泛使用了C++11/14/17的特性及经典设计模式。

  1. 智能指针管理资源:整个系统充斥着动态创建的对象,如特征点、地图点、关键帧。使用原始指针进行内存管理极易导致泄漏或悬垂指针。我全面采用std::shared_ptrstd::unique_ptr。例如,一个地图点MapPoint会被多个关键帧观测到,因此使用std::shared_ptr<MapPoint>来共享所有权。而一个只属于某个特定关键帧的临时数据,则使用std::unique_ptr。这从根本上避免了内存泄漏,也让代码意图更清晰。

  2. 使用移动语义避免拷贝:特征描述子、图像数据等都是较大的数据块。在函数间传递时,我大量使用右值引用和std::move来转移所有权,而非深拷贝。例如,将一帧图像的特征点向量传递给跟踪线程时,使用移动语义可以几乎零成本地转移数据。

    // 将当前帧的特征移动到跟踪线程的数据结构中 tracking_data->features = std::move(current_frame.features_); // 此后 current_frame.features_ 变为空,所有权转移
  3. 设计模式提升架构弹性

    • 工厂模式:用于创建不同的特征提取器、匹配器、优化器等。通过一个统一的工厂接口,根据配置字符串动态生成对应的组件实例。
    • 观察者模式:用于模块间通信。例如,当后端优化完成时,可以通知地图显示模块更新视图,而两者之间没有直接的耦合。
    • 策略模式:如前所述,用于特征提取、匹配等算法的灵活切换。

3.2 多线程与数据同步

SLAM系统通常包含多个并行任务:前端跟踪需要高频率运行(如30Hz),后端优化和闭环检测是低频但计算密集的任务。为了让它们互不阻塞,必须采用多线程架构。

我设计了一个典型的三线程架构:

  1. 跟踪线程:主线程,负责读取图像、特征提取、运动估计,并决定是否插入关键帧。这是实时性要求最高的线程。
  2. 局部建图线程:负责处理新的关键帧,进行局部地图点三角化、局部Bundle Adjustment优化,并剔除冗余地图点。
  3. 闭环检测线程:负责在后台进行词袋检索、闭环假设验证和全局优化。

线程间的数据同步是难点。我主要使用以下机制:

  • 共享数据加锁:对于关键帧队列、全局地图点云等共享资源,使用std::mutex进行保护。但锁的粒度要尽可能小,持有锁的时间要尽可能短,避免线程长时间阻塞。
  • 生产者-消费者队列:跟踪线程生产关键帧,并将其推入一个线程安全的队列。局部建图线程作为消费者,从队列中取出关键帧进行处理。我使用std::condition_variable来实现高效的等待-通知机制。
  • 拷贝与快照:对于后端优化需要使用的数据,有时直接传递一份拷贝(快照)给优化线程更安全,可以避免优化过程中数据被前端线程修改导致的竞态条件。虽然增加了拷贝开销,但简化了并发逻辑。

3.3 数学库与性能优化

SLAM算法底层是大量的线性代数运算。选择一个高效且易用的数学库至关重要。我选择了Eigen库,因为它纯头文件、接口优雅、且运算经过高度优化。

  1. Eigen的使用技巧

    • 固定尺寸矩阵(如Eigen::Matrix3d,Eigen::Vector2d)在编译时确定大小,性能远高于动态矩阵,应优先使用。
    • 利用Eigen的Map功能,可以直接将现有的内存块(如OpenCV的cv::Mat数据)映射为Eigen矩阵,避免不必要的拷贝。
    • 注意Eigen对象的对齐问题,特别是在STL容器中存储Eigen固定大小对象时,需要使用Eigen::aligned_allocator
  2. 性能热点分析与优化

    • 特征匹配:这是前端的性能瓶颈。除了使用汉明距离和FLANN,我还将描述子数据按内存对齐存储,并利用CPU的SIMD指令集(如SSE, AVX2)进行并行化距离计算,带来了数倍的加速。
    • 矩阵运算:确保Eigen使用了正确的编译优化标志(如-march=native以启用本地CPU的所有指令集)。
    • 内存分配:频繁创建销毁小对象(如特征点)会带来堆内存分配开销。我实现了一个简单的对象池(Object Pool)用于管理FeatureMapPoint对象,显著减少了动态内存分配的次数。

4. 关键模块的代码级实现解析

让我们深入到几个核心模块,看看具体的代码是如何组织的。这里以特征跟踪和局部BA优化为例。

4.1 特征跟踪器的实现

特征跟踪器是前端的核心,它封装了从图像到位姿估计的完整流程。我将其设计为一个类FeatureTracker

class FeatureTracker { public: struct Config { std::string feature_type = “ORB”; int num_features = 1000; int pyramid_level = 8; float scale_factor = 1.2f; // ... 其他参数 }; FeatureTracker(const Config& config); bool track(const cv::Mat& prev_image, const cv::Mat& curr_image, Sophus::SE3d& pose_estimate, // 输出位姿 std::vector<cv::Point2f>& tracked_points); // 输出跟踪到的点 private: std::unique_ptr<FeatureExtractor> extractor_; // 策略模式:特征提取器 std::unique_ptr<DescriptorMatcher> matcher_; // 策略模式:特征匹配器 cv::Ptr<cv::CLAHE> clahe_; // 用于光照均衡化的CLAHE // ... 其他成员,如上一帧数据、相机内参等 // 内部方法 void extractAndDescribe(const cv::Mat& image, std::vector<KeyPoint>& kps, cv::Mat& descs); void matchFeatures(const cv::Mat& desc1, const cv::Mat& desc2, std::vector<Match>& matches); void rejectOutliersRANSAC(const std::vector<cv::Point2f>& pts1, const std::vector<cv::Point2f>& pts2, std::vector<uchar>& status); bool estimateMotion(const std::vector<cv::Point2f>& pts1, const std::vector<cv::Point2f>& pts2, Sophus::SE3d& pose); };

track方法的内部流程清晰地反映了前端VO的步骤:

  1. 对当前帧图像进行预处理(如直方图均衡化)。
  2. 调用extractAndDescribe提取特征点和描述子。
  3. 调用matchFeatures与上一帧特征进行匹配。
  4. 调用rejectOutliersRANSAC使用RANSAC剔除误匹配。
  5. 调用estimateMotion从筛选后的匹配点对中,通过对极几何或PnP求解相机运动。
  6. 根据跟踪到的点数量、运动估计的置信度等判断本次跟踪是否成功。

4.2 局部Bundle Adjustment的实现

局部BA优化发生在局部建图线程中,它优化一个共视窗口内的关键帧和它们观测到的所有地图点。我使用g2o库来构建和求解这个优化问题。

void LocalMapping::localBundleAdjustment(const std::vector<std::shared_ptr<KeyFrame>>& local_keyframes, const std::vector<std::shared_ptr<MapPoint>>& local_mappoints) { // 1. 构建优化器 g2o::SparseOptimizer optimizer; optimizer.setVerbose(false); // 选择LM算法求解器 std::unique_ptr<g2o::BlockSolver_6_3::LinearSolverType> linearSolver( new g2o::LinearSolverEigen<g2o::BlockSolver_6_3::PoseMatrixType>()); std::unique_ptr<g2o::BlockSolver_6_3> blockSolver( new g2o::BlockSolver_6_3(std::move(linearSolver))); g2o::OptimizationAlgorithmLevenberg* algorithm = new g2o::OptimizationAlgorithmLevenberg(std::move(blockSolver)); optimizer.setAlgorithm(algorithm); // 2. 添加顶点:局部关键帧的位姿 std::map<std::shared_ptr<KeyFrame>, g2o::VertexSE3Expmap*> vertex_pose_map; int max_kf_id = 0; for (const auto& kf : local_keyframes) { auto* v = new g2o::VertexSE3Expmap(); v->setId(kf->id_); v->setEstimate(kf->getPose()); // SE3位姿 // 固定第一帧(或参考帧)不动,提供尺度基准 if (kf == local_keyframes[0]) { v->setFixed(true); } optimizer.addVertex(v); vertex_pose_map[kf] = v; max_kf_id = std::max(max_kf_id, kf->id_); } // 3. 添加顶点:局部地图点的位置 std::map<std::shared_ptr<MapPoint>, g2o::VertexSBAPointXYZ*> vertex_point_map; for (const auto& mp : local_mappoints) { auto* v = new g2o::VertexSBAPointXYZ(); int vid = mp->id_ + max_kf_id + 1; // 确保ID不与关键帧冲突 v->setId(vid); v->setEstimate(mp->getPosition()); // 3D坐标 v->setMarginalized(true); // 点云变量需要被边缘化,以加速求解 optimizer.addVertex(v); vertex_point_map[mp] = v; } // 4. 添加边:重投影误差 const float huber_threshold = sqrt(5.991); // 卡方检验,95%置信度 for (const auto& kf : local_keyframes) { const auto& observations = kf->getMapPointObservations(); for (const auto& obs : observations) { auto mp = obs.first.lock(); // weak_ptr 转 shared_ptr if (!mp || vertex_point_map.find(mp) == vertex_point_map.end()) continue; const cv::Point2f& pixel = obs.second; auto* edge = new g2o::EdgeProjectXYZ2UV(); edge->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(vertex_point_map[mp])); edge->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(vertex_pose_map[kf])); edge->setMeasurement(Eigen::Vector2d(pixel.x, pixel.y)); edge->setInformation(Eigen::Matrix2d::Identity()); // 信息矩阵 // 使用鲁棒核函数Huber,降低外点影响 auto* huber = new g2o::RobustKernelHuber; huber->setDelta(huber_threshold); edge->setRobustKernel(huber); // 设置相机内参 edge->fx = camera_->fx_; edge->fy = camera_->fy_; edge->cx = camera_->cx_; edge->cy = camera_->cy_; optimizer.addEdge(edge); } } // 5. 执行优化 optimizer.initializeOptimization(); optimizer.optimize(10); // 迭代10次 // 6. 更新数据:将优化后的值写回关键帧和地图点 for (auto& kv : vertex_pose_map) { kv.first->setPose(kv.second->estimate()); } for (auto& kv : vertex_point_map) { kv.first->setPosition(kv.second->estimate()); } }

这段代码清晰地展示了图优化的构建过程:创建优化器与求解器 -> 添加位姿和地图点顶点 -> 添加基于观测的重投影误差边 -> 执行迭代优化 -> 回写优化结果。其中,将地图点顶点设置为setMarginalized(true)是图优化中的关键技巧,它利用舒尔消元(Schur Elimination)在求解时先消去地图点变量,大幅减少了计算量。

5. 系统集成、调试与性能评估

将各个模块集成在一起,并让整个系统稳定运行,是项目最后也是最考验人的阶段。这里分享一些集成调试的经验和性能评估方法。

5.1 数据流与系统集成

我设计了一个主类VisualSLAMSystem作为系统的总控中心。它持有跟踪器、局部建图器、闭环检测器、地图等所有核心组件的实例,并管理着它们之间的数据流和线程生命周期。

class VisualSLAMSystem { public: enum SystemStatus { INITIALIZING, TRACKING, LOST, RESETTING }; VisualSLAMSystem(const std::string& config_file); ~VisualSLAMSystem(); // 主接口:输入一帧图像和时间戳 void feedImage(const cv::Mat& image, double timestamp); // 获取当前状态 SystemStatus getStatus() const; Sophus::SE3d getCurrentPose() const; std::shared_ptr<Map> getMap() const; private: // 核心组件 std::unique_ptr<FeatureTracker> tracker_; std::unique_ptr<LocalMapper> local_mapper_; std::unique_ptr<LoopCloser> loop_closer_; std::shared_ptr<Map> map_; // 线程与同步 std::thread local_mapping_thread_; std::thread loop_closing_thread_; std::mutex mutex_state_; std::condition_variable cond_kf_queue_; // 状态与数据 SystemStatus state_; std::queue<std::shared_ptr<KeyFrame>> keyframe_queue_; // ... };

feedImage是系统的主循环,它在跟踪线程中运行:

  1. 根据当前state_(初始化、跟踪、丢失)调用跟踪器相应的处理函数。
  2. 如果跟踪成功且生成了新的关键帧,则将关键帧放入keyframe_queue_,并通知局部建图线程。
  3. 局部建图线程被唤醒,从队列中取出关键帧进行处理(三角化、局部BA)。
  4. 闭环检测线程定期检查是否有闭环候选,并进行验证和全局优化。 整个流程通过互斥锁和条件变量进行同步,确保数据一致性。

5.2 调试技巧与可视化

SLAM系统内部状态复杂,纯靠打印日志很难调试。强大的可视化工具至关重要。

  1. 轨迹与地图可视化:我使用Pangolin库实时绘制相机轨迹(位姿连线)和三维地图点云。这能最直观地反映系统运行状态:轨迹是否平滑?地图点是否收敛?闭环时轨迹是否被正确校正?
  2. 关键帧与特征点可视化:使用OpenCV的绘图功能,将当前帧图像、提取的特征点、匹配关系、跟踪状态等实时显示出来。这有助于调试前端跟踪的质量。
  3. 性能剖析:使用std::chrono在代码关键部位打点,统计各模块(特征提取、匹配、优化)的耗时,找出性能瓶颈。也可以使用更专业的工具如 gprof 或 Valgrind 的 Callgrind。
  4. 数据集测试:使用公开的SLAM数据集(如KITTI, EuRoC, TUM RGB-D)进行测试。这些数据集提供了真实场景的图像、真值轨迹和传感器数据,是评估算法性能的黄金标准。将自己的轨迹与真值轨迹对齐后,可以计算绝对轨迹误差(ATE)和相对位姿误差(RPE)来定量评估精度。

5.3 常见问题与排查实录

在开发过程中,我遇到了无数个“坑”。这里列举几个典型问题及其解决方案:

  1. 问题:轨迹运行一段时间后突然发散。

    • 排查:首先检查可视化,看是突然跳变还是逐渐漂移。如果是跳变,可能是某一帧跟踪严重失败(特征点极少或误匹配极多),导致了一个错误的位姿估计。
    • 解决:在前端增加更严格的跟踪质量检查。例如,要求RANSAC后的内点数必须大于一个阈值(如50),并且内点比例要高于一定比例(如0.6)。如果检查不通过,则标记本帧跟踪失败,尝试与更早的关键帧进行重定位,而不是使用错误的运动估计。
  2. 问题:闭环检测频繁误触发,导致地图被“拉歪”。

    • 排查:检查词袋模型的相似度分数阈值是否设置过低。更重要的是,检查几何验证步骤是否足够严格。
    • 解决:提高闭环检测的置信度要求。除了词袋相似度,我增加了两个条件:a) 候选帧与当前帧的共视地图点数量必须足够多;b) 通过Sim3变换求解和优化得到的尺度因子必须在合理范围内(如0.8~1.2)。只有同时满足多个条件,才确认闭环。
  3. 问题:后端优化速度越来越慢,最终卡死。

    • 排查:这是图优化中常见的问题。随着关键帧和地图点增多,优化图的规模线性增长,计算量呈平方甚至立方增长。
    • 解决:实施“滑动窗口”优化。只对最近N个关键帧及其观测到的地图点进行局部BA优化,而将更早的关键帧固定住或边缘化掉。同时,定期进行地图点剔除,删除那些质量差、观测少的点,保持图的稀疏性。
  4. 问题:系统在纹理缺失的区域(如白墙)跟踪失败。

    • 排查:这是基于特征点法的固有弱点。在纹理少的区域,提取不到足够多且稳定的特征点。
    • 解决:这是一个更根本的问题。除了尝试调整特征提取参数(如降低FAST角点阈值),更鲁棒的方案是引入其他线索。例如,可以融合IMU数据,在视觉信息失效时提供短时间的运动预测;或者考虑使用直接法(如LSD-SLAM, DSO),它们对纹理的依赖性相对较低。在我的实现中,我暂时通过IMU融合模块来缓解这个问题。

实现一个完整的视觉SLAM系统是一次漫长而充实的旅程。它强迫你从最底层的图像处理、几何计算,到中间层的状态估计、优化理论,再到上层的系统架构、并发编程,进行全方位的思考与实践。最终,当看到自己编写的程序能够驱动虚拟的或真实的相机,在未知环境中一步步构建出清晰的地图并规划出准确的轨迹时,那种成就感是无与伦比的。这个项目留下的不仅仅是一套可运行的代码,更是一套处理复杂感知与估计问题的思维框架和工程方法论,这对于从事任何相关的机器人、自动驾驶或三维视觉项目,都是极为宝贵的财富。