SLAM14-5
注意: 建议安装 opencv-3.4.1 此外按照14讲书中将opencv依赖项安装上
如何在CLion中带参数进行调试程序:
1.运行--> 编辑配置
2.工作目录(当前项目所在目录)
3.程序实参 (命令行需要输入的参数路径)
一、图像基本容器–Mat
Mat
是一个类,由两个数据部分组成(矩阵头以及矩阵本身):矩阵头(包含矩阵尺寸,存储方法,存储地址等信息)和一个指向存储所有像素值的矩阵(根据所选存储方法的不同矩阵可以是不同的维数)的指针。**矩阵头的尺寸是常数值,但矩阵本身的尺寸会依图像的不同而不同,通常比矩阵头的尺寸大数个数量级。**因此,当在程序中传递图像并创建拷贝时,大的开销是由矩阵造成的,而不是信息头。OpenCV是一个图像处理库,囊括了大量的图像处理函数,为了解决问题通常要使用库中的多个函数,因此在函数中传递图像是家常便饭。同时不要忘了我们正在讨论的是计算量很大的图像处理算法,因此,除非万不得已,我们不应该拷贝大的图像,因为这会降低程序速度。
如何解决在拷贝图像时,由于矩阵本身数据大造成的巨大开销:
OpenCV使用引用计数机制。其思路是让每个Mat对象有自己的信息头**,但共享同一个矩阵。这通过让矩阵指针指向同一地址而实现**。而拷贝构造函数则 只拷贝信息头和矩阵指针 ,而不拷贝矩阵。
Mat A, C; // 只创建信息头部分
A = imread(argv[1], CV_LOAD_IMAGE_COLOR); // 这里为矩阵开辟内存
Mat B(A); // 使用拷贝构造函数
C = A; // 赋值运算符
以上代码中的所有Mat对象最终都指向同一个也是唯一一个数据矩阵。虽然它们的信息头不同,但通过任何一个对象所做的改变也会影响其它对象。实际上,不同的对象只是访问相同数据的不同途径而已。
如果矩阵属于多个 Mat 对象,那么当不再需要它时谁来负责清理?简单的回答是:最后一个使用它的对象。通过引用计数机制来实现。无论什么时候有人拷贝了一个 Mat 对象的信息头,都会增加矩阵的引用次数;反之当一个头被释放之后,这个计数被减一;当计数值为零,矩阵会被清理。但某些时候你仍会想拷贝矩阵本身(不只是信息头和矩阵指针),这时可以使用函数 clone() 或者 copyTo() 。
Mat F = A.clone();
Mat G;
A.copyTo(G);
现在改变 F 或者 G 就不会影响 Mat 信息头所指向的矩阵。总结一下,你需要记住的是:
- OpenCV函数中输出图像的内存分配是自动完成的(如果不特别指定的话)。
- 使用OpenCV的C++接口时不需要考虑内存释放问题。
- 赋值运算符和拷贝构造函数( ctor )只拷贝信息头。
- 使用函数 cv::Mat::clone() 或者 cv::Mat::copyTo() 来拷贝一副图像的矩阵数据。
1.显示创建Mat对象
Mat
不但图像容器类,它同时也是一个通用的矩阵类,所以可以用来创建和操作多维矩阵。创建一个Mat
对象有多种方法:
cv::Mat() 构造函数
Mat M(2,2, CV_8UC3, Scalar(0,0,255)); // 矩阵大小 2*2 可以装下 4个像素值 每个像素由三个通道组成 则矩阵有 2*2*3个元素
cout << "M = " << endl << " " << M << endl << endl;
// CV_8UC3 表示Mat类对象类型为 8为无符号3通道的矩阵,三通道则:2*3 = 6列
cv::Mat::Create()
函数:
M.create(4,4, CV_8UC(2));// 4行4列,每个像素值由 2 个无符号的8位组成
cout << "M = "<< endl << " " << M << endl << endl;
Mat初始化方式:zeros()、ones()、eyes()
// eye是单位矩阵
Mat E = Mat::eye(4, 4, CV_64F);
cout << "E = " << endl << " " << E << endl << endl;
Mat O = Mat::ones(2, 2, CV_32F);
cout << "O = " << endl << " " << O << endl << endl;
Mat Z = Mat::zeros(3,3, CV_8UC1);
cout << "Z = " << endl << " " << Z << endl << endl;
使用逗号分隔初始化函数:
Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
cout << "C = " << endl << " " << C << endl << endl;
使用 clone() 或者 copyTo() 为一个存在的 Mat 对象创建一个新的信息头:
Mat RowClone = C.row(1).clone();
cout << "RowClone = " << endl << " " << RowClone << endl << endl;
具体内容可以参考链接:https://www.w3cschool.cn/opencv/opencv-bedc2caa.html
二、OpenCV的基本使用方法
**遍历图像方式:**https://blog.csdn.net/m0_37833142/article/details/105940111
1.代码
CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(imageBasics)
set(CMAKE_CXX_STANDARD 14)
# 寻找opencv 库
find_package(OpenCV 3 REQUIRED)
# 添加opencv头文件
include_directories(${Opencv_INCLUDE_DIRS})
add_executable(imageBasics main.cpp)
# 链接opencv库
target_link_libraries(imageBasics ${OpenCV_LIBS})
main.cpp
#include <iostream>
#include <chrono>
// opencv库的基础结构以及基本操作
#include "opencv2/core/core.hpp"
// 模块包含可以用来显示图像或者简单的输出的用户交互函数
#include "opencv2/highgui/highgui.hpp"
using namespace std;
int main(int argc,char **argv) {
cv::Mat image;
// 通过命令行读取指定目录下的图像
image = cv::imread(argv[1]);
// 判断图像文件是否读取正确(数据不存在,可能是文件不存在)
if(image.data == nullptr)
{
cerr << "文件" << argv[1] << "不存在" << endl;
return 0; // 若文件读取错误,退出执行
}
// 文件读取顺利,输出图像信息
cout << "图像宽为:" << image.cols << " 高为: " << image.rows << " 通道数为: " << image.channels() << endl;
// 显示图像
cv::imshow("image",image);
// 暂停程序,等待一个按键进行输入,才继续执行
cv::waitKey(0);
// 判断image的类型 CV_8UC1:每个通道由8位表示,1个通道(灰度图) CV_8UC3:每个通道由8位表示,3个通道(彩色图)
if(image.type() != CV_8UC1 && image.type() != CV_8UC3)
{
cout << "请输入一张彩色图/灰度图" << endl;
return 0;
}
// 遍历所有图像
// 使用 std::chrono 给算法计时
chrono::steady_clock::time_point t1 = chrono::steady_clock::now(); // 获取遍历前当前时间
for(size_t y = 0;y<image.rows;y++)
{
for(size_t x = 0;x<image.cols;x++)
{
// 访问位于 x y 处的像素,像素坐标 与一般数组的行列互换 像素坐标I(x,y)表示图像矩阵中image[y][x]的数据
// 用 cv::Mat::ptr 获得图像的行指针
// row_ptr 是第 y行的头指针
unsigned char *row_ptr = image.ptr<unsigned char>(y);
// data_ptr 指向待访问的像素数据(一个像素可能占有几个通道)
unsigned char* data_ptr = &row_ptr[x*image.channels()];
// 输出该像素每一个通道的值,如果是灰度图则只有一个通道,彩色图三个通道
for(int c=0;c<image.channels();c++)
{
// data 为 像素I(x,y)的第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 拷贝
// 直接赋值 并不会赋值矩阵数据,只是通过一个新的cv::Mat 矩阵头还有指针指向原来的图像矩阵数据
cv::Mat image_another = image;
// 修改 image_another 会修改 image
// 将 image_another 左上角 (100,100)置为255 白色
image_another(cv::Rect(0,0,100,100)).setTo(255);
cv::imshow("image_another",image_another);
cv::waitKey(0);
cv::imshow("image",image);
cv::waitKey(0);
// 使用clone() 函数拷贝数据 clone() 函数会拷贝矩阵数据本身(开销大)
cv::Mat image_clone = image.clone();
image_clone(cv::Rect(0,0,100,100)).setTo(0); // 置为0 为黑色
cv::imshow("image_clone",image_clone);
cv::waitKey(0);
cv::imshow("image2",image);// 此时左上角还是白色
cv::waitKey(0);
// 销毁所有图像窗口
cv::destroyAllWindows();
return 0;
}
运行结果:
/home/zxz/my_slam14/ch5/imageBasics/cmake-build-debug/imageBasics ./ubuntu.png
图像宽为:1200 高为: 674 通道数为: 3
遍历图像所用时间: 0.0232808 秒
进程已结束,退出代码为 0
2.重要代码解析
遍历图像
// 遍历所有图像
// 使用 std::chrono 给算法计时
chrono::steady_clock::time_point t1 = chrono::steady_clock::now(); // 获取遍历前当前时间
for(size_t y = 0;y<image.rows;y++)
{
for(size_t x = 0;x<image.cols;x++)
{
// 访问位于 x y 处的像素,图像矩阵 与一般数组的行列互换
// 用 cv::Mat::ptr 获得图像的行指针
// row_ptr 是第 y行的头指针
unsigned char *row_ptr = image.ptr<unsigned char>(y);
// data_ptr 指向待访问的像素数据(一个像素可能占有几个通道)
unsigned char* data_ptr = &row_ptr[x*image.channels()];
// 输出该像素每一个通道的值,如果是灰度图则只有一个通道,彩色图三个通道
for(int c=0;c<image.channels();c++)
{
// data 为 像素I(x,y)的第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;
chrono
**chrono是c++ 11中的时间库,提供计时,时钟等功能。**相较于<ctime>
精度更加高
// 获得一个当前时间对象(当前时间点) chrono命名空间下,结构体steady_clock(稳定时钟)下,结构体time_point 对象t1
chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
// 创建一个双浮点型的 时间段 对象 可以直接用时间点对象进行运算得到
chrono::duration<double> time_used = t2 - t1; // 存在一个隐式的数据类型转换
// 代码后部分对于 t2-t1进行一个显示的数据类型转换
chrono::duration<double> time_used = chrono::duration_cast<chrono::duration<double>>(t2-t1);
// 调用 count 时会返回一个双浮点型的数值,单位时秒
double a = time_used.count();
**也可以使用传统的计时方法进行计时操作:**但是ctime
的操作相较于chrono
得到的时间结果精度更低
// 导入头文件
#include <ctime>
using namespace std;
clock_t start = clock();
// do something...
clock_t end = clock();
cout << "花费了" << (double)(end - start) / CLOCKS_PER_SEC << "秒" << endl;
size_t
size_t
是C/C++
中任何对象所能达到的最大长度,它是无符号整数,它是为了方便系统之间的移植而定义的,不同的系统上,定义size_t
可能不一样。size_t
在32位系统上定义为 unsigned int,也就是32位无符号整型。在64位系统上定义为unsigned long
,也就是64位无符号整形。size_t
的目的是提供一种可移植的方法来声明与系统中可寻址的内存区域一致的长度。
uchar* Mat::ptr(int y)
// 获得图像第y行的数据的指针
unsigned char* row_ptr = image.ptr<unsigned char >(y);
// data_ptr 指向待访问的像素数据
unsigned char* data_ptr = &row_ptr[x * image.channels()];
3.相关函数
int cv::Mat::channels() const
// 返回结果是几就是几通道
int cv::Mat::type() const
// 以下表格为type函数返回的Mat类矩阵的类型
Rect类
Rect_
类有些意思,成员变量x、y、width、height
,分别为左上角点的坐标和矩形的宽和高。常用的成员函数有Size()
返回值为一个Size
,area()
返回矩形的面积,contains(Point)
用来判断点是否在矩形内,inside(Rect)
函数判断矩形是否在该矩形内,tl()
返回左上角点坐标,br()
返回右下角点坐标。
// 两个矩阵的交并
Rect rect = rect1 & rect2;
Rect rect = rect1 | rect2;
// 两个矩阵的平移或者缩放
Rect rectShift = rect + point;
Rect rectScale = rect + size;
//如果创建一个Rect对象rect(100, 50, 50, 100),那么rect会有以下几个功能:
rect.area(); //返回rect的面积 5000
rect.size(); //返回rect的尺寸 [50 × 100]
rect.tl(); //返回rect的左上顶点的坐标 [100, 50]
rect.br(); //返回rect的右下顶点的坐标 [150, 150]
rect.width(); //返回rect的宽度 50
rect.height(); //返回rect的高度 100
rect.contains(Point(x, y)); //返回布尔变量,判断rect是否包含Point(x, y)点
//还可以求两个矩形的交集和并集
rect = rect1 & rect2;
rect = rect1 | rect2;
//还可以对矩形进行平移和缩放
rect = rect + Point(-100, 100); //平移,也就是左上顶点的x坐标-100,y坐标+100
rect = rect + Size(-100, 100); //缩放,左上顶点不变,宽度-100,高度+100
//还可以对矩形进行对比,返回布尔变量
rect1 == rect2;
rect1 != rect2;
//OpenCV里貌似没有判断rect1是否在rect2里面的功能,所以自己写一个吧
bool isInside(Rect rect1, Rect rect2)
{
return (rect1 == (rect1&rect2));
}
//OpenCV貌似也没有获取矩形中心点的功能,还是自己写一个
Point getCenterPoint(Rect rect)
{
Point cpt;
cpt.x = rect.x + cvRound(rect.width/2.0);
cpt.y = rect.y + cvRound(rect.height/2.0);
return cpt;
}
//围绕矩形中心缩放
Rect rectCenterScale(Rect rect, Size size)
{
rect = rect + size;
Point pt;
pt.x = cvRound(size.width/2.0);
pt.y = cvRound(size.height/2.0);
return (rect-pt);
}
三、图像去畸变
由于相机透镜的形状以及组装误差会导致径向畸变、与切向畸变
畸变数学模型:
成像的过程实质上是几个坐标系的转换。首先空间中的一点由 世界坐标系 转换到 摄像机坐标系 ,然后再将其投影到成像平面 ( 图像物理坐标系 ) ,最后再将成像平面上的数据转换到图像平面 ( 图像像素坐标系 ) 。
图像像素坐标系 (uOv
坐标系) 下的无畸变坐标 (U, V)
,经过 经向畸变 和 切向畸变 后落在了uOv
坐标系 的(Ud, Vd)
上。即就是说,真实图像 imgR
与 畸变图像imgD
之间的关系为: imgR(U, V) = imgD(Ud, Vd)
转自:
https://blog.csdn.net/darlingqiang/article/details/111559053
1.代码
CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(undistortImage)
set(CMAKE_CXX_STANDARD 14)
# 找opencv3 库
find_package(OpenCV 3 REQUIRED)
# 添加头文件
include_directories(${Opencv_INCLUDE_DIRS})
add_executable(undistortImage main.cpp)
# 链接opencv库
target_link_libraries(undistortImage ${OpenCV_LIBS})
main.cpp
/*
* 本程序实现去畸变部分的代码,同时也可以直接使用OpenCV中去畸变的函数进行操作
* */
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
// 畸变图像路径
string image_file = "../distorted.png";
int main(int argc,char **argv) {
// 图像是灰度图 CV_8UC1
cv::Mat image = cv::imread(image_file,0);
// 判断图像文件是否读取正确(数据不存在,可能是文件不存在)
if(image.data == nullptr)
{
cerr << "文件" << argv[1] << "不存在" << endl;
return 0; // 若文件读取错误,退出执行
}
//畸变参数
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;
int rows = image.rows; int 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++)
{
// 按照公式计算点(u,v)对应到畸变图像中的坐标(u_distorted,v_distorted)
double x = (u-cx)/fx,y = (v-cy)/fy; // 相机坐标系下归一化平面的坐标[x,y]
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;
// 赋值(最邻近插值)
// 判断得到的 u_distorted v_distorted 是否在有效的范围
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("image",image);
cv::waitKey(0);
// 显示去畸变的图像
cv::imshow("undistorted",image_undistort);
cv::waitKey(0);
return 0;
}
运行结果(去畸变效果明显):
2.重点代码
// 计算去畸变后图像的内容
for(int v=0;v<rows;v++)
{
for(int u=0;u<cols;u++)
{
// 按照公式计算点(u,v)对应到畸变图像中的坐标(u_distorted,v_distorted)
double x = (u-cx)/fx,y = (v-cy)/fy; // 相机坐标系下归一化平面的坐标[x,y]
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;
// 赋值(最邻近插值)
// 判断得到的 u_distorted v_distorted 是否在有效的范围
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;
}
}
}