批量的状态估计问题可以通过构建最小二乘的方式求解。
针孔相机模型:相机将三维世界中的坐标点(单位为米)映射到二维图像平面上(单位为像素)的过程。由于相机镜头上的透镜的存在,使得光线投影到成像平面的过程会产生畸变,因此,我们用针孔和畸变两个模型来描述整个投影过程。
小孔成像得到的像是倒立的,为了让模型符合实际,我们可以等价的把成像平面对称的放到相机的前面,和三维空间点一起放到摄像机坐标系的同一侧。
空间点的单位为米,不过,在相机中,我们最终获得的是一个个的像素,这还需要在成像平面上对像进采样和量化。为了描述传感器将感受到的光线转换成图像像素的过程,我们设在物理成像平面上固定着一个像素平面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相机按原理分为:
图像在数学中,可以用一个矩阵来描述;在计算机中,占据了一连串连续的磁盘或空间,可以用二维数组来表示。
计算机中的图像-——灰度图,在灰度图中,每个像素位置(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描述,用李代数来优化)的等式不是精确的。
状态估计的方式:
最大似然估计的意义:“在什么样的状态下,最可能产生现在观测到的数据”
由上式的最后一项可以看出:该式等价于最小化噪声项(误差)的一个二次型,这个二次型称为马哈拉若比斯距离也叫做马氏距离
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
#include
using namespace std;
#include
#include
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})
#include
#include
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
#include
#include
#include
#include //linxu系统内置头文件,包含许多系统服务器的内置函数 如read,write, usleep等
#include
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位浮点型Mat
disparity_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; //由深度信息获取真实坐标系下的X
point[1] = y * depth;
point[2] = depth;
pointcloud.push_back(point); //获取相机坐标系下的点云位置
}
}
cv::imshow("disparity" , disparity / 96.0); //把视差限制在0-96
cv::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
#include
#include
#include
#include
#include
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()]; //b
p[4] = color.data[v * color.step + u * color.channels()+ 1]; //g
p[3] = color.data[v * color.step + u * color.channels() + 2]; //r
pointcloud.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})