【OpenCV C++20 学习笔记】操作图片

操作图片

概述

在本专栏的第一篇文章中就介绍了一个用OpenCV处理图片的实例(《图片处理基础》),这篇文章进一步详细介绍OpenCV中处理图片的一些操作。

我这里使用的都是C++20的初始化语法,之前版本的C++可以参考下面这节中不同版本C++语法的对比。

图片的导入和保存

从图片文件中导入图片数据:

cpp 复制代码
Mat img = imread(filename);
Mat imgCpp20 { imread(filename) };	//C++20的初始化语法

如果导入的是jpg格式的图片,那么默认是3通道的图像数据。如果想要以灰度(只有黑白两色)格式导入,可以这样导入:

cpp 复制代码
Mat img = imread(filename, IMREAD_GRAYSCALE);
Mat imgCpp20 { imread(filename, IMREAD_GRAYSCALE) };	//C++20的初始化语法

要将数据保存到图片:

cpp 复制代码
imwrite(filename, img);

对导入的图片的操作

获取像素值

要获取像素的值,必须要知道图片的类型以及颜色通道数量。

关于图片数据的类型,可以参考该合集中的《基本图像容器------Mat》

如果要获取一个单通道灰度图片(即,8UC1类型)中(x, y)坐标上的像素的值,可以使用下面这条语句:

cpp 复制代码
Scalar intensity { img.at<uchar>(y, x) };

**注意这里坐标的表示是(y, x)。**因为在OpenCV中图片都是用矩阵来表示的,而矩阵一般是通过(row, col)的先行后列的模式来定位的,为了统一,OpenCV中坐标的表示也是纵坐标在前、横坐标在后。

在C++中,还可以使用Point来换回传统的坐标表示:

cpp 复制代码
Scalar intensity { img.at<uchar>(Point(x, y) };

如果是3通道的BGR格式的图片,要获取某个像素上每个通道的颜色值,可以使用以下方法:

cpp 复制代码
Vec3b intensity { img.at<Vec3b>(y, x) };
uchar blue { intensity.val[0] };
uchar green { intensity.val[1] };
uchar red { intensity.val[2] };

可以看到,储存单通道的像素值,使用的是Scalar类型;而储存3通道的像素值,使用的是Vec3b类型 ;3通道中单个通道的颜色值则是uchar类型。

获取像素值的方法也可以用来修改像素值:

cpp 复制代码
img.at<uchar>(y, x) = 128;

Point类型和图片像素

在C++中,用2D或3D的Point类型的数组也可以创建Mat对象,这种Mat矩阵只有1列,每一行对应一个Point对象;而且矩阵的数据类型应该是32FC2或者32FC3,相应的Point对象的类型也应该是Point2f或者Point3f。示例如下:

cpp 复制代码
vector<Point2f> points;
// ... 填充该数组
Mat pointsMat { Mat(points) };

这种矩阵可以从中获取Point对象:

cpp 复制代码
Point2f point { pointsMat.at<Point2f>(i, 0) };

内存管理和引用计数

如该合集的《基本图像容器------Mat》中详细描述的那样,Mat对象只储存指向矩阵数据的指针以及描述矩阵数据的一些信息,所以若干个Mat对象共享同一个矩阵数据是被允许的。下面结合一个比较复杂的例子来讨论这个问题:

cpp 复制代码
vector<Point3f> points;
// ... 填充数组
Mat pointsMat { Mat(points).reshape(1) };	//reshape函数重新设置Mat对象的通道数

上面的例子中pointsMat最终还是一个N3的矩阵,并不是N 1的矩阵。因为reshape函数不复制数据,它修改的只是Mat对象中对矩阵的描述。所以矩阵还是原来的N*3的矩阵,只不过在Mat(points)中创建的临时Mat对象将它描述成3通道的矩阵,而pointsMat将其描述成单通道的矩阵。

要想真正的复制数据,则需要用到cv::Mat::copyTo或者cv::Mat::clone函数:

cpp 复制代码
Mat img { imread("image.jpg");
Mat img1 { img.clone() };

**空Mat对象也可以作为函数的输出参数,用来储存计算结果。**这是因为OpenCV中的函数都会调用Mat::create方法来修改输出矩阵。如果输出矩阵是空的,那就为它分配所需要的内存;如果输出矩阵不是空的,而且大小和类型都刚好,那就不会进行任何更改;如果大小和类型不符合需求,就会先释放原有的内存然后重新分配新的内存。示例如下:

cpp 复制代码
Mat img{ imread("image.jpg");
Mat sobelx;
Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0,
								-1, 5, -1,
								0, -1, 0);
filter2D(img, sobelx, img.depth(), kernel);	//掩码操作函数,第二个形参为输出的矩阵

一些简便操作

将灰度图片变成黑色图片:

cpp 复制代码
img = Scalar(0); 	//img为储存灰度图片数据的Mat对象

运行结果如下:

选择兴趣区(ROI):

cpp 复制代码
Rect r(10, 10, 100, 100);
Mat smallImg { img(r) };

定义在<opencv2/imgproc.hpp>模块中的cvtColor函数可以将BGR格式的图片转换成灰度图片:

cpp 复制代码
Mat Img { imread("image.jpg") };
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);

将图片从8UC1格式转换成32FC1格式:

cpp 复制代码
src.convertTo(dst, CV_32F);	//src为原矩阵,dst为转换后的矩阵

图片可视化

在开发过程中能及时看到算法处理的结果是很有帮助的。OpenCV提供了一个简便的图片可视化方法。例如,一个8U格式的图片可以这样展示:

cpp 复制代码
Mat img { imread("image.jpg") };
namedWindow("image", WINDOW_AUTOSIZE);	//可以不用,因为下面的imshow也会自动创建窗口
imshow("image", img);
waitKey();

waitKey();函数开启一个信息传输循环,等待在图片展示窗口上的按键操作,一旦有检测到按键就会停止循环,执行下面的语句。

其他格式的图片需要转换成8U格式的,才能在窗口展示,这就涉及到了类型转换

更精确的类型转换

在该合集的《矩阵上的掩码(mask)操作》中有提到过类型转换的问题。saturate_cast可以采取截断的方法避免信息的丢失,但它只是保证数据落在值域之内,没有进行对应的缩放。下面是一个更精确的类型转换的例子:

cpp 复制代码
Mat img { imread("image.jpg") };
Mat gray;
cvtColor(img, grey, COLOR_BGR2GRAY);
Mat sobelx;
Sobel(grey, sobelx, CV_32F, 1, 0);	//得到一个32F格式的sobelx对象
double minVal, maxVal;
minMaxLoc(sobelx, &minVal, &maxVal); //找到sobelx中的最小值和最大值
Mat draw;
sobelx.convertTo(draw, CV_8U, 255.0/(maxVal - minVal), -minVal * 255.0/(maxVal - minVal));	//转换语句
namedWindow("image", WINDOW_AUTOSIZE);
imshow("image", draw);
waitKey();

上例中的convertTo语句的最后两个参数是用来将原来的32F格式的值转换成8U格式的。
convertTo的4个参数分别是:

  • 目标矩阵 m m m,储存转换结果
  • 目标格式 r t y p e rtype rtype,转换后的格式
  • α α α值
  • β β β值
    α α α和 β β β值,则会用来进行以下运算:
    m ( x , y ) = s a t u r a t e _ c a s t < r t y p e > ( α ( ∗ t h i s ) ( x , y ) + β ) ; m(x,y) = saturate\_cast<rtype>(α(*this)(x,y)+β); m(x,y)=saturate_cast<rtype>(α(∗this)(x,y)+β);
    可以看出, α α α实际上是一个缩放系数,所以上例将255.0/(maxVal - minVal)作为 α α α。因为255是8U格式的最大值和最小值之间的差,将它除以原始矩阵中的最大值与最小值之间的差,相当于是两个值域的比值。另一方面,β则是缩放后进行偏移量。上例将-minVal * 255.0/(maxVal - minVal)作为偏移量,代表所有的原始值在缩放之后都要向最小值偏移一定的距离。
相关推荐
IVEN_7 天前
Python OpenCV: RGB三色识别的最佳工程实践
python·opencv
西岸行者13 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意13 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码13 天前
嵌入式学习路线
学习
毛小茛13 天前
计算机系统概论——校验码
学习
babe小鑫13 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms13 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下13 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。13 天前
2026.2.25监控学习
学习
im_AMBER13 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode