图像处理笔记(一)基础操作
Published in:2024-03-25 | category: 智能车
Words: 955 | Reading time: 3min | reading:

前面一部分笔记都是学习一个大佬的教程做的,但是大佬用的python,我在笔记里都转换成了c++

基础配置

图片基本读取操作

电脑上的彩色图是以 RGB(红 - 绿-蓝,Red-Green-Blue) 颜色模式显示的,但 OpenCV 中彩色图是以 B-G-R 通道顺序存储的,灰度图只有一个通道。

图像坐标的起始点是在左上角,所以行对应的是 y,列对应的是 x:

加载图片

imread()

两个参数:

  • 参数 1:图片的文件名
    如果图片放在当前文件夹下,直接写文件名就行,如”lena.jpg”
    否则需要给出绝对路径,如”C:/Users/lxcqm/Desktop/图片和视频/11111.png”
  • 参数 2:读入方式,省略即采用默认值
    cv2.IMREAD_COLOR:彩色图,默认值(1)or(IMREAD_COLOR)
    cv2.IMREAD_GRAYSCALE:灰度图(0)or(IMREAD_GRAYSCALE)
    cv2.IMREAD_UNCHANGED:包含透明通道的彩色图(-1)or(IMREAD_UNCHANGED)
1
2
//加载灰度图
Mat img = imread("C:/Users/lxcqm/Desktop/images/11111.png",0);

经验之谈:路径中不能有中文噢,并且没有加载成功的话是不会报错的,print(img)的结果为 None,后面处理才会报错,算是个小坑。

显示图片

使用imshow()显示图片,窗口会自适应图片的大小:

imshow()

1
2
3
4
//加载灰度图
Mat img = imread("C:/Users/lxcqm/Desktop/images/11111.png",0);
imshow("pic", img);
waitKey(0);

参数 1 是窗口的名字,参数 2 是要显示的图片。不同窗口之间用窗口名区分,所以窗口名相同就表示是同一个窗口,显示结果如下:
效果:

waitKey()是让程序暂停的意思,参数是等待时间(毫秒 ms)。
时间一到,会继续执行接下来的程序,传入 0 的话表示一直等待。等待期间也可以获取用户的按键输入:k = waitKey(0)

我们也可以先用namedWindow()创建一个窗口,之后再显示图片:

1
2
3
namedWindow("pic2", WINDOW_NORMAL);
imshow("pic2", img);
waitKey(0);

参数 1 依旧是窗口的名字,参数 2 默认是WINDOW_AUTOSIZE,表示窗口大小自适应图片,也可以设置为WINDOW_NORMAL,表示窗口大小可调整。图片比较大的时候,可以考虑用后者。

保存图片

imwrite()

使用imwrite()保存图片,参数 1 是包含后缀名的文件名:

1
imwrite("pictest.jpg", img);

会保存在目录文件夹里

摄像头

打开摄像头

要使用摄像头,需要使用VideoCapture capture(0);参数 0 指的是摄像头的编号,如果你电脑上有两个摄像头的话,访问第 2 个摄像头就可以传入 1,依此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main() {
VideoCapture capture(0);

if (!capture.isOpened()) {
return -1;
}

while (true) {
Mat frame;
//>> 是输入流提取运算符
// 它通常用于从输入流中提取数据并将其存储在相应的变量中
// 表示从 capture 输入流中读取数据,并将其存储在 frame 变量中
capture >> frame;

Mat gray;
cvtColor(frame, gray, COLOR_BGR2GRAY);

imshow("frame", gray);
if (waitKey(1) == 'q') {
break;
}
}

return 0;
}

cv2.cvtColor()用来转换颜色,这里将彩色图转成灰度图。
通过cap.get(propId)可以获取摄像头的一些属性,比如捕获的分辨率,亮度和对比度等。propId 是从 0~18 的数字,代表不同的属性,完整的属性列表可以参考:VideoCaptureProperties(https://docs.opencv.org/4.0.0/d4/d15/group__videoio__flags__base.html#gaeb8dd9c89c10a5c63c139bf7c4f5704d)
也可以使用cap.set(propId,value)来修改属性值。比如说,我们在 while 之前添加下面的代码:

1
2
3
4
5
6
double width = capture.get(CAP_PROP_FRAME_WIDTH);
double height = capture.get(CAP_PROP_FRAME_HEIGHT);
std::cout << "Original width: " << width << ", height: " << height << std::endl;

capture.set(CAP_PROP_FRAME_WIDTH, width * 2);
capture.set(CAP_PROP_FRAME_HEIGHT, height * 2);
std::cout << Original width: << width <<

这行代码使用了C++中的流输出操作符”<<”,它用于将数据插入到输出流中。在这个语句中:

  • std::cout: 是C++标准库中的标准输出流对象,用于将数据输出到控制台。
  • "Original width: "width", height: "heightstd::endl 都是要输出的具体内容。
  • <<:是流输出操作符,用于将后面的内容插入到输出流中。

所以,std::cout << "Original width: " << width << ", height: " << height << std::endl; 这行代码的作用是将描述原始宽度和高度的字符串以及相应的变量值输出到控制台,并在最后换行。

输出

经验之谈:某些摄像头设定分辨率等参数时会无效,因为它有固定的分辨率大小支持,一般可在摄像头的资料页中找到。

播放本地视频

跟打开摄像头一样,如果把摄像头的编号换成视频的路径就可以播放本地视频了。回想一下waitKey(),它的参数表示暂停时间,所以这个值越大,视频播放速度越慢,反之,播放速度越快,通常设置为 2530

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main() {
VideoCapture capture("C:/Users/lxcqm/Desktop/WeChat_20240325194731.mp4");

if (!capture.isOpened()) {
cout << "Error opening video file" << endl;
return -1;
}

Mat frame, gray;
while (true) {
if (!capture.read(frame)) {
break;
}

cvtColor(frame, gray, COLOR_BGR2GRAY);

imshow("frame", gray);

if (waitKey(30) == 'q') {
break;
}
}

capture.release();
destroyAllWindows();

return 0;
}

录制视频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main() {
VideoCapture capture(0);

// 定义编码方式并创建 VideoWriter 对象
VideoWriter outfile("output.avi", VideoWriter::fourcc('M', 'J', 'P', 'G'), 25., Size(640, 480));

while (capture.isOpened()) {
Mat frame;
capture.read(frame);

if (!frame.empty()) {
outfile.write(frame); // 写入文件
imshow("frame", frame);
if (waitKey(1) == 'q') {
break;
}
}
else {
break;
}
}

return 0;
}

基本操作

Mat创建图像(矩阵)

创建图像(矩阵):Mat

使用Mat创建图像(矩阵)的常用形式有:

  1. 创建一个空图像,大小为0

    1
    Mat image1;
  2. 指定矩阵大小,指定数据类型:

    1
    Mat image1(100,100,CV_8U);

    参数:矩阵行数,矩阵列数,数据类型

    其中数据类型有很多种,常用的应该有:

    CV_8U:8位无符号型(0~255),即灰度图像;

    CV_8UC3:三通道8位无符号型,这里三通道指B(蓝)G(绿)R(红),与matlab中的RGB正好相反。

    这里创建矩阵时未指定矩阵的值,发现默认值的大小为205.

  3. 指定矩阵大小,指定数据类型,设置初始值:

    1
    Mat image1(100,100,CV_8U, 100);

    这里包含四个参数:矩阵行数,矩阵列数,数据类型,初始值;
    对于灰度图像:可以直接给出初始值,也可以使用Scalar();

    1
    2
     Mat image1(100,100,CV_8U, 100);
    Mat image1(100,100,CV_8U, Scalar(100));//Scalar:标量

    对于三通道图像:使用Scalar();

    1
    Mat image1(100,100,CV_8UC3, Scalar(100,100,100));

获取图像信息

获取图像的宽度(列数),高度(行数),尺寸和通道数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<iostream>
#include<opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main()
{
Mat image1 = imread("lena.png"); //读取图像;
if (image1.empty())
{
cout << "读取错误" << endl;
return -1;
}

imshow("image1", image1); //显示图像;

cout << "图像的行数为: " << image1.rows << endl; //获取图像的高度,行数;
cout << "图像的列数为: " << image1.cols << endl; //获取图像的宽度,列数;
cout << "图像的通道数为: " << image1.channels() << endl; //获取图像的通道数,彩色图=3,灰度图=1;
cout << "图像的尺寸为: " << image1.size << endl; //获取图像的尺寸,行*列;

waitKey(0); //暂停,保持图像显示,等待按键结束

return 0;
}

输出

感兴趣区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
#include<opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main()
{
Mat image1 = imread("fire.jpg"); //读取图像;
if (image1.empty())
{
cout << "读取错误" << endl;
return -1;
}

imshow("image1", image1); //显示图像;

Mat imageROI(image1, Rect(0, 0, 10, 10)); //定义感兴趣区域

waitKey(0); //暂停,保持图像显示,等待按键结束
return 0;
}

其中Rect()有四个参数,Rect(a,b,c,d):

  • a:感兴趣区域列(cols)的起点;
  • b:感兴趣区域行(rows)的起点;
  • c:感兴趣区域的列数(cols);
  • d:感兴趣区域的行数(rows);

通过鼠标点击操作获取图像的像素坐标和像素值

创建鼠标操作函数的头文件

onMouse.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#pragma once
#include<iostream>
#include<opencv2/opencv.hpp>

using namespace cv;
using namespace std;

void onMouse(int event, int x, int y, int flags, void* param); //evnet:鼠标事件类型 x,y:鼠标坐标 flags:鼠标哪个键

void onMouse(int event, int x, int y, int flags, void* param) //evnet:鼠标事件类型 x,y:鼠标坐标 flags:鼠标哪个键
{
Mat* im = reinterpret_cast<Mat*>(param);
switch (event) {

case EVENT_LBUTTONDOWN:
//显示图像像素值

if (static_cast<int>(im->channels()) == 1)
{
//若图像为单通道图像,则显示鼠标点击的坐标以及灰度值
switch (im->type())
{
case 0:
cout << "at (" << x << ", " << y << " ) value is: " << static_cast<int>(im->at<uchar>(Point(x, y))) << endl; break;
case 1:
cout << "at (" << x << ", " << y << " ) value is: " << static_cast<int>(im->at<char>(Point(x, y))) << endl; break;
case 2:
cout << "at (" << x << ", " << y << " ) value is: " << static_cast<int>(im->at<ushort>(Point(x, y))) << endl; break;
case 3:
cout << "at (" << x << ", " << y << " ) value is: " << static_cast<int>(im->at<short>(Point(x, y))) << endl; break;
case 4:
cout << "at (" << x << ", " << y << " ) value is: " << static_cast<int>(im->at<int>(Point(x, y))) << endl; break;
case 5:
cout << "at (" << x << ", " << y << " ) value is: " << static_cast<int>(im->at<float>(Point(x, y))) << endl; break;
case 6:
cout << "at (" << x << ", " << y << " ) value is: " << static_cast<int>(im->at<double>(Point(x, y))) << endl; break;
}
}
else
{
//若图像为彩色图像,则显示鼠标点击坐标以及对应的B, G, R值
cout << "at (" << x << ", " << y << ")"
<< " B value is: " << static_cast<int>(im->at<Vec3b>(Point(x, y))[0])
<< " G value is: " << static_cast<int>(im->at<Vec3b>(Point(x, y))[1])
<< " R value is: " << static_cast<int>(im->at<Vec3b>(Point(x, y))[2])
<< endl;
}

break;
}
}

//setMouseCallback("image1", onMouse, reinterpret_cast<void*>(&image1)); //关联图像显示窗口和onMouse函数


//Mat每个格子内的数据格式---------- - Mat定义
//Mat_<uchar>-------- - CV_8U(0-255)
//Mat<char>---------- - CV_8S(-128-127)
//Nat_<short>-------- - CV_16S(-32768-32767)
//Mat_<ushort>--------CV_16U(0-65535)
//Mat_<int>---------- - CV_32S(-2147483648-2147483647)
//Mat_<float>----------CV_32F(-FLT_MAX…FLT_MAX,INF,NAN)
//Mat_<double>--------CV_64F(-DBL_MAX…DBL_MAX,INF,NAN)

//Mat数据类型和通道对应的type()
// C1 C2 C3 C4
//CV_8U 0 8 16 24
//CV_8S 1 9 17 25
//CV_16U 2 10 18 26
//CV_16S 3 11 19 27
//CV_32S 4 12 20 28
//CV_32F 5 13 21 29
//CV_64F 6 14 22 30

主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<iostream>
#include<opencv2/opencv.hpp>
#include "onMouse.h"

using namespace cv;
using namespace std;

int main()
{

Mat image1 = imread("fire.jpg"); //读取图像;
if (image1.empty())
{
cout << "读取错误" << endl;
return -1;
}
imshow("image1", image1); //显示图像;


setMouseCallback("image1", onMouse, reinterpret_cast<void*>(&image1)); //关联图像显示窗口和onMouse函数
waitKey(0); //暂停,保持图像显示,等待按键结束

return 0;
}

效果

访问图像像素

访问(j,i)处像素

以8位(0~255)灰度图像和BGR彩色图像为例,用at可以访问图像像素:

1
2
3
4
5
6
//灰度图像:
image.at<uchar>(j, i) //j为行数,i为列数
//BGR彩色图像
image.at<Vec3b>(j, i)[0] //B分量
image.at<Vec3b>(j, i)[1] //G分量
image.at<Vec3b>(j, i)[2] //R分量

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
#include<opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main()
{

Mat image1 = imread("fire.jpg"); //读取图像;
imshow("image1", image1); //显示图像;
int i = 50, j = 50;
int b = image1.at<Vec3b>(j, i)[0];
int g = image1.at<Vec3b>(j, i)[1];
int r = image1.at<Vec3b>(j, i)[2];
cout << "图像该点的像素(BGR):"<<b<<"," << g << "," << r << endl;
waitKey(0);
return 0;
}

输出:

在图像中加入白色椒盐噪声

创建Salt头文件
Salt.h
1
2
3
4
5
6
7
8
9
10
#pragma once
#include<iostream>
#include<opencv2/opencv.hpp>
#include <random> //随机数头文件


using namespace cv;
using namespace std;

void Salt(Mat image, int n); //n:加入噪声点数
创建Salt源文件
Salt.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "Salt.h"

void Salt(Mat image, int n)
{
//随机数生成器
default_random_engine generater;
uniform_int_distribution<int>randomRow(0, image.rows - 1);
uniform_int_distribution<int>randomCol(0, image.cols - 1);

int i, j;
for (int k = 0; k < n; k++)
{
i = randomCol(generater);
j = randomRow(generater);
if (image.channels() == 1)
{
image.at<uchar>(j, i) = 255;
}
else if (image.channels() == 3)
{
image.at<Vec3b>(j, i)[0] = 255;
image.at<Vec3b>(j, i)[1] = 255;
image.at<Vec3b>(j, i)[2] = 255;
}
}
}
main
main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<iostream>
#include<opencv2/opencv.hpp>
#include "Salt.h"

using namespace cv;
using namespace std;

int main()
{

Mat image1 = imread("lena.png"); //读取图像;
if (image1.empty())
{
cout << "读取错误" << endl;
return -1;
}
imshow("image1", image1); //显示原图像;

Salt(image1, 5000); //加入5000个噪声点
imshow("image2", image1); //显示噪声图像;

waitKey(0); //暂停,保持图像显示,等待按键结束
return 0;
}

效果

遍历图像像素

指针扫描

以下面模板对图像进行扫描运算为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<iostream>
#include<opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main()
{
Mat image1, output_image; //定义输入图像和输出图像
image1 = imread("lena.png"); //读取图像;
if (image1.empty())
{
cout << "读取错误" << endl;
return -1;
}
output_image = Mat(image1.size(), image1.type()); //定义输出图像大小
output_image = image1.clone(); //克隆原图像素值

int rows = image1.rows; //原图行数
int stepx = image1.channels(); //原图通道数
int cols = (image1.cols) * image1.channels(); //矩阵总列数,在BGR彩色图像中,每个像素的BGR通道按顺序排列,因此总列数=像素宽度*通道数

for (int row =1 ; row < (rows - 1); row++) //对行遍历
{
const uchar* previous = image1.ptr<uchar>(row - 1); //原图上一行指针
const uchar* current = image1.ptr<uchar>(row); //原图当前行指针
const uchar* next = image1.ptr<uchar>(row + 1); //原图下一行指针
uchar* output = output_image.ptr<uchar>(row); //输出图像当前行指针

for (int col = stepx; col < (cols- stepx); col++) //对列遍历
{
//卷积计算
output[col] = saturate_cast<uchar>(5*current[col] - (previous[col]+ current[col- stepx]+ current[col + stepx]+ next[col]));
//saturate_cast<uchar>(a),当a在0—255时输出a,当a小于0输出0,当a大于255输出255,保证a的值在0~255之间
}
}

imshow("image1", image1);
imshow("output_image", output_image);

waitKey(0); //暂停,保持图像显示,等待按键结束
return 0;
}

输出

把卷积核中间改成4:
输出

opencv自带的卷积运算:filter2D

上面的方法可以简化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>
#include<opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main()
{
Mat image1, output_image; //定义输入图像和输出图像
image1 = imread("lena.png"); //读取图像;
if (image1.empty())
{
cout << "读取错误" << endl;
return -1;
}

Mat kernel = (Mat_<char>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0); //创建滤波器
filter2D(image1, output_image, image1.depth(), kernel); //卷积

imshow("image1", image1);
imshow("output_image", output_image);

waitKey(0); //暂停,保持图像显示,等待按键结束
return 0;
}

在filter2D函数中,image1.depth()是用来获取图像image1的深度(depth)信息的函数调用。

图像的深度表示图像中每个像素值的数据类型。在OpenCV中,常见的图像深度包括:

  • CV_8U:8位无符号整数(即Unsigned char),对应于灰度图像的深度
  • CV_16U:16位无符号整数
  • CV_16S:16位有符号整数
  • CV_32S:32位有符号整数
  • CV_32F:32位浮点数
  • CV_64F:64位浮点数
Prev:
图像处理笔记(二)灰度变换和空间滤波基础
Next:
c语言计算机二级笔记