opencv 学习: 03 初识 cv:Mat
事实上,一张图片就是一个像素点矩阵。图像处理就是针对这个矩阵的各种计算。
cv::Mat 数据结构是 opencv 的核心元素。它用来存放图像数据,并基于它来对图像进行处理。
总的来说,图像处理就是对以 Mat 进行表示的像素矩阵的处理。所以,对这个数据结构的了解是非常必要的。
1.Mat 的一些简单用法
#include <iostream>
#include <opencv2/opencv.hpp> int main(int argc, char* argv[])
{// 检查命令行参数if (argc != 3) {std::cerr << "Usage: " << argv[0] << " <input_image> <output_image>" << std::endl;return -1;}// 读取输入图像 imread 完成对文件的读取,图片数据的解码,存放为 Mat 对象 这三个功能cv::Mat inputImage = cv::imread(argv[1]);if (inputImage.empty()) {std::cerr << "Error: Could not load image " << argv[1] << std::endl;return -1;}std::cout << "[1] inputImage... "<< std::endl;// 显示原始图片cv::namedWindow("Input Image", cv::WINDOW_NORMAL);// 被显示的图片会经过一定的预处理,CV_16U和CV_32S 的像素值类型会被除以256.浮点型像素也会将 在 0.0 - 1.0之间的数据显示,小于 0 的显示黑色,大于1显示为白色。cv::imshow("Input Image", inputImage);cv::waitKey(0);std::cout << "[2] grayInputImage... "<< std::endl;// 直接读取成灰度图cv::Mat grayInputImage = cv::imread(argv[1], cv::IMREAD_GRAYSCALE);cv::imshow("Input Image", grayInputImage);cv::waitKey(0);std::cout << "[3] floatImage... "<< std::endl;// 转换为浮点矩阵cv::Mat floatImage;// m – output matrix; if it does not have a proper size or type before the operation, it is reallocated.// rtype – desired output matrix type or, rather, the depth since the number of channels are the same as the input has; if rtype is negative, the output matrix will have the same type as the input.// alpha – optional scale factor.// beta – optional delta added to the scaled values.grayInputImage.convertTo(floatImage, CV_32F, 1.0 / 255.0, 0.0);cv::imshow("Input Image", floatImage);cv::waitKey(0);// 创建灰度图: 240 行 300 列, 类型为 8bit 无符号整型, 像素值为 20cv::Mat image(240, 320, CV_8U, 255);//纯白if (image.empty()) {std::cerr << "Error: Could not load image " << argv[1] << std::endl;return -1;}//获取图片行数和列数std::cout << "Image rows: " << image.rows << ", columns: " << image.cols << std::endl;std::cout << "[4] image... "<< std::endl;// 显示原始图片 同名窗口已存在,不会再创建cv::namedWindow("Input Image", cv::WINDOW_NORMAL);// 被显示的图片会经过一定的预处理,CV_16U和CV_32S 的像素值类型会被除以256.浮点型像素也会将 在 0.0 - 1.0之间的数据显示,小于 0 的显示黑色,大于1显示为白色。cv::imshow("Input Image", image);cv::waitKey(0);std::cout << "[5] image... "<< std::endl;// 重新创建图片 re-allocate the imageimage.create(320, 240, CV_8U);// 将所有像素值设置为 200 ,变黑image = 200;cv::imshow("Input Image", image);cv::waitKey(0);std::cout << "[6] image2... "<< std::endl;//创建一张彩色图片 240 行 320 列, 类型为 8bit 3通道 BGR 的顺序cv::Mat image2(240, 320, CV_8UC3, cv::Scalar(0, 0, 255));cv::imshow("Input Image", image2);cv::waitKey(0);std::cout << "[7] image3... "<< std::endl;//以下两种复制方式,将数据库中的数据复制到 image3 中,两者的数据块是独立的//可以理解成,显式拷贝的方式,是深度拷贝cv::Mat image3;inputImage.copyTo(image3);image3 = inputImage.clone();cv::putText(image3, "you can't see this text,on inputImage.", cv::Point(50, 50), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0));cv::putText(image3, "image3 point to a new data block.", cv::Point(50, 100), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0));cv::imshow("Input Image", inputImage);cv::waitKey(0);//为了表明换了张图片,将图片左右翻转cv::flip(inputImage, inputImage, 1);std::cout << "[8] image4... "<< std::endl;//以下两种复制方式,拷贝构造一张图片,指向同一个数据块//可以理解成,是一种浅拷贝,引用计数的方式拷贝的。cv::Mat image4(inputImage);image4 = inputImage;cv::putText(image4, "you can see this text,on inputImage.", cv::Point(50, 50), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0));cv::putText(image4, "image4 point to same data block.", cv::Point(50, 100), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0));cv::imshow("Input Image", inputImage);cv::waitKey(0);cv::destroyAllWindows();return 0;
}
2.Mat 结构体简述
基本上分为两个部分:属性和数据块。
属性,在上面的例子中已经简单了解了,比如 size、rows、cols、channels
数据块,通过一个指针变量,采用引用计数的方式,指向一块数据。这块数据保存图片的的像素数据。
这块数据,只有在显式要求时,才会进行数据的拷贝。大多数时候,对Mat对象的拷贝也仅仅是拷贝了属性信息,对数据块还是引用的同一个。这在提高效率和避免内存泄露方面是很有效的内存管理方式。需要对这个细节铭记在心,否则也可能会引发一些处理不当的问题。
一个创建cv::Mat 的例子:
//创建 240 行, 320 列, 3 通道, 8 位无符号整型, 红色图片(BGR 颜色顺序)cv::Mat image4(240, 320, CV_8UC3, cv::Scalar(0, 0, 255));
尺寸:行数和列数,就是图片的尺寸。另外,可以通过 cv::Size 这个类型来设置和获取图片的尺寸信息,对应的就是 height 和 width 。
像素格式:每个像素类型可以被指定为:
- CV_8U : 8 bit unsigned integers
- CV_8S : 8 bit signed integers
- CV_8UC3 : 3 通道,每个通道都是 8 bit unsigned integers
- 此外还有:CV_16SC3、CV_32F 等等
像素值类型:对每个像素,进行操作时可能包含多个值。(也就是多个通道)因此产生了 cv::Scalar 这个数据结构,用来存储,传递一个像素的数据。
对应上面的 cv::Scalar(0,0,255) 表示 一个三通道颜色数值。对于一个灰度图,只有一个值时,可以使用 cv::Scalar(100) 来表示一个灰阶数值。
cv::Mat 采用引用计数的方式管理图片数据块。
一般情况下,当调用 create 方法时,release 原有数据块,引用计数 -1 . 并创建 新的数据块,引用计数初始为1. 特殊情况下,当创建的图片 维度和类型,与原有图片完全一致时,将什么都不做,直接返回。
//create 方法用于释放图片的旧数据块,创建新数据块//当创建的图片大小和类型,与旧图片相同时,//create 方法将不会释放图片的旧数据块,也不会创建新的数据块,立即返回。//所以,显示的图片不会有变化image.create(240, 320, CV_8UC3);
采用引用计数的方式,提高了效率,消除了内存泄露的风险。但是也要注意,这是一种浅拷贝,注意使用得当。如果需要对一张图片单独处理,不影响别的地方,还是要独立拷贝一份。也就是 copyto 和 clone 方法。
如果想要将一张图片转换,拷贝为另一种类型的格式,可以使用 convertTo
//convertTo 方法用于将图片的像素数据从一种类型转换为另一种类型cv::Mat image2;image.convertTo(image2, CV_32F, 1/255.0,0.0);
上面的代码中 image2,用于存放结果。CV_32F 表示像素类型要转换为 32 位 浮点数据。
1/255.0 为 缩放因子 ,与像素值计算得到新的缩放值。
0.0 为 偏移值,添加到缩放值的可选增量。。
新的像素值 = 缩放因子 * 原有像素值 + 偏移值
另外,说是要注意转换的图片要与原始图片具有相同的通道数。但是试了下没看到什么问题。
3.关于cv::Mat 的引用计数要注意的一个点
最后,补充一个容易出错的,使用场景:就是让类通过返回 cv::Mat 来获取其属性:
class Test
{//image 属性cv::Mat image;
public:Test():image(240, 320, CV_8UC3, cv::Scalar(0, 0, 255)) {}~Test() {std::cout << "~Test()" << std::endl;}cv::Mat get_image() {//直接返回的是浅拷贝,将可以被外部修改imagereturn image;//为了避免外部意外修改内部image,需要返回一个副本return image.clone();}
};
浅拷贝很好,但是有时,可能会忘记了这个机制。为了避免外部意外修改内部image,可以返回一个副本。
4.完整代码
完整示例代码:
#include <iostream>
#include <opencv2/opencv.hpp> cv::Mat create_image() {//引用计数的实例,该函数调用完成后,将由外部赋值的Mat对象管理cv::Mat image(240, 320, CV_8UC3, cv::Scalar(0, 0, 255));return image;
}class Test
{//image 属性cv::Mat image;
public:Test():image(240, 320, CV_8UC3, cv::Scalar(0, 0, 255)) {}~Test() {std::cout << "~Test()" << std::endl;}cv::Mat get_image() {//直接返回的是浅拷贝,将可以被外部修改imagereturn image;//为了避免外部意外修改内部image,可以返回一个副本return image.clone();}
};int main(int argc, char* argv[])
{//创建 240 行, 320 列, 3 通道, 8 位无符号整型, 红色图片(BGR 颜色顺序)cv::Mat image(240, 320, CV_8UC3, cv::Scalar(0, 0, 255));cv::Size size = image.size();cv::putText(image, "height:"+ std::to_string(size.height), cv::Point(50, 50), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0));cv::putText(image, "width:"+ std::to_string(size.width), cv::Point(50, 100), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0));cv::imshow("Input Image", image);cv::waitKey(0);//create 方法用于释放图片的旧数据块,创建新数据块//当创建的图片大小和类型,与旧图片相同时,//create 方法将不会释放图片的旧数据块,也不会创建新的数据块,立即返回。//所以,显示的图片不会有变化image.create(240, 320, CV_8UC3);cv::imshow("Input Image", image);cv::waitKey(0);//当创建的图片大小和类型,与旧图片不相同时,//create 方法将释放图片的旧数据块,并创建新的数据块image.create(240, 320, CV_8U);cv::imshow("Input Image", image);cv::waitKey(0);//convertTo 方法用于将图片的像素数据从一种类型转换为另一种类型cv::Mat image2;image.convertTo(image2, CV_32F, 1/255.0,0.0);cv::imshow("Input Image", image2);cv::waitKey(0);//示例,转换的图片与原始图片通道数不同//好像也没发生什么。。。cv::Mat image3;image.convertTo(image3, CV_32FC3, 1/255.0,0.0);cv::imshow("Input Image", image3);cv::waitKey(0);//引用计数示例{cv::Mat image4 = create_image();cv::imshow("Input Image", image4);cv::waitKey(0);}//离开块,引用计数减1,当引用计数为0时,图片数据块 最终被释放//浅拷贝的意外示例Test test;cv::Mat otherImage = test.get_image();cv::putText(otherImage, "otherImage modified this image.", cv::Point(50, 50), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0));//由于引用的浅拷贝机制,导致意外的修改,影响到了 Test 类的 image 属性cv::imshow("Input Image", test.get_image());cv::waitKey(0);cv::destroyAllWindows();return 0;
}