OpenCV简介
OpenCV(开源计算机视觉库)是一个开源库,是基于C/C++开发的:
- OpenCV 1.x:基于C语言开发,需要手动分配、释放内存。
- OpenCV 2.x之后:引入C++语言,实现自动化内存管理。
它包含数百种计算机视觉算法,有以下主要模块:
- 核心功能(
Core
):定义基本数据结构的模块,包括密集的多维数组和被其他模块使用的基本函数。 - 图像处理(
imgproc
):一个图像处理模块,包括线性和非线性图像滤波、几何图像变换(调整大小、仿射和透视扭曲等)、颜色空间转换、直方图等。 - 视频分析(
Video
):一个视频分析模块,包括运动估计、背景减除和对象跟踪算法。 - 摄像机校准和三维重建(
calib3d
):基本的多视图几何算法、单摄像机和立体摄像机校准、物体姿态估计、立体对应算法和三维重建。 - 2D特征框架(
features2d
):显著特征检测器、描述符和描述符匹配器。 - 对象检测(
objdetect
):检测预定义类别的实例和对象(例如,人脸、眼睛、杯子、人物、汽车等)。 - 高级GUI(
highgui
):一个简单易于使用的用户界面。 - 视频I/O(
videoio
):一个易于使用的视频捕获和视频编解码器接口。
所有OpenCV类和函数都放在cv
命名空间中。因此,要用代码使用OpenCV的一些功能,需使用cv::
来指定命名空间cv
。
另外,OpenCV提供了Python的 API——OpenCV-Python,它结合了OpenCV C++ API和Python语言的最佳特性。
Mat
OpenCV是一个计算机视觉库,其主要重点是处理和操作图像信息。因此,我们首先需要熟悉OpenCV的基本图像容器——Mat
。
Mat
本质上是由两个数据部分组成的类: 矩阵头(包含信息有矩阵的大小,用于存储的方法,矩阵存储的地址等) 和一个指向包含像素值的矩阵的指针。矩阵头的大小是恒定的。然而,矩阵本身的大小因图像的不同而不同,通常比较大。
OpenCV包含大量图像处理的函数。因此,在使用OpenCV时常常要将图像传递给函数;而图像处理算法的计算量往往很大。如果每次传递都是复制传递整个图像数据,那么将影响到程序的速度。
为了解决这个问题,OpenCV使用了一个引用计数系统。其思想是,每个Mat
对象都有自己的头,但通过让两个Mat
对象的矩阵指针指向同一地址,可以使得两个Mat
对象共享一个矩阵。此外,复制操作符只会复制头和矩阵的指针,而不会复制数据本身。
Mat A, C; //仅创建了头部
A = imread(argv[1], CV_LOAD_IMAGE_COLOR); // 为A分配矩阵
Mat B(A); //使用拷贝构造函数
C = A; //赋值运算符
最后,上述所有对象都指向同一个矩阵,使用其中任何一个进行修改也会影响其他所有对象。实际上,不同的对象只是为相同的底层数据提供了不同的访问方法。然而,它们的头部是不同的。另外,你还可以创建只引用部分数据(图像的一小块)的头。例如,要在图像中创建感兴趣区域(ROI),只需创建一个带有指定边界的新头:
Mat D (A, Rect(10, 10, 100, 100) ); // 用矩形指定边界
Mat E = A(Range:all(), Range(1,3)); // 用行和列来指定边界
问题:如果矩阵本身可能属于多个Mat
对象,当不再需要时,谁负责清理它?
回答:最后一个使用它的对象。
这是通过使用引用计数机制来处理的。每当有人复制Mat
对象的头部 时,矩阵的计数器就会增加。每清除一个头时,该计数器就会减小。当计数器为零时,矩阵被释放。
有时你也会想复制矩阵本身,所以OpenCV提供了cv::Mat::clone()
和cv::Mat::copyTo()
函数。
Mat F = A.clone();
Mat G;
A.copyTo(G);
图像存储方式
我们可以选择颜色空间和使用的数据类型来存储图像。颜色空间指的是我们如何组合颜色组件来对给定的颜色进行编码。最简单的一种是灰度,我们可以使用黑色和白色结合创造出许多灰色的阴影。
对于彩色的图像,我们有更多的方式可供选择,且每种都有各自的优点:
RGB
是最常见的,它也是我们的眼睛形成颜色的方式。为了对颜色的透明度进行编码,有时会添加第四个元素alpha(RGBA)。但是注意,OpenCV的图像空间使用的是BGR
颜色空间。HSV
和HLS
将颜色分解为色调、饱和度和值/亮度分量,这是我们描述颜色更自然的方式。你可以忽略最后一个分量,使算法对输入图像的光照条件不那么敏感。YCrCb
是流行的JPEG图像格式。CIE L*a*b*
是一种均匀颜色空间,如果你需要测量一种颜色到另一种颜色的距离,它很有用。
颜色空间的每个构件都有自己的有效值域。这由使用的数据类型决定。对于数据类型,最小的是char
:一个字节或8位。它可以是无符号的(因此可以存储0到255的值)或有符号的(从-127到+127的值)。虽然在三个组件(如RGB)的情况下,它可以表示1600多万($256\times 256\times 256$)种颜色,但我们可以通过为每个组件使用浮点(4字节=32位)或双精度(8字节=64位)数据类型来获得更精细的表示。然而,要记住到,增加精度也会增加整个图片的占用内存。
显式创建Mat对象
cv::Mat::Mat
构造函数
Mat M(2,2, CV_8UC3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;
对于二通道和多通道图像,我们首先定义它们的大小:行和列(2,2)
。然后我们需要指定用于存储元素的数据类型以及每个矩阵点的通道数。为此,我们定下以下约定定义图像:
CV_[每一项的bit数][有符号或无符号][类型前缀]C[通道数]
例如,CV_8UC3
意味着我们使用8位无符号字符类型,每个像素由三个通道表示。
cv::Scalar
为四元素的短向量(BGRA)。通过它可以用自定义值初始化所有矩阵点各个通道的值。
如果需要更多维度的矩阵,可以使用宏CV_8UC
,并在括号中设置通道数CV_8UC(1)
,如下所示:
- 使用C/C++数组并通过构造函数初始化
int sz[3] = {2,2,2};
Mat L(3,sz, CV_8UC(1), Scalar::all(0));
上面的示例显示了如何创建具有两个以上维度的矩阵。指定其维度3
,然后传递一个包含每个维度大小的指针sz
,其他保持不变。Scalar::all(0)
就是给每个通道都赋值0
cv::Mat::create
函数:
Mat N;
N.create(4, 4, CV_8UC(3));
cout << "N = " << endl << " " << N << 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;
E | O | Z |
---|---|---|
- 对于小矩阵,可以使用逗号分隔的初始值设定项或初始值设定项列表:
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;
说明:这一方式需要C++11支持)
- 为现有Mat对象创建一个新头,并用
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));
输出格式
OpenCV允许你格式化矩阵输出:
- 默认
cout << "R (default) = " << endl << R << endl << endl;
- Python
cout << "R (python) = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl;
- 逗号分隔值(CSV)
cout << "R (csv) = " << endl << format(R, Formatter::FMT_CSV ) << 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;
Mat更多功能: https://docs.opencv.org/4.5.5/d3/d63/classcv_1_1Mat.html
图像读取、显示和保存
了解了Mat
,下面介绍图像的读取、显示和保存
C++
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>
using namespace cv;
int main()
{
std::string image_path = samples::findFile("xing.jpg");
Mat img = imread(image_path, IMREAD_COLOR);
if (img.empty())
{
std::cout << "Could not read the image: " << image_path << std::endl;
return 1;
}
namedWindow("窗口1", WINDOW_AUTOSIZE);
imshow("窗口1", img); //在“窗口1”这个窗口输出图片。
int k = waitKey(0);
if (k == 's')
{
imwrite("image_save.jpg", img);
}
return 0;
}
代码解释:
(1)导入头文件
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>
using namespace cv;
core
:定义了OpenCV库的基本组成部分
imgcodecs
:提供读写功能
highgui
:包含在窗口中显示图像的功能
通过使用using namespace cv
;在下面的示例中,我们就可以直接访问库函数,而无需显式说明命名空间。
(2)读取图像
调用samples::findFile
获取图像路径,调用 cv::imread
读取图像:
std::string image_path = samples::findFile("xing.jpg");
Mat img = imread(image_path, IMREAD_COLOR);
读取的图像数据将存储在cv::Mat
对象中。
imread()
第一个参数是图像的路径。第二个参数是可选的,用于指定图像的格式。它可以是:
IMREAD_COLOR
:默认参数,以BGR 8bit格式读取图像,忽略alpha通道(透明度),可用1
替代。IMREAD_GRAYSCALE
:读入灰度图像,可用0
替代。IMREAD_UNCHANGED
:读入图像,包括alpha通道(透明度),可用-1
替代。
检查图像是否加载成功,失败则退出程序:
if(img.empty())
{
std::cout << "Could not read the image: " << image_path << std::endl;
return 1;
}
(3)在OpenCV窗口展示图像:
namedWindow("窗口1", WINDOW_AUTOSIZE);
imshow("窗口1", img); //在“窗口1”这个窗口输出图片。
int k = waitKey(0);
WINDOW_AUTOSIZE
窗口大小自动适应图片大小,并且不可手动更改。WINDOW_NORMAL
用户可以改变这个窗口大小WINDOW_OPENGL
窗口创建的时候会支持OpenGL
因为我们希望在用户按下任意键之前显示窗口(否则程序结束得太快),所以我们使用waitKey()
,它唯一的参数是它等待用户输入的时间(以毫秒为单位)。0意味着永远等待。返回值k
是按下的键。
(4)保存图像:
如果按下的键是“s
”键,图像将被保存。
if (k == 's')
{
imwrite("image_save.jpg", img);
}
如果是32F的图像,需要转换为8U类型。例如:
#include <opencv2\opencv.hpp>
using namespace cv;
int main()
{
Mat image1 = imread("image.jpg");
namedWindow("image1", WINDOW_NORMAL);
imshow("image1", image1);
waitKey(0);
Mat image2;
image1.convertTo(image2, CV_32F); // 将图像类型从8UC1更改为32FC1
namedWindow("image2", WINDOW_AUTOSIZE);
imshow("image2", image2);
waitKey(0);
return 0;
}
Python
import cv2 as cv
import sys
img = cv.imread(cv.samples.findFile("image.jpg"))
if img is None:
sys.exit("Could not read the image.")
cv.namedWindow("window", cv.WINDOW_AUTOSIZE)
cv.imshow("window", img)
k = cv.waitKey(0)
if k == ord("s"):
cv.imwrite("image_save.jpg", img)
代码解释:
(1) 导入库
import cv2
import sys
sys
用于退出程序
(2)读取图像
img = cv.imread(cv.samples.findFile("image.jpg"),0)
检查图像是否加载成功,失败则退出程序:
if img is None:
sys.exit("Could not read the image.")
(3)在OpenCV窗口展示图像:
cv.namedWindow("window", cv.WINDOW_AUTOSIZE)
cv.imshow("window", img)
k = cv.waitKey(0)
(4)保存图像:
if k == ord("s"):
cv.imwrite("image_save.jpg", img)
更改图像数据类型:
dst = src.astype(np.float32) # 将图像类型从8UC1更改为32FC1
代码:
https://gitee.com/BinaryAI/open-cv-c--and-python/tree/master