第四讲 相机模型 非线性优化

- 针孔相机模型与图像

- 实践:OpenCV/RGBD图像拼接

- 批量状态估计问题

- 非线性最小二乘法

- 实践:Ceres和g20(非线性优化库,图优化库)


批量的状态估计问题可以通过构建最小二乘的方式求解。
针孔相机模型:相机将三维世界中的坐标点(单位为米)映射到二维图像平面上(单位为像素)的过程。由于相机镜头上的透镜的存在,使得光线投影到成像平面的过程会产生畸变,因此,我们用针孔和畸变两个模型来描述整个投影过程。

小孔成像得到的像是倒立的,为了让模型符合实际,我们可以等价的把成像平面对称的放到相机的前面,和三维空间点一起放到摄像机坐标系的同一侧。

空间点的单位为米,不过,在相机中,我们最终获得的是一个个的像素,这还需要在成像平面上对像进采样和量化。为了描述传感器将感受到的光线转换成图像像素的过程,我们设在物理成像平面上固定着一个像素平面o-u-v.在像素平面上的到P‘的像素坐标:[u,v]T.
像素坐标系与成像平面之间,相差了一个缩放和平移,我们设像素坐标在u轴上缩放了 α \alpha α倍,在v轴上缩放了 β \beta β倍。同时,原点平移了[cx,cy]T,所以P‘的坐标与像素坐标[u,v]T的关系如下:

有的相机生产厂商会告诉相机的内参,而有时需要自己确定相机的内参,也就是所谓的标定.
单目棋盘格张正友标定法



相机的外参数:相机的位姿,相比于不变的内参,外参数是随着相机运动发生改变的,也是SLAM中待估计的目标,代表着机器人的轨迹。
这个模型中也可以看出,如果对相机坐标乘以任意一非零常数,归一化坐标都是一样的,这说明点的深度在投影过程中被丢失了,所以单目视觉中没法得到像素点的深度值。

在世界坐标系下有一点Pw,通过旋转和平移计算出相机坐标系下的坐标(R*Pw + t),然后除掉Z,就得到了归一化平面的坐标,再左乘相机的内参,就的到了图像中的像素值
相机的旋转和平移是slam里要估计的目标(R,t)是相机的外参,通常是我们不知道的,而内参(K)是可以通过标定确定下来的,这个是投影过程,同时也是观测过程。
观测过程:xk处观察到yj路标再加噪声的过程,xk是相机的旋转和平移,yj是路标点在世界坐标系下的坐标,zkj是观测到的像素。针孔相机描述了观测方程

针孔相机是理想的模型,实际当中由于镜头的因素(镜头是广角,鱼眼镜头),使得图像发生变化引起了畸变

畸变模型:为了获得更好的成像效果,我们在相机前面加入了透镜。透镜的加入会对成像过程中光线的传播产生新的影响;

  • 透镜自身的形状对光线传播的影响
  • 在机械组装过程中,透镜和成像平面不可能完全平行,这也会使得光线透过透镜投影到成像平面时的位置发生变化
    径向畸变(桶形畸变枕形畸变):由透镜的形状引起的畸变,在实际的拍摄过程中会使得真实环境中的一条直线在图片中变成了曲线。越靠近图像边缘,现象越明显。透镜是中心对称的,使得这种不规则的畸变通常是径向对称的。
    桶形畸变图像放大率随着与光轴之间的距离增大而减小,而枕形畸变相反
    切向畸变:由于在相机的组装过程中不能使透镜和成像面严格平行而引起的。


径向畸变可以看成坐标点沿着长度方向发生了变化,也就是其距离原点的长度发生了变化。
切向畸变可以看成坐标点沿着切线方向发生了变化,也就是水平夹角发生了变化。

[Xdistorted , Ydistorted]是畸变后点的归一化坐标。

3,4步骤之间补充:有畸变时,根据畸变的参数计算Pc发生畸变后的坐标。
综上所述,我们谈到了四个坐标:世界坐标,相机坐标,归一化坐标,像素坐标。它们的关系反映了整个成像过程。

单目相机的缺点
针孔相机模型描述了单个相机的成像模型。然而仅根据一个像素,我们无法确定这个空间点的具体位置。这是因为i相机光心到归一化连线上的所有点,都可以投影到该像素上。只有当P的深度确定时,我们才能知道它的空间位置。

双目相机的 基线(记作b): 两个水平放置的相机光圈中心的距离
视差(记作d):左右图的横坐标之差
视差与距离成反比:视差越大,距离越近。视差最小为一个像素,于是双目的深度存在一个理论上的最大值,由fb确定。基线越长,双目能测到的最大距离就越远;

双目相机的缺点

  • 虽然视差计算深度的公式简洁,但视差d本身计算比较困难,我们需要确切的知道左眼图像的某个像素在右眼中具体的位置。

  • 当我们想计算每个像素的深度时,计算量和精度又是问题,而且置有在图像有纹理的地方才能计算视差。由于计算量的原因,双目深度估计仍需要使用GPU或FPGA来实时计算。


RGB-D相机能够主动测量每个像素的深度
RGB-D相机按原理分为:

  • 通过红外光(Structured Light)原理测量像素距离
  • 通过飞行时间(Time-of-Flight,ToF)原理测量像素距离,ToF的传感器。
    都需要向探测目标发射一束光线(通常是红外光)。在结构光原理中,返回的结构光图案,计算物体与自身之间的距离 。ToF原理中,相机和激光雷达很相似,只是激光是通过逐点扫描来测距的,而ToF相机可以获取整个图像的像素深度。
    RGB-D由摄像头,发射器和接收器组成。
    可以在同一个图像位置,读取到色彩信息和距离信息,计算像素的3D相机坐标,生成点云图(Point Cloud),所以即可以在图像层面对RGB-D数据进行处理,也可以在点云层面处理。
    RGB-D相机的缺点
  • 使用发射接收的测量,其使用范围比较受限。
  • 用红外光进行测量深度值的RGB-D相机,易受日光和其他传感器发射的红外光的干扰,因此不能在室外使用。
  • 在没有调制的情况下,同时使用多个RGB-D相机会相互干扰。
  • 对透射材质的物体,因为接受不到反射光,无法测距,也就无法知道这些点的具体位置。
  • RGB-D相机成本,功耗高


图像在数学中,可以用一个矩阵来描述;在计算机中,占据了一连串连续的磁盘或空间,可以用二维数组来表示。
计算机中的图像-——灰度图,在灰度图中,每个像素位置(x,y)对应一个灰度值I,然而计算机并不能表达实数空间 ,所以我们需要对下标和图像读数在某个范围内进行量化
0~255的读数(一个字节)来表示图像的灰度读数。
在图像中,数组的行数对应图像的高度,列数对应图像的宽度。
访问像素 :unsigned char pixel = image[y][x];
当我们要记录更多的信息时,一个字节恐怕不够,例如:RGB-D相机的深度图中,记录每个像素与相机之间的距离,这个距离通常是以毫米为单位的,而RGB-D的量程通常在十几米,超过了255.这时采用16位整数(unsigned short)来记录深度图的信息(0~65535),最大可达65米
彩色图像的表示需要通道的。对于每个像素,就要记录R,G,B三个值,每个值是一个通道,每个通道由8位整数来表示。彩色图共占据了24位空间。
OpenCV彩色图中,通道默认B,G,R,如果还要表达透明度,就使用R,G,B,A四个通道。

为什么需要引入最优化,无约束非线性优化等方法呐?
解决状态估计问题,为什么要进行状态估计?
由于噪声的存在,运动方程(相机成像)和观测方程(位姿由T描述,用李代数来优化)的等式不是精确的

状态估计的方式:

  • 增量/渐进(incremental):(滤波器)观测和运动数据是随时间逐渐到来的,我们应持有一个当前时刻的状态估计,然后用新的数据来更新它。
  • 批量(batch):把0-k时刻的所有输入和观测数据放在一起,估计0-k时刻的轨迹和地图。
    显然批量是不实时的,所以我们采用折中的办法:滑动窗口法:固定一些历史轨迹,仅对当前时刻的一些轨迹进行优化。


最大似然估计的意义:“在什么样的状态下,最可能产生现在观测到的数据”


由上式的最后一项可以看出:该式等价于最小化噪声项(误差)的一个二次型,这个二次型称为马哈拉若比斯距离也叫做马氏距离
Qk,j -1叫作信息矩阵





求解上面这个方程需要我们知道关于这个目标函数的全局性质,通常这是不可能的。对于不方便求解的最小二乘问题,我们用迭代的方式

迭代的方式让求解导函数为0的问题变成了一个不断寻找下降增量 Δ \Delta Δx的问题,当函数下降直到增量非常小的时候,就认为算法收敛,目标函数达到了一个极小值。在这个寻找的过程中,如何找到每次迭代的增量,这是一个局部问题,我们只关系f在迭代值处的局部性质而不是全局性质。

最速下降法:只要我们沿着反向梯度方向前进(梯度方向是正的),在一阶(线性)下,目标函数必定会下降。

一阶和二阶中的f其实是F(x),一阶,二阶梯度法很直观,只要我们把函数在迭代点附近进行泰勒展开,并针对更新量作最小化即可。我们就是用一个一次或二次的函数来近似了原函数,然后用近似函数的最小值来猜测原函数的极小值。只要目标函数局部看起来是一次或二次函数,这种算法算是成立的。


这里的函数fx不是目标函数F(x)。

增量方程(高斯牛顿方程 或者 正规方程):H Δ \Delta Δx = g
高斯牛顿方用JJT作为牛顿法中的Hessian矩阵的近似从而忽略了H的计算。
高斯牛顿法的缺点:
为了求解增量方程,我们需要求解H的逆,这需要H是可逆的,但实际得到的H是JJT是半正定的,也就是说,在使用高斯牛顿法的时候,可能出现JJT为奇异矩阵或病态的情况,此时的增量的稳定性差,导致算法不收敛。直观的讲,原函数在这个点局部不像是二次函数。
即使 H非奇异非病态,如果我们求出的步长 Δ \Delta Δx太大,也会导致我们采用的局部近似不够准确,无法保证它的迭代收敛。

列文伯格-马夸尔特法修正个这个问题,它的收敛速度不高斯牛顿法满,被称为阻尼牛顿法

高斯牛顿法中采用的近似二阶泰勒展开只能在展开点有较好的近似,所以我们想到应该给 Δ \Delta Δx添加一个范围,称为信赖区域
如何确定这个范围尼?
根据近似模型与实际函数之间的差异来确定:如果差异小,说明近似好,我们应该扩大近似范围;如果差异大,就缩小近似范围
ρ \rho ρ的分子是实际下降的值,分母是近似模型下降的值, ρ \rho ρ接近1,近似效果好,太小,说明实际减小的值远少于吉尼斯减小的值,则认为近似效果差,需要缩小近似范围;如果 ρ \rho ρ较大,则说明实际的比预计的更大,我们可以放大范围

我们把增量限定在一个半径为 μ \mu μ的球中,只有在这个球内才是有效的,带上D之后,这个球可能是个椭球。在LM中D取单位阵,相当与把增量范围约束在一个球中。

在LM中,我们用拉格朗日乘子把约束项放到目标函数中,构成拉格朗日函数:

拉格朗日函数关于 Δ \Delta Δx的导数为0,增量的线性方程为:


当参数 λ \lambda λ比较小时,H占主要地位,说明二次近似模型在该范围内是比较好的,LM更接近于高斯牛顿法。当 λ \lambda λ不较大时, λ \lambda λ I占主要地位,LM更接近于一阶梯度法(最速下降法),说明附近二次近似不好

LM的优点:
一定程度上避免线性方程组的系数矩阵的非奇异和病态问题,提供更稳定,更准确的增量。
当问题性质较好时,用GN
当问题接近病态,用LM

后期,我们会用ICP,PnP之类的算法提供优化的初始值。
实例
1,操作opencv图像

#include <iostream>
#include <chrono>
using namespace std;#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>int main(int argc, char **argv)
{cv::Mat image;image = cv::imread(argv[1]);if(image.data == nullptr) {cerr << "file " << argv[1] << "no exist!" << endl;return 0;}cout << "图像宽度:" << image.cols << ",高度:" << image.rows << ",通道数为:" << image.channels() << endl;cv::imshow("image", image);cv::waitKey(0);if(image.type() != CV_8UC1 && image.type() != CV_8UC3) {cout << "请输入一张彩色图或灰度图" << endl;return 0;}chrono::steady_clock::time_point t1 = chrono::steady_clock::now();for(size_t y = 0; y < image.rows; y++) {unsigned char *row_ptr = image.ptr<unsigned char>(y);for(size_t x = 0; x < image.cols; x++) {unsigned char *data_ptr = &row_ptr[x * image.channels()];for(int c = 0; c != image.channels(); c++) {unsigned char data = data_ptr[c];;}}}chrono::steady_clock::time_point t2 = chrono::steady_clock::now();chrono::duration<double> time_used = chrono::duration_cast<chrono::duration<double>>(t2 - t1);cout << "遍历图像用时:" << time_used.count() << "秒。" << endl;cv::Mat image_another = image;image_another(cv::Rect(0, 0, 100, 100)).setTo(0);cv::imshow("image", image);cv::waitKey(0);cv::Mat image_clone = image.clone();image_clone(cv::Rect(0, 0, 100, 100)).setTo(255);cv::imshow("image", image);cv::imshow("image_clone", image_clone);cv::waitKey(0);cv::destroyAllWindows();return 0;
}

CMakeLists.txt

project(imageBasics)
find_package(OpenCV REQUIRED)
include_directories (${OpenCV_INCLUDE_DIRS})
add_executable(imageBasics imageBasics.cpp)
# 链接OpenCV库
target_link_libraries(imageBasics ${OpenCV_LIBS})

运行结果:


2,图像去畸变

#include <opencv2/opencv.hpp>
#include <string>using namespace std;
string image_file = "../distorted.png";int main(int argc, char **argv[])
{double k1 = -0.28340811, k2 = 0.07395907, p1 = 0.00019359, p2 = 1.76187114e-05;double fx = 458.654, fy = 457.296, cx = 367.215, cy = 248.375;cv::Mat image = cv::imread(image_file, 0);int rows = image.rows , cols = image.cols;cv::Mat image_undistort = cv::Mat(rows, cols, CV_8UC1);for(int v = 0; v < rows; v++) {for(int u = 0; u < cols; u++) {double x = (u - cx) / fx, y = (v - cy) / fy;double r = sqrt(x * x + y * y);double x_distorted = x * (1 + k1 * r * r + k2 * r * r * r * r) + 2 * p1 * x * y + p2 * (r * r + 2 * x * x);double y_distorted = y * (1 + k1 * r * r + k2 * r * r * r * r) + p1 * (r * r + 2 * y * y) + 2 * p2 * x * y;double u_distorted = fx * x_distorted + cx;double v_distorted = fy * y_distorted + cy;if(u_distorted >= 0 && v_distorted >= 0 && u_distorted < cols && v_distorted < rows) {image_undistort.at<uchar>(v,u) = image.at<uchar>((int) v_distorted, (int) u_distorted);} else {image_undistort.at<uchar>(v,u) = 0;}}}cv::imshow("distorted", image);cv::imshow("undistorted", image_undistort);cv::waitKey();return 0;}

CMakeLists.txt:

project(imageBasics)
find_package(OpenCV REQUIRED)
include_directories (${OpenCV_INCLUDE_DIRS})
add_executable(undistortImage undistortImage.cpp)
# 链接OpenCV库
target_link_libraries(undistortImage ${OpenCV_LIBS})

运行结果:

3,双目视觉,计算视差图和点云

#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include <Eigen/Core>
#include <unistd.h> //linxu系统内置头文件,包含许多系统服务器的内置函数 如read,write, usleep等
#include <pangolin/pangolin.h>using namespace std;
using namespace Eigen;string left_file = "../left.png";
string right_file = "../right.png";//点云图绘制接口,传入带一个通道的颜色信息的四维向量集
void showPointCloud(const vector<Vector4d, Eigen::aligned_allocator<Vector4d>> &pointcloud);int main(int argc, char **argv)
{// 内参double fx = 718.856, fy = 718.856, cx = 607.1928, cy = 185.2157;// 基线double b = 0.573; //相机光轴间的距离//读取图像(0,灰度图)cv::Mat left = cv::imread(left_file, 0);cv::Mat right = cv::imread(right_file, 0);//SGBM是立体匹配算法中的半全局块匹配,得到的视差图相比于BM算法来说,减少了很多不确定的匹配点,尤其是深度不连续的区域,,速度上SGBM比BM慢cv::Ptr<cv::StereoSGBM> sgbm = cv::StereoSGBM::create(0, 96, 9, 8 * 9 * 9, 32 * 9 * 9, 1, 63, 10, 100, 32);cv::Mat disparity_sgbm, disparity;//由左右视图按照sgbm算法匹配计算得到视差图sgbm->compute(left, right, disparity_sgbm);//将16位符号整形的视差Mat转换为32位浮点型Matdisparity_sgbm.convertTo(disparity, CV_32F, 1.0 / 16.0f);vector<Vector4d, Eigen::aligned_allocator<Vector4d>> pointcloud;for(int v= 0; v < left.rows; v++) {for(int u = 0; u < left.cols; u++) {if(disparity.at<float>(v, u) <= 0.0 || disparity.at<float>(v, u) >= 96.0) continue;Vector4d point(0, 0, 0, left.at<uchar>(v, u) / 255.0);//根据双目模型计算point的位置,计算的是左视图点的相机位置double x = (u - cx) / fx;  //计算的是点在归一化平面上的相机位置double y = (v - cy) / fy;double depth = fx * b / (disparity.at<float>(v, u)); //由视差,计算像素点的实际深度信息point[0] = x * depth; //由深度信息获取真实坐标系下的Xpoint[1] = y * depth;point[2] = depth;pointcloud.push_back(point);   //获取相机坐标系下的点云位置}}cv::imshow("disparity" , disparity / 96.0);  //把视差限制在0-96cv::waitKey(0);showPointCloud(pointcloud);return 0;
}void showPointCloud(const vector<Vector4d, Eigen::aligned_allocator<Vector4d>> &pointcloud)
{if(pointcloud.empty()) {cerr << "Point cloud is empty!" << endl;return ;}pangolin::CreateWindowAndBind("Point Cloud Viewer", 1024, 768);glEnable(GL_DEPTH_TEST);glEnable(GL_BLEND);glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);pangolin::OpenGlRenderState s_cam(pangolin::ProjectionMatrix(1024, 768, 500, 500, 512, 389, 0.1, 1000),//相机参数pangolin::ModelViewLookAt(0, -0.1, -1.8, 0, 0, 0, 0.0, -1.0, 0.0) //观测视角);pangolin::View &d_cam = pangolin::CreateDisplay().SetBounds(0.0, 1.0, pangolin::Attach::Pix(175), 1.0, -1024.0f / 768.0f).SetHandler(new pangolin::Handler3D(s_cam));while(pangolin::ShouldQuit() == false) {glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);d_cam.Activate(s_cam);glClearColor(1.0f, 1.0f, 1.0f, 1.0f);glPointSize(2);glBegin(GL_POINTS);for(auto &p:pointcloud) {glColor3f(p[3], p[3], p[3]);glVertex3d(p[0], p[1], p[2]);}glEnd();pangolin::FinishFrame();usleep(5000);}return ;}

函数解释:
参考:https://candyguo.github.io/blog-post-2/
半全局匹配算法(SGBM)是时是立体视觉中最流行的一个算法。
static Ptr cv::StereoSGBM::create (int minDisparity = 0,
int numDisparities = 16,
int blockSize = 3,
int P1 = 0,
int P2 = 0,
int disp12MaxDiff = 0,
int preFilterCap = 0,
int uniquenessRatio = 0,
int speckleWindowSize = 0,
int speckleRange = 0,
int mode = StereoSGBM::MODE_SGBM
)

参数:

  • minDisparity:最小的视差值。

  • numDisparity: 视差个数(64/96/128/256…)。

  • blockSize:灰度相关时的窗口大小(3/5/7…) 。

  • P1,P2:平滑惩罚系数,一般:P1=8通道数SADWindowSizeSADWindowSize,P2=4P1

  • disp12MaxDiff :左右视差图的最大容许差异(超过将被清零),默认为 -1,即不执行左右视差检查。

  • preFilterCap:预滤波图像像素的截断值。主要是图像预处理的操作,用来排除u噪声干扰,提高边界的可区分性。

  • uniquenessRatio:视差唯一性百分比, 视差窗口范围内最低代价是次低代价的(1 +
    uniquenessRatio/100)倍时,最低代价对应的视差值才是该像素点的视差,否则该像素点的视差为 0,通常为5~15.

  • speckleWindowSize :平滑视差区域的最大尺寸(过滤一些斑点噪声)

  • speckleRange:视差变化阈值。

  • mode:模式

CMakeLists.txt:

find_package(Pangolin REQUIRED)
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
add_executable(stereoVision stereoVision.cpp)
target_link_libraries(stereoVision ${OpenCV_LIBS} ${Pangolin_LIBRARIES})

左右图像:

实现效果:

可以看到有许多空洞和视差不连续的地方,正常流程中还包括一步视差图滤波后处理。

点云图:

4,RGB-D视觉

#include <iostream>
#include <fstream>
#include <opencv2/opencv.hpp>
#include <boost/format.hpp>
#include <sophus/se3.hpp>
#include <pangolin/pangolin.h>using namespace std;
typedef vector<Sophus::SE3d, Eigen::aligned_allocator<Sophus::SE3d>> TrajectoryType;
typedef Eigen::Matrix<double ,6, 1> Vector6d;void showPointCloud(const vector<Vector6d, Eigen::aligned_allocator<Vector6d>> &ponitcloud);int main(int argc, char ** argv)
{vector<cv::Mat> colorImgs, depthImgs;TrajectoryType poses;ifstream fin("../pose.txt");if(!fin ) {cerr << "在pose.txt 下一级目录下运行" << endl;return 1;}for(int i = 0; i < 5; i++) {boost::format fmt("../%s/%d.%s");colorImgs.push_back(cv::imread((fmt % "color" % (i + 1) % "png").str()));depthImgs.push_back(cv::imread((fmt % "depth" % (i + 1) % "pgm").str(), -1));double data[7] = {0};for(auto &d:data) fin >> d;Sophus::SE3d pose(Eigen::Quaterniond(data[6], data[3], data[4], data[5]),Eigen::Vector3d(data[0], data[1], data[2]));poses.push_back(pose);}double cx = 325.5;double cy = 253.5;double fx = 518.0;double fy = 519.0;double depthScale = 1000.0;vector<Vector6d, Eigen::aligned_allocator<Vector6d>> pointcloud;pointcloud.reserve(1000000);for(int i = 0; i < 5; i++) {cout << "转换图像中:" << i + 1 << endl;cv::Mat  color = colorImgs[i];cv::Mat depth = depthImgs[i];Sophus::SE3d T = poses[i];for(int v = 0; v < color.rows; v++) {for(int u = 0; u < color.cols; u++) {unsigned int d = depth.ptr<unsigned short>(v)[u];if(d == 0) continue;Eigen::Vector3d point;point[2] = double(d) / depthScale;    //depthScale ,我们需要把坐标都转到世界坐标系中,d是乘了缩放因子后的相机坐标系下的深度信息point[1] = (u - cx) * point[2]/ fx;point[0] = (v - cy) * point[2] / fy;Eigen::Vector3d pointWorld = T * point;Vector6d p;p.head<3>() = pointWorld;// cout << "color.step:" << color.step << endl;//color.step 等于像素宽度乘以3 , 三个字节为一个像素的bgr值,第一行是从0- 3*像素宽度,第二行从第一行的基础上加就可以。p[5] = color.data[v * color.step + u * color.channels()];  //bp[4] = color.data[v * color.step + u * color.channels()+ 1]; //gp[3] = color.data[v * color.step + u * color.channels() + 2]; //rpointcloud.push_back(p);}}}cout << "点云共有:" << pointcloud.size() << "个点" << endl;showPointCloud(pointcloud);return 0;}void showPointCloud(const vector<Vector6d, Eigen::aligned_allocator<Vector6d>> &pointcloud) {if (pointcloud.empty()) {cerr << "Point cloud is empty!" << endl;return;}pangolin::CreateWindowAndBind("Point Cloud Viewer", 1024, 768);glEnable(GL_DEPTH_TEST);glEnable(GL_BLEND);glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);pangolin::OpenGlRenderState s_cam(pangolin::ProjectionMatrix(1024, 768, 500, 500, 512, 389, 0.1, 1000),pangolin::ModelViewLookAt(0, -0.1, -1.8, 0, 0, 0, 0.0, -1.0, 0.0));pangolin::View &d_cam = pangolin::CreateDisplay().SetBounds(0.0, 1.0, pangolin::Attach::Pix(175), 1.0, -1024.0f / 768.0f).SetHandler(new pangolin::Handler3D(s_cam));while (pangolin::ShouldQuit() == false) {glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);d_cam.Activate(s_cam);glClearColor(1.0f, 1.0f, 1.0f, 1.0f);glPointSize(2);glBegin(GL_POINTS);for (auto &p: pointcloud) {glColor3d(p[3] / 255.0, p[4] / 255.0, p[5] / 255.0);glVertex3d(p[0], p[1], p[2]);}glEnd();pangolin::FinishFrame();usleep(5000);   // sleep 5 ms}return;
}

CMakeLists.txt:

find_package(Sophus REQUIRED)
include_directories(${Sophus_INCLUDE_DIRS})
find_package(Pangolin REQUIRED)
find_package(OpenCV 3.4.15 REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
include_directories(${Pangolin_INCLUDE_DIRS})
add_executable(joinMap joinMap.cpp)
target_link_libraries(joinMap ${OpenCV_LIBS} ${Sophus_LIBRARIES} ${Pangolin_LIBRARIES})

实现效果:

深蓝-视觉slam-第四讲学习笔记相关推荐

  1. 视觉SLAM十四讲学习笔记-第七讲-视觉里程计-三角测量和实践

     专栏汇总 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第二讲-初识SLAM_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第 ...

  2. 视觉SLAM十四讲学习笔记-第七讲-视觉里程计-对极几何和对极约束、本质矩阵、基础矩阵

    专栏系列文章如下:  专栏汇总 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第二讲-初识SLAM_goldqiu的博客-CSDN博客 视觉SLA ...

  3. 视觉SLAM十四讲学习笔记-第六讲学习笔记总结(1)---非线性优化原理

    第六讲学习笔记如下: 视觉SLAM十四讲学习笔记-第六讲-非线性优化的状态估计问题_goldqiu的博客-CSDN博客 ​​​​​​视觉SLAM十四讲学习笔记-第六讲-非线性优化的非线性最小二乘问题_ ...

  4. 视觉SLAM十四讲学习笔记-第四讲---第五讲学习笔记总结---李群和李代数、相机

    第四讲---第五讲学习笔记如下: 视觉SLAM十四讲学习笔记-第四讲-李群与李代数基础和定义.指数和对数映射_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第四讲-李代数求导与扰动模 ...

  5. 视觉SLAM十四讲学习笔记---前三讲学习笔记总结之SLAM的作用、变换和位姿表示

    经过半年学习SLAM相关知识,对SLAM系统有了一些新的认识,故回看以前的学习记录,做总结和校正. 前三讲学习笔记如下: 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉S ...

  6. 视觉SLAM十四讲学习笔记-第七讲-视觉里程计-特征点法和特征提取和匹配实践

    专栏系列文章如下: 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第二讲-初识SLAM_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习 ...

  7. 视觉SLAM十四讲学习笔记-第六讲-非线性优化的实践-高斯牛顿法和曲线拟合

    专栏系列文章如下: 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第二讲-初识SLAM_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习 ...

  8. 视觉SLAM十四讲学习笔记-第六讲-非线性优化的非线性最小二乘问题

    专栏系列文章如下: 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第二讲-初识SLAM_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习 ...

  9. 视觉SLAM十四讲学习笔记-第六讲-非线性优化的状态估计问题

    专栏系列文章如下: 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第二讲-初识SLAM_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习 ...

  10. 视觉SLAM十四讲学习笔记-第四讲-Sophus实践、相似变换群与李代数

    专栏系列文章如下: 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第二讲-初识SLAM_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习 ...

最新文章

  1. WPF学习笔记(4):获取DataGridTemplateColumn模板定义的内容控件(转)
  2. Error parsing HTTP request header Note: further occurrences of HTTP header parsing errors
  3. NYOJ 1068 ST(段树 为段更新+间隔总和)
  4. 启动hadoop时报root@localhost's password: localhost: Permission denied, please try again.错误。...
  5. 济源一中2021高考成绩查询入口,济源一中2019高考成绩喜报、一本二本上线人数情况...
  6. FPGA复位方法总结
  7. 用unity制作能量护盾(1)
  8. 正则表达式-基本概念与简单元字符
  9. linux 修改mysql端口
  10. 关于ASCII码和机内码
  11. 不是吧!你还不懂DHT协议?
  12. 服务器防火墙firewalld,指定端口开放
  13. mysql stop failed_Mysql报错:Failed to stop mysqld.service: Unit mysqld.service not loaded.
  14. Windos下安装nexus私服(nexus-2.12.0-01)
  15. 如何使用Fork版本工具下载分支内容
  16. 解决Win10搜索框不能使用
  17. android+m3+max,Android Opencore OpenMAX学习(3)...
  18. 2020-09-23
  19. ORA-01840: 输入值对于日期格式不够长
  20. 空间杜宾模型SPDM

热门文章

  1. ms17-010永恒之蓝漏洞复现
  2. 店盈通:拼多多商家如何推广?有哪些推广技巧?
  3. maven wagon插件实现自动化部署
  4. Vue 中调用 QQ官方的登录
  5. CC2530——温湿度传感器DHT11
  6. 子网掩码换算和计算网络号
  7. 武汉生物工程学院计算机专业怎么样,武汉生物工程学院就业率怎么样(含好就业的专业)...
  8. java从入门到放弃,学习路线导航(附学习资源)
  9. java查询城市区号,java根据手机号获取归属地
  10. English语法_并列连词-or