转载请注明出处:http://blog.csdn.net/c602273091/article/details/54955663
在mono_kitti.cc中运行kitti数据集的时候,初始化完了system之后,就出现了并行的多个线程:追踪、局部地图构建、闭环检测、地图显示等等。这个时候我们就需要喂入数据给整个系统,所以就是:
SLAM.TrackMonocular(im,tframe);
把传感器采集的图片以及时间戳传入,我们就可以更新系统的状态,获取新的数据,更新地图。
对TrackMonocular进行解释,具体如下:
cv::Mat System::TrackMonocular(const cv::Mat &im, const double ×tamp)
{
// 传感器不是单目摄像头、退出
if(mSensor!=MONOCULAR)
{
cerr << "ERROR: you called TrackMonocular but input sensor was not set to Monocular." << endl;
exit(-1);
}
// Check mode change
// 这一部分主要是对局部地图线程进行操作.
// mbActivateLocalizationMode是是否停止局部地图线程
// mbDeactivateLocalizationMode是是否清空局部地图.
{
// 独占锁,主要是为了mbActivateLocalizationMode和mbDeactivateLocalizationMode
// 不会发生混乱,没有死锁或者在临界区
unique_lock<mutex> lock(mMutexMode);
// mbActivateLocalizationMode为true会关闭局部地图线程
if(mbActivateLocalizationMode)
{
mpLocalMapper->RequestStop();
// 设置local map的mbStopRequested,mbAbortBA为true.
// 当这两个为true的时候,那么进行就会去关闭局部地图的线程
// Wait until Local Mapping has effectively stopped
// mbStopped为true,说明局部地图线程已经关闭了
while(!mpLocalMapper->isStopped())
{
usleep(1000);
}
// 局部地图关闭以后,只进行追踪的线程
// 只计算相机的位姿,没有对局部地图进行更新
// 设置mbOnlyTracking为真
mpTracker->InformOnlyTracking(true);
// 执行完当前的部分之和把mbActivateLocalizationMode再置回false.
// 当然这里设置mbActivateLocalizationMode为true的部分应该是没有新的关键帧和点云的时候
// 关闭线程可以使得别的线程得到更多的资源
mbActivateLocalizationMode = false;
}
// 如果mbDeactivateLocalizationMode是true
// 设置mbActivateLocalizationMode为false
// 局部地图线程就被释放, 关键帧从局部地图中删除.
// mbStopped和mbStopRequested被置为false.
if(mbDeactivateLocalizationMode)
{
mpTracker->InformOnlyTracking(false);
mpLocalMapper->Release();
mbDeactivateLocalizationMode = false;
}
}
// Check reset
// 检查是否需要进行复位重置.
{
// 给mbReset加锁,防止被别的线程修改
unique_lock<mutex> lock(mMutexReset);
if(mbReset)
{
// mpViwer暂停,视图停止更新
// 局部地图:mpLocalMapper和闭环检测:mpLoopClosing被停止.
// Bow:mpKeyFrameDB和mpMap被清空
// 就是把所有资源释放
mpTracker->Reset();
mbReset = false;
}
}
// 可以看出上面这两部分都是对于各个线程状态的反馈.
// 其实可以看做是对上一个状态的反馈.
// 接下来的部分才是最重要的部分,获取数据,对各个线程的数据进行更新, 对地图重新进行计算.
return mpTracker->GrabImageMonocular(im,timestamp);
}
这部分是上一部分的核心。对GrabImageMonocular的解释如下:
cv::Mat Tracking::GrabImageMonocular(const cv::Mat &im, const double ×tamp)
{
// 把输入的图片设为当前帧.
mImGray = im;
// 若图片是三\四通道的,还需要转化为灰度图.
if(mImGray.channels()==3)
{
if(mbRGB)
cvtColor(mImGray,mImGray,CV_RGB2GRAY);
else
cvtColor(mImGray,mImGray,CV_BGR2GRAY);
}
else if(mImGray.channels()==4)
{
if(mbRGB)
cvtColor(mImGray,mImGray,CV_RGBA2GRAY);
else
cvtColor(mImGray,mImGray,CV_BGRA2GRAY);
}
// 若状态是未初始化或者当前获取的图片是第一帧的时候,那么就需要进行初始化.
// 传入的参数就是当前帧(灰度图)、时间戳、ORBextractor(ORB特征)、DBoW2的字典、标定矩阵、畸变参数、立体视觉的(baseline)x(fx)、可靠点的距离的阈值
// mCurrentFrame就是对当前帧进行处理. 获取ORB特征、获取尺度信息.
// 这里需要进行更加深入的研究.
if(mState==NOT_INITIALIZED || mState==NO_IMAGES_YET)
mCurrentFrame = Frame(mImGray,timestamp,mpIniORBextractor,mpORBVocabulary,mK,mDistCoef,mbf,mThDepth);
else
mCurrentFrame = Frame(mImGray,timestamp,mpORBextractorLeft,mpORBVocabulary,mK,mDistCoef,mbf,mThDepth);
// 追踪,初始化以及特征匹配.
// 这里需要更加深入的研究.
Track();
// mTcw是位姿
// clone是cv::Mat特有的方法.
// 实现的就是完全拷贝, 把数据完全拷贝而不共享数据.
return mCurrentFrame.mTcw.clone();
}
对于这一部分,需要特别研究一下:
// 若状态是未初始化或者当前获取的图片是第一帧的时候,那么就需要进行初始化.
// 传入的参数就是当前帧(灰度图)、时间戳、ORBextractor(ORB特征)、DBoW2的字典、标定矩阵、畸变参数、立体视觉的(baseline)x(fx)、可靠点的距离的阈值
// mCurrentFrame就是对当前帧进行处理. 获取ORB特征、获取尺度信息.
// 这里需要进行更加深入的研究.
if(mState==NOT_INITIALIZED || mState==NO_IMAGES_YET)
mCurrentFrame = Frame(mImGray,timestamp,mpIniORBextractor,mpORBVocabulary,mK,mDistCoef,mbf,mThDepth);
else
mCurrentFrame = Frame(mImGray,timestamp,mpORBextractorLeft,mpORBVocabulary,mK,mDistCoef,mbf,mThDepth);
整个Frame的流程图如上所示,比较清晰。
对Frame部分进行注释得到:
Frame::Frame(const cv::Mat &imGray, const double &timeStamp, ORBextractor* extractor,ORBVocabulary* voc, cv::Mat &K, cv::Mat &distCoef, const float &bf, const float &thDepth)
:mpORBvocabulary(voc),mpORBextractorLeft(extractor),mpORBextractorRight(static_cast(NULL)),
mTimeStamp(timeStamp), mK(K.clone()),mDistCoef(distCoef.clone()), mbf(bf), mThDepth(thDepth)
{
// Frame ID
// 当前帧的ID
// 每一帧都有唯一的一个ID做标识
// 从0开始
mnId=nNextId++;
// Scale Level Info
// 获取尺度信息,目前还不了解这些是做什么的?
// 这些是属于Frame的变量.
mnScaleLevels = mpORBextractorLeft->GetLevels();
mfScaleFactor = mpORBextractorLeft->GetScaleFactor();
mfLogScaleFactor = log(mfScaleFactor);
mvScaleFactors = mpORBextractorLeft->GetScaleFactors();
mvInvScaleFactors = mpORBextractorLeft->GetInverseScaleFactors();
mvLevelSigma2 = mpORBextractorLeft->GetScaleSigmaSquares();
mvInvLevelSigma2 = mpORBextractorLeft->GetInverseScaleSigmaSquares();
// ORB extraction
// 提取ORB特征.
// 可以继续研究.
ExtractORB(0,imGray);
// KeyPoint (float x, float y, float _size, float _angle=-1, float _response=0, int _octave=0, int _class_id=-1)
// 进行ORB特征提取以后可以得到关键点的数量. 存储在关键点的向量内.
N = mvKeys.size();
// 没有找到关键点,返回.
if(mvKeys.empty())
return;
// 进行畸变校正,找到关键点实际应该在普通摄像头中的位置.
UndistortKeyPoints();
// Set no stereo information
// 把立体信息部分设置为-1.
mvuRight = vector<float>(N,-1);
mvDepth = vector<float>(N,-1);
// 初始化点和各个点是否是outlier的状态.
mvpMapPoints = vector (N,static_cast(NULL));
mvbOutlier = vector<bool>(N,false);
// This is done only for the first Frame (or after a change in the calibration)
// 第一帧或者是标定矩阵发生了变化以后
if(mbInitialComputations)
{
// 计算畸变矫正之后的边界.
ComputeImageBounds(imGray);
// mnMax(Min)X(Y)是畸变矫正以后的边界.
mfGridElementWidthInv=static_cast<float>(FRAME_GRID_COLS)/static_cast<float>(mnMaxX-mnMinX);
mfGridElementHeightInv=static_cast<float>(FRAME_GRID_ROWS)/static_cast<float>(mnMaxY-mnMinY);
// 从配置文件中读取数据赋给相应元素.
fx = K.at<float>(0,0);
fy = K.at<float>(1,1);
cx = K.at<float>(0,2);
cy = K.at<float>(1,2);
invfx = 1.0f/fx;
invfy = 1.0f/fy;
mbInitialComputations=false;
}
// Stereo baseline in meters.
// 计算立体匹配的时候的baseline.
// mbf来自与配置文件.
mb = mbf/fx;
// 把每一帧分割成48x64个网格
// 根据关键点的畸变矫正后的位置分在不同的网格里面.
AssignFeaturesToGrid();
}
ExtractORB(0,imGray);
这里提取的就是左边的摄像头的数据。
void Frame::ExtractORB(int flag, const cv::Mat &im)
{
if(flag==0)
(*mpORBextractorLeft)(im,cv::Mat(),mvKeys,mDescriptors);
else
(*mpORBextractorRight)(im,cv::Mat(),mvKeysRight,mDescriptorsRight);
}
这个是在ORBExtractor.h的ORBExtractor类里重载的操作符’()’: 这里忽略了mask这个变量。
void operator()( cv::InputArray image, cv::InputArray mask,
std::vector & keypoints,
cv::OutputArray descriptors);
在ORBextractor.cc里面对这个有详细的介绍。接下来对这个模块进行详细的介绍。
// 输入的变量:
// _image: 获取的图片像素信息(灰度图)
// _mask: 掩码,这个部分的位置就不需要计算描述子
// _keypoints: 关键点的位置
// _descriptors: 描述子,没有使用引用,看来是没有用到了
void ORBextractor::operator()( InputArray _image, InputArray _mask, vector & _keypoints,
OutputArray _descriptors)
{
// 如果没有获取图片,那么返回
if(_image.empty())
return;
// 获取图片信息赋给Mat类型的image
Mat image = _image.getMat();
// 判断通道是否为单通道灰度图
assert(image.type() == CV_8UC1 );
// Pre-compute the scale pyramid
// 计算尺度的金字塔,可以稍微看一眼
ComputePyramid(image);
// 保存所有的关键点
vector < vector > allKeypoints;
// 计算关键点,找到FAST关键点,值得看一下
ComputeKeyPointsOctTree(allKeypoints);
//ComputeKeyPointsOld(allKeypoints);
Mat descriptors;
int nkeypoints = 0;
for (int level = 0; level < nlevels; ++level)
nkeypoints += (int)allKeypoints[level].size();
if( nkeypoints == 0 )
_descriptors.release();
else
{
_descriptors.create(nkeypoints, 32, CV_8U);
descriptors = _descriptors.getMat();
}
_keypoints.clear();
_keypoints.reserve(nkeypoints);
int offset = 0;
for (int level = 0; level < nlevels; ++level)
{
vector & keypoints = allKeypoints[level];
int nkeypointsLevel = (int)keypoints.size();
if(nkeypointsLevel==0)
continue;
// preprocess the resized image
Mat workingMat = mvImagePyramid[level].clone();
// 使用高斯模糊为了计算BRIEF的时候去噪
GaussianBlur(workingMat, workingMat, Size(7, 7), 2, 2, BORDER_REFLECT_101);
// Compute the descriptors
// 计算rBRIEF描述子,采用的是高斯分布取点
Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
computeDescriptors(workingMat, keypoints, desc, pattern);
offset += nkeypointsLevel;
// 对关键点的位置做尺度恢复,恢复到原图的位置
// Scale keypoint coordinates
if (level != 0)
{
float scale = mvScaleFactor[level]; //getScale(level, firstLevel, scaleFactor);
for (vector ::iterator keypoint = keypoints.begin(),
keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
keypoint->pt *= scale;
}
// And add the keypoints to the output
//
_keypoints.insert(_keypoints.end(), keypoints.begin(), keypoints.end());
}
}
在这里我发现还是要复习一下ORB特征【1】【2】。看【2】会更好,因为在这里我把算法流程讲得还算清楚。
在这里的level和scalefactor我不清楚,我把它打印出来看了一下:
图片金字塔一共是8层,随着level越来越大,scale factor越来越大,图片越来越小。当恢复金字塔关键点的原图坐标的时候就把它乘以scale factor。
计算尺度金字塔。
void ORBextractor::ComputePyramid(cv::Mat image)
{
// 计算nlevel个尺度的图片
for (int level = 0; level < nlevels; ++level)
{
// 获取尺度
float scale = mvInvScaleFactor[level];
// 计算当前尺度下图片的大小
Size sz(cvRound((float)image.cols*scale), cvRound((float)image.rows*scale));
Size wholeSize(sz.width + EDGE_THRESHOLD*2, sz.height + EDGE_THRESHOLD*2);
Mat temp(wholeSize, image.type()), masktemp;
// 图片初始化
mvImagePyramid[level] = temp(Rect(EDGE_THRESHOLD, EDGE_THRESHOLD, sz.width, sz.height));
// Compute the resized image
// 计算图片金字塔在该尺度下的金字塔
if( level != 0 )
{
// 对图片进行尺度变换
// 在这里我们一般是把更清晰的图片变化更加模糊的图片
// 从这里可以看出,这里的尺度的值应该是小于1的,所以才会命名为mvIneScale,就是scale的倒数。
resize(mvImagePyramid[level-1], mvImagePyramid[level], sz, 0, 0, INTER_LINEAR);
// 【3】这里主要是为了方便做一些卷积计算来做一些边界补充
copyMakeBorder(mvImagePyramid[level], temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, BORDER_REFLECT_101+BORDER_ISOLATED);
}
else
{
copyMakeBorder(image, temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, BORDER_REFLECT_101);
}
}
}
计算关键点。
void ORBextractor::ComputeKeyPointsOctTree(vector<vector >& allKeypoints)
{
// 一共计算nlevel个尺度的关键点
allKeypoints.resize(nlevels);
// 窗口的大小
const float W = 30;
// 对每个尺度计算它的关键点
for (int level = 0; level < nlevels; ++level)
{
// 计算边界,在这个边界内计算FAST关键点
const int minBorderX = EDGE_THRESHOLD-3;
const int minBorderY = minBorderX;
const int maxBorderX = mvImagePyramid[level].cols-EDGE_THRESHOLD+3;
const int maxBorderY = mvImagePyramid[level].rows-EDGE_THRESHOLD+3;
vector vToDistributeKeys;
vToDistributeKeys.reserve(nfeatures*10);
// 计算宽度和高度
const float width = (maxBorderX-minBorderX);
const float height = (maxBorderY-minBorderY);
// 计算有多少个窗口
const int nCols = width/W;
const int nRows = height/W;
// 计算每个窗口的大小
const int wCell = ceil(width/nCols);
const int hCell = ceil(height/nRows);
// 对每个窗口进行遍历
for(int i=0; iconst float iniY =minBorderY+i*hCell;
float maxY = iniY+hCell+6;
// 已经出了图片的有效区域了
if(iniY>=maxBorderY-3)
continue;
// 超出边界的话就使用计算最宽的边界
if(maxY>maxBorderY)
maxY = maxBorderY;
for(int j=0; j// *********************************
// 计算每列的位置
const float iniX =minBorderX+j*wCell;
float maxX = iniX+wCell+6;
if(iniX>=maxBorderX-6)
continue;
if(maxX>maxBorderX)
maxX = maxBorderX;
// 计算FAST关键点
vector vKeysCell;
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
vKeysCell,iniThFAST,true);
if(vKeysCell.empty())
{
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
vKeysCell,minThFAST,true);
}
// 如果找到的关键点不为空,就加入到vToDistributeKeys.
if(!vKeysCell.empty())
{
for(vector ::iterator vit=vKeysCell.begin(); vit!=vKeysCell.end();vit++)
{
// 计算实际的位置
(*vit).pt.x+=j*wCell;
(*vit).pt.y+=i*hCell;
vToDistributeKeys.push_back(*vit);
}
}
}
}
vector & keypoints = allKeypoints[level];
// 至少保留n个特征点
keypoints.reserve(nfeatures);
// 根据Harris角点的score进行排序,保留正确的
keypoints = DistributeOctTree(vToDistributeKeys, minBorderX, maxBorderX,
minBorderY, maxBorderY,mnFeaturesPerLevel[level], level);
// 这里的PATCH_SIZE是31
const int scaledPatchSize = PATCH_SIZE*mvScaleFactor[level];
// Add border to coordinates and scale information
const int nkps = keypoints.size();
for(int i=0; i// compute orientations
// 计算nlevels个尺度下各个关键点的方向
for (int level = 0; level < nlevels; ++level)
computeOrientation(mvImagePyramid[level], allKeypoints[level], umax);
}
计算用FAST选出来的特征点是否合格。
vector ORBextractor::DistributeOctTree(const vector & vToDistributeKeys, const int &minX, const int &maxX, const int &minY, const int &maxY, const int &N, const int &level)
这个地方看得不是很懂,这里是计算出每个level的关键点的score,接着进行排序,选出了n个关键点。
Mark 一下,改日回来继续看。
计算rBRIEF描述子。
在这个函数里计算了rBrief的描述子。
static void computeDescriptors(const Mat& image, vector & keypoints, Mat& descriptors,
const vector & pattern)
{
// 初始化描述子
descriptors = Mat::zeros((int)keypoints.size(), 32, CV_8UC1);
for (size_t i = 0; i < keypoints.size(); i++)
// 计算每个关键点的描述子
computeOrbDescriptor(keypoints[i], image, &pattern[0], descriptors.ptr((int)i));
}
计算描述子如下:
计算256维的描述子。
const float factorPI = (float)(CV_PI/180.f);
static void computeOrbDescriptor(const KeyPoint& kpt,
const Mat& img, const Point* pattern,
uchar* desc)
{
float angle = (float)kpt.angle*factorPI;
float a = (float)cos(angle), b = (float)sin(angle);
const uchar* center = &img.at(cvRound(kpt.pt.y), cvRound(kpt.pt.x));
const int step = (int)img.step;
#define GET_VALUE(idx) \
center[cvRound(pattern[idx].x*b + pattern[idx].y*a)*step + \
cvRound(pattern[idx].x*a - pattern[idx].y*b)]
for (int i = 0; i < 32; ++i, pattern += 16)
{
int t0, t1, val;
t0 = GET_VALUE(0); t1 = GET_VALUE(1);
val = t0 < t1;
t0 = GET_VALUE(2); t1 = GET_VALUE(3);
val |= (t0 < t1) << 1;
t0 = GET_VALUE(4); t1 = GET_VALUE(5);
val |= (t0 < t1) << 2;
t0 = GET_VALUE(6); t1 = GET_VALUE(7);
val |= (t0 < t1) << 3;
t0 = GET_VALUE(8); t1 = GET_VALUE(9);
val |= (t0 < t1) << 4;
t0 = GET_VALUE(10); t1 = GET_VALUE(11);
val |= (t0 < t1) << 5;
t0 = GET_VALUE(12); t1 = GET_VALUE(13);
val |= (t0 < t1) << 6;
t0 = GET_VALUE(14); t1 = GET_VALUE(15);
val |= (t0 < t1) << 7;
desc[i] = (uchar)val;
}
#undef GET_VALUE
}
描述子是采用了高斯分布采样得到的。在里面定义为:
static int bit_pattern_31_[256*4] =
{
8,-3, 9,5/*mean (0), correlation (0)*/,
4,2, 7,-12/*mean (1.12461e-05), correlation (0.0437584)*/,
-11,9, -8,2/*mean (3.37382e-05), correlation (0.0617409)*/,
7,-12, 12,-13/*mean (5.62303e-05), correlation (0.0636977)*/,
2,-13, 2,12/*mean (0.000134953), correlation (0.085099)*/,
1,-7, 1,6/*mean (0.000528565), correlation (0.0857175)*/,
-2,-10, -2,-4/*mean (0.0188821), correlation (0.0985774)*/,
-13,-13, -11,-8/*mean (0.0363135), correlation (0.0899616)*/,
-13,-3, -12,-9/*mean (0.121806), correlation (0.099849)*/,
10,4, 11,9/*mean (0.122065), correlation (0.093285)*/,
-13,-8, -8,-9/*mean (0.162787), correlation (0.0942748)*/,
-11,7, -9,12/*mean (0.21561), correlation (0.0974438)*/,
7,7, 12,6/*mean (0.160583), correlation (0.130064)*/,
......
}
在computeOctKeypointTree里调用了这个函数,调用为:
computeOrientation(mvImagePyramid[level], allKeypoints[level], umax);
static void computeOrientation(const Mat& image, vector & keypoints, const vector<int>& umax)
{
for (vector ::iterator keypoint = keypoints.begin(),
keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
{
// 计算nlevel这个尺度的图片的关键点的方向
keypoint->angle = IC_Angle(image, keypoint->pt, umax);
}
}
IC_Angle: 这个部分对照我介绍的ORB特征的那篇博客,里面有详细介绍【2】。里面有详细的介绍。
static float IC_Angle(const Mat& image, Point2f pt, const vector<int> & u_max)
{
int m_01 = 0, m_10 = 0;
const uchar* center = &image.at (cvRound(pt.y), cvRound(pt.x));
// Treat the center line differently, v=0
for (int u = -HALF_PATCH_SIZE; u <= HALF_PATCH_SIZE; ++u)
m_10 += u * center[u];
// Go line by line in the circuI853lar patch
int step = (int)image.step1();
for (int v = 1; v <= HALF_PATCH_SIZE; ++v)
{
// Proceed over the two lines
int v_sum = 0;
int d = u_max[v];
for (int u = -d; u <= d; ++u)
{
int val_plus = center[u + v*step], val_minus = center[u - v*step];
v_sum += (val_plus - val_minus);
m_10 += u * (val_plus + val_minus);
}
m_01 += v * v_sum;
}
return fastAtan2((float)m_01, (float)m_10);
}
在上面对一帧的关键提取出来以后,就需要进行追踪了。
关键帧提取出了需要的关键点和描述子,接下来就是进行追踪了。
在trcking.cc里面,有void Tracking::Track()
这里面的东西还需要继续挖掘~ 已经Mark
参考:
【1】ORB:an efficient alternative to SIFT or SURF
【2】ORB特征讲解: http://blog.csdn.net/c602273091/article/details/56008370
【3】copyMakeBorder:http://blog.csdn.net/viewcode/article/details/8287599
【4】图像金字塔:http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/imgproc/pyramids/pyramids.html
【5】OpenCV resize:http://www.cnblogs.com/korbin/p/5612427.html
【6】OpenCV Copyboarder: http://blog.csdn.net/viewcode/article/details/8287599
【7】OpenCV PI: http://www.zybang.com/question/5768af44d5df172aedf5d9d6c9dc7c1e.html
【8】Fastatan2: http://blog.csdn.net/mingzhentanwo/article/details/45155307
【9】OpenCV FAST 源码分析:http://blog.csdn.net/zhaocj/article/details/40301561
【10】OpenCV FAST使用: http://www.bkjia.com/ASPjc/976906.html
【11】FAST API:http://opencv.jp/opencv-2.2_org/cpp/features2d_feature_detection_and_description.html?highlight=fast#cv-fast