前言
cv::Mat
是 OpenCV 中最常用的数据结构之一,本文主要介绍 cv::Mat
的几种传参方式。
在开始之前要说明一下 cv::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标头指向的矩阵。从这一切中你需要记住的是:
- OpenCV 函数的输出图像分配是自动的(除非另有说明)。
- 使用 OpenCV 的 C++ 接口,您无需考虑内存管理。
- 赋值运算符和复制构造函数仅复制标头。
- 可以使用cv::Mat::clone()和cv::Mat::copyTo()函数复制图像的基础矩阵。
实验过程
说了上面一堆,我们了解到在复制 矩阵头 时,矩阵本身并没有被复制,只是指针指向了同一个地址。
测试代码如下:
#include <iostream>
#include <opencv2/opencv.hpp>
/// @brief 值传递
/// @param InMat
void fun_1(cv::Mat InMat)
{
std::cout << "InMat head pointer: " << &InMat << std::endl;
std::cout << "InMat.data before: " << (void*)InMat.data << std::endl;
InMat.at<uchar>(0, 0) = 100;
std::cout << "InMat.data after: " << (void*)InMat.data << std::endl;
}
/// @brief 引用传递
/// @param InMat
void fun_2(cv::Mat& InMat)
{
std::cout << "InMat head pointer: " << &InMat << std::endl;
std::cout << "InMat.data before: " << (void*)InMat.data << std::endl;
InMat.at<uchar>(0, 0) = 100;
std::cout << "InMat.data after: " << (void*)InMat.data << std::endl;
}
int main(int argc, char* argv[])
{
cv::Mat orig_img = cv::Mat::ones(3, 3, CV_8UC1);
std::cout << "orig_img head pointer: " << &orig_img << std::endl;
std::cout << "orig_img.data: " << (void*)orig_img.data << std::endl;
fun_1(orig_img);
fun_2(orig_img);
return 0;
}
运行结果如下:
orig_img head pointer: 0x16b107040
orig_img.data: 0x12f72dec0
# fun_1
InMat head pointer: 0x16b106e70
InMat.data before: 0x12f72dec0
InMat.data after: 0x12f72dec0
# fun_2
InMat head pointer: 0x16b107040
InMat.data before: 0x12f72dec0
InMat.data after: 0x12f72dec0
从结果可以看出,InMat
的头指针在 fun_1
和 fun_2
中的地址是不同的,但是 InMat.data
的地址是相同的,说明在值传递的情况下,InMat
的头指针是被复制了的,但是由于 引用计数系统 的特性,InMat.data
部分是共享的。
Opencv 的初学者可能会看到一篇讲 cv::Mat
、cv::Mat&
、const cv::Mat
、const cv::Mat&
传参方式的文章,那篇文章纯属误人子弟。从上面的 cv::Mat
、cv::Mat&
的实验结果可以看到,由于矩阵的共享,在函数内部修改 InMat
的值,都会影响到原始的 orig_img
。
至于 const cv::Mat
、const cv::Mat&
的传参方式呢?在 const 的修饰下,当尝试修改 InMat
的值时,编译器会报错:
关于Mat的几种传递方式的区别.cpp:10:27: error: cannot assign to return value because function 'at<unsigned char>' returns a const value
InMat.at<uchar>(0, 0) = 100;
~~~~~~~~~~~~~~~~~~~~~ ^
/opt/opencv/include/opencv4/opencv2/core/mat.inl.hpp:905:7: note: function 'at<unsigned char>' which returns const-qualified type 'const unsigned char &' declared here
const _Tp& Mat::at(int i0, int i1) const
^~~~
总结
事实证明,实践是检验真理的唯一标准。
作为开发者要脚踏实地,不要被一些文章误导,要多动手实践,多思考,多总结。