OpenCV C++ 快速入门与基础操作
本文详细介绍了 OpenCV C++ 库的基础使用,涵盖项目构建流程(CMake 及 ROS 环境)、Mat 类数据结构(构造、赋值、运算、元素访问)、图像与视频的读写显示、以及常见的图像处理技术(颜色空间转换、几何变换、直方图、滤波等)。通过代码示例帮助读者掌握 OpenCV 核心 API,适用于计算机视觉入门学习。

本文详细介绍了 OpenCV C++ 库的基础使用,涵盖项目构建流程(CMake 及 ROS 环境)、Mat 类数据结构(构造、赋值、运算、元素访问)、图像与视频的读写显示、以及常见的图像处理技术(颜色空间转换、几何变换、直方图、滤波等)。通过代码示例帮助读者掌握 OpenCV 核心 API,适用于计算机视觉入门学习。

重要提示
本文主要介绍 ROS 和 OpenCV 的学习,使用 C++ 版本。
基础能力
你需要具备 bash(常用命令)、CMake(会编译、写 CMakeLists.txt 文件等)、C++、线性代数等的基础,最好会使用 Git。目前我们只学习纯 OpenCV 的用法,将来会用到 ROS,所以还需要 ROS 的基础。这里建议用 VSCode、CLion,推荐使用 VSCode。
安装 OpenCV、VSCode
(1) 安装 OpenCV
在 Ubuntu 20.04 LTS 中安装了 ROS Noetic,因为 ROS Noetic 已经内置了 OpenCV,也就是说当我们安装好 ROS 后 OpenCV 就已经装好了,所以最好不要再在电脑上单独下载 OpenCV,否则会很麻烦。
(2) 安装 VSCode
前往 VSCode 官网,选择下载
.deb,然后找到.deb所在目录,右键打开终端,输入命令sudo dpkg -i (deb 包名)安装。VSCode 的一些插件:
C/C++、C/C++ Extension Pack、C/C++ Themes、CMake Tools、Github Copilot、Github Copilot Chat、Python Debugger、Python Environments、Pylance、Python、Robot Developer Extensions for ROS 1、XML Tools、Chinese (Simplified) (简体中文) Language Pack for Visual Studio Code。(3) 检查 OpenCV 是否安装成功
查看 OpenCV 的版本,输入命令
opencv_version,如果有返回,说明安装成功,我这里显示4.2.0。
一般的流程如下:
根目录: ~/opencv_ws/(或者你自定义的名字)
源代码目录: ~/opencv_ws/src(存放源代码文件)
构建目录: ~/opencv_ws/build(存放编译生成的文件)
tree 结构
cmake_minimum_required(VERSION 3.16.3) project(OpenCV_Project) set(CMAKE_CXX_STANDARD 11) find_package(OpenCV REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS}) add_subdirectory(src)
配置文件 根目录下的 CMakeLists.txt 文件和 src 目录下的 CMakeLists.txt 文件
根目录下的 CMakeLists.txt 文件
cmake_minimum_required(VERSION 3.16.3) project(OpenCV_Project) set(CMAKE_CXX_STANDARD 11) find_package(OpenCV REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS}) add_subdirectory(src)
src 目录下的 CMakeLists.txt 文件
add_executable(main main.cpp) target_link_libraries(main ${OpenCV_LIBS})
源代码文件一般放在 src 目录下
编译结果目录: 可执行文件一般放在 build 目录下
在终端中进入 build 目录,执行以下命令进行编译:
cd ~/opencv_ws/build
cmake ..
make
编译完成后,在 build 目录下会生成可执行文件 main,运行程序:
./main
这样就完成了一个简单的 OpenCV 项目的编译和运行。
mkdir -p ~/catkin_ws/src
cd ~/catkin_ws/
catkin build
cd ~/catkin_ws/src
catkin_create_pkg cv4_learning_1_1 std_msgs rospy roscpp cv_bridge image_transport
├── devel
├── src
└── CMakeLists.txt
cmake_minimum_required(VERSION 3.16.3) project(cv4_learning_1_1) set(CMAKE_CXX_STANDARD 11) find_package(catkin REQUIRED COMPONENTS roscpp std_msgs cv_bridge image_transport ) find_package(OpenCV REQUIRED) catkin_package() include_directories( ${catkin_INCLUDE_DIRS} ${OpenCV_INCLUDE_DIRS} ) add_executable(main src/main.cpp) target_link_libraries(main ${catkin_LIBRARIES} ${OpenCV_LIBS} )
在终端中进入 catkin_ws 目录,执行以下命令进行编译:
cd ~/catkin_ws
catkin build
编译完成后,在 devel 目录下会生成可执行文件 main,运行程序:
source devel/setup.bash
rosrun cv4_learning_1_1 main
这样就完成了一个简单的 ROS 下 OpenCV 项目的编译和运行。
重要提示
目前我们只学习纯 OpenCV 项目
# 创建项目文件
mkdir -p ~/opencv/build
cd ~/opencv/
touch CMakeLists.txt main.cpp
OpenCV 4.2.0 是基于 C++11 的,所以需要确保 CMake 使用 C++11 标准进行编译。
一般我们将CMakeLists.txt文件中添加以下行:
cmake_minimum_required(VERSION 3.16.3) #最低版本
project(OpenCV_Learning_1_1) #项目名称
set(CMAKE_CXX_STANDARD 11) #设置 C++11 标准
find_package(OpenCV REQUIRED) # 查找 OpenCV
include_directories(${OpenCV_INCLUDE_DIRS})# 包含头文件路径
add_executable(main main.cpp) # 生成可执行文件
target_link_libraries(main ${OpenCV_LIBS}) # 链接 OpenCV 库
cd ~/opencv/build
cmake ..
make
./main # 运行程序
根据案例,我们来创建一个简单的 OpenCV 程序,读取并显示一张图片。
mkdir -p ~/opencv/build
cd ~/opencv/
touch CMakeLists.txt main.cpp
main.cpp 内容如下:
#include <opencv2/opencv.hpp>//包含 opencv 头文件
#include <iostream>
using namespace cv;//使用 opencv 命名空间
using namespace std;
int main(int argc, char** argv){
Mat img = imread("test.jpg");//读取图片,注意图片路径,可以是相对路径或绝对路径
if(img.empty()){
cout << "图片读取失败!" << endl;
return -1;
}
imshow("显示图片",img);//显示图片
waitKey(0);//等待按键
return 0;
}
CMakeLists.txt 内容如下:
cmake_minimum_required(VERSION 3.16.3) #最低版本
project(OpenCV_Learning_1_1) #项目名称
set(CMAKE_CXX_STANDARD 11) #设置 C++11 标准
find_package(OpenCV REQUIRED) # 查找 OpenCV
include_directories(${OpenCV_INCLUDE_DIRS})# 包含头文件路径
add_executable(main main.cpp) # 生成可执行文件
target_link_libraries(main ${OpenCV_LIBS}) # 链接 OpenCV 库
编译和运行程序:
cd ~/opencv/build
cmake ..# ..是告诉 CMake 去上一级目录找 CMakeLists.txt 文件
make
./main # 运行程序
这样就完成了一个简单的 OpenCV 项目,读取并显示一张图片。
提示
建议看看了解就行,目前不必深究
| 模块名称 | 功能描述 |
|---|---|
| core | 基础数据结构和算法 |
| imgproc | 图像处理和变换 |
| highgui | 图形用户界面和图像显示 |
| imgcodecs | 图像编解码 |
| videoio | 视频输入输出 |
| features2d | 特征检测和描述 |
| calib3d | 相机标定和三维重建 |
| ml | 机器学习 |
| objdetect | 目标检测 |
| photo | 图像修复和增强 |
| video | 视频分析 |
| stitching | 图像拼接 |
| dnn | 深度神经网络 |
| flann | 快速近似最近邻搜索 |
| gapi | 图形加速和计算图 |
通过学习和实践这些模块,可以更好地理解和应用 OpenCV 的强大功能。
提示
建议看看了解就行,目前不必深究
提示
这一节主要是了解,无需深究。
(1) Mat 类的介绍与创建
Mat 类用来保存矩阵类型的数据信息,包括向量、矩阵、灰度或彩色图像等数据。
Mat 类分为矩阵头和指向存储数据的矩阵指针两部分。
矩阵头中包含矩阵的尺寸、存储方法、地址和引用次数等。矩阵头的大小是一个常数,不会随着矩阵尺寸的大小而改变。
在绝大多数情况下,矩阵头大小远小于矩阵中数据量的大小,因此图像复制和传递过程中主要的开销是存放在矩阵数据。为了解决这个问题,OpenCV 中复制和传递图像时,只是复制了矩阵头和指向存储数据的指针 (类似于浅拷贝),因此,在创建 Mat 类时,可以先创建矩阵头后赋值数据,其方法如下所示
代码 2-1 创建 Mat 类
Mat a;//创建一个名为 a 的矩阵头
a=imread("test.jpg");//向 a 中赋值图像数据,矩阵指针指向像素数据
Mat b=a;//复制矩阵头,并且命名为 b,b 和 a 指向同一块数据区域
上面的代码首先创建了一个名为 a 的矩阵头,之后读入了一张图像并将 a 中的矩阵指针指向该图像的像素数据,最后将 a 矩阵头中的内容复制到 b 矩阵头中。
虽然 a、b 有各自的矩阵头,但是其矩阵指针指向的是同一个矩阵数据,通过任意一个矩阵头修改矩阵中的数据,另一个矩阵头指向的数据会跟着发生改变。因为矩阵头中引用次数标记了引用某个矩阵数据的次数,只有当矩阵数据的引用次数为 0 时,矩阵数据才会被释放。
提示
矩阵头的引用次数,目前不重要,不必深究
小知识
注意
采用引用次数来释放存储内容是 C++ 中常见的方式,用这种方式可以避免仍有某个变量引用数据时将这个数据删除造成程序崩溃的问题,同时极大地缩减程序运行时所占用的内存。
(2) Mat 类的存储数据类型
注意
Mat 类可以存储的数据类型包含 double、float、uchar、unsigned char,以及自定义的模板等。
也即:Mat_<Tp>;、Mat_<double>;、Mat_<float>;、Mat_<uchar>;、Mat_<unsigned char>;。
可以通过下面的方式来声明一个存放指定类型的 Mat 类变量:
代码 2-2 声明一个指定类型的 Mat 类
Mat A = Mat_<double>(3,3);//创建一个 3*3 的矩阵用于存放 double 类型数据
由于 OpenCV 提出Mat 类主要用于存储图像,而像素值的最大值又决定了图像的质量,如果用 8 位无符号整数存储 16 位图像,会造成严重的图像颜色失真或造成数据错误。而由于不同位数的编译器对数据长度定义不同,为了避免在不同环境下因变量位数长度不同而造成程序执行问题,OpenCV 根据数值变量存储位数长度定义了数据类型。如下表:
OpenCV 中的数据类型与取值范围
| 数据类型 | 具体类型 | 取值范围 |
|---|---|---|
| CV_8U | 8 位无符号整数 | 0~255 |
| CV_8S | 8 位符号整数 | -128~127 |
| CV_16U | 16 位无符号整数 | 0~65535 |
| CV_16S | 16 位符号整数 | -32768~32767 |
| CV_32S | 32 位符号整数 | -2147483648~2147483647 |
| CV_32F | 32 位浮点整数 | -FLT_MAX~FLT_MAX,INF,NAN |
| CV_64F | 64 位浮点整数 | -DBL_MAX~DBL_MAX,INF,NAN |
提示
这个表格先看着,因为目前来说用不到,但是对后面大有用处。
注意
仅有数据类型是不够的,还需要定义图像数据的通道 (Channel) 数,例如灰度图像数据是单通道数据,彩色图像数据是 3 通道或者 4 通道数据。因此,针对这个情况,OpenCV 还定义了通道数标识,C1、C2、C3、C4分别表示单通道、双通道、3 通道、4 通道。因为每一种数据类型都存在多个通道的情况,所以将数据类型与通道数表示结合便得到了 OpenCV 中对图像数据类型的完整定义,例如CV_8UC1 表示的是 8 位单通道数据,用于表示 8 位灰度图,而 CV_8UC3 表示的是 8 位 3 通道数据,用于表示 8 位彩色图。
我们可以通过如下方式创建一个声明通道数和数据类型的 Mat 类
代码 2-3 通过 OpenCV 数据类型创建 Mat 类
Mat a(640,480,CV_8UC3)//创建一个 640*480 的三通道 3 通道矩阵用于存放彩色图像
Mat a(3,3,CV_8UC1)//创建一个 3*3 的 8 位无符号整数的单通道矩阵
Mat a(3,3,CV_8U)//创建单通道矩阵,C1 标识可以省略
注意
虽然在 64 位编译器里,uchar 和 CV_8U 都表示 8 位无符号整数,但是两者有严格的定义,CV_8U 只能用在 Mat 类内部的方法。如果用
Mat_<CV_8U>(3,3)和Mat a(3,3,uchar),就会提示错误。
提示
根据 OpenCV 源码的定义,关于 Mat 类的构造方式有 20 多种,本节重点讲解常用的方式。
函数原型如下:
代码 2-4 默认构造函数使用方式
Mat::Mat();
方式一
函数原型如下:
代码 2-5 利用矩阵尺寸和类型参数构造 Mat 类
Mat::Mat(int rows,int cols,int type)
注意
这种方式常用在明确需要存储数据尺寸和数据存储类型的情况下,例如相继的内参矩阵、物体的旋转矩阵等
必须记住
rows 是 行
cols 是 列
这个 type 在后面会学到,下面的内容看看就行
此处,除 CV_8UC1、CV_64FC4 等从 1 到 4 通道以外,还提供了更多通道的参数,通过 CV_8UC(n) 中的 n 来构建多通道矩阵,其中 n 最大可以取到 512。
示例:
Mat a(400,600,CV_8UC1);
方式二
通过将行和列组成一个 Size() 结构进行赋值,构造方法的原型如下所示
代码 2-6 用 Size() 结构构造 Mat 类
Mat::Mat(Size size(),int type)
Size(cols,rows) 进行赋值。注意
使用 Size() 时,列在前 (cols)、行在后 (rows)。如果不注意,虽然同样会构造成功 Mat 类,但是当我们需要查看某一个元素时,我们并不知道行与列颠倒,就可能会出现数组越界的错误。
函数原型如下
代码清单 2-8 利用已有矩阵构造 Mat 类
//代码清单 2-8
Mat::Mat(const Mat & m);
这种构造方式非常简单,可以构造出与已有 Mat 类变量存储内容一样的变量。
注意
这种构造方式只是复制了 Mat 类的矩阵头,矩阵指针指向的是同一个地址,因此,如果通过某一个 Mat 类变量修改了矩阵中的数据,那么另一个变量中的数据也会发生变化。(浅拷贝)。
如果希望复制两个一模一样的 Mat 类而彼此之间不会受影响,那么可以使用
m=a.clone()实现 (深拷贝)
另一种情况
如果需要构造的矩阵尺寸比已有矩阵小,并且存储的是已有矩阵的子内容,那么可以用如下所示的方法构建。
函数原型如下
代码 2-9 构造已有 Mat 类的子类
Mat::Mat(const Mat & m,const Range & rowRange,const Range &colRange =Range::all());
Range(2,5)。Range(2,5),当不输入任何值时,表示所有列都会被截取。注意
这种方式主要用于在原图中截图使用。
通过这种方式构造的 Mat 类与已有 Mat 类享有共同的数据,即如果两个 Mat 类中有一个数据发生更改,那么另一个也会随之更改。
示例:
代码清单 2-10 在原 Mat 中截取子 Mat 类
Mat b(a,Range(2,5),Range(2,5));//从 a 中截取部分数据构造 b
Mat c(a,Range(2,5));//默认最后一个参数构造 c
构建完成 Mat 类后,变量里并没有数据,需要将数据赋值给它。针对不同情况,有不同赋值方式,下面介绍如何给 Mat 类变量赋值。
函数原型如下
代码清单 2-11 在构造时赋值的方法
Mat::Mat(int rows,int cols,int type,const Scalar & s);
Scalar(0,0,255)注意
用此方法会将图像中的每个元素赋予相同的数值,例如 Scalar(0,0,255) 会将每个像素的 3 个通道值分别赋为 0、0、255.
示例:
代码清单 2-12 在构造时赋值示例
Mat a(2,2,CV_8UC3,Scalar(0,0,255));//创建一个 3 通道矩阵,每个像素都是 0,0,255
Mat b(2,2,CV_8UC2,Scalar(0,255));//创建一个 2 通道矩阵,每个像素都是 0,255
Mat c(2,2,CV_8UC1,Scalar(255));//创建一个单通道矩阵,每个像素都是 255
我们可以在程序 return 语句之前加上断点进行调试,用Image Watch查看每一个 Mat 类变量里的数据。
使用 Scalar 结构给 Mat 类赋值结果
注意
Scalar 结构中变量的个数一定要与定义中的通道数相对应。如果Scalar 结构中变量的个数大于通道数,则位置在大于通道数之后的数值将不会被读取,例如执行
a(2,2,CV_8UC2,Scalar(0,0,255))后,每个像素值都将是 (0,0),而 255 不会被读取;如果 Scalar 结构中变量的个数小于通道数,则会以 0 补充。
警告
双通道矩阵不能用
imshow()来展示
这种赋值方式是将矩阵中所有的元素一一列举,并用数据流的形式赋值给 Mat 类 (也就是手动确定每个元素的值)。具体形式如下所示
示例:
代码清单 2-13 利用枚举法赋值示例
Mat a =(Mat_<int>(3,3)<<1,2,3,4,5,6,7,8,9);
Mat b =(Mat_<double>(2,3)<<1,0,2.1,3.2,4.0,5.1,6.2);
[ 1 2 3; 4 5 6; 7 8 9 ]注意
用枚举法时,输入的数据个数一定要与矩阵元素个数相同,例如,在代码清单 2-13 中第一行代码只输入 1~8 共 8 个数时,赋值过程会出现报错,因此本方法常用在矩阵数据比较少的情况下。
与通过枚举法赋值方法相似,循环法赋值也是对矩阵中的每一个元素进行赋值,但是可以不在声明变量的时候进行赋值,而且可以对矩阵中的任意部分进行赋值。具体赋值形式如下所示。
示例:
代码清单 2-14 利用循环法赋值示例
Mat c = Mat_<int>(3,3);//定义一个 3*3 的矩阵
for(int i=0; i< c.rows; i++)//矩阵行数循环
{
for(int j =0; j< c.cols; j++)//矩阵列数循环
{
c.at<int>(i,j)= i+j;//这里不知道什么意思
}
}
注意
上面代码创建了一个 3x3 的矩阵,通过for 循环的方式,对矩阵中的每一个元素进行赋值。需要注意的是,在给矩阵每个元素赋值的时候,赋值函数中声明的变量类型需要与矩阵定义时的变量类型相同,即代码清单 2-14 中第一行和第 6 行中变量类型相同,如果第 6 行代码改称
c.at<double>(i,j),程序就会报错,无法赋值。这里的
at<>()还没学到,看2.1.4
在 Mat 类里,提供了可以快速赋值的方法,可以初始化指定的矩阵。例如,生成单位矩阵、对角矩阵、所有元素都为 0 或者 1 的矩阵等。具体使用方法如下所示。
示例:
代码清单 2-15 利用类方法赋值示例
Mat a = Mat::eye(3,3,CV_8UC1);
Mat b =(Mat_<int>(1,3)<<1,2,3);
Mat c = Mat::diag(b);
Mat d = Mat::ones(3,3,CV_8UC1);
Mat e = Mat::zeros(4,2,CV_8UC3);
这种方法与枚举法类似,但是该方法可以根据需求改变 Mat 类矩阵的通道数,可以看作枚举法的拓展,这种方法的复制形式如下所示。
示例:
代码清单 2-16 利用数组进行赋值示例
float a[8]={5,6,7,8,1,2,3,4};
Mat b = Mat(2,2,CV_32FC2,a);
Mat c = Mat(2,4,CV_32FC1,a);
这种赋值方式首先将需要存入 Mat 类中的变量存入一个数组中,之后通过设置 Mat 类矩阵的尺寸和通道数将数组变量拆分成矩阵,这种拆分方式可以自由定义矩阵的通道数。
注意
当矩阵中的元素数目大于数组中的数据时,将用 -1.0737418e+08 填充赋值给矩阵;
当矩阵中元素的数目小于数组中的数据时,将矩阵赋值完成后,数组中剩余数据将不再赋值。
由数组赋值给矩阵的过程是首先将矩阵中的第一个元素的所有通道依次赋值,之后再赋值下一个元素。
为了更好地体会这个过程,我们将定义的 b 和 c 矩阵在下图中给出:
矩阵 b 和 c 中存储的数据
在处理数据时,需要对数据进行加减乘除运算,例如对图像进行滤波、增强等操作都需要对像素级别进行加减乘除运算。为了方便运算,Mat 类变量支持矩阵的加减乘除运算,即我们在使用 Mat 类变量时,将其看作普通的矩阵即可,例如 Mat 类变量与常数相乘遵循矩阵与常数相乘的运算法则。Mat 类与常数运算时,可以直接通过加减乘除符号实现。
提示
这一节了解一下 Mat 类支持加减乘除运算即可,目前无需深究。
Mat 类变量与常数进行加减乘除运算的示例如下:
代码清单 2-17 Mat 类的加减乘除运算
Mat a =(Mat_<int>(3,3)<<1,2,3,4,5,6,7,8,9);
Mat b =(Mat_<int>(3,3)<<1,2,3,4,5,6,7,8,9);
Mat c =(Mat_<double>(3,3)<<1.0,2.1,3.2,4.0,5.1,6.2,2,2);
Mat d =(Mat_<double>(3,3)<<1.0,2.1,3.2,4.0,5.1,6.2,2,2);
e=a+b;
f=c-d;
g=2*a;
h=d/2.0;
i=a-1;
注意
当两个 Mat 类变量进行加减运算时,必须保证两个矩阵中的数据类型是相同的,即两个分别保存 int 和 double 数据类型的 Mat 类变量不能进行加减运算。
与常规的乘除法不同之处在于,**常数 Mat 类变量运算结果的数据类型保留 Mat 类变量的数据类型。**例如,double 类型的常数与 int 类型的 Mat 类变量运算,最后结果仍然为 int 类型。
在代码清单 2-17的最后一行代码中,Mat 类变量减去一个常数,表示的含义是 Mat 类变量中的每一个元素都要减去这个常数。
在对图像进行卷积运算时,需要两个矩阵进行乘法运算,OpenCV 不但提供了两个 Mat 类矩阵的乘法运算,而且定义了两个矩阵的内积和对应位的乘法运算。
两个 Mat 类矩阵的乘法代码实现如下所示
代码清单 2-18 两个 Mat 矩阵的乘法运算
Mat j,m;
double k;
j=c*d;
k=a.dot(b);
m=a.mul(b);
代码清单 2-18 中矩阵定义和赋值与代码清单 2-17 中相同。在代码定义中定义了两个 Mat 类变量和一个 double 变量,分别实现了两个 Mat 类矩阵的乘法、内积和对应位乘法。第三行代码的**"*"运算符表示两个矩阵的数学乘积**。
例如存在两个矩阵 $A_{3\times3}$ 和 $B_{3\times3}$,"*"运算结果为矩阵 $B_{3\times3}$,$B_{3\times3}$ 中的每一个元素表示为: $c_{ij} = a_{i1}b_{1j} + a_{i2}b_{2j} + a_{i3}b_{3j}$
注意
"*"运算要求第一个 Mat 类矩阵的列数必须与第二个 Mat 类矩阵的行数相同
而且该运算要求 Mat 类中的数据类型必须是CV_32FC1、CV_64FC1、CV_32FC2、CV_64FC2这 4 种中的一种
也就是对于一个二维的 Mat 类矩阵,其保存的数据类型必须是 float 类型或者 double 类型
代码清单 2-18 中的第 4 行代码表示两个 Mat 类矩阵的内积。根据输出结果可以知道 dot() 方法结果是一个 double 类型的变量,该运算的目的是求取一个行向量和一个列向量点乘。
例如存在两个向量 $D = [d_1 \quad d_2 \quad d_3]$ 和 $E = [e_1 \quad e_2 \quad e_3]^T$,经过 dot() 方法运算的结果为: $f = d_1 e_1 + d_2 e_2 + d_3 e_3$
注意
输入的两个 Mat 类矩阵必须具有相同的元素数目
但是无论输入的两个 Mat 类矩阵的维数是多少,都会将两个 Mat 类矩阵扩展成一个行向量和一个列向量
因此**".dot"运算的结果永远是一个 double 类型的变量**。
代码清单 2-18 中的第 5 行代码表示两个 Mat 类矩阵对应位的乘积。根据输出结果可以知道mul() 方法运算结果同样是一个 Mat 类矩阵。
例如对于两个矩阵 $A_{3\times3}$ 和 $B_{3\times3}$,经过 mul() 方法运算的结果 $C_{3\times3}$ 中每一个元素都可以表示为: $c_{ij} = a_{ij} b_{ij}$
注意
不同于前两种乘法运算,参与 mul() 方法运算的两个 Mat 类矩阵中保存的数据在保证相同的前提下可以是任何一种类型,并且默认的输出数据类型与两个 Mat 类矩阵保持一致。
在图像处理领域,常用的数据类型是 CV_8U,其范围是 0~255,当两个比较大的整数相乘时,就会产生结果溢出的现象,输出结果为 255,因此,在使用 mul() 方法时,需要防止出现数据溢出的问题。
本小节将详细介绍如何读取 Mat 类矩阵中的元素,并对其数值进行修改。
首先需要知道 Mat 类变量在计算机中是如何存储的,看下面引用。
多通道的 Mat 类矩阵类似于三维数据,而计算机的存储空间是一个二维空间,因此 Mat 类矩阵在计算机中存储时是将三维数据变成二维数据,先存储第一个元素每个通道的数据,之后再存储第二个元素每个通道的数据。每一行的元素都按照这种方式进行存储,因此,如果我们找到了每个元素的起始位置,那么可以找到这个元素中每个通道的数据。下图展示了一个三通道矩阵的存储方式,其中连续的蓝色、绿色和红色方块分别代表每个元素的 3 个通道。
三通道 3×3 矩阵存储方式
在了解了 Mat 类变量的存储方式之后,我们来看Mat 类具有的属性。
提示
建议记住下表给出的 Mat 类矩阵常用属性及作用
Mat 类矩阵常用的属性
| 属性 | 作用 |
|---|---|
| cols | 矩阵的列数 |
| rows | 矩阵的行数 |
| step | 以字节为单位的矩阵的有效宽度 |
| elemSize1() | 单通道的字节数 (基本单位) |
| elemSize() | 每个元素 (所有通道) 的总字节数 |
| total() | 矩阵中元素的个数 |
| channels() | 矩阵的通道数 |
注意
这些属性之间互相组合可以得到多数 Mat 类矩阵的属性
例如step 属性与 cols 属性组合,可以求出每个元素所占据的字节数,再与 channels() 属性结合,就可以知道每个通道的字节数,进而知道矩阵中存储的数据量的类型。
下面通过一个例子具体说明每个属性的用处:用**Mat(3,4,CV_32FC3)**定义一个矩阵,这时通道数 channels() 为 3;列数 cols 为 4;行数 rows 为 3;32F 表示每个通道的数据类型是 32 位的单精度浮点数 (float)、C3 表示这是一个 3 通道矩阵。在图像处理中,这通常代表一个彩色像素 (BGR);矩阵中元素的个数为 3×4,结果为 12,下面是运算结果:
每个元素的字节数(1 字节=8 位) 为 32 / 8 × channels(),本例最后结果为 12(这里 32/8 是求字节数,乘以 channels() 就是一个元素的字节数);
以字节为单位的有效长度step 为elemSize()×cols,本例结果为 48。
常用的 Mat 类矩阵的元素读取方式包括通过 at 方法进行读取、通过指针 ptr 进行读取、通过迭代器进行读取、通过矩阵元素的地址定位方式进行读取。下面将详细介绍这 4 种读取方式。
通过 at 方法读取矩阵元素分为针对单通道的读取方法和针对多通道的读取方法
由于单通道图像是一个二维矩阵,因此在 at 方法的最后给出二维平面坐标即可访问对应位置元素。
示例如下
代码清单 2-19 at 方法读取 Mat 类单通道矩阵元素
Mat a =(Mat_<uchar>(3,3)<<1,2,3,4,5,6,7,8,9);
int value =(int)a.at<uchar>(0,0);
注意
通过 at 方法读取元素需要在后面跟上**"<数据类型>"**
如果此处的数据类型与矩阵定义时的数据类型不相同,就会出现因数据类型不匹配而产生的报错信息。
该方法以坐标的形式给出需要读取的元素坐标(行数,列数)。需要说明的是,如果矩阵定义的是 uchar 类型的数据,那么在需要输入数据的时候,需要强制转换成 int 类型的数据进行输出,否则输出的结果并不是整数。
多通道矩阵每一个元素坐标处都是多个数据,因此引入一个变量用于表示同一元素的多个数据。
注意
在 OpenCV 中,针对三通道矩阵,定义了 cv::Vec3b、cv::Vec3s、cv::Vec3w、cv::Vec3d、cv::Vec3f、cv::Vec3i 共 6 种类型用于表示同一个元素的 3 个通道数据。
通过这 6 种数据类型可以总结出其命名规则,其中的数字表示通道的个数,最后一位是数据类型的缩写,b 是 uchar 类型的缩写、s 是 short 类型的缩写、w 是 ushort 类型的缩写、d 是 double 类型的缩写、f 是 float 类型的缩写、i 是 int 类型的缩写。
当然,OpenCV 也为二通道和四通道定义了对应的变量类型,其命名方式也遵循这个命名规则
例如二通道和四通道的 uchar 类型分别用 cv::Vec2b 和 cv::Vec4b 表示。
通过 at 方法读取多通道矩阵的实现代码
示例如下
代码清单 2-20 at 方法读取 Mat 类多通道矩阵元素
Mat b(3,4, CV_8UC3,Scalar(0,0,1));
Vec3b vc3 = b.at<Vec3b>(0,0);
first =(int)vc3.val[0];
second =(int)vc3.val[1];
third =(int)vc3.val[2];
注意
在使用多通道变量类型时,同样需要注意 at 方法中数据变量类型与矩阵的数据变量类型相对应。
Vec3b 类型在输入每个通道数据时需要将其变量类型强制转换成 int 类型。**
如果将 at 方法读取出的数据直接赋值给 Vec3i 类型变量,就不需要在输出每个通道数据时进行数据类型的强制转换。
Mat 类矩阵在内存中的存放方式,矩阵中每一行中的每个元素都是挨着存放的。
如果找到每一行元素的起始地址位置,那么读取矩阵中每一行不同位置的元素时将指针在起始位置向后移动若干位即可。
代码清单 2-21 指针 ptr 读取 Mat 类矩阵元素
Mat b(3,4, CV_8UC3,Scalar(0,0,1));
for(int i =0; i < b.rows; i++)
{
uchar* ptr = b.ptr<uchar>(i);
for(int j =0; j < b.cols*b.channels(); j++)
{
cout <<(int)ptr[j]<< endl;
}
}
注意
在程序里,首先有一个大循环用来控制矩阵中每一行,之后定义一个 uchar 类型的指针 ptr,在定义时需要声明 Mat 类矩阵的变量类型,并在定义最后用小括号声明指针指向 Mat 类矩阵的哪一行。第二个循环控制用于输出矩阵中每一行所有通道的数据。
根据之前学习的存储形式,每一行中存储的数据数量为列数与通道数的乘积,即指针可以向后移动 cols×channels()-1 位 (列数 x 通道数 -1)
如第 7 行代码所示,指针向后移动的位数在中括号给出。程序中给出了循环遍历 Mat 类矩阵中的每一个数据的方法,当我们能够确定需要访问的数据时,可以直接通过给出行数和指针后移的位数进行访问
例如,当读取第 2 行数据中第 3 个数据时,可以用
a.ptr<uchar>(1)[2]的形式来直接访问。
Mat 类变量同时也是一个容器变量,因此,Mat 类变量拥有迭代器,用于访问 Mat 类变量中的数据,通过迭代器可以实现对矩阵中每一个元素的遍历。
示例如下
代码清单 2-22 指针 ptr 读取 Mat 类矩阵元素
MatIterator_<uchar> it = a.begin<uchar>();
MatIterator_<uchar> it_end = a.end<uchar>();
for(int i =0; it != it_end; it++)
{
cout <<(int)(*it)<< " ";
if((++i% a.xls)==0)
{
cout << endl;
}
}
注意
Mat 类的迭代器变量类型是
MatIterator_<>,在定义时同样需要在尖括号中声明数据的变量类型。Mat 类迭代器的起始是
Mat.begin<>(),结束是Mat.end<>(),与其他迭代器用法相同,通过'++'运算实现指针位置向下迭代,数据的读取方式是先读取第一个元素的每一个通道,之后再读取第二个元素的每一个通道,直到最后一个元素的最后一个通道。
前面 3 种读取元素的方式都需要知道 Mat 类矩阵存储数据的类型,而且在认知上,我们更希望能够通过声明'第 x 行第 x 列第 x 通道'的方式来读取某个通道内的数据,
代码清单 2-23 通过矩阵元素的地址定位方式访问元素
1.(int)(*(b.data + b.step[0]* row + b.step[1]* col + channel));
注意
代码中 row 变量的含义是某个数据所在元素的行数,col 变量的含义是某个数据所在元素的列数,channel 变量的含义是某个数据所在元素的通道数。
这种方式与我们通过指针读取数据的形式类似,都是通过将首个数据的地址指针移动若干位后指向需要读取的数据,只不过这种方式可以通过直接给出行、列和通道数进行读取,不需要用户再计算某个数据在这行数据存储空间中的位置。
图像的种类非常多,包括彩色图像、灰度图像、16 位深度图、32 位深度图等。本节中将详细介绍图像读取和显示的相关功能。
我们在前面已经介绍过了图像读取函数 imread() 的调用方式,这里我们给出函数的原型。
代码清单 2-24imread() 函数的原型
Mat cv::imread(const String & filename,int flags=IMREAD_COLOR );
注意
(1)
函数用于读取指定的图像并将其返回给一个 Mat 类变量,当图像文件不存在、破损或者格式不受支持时,则无法读取图像,此时函数返回一个空矩阵,因此可以通过返回矩阵的 data 属性是否为空或者 empty() 函数是否为真来判断是否成功读取图像,如果读取图像失败,那么 data 属性返回值为 0,empty() 函数返回值为 1。
(2)
函数能够读取多种格式的图像文件,但是,在不同操作系统中,由于使用的编解码器不同,因此在某个系统中能够读取的图像文件可能在其他系统中就无法读取。无论在哪个系统中,BMP 文件和 DIB 文件都是始终可以读取的。在 Windows 和 macOS 系统中,默认情况下使用 OpenCV 自带的编解码器(libjpeg、libpng、libtiff 和 libjasper),因此可以读取 JPEG(.jpg、.jpeg、.jpe)、PNG、TIFF(.tiff、.tif)文件,在 Linux 系统中,需要自行安装这些编解码器,安装后同样可以读取这些类型的文件。
(3)
该函数能否读取文件数据与扩展名无关,而是通过文件的内容确定图像的类型,例如,在将一个扩展名由 png 修改成.exe 时,该函数一样可以读取该图像,但是将扩展名.exe 改成 png,该函数不能加载该文件。
在 OpenCV 4.2 中给出了13 种模式读取图像的形式,总结起来分别是以原样式读取、灰度图读取、彩色图读取、多位数读取、在读取时将图像缩小一定尺寸等形式,具体可选择的参数及作用见下表。
这里需要指出的是,将彩色图像转成灰度图通过编解码器内部转换,可能会与 OpenCV 程序中将彩色图像转成灰度图的结果存在差异。这些标志参数在功能不冲突的前提下可以同时声明多个,不同参数之间用'|'隔开。
imread() 函数读取图像形式参数
| 标志参数 | 简记 | 作用 |
|---|---|---|
| IMREAD_UNCHANGED | -1 | 按照图像原样读取,保留 Alpha 通道 (第 4 通道) |
| IMREAD_GRAYSCALE | 0 | 将图像转成单通道灰度图像后读取 |
| IMREAD_COLOR | 1 | 将图像转换成 3 通道 BGR 彩色图像 |
| IMREAD_ANYDEPTH | 2 | 保留原图像的 16 位、32 位深度,不声明该参数则转成 8 位读取 |
| IMREAD_ANycolor | 4 | 以任何可能的颜色读取图像 |
| IMREAD_LOAD_GDAL | 8 | 使用 gdal 驱动程序加载图像 |
| IMREAD_REDUCED_GRAYSCALE_2 | 16 | 将图像转成单通道灰度图像,尺寸缩小 1/2。可以更改最后一位数字实现缩小 1/4(最后一位改为 4) 和 1/8(最后一位改为 8) |
| IMREAD_REDUCED_COLOR_2 | 17 | 将图像转成 3 通道彩色图像,尺寸缩小 1/2。可以更改最后一位数字实现缩小 1/4(最后一位改为 4) 和 1/8(最后一位改为 8) |
| IMREAD_IGNORE_ORIENTATION | 128 | 不以 EXIF 的方向旋转图像 |
注意
在默认情况下,读取图像的像素数目必须小于 2^30
这个要求在绝大多数图像处理领域是不受影响的,但是卫星遥感图像、超高分辨率图像的像素数目可能会超过这个阈值。可以通过修改系统变量中的OPENCV_IO_MAX_IMAGE_PIXELS参数调整能够读取的最大像素数目。
在我们之前的程序中并没有介绍过窗口函数,因为在显示图像时如果没有主动定义图像窗口,程序会自动生成一个窗口用于显示图像,然而有时需要在显示图像之前对图像窗口进行操作,例如添加滑动条,此时就需要提前创建图像窗口。
代码清单 2-25 namedWindow() 函数的原型
void cv::namedWindow(const String & winame,int flags = WINDOW_AUTOSIZE );
注意
该函数的第一个参数是声明窗口的名称,用于窗口的唯一识别。第二个参数是声明窗口的属性,主要用于设置窗口的大小是否可调、显示的图像是否填充满窗口等,具体可选择的参数及含义在下表中给出,默认情况下,函数加载的标志参数为**"WINDOW_AUTOSIZE | WINDOW_KEEPRATIO | WINDOW_GUI_EXPANDED"**。
该函数会创建一个窗口变量,用于显示图像和滑动条,通过窗口的名称引用该窗口,如果在创建窗口时已经存在具有相同名称的窗口,则该函数不会执行任何操作。
创建一个窗口需要占用部分内存资源,因此,通过该函数创建窗口后,在不需要窗口时需要关闭窗口来释放内存资源。OpenCV 提供了两个关闭窗口资源的函数,分别是
destroyWindow()函数和destroyAllWindows()。前一个函数是用于关闭一个指定名称的窗口,即在括号内输入窗口名称的字符串即可将对应窗口关闭;
后一个函数是关闭程序中所有的窗口,一般用于程序的最后。
不过事实上,在一个简单的程序里,我们并不需要调用这些函数,因为程序退出时会自动关闭应用程序的所有资源和窗口。虽然不主动释放窗口也会在程序结束时释放窗口资源,
namedWindow() 函数窗口属性标志参数
| 标志参数 | 简记 | 作用 |
|---|---|---|
| WINDOW_NORMAL | 0x00000000 | 显示图像后,允许用户随意调整窗口大小 |
| WINDOW_AUTOSIZE | 0x00000001 | 根据图像大小显示窗口,不允许用户调整大小 |
| WINDOW_OPENGL | 0x00001000 | 创建窗口的时候会支持 OpenGL |
| WINDOW_FULLSCREEN | 1 | 全屏显示窗口 |
| WINDOW_FREERATIO | 0x00000100 | 调整图像尺寸以充满窗口 |
| WINDOW_KEEPRATIO | 0x00000000 | 保持图像的比例 |
| WINDOW_GUI_EXPENDED | 0x00000000 | 创建的窗口允许添加工具栏和状态栏 |
| WINDOW_GUI_NORMAL | 0x00000010 | 创建没有状态栏和工具栏的窗口 |
我们在前面已经介绍过了图像显示函数 imshow() 的调用方式,这里我们给出函数的原型。
代码清单 2-26 imshow() 函数的原型
void cv::imshow(const String & winname, InputArray mat );
注意
(1)
该函数会在指定的窗口中显示图像。
如果在此函数之前没有创建同名的图像窗口,就会以 WINDOW_AUTOSIZE 标志创建一个窗口,显示图像的原始大小;
如果创建了图像窗口,那么会缩放图像以适应窗口属性。
该函数会根据图像的深度将其缩放,具体缩放规则如下:如果图像是 8 位无符号类型,那么按照原样显示。如果图像是 16 位无符号类型或者 32 位整数类型,那么会将像素除以 256,将范围由 $[0,255 \times 256]$ 映射到 $[0,255]$。如果图像是 32 位或 64 位浮点类型,那么将像素乘以 255,即将范围由 $[0,1]$ 映射到 $[0,255]$。
函数中第一个参数为图像显示窗口的名称,第二个参数是需要显示的图像 Mat 类矩阵。这里需要特殊说明的是,我们看到第二个参数并不是常见的 Mat 类,而是 InputArray,这个是 OpenCV 定义的一个类型声明引用,用作输入参数的标识,我们在遇到它时可以认为是需要输入一个 Mat 类数据。同样,OpenCV 对输出也定义了 OutputArray 类型,我们同样可以认为是输出一个 Mat 类数据。
(2)
此函数运行后会继续执行后面程序。如果后面程序执行完直接退出,那么显示的图像有可能闪一下就消失,因此在需要显示图像的程序中,往往会在 imshow() 函数后跟有 cv::waitKey() 函数,用于将程序暂停一段时间。waitKey() 函数是以毫秒计的等待时长,如果参数默认或者为'0',那么表示等待用户按键结束该函数。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online