2.1 openCv -- Mat

目标 我们有多种方式从现实世界中获取数字图像:数码相机、扫描仪、计算机断层扫描(CT)、磁共振成像(MRI)等。在所有情况下,我们(人类)看到的是图像。然而,当我们把这些转化为我们的数字设备时,我们记录的是图像每个点的数值。

例如,在上面的图中你可以看到汽车的后视镜其实就是一个矩阵,包含了所有像素点的强度值。我们获取和存储像素值的方式可能因我们的需求而异,但最终,在计算机世界中所有图像都可以简化为数值矩阵和其他描述矩阵的信息。OpenCV是一个计算机视觉库,其主要关注点是处理和操控这些信息。因此,你首先需要熟悉的是OpenCV如何存储和处理图像。

Mat OpenCV自2001年起就存在。在那些日子里,库是围绕C接口构建的,为了在内存中存储图像,他们使用了一个名为IplImage的C结构体。这是在大多数旧教程和教育材料中你会看到的。这种做法的问题是它带来了C语言的所有缺点。最大的问题是手动内存管理。它基于这样的假设,即用户负责内存的分配和释放。虽然这对小型程序不是问题,但是一旦你的代码库增长,处理这一切将比专注于解决你的开发目标更加困难。

幸运的是,C++出现了,并引入了类的概念,通过自动内存管理(或多或少)使用户的工作变得更轻松。好消息是C++与C完全兼容,所以从C转向C++不会产生任何兼容性问题。因此,OpenCV 2.0引入了一个新的C++接口,提供了一种新的做事方式,意味着你不必再为内存管理操心,使你的代码更加精炼(写得更少,做得更多)。C++接口的主要缺点是,目前许多嵌入式开发系统仅支持C。因此,除非你是针对嵌入式平台,否则使用旧的方法没有意义(除非你是一个自虐型程序员,自找麻烦)。

关于Mat,你需要知道的第一件事是你不再需要手动分配其内存并在不需要时释放它。尽管这样做仍然是可能的,但大多数OpenCV函数将自动分配其输出数据。作为一个不错的奖励,如果你传递一个已经存在的Mat对象,它已经分配了矩阵所需的存储空间,这个空间将会被重用。换句话说,我们始终只使用执行任务所需的确切内存。

Mat基本上是一个类,具有两部分数据:矩阵头(包含诸如矩阵大小、存储方法、矩阵存储地址等信息)和指向包含像素值的矩阵的指针(根据选择的存储方法,它可以具有任何维度)。矩阵头的大小是恒定的,然而矩阵本身的大小可能从一幅图像到另一幅图像不等,而且通常要大几个数量级。

OpenCV是一个图像处理库。它包含大量图像处理函数。为了解决计算挑战,大多数情况下,你将最终使用库中的多个函数。正因为如此,将图像传递给函数是一种常见做法。我们不应忘记,我们谈论的是图像处理算法,它们往往计算密集型。我们最不想做的就是通过不必要的复制潜在的大图像进一步降低程序的速度。

为了解决这个问题,OpenCV使用了一个引用计数系统。想法是每个Mat对象都有自己的头,然而一个矩阵可以在两个Mat对象之间共享,通过让它们的矩阵指针指向相同的地址。此外,复制运算符只会复制头和指向大型矩阵的指针,而不是数据本身。

Mat A, C; // 创建头部分 A = imread(argv[1], IMREAD_COLOR); // 这里我们会知道使用的方法(分配矩阵)

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); 现在修改F或G将不会影响A的头所指向的矩阵。你需要记住的是:

OpenCV函数的输出图像分配是自动的(除非另有说明)。 使用OpenCV的C++接口时,你不需要考虑内存管理。 赋值运算符和复制构造函数只复制头。 图像的底层矩阵可以使用cv::Mat::clone()和cv::Mat::copyTo()函数复制。

存储方法

这部分讨论的是如何存储像素值。你可以选择颜色空间和使用的数据类型。颜色空间指的是我们如何组合颜色成分来编码特定的颜色。最简单的颜色空间是灰度,其中可用的颜色是黑色和白色。这些颜色的组合允许我们创建多种灰色调。

对于丰富多彩的色彩表达,我们有更多的方法可以选择。每一种方法都将颜色分解为三到四个基本成分,我们可以利用这些成分的组合来创造其他颜色。最流行的一种是RGB(红绿蓝),主要是因为这也是我们的眼睛构建颜色的方式。其基本颜色是红色、绿色和蓝色。为了编码颜色的透明度,有时会添加第四个元素,即alpha(A)。

然而,还有许多其他颜色系统,每个系统都有自己的优势:

RGB是最常见的,因为我们的视觉系统使用类似的方式来感知颜色,但请记住,OpenCV的标准显示系统使用BGR颜色空间来组合颜色(红色和蓝色通道的位置被交换)。 HSV和HLS将颜色分解为色调(hue)、饱和度(saturation)和明度(value)/亮度(luminance)的成分,这是一种我们描述颜色更为自然的方式。例如,你可能会忽略最后一个成分,从而使你的算法对输入图像的光照条件不那么敏感。 YCrCb被流行的JPEG图像格式使用。 CIE Lab*是一个感知上均匀的颜色空间,如果你需要测量一种颜色与另一种颜色之间的距离,这会非常有用。

每个构成成分都有自己的有效范围。这引出了使用的数据类型。我们如何存储一个成分定义了我们对它的控制范围。可能的最小数据类型是char,意味着一个字节或8位。这可以是无符号的(可以存储0到255的值)或有符号的(值从-127到+127)。尽管在这种宽度下,对于三个成分(如RGB),已经可以表示1600万种可能的颜色,我们还可以通过为每个成分使用float(4字节=32位)或double(8字节=64位)数据类型获得更精细的控制。然而,请记住,增加一个成分的大小也会增加整个图像在内存中的尺寸。

在选择存储方法时,你应当考虑以下几个方面:

  1. 颜色空间选择:RGB是默认的选择,但如果应用涉及特定的色彩分析,比如皮肤检测或者光照不变的场景识别,HSV或HLS可能更有优势。

  2. 数据类型:根据图像处理的需求,选择合适的精度。例如,对于实时视频处理,8位的char类型可能就足够了;而对于需要高精度的科学成像,可能需要使用float或double类型。

  3. 存储效率:更高的数据精度意味着更大的存储需求。在处理大规模图像或视频流时,这可能成为瓶颈。因此,优化数据类型的选择和使用压缩技术可以提高效率。

  4. 兼容性和标准:在某些场合,如JPEG编码的图像,YCrCb颜色空间是必需的,因为这是JPEG标准所采用的。

  5. 算法需求:某些图像处理算法可能对特定颜色空间有偏好。例如,对于肤色检测,HSV空间中的饱和度和色调可能比RGB空间更容易区分。

总之,选择正确的颜色空间和数据类型是图像处理和计算机视觉项目成功的关键。这不仅影响图像的表示和处理效率,还可能直接影响到算法的性能和准确性

在“加载、修改和保存图像”教程中,你已经学会了如何使用cv::imwrite()函数将矩阵写入图像文件。然而,出于调试目的,直接查看实际的数值通常更加方便。你可以使用Mat类的<<运算符来实现这一点,但需要注意的是,这仅适用于二维矩阵。

虽然Mat作为图像容器表现得非常好,但它也是一个通用的矩阵类。因此,创建和操作多维矩阵也是可能的。你可以通过以下几种方式来创建一个Mat对象:

  1. 使用cv::Mat::Mat构造函数

    Cpp

    1Mat M(2,2, CV_8UC3, Scalar(0,0,255));
    2cout << "M = " << endl << " " << M << endl << endl;

    对于二维和多通道图像,我们首先定义其大小:行数和列数。

     

    然后,我们需要指定用于存储元素的数据类型和每个矩阵点的通道数。我们可以通过以下约定构造多个定义: CV_[每项的位数][有符号或无符号][类型前缀]C[通道数] 例如,CV_8UC3意味着我们使用8位长的无符号字符类型,每个像素有三个这样的类型以形成三个通道。

  2. 使用C/C++数组并通过构造函数初始化

    Cpp
    1int sz[3] = {2,2,2};
    2Mat L(3,sz, CV_8UC(1), Scalar::all(0));

    上面的例子展示了如何创建多于两个维度的矩阵。指定其维度,然后传递一个包含每个维度大小的指针,其余保持相同。

  3. 使用cv::Mat::create函数

    Cpp
    1M.create(4,4, CV_8UC(2));
    2cout << "M = "<< endl << " "  << M << endl << endl;

    你不能用这种方式初始化矩阵值。如果新大小不适合旧的矩阵,则它只会重新分配矩阵数据内存。

  4. MATLAB风格初始化器: cv::Mat::zeros, cv::Mat::ones, cv::Mat::eye

    Cpp
    
    1Mat E = Mat::eye(4, 4, CV_64F);
    2Mat O = Mat::ones(2, 2, CV_32F);
    3Mat Z = Mat::zeros(3,3, CV_8UC1);
  5. 小矩阵的逗号分隔初始化器或初始化列表

     Cpp 
    1Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
  6. 为现有Mat对象创建新的头部并克隆或使用cv::Mat::clonecv::Mat::copyTo

     Cpp 
    1Mat RowClone = C.row(1).clone();

注释:你可以使用cv::randu()函数填充矩阵的随机值。你需要给出随机值的下限和上限。

输出格式化 在上述示例中,你看到了默认的格式选项。但是,OpenCV允许你格式化矩阵输出:

  • 默认格式
  • Python风格
  • 逗号分隔值(CSV)
  • Numpy风格
  • C风格

输出其他常见项目 OpenCV也通过<<运算符提供了对其他常见OpenCV数据结构的输出支持:

  • 二维点
  • 三维点
  • 通过cv::Matstd::vector
  • std::vector中的点

以上大部分示例都包含在一个小型控制台应用程序中,你可以从这里下载,或在cpp样本的核心部分找到。这些示例展示了Mat类以及相关函数的使用方法,帮助你更好地理解和操作图像和矩阵数据。

  • 16
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OpenCv学堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值