本文作者:小嗷
微信号:aoxiaoji
简书链接:https://www.jianshu.com/u/45da1fbce7d0
关键词:基本的图像容器
例如,在上面的图片中,你可以看到,汽车的镜子只不过是一个包含了像素点的所有强度值的矩阵。我们如何获取和存储像素值可能根据我们的需要而变化,但最终,计算机世界中的所有图像都可能被简化为数字矩阵和描述矩阵本身的其他信息。OpenCV是一个计算机视觉库,它的主要焦点是处理和操作这些信息。因此,您需要熟悉的第一件事是OpenCV如何存储和处理图像。
Mat
OpenCV自2001年以来就一直存在。在那些日子里,图书馆是围绕一个C接口构建的,并将图像存储在内存中,他们使用了一个名为IplImage的C结构。这是你将在大多数旧的教程和教育材料中看到的。这个问题的问题在于,它把C语言的所有缺点都带到了桌子上。最大的问题是手动内存管理。它建立在用户负责处理内存分配和回收的假设之上。虽然这不是小程序的问题,但是一旦您的代码库增长了,那么处理所有这些问题将会更加困难,而不是专注于解决您的开发目标。
幸运的是,C++出现了,并引入了类的概念,通过自动内存管理(或多或少)使用户更容易。好消息是,C++与C完全兼容,因此不可能产生兼容性问题。因此,opencv2.0引入了一个新的C++接口,它提供了一种新的做事方式,这意味着您不需要修改内存管理,使您的代码简洁(更少写,以获得更多)。C++接口的主要缺点是,目前许多嵌入式开发系统只支持c.因此,除非您的目标是嵌入式平台,否则使用旧方法是没有意义的(除非您是一个受虐待的程序员,并且您正在自找麻烦)。
关于Mat,您需要知道的第一件事是,您不再需要手动分配它的内存,并在您不需要它的时候立即释放它。虽然这仍然是一种可能,但大多数OpenCV函数都会自动分配它的输出数据。如果你传递一个已经存在的Mat对象,它已经为矩阵分配了所需的空间,那么这将被重用。换句话说,我们在任何时候都只使用我们执行任务所需的内存。
Mat基本上是一个类有两个数据部分:矩阵头(包含信息矩阵的大小等,用于存储的方法,在解决矩阵存储,等等)和一个指向包含像素值的矩阵(采取任何维数取决于选择的方法来存储)。矩阵头的大小是恒定的,但是矩阵本身的大小可能因图像而异,而且通常是按数量级放大的。
OpenCV是一个图像处理库。它包含大量的图像处理功能。要解决计算难题,大多数情况下,您最终会使用库的多个功能。正因为如此,将图像传递给函数是一种常见的做法。我们不应该忘记,我们讨论的是图像处理算法,它的计算量非常大。我们最不想做的事情就是通过不必要地复制潜在的大图片来进一步降低程序的速度。
为了解决这个问题,OpenCV使用一个引用计数系统。其思想是,每个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
所有上面的对象,最后都指向同一个单一的数据矩阵。但是,它们的标题是不同的,并且使用它们中的任何一个进行修改也会影响到所有其他的。在实践中,不同的对象只是为相同的底层数据提供不同的访问方法。然而,它们的头部分是不同的。真正有趣的部分是,您可以创建只引用完整数据的子部分的头部。例如,在一个图像中创建一个感兴趣的区域(ROI),您只需创建一个带有新边界的新标头
Mat D (A, Rect(10, 10, 100, 100) ); // using a rectangle
Mat E = A(Range::all(), Range(1,3)); // using row and column boundaries
现在你可能会问,这个矩阵本身是否属于多个Mat对象,当它不再需要时,它负责清理它。简短的回答是:最后一个使用它的对象。这是通过使用引用计数机制来处理的。每当有人复制一个Mat对象的头,就会为矩阵增加一个计数器。当一个头被清理时,这个计数器就会减少。当计数器达到零时,矩阵也被释放了。有时您也想要复制矩阵本身,所以OpenCV提供了cv::Mat::clone() 和cv::Mat::copyTo() 函数。
Mat F = A.clone();
Mat G;
A.copyTo(G);
现在修改F或G不会影响到由Mat头指向的矩阵。你需要记住的是:
- OpenCV功能的输出图像分配是自动的(除非另有说明)。
- 你不需要用OpenCVs C++接口来考虑内存管理。
- 转让操作人和复制构造器只复制标题。
- 图像的基本矩阵可以用cv::Mat::clone()和cv::Mat::copyTo() 函数来复制。
存储方法
这是关于如何存储像素值的。您可以选择使用的颜色空间和数据类型。颜色空间指的是我们如何将颜色组件组合成一种特定的颜色。最简单的是灰色的比例,我们可以使用的颜色是黑白的。这些组合可以让我们创造出许多灰色的阴影。
对于丰富多彩的方式,我们有更多的方法可供选择。每一个都将其分解为3或4个基本组成部分,我们可以使用这些组合来创建其他的组件。最受欢迎的是RGB,主要是因为这也是我们的眼睛是如何构建颜色的。它的底色是红色、绿色和蓝色。要对颜色的透明度进行编码,有时会有第四个元素:alpha(A)被添加。
然而,还有许多其他的颜色系统都有各自的优势:
- RGB是最常见的,因为我们的眼睛使用的是类似的东西,但请记住,OpenCV标准显示系统使用BGR颜色空间(红色和蓝色通道的开关)构成颜色。
- HSV和HLS将颜色分解为它们的色调、饱和度和亮度/亮度组件,这是我们描述颜色的一种更自然的方式。例如,您可能会忽略最后一个组件,使您的算法对输入图像的光线条件不那么敏感。
- YCrCb被流行的JPEG图像格式使用。
- CIE b是一种感知统一的颜色空间,如果你需要测量给定颜色到另一种颜色的距离,这就很方便了。
每个building 组件都有自己的有效域。这将导致使用的数据类型。我们如何存储一个组件定义了我们对其领域的控制。最小的数据类型可能是char,这意味着一个字节或8位。这可能是无符号的(也可以将值从0到255)或签名(从-127到+127的值)。尽管在三个组件的情况下,这已经提供了1600万种可能的颜色(如RGB),但是我们可以通过使用浮点数(4字节=32位)或双(8字节=64位)数据类型来获得更精确的控制。不过,请记住,增加组件的大小也会增加内存中整个图像的大小。
显式地创建Mat对象
在Load、修改和保存一个图像教程中,您已经学习了如何通过使用cv::imwrite()函数来将矩阵写入到图像文件中。但是,为了便于调试,可以更方便地看到实际的值。你可以用Mat的运算符来做这个,要知道这只适用于二维矩阵。
虽然Mat作为一个图像容器很好地工作,但它也是一个通用的矩阵类。因此,创建和操作多维矩阵是可能的。您可以以多种方式创建Mat对象
Mat M(2,2, CV_8UC3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;
对于二维和多通道的图像,我们首先定义它们的大小:行和列计数。
然后,我们需要指定用于存储元素的数据类型和每个矩阵点的通道数量。为了做到这一点,我们根据以下约定构建了多个定义:
CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
例如,cv8uc3意味着我们使用的是8位长的无符号char类型,每个像素都有三个组成三个通道。这是预先定义的最多4个通道号码。cv:标量是四个元素的短向量。指定这一点,您可以用定制值初始化所有矩阵点。如果您需要更多,您可以使用上面的宏创建类型,在括号中设置通道号,如下所示。
使用c/c++阵列并通过构造器初始化
int sz[3] = {2,2,2};
Mat L(3,sz, CV_8UC(1), Scalar::all(0));
上面的例子展示了如何创建一个包含两个维度的矩阵。指定它的维度,然后传递一个包含每个维度大小的指针,其余的保持不变。
cv::Mat::create函数:
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;
对小型矩阵可以使用逗号分隔的初始化或初始化器列表(c++ 11支持是必需的在过去的情况下):
Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
cout << "C = " << endl << " " << C << endl << endl;
C = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3);
cout << "C = " << endl << " " << C << endl << endl;
为现有的Mat对象创建一个新标题和cv::Mat::clone or cv::Mat::copyTo
Mat RowClone = C.row(1).clone();
cout << "RowClone = " << endl << " " << RowClone << endl << endl;
注意:您可以使用cv::randu()函数填写一个带有随机值的矩阵。你需要给随机值赋予更低的和更高的值
Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));
输出格式
在上面的例子中,您可以看到默认的格式化选项。然而,OpenCV允许你格式化你的矩阵输出:
默认
cout << "R (default) = " << endl << R << endl << endl;
Python
cout << "R (python) = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl;
Numpy
cout << "R (numpy) = " << endl << format(R, Formatter::FMT_NUMPY ) << endl << endl;
C
cout << "R (c) = " << endl << format(R, Formatter::FMT_C ) << endl << endl;
其他常见命令的输出
OpenCV通过<<操作符:也为其他常见的OpenCV数据结构的输出提供支持:
•2D Point
Point2f P(5, 1);
cout << "Point (2D) = " << P << endl << endl;
•3D Point
Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;
•std::vector via cv::Mat
vector<float> v;
v.push_back( (float)CV_PI); v.push_back(2); v.push_back(3.01f);
cout << "Vector of floats via Mat = " << Mat(v) << endl << endl;
•std::vector of points
vector<Point2f> vPoints(20);
for (size_t i = 0; i < vPoints.size(); ++i)
vPoints[i] = Point2f((float)(i * 5), (float)(i % 7));
cout << "A vector of 2D Points = " << vPoints << endl << endl;
//---------------------------------【头文件、命名空间包含部分】---------------------------
// 描述:包含程序所使用的头文件和命名空间
//-----------------------------------------------------------------------------------------------
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace std;
using namespace cv;
//--------------------------------------【main( )函数】-----------------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//-----------------------------------------------------------------------------------------------
int main(int, char**)
{
//改变控制台的前景色和背景色
system("color 8F");
Mat I = Mat::eye(4, 4, CV_64F);
I.at<double>(1, 1) = CV_PI;
cout << "\nI = " << I << ";\n" << endl;
Mat r = Mat(10, 3, CV_8UC3);
randu(r, Scalar::all(0), Scalar::all(255));
//此段代码的OpenCV2版为:
//cout << "r (OpenCV默认风格) = " << r << ";" << endl << endl;
//cout << "r (Python风格) = " << format(r,"python") << ";" << endl << endl;
//cout << "r (Numpy风格) = " << format(r,"numpy") << ";" << endl << endl;
//cout << "r (逗号分隔风格) = " << format(r,"csv") << ";" << endl<< endl;
//cout << "r (C语言风格) = " << format(r,"C") << ";" << endl << endl;
//此段代码的OpenCV3版为:
cout << "r (OpenCV默认风格) = " << r << ";" << endl << endl;
cout << "r (Python风格) = " << format(r, Formatter::FMT_PYTHON) << ";" << endl << endl;
cout << "r (Numpy风格) = " << format(r, Formatter::FMT_NUMPY) << ";" << endl << endl;
cout << "r (逗号分隔风格) = " << format(r, Formatter::FMT_CSV) << ";" << endl << endl;
cout << "r (C语言风格) = " << format(r, Formatter::FMT_C) << ";" << endl << endl;
Point2f p(6, 2);
cout << "【2维点】p = " << p << ";\n" << endl;
Point3f p3f(8, 2, 0);
cout << "【3维点】p3f = " << p3f << ";\n" << endl;
vector<float> v;
v.push_back(3);
v.push_back(5);
v.push_back(7);
cout << "【基于Mat的vector】shortvec = " << Mat(v) << ";\n" << endl;
vector<Point2f> points(20);
for (size_t i = 0; i < points.size(); ++i)
points[i] = Point2f((float)(i * 5), (float)(i % 7));
cout << "【二维点向量】points = " << points << ";";
getchar();//按任意键退出
return 0;
}
效果图
[图片上传失败...(image-f2cd1e-1526805404336)]
- 本人是抱着玩一玩的心态,学习opencv(其实深度学习没有外界说的这么高深,小嗷是白板,而且有工作在身并且于代码无关)
- 大家可以把我的数学水平想象成初中水平,毕竟小嗷既不是代码靠吃饭又不是靠数学吃饭,毕业N年
- 写文章主要是为了后人少走点弯路,多交点朋友,一起学习
- 如果有好的图像识别群拉我进去QQ:631821577
- 就我一个白板,最后还是成的,你们别怕,慢慢来把
分享可以无数次,转载成自己文章QQ邮箱通知一下,未经授权请勿转载。
- 邮箱:631821577@qq.com
- QQ群:736854977
- 有什么疑问公众号提问,下班或者周六日回答,ths