【Opencv】【OpenCV实践】【OpenCV的使用学习记录】【fmt学习记录】
- 0 前言
- 1 opencv使用说明
- 1.1 头文件的使用
- 1.2 CMakeLists.txt的使用
- 1.3 代码的基础使用
- 1.4 额外的功能代码
- 1.5 进阶使用
0 前言
- 全文代码参考【slam十四讲第二版】【课本例题代码向】【第五讲~相机与图像】【opencv3.4.1安装】【OpenCV、图像去畸变、双目和RGB-D、遍历图像像素14种】的3 OpenCV基本使用方法
1 opencv使用说明
1.1 头文件的使用
- 核心头文件
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
- 2d特征点(ORB特征点)
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>
- 好像使用这一个就行
#include <opencv2/opencv.hpp>
1.2 CMakeLists.txt的使用
find_package( OpenCV 3 REQUIRED )
include_directories(${OpenCV_INCLUDE_DIR})#或者 include_directories(${OpenCV_INCLUDE_DIRS})
add_executable(xxx src/xxx.cpp)
target_link_libraries( xxx ${OpenCV_LIBS} )
#或者
#target_link_libraries(xxx ${OpenCV_LIBRARIES})
1.3 代码的基础使用
- 头文件
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
1.3.1 定义图像格式
// 读取argv[1]指定的图像
cv::Mat image;
1.3.2 读取指定路径下的图像|并判断是否读取
1.3.2.1 读取方式
- 读取指定路径下的图像
1.1 读取指定文件
image = cv::imread("./src/ubuntu.png",CV_LOAD_IMAGE_COLOR);
1.2 定义指定文件路径串
string first_file = "../src/1.png";
cv::Mat first_image = cv::imread(first_file, 0);
1.3 读取输入变量
image = cv::imread(argv[1]); //cv::imread函数读取指定路径下的图像
1.4 读取组装路径
string path_to_dataset= "../data";
string associate_file = path_to_dataset + "/associate.txt";
color = cv::imread(path_to_dataset + "/" + rgb_file);//默认是IMREAD_COLOR = 1,彩色图
1.5 读取文件夹下所有文件并存储在vector里
vector<Mat> images; //图像
for ( int i=0; i<10; i++ )//遍历读取十张图像
{
string path = "./data/"+to_string(i+1)+".png";
images.push_back( imread(path) );
}
1.6 读取自定义变量组装的路径
for (int i = 0; i < 5; i++) {
boost::format fmt("../data/%s/%d.%s"); //图像文件格式
colorImgs.push_back(cv::imread((fmt % "color" % (i + 1) % "png").str()));
depthImgs.push_back(cv::imread((fmt % "depth" % (i + 1) % "png").str(), -1)); // 使用-1读取原始图像
}
1.7 给出图片所在文件夹,获取该文件夹下所有的图片路径
String directoryPath = "/home/bupo/my_study/slam14/slam14_my/cap11/gen_vocab_large/rgbd_dataset_freiburg1_desk2/rgb";//图像路径
vector<String> imagesPath;
cv::glob(directoryPath, imagesPath);
for ( String path : imagesPath )
{
}
- 判断是否读取
2.1 方式一
// 判断图像文件是否正确读取
if (image.data == nullptr) { //数据不存在,可能是文件不存在
cerr << "文件" << argv[1] << "不存在." << endl;
return 0;
}
2.2 方式二
//检查图片指针是否为空
assert(first_image.data != nullptr && second_image.data != nullptr);
- 判断输入变量个数是否满足要求
if (argc != 2) {
cout << "Usage: pose_graph_g2o_SE3 sphere.g2o" << endl;
return 1;
}
- 判断文件是否读取成功
ifstream fin(argv[1]);
if (!fin) {
cout << "file " << argv[1] << " does not exist." << endl;
return 1;
}
- 读取文件并判断文件是否读取结束
ifstream fin(path + "/first_200_frames_traj_over_table_input_sequence.txt");
if (!fin) return false;
while (!fin.eof()) {
// 数据格式:图像文件名 tx, ty, tz, qx, qy, qz, qw ,注意是 TWC 而非 TCW
string image;
fin >> image;
double data[7];
for (double &d:data) fin >> d;
color_image_files.push_back(path + string("/images/") + image);
poses.push_back(
SE3d(Quaterniond(data[6], data[3], data[4], data[5]),
Vector3d(data[0], data[1], data[2]))
);
if (!fin.good()) break;
}
fin.close();
- 读取图像深度(包含使用Mat初始化其长、宽、数据格式)
// load reference depth
fin.open(path + "/depthmaps/scene_000.depth");
ref_depth = cv::Mat(height, width, CV_64F);
if (!fin) return false;
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++) {
double depth = 0;
fin >> depth;
ref_depth.ptr<double>(y)[x] = depth / 100.0;
}
1.3.2.2 读取方式的含义
CV_LOAD_IMAGE_UNCHANGED
:数值为-1;可用于读取深度图,深度图为16位无符号数,单通道图像IMREAD_GRAYSCALE
:数值为0,用于读取单通道灰度图像CV_LOAD_IMAGE_COLOR
:默认值,数值为1;读取彩色图,RGB图像
1.3.3 图像的高、宽、通道数、灰度值
// 文件顺利读取, 首先输出一些基本信息
cout << "图像宽为" << image.cols << ",高为" << image.rows << ",通道数为" << image.channels() << endl;
输出:
图像宽为1200,高为674,通道数为3
- 灰度值的使用是图像保存为0,如1.3.2
2.1 使用at
//计算区域内的像素坐标,关键点坐标(x,y)+偏移坐标(dx,dy)
uchar pixel = img.at<uchar>(kp.pt.y+dy,kp.pt.x+dx);
2.2 使用ptr
- keypoints_1参考本文的1.5.1.2.1
- m就是
for(cv::DMatch m : matches)
,参考本文的1.5.3.6
d1 = cv::imread(argv[3], -1);// 深度图为16位无符号数,单通道图像
ushort d = d1.ptr<unsigned short> (int (keypoints_1[m.queryIdx].pt.y))[int (keypoints_1[m.queryIdx].pt.x)];
2.3 使用img.data[(x,y)]
const Mat &img
const Vector2d &pt
uchar *d = &img.data[int(pt(1, 0)) * img.step + int(pt(0, 0))];
- 获取行数和通道数
color.step
是指每一行有多少个字节color.channels()
是说每一组信息有多少个通道,一般是RGB图像中,是3
cv::Mat color = colorImgs[i];
p.b = color.data[v * color.step + u * color.channels()];
p.g = color.data[v * color.step + u * color.channels() + 1];
p.r = color.data[v * color.step + u * color.channels() + 2];
1.3.4 显示图像
cv::imshow("image", image); // 用cv::imshow显示图像
//如果值为1,表示等待1ms
cv::waitKey(0); // 暂停程序,等待一个按键输入
1.3.5 判断图像类型
// 判断image的类型
if (image.type() != CV_8UC1 && image.type() != CV_8UC3) {
// 图像类型不符合要求
cout << "请输入一张彩色图或灰度图." << endl;
return 0;
}
1.3.6 遍历图像的一种方式
// 遍历图像, 请注意以下遍历方式亦可使用于随机像素访问
// 使用 std::chrono 来给算法计时
chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
for (size_t y = 0; y < image.rows; y++) {
// 用cv::Mat::ptr获得图像的行指针
unsigned char *row_ptr = image.ptr<unsigned char>(y); // row_ptr是第y行的头指针
for (size_t x = 0; x < image.cols; x++) {
// 访问位于 x,y 处的像素
unsigned char *data_ptr = &row_ptr[x * image.channels()]; // data_ptr 指向待访问的像素数据
// 输出该像素的每个通道,如果是灰度图就只有一个通道
for (int c = 0; c != image.channels(); c++) {
unsigned char data = data_ptr[c]; // data为I(x,y)第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;
输出:
遍历图像用时:0.00867939 秒。
1.3.7 图像复制的两种方式
1.3.7.1 直接=赋值
- 这种方式修改 image_another 会导致 image 发生变化
- 这种方式类似于创建快捷方式
// 关于 cv::Mat 的拷贝
// 直接赋值并不会拷贝数据
cv::Mat image_another = image;
// 修改 image_another 会导致 image 发生变化
image_another(cv::Rect(0, 0, 100, 100)).setTo(0); // 将左上角100*100的块置零
cv::imshow("image", image);
cv::imshow("image_another", image_another);
cv::waitKey(0);
1.3.7.2 使用clone赋值
- 这种方式类似于拷贝
// 使用clone函数来拷贝数据
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);
1.3.8 关闭所有图像窗口
cv::destroyAllWindows();
1.4 额外的功能代码
1.4.1 随机数产生
- 定义随机数产生器
cv::RNG rng; // OpenCV随机数产生器 OpenCV random number generator
1.4.1.1 生成高斯随机数gaussion(double sigmma)
- 如果要产生均值为λ,标准差为σ的随机数,可以λ+ RNG::gaussian( σ)
double x;
x = exp(λ) + rng.gaussian(σ)
1.4.1.2 在设定阈值内生成随机数
int x = rng.uniform(10, 100);
1.4.2 保存图片
1.4.2.1 cv::Mat的填充
- 代码含义参考本文的1.5.1.5
cv::Mat outimg1;
cv::drawKeypoints(img_1,keypoints_1,outimg1,cv::Scalar::all(-1),cv::DrawMatchesFlags::DEFAULT);
1.4.2.2 进行保存
cv::Mat outimg1;
...
if(cv::imwrite("../src/ORB_features.png",outimg1) == false)
{
cout << "Failed to save the image" << endl;
}
1.4.3 Mat当作矩阵使用
1.4.3.1 矩阵定义和赋值cv::Mat<double>(3,3)
// 相机内参,TUM Freiburg2
cv::Mat K = (cv::Mat_<double>(3,3) << 520.9, 0, 325.1, 0, 521.0, 249.7, 0, 0, 1);
1.4.3.2 cout输出矩阵
cout << K << endl;
1.4.4 二维点cv::Point2f
1.4.4.1 定义
vector<cv::Point2f> points1;
1.4.4.2 赋值
- matches是1.5.1.6介绍
- keypoints_1是1.5.1.2.1 介绍
points1.push_back(keypoints_1[matches[i].queryIdx].pt);//匹配点对中第一张图片上的点
points2.push_back(keypoints_2[matches[i].trainIdx].pt);//匹配点对中第二张图片上的点
1.4.5 cv中的旋转矩阵R转换
1.4.5.1 旋转向量->旋转矩阵
cv::Mat r;
cv::Mat R;
cv::Rodrigues(r, R);//r为旋转向量形式,利用cv的Rodrigues()函数将旋转向量转换为旋转矩阵
1.4.6 画图
1.4.6.1 画圈(点)
cv::circle(img_show, kp, 10, cv::Scalar(0, 240, 0), 1);
参数1 img_show
:cv::Mat格式,表示圈要画在该图像中参数2 kp
:cv::Point2d格式,表示圈的圆心参数3 10
:表示圈的半径参数4 cv::Scalar(0, 240, 0)
:表示圈的颜色参数5 1
: 表示圈的边的粗细
1.4.6.2 画线
cv::line(img2_show, cv::Point2f(p_ref[0], p_ref[1]), cv::Point2f(p_cur[0], p_cur[1]), cv::Scalar(0, 250, 0));
参数1 img2_show
:cv::Mat格式,表示圈要画在该图像中参数2 cv::Point2f(p_ref[0], p_ref[1])
:cv::Point2f格式,表示线段的起点参数3 cv::Point2f(p_cur[0], p_cur[1])
:cv::Point2f格式,表示线段的终点参数4 cv::Scalar(0, 240, 0)
:表示圈的颜色参数5 1
: 表示圈的边的粗细
1.4.6.3 画矩形
- 参考 1.4.9.1 矩阵形状
1.4.6.3 转换颜色空间
-
将图像从一个颜色空间转换为另一个颜色空间。
-
该函数将输入图像从一个颜色空间转换为另一个颜色空间。在从RGB颜色空间转换的情况下,应该显式指定通道的顺序(RGB或BGR)。注意,OpenCV中的默认颜色格式通常被称为RGB,但它实际上是BGR(字节颠倒)。因此,一个标准(24位)彩色图像的第一个字节将是一个8位的蓝色组件,第二个字节将是绿色的,第三个字节将是红色的。第四个、第五个和第六个字节将是第二个像素(然后是蓝色、绿色、红色),依此类推。
-
常规的R, G和B通道值范围是:
0~255为CV_8U图象
0 ~ 65535为CV_16U图象
0~1 CV_32F图像 -
对于线性变换,值域无关紧要。但是在非线性转换的情况下,一个输入的RGB图像应该被规范化到适当的值范围,以得到正确的结果。
-
例如,如果你有一个32位浮点图像直接从一个8位图像转换而来,没有任何缩放,那么它将有0…255的取值范围,而不是0…函数假定的1。因此,在调用
cvtColor
之前,你需要先将图像缩小:
img *= 1./255;
cvtColor (img, img, COLOR_BGR2Luv);
- 如果你使用
cvtColor
与8位图像,转换会有一些信息丢失。对于许多应用程序,这将不明显,但建议使用32位图像的应用程序,需要全范围的颜色或在操作前转换图像,然后转换回来。 - 如果转换增加了alpha通道,其值将设置为相应通道范围的最大值:CV_8U的值为255,CV_16U的值为65535,CV_32F的值为1。
- 参数
-
@param
src
输入图像:8位无符号,16位无符号(CV_16UC…),或者单精度浮点。 -
@param
DST
输出与src相同大小和深度的图像。 -
@param
code
颜色空间转换代码(参见# colorconverversioncodes)。
可取值如下
COLOR_GRAY2BGR = 8,
- @param dstCn目标映像中的通道数;如果参数为0,则表示通道是自动从SRC和代码派生的。
CV_EXPORTS_W void cvtColor( InputArray src, OutputArray dst, int code, int dstCn = 0 );
例如:
cv::cvtColor(ref, ref_show, CV_GRAY2BGR);//表示将当前灰度图像转变为RGB图像
1.4.7 从参考图像中获取灰度值(双线性插值)
- 函数声明和函数实现方法一
/**
* get a gray scale value from reference image (bi-linear interpolated)
* 从参考图像中获取灰度值(双线性插值)
* @param img
* @param x
* @param y
* @return the interpolated value of this pixel
*/
inline float GetPixelValue(const cv::Mat &img, float x, float y) {
// boundary check
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x >= img.cols) x = img.cols - 1;
if (y >= img.rows) y = img.rows - 1;
uchar *data = &img.data[int(y) * img.step + int(x)];
float xx = x - floor(x);
float yy = y - floor(y);
return float(
(1 - xx) * (1 - yy) * data[0] +
xx * (1 - yy) * data[1] +
(1 - xx) * yy * data[img.step] +
xx * yy * data[img.step + 1]
);
}
- 方法二
// 双线性灰度插值
inline double getBilinearInterpolatedValue(const Mat &img, const Vector2d &pt) {
uchar *d = &img.data[int(pt(1, 0)) * img.step + int(pt(0, 0))];//读取
double xx = pt(0, 0) - floor(pt(0, 0));
double yy = pt(1, 0) - floor(pt(1, 0));
return ((1 - xx) * (1 - yy) * double(d[0]) +
xx * (1 - yy) * double(d[1]) +
(1 - xx) * yy * double(d[img.step]) +
xx * yy * double(d[img.step + 1])) / 255.0;
}
1.4.8 图片重新赋予尺寸
- 函数声明
void resize( InputArray src, OutputArray dst,
Size dsize, double fx = 0, double fy = 0,
int interpolation = INTER_LINEAR );
- 使用案例
摘自:【slam十四讲第二版】【课本例题代码向】【第十三讲~实践:设计SLAM系统】的3.2.4.3 NextFrame()
cv::resize(image_left, image_left_resized, cv::Size(), 0.5, 0.5,
cv::INTER_NEAREST);//cv::INTER_NEAREST = 最近邻插值
- 参数
image_left
: 格式cv::Mat
,输入的原图像 - 参数
image_left_resized
:格式cv::Mat
,输出的改变尺寸后的图像 - 参数
cv::Size()
:也就是dsize
,输出图像大小;如果它等于零,则计算为dsize=Size((fx*src.cols),(fy*src.rows)))
- 参数
0.5
:也就是fx - 参数
0.5
:也就是fy - 参数
cv::INTER_NEAREST
:对于插值方法,
- 插值方法
INTER_NEAREST
:也就是0,即最近邻插值INTER_LINEAR
:也就是1,即线性插值
- dsize或fx和fy都必须为非零。
1.4.9 裁取指定区域
1.4.9.1 矩阵形状
- 摘自与:【slam十四讲第二版】【课本例题代码向】【第十三讲~实践:设计SLAM系统】的3.2.6.14 DetectFeatures()
- 如下:
cv::rectangle(mask, feat->position_.pt - cv::Point2f(10, 10),
feat->position_.pt + cv::Point2f(10, 10), 0, CV_FILLED);
- 函数声明:
函数cv::rectangle
绘制一个矩形轮廓或一个填充矩形,其两个相对的角是pt1和pt2。
CV_EXPORTS_W void rectangle(InputOutputArray img, Point pt1, Point pt2,
const Scalar& color, int thickness = 1,
int lineType = LINE_8, int shift = 0);
- 参数
img
:输入图像也是输出图像 - 参数
pt1
:相对的角之一 - 参数
pt2
:相对的角之二 - 参数
color
:矩形颜色或亮度(灰度图像) - 参数
thickness
:默认是1,构成矩形的线的厚度。负值,如CV_FILLED
,意味着函数必须绘制一个填充矩形。 - 参数
lineType
:默认是LINE_8
,线条类型,还有抗锯齿线LINE_AA
- 参数
shift
:默认是0,点坐标中的小数位数。
1.4.10 对mat矩阵的一些操作
1.4.10.1 求特征值和特征向量cv::eigen()
bool cv::eigen ( InputArray src,
OutputArray eigenvalues,
OutputArray eigenvectors = noArray()
)
- 解析:
src:输入矩阵,只能是 CV_32FC1 或 CV_64FC1 类型的方阵(即矩阵转置后还是自己),可以使用定义方法cv::Mat matA(pointSelNum, 3, CV_32F, cv::Scalar::all(0));
eigenvalues:输出的特征值组成的向量,数据类型同输入矩阵,排列从大到小
eigenvectors:输出的特征向量组成的矩阵,数据类型同输入矩阵,每一行是一个特征向量,对应相应位置的特征值 - 备注: 对于非对称矩阵,可以使用
cv::eigenNonSymmetric()
计算特征值和特征向量。
1.4.10.2 对矩阵求转置 cv::transpose()
cv::Mat matA(pointSelNum, 3, CV_32F, cv::Scalar::all(0));
cv::Mat matAt(3, pointSelNum, CV_32F, cv::Scalar::all(0));
cv::transpose(matA, matAt);
1.4.10.3 矩阵的复制mat.copy()
//将matV复制给matV2
cv::Mat matV(3, 3, CV_32F, cv::Scalar::all(0));
cv::Mat matV2(3, 3, CV_32F, cv::Scalar::all(0));
matV.copyTo(matV2);
1.4.11 cv对数据的转换
1.4.11.1 eigen矩阵转换为cv::Mat矩阵cv::eigen2cv()
Eigen::Matrix3d R_initial;
cv::Mat tmp_r;
cv::eigen2cv(R_initial, tmp_r);
1.4.11.2 cv::Mat矩阵转换为eigen矩阵cv::cv2eigen()
cv::Mat r;
MatrixXd R_pnp;
cv::cv2eigen(r, R_pnp);
1.4.11.3 cv::Mat的旋转矩阵转换为cv::Mat格式的旋转向量cv::Rodrigues()
cv::Mat rvec, t, tmp_r;
cv::Rodrigues(tmp_r, rvec);
1.4.11.4 cv::Mat的旋转向量转换为cv::Mat格式的旋转矩阵cv::Rodrigues()
cv::Mat rvec, r;
cv::Rodrigues(rvec, r);
1.5 进阶使用
1.5.1 OpenCV的ORB特征
- 参考【slam十四讲第二版】【课本例题代码向】【第七讲~视觉里程计Ⅰ】【1OpenCV的ORB特征】【2手写ORB特征】【3对极约束求解相机运动】【4三角测量】【5求解PnP】【3D-3D:ICP】的1 OpenCV的ORB特征
1.5.1.1 头文件
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>
1.5.1.2 初始化
1.5.1.2.1 定义特征点和描述子
std::vector<cv::KeyPoint> keypoints_1, keypoints_2;
cv::Mat descriptors_1, descriptors_2;
1.5.1.2.1.1 遍历descriptors_1
for(int i = 0; i < descriptors_1.rows; i++)
{
...
}
1.5.1.2.1.2 获取匹配对matches里的某一对的点
points1.push_back(keypoints_1[matches[i].queryIdx].pt);//匹配点对中第一张图片上的点
points2.push_back(keypoints_2[matches[i].trainIdx].pt);//匹配点对中第二张图片上的点
1.5.1.2.2 定义ORB提取器
cv::Ptr<cv::FeatureDetector> detector = cv::ORB::create();
cv::Ptr<cv::DescriptorExtractor> descriptor = cv::ORB::create();
1.5.1.2.2.1 补充:定义FAST提取器
cv::Ptr<cv::FastFeatureDetector> detector = cv::FastFeatureDetector::create();
detector->detect(img_1, keypoints);
1.5.1.2.2.2 补充:定义GFTT角点提取器
- 之前角点检测的算法中,一个是cornerHarris计算角点,但是这种角点检测算法容易出现聚簇现象以及角点信息有丢失和位置偏移现象,
- 所以后面又提出一种名为goodFeatureToTrack的角点检测算法,opencv的feature2D接口集成了这种算法,名称为 GFTTDetector。
Ptr<GFTTDetector> detector = GFTTDetector::create(500, 0.01, 20); // maximum 500 keypoints
detector->detect(img1, keypoints);
参数1 500
:即maxorners,也就是最大的角点数量参数2 0.01
:没搞明白,默认就是0.01参数3 20
:角点之间最小距离
1.5.1.2.2.3 补充:提取器的使用的三个参数
- 正常就是前面使用的类似
detector->detect(img1, keypoints);
,但是要知道这个是可以有第三个参数的,默认是不是用该功能 - 函数声明如下:
CV_WRAP virtual void detect( InputArray image,
CV_OUT std::vector<KeyPoint>& keypoints,
InputArray mask=noArray() );
image
:就是输入的图像keypoints
:就是提取得到的特征点mask
:指定在何处查找关键点的mask(可选)。它必须是8位整数在感兴趣区域中具有非零值的矩阵。我的理解是在这一块不进行特征点提取,跳过,如果有误,之后再改,暂时先这样理解
- 这个矩阵的定义类似于使用
cv::rectangle()
函数,具体的使用参考本文的1.4.9.1 矩阵形状
1.5.1.2.3 定义匹配器
cv::Ptr<cv::DescriptorMatcher> matcher = cv::DescriptorMatcher::create("BruteForce-Hamming");
1.5.1.3 检测Oriented FAST角点位置
detector->detect(img_1, keypoints_1);
detector->detect(img_2, keypoints_2);
1.5.1.4 根据角点位置计算BRIFE描述子
descriptor->compute(img_1,keypoints_1,descriptors_1);
descriptor->compute(img_2,keypoints_2,descriptors_2);
1.5.1.4.1 同时检测ORB特征点和计算描述子
detector->detectAndCompute( img_1, Mat(), keypoints_1, descriptors_1 );//检测和计算
- 具体的使用可以参考:【slam十四讲第二版】【课本例题代码向】【第十一讲~回环检测】【DBoW3的安装】【创建字典】【相似度检测】【增加字典规模】的2 创建字典
1.5.1.5 将图片和特征点绘制在一起
cv::Mat outimg1;
cv::drawKeypoints(img_1,keypoints_1,outimg1,cv::Scalar::all(-1),cv::DrawMatchesFlags::DEFAULT);
cv::imshow("ORB features", outimg1);
cv::waitKey(0);
1.5.1.6 对两张图像的BRIFE描述子进行匹配cv::DMatch
- 使用Hamming距离
1.5.1.6.1 定义用于匹配关键点描述子的类
vector<cv::DMatch> matches; //cv::DMatch 用于匹配关键点描述子的类
1.5.1.6.1.1 输出匹配对的距离
double dist = matches[i].distance;
1.5.1.6.1.2 输出匹配对的大小
- 这个应该是vector的内置函数,而不是
cv::DMatch
的
cout<<"一共找到了"<<matches.size() <<"组匹配点"<<endl;
1.5.1.6.1.3 输出其匹配点对在各自点云的下标
- keypoints_1是1.5.1.2.1介绍
- points1是1.4.4介绍
- 主要图
matches[i].queryIdx
- 使用举例:
points1.push_back(keypoints_1[matches[i].queryIdx].pt);//匹配点对中第一张图片上的点
- 配对图
matches[i].trainIdx
- 使用举例
points2.push_back(keypoints_2[matches[i].trainIdx].pt);//匹配点对中第二张图片上的点
1.5.1.6.2 进行匹配
matcher->match(descriptors_1,descriptors_2,matches);
1.5.1.7 匹配点筛选
1.5.1.7.1 计算最小距离和最大距离
//--第四步:匹配点对筛选
//计算最小距离和最大距离
auto min_max = minmax_element(matches.begin(),matches.end(),
[](const cv::DMatch &m1, const cv::DMatch &m2){return m1.distance<m2.distance;});
double min_dist = min_max.first->distance;
double max_dist = min_max.second->distance;
1.5.1.7.2 筛选匹配失误的匹配
//当描述子之间的距离大于两倍的最小距离时,即认为匹配有误。但有时最小距离会非常小,所以要设置一个经验值30作为下限
std::vector<cv::DMatch> good_matches;
for(int i = 0; i < descriptors_1.rows;i++)
{
if(matches[i].distance <= max(2 * min_dist, 30.0))
{
good_matches.push_back(matches[i]);
}
}
1.5.1.7.3 将两张图像与它们的特征点和匹配对绘制在一起
//--第五步:绘制匹配结果
cv::Mat img_match;
cv::Mat img_goodmatch;
cv::drawMatches(img_1, keypoints_1, img_2,keypoints_2,matches,img_match);
cv::drawMatches(img_1, keypoints_1, img_2,keypoints_2,good_matches,img_goodmatch);
cv::imshow("all matches", img_match);
cv::imshow("good matches", img_goodmatch);
cv::waitKey(0);
1.5.2 手写ORB特征
- 参考【slam十四讲第二版】【课本例题代码向】【第七讲~视觉里程计Ⅰ】【1OpenCV的ORB特征】【2手写ORB特征】【3对极约束求解相机运动】【4三角测量】【5求解PnP】【3D-3D:ICP】的2 手写ORB特征
1.5.2.1 头文件
#include <opencv2/opencv.hpp>
1.5.2.2 利用FAST从图中提取关键点keypoints
//ORB使用FAST算法检测特征点
//OpenCV中的ORB采用了图像金字塔来解决尺度变换一致性
//****自定义ComputeORB函数来描述ORB特征点,并旋转使其具备旋转尺度不变性
vector<cv::KeyPoint> keypoints1;
// ORB提取图1特征threshold=40
//利用FAST从图1中提取关键点keypoints1
cv::FAST(first_image,keypoints1,40);
1.5.3 对极约束求解相机运动
- 参考【slam十四讲第二版】【课本例题代码向】【第七讲~视觉里程计Ⅰ】【1OpenCV的ORB特征】【2手写ORB特征】【3对极约束求解相机运动】【4三角测量】【5求解PnP】【3D-3D:ICP】的3 对极约束求解相机运动
1.5.3.1 计算基础矩阵cv::findFundamentalMat()
- 基础矩阵F(p167)
//-- 计算基础矩阵
cv::Mat fundamental_matrix;
//计算给定一组对应点的基本矩阵 八点法
fundamental_matrix = cv::findFundamentalMat(points1, points2,CV_FM_8POINT);
cout << "fundamental_matrix is" << endl << fundamental_matrix << endl;
1.5.3.2 计算本质矩阵cv::findEssentialMat()
- 本质矩阵E(p167~p168)
- 相机光心,就是相机内参数的cx、cy
- 相机焦距,这里我不确定,但是我感觉是fx、fy,这俩相等?
//-- 计算本质矩阵
cv::Point2d principal_point (325.1, 249.7); //相机光心, TUM dataset标定值
double focal_length = 521; //相机焦距, TUM dataset标定值
cv::Mat essential_matrix;
essential_matrix = cv::findEssentialMat(points1,points2,focal_length,principal_point);
cout<<"essential_matrix is "<<endl<< essential_matrix<<endl;
1.5.3.3 计算单应矩阵cv::findHomography()
- 单应矩阵H(p170)
RANSAC
:(Random Sample Concesus, RANSAC,随即采样一致性),非最小二乘法。其适用于很多带错误数据的情况,可以处理带有错误匹配的数据。
//-- 计算单应矩阵
cv::Mat homography_matrix;
homography_matrix = cv::findHomography(points1, points2, cv::RANSAC, 3);
cout<<"homography_matrix is "<<endl<<homography_matrix<<endl;
1.5.3.4 从本质矩阵中恢复旋转和平移信息cv::recoverPose()
- 相机光心principal_point,就是相机内参数的cx、cy
- 相机焦距focal_length,这里我不确定,但是我感觉是fx、fy,这俩相等?
- 本质矩阵essential_matrix
//-- 从本质矩阵中恢复旋转和平移信息.
cv::recoverPose(essential_matrix, points1, points2, R,t,
focal_length,principal_point);
cout<<"R is "<<endl<<R<<endl;
cout<<"t is "<<endl<<t<<endl;
1.5.3.5 把像素坐标转换为相机归一化坐标函数pixel2cam ()
- 函数声明
// 像素坐标转相机归一化坐标
cv::Point2d pixel2cam (const cv::Point2d& p, const cv::Mat& K);
- 函数实现
// 像素坐标转相机归一化坐标
cv::Point2d pixel2cam (const cv::Point2d& p, const cv::Mat& K)
{
return cv::Point2d( //at是内参数矩阵
(p.x - K.at<double> (0,2)) / K.at<double>(0,0),
(p.y - K.at<double> (1,2)) / K.at<double>(1,1)
);
}
1.5.3.6 验证对级约束
- 参考书上p167的公式p7.8
//-- 验证对极约束 // 相机内参
cv::Mat K = (cv::Mat_<double>(3,3) << 520.9, 0, 325.1, 0, 521.0, 249.7, 0,0,1);
for(cv::DMatch m:matches)
{
cv::Point2d pt1 = pixel2cam(keypoints_1[m.queryIdx].pt,K);
cv::Mat y1 = (cv::Mat_<double>(3,1) << pt1.x,pt1.y,1);
cv::Point2d pt2 = pixel2cam ( keypoints_2[ m.trainIdx ].pt, K );
cv::Mat y2 = ( cv::Mat_<double> ( 3,1 ) << pt2.x, pt2.y, 1 );
cv::Mat d = y2.t() * t_x * R * y1;
cout << "epipolar constraint = " << d << endl;
}
1.5.4 三角测量
- 参考【slam十四讲第二版】【课本例题代码向】【第七讲~视觉里程计Ⅰ】【1OpenCV的ORB特征】【2手写ORB特征】【3对极约束求解相机运动】【4三角测量】【5求解PnP】【3D-3D:ICP】的4 三角测量
1.5.4.1 实现三角测量的函数triangulation()
pixel2cam()函数
参考本文的1.5.3.5
- 函数声明
//加入了三角测量部分
void triangulation(const vector<cv::KeyPoint>& keypoint_1, const vector<cv::KeyPoint>& keypoint_2,
const std::vector<cv::DMatch>& matches, const cv::Mat& R, const cv::Mat& t,
vector<cv::Point3d>& points);
- 函数实现
//加入了三角测量部分
void triangulation(const vector<cv::KeyPoint>& keypoint_1, const vector<cv::KeyPoint>& keypoint_2,
const std::vector<cv::DMatch>& matches, const cv::Mat& R, const cv::Mat& t,
vector<cv::Point3d>& points)
{
cv::Mat T1 = (cv::Mat_<float> (3,4) <<
1,0,0,0,
0,1,0,0,
0,0,1,0);
cv::Mat T2 = (cv::Mat_<float> (3,4) <<
R.at<double>(0,0), R.at<double>(0,1), R.at<double>(0,2), t.at<double>(0,0),
R.at<double>(1,0), R.at<double>(1,1), R.at<double>(1,2), t.at<double>(1,0),
R.at<double>(2,0), R.at<double>(2,1), R.at<double>(2,2), t.at<double>(2,0));
cv::Mat K = (cv::Mat_<double> (3,3) << 520.9, 0, 325.1, 0, 521.0, 249.7, 0, 0, 1);
vector<cv::Point2f> pts_1,pts_2;
for(cv::DMatch m : matches)
{
// 将像素坐标转换至相机坐标
pts_1.push_back(pixel2cam(keypoint_1[m.queryIdx].pt, K));
pts_2.push_back(pixel2cam(keypoint_2[m.trainIdx].pt, K));
}
cv::Mat pts_4d;
//第一个相机的3x4投影矩阵。
//第2个相机的3x4投影矩阵。
cv::triangulatePoints(T1,T2,pts_1,pts_2,pts_4d);
// 转换成非齐次坐标
for(int i = 0; i < pts_4d.cols; i++)
{
cv::Mat x = pts_4d.col(i);
x /= x.at<float>(3,0);// 归一化
cv::Point3d p(x.at<float>(0,0), x.at<float>(1,0), x.at<float>(2,0));
points.push_back(p);
}
}
1.5.4.2 验证三角化点与特征点的重投影关系
- 输出结果查看【slam十四讲第二版】【课本例题代码向】【第七讲~视觉里程计Ⅰ】【1OpenCV的ORB特征】【2手写ORB特征】【3对极约束求解相机运动】【4三角测量】【5求解PnP】【3D-3D:ICP】的4.3 三角测量
- 这里主要是将原本的点与经过三角测量计算深度并还原后对比
//-- 验证三角化点与特征点的重投影关系
cv::Mat K = (cv::Mat_<double>(3,3) << 520.9, 0, 325.1, 0, 521.0, 249.7, 0, 0, 1 );
for (int i = 0; i < matches.size(); i++)
{
//第一个图
cv::Point2d pt1_cam = pixel2cam(keypoints_1[matches[i].queryIdx].pt,K);
cv::Point2d pt1_cam_3d(points[i].x/points[i].z, points[i].y/points[i].z);
cout<<"point in the first camera frame: "<<pt1_cam<<endl;
cout<<"point projected from 3D "<<pt1_cam_3d<<", d="<<points[i].z<<endl;
// 第二个图
cv::Point2f pt2_cam = pixel2cam(keypoints_2[matches[i].trainIdx].pt,K);
cv::Mat pt2_trans = R * (cv::Mat_<double>(3,1) << points[i].x,points[i].y,points[i].z) + t;
pt2_trans /= pt2_trans.at<double>(2,0);
cout<<"point in the second camera frame: "<<pt2_cam<<endl;
cout<<"point reprojected from second frame: "<<pt2_trans.t()<<endl;
cout<<endl;
}
1.5.5 求解PnP
- 参考【slam十四讲第二版】【课本例题代码向】【第七讲~视觉里程计Ⅰ】【1OpenCV的ORB特征】【2手写ORB特征】【3对极约束求解相机运动】【4三角测量】【5求解PnP】【3D-3D:ICP】的5求解PnP
1.5.5.1 cv的PnP计算函数
1.5.5.1.1 直接线性变换(默认)cv::SOLVEPNP_ITERATIVE
.
参数pts_3d
:为目标点在该图片相机坐标系下的三维坐标,输入量参数pts_2d
:为匹配点在该相机坐标系下的归一化非齐次二位坐标,输入量参数K
,相机内参,输入量cv:Mat()
:这个参数指的是畸变系数向量参数r
:求得的旋转向量形式,可以使用cv::Rodrigues(r, R);
将其转换为旋转矩阵参数t
:求得的平移向量参数false
:false表示输入的r t不作为初始化值 如果是true则此时会把t r作为初始值进行迭代参数flags
:求解的方法返回值bool
:计算成功就返回true
SOLVEPNP_ITERATIVE = 0, //默认值
SOLVEPNP_EPNP = 1, //!< EPnP: Efficient Perspective-n-Point Camera Pose Estimation @cite lepetit2009epnp
SOLVEPNP_P3P = 2, //!< Complete Solution Classification for the Perspective-Three-Point Problem @cite gao2003complete
SOLVEPNP_DLS = 3, //!< A Direct Least-Squares (DLS) Method for PnP @cite hesch2011direct
SOLVEPNP_UPNP = 4, //!< Exhaustive Linearization for Robust Camera Pose and Focal Length Estimation @cite penate2013exhaustive
SOLVEPNP_AP3P = 5, //!< An Efficient Algebraic Solution to the Perspective-Three-Point Problem @cite Ke17
SOLVEPNP_MAX_COUNT //!< Used for count
- 代码实例如下
cv::Mat r,t;
//Mat()这个参数指的是畸变系数向量
// 调用OpenCV 的 PnP 求解,可选择EPNP,DLS等方法
cv::solvePnP( pts_3d, pts_2d, K, cv::Mat(), r, t, false ,cv::SOLVEPNP_ITERATIVE);
1.5.5.1.1 EPNP方法cv::SOLVEPNP_EPNP
- 仅需将
参数flags
改为cv::SOLVEPNP_EPNP
1.5.5.1.2 UPNP方法cv::SOLVEPNP_UPNP
- 仅需将
参数flags
改为cv::SOLVEPNP_UPNP
1.5.5.1.3 DLS方法cv::SOLVEPNP_DLS
*仅需将参数flags
改为cv::SOLVEPNP_DLS
1.5.5.1.4 P3P方法cv::SOLVEPNP_P3P
- 由于其只需要4个点对
vector<cv::Point3f> pts_p3p_3d;//创建容器pts_3d存放3d点(图1对应的特征点的相机坐标下的3d点)
vector<cv::Point2f> pts_p3p_2d;//创建容器pts_2d存放图2的特征点
//取出其中的4个点对
for (int i = 0; i < 4; i++)
{
pts_p3p_3d.push_back(pts_3d[i]);
pts_p3p_2d.push_back(pts_2d[i]);
}
t1 = chrono::steady_clock::now();
//Mat()这个参数指的是畸变系数向量
cv::solvePnP(pts_p3p_3d, pts_p3p_2d, K, cv::Mat(), r, t, false,cv::SOLVEPNP_P3P); // 调用OpenCV 的 PnP 求解,可选择EPNP,DLS等方法
1.5.5.2 利用RGB图和深度图还原三维点
pixel2cam()函数
参考本文的1.5.3.5
// 建立3D点,把深度图信息读进来,构造三维点
//d1 = cv::imread(argv[3], -1);// 深度图为16位无符号数,单通道图像
cv::Mat K = (cv::Mat_<double>(3,3) << 520.9, 0, 325.1, 0, 521.0, 249.7, 0, 0, 1 );
vector<cv::Point3f> pts_3d;//创建容器pts_3d存放3d点(图1对应的特征点的相机坐标下的3d点)
for(cv::DMatch m : matches)
{
//把对应的图1的特征点的深度信息拿出来
ushort d = d1.ptr<unsigned short> (int (keypoints_1[m.queryIdx].pt.y))[int (keypoints_1[m.queryIdx].pt.x)];
if(d == 0) // bad depth
continue;
float dd = d/5000.0;//用dd存放换算过尺度的深度信息
cv::Point2d p1 = pixel2cam(keypoints_1[m.queryIdx].pt, K);//p1里面放的是图1特征点在相机坐标下的归一化坐标(只包含 x,y)
pts_3d.push_back(cv::Point3f (p1.x *dd, p1.y*dd,dd));//得到图1特征点在相机坐标下的3d坐标
}
cout << "3d-2d pairs: " << pts_3d.size() << endl;//3d-2d配对个数得用pts_3d的size
1.5.6 光流追踪
使用带金字塔的迭代Lucas-Kanade方法计算稀疏特征集的光流。
1.5.6.1 头文件的使用
#include <opencv2/video/tracking.hpp>
1.5.6.2 光流追踪函数cv::calcOpticalFlowPyrLK()
1.5.6.1.1 声明函数
CV_EXPORTS_W void calcOpticalFlowPyrLK( InputArray prevImg, InputArray nextImg,
InputArray prevPts, InputOutputArray nextPts,
OutputArray status, OutputArray err,
Size winSize = Size(21,21), int maxLevel = 3,
TermCriteria criteria = TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 0.01),
int flags = 0, double minEigThreshold = 1e-4 );
-
参数
prevPts
:由buildOpticalFlowPyramid构建的第一个8位输入图像或金字塔。 -
参数
nextImg
:第二输入图像或与prevImg相同大小和类型的金字塔。 -
参数
prevPts
:需要找到LK流的2D点的向量;点坐标必须是单精度浮点数。 -
参数
nextPts
:2D点的输出向量(具有单精度浮点坐标),其包含第二图像中输入特征的计算出的新位置;当传递OPTFLOW_USE_INITIAL_FLOW标志时,向量的大小必须与输入中的大小相同。 -
参数
status
:输出状态向量(无符号字符);如果找到了相应特征的流,则将向量的每个元素设置为1,否则,将其设置为0。 -
参数
err
:误差的输出矢量;向量的每个元素都被设置为对应特征的误差,误差度量的类型可以在flags参数中设置;如果未找到流,则未定义错误(使用状态参数查找此类情况)。 -
参数
winSize
:每个金字塔级别的搜索窗口大小。 -
参数
maxLevel
:基于0的最大金字塔级数;如果设置为0,则不使用金字塔(单个级别),如果设置为1,则使用两个级别,依此类推;若金字塔被传递给输入,那个么算法将使用和金字塔一样多的级别,但不超过maxLevel。 -
参数
criteria
:参数,指定迭代搜索算法的终止条件(在指定的最大迭代次数criteria.maxCount之后或搜索窗口移动小于Criterias.epsilon时)。 -
参数
flags
:操作标志:
-OPTFLOW_USE_INITIAL_FLOW使用存储在下一个TPTS中的初始估计;如果未设置该标志,则将prevPts复制到下一个TPTS,并将其视为初始估计。
-OPTFLOW_LK_GET_MIN_EIGENVALS使用最小特征值作为误差度量(请参见minEigThreshold描述);如果未设置该标志,则使用原始点和移动点周围的面片之间的L1距离除以窗口中的像素数作为误差度量。 -
参数
minEigThreshold
:该算法计算光流方程的2x2法向矩阵(该矩阵在@cite Bouguet00中称为空间梯度矩阵)的最小特征值,除以窗口中的像素数;如果该值小于minEigThreshold,则会过滤出相应的功能,并且不会处理其流程,因此可以消除缺点并提高性能。
1.5.6.1.2 应用实例一
cv::calcOpticalFlowPyrLK( last_color, color, prev_keypoints, next_keypoints, status, error );
其中
参数1 last_color
:cv::Mat格式,这是上一个图片的Mat格式;已知量参数2 color
:cv::Mat格式,这是当前图片的Mat;已知量参数3 prev_keypoints
:vectorcv::Point2f格式,这是上一个图片所提取的fast特征点;已知量参数4 next_keypoints
:vectorcv::Point2f格式,这是经过计算得到的当前图片的fast特征点;所求量参数5 status
:vector格式,根据其是否为1可以确定对应的点是否被正确的追踪到。参数6 error
:vector格式,这是每个特征点在上一个和当前图片之间的特征点的偏差
1.5.6.1.3 应用实例二
- 摘自:【slam十四讲第二版】【课本例题代码向】【第十三讲~实践:设计SLAM系统】的3.2.6.15 FindFeaturesInRight()
cv::calcOpticalFlowPyrLK(
current_frame_->left_img_, current_frame_->right_img_, kps_left,
kps_right, status, error, cv::Size(11, 11), 3,
cv::TermCriteria(cv::TermCriteria::COUNT + cv::TermCriteria::EPS, 30,0.01),
cv::OPTFLOW_USE_INITIAL_FLOW);