DynaSLAM 代码分析

Share:
[toc] # 目标: - 理解DynaSLAM的主程序流程 - 理解DynaSLAM的核心算法 DynaSLAM Youtube demo: https://www.youtube.com/embed/aiqLlyiriXI # 程序流程分析 ## 主程序流程 入口程序:rgbd_tum.cc - 读取数据集 LoadImages - 创建实例分割的实例 SegmentDynObject SegmentDynObject 这个类是负责实例分割的。MaskRCNN是基于Python开发的,在此用CPP来调用Python接口。由于ROS是只支持Python2.7的,所以此处的MaskRCNN是使用的Python2. 在类中,最关键的函数是: ```cpp cv::Mat GetSegmentation(cv::Mat &image, std::string dir = "no_save", std::string rgb_name = "no_file"); ``` - 初使化SLAM (RGB-D mode) ```cpp ORB_SLAM2::System SLAM(argv[1], argv[2], ORB_SLAM2::System::RGBD, true); ``` - 设置中间结果保存路径, 结果将保存到 rgb, depth, mask, label 文件夹中 - 开始主循环,遂帧读取图像 (nImages 为数据集中图像的个数): ```cpp for (int ni = 0; ni < nImages; ni++) { //LOOP } ``` - 形态学滤波, 设置核函数 ```cpp // Dilation settings int dilation_size = 15; cv::Mat kernel = getStructuringElement( cv::MORPH_ELLIPSE, cv::Size(2 * dilation_size + 1, 2 * dilation_size + 1), cv::Point(dilation_size, dilation_size)); ``` - 实例分割 ```cpp cv::Mat maskRCNN; maskRCNN = MaskNet->GetSegmentation(imRGB, string(argv[5]), vstrImageFilenamesRGB[ni].replace(0, 4, "")); cv::Mat maskRCNNdil = maskRCNN.clone(); cv::dilate(maskRCNN, maskRCNNdil, kernel); mask = mask - maskRCNNdil; //inverse ``` 得到分割后的图像maskRCNN, 进行形态学滤波, 之后求逆 (考滤:为什么要这么操作呢?) - Tracking线程 分两种情况, 一种是使用语义分割: ```cpp SLAM.TrackRGBD(imRGB, imD, mask, tframe, imRGBOut, imDOut, maskOut); ``` 另一种是不使用语义分割,全依靠几何信息: ```cpp SLAM.TrackRGBD(imRGB, imD, mask, tframe); ``` 考滤:第二种情况与传统的ORB SLAM相比有何异同? - 收尾工作 - 计算时间消耗 - 保存轨迹 ```cpp SLAM.SaveTrajectoryTUM("CameraTrajectory.txt"); SLAM.SaveKeyFrameTrajectoryTUM("KeyFrameTrajectory.txt"); ``` ## Tracking分析 - TrackRGBD的跟踪主函数入口 Tracking是一个单独的线程。 ```cpp cv::Mat System::TrackRGBD(const cv::Mat &im, const cv::Mat &depthmap, cv::Mat &mask, const double ×tamp, cv::Mat &imRGBOut, cv::Mat &imDOut, cv::Mat &maskOut) ``` 输入参数有四个:RGB图像,Depth图像,Mask 语义Label, 时间戳。 输出有三个:处理后的RGB图像,处理后的深度图像,处理后的Mask 。 考虑:进行了哪些处理? - GrabImageRGBD (Tracking.cc) 在TrackRGBD中最主要的函数就是GrabImageRGBD。参数与上面相同,程序返回当前帧的位姿$Tcw$. ```cpp cv::Mat Tracking::GrabImageRGBD(const cv::Mat &imRGB, const cv::Mat &imD, cv::Mat &mask, const double ×tamp, cv::Mat &imRGBOut, cv::Mat &imDOut, cv::Mat &maskOut) ``` - 将RGB图像转为灰度图像 ```cpp cvtColor(mImGray,mImGray,CV_BGR2GRAY); ``` - 对深度图像进行预处理 ```cpp if((fabs(mDepthMapFactor-1.0f)>1e-5) || imDepth.type()!=CV_32F) imDepth.convertTo(imDepth,CV_32F,mDepthMapFactor); ``` convertTo的原型为:https://docs.opencv.org/trunk/d3/d63/classcv_1_1Mat.html ```cpp void cv::Mat::convertTo (OutputArray m, int rtype, double alpha = 1, double beta = 0 ) const ``` m(x,y)=saturate_cast(α(∗this)(x,y)+β) 以前没注意到居然有这么个函数可以直接解决depth facter转换的问题。 - 构造帧 ```cpp mCurrentFrame = Frame(mImGray,imDepth,imMask,_imRGB,timestamp, mpORBextractorLeft,mpORBVocabulary,mK,mDistCoef,mbf,mThDepth); ``` 原型为:(Frame.cc) ```cpp Frame::Frame(const cv::Mat &imGray, const cv::Mat &imDepth, const cv::Mat &imMask, const cv::Mat &imRGB, const double &timeStamp, ORBextractor* extractor, ORBVocabulary* voc, cv::Mat &K, cv::Mat &distCoef, const float &bf, const float &thDepth) ``` 要注意,构造函数有好多个版本,要注意实际使用的是哪个版本。 - 轻量级的跟踪 具体做了什么,后面再看。 ```cpp LightTrack(); ``` - 如果LightTrack成功, 使用几何模型来进行修正 考虑:如何修正,几何模型做了些什么工作? ```cpp if (!mCurrentFrame.mTcw.empty()) { mGeometry.GeometricModelCorrection(mCurrentFrame,imDepth,imMask); } ``` - 重新构造帧 ```cpp imRGBOut = _imRGB; mCurrentFrame = Frame(mImGray,imDepth,imMask,imRGBOut, timestamp,mpORBextractorLeft,mpORBVocabulary,mK,mDistCoef,mbf,mThDepth); ``` 考虑:为什么要重新构造帧呢? 仔细观察会发现 第四个参数发生了变化。 - Track ```cpp Track(); ``` 这里才是进行正式的跟踪算法,那么之前的LightTrack有什么用呢? - 跟踪成功后对图像进行Inpaint (不知如何翻译) 因为去掉了动态物体之后,会留下很多的黑洞,所以要修补图像。 ```cpp if (!mCurrentFrame.mTcw.empty()) { mGeometry.InpaintFrames(mCurrentFrame, mImGray, imDepth, imRGBOut, imMask); } ``` - 将当前帧加入到几何模块的数据库中 ```观察家「 mGeometry.GeometricModelUpdateDB(mCurrentFrame); ``` 函数原型为:(Geometry.cc) ```cpp void Geometry::GeometricModelUpdateDB(const ORB_SLAM2::Frame ¤tFrame){ if (currentFrame.mIsKeyFrame) { mDB.InsertFrame2DB(currentFrame); } } ``` - 返回结果 ```cpp imDOut = imDepth; imDepth.convertTo(imDOut,CV_16U,1./mDepthMapFactor); maskOut = imMask; return mCurrentFrame.mTcw.clone(); ``` 分析到这里,我们遇到了非常多的疑问: - 为什么两次构建帧结构? - LightTrack 是什么? - Track 与传统的ORB SLAM相比有何异同? - 几何模块的: GeometricModelCorrection、InpaintFrames、GeometricModelUpdateDB是什么? --- # 语义模块分析 ## 构造帧 - 函数定义 ```cpp Frame::Frame(const cv::Mat &imGray, const cv::Mat &imDepth, const cv::Mat &imMask, const cv::Mat &imRGB, const double &timeStamp, ORBextractor* extractor, ORBVocabulary* voc, cv::Mat &K, cv::Mat &distCoef, const float &bf, const float &thDepth) ``` - 提取ORB特征 ```cpp ExtractORB(0,imGray); ``` - 对Mask 语义标签图像进行滤波 ```cpp // Delete those ORB points that fall in Mask borders (Included by Berta) cv::Mat Mask_dil = imMask.clone(); int dilation_size = 15; cv::Mat kernel = getStructuringElement(cv::MORPH_ELLIPSE, cv::Size( 2*dilation_size + 1, 2*dilation_size+1 ), cv::Point( dilation_size, dilation_size ) ); cv::erode(imMask, Mask_dil, kernel); ``` 还记得在主程序中,对mask进行了一次dilate操作。 - 去掉人身上的ORB 特征点 ```cpp std::vector _mvKeys; cv::Mat _mDescriptors; for (size_t i(0); i < mvKeys.size(); ++i) { int val = (int)Mask_dil.at(mvKeys[i].pt.y,mvKeys[i].pt.x); if (val == 1) { _mvKeys.push_back(mvKeys[i]); _mDescriptors.push_back(mDescriptors.row(i)); } } mvKeys = _mvKeys; mDescriptors = _mDescriptors; ``` 到这里我们才开始真正走进了DynaSLAM的算法学习部分。之前的基本上都是ORB SLAM2中的东西 。 要理解此处的操作,先要弄清楚最初的mask长的是什么样的,查阅MaskNet.cc发现, 它就是一个0,1图像。 ```sh seg.cv::Mat::convertTo(seg, CV_8U); //0 background y 1 foreground ``` 主程序中在形态学滤波后对其进行了取反操作: ```cpp cv::Mat mask = cv::Mat::ones(480, 640, CV_8U); cv::Mat maskRCNN; maskRCNN = MaskNet->GetSegmentation(imRGB, string(argv[5]), vstrImageFilenamesRGB[ni].replace(0, 4, "")); cv::Mat maskRCNNdil = maskRCNN.clone(); cv::dilate(maskRCNN, maskRCNNdil, kernel); mask = mask - maskRCNNdil; //inverse ``` 那就变成了: 1 为背景,0为前景。又因为此处进行了一次dilate操作,所以现在要进行erode操作来还原(起到了滤波的效果),相当于进行了一次开(Opening)操作。在背影图像中的前景小洞就被滤除了。 之前的dilate 膨胀操作用来去除边缘上的特征点。 之后把前景中的特征点去掉就可以了。 这就是去除人身上特征点的过程,仅此而已。 --- ## LightTrack 这里面最重要的是这个函数: ```cpp bool Tracking::LightTrackWithMotionModel(bool &bVO) ``` 如果未初始化或者没有图像数据则不执行LightTrack, 也就意味着程序刚开始运行时,是不会执行的。 ```cpp if(mState==NOT_INITIALIZED || mState==NO_IMAGES_YET) { cout << "Light Tracking not working because Tracking is not initialized..." << endl; return; } ``` 下面与ORB SLAM类似。让我们来看关键的LightTrackWithMotionModel函数。 这个函数是原版本的一个变体: ```cpp bool Tracking::TrackWithMotionModel() ``` 对照这两个函数来看差异。但是我几乎没看到有多大区别,除了: ```cpp Frame lastFrameBU = mLastFrame; list lpTemporalPointsBU = mlpTemporalPoints; ... mLastFrame = lastFrameBU; mlpTemporalPoints = lpTemporalPointsBU; ``` 似乎只是增加了对现场的保存工作。 在重新构建Frame之后进行传统的Track. Track部分,作者未做改动 ```cpp void Tracking::Track() ``` 那么之前的LightTrack起到了什么作用? 我觉得: 1)可能更新了Local Map (论文中图2所示的结构中没有对Local Map进行写入操作,这个猜想可能是错误的,有待验证) 2)可能优化了位姿 因为是去除了大部分动态的特征点,所以误差会减小。 # 几何模块分析 ## 简介 代码在 Geometry.h/cpp 中。 虽然通过语义图像分割可以去除掉大部分动态特征点,但这些只是基于先验知识。对于先验知识以外的动态物体,可以通过几何模块将这些动态物体上的特征点去除掉。 设计了动态关键点类: ```cpp class DynKeyPoint { public: cv::Point2i mPoint; int mRefFrameLabel; }; ``` 设计了一个数据库类: ```cpp class DataBase { public: vector mvDataBase = vector(MAX_DB_SIZE); int mIni=0; int mFin=0; int mNumElem = 0; bool IsFull(); void InsertFrame2DB(const ORB_SLAM2::Frame ¤tFrame); }; ``` 比较关键的是这几个函数: ```cpp void GeometricModelCorrection(const ORB_SLAM2::Frame ¤tFrame, cv::Mat &imDepth, cv::Mat &mask); void InpaintFrames(const ORB_SLAM2::Frame ¤tFrame, cv::Mat &imGray, cv::Mat &imDepth, cv::Mat &imRGB, cv::Mat &mask); void GeometricModelUpdateDB(const ORB_SLAM2::Frame &mCurrentFrame); ``` ## GeometricModelUpdateDB ```cpp void Geometry::GeometricModelUpdateDB(const ORB_SLAM2::Frame ¤tFrame){ if (currentFrame.mIsKeyFrame) { mDB.InsertFrame2DB(currentFrame); } } ``` 这一步就是将关键帧插入到几何数据库之中 ```cpp void Geometry::DataBase::InsertFrame2DB(const ORB_SLAM2::Frame ¤tFrame){ if (!IsFull()){ mvDataBase[mFin] = currentFrame; mFin = (mFin + 1) % MAX_DB_SIZE; mNumElem += 1; } else { mvDataBase[mIni] = currentFrame; mFin = mIni; mIni = (mIni + 1) % MAX_DB_SIZE; } } ``` 猜测一下这是用的什么数据结构?else后面的总感觉不大对劲, mIni比mFin要超前一步,这样不就一直都是Full了。 ```cpp #define MAX_DB_SIZE 20 bool Geometry::DataBase::IsFull(){ return (mIni == (mFin+1) % MAX_DB_SIZE); } ``` 后面那一句是不是应该改成: ```cpp mFin = (mIni + 1) % MAX_DB_SIZE; ``` 感觉mFin应该是队尾指针,从队尾插入新的数据。mIni应该是队头指针,初始值为0. 暂且这么理解吧。 ## GeometricModelCorrection ```cpp void Geometry::GeometricModelCorrection(const ORB_SLAM2::Frame ¤tFrame, cv::Mat &imDepth, cv::Mat &mask){ if(currentFrame.mTcw.empty()){ std::cout << "Geometry not working." << std::endl; } else if (mDB.mNumElem >= ELEM_INITIAL_MAP){ vector vRefFrames = GetRefFrames(currentFrame); vector vDynPoints = ExtractDynPoints(vRefFrames,currentFrame); mask = DepthRegionGrowing(vDynPoints,imDepth); CombineMasks(currentFrame,mask); } } ``` 输入参数: 当前帧, 深度图 与 掩码图像 如果当前帧跟踪失败的话,就退出 。所以当我们看到 ...not working的提示时,还以为我们的开发环境没编译好,其实不是的,这是正常现像。 ```cpp if(currentFrame.mTcw.empty()){ std::cout << "Geometry not working." << std::endl; } ``` 如果数据库中的项目数大于最低要求5. 那么就继续。 - 获得当前帧的参考帧 - 提取动态特征点 - 通过深度区域增长算法来生成mask - 将生成的mask整合到当前帧中 ## GetRefFrames ```cpp vector Geometry::GetRefFrames(const ORB_SLAM2::Frame ¤tFrame) ``` 从几何模块数据库中去搜索参考帧,返回的是一个数组,所以说找到的参考帧可能有多个。 大体方法是比较平移与旋转的二范数距离差,位姿相近的帧看作是参考帧。 TODO: 细节暂且略过,因为不妨碍继续阅读。 ## ExtractDynPoints ```cpp vector Geometry::ExtractDynPoints(const vector &vRefFrames, const ORB_SLAM2::Frame ¤tFrame) ``` 这段程序好长啊。 ```cpp mDepthThreshold = 0.6; cv::Mat matDepthDifference = matProjDepth - matDepthCurrentFrame; for (int i(0); i < matCurrentFrame.rows; i++) { if (matDepthDifference.at(i,0) > mDepthThreshold) {} } ``` 动态点的判定条件: mDepthThreshold matDepthDifference: 投影深度$z_{proj}$与当前帧中的观测得到的深度$z\prime$之差。对应论文中的图3. A pixel is labeled as dynamic if the difference $\Delta z = z_{proj} - z \prime$ is greater than a threshold $\tau_z$.

No comments