原文https://www.docs.opencv.org/4.5.3/
OpenCV(开源计算机视觉库:http://opencv.org)是一个开源库,它包括数百种计算机视觉算法。该文件描述了OpenCV 2.x API,它本质上是一个C++ API,而不是基于C的OpenCV 1.x API(C API 自 OpenCV 2.4 发布以来已弃用且未使用"C"编译器进行测试)。
OpenCV 具有模块化结构,这意味着该包包括多个共享或静态库。提供以下模块:
文档的其他章节描述了每个模块的功能。但首先,请务必熟悉库中彻底使用的常见 API 概念。
所有 OpenCV 类和函数都放在 cv
命名空间中。因此,要从您的代码访问此功能,请使用 cv::
说明符或 using namespace cv;
标识:
#include "opencv2/core.hpp"
...
cv::Mat H = cv::findHomography(points1, points2, cv::RANSAC, 5);
...
或
#include "opencv2/core.hpp"
using namespace cv;
...
Mat H = findHomography(points1, points2, RANSAC, 5 );
...
某些当前或未来的 OpenCV 外部名称可能与 STL 或其他库冲突。在这种情况下,使用显式命名空间说明符来解决名称冲突:
Mat a(100, 100, CV_32F);
randu(a, Scalar::all(1), Scalar::all(std::rand()));
cv::log(a, a);
a /= std::log(2.);
OpenCV 自动处理所有内存。
首先,函数和方法使用的 std::vector
、cv::Mat
和其他数据结构具有析构函数,可在需要时释放底层内存缓冲区。这意味着析构函数并不总是像 Mat 那样释放缓冲区。他们考虑了可能的数据共享。析构函数递减与矩阵数据缓冲区关联的参考计数器。当且仅当引用计数器达到零时,即当没有其他结构引用同一缓冲区时,才会释放缓冲区。类似地,当复制 Mat 实例时,并没有真正复制实际数据。相反,引用计数器递增以记住相同数据的另一个所有者。还有 Mat::clone
方法可以创建矩阵数据的完整副本。请参阅下面的示例:
// create a big 8Mb matrix
//创建一个大的 8Mb 矩阵
Mat A(1000, 1000, CV_64F);
// create another header for the same matrix;
// this is an instant operation, regardless of the matrix size.
//为相同矩阵创建另一个标题;
//这是一个即时操作,无论矩阵大小如何。
Mat B = A;
// create another header for the 3-rd row of A; no data is copied either
//为第3行创建另一个标题;任何一个数据都没有复制
Mat C = B.row(3);
// now create a separate copy of the matrix
//现在创建一个单独的矩阵副本
Mat D = B.clone();
// copy the 5-th row of B to C, that is, copy the 5-th row of A to the 3-rd row of A.
//复制B的第5行到C,即复制A的第5行到A的第3行。
B.row(5).copyTo(C);
// now let A and D share the data; after that the modified version of A is still referenced by B and C.
//现在让A和D共享数据;之后修改了A仍被B和C引用的版本。
A = D;
// now make B an empty matrix (which references no memory buffers),
// but the modified version of A will still be referenced by C,
// despite that C is just a single row of the original A
//现在让B变为一个空矩阵(没有内存缓冲区的引用),
//但A修改后的版本将仍然被C引用,
//尽管C只是原来A中的一行
B.release();
// finally, make a full copy of C. As a result, the big modified
// matrix will be deallocated, since it is not referenced by anyone
//最后,制作C的完整副本。结果,大修改了
//矩阵将被释放,因为它没有被任何人引用
C = C.clone();
你看到使用 Mat
和其他基本结构很简单。但是在不考虑自动内存管理的情况下创建高级类甚至用户数据类型呢?对于他们而言,OpenCV提供了类似于C ++ 11的 std::hared_ptr
的 cv::Ptr
模板类。因此,不要使用普通指针:
T* ptr = new T(...);
你可以使用:
Ptr<T> ptr(new T(...));
//或者
Ptr<T> ptr = makePtr<T>(...);
Ptr< T>
封装指向T实例的指针和与指针关联的引用计数器。有关详细信息,请参见 cv::Ptr
说明。
OpenCV自动释放内存,并在大多数情况下自动为输出函数参数分配内存。因此,如果函数有一个或多个输入数组(例如cv::Mat
)和一些输出数组,则输出数组将自动分配或重新分配。输出数组的大小和类型由输入数组的大小和类型决定。如果需要,这些函数将使用额外的参数来帮助确定输出数组属性。
例如:
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
using namespace cv;
int main(int, char**)
{
VideoCapture cap(0);
if(!cap.isOpened()) return -1;
Mat frame, edges;
namedWindow("edges", WINDOW_AUTOSIZE);
for(;;)
{
cap >> frame;
cvtColor(frame, edges, COLOR_BGR2GRAY);
GaussianBlur(edges, edges, Size(7,7), 1.5, 1.5);
Canny(edges, edges, 0, 30, 3);
imshow("edges", edges);
if(waitKey(30) >= 0) break;
}
return 0;
}
阵列帧由 >>
操作自动分配,因为视频捕获模块知道视频帧分辨率和位深度。阵列边缘由 cvtColor
函数自动分配。它与输入数组具有相同的大小和位深度。通道数为1,因为传递了颜色转换代码 cv::color_BGR2GRAY
,这意味着彩色到灰度的转换。注意,在循环体的第一次执行期间,帧和边仅分配一次,因为所有下一个视频帧具有相同的分辨率。如果您以某种方式更改视频分辨率,阵列将自动重新分配。
该技术的关键组件是 Mat::create
方法。它采用所需的数组大小和类型。如果数组已经具有指定的大小和类型,则该方法不执行任何操作。否则,它将释放先前分配的数据(如果有)(这部分涉及减少引用计数器并将其与零进行比较),然后分配所需大小的新缓冲区。大多数函数为每个输出数组调用 Mat::create
方法,因此实现了自动输出数据分配。
此方案的一些显著例外是 cv::MixChannel
、cv::RNG::fill
和其他一些函数和方法。他们无法分配输出数组,因此你必须提前这样做。
作为一个计算机视觉库,OpenCV处理很多图像像素,这些像素通常以紧凑的、每通道8位或16位的形式编码,因此具有有限的值范围。此外,图像上的某些操作,如颜色空间转换、亮度/对比度调整、锐化、复杂插值(双立方插值、Lanczos)可能会产生超出可用范围的值。如果只存储结果的最低8(16)位,则会产生视觉伪影,并可能影响进一步的图像分析。为了解决这个问题,使用了所谓的饱和算法。例如,要将操作结果r存储到8位图像中,你可以找到0…255范围内最接近的值:
I ( x , y ) = m i n ( m a x ( r o u n d ( r ) , 0 ) , 255 ) I(x,y)=min(max(round(r),0),255) I(x,y)=min(max(round(r),0),255)
类似的规则适用于8位有符号、16位有符号和无符号类型。这种语义在库中随处可见。在C++代码中,它使用类似于标准C++ CAST操作的 cv::saturate_cast<>
函数。见上文公式的实施情况:
I.at<uchar>(y, x) = saturate_cast<uchar>(r);
其中 cv::uchar
是OpenCV 8位无符号整数类型。在优化的SIMD代码中,使用了诸如ppaddusb, packuswb等SSE2指令。它们有助于实现与C++代码完全相同的行为。
注:当结果为32位整数时,不应用饱和。
模板是C++的一大特色,它能够实现非常强大、高效且安全的数据结构和算法。但是,模板的广泛使用可能会显著增加编译时间和代码大小。此外,如果只使用模板,则很难将接口和实现分开。这对于基本算法来说可能很好,但对于计算机视觉库来说并不好,因为在计算机视觉库中,单个算法可能跨越数千行代码。因此,为了简化其他语言(如Python、Java、Matlab)绑定的开发,这些语言根本没有模板或模板功能有限,当前的OpenCV实现基于多态性和运行时模板调度。在运行时调度太慢(如像素访问操作符)、不可能(通用 cv::Ptr
实现)或非常不方便( cv::saturate_cast<>()
)的地方,当前的实现引入了小模板类、方法和函数。在当前OpenCV版本的任何其他地方,模板的使用都是有限的。
因此,库可以操作的基本数据类型集是有限的。也就是说,数组元素应具有以下类型之一:
CV_CN_MAX
常量定义,该常量当前设置为512。对于这些基本类型,将应用以下枚举:
enum { CV_8U=0, CV_8S=1, CV_16U=2, CV_16S=3, CV_32S=4, CV_32F=5, CV_64F=6 };
可以使用以下选项指定多通道(n通道)类型:
CV_8UC1
… CV_64FC4
常数(适用于从1到4的多个通道)CV_8UC(n)
… CV_64FC(n)
or CV_MAKETYPE(CV_8U, n)
… CV_MAKETYPE(CV_64F, n)
编译时通道数大于4或未知时的宏。注:
CV_32FC1 == CV_32F
,CV_32FC2 == CV_32FC(2) == CV_MAKETYPE(CV_32F, 2)
, andCV_MAKETYPE(depth, n) == ((depth&7) + ((n-1)<<3)
. 这意味着常数类型由深度形成,取最低的3位,通道数减1,取下一个log2(CV_CN_MAX)
位。
例如:
Mat mtx(3, 3, CV_32F);
// make a 3x3 floating-point matrix
// 创建一个3x3浮点矩阵
Mat cmtx(10, 1, CV_64FC2);
// make a 10x1 2-channel floating-point
// matrix (10-element complex vector)
// 创建一个10x1的2通道浮点值
// 矩阵(10元素的复杂向量)
Mat img(Size(1920, 1080), CV_8UC3);
// make a 3-channel (color) image of 1920 columns and 1080 rows.
// 创建一个有1920列和1080行的3通道(彩色)图像。
Mat grayscale(image.size(), CV_MAKETYPE(image.depth(), 1));
// make a 1-channel image of the same size and same channel type as img
// 创建一个与img的尺寸和通道类型相同的单通道图像。
无法使用OpenCV构造或处理包含更复杂元素的数组。此外,每个函数或方法只能处理所有可能数组类型的子集。通常,算法越复杂,支持的格式子集越小。见以下此类限制的典型示例:
cv::add
,支持所有类型。每个功能支持的类型子集已根据实际需要定义,并可在将来根据用户请求进行扩展。
许多OpenCV函数处理密集的二维或多维数值数组。通常,此类函数将cppMat作为参数,但在某些情况下,使用std::vector<>
(例如,对于点集)或 cv::Matx<>
(对于3x3单应矩阵等)更方便。为了避免API中的许多重复,引入了特殊的“代理”类。基本的“代理”类是 cv::InputArray
。它用于在函数输入上传递只读数组。派生自InputArray类cv::OutputArray
用于指定函数的输出数组。通常,你不应该关心这些中间类型(也不应该显式地声明这些类型的变量)——它将自动工作。您可以假设可以始终使用Mat
、std::vector<>
、cv::Matx<>
、cv::Vec<>
或cv::Scalar
代替InputArray/OutputArray
。如果函数具有可选的输入或输出数组,而你没有想要或不想要,请传递cv::noArray()
。
OpenCV使用异常来表示严重错误。当输入数据具有正确的格式并且属于指定的值范围,但由于某种原因(例如,优化算法没有收敛)算法无法成功时,它将返回一个特殊的错误代码(通常,只是一个布尔变量)。
异常可以是 cv::Exception
类或其派生类的实例。反过来,cv::Exception
是std::Exception
的派生。因此,它可以在代码中优雅地使用其他标准的C++库组件来处理。
通常使用CV_Error(errcode, description)
宏或printf-like的CV_Error_(errcode, (printf-spec, printf-args))
变量引发异常,或者使用CV_Assert(condition)
宏检查条件并在不满足条件时引发异常。对于性能关键代码,CV_DbgAssert(condition)
仅保留在调试配置中。由于自动内存管理,所有中间缓冲区在发生突然错误时自动释放。如果需要,只需添加try语句即可捕获异常:
try
{
... // call OpenCV
}
catch (const cv::Exception& e)
{
const char* err_msg = e.what();
std::cout << "exception caught: " << err_msg << std::endl;
}
当前的OpenCV实现是完全可重新输入的。也就是说,可以从不同线程调用不同类实例的相同函数或相同方法。同样,相同的Mat也可以在不同的线程中使用,因为引用计数操作使用特定于体系结构的原子指令。
在计算机上构建和安装OpenCV
原始作者 | Bernát Gábor |
---|---|
兼容性 | OpenCV >= 3.0 |
我们有多种方法从现实世界获取数字图像:数码相机、扫描仪、计算机断层扫描和磁共振成像等等。在任何情况下,我们(人类)看到的都是图像。然而,当将其转换到数字设备时,我们记录的是图像每个点的数值。
例如,在上图中,您可以看到汽车的后视镜只不过是一个包含所有像素点强度值的矩阵。我们获取和存储像素值的方式可能会根据我们的需要而有所不同,但最终,计算机世界中的所有图像可能会简化为数字矩阵和其他描述矩阵本身的信息。OpenCV是一个计算机视觉库,其主要重点是处理和操作这些信息。因此,您首先需要熟悉OpenCV如何存储和处理图像。
OpenCV从2001年就开始出现了。那个时候该库是围绕C语言接口构建的,为了将图像存储在内存中,他们使用了一种称为IplImage的C结构。这是您将在大多数旧教程和教材中看到的。问题是,它将C语言的所有缺点都带到表面。最大的问题是手动内存管理。它建立在用户负责内存分配和释放的假设之上。虽然这对于较小的程序来说不是问题,但一旦代码库增长,处理所有这些问题将比专注于解决开发目标更困难。
幸运的是C++出现并且采用了通过自动内存管理(或多或少)为用户提供更容易的类的概念。好消息是C++完全兼容C,所以不需要从更改中产生兼容性问题。因此,OpenCV 2 引入了一个新的C++接口,它提供了一种新的处理方式,这意味着你不需要费力地进行内存管理,使代码简洁(少写,多做)。C++接口的主要缺点是,目前许多嵌入式开发系统只支持C。因此,除非您针对嵌入式平台,否则使用旧方法是没有意义的(除非您是一个受虐狂程序员并且自找麻烦)。
关于Mat,您需要知道的第一件事是,您不再需要手动分配其内存,并在不需要时尽快释放它。虽然这样做仍然是可能的,但大多数OpenCV函数将自动分配其输出数据。如果您传递一个已经存在的Mat对象(它已经为矩阵分配了所需的空间),这将是一个很好的奖励,它将被重用。换句话说,我们在任何时候都只使用执行任务所需的内存。
Mat基本上是一个包含两个数据部分的类:矩阵头(包含诸如矩阵大小、用于存储的方法、存储矩阵的地址等信息)和指向包含像素值的矩阵的指针(根据选择的存储方法采用任何维度)。矩阵头大小是恒定的,但矩阵本身的大小可能因图像而异,通常会大几个数量级。
OpenCV是一个图像处理库。它包含大量的图像处理功能。为了解决计算难题,大多数情况下,你将使用库的多个函数。因此,将图像传递给函数是一种常见的做法。我们不应该忘记,我们谈论的是图像处理算法,它们往往计算量很大。我们最不希望做的事情是通过对可能较大的图像进行不必要的复制来进一步降低程序的速度。
为了解决这个问题,OpenCV使用了一个引用计数系统。其思想是,每个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
// 赋值运算符
最后,上述所有对象都指向同一个数据矩阵,使用其中任何一个进行修改也会影响所有其他对象。实际上,不同的对象只是为相同的底层数据提供了不同的访问方法。然而,它们的头部分是不同的。真正有趣的是,您可以创建只引用完整数据的一部分到头。例如,要在图像中创建感兴趣区域(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不会影响A的标题所指向的矩阵。你需要记住的是:
cv::Mat::clone()
和cv::Mat::copyTo()
函数复制图像的基础矩阵。这是关于如何存储像素值的。您可以选择颜色空间和使用的数据类型。颜色空间是指我们如何组合颜色组件以编码给定的颜色。最简单的一种是灰度,我们可以使用的颜色是黑色和白色。这些因素的结合使我们能够创造出许多灰色阴影。
对于丰富多彩的方式,我们有更多的方式可供选择。它们中的每一个都将其分解为三到四个基本组件,我们可以使用这些组件的组合来创建其他组件。最流行的是RGB,主要是因为这也是我们的眼睛建立颜色的方式。它的基色是红色、绿色和蓝色。为了对颜色的透明度进行编码,有时会添加第四个元素:alpha(A)。
然而,也还有许多其他颜色系统,每种颜色系统都有各自的优点:
每个创建的构件都有自己的有效域。这将导致使用的数据类型。我们存储组件的方式定义了我们对其域的控制。可能的最小数据类型是char,即一个字节或8位。这可能是无符号的(因此可以存储0到255之间的值)或有符号的(值从-127到+127)。虽然在三个组件的情况下,这已经提供了1600万种可能的颜色来表示(如RGB),但我们可以通过使用每个组件的浮点(4字节=32位)或双(8字节=64位)数据类型来获得更精细的控制。然而,请记住,增加组件的大小也会增加内存中整个图片的大小。
在“加载、修改和保存图像”教程中,您已经学习了如何使用cv::imwrite()
函数将矩阵写入图像文件。但是,出于调试目的,查看实际值要方便得多。可以使用Mat的<<
运算符执行此操作。请注意,这仅适用于二维矩阵。
虽然Mat作为图像容器工作得很好,但它也是一个通用的矩阵类。因此,可以创建和操作多维矩阵。可以通过多种方式创建垫对象:
cv::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]
例如,CV_8UC3意味着我们使用8位长的无符号字符类型,每个像素有三个字符来形成三个通道。最多可为四个通道预定义类型。标量是四元素短向量。指定它,可以使用自定义值初始化所有矩阵点。如果需要更多,可以使用上宏创建类型,如下面所示,在括号中设置通道号。
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;
不能使用此构造初始化矩阵值。只有当新的大小不适合旧的大小时,它才会重新分配其矩阵数据内存。
cv::Mat::Zero
,cv::Mat::One
,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;
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;
cv::Mat::clone
或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));
cout << "R (default) = " << endl << R << endl << endl;
cout << "R (python) = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl;
cout << "R (csv) = " << endl << format(R, Formatter::FMT_CSV ) << endl << endl;
cout << "R (numpy) = " << endl << format(R, Formatter::FMT_NUMPY ) << endl << endl;
cout << "R (c) = " << endl << format(R, Formatter::FMT_C ) << endl << endl;
OpenCV还通过<<运算符提供对其他常见OpenCV数据结构输出的支持:
Point2f P(5, 1);
cout << "Point (2D) = " << P << endl << endl;
Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;
std::vector
通过 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;
这里的大多数示例都包含在一个小型控制台应用程序中。你可以从这里或cpp示例的核心部分下载。
你也可以在YouTube上找到这方面的快速视频演示。