0.Getting Started with Images
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>
using namespace cv;
int main()
{
std::string image_path = "../test.png";
Mat img = imread(image_path, IMREAD_COLOR);
if(img.empty())
{
std::cout << "Could not read the image: " << image_path << std::endl;
return 1;
}
imshow("Display window", img);
int k = waitKey(0); // Wait for a keystroke in the window
if(k == 's')
{
imwrite("test.png", img);
}
return 0;
}
(1)读取图像
std::string image_path = "../test.png";
Mat img = imread(image_path, IMREAD_COLOR);
调用 cv::imread
函数使用指定的文件路径加载图像第一个参数。第二个参数是可选的,指定我们想要的图像格式。
图像格式:
格式名称 | 描述 |
---|---|
IMREAD_COLOR | 以 BGR 8 位格式加载图像 |
IMREAD_UNCHANGED | 按原样加载图像(包括 alpha 通道,如果存在) |
IMREAD_GRAYSCALE | 将图像加载为强度图像 |
(2)检查图像(加载图像成功)
if(img.empty())
{
std::cout << "Could not read the image: " << image_path << std::endl;
return 1;
}
(3)显示图像
调用 cv::imshow
函数显示图像。第一个参数是窗口的标题,第二个参数是将显示的 cv::Mat
对象。
因为希望窗口一直显示到用户按下一个键为止(否则程序会很快结束),所以我们使用cv::waitKey
函数,该函数的唯一参数是它应该等待用户输入多长时间(以毫秒为单位)。零意味着永远等待。返回值是按下的键。
imshow("Display window", img);
int k = waitKey(0); // Wait for a keystroke in the window
(4)
如果按下的键是 “s” 键,则图像会写入文件。为此,调用了cv::imwrite
函数,该函数将文件路径和cv::Mat
对象作为参数。
if(k == 's')
{
imwrite("test.png", img);
}
return 0;
1.The Core Functionality
1.1 Mat - 基本图像容器
(1)Mat
Mat基本上是一个包含两个数据部分的类:矩阵头(包含矩阵大小、用于存储的方法、存储矩阵的地址等信息)和一个指向包含像素值的矩阵的指针(根据选择的存储方法取任意维数)。
Mat A, C; // creates just the header parts
A = imread(argv[1], IMREAD_COLOR); // here we'll know the method used (allocate matrix)
Mat B(A); // Use the copy constructor
C = A; // Assignment operator
(2)存储方法
存储像素值,可以选择使用的颜色空间和数据类型。
颜色空间是指如何组合颜色分量以编码给定的颜色。最简单的一个是灰色,我们可以使用的颜色是黑色和白色。这些组合使我们能够创建许多灰色阴影。
对于丰富多彩的方式,我们有更多的选择方法。将它们分解成三到四个基本组件,我们可以使用这些组合来创建其他组件。最流行的是RGB,主要是因为这也是我们的眼睛如何建立颜色。其基色为红,绿,蓝。为了编码颜色的透明度有时是第四个元素:添加了α(A)。
然而,还有许多其他颜色系统都有自己的优势:
- RGB是最常见的,因为我们的眼睛使用类似的东西,但是,OpenCV标准显示系统使用BGR颜色空间(红色和蓝色通道的开关)组成颜色。
- HSV和HLS将颜色分解为色调,饱和度和值/亮度分量,这是我们描述颜色的更自然的方式。例如,您可能会忽略最后一个组件,使您的算法对输入图像的光线条件不太敏感。
- YCrCb被流行的JPEG图像格式使用。
- CIE L * a * b *是感知统一的颜色空间,如果您需要测量给定颜色与其他颜色的距离,则可方便使用。
(3)创建一个Mat对象
Mat是一个图像容器,但它也是一个通用的矩阵类。因此,可以创建和操纵多维矩阵。
Mat M(2,2, CV_8UC3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;
对于二维和多通道图像,首先定义它们的大小:行数和列数。然后,需要指定用于存储元素的数据类型和每个矩阵点的通道数。为此,我们根据以下约定构造了多个定义:
CV_[位数][有无符号][类型前缀]C[通道数]
例如,CV_8UC3意味着我们使用8位长的无符号字符类型,每个像素有其中的三个以形成三个通道。cv::Scalar
is four element short vector。指定它,可以使用自定义值初始化所有矩阵点。
上面的例子显示了如何创建一个具有两维以上的矩阵。指定其维度,然后传递一个包含每个维的大小的指针,其余的保持不变。
M.create(4,4, CV_8UC(2));
cout << "M = "<< endl << " " << M << endl << endl;
MATLAB样式初始化器: cv::Mat::zeros
, cv::Mat::ones
, cv::Mat::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;
(4)输出格式
使用默认就够了。
1.2 如何使用OpenCV扫描图像、查找表格和时间测量
1.3 矩阵上的掩码操作
1.4 图像操作
(1)输入/输出
从文件加载图像:
Mat img = imread(filename);
如果读取jpg文件,则默认情况下会创建3通道映像。如果需要灰度图像,则使用:
Mat img = imread(filename, IMREAD_GRAYSCALE);
将图像保存到文件中:
imwrite(filename, img);
(2)基本操作
1 访问像素强度值
为了获得像素强度值,必须知道图像的类型和通道数。以下是单通道灰度图像(类型8UC1)和像素坐标x和y的示例:
Scalar intensity = img.at<uchar>(y, x);
现在考虑使用BGR颜色排序的3通道图像(由imread返回的默认格式):
Vec3b intensity = img.at < Vec3b >(y,x);
uchar blue = intensity.val [0];
uchar green = intensity.val [1];
uchar red = intensity.val [2];
Vec3b intensity = img.at < Vec3b > (y, x);
float blue = intensity.val [0];
float green = intensity.val [1];
float red = intensity.val [2];
2 内存管理和引用计数
3 可视化图像
1.5 使用OpenCV添加(混合)两个图像
(1)混合理论
g ( x ) = ( 1 − α ) f 0 ( x ) + α f 1 ( x ) g(x) = (1- \alpha)f_0(x) + \alpha f_1(x) g(x)=(1−α)f0(x)+αf1(x)
其中, g ( x ) g(x) g(x) 表示融合图片中的像素点, f 0 ( x ) f_0(x) f0(x) 和 f 1 ( x ) f_1(x) f1(x) 分别表示背景和前景中的像素点, α \alpha α 表示两种图片的融合比例。
(2)代码
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
#include <iostream>
using namespace cv;
// we're NOT "using namespace std;" here, to avoid collisions between the beta variable and std::beta in c++17
using std::cin;
using std::cout;
using std::endl;
int main(void)
{
double alpha = 0.5;
double beta;
double input;
Mat src1, src2, dst;
cout << " Simple Linear Blender " << endl;
cout << "-----------------------" << endl;
cout << "* Enter alpha [0.0-1.0]: ";
cin >> input;
// We use the alpha provided by the user if it is between 0 and 1
if( input >= 0 && input <= 1 )
{
alpha = input;
}
src1 = imread("../Linux_logo.jpg");
src2 = imread("../Windows-logo.jpg");
if( src1.empty() )
{
cout << "Error loading src1" << endl;
return EXIT_FAILURE;
}
if( src2.empty() )
{
cout << "Error loading src2" << endl;
return EXIT_FAILURE;
}
resize(src2, src2, Size(src1.cols, src1.rows));
beta = ( 1.0 - alpha );
addWeighted( src1, alpha, src2, beta, 0.0, dst);
imshow( "Linear Blend", dst );
waitKey(0);
return 0;
}
1.6 改变图像的对比度和亮度
(1)理论
g ( x ) = α f ( x ) + β g(x) = \alpha f(x) + \beta g(x)=αf(x)+β
其中,通常 α > 0 \alpha>0 α>0 ,且 α \alpha α 被称为增益(gain), β \beta β 被称为偏置(bias)。
(2)注意点
1 新图像:初始像素值等于零,与原始图像相同的大小和类型
Mat new_image = Mat::zeros( image.size(), image.type() );
2 BGR图像
每像素(B,G和R)有三个值,因此将分别访问它们。
3 saturate_cast()
函数
在图像处理方面,无论是加是减,乘除,都会超出一个像素灰度值的范围(0~255),saturate_cast函数的作用即是:当运算完之后,结果为负,则转为0,结果超出255,则为255。
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace std;
using namespace cv;
int main( int argc, char** argv )
{
double alpha = 1.0; /*< Simple contrast control */
int beta = 0; /*< Simple brightness control */
String imageName("../ubuntu.png"); // by default
if (argc > 1)
{
imageName = argv[1];
}
Mat image = imread( imageName );
Mat new_image = Mat::zeros( image.size(), image.type() );
cout << " Basic Linear Transforms " << endl;
cout << "-------------------------" << endl;
cout << "* Enter the alpha value [1.0-3.0]: "; cin >> alpha;
cout << "* Enter the beta value [0-100]: "; cin >> beta;
for( int y = 0; y < image.rows; y++ ) {
for( int x = 0; x < image.cols; x++ ) {
for( int c = 0; c < 3; c++ ) {
new_image.at<Vec3b>(y,x)[c] =
saturate_cast<uchar>( alpha*( image.at<Vec3b>(y,x)[c] ) + beta );
}
}
}
namedWindow("Original Image", WINDOW_AUTOSIZE);
namedWindow("New Image", WINDOW_AUTOSIZE);
imshow("Original Image", image);
imshow("New Image", new_image);
waitKey();
return 0;
}
1.7 离散傅里叶变换
(1)载入原始图像
(2)将图像扩大到合适的尺寸
DFT 的性能取决于图像大小。 对于数字二、三和五的倍数的图像大小,它往往是最快的。 因此,为了获得最大的性能,通常将边界值填充到图像以获得具有此类特征的大小是一个好主意。 getOptimalDFTSize()
返回这个最佳尺寸,可以使用 copyMakeBorder()
函数来扩展图像的边框(附加像素初始化为零):
int m = getOptimalDFTSize(I.rows);
int n = getOptimalDFTSize( I.cols ); // on the border add zero values
copyMakeBorder(I, padded, 0, m - I.rows, 0, n - I.cols, BORDER_CONSTANT, Scalar::all(0));
(3)傅里叶变换结果分配存储空间
变换结果为复数,每个原图像值结果会有两个图像值
Mat planes[] = {Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F)};
Mat complexI;
merge(planes, 2, complexI); // Add to the expanded another plane with zeros
(4)傅里叶变换
dft(complexI, complexI); // this way the result may fit in the source matrix
(5)复数转化为幅值
变换结果是复数,对应幅值表示为M:
M
=
R
e
(
D
F
T
(
I
)
)
2
+
I
m
(
D
F
T
(
I
)
2
)
M = \sqrt{Re(DFT(I))^2 + Im(DFT(I)^2)}
M=Re(DFT(I))2+Im(DFT(I)2)
//将多通道 complexI 分离成几个单通道数组
split(complexI, planes); // planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
magnitude(planes[0], planes[1], planes[0]);// planes[0] = magnitude
Mat magI = planes[0];
(6)进行对数尺度缩放
M 1 = l n ( 1 + M ) M_1 = ln(1+M) M1=ln(1+M)
magI += Scalar::all(1); // switch to logarithmic scale
log(magI, magI);
(7)剪切和重分布幅度图像象限
第2步扩展了图像,现在需要将新添加的像素剔除。可以重新分布图像象限位置。
magI = magI(Rect(0, 0, magI.cols & -2, magI.rows & -2));
// rearrange the quadrants of Fourier image so that the origin is at the image center
int cx = magI.cols/2;
int cy = magI.rows/2;
Mat q0(magI, Rect(0, 0, cx, cy)); // Top-Left - Create a ROI per quadrant
Mat q1(magI, Rect(cx, 0, cx, cy)); // Top-Right
Mat q2(magI, Rect(0, cy, cx, cy)); // Bottom-Left
Mat q3(magI, Rect(cx, cy, cx, cy)); // Bottom-Right
Mat tmp; // swap quadrants (Top-Left with Bottom-Right)
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
q1.copyTo(tmp); // swap quadrant (Top-Right with Bottom-Left)
q2.copyTo(q1);
tmp.copyTo(q2);
(8)归一化
normalize(magI, magI, 0, 1, NORM_MINMAX); // Transform the matrix with float values into a
(9)结论
从频谱图上可以看出,当将频谱移频到原点以后,图像中心比较亮。在频谱图中,一个点的亮暗主要与这个频率中点的数目和点的灰度值有关,也就是说在空间域中包含这种频率的点越多。而经过频移后,频率为0的部分,也就是傅里叶变换所得到的常量分量在图像中心,往外扩散,点所代表的频率越来越高。也说明了图像中的“能量”主要集中在低频部分。
1.5 视觉slam十四讲中涉及的
(1)基本使用
#include <iostream>
#include <chrono>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
int main(int argc, char *argv[])
{
std::string img_path = "../ubuntu.png";
cv::Mat img;
img = cv::imread(img_path);
if (img.data == nullptr)
{
std::cerr << "文件不存在!" << std::endl;
}
std::cout << "图像的宽:" << img.cols << "\n"
<< "图像的高:" << img.rows << "\n"
<< "通道数:" << img.channels() << std::endl;
cv::imshow("image", img);
cv::waitKey(0);
if (img.type() != CV_8UC1 && img.type() != CV_8UC3)
{
std::cout << "请输入一张彩色图或灰度图!!!" << std::endl;
return 0;
}
//std::chrono 计时
std::chrono::steady_clock::time_point t1 = std::chrono::steady_clock::now();
for (size_t y = 0; y < img.rows; y++)
{
unsigned char* row_ptr = img.ptr<unsigned char>(y);
for (size_t x = 0; x < img.cols; x++)
{
unsigned char* data_ptr = &row_ptr[x * img.channels()];
for (int c = 0; c != img.channels(); c++)
{
unsigned char data = data_ptr[c]; //data: I(x, y)第c个通道的值
}
}
}
std::chrono::steady_clock::time_point t2 = std::chrono::steady_clock::now();
std::chrono::duration<double> time_used = std::chrono::duration_cast<std::chrono::duration<double>> (t2 - t1);
std::cout << "遍历图像用时:" << time_used.count() << " s" << std::endl;
//cv::Mat
cv::Mat img_another = img;
img_another(cv::Rect(0, 0, 100, 100)).setTo(100);
cv::imshow("img_another", img);
cv::waitKey(0);
cv::Mat image = img.clone();
image(cv::Rect(0, 0, 100, 100)).setTo(255);
cv::imshow("image_clone", image);
cv::imshow("img", img);
cv::waitKey(0);
//cv::destroyAllWindows();
return 0;
}
(2)图像去畸变
重看确实有新的体会,为什么第一遍的时候没想到 _
输入图像有畸变,即需要将畸变图像的像素坐标转化到归一化坐标,根据公式计算去畸变后的归一化坐标,然后再投影到像素平面得到新的去畸变后的图像。
#include <iostream>
#include <string>
#include <opencv2/opencv.hpp>
std::string image_file = "../jibian.jpg";
int main(int argc, char *argv[])
{
//畸变参数
double k1 = -0.28340811;
double k2 = 0.07395907;
double p1 = 0.00019359;
double p2 = 1.76187114e-05;
//内参
double fx = 458.654;
double fy = 457.296;
double cx = 367.215;
double cy = 248.375;
cv::Mat image = cv::imread(image_file, 0);
int rows = image.rows;
int cols = image.cols;
cv::Mat image_undistort = cv::Mat(rows, cols, 0);
for (int v = 0; v < image.rows; v++)
{
for (int u = 0; u < image.cols; u++)
{
double x;
double y;
x = (u - cx) / fx;
y = (v - cy) / fy;
double r;
double r2;
double r4;
r = sqrt(x * x + y * y);
r2 = r * r;
r4 = r2 * r2;
double x_distorted;
double y_distorted;
x_distorted = x * (1 + k1 * r2 + k2 * r4) + 2 * p1 * x * y + p2 * (r2 + 2 * x * x);
y_distorted = y * (1 + k1 * r2 + k2 * r4) + 2 * p2 * x * y + p1 * (r2 + 2 * y * y);
double u_dis = fx * x_distorted + cx;
double v_dis = fy * y_distorted + cy;
if (u_dis >= 0 && v_dis >= 0 && u_dis < cols && v_dis < rows)
{
image_undistort.at<uchar>(v, u) = image.at<uchar> ((int) v_dis, (int) u_dis);
}
else
{
image_undistort.at<uchar> (v, u) = 0;
}
}
}
cv::imshow("未去畸变图", image);
cv::imshow("去畸变后的图", image_undistort);
cv::waitKey(0);
return 0;
}