使用 OpenCV4 和 C++ 构建计算机视觉项目:1~5

原文:Building Computer Vision Projects with OpenCV 4 and C++

协议:CC BY-NC-SA 4.0

译者:飞龙

本文来自【ApacheCN 计算机视觉 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。

当别人说你没有底线的时候,你最好真的没有;当别人说你做过某些事的时候,你也最好真的做过。

一、OpenCV 入门

计算机视觉应用是有趣和有用的,但是底层算法是计算密集型的。 随着云计算的到来,我们可以使用更多的处理能力。

OpenCV 库使我们能够实时高效地运行计算机视觉算法。 它已经有很多年的历史了,并且已经成为该领域的标准库。 OpenCV 的主要优势之一是它高度优化,几乎可以在所有平台上使用。

这本书将涵盖我们将使用的各种算法,我们为什么要使用它们,以及如何在 OpenCV 中实现它们。

在本章中,我们将学习如何在各种操作系统上安装 OpenCV。 我们将讨论 OpenCV 提供的开箱即用功能,以及使用内置函数可以做的各种事情。

在本章结束时,您将能够回答以下问题:

  • 人类是如何处理视觉数据的,他们又是如何理解图像内容的呢?
  • 我们可以使用 OpenCV 做什么,OpenCV 中有哪些模块可以用来实现这些功能?
  • 如何在 Windows、Linux 和 Mac OS X 上安装 OpenCV?

了解人类的视觉系统

在我们进入 OpenCV 功能之前,我们首先需要了解为什么要构建这些功能。 重要的是要了解人类视觉系统是如何工作的,这样你才能开发出正确的算法。

计算机视觉算法的目标是理解图像和视频的内容。 人类似乎可以毫不费力地做到这一点! 那么,我们如何让机器以同样的精确度来做这件事呢?

让我们考虑下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iJevciLJ-1681961622324)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/17fb5e37-8cf5-48e5-82f9-830f0a0002c2.png)]

人眼可以捕捉到所有的信息,比如颜色、形状、亮度等等。 在上图中,人眼捕捉到关于这两个主要物体的所有信息,并以某种方式进行存储。 一旦我们了解了我们的系统是如何工作的,我们就可以利用它来实现我们想要的。

例如,以下是我们需要知道的几件事:

  • 我们的视觉系统对低频内容比高频内容更敏感。 低频内容是指像素值变化不快的平面区域,高频内容是指像素值波动较大的角和边区域。 我们可以很容易地看到平面上是否有斑点,但在高度纹理的表面上很难发现这样的东西。

  • 人眼对亮度的变化比对颜色的变化更敏感。

  • 我们的视觉系统对运动很敏感。 我们可以很快识别出是否有什么东西在我们的视野中移动,即使我们没有直接看着它。

  • 我们倾向于在脑海中记下我们视野中的要点。 假设你看到一张白色的桌子,它有四条黑色的腿,桌子表面的一角有一个红点。 当你看着这张桌子时,你会立即在脑海中注意到表面和腿的颜色是相反的,其中一个角上有一个红点。 我们的大脑在这方面真的很聪明! 我们会自动这样做,这样如果我们再次遇到一个物体,我们就可以立即认出它。

为了了解我们的视野,让我们看看人类的俯视图,以及我们观察各种事物的角度:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AnSDrA5N-1681961622325)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/7942da39-b1e4-4cfb-b12e-99dfe3089376.png)]

我们的视觉系统实际上有更多的能力,但这应该足以让我们开始。 您可以通过在网络上阅读人类视觉系统(HVS)模型来进一步探索。

人类如何理解图像内容?

如果你环顾四周,你会看到很多东西。 你每天都会遇到许多不同的物体,你几乎可以毫不费力地在瞬间认出它们。 当你看到一把椅子时,你不用等几分钟就会意识到它实际上是一把椅子。 你只要知道它马上就是一把椅子。

另一方面,计算机发现很难完成这项任务。 研究人员多年来一直在努力找出为什么计算机在这方面不如我们。

为了得到这个问题的答案,我们需要了解人类是如何做到这一点的。 视觉数据处理发生在腹侧视流中。 这种腹侧视觉流指的是我们视觉系统中与物体识别相关的路径。 它基本上是我们大脑中帮助我们识别物体的区域的层次结构。

人类可以毫不费力地识别不同的物体,并可以将相似的物体聚集在一起。 我们之所以能做到这一点,是因为我们已经开发出对同一类对象的某种不变性。 当我们看着一个物体时,我们的大脑以这样一种方式提取突出点,即方向、大小、视角和照明等因素都无关紧要。

一把椅子的大小是正常大小的两倍,并且旋转了 45 度,它仍然是一把椅子。 我们可以很容易地识别它,因为我们处理它的方式。 机器不能这么容易做到这一点。 人类往往会根据物体的形状和重要特征来记住它。 无论物体是如何放置的,我们仍然可以识别它。

在我们的视觉系统中,我们建立了关于位置、比例和视点的层次不变性,这有助于我们变得非常健壮。 如果你更深入地观察我们的系统,你会发现人类的视觉皮层中有能对曲线和线条等形状做出反应的细胞。

当我们沿着腹侧流走得更远时,我们会看到更复杂的细胞,它们被训练成对更复杂的物体(如树木、大门等)做出反应。 沿着腹侧流的神经元倾向于显示出感受场的大小增加。 再加上他们喜欢的刺激的复杂性也增加了。

为什么机器很难理解图像内容?

我们现在了解了视觉数据是如何进入人类视觉系统的,以及我们的系统是如何处理它的。 问题是我们仍然不能完全理解我们的大脑是如何识别和组织这些视觉数据的。 在机器学习中,我们只是从图像中提取一些特征,然后要求计算机使用算法来学习它们。 我们仍然有这些变化,例如形状、大小、透视、角度、照明、遮挡等等。

例如,当您从纵断面图查看时,同一张椅子在机器看来非常不同。 人们可以很容易地认出它是一把椅子,不管它是如何呈现给我们的。 那么,我们该如何向我们的机器解释这一点呢?

要做到这一点,一种方法是存储对象的所有不同变化,包括大小、角度、透视等。 但这一过程既繁琐又耗时。 而且,实际上不可能收集到涵盖每一种变异的数据。 这些机器将消耗大量的内存和大量的时间来建立一个能够识别这些物体的模型。

即便如此,如果一个物体被部分遮挡,计算机仍然无法识别它。 这是因为他们认为这是一个新的物体。 因此,当我们构建计算机视觉库时,我们需要构建底层功能块,这些功能块可以以多种不同的方式组合在一起,以形成复杂的算法。

OpenCV 提供了很多这样的功能,而且它们都经过了高度优化。 因此,一旦我们了解了 OpenCV 的能力,我们就可以有效地利用它来构建有趣的应用。

让我们在下一节继续探索这一点。

您可以使用 OpenCV 做什么?

使用 OpenCV,你几乎可以完成你能想到的每一项计算机视觉任务。 现实生活中的问题需要您一起使用许多计算机视觉算法和模块才能达到预期的结果。 因此,您只需要了解要使用哪些 OpenCV 模块和函数,就可以获得您想要的东西。

让我们看看 OpenCV 可以实现哪些开箱即用的功能。

内置数据结构和输入/输出

OpenCV 最好的一点是它提供了大量内置原语来处理与图像处理和计算机视觉相关的操作。 如果必须从头开始编写内容,则必须定义ImagePointRectangle等。 这些都是几乎所有计算机视觉算法的基础。

OpenCV 附带了所有这些开箱即用的基本结构,包含在核心模块中。 另一个优点是这些结构已经针对速度和内存进行了优化,因此您不必担心实现细节。

imgcodecs模块处理图像文件的读取和写入。 当您操作输入图像并创建输出图像时,可以用一个简单的命令将其保存为.jpg.png文件。

当您使用摄像机工作时,您将处理大量视频文件。 videoio模块处理与视频文件的输入和输出相关的一切。 您可以轻松地从网络摄像头捕获视频或读取多种不同格式的视频文件。 您甚至可以通过设置属性(如每秒帧数、帧大小等)将一串帧另存为视频文件。

图像处理操作

在编写计算机视觉算法时,有很多基本的图像处理操作需要反复使用。 这些功能中的大多数都存在于imgproc模块中。 您可以执行图像过滤、形态操作、几何变换、颜色转换、在图像上绘图、直方图、形状分析、运动分析、特征检测等操作。

让我们来看一下下面的照片:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UosD055M-1681961622326)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/b676eeeb-8436-4b28-a840-719c1c1734f4.png)]

右图是左图的旋转版本。 在 OpenCV 中,我们只需一行即可执行此转换。

还有另一个模块,称为ximgproc,它包含用于边缘检测的结构森林、域变换滤波器、自适应流形滤波器等高级图像处理算法。

图形用户界面

OpenCV 提供了一个名为highgui的模块,用于处理所有高级用户界面操作。 假设您正在处理一个问题,并且希望在继续下一步之前检查图像的外观。 此模块具有可用于创建窗口以显示图像和/或视频的功能。

有一个等待功能,它会等到你按下键盘上的一个键,然后它才会进入下一步。 还有一个可以检测鼠标事件的功能。 这在开发交互式应用时非常有用。

使用此功能,您可以在这些输入窗口上绘制矩形,然后根据所选区域继续。 请考虑以下屏幕截图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d7O6yNwl-1681961622326)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/4b1a123e-8867-494b-ab3f-b28c501ac940.png)]

如您所见,我们在窗口顶部绘制了一个绿色矩形。 一旦我们有了那个矩形的坐标,我们就只能在那个区域上行动了。

视频分析

视频分析包括分析视频中连续帧之间的运动、跟踪视频中的不同对象、创建视频监控模型等任务。 OpenCV 提供了一个名为video的模块,可以处理所有这些问题。

还有一个名为videostab的模块,用于处理视频稳定。 视频稳定很重要,因为当你用手持相机拍摄视频时,通常会有很多抖动需要纠正。 所有现代设备在将视频呈现给最终用户之前都会使用视频稳定器对视频进行处理。

三维重建

三维重建是计算机视觉中的一个重要课题。 在给定一组 2D 图像的情况下,我们可以使用相关算法重建 3D 场景。 OpenCV 提供了一些算法,可以找到这些 2D 图像中各种对象之间的关系,从而在其calib3d模块中计算它们的 3D 位置。

该模块还可以处理摄像机校准,这是估计摄像机参数所必需的。 这些参数定义摄影机如何看到它前面的场景。 我们需要知道这些参数来设计算法,否则我们可能会得到意想不到的结果。

让我们考虑下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4jCHkNSH-1681961622327)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/e5b39e1a-aa0f-4a86-965d-93796daa524c.png)]

正如我们在这里看到的,同一对象是从多个位置捕获的。 我们的工作是使用这些 2D 图像重建原始对象。

特征提取

正如我们前面讨论的,人类视觉系统倾向于从给定的场景中提取显著特征,以便记住它以备以后检索。 为了模仿这一点,人们开始设计各种特征提取器,可以从给定的图像中提取这些显著点。 流行的算法包括尺度不变特征变换(SIFT)、加速鲁棒特征(SURF)和F****EATURES from Accelerated Segment Test(FAST)。

名为features2d的 OpenCV 模块提供了检测和提取所有这些特征的功能。 另一个名为xfeatures2d的模块提供了更多的特征提取器,其中一些仍处于实验阶段。 如果你有机会,你可以玩这些东西。

还有一个名为bioinspired的模块,它为生物启发的计算机视觉模型提供算法。

目标检测

目标检测是指检测目标在给定图像中的位置。 此过程与对象的类型无关。 如果你设计了一个椅子探测器,它不会告诉你一张给定图像中的椅子是红色的高靠背还是蓝色的低靠背-它只会告诉你椅子的位置。

目标位置检测是许多计算机视觉系统中的关键步骤。 请看下面的照片:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eOkd155i-1681961622327)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/a556000b-9c67-4577-9e43-07eaacaf258d.png)]

如果你在这张图片上运行椅子探测器,它会在所有的椅子周围放一个绿色的方框-但它不会告诉你这是哪种椅子。

对象检测过去是一项计算密集型任务,因为执行各种尺度的检测所需的计算量很大。 为了解决这个问题,Paul Viola 和 Michael Jones 在他们 2001 年的开创性论文中提出了一个很棒的算法,您可以通过以下链接阅读:https://www.cs.cmu.edu/~efros/courses/LBMV07/Papers/viola-cvpr-01.pdf。 它们提供了一种为任何对象设计对象检测器的快速方法。

OpenCV 有称为objdetectxobjdetect的模块,它们提供了设计对象检测器的框架。 您可以使用它来开发用于太阳镜、靴子等随机物品的检测器。

机器学习

机器学习算法被广泛用于构建计算机视觉系统,用于目标识别、图像分类、人脸检测、视觉搜索等。

OpenCV 提供了一个名为ml的模块,其中捆绑了许多机器学习算法,包括贝叶斯分类器k 近邻(KNN)、支持向量机(SVM)、决策树神经网络等等。

它还有一个名为快速近似最近邻搜索库(FLAN)的模块,其中包含在大数据集中进行快速最近邻搜索的算法。

计算摄影

计算摄影是指使用先进的图像处理技术来改善相机捕获的图像。 计算摄影使用软件来处理视觉数据,而不是关注光学过程和图像捕捉方法。 应用包括高动态范围成像、全景图像、图像重新照明和光场相机。

让我们看下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KfBSifVY-1681961622328)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/0f552b9a-c61e-47d0-bd5b-7a3e7379742a.png)]

看看那些鲜艳的颜色! 这是一个高动态范围图像的例子,使用传统的图像捕捉技术是不可能获得的。 要做到这一点,我们必须在多次曝光时捕捉相同的场景,将这些图像彼此配准,然后将它们很好地混合在一起,以创建这张图像。

photoxphoto模块包含提供与计算摄影有关的算法的各种算法。 还有一个名为stitching的模块,它提供了创建全景图像的算法。

The image shown can be found here: https://pixabay.com/en/hdr-high-dynamic-range-landscape-806260/.

形状分析

形状的概念在计算机视觉中至关重要。 我们通过识别图像中各种不同的形状来分析视觉数据。 这实际上是许多算法中的重要一步。

假设您正在尝试识别图像中的特定徽标。 您知道它可以以各种形状、方向和大小出现。 开始的一个好方法是量化物体形状的特征。

shape模块提供提取不同形状、测量它们之间的相似性、变换对象形状等所需的所有算法。

光流算法

在视频中使用光流算法来跟踪连续帧上的特征。 假设您想要跟踪视频中的特定对象。 在每个帧上运行特征提取器的计算代价会很高;因此,该过程会很慢。 因此,您只需从当前帧中提取特征,然后在连续的帧中跟踪这些特征。

光流算法广泛应用于计算机视觉中基于视频的应用中。 optflow模块包含执行光流所需的所有算法。 还有一个名为tracking的模块,它包含更多可用于跟踪功能的算法。

人脸和物体识别

人脸识别是指识别给定图像中的人。 这与人脸检测不同,在人脸检测中,您只需识别给定图像中人脸的位置。

如果你想构建一个实用的生物识别系统,能够识别摄像头前的人,你首先需要运行人脸检测器来识别人脸的位置,然后运行单独的人脸识别器来识别这个人是谁。 有一个名为face的 OpenCV 模块处理人脸识别。

正如我们前面讨论的,计算机视觉试图根据人类感知视觉数据的方式对算法进行建模。 因此,在图像中发现显著区域和目标将有助于不同的应用,如目标识别、目标检测和跟踪等。 有一个名为saliency的模块就是为此目的而设计的。 它提供了可以检测静态图像和视频中显著区域的算法。

曲面匹配

我们越来越多地与能够捕捉我们周围物体的 3D 结构的设备互动。 这些设备主要捕捉深度信息,以及常规的 2D 彩色图像。 因此,构建能够理解和处理 3D 对象的算法对我们来说非常重要。

Kinect就是捕获深度信息和视觉数据的设备的一个很好的例子。 手头的任务是通过将输入的 3D 对象与我们数据库中的一个模型进行匹配来识别输入的 3D 对象。 如果我们有一个可以识别和定位物体的系统,那么它可以用于许多不同的应用。

有一个名为surface_matching的模块,它包含 3D 对象识别算法和使用 3D 特征的姿势估计算法。

文本检测与识别

识别特定场景中的文本和识别内容变得越来越重要。 应用包括车牌识别、自动驾驶汽车的路标识别、书籍扫描以数字化内容等。

有一个名为text的模块,它包含处理文本检测和识别的各种算法。

深度学习

深度学习对计算机视觉和图像识别的影响很大,比其他机器学习和人工智能算法具有更高的准确率。 深度学习并不是一个新概念;它在 1986 年左右被引入社区,但它在 2012 年左右开始了一场革命,当时新的 GPU 硬件针对并行计算进行了优化,卷积神经网络(CNN)实现和其他技术允许在合理的时间内训练复杂的神经网络结构。

深度学习可以应用于多个用例,例如图像识别、对象检测、语音识别和自然语言处理。 从 3.4 版本开始,OpenCV 就一直在实现深度学习算法–在最新版本中,为TensorFlowCaffe等重要框架添加了多个导入器。

安装 OpenCV

让我们看看如何在各种操作系统上启动和运行 OpenCV。

Windows 操作系统

为简单起见,让我们使用预构建库安装 OpenCV。 转到opencv.org并下载 Windows 的最新版本。 目前的版本是 4.0.0,您可以从 OpenCV 主页获取下载链接。 在继续之前,您应该确保您拥有管理员权限。

下载的文件将是可执行文件,因此只需双击它即可开始安装。 安装程序将内容展开到文件夹中。 您将能够选择安装路径,并通过检查文件来检查安装。

完成上一步后,我们需要设置 OpenCV 环境变量并将它们添加到系统路径以完成安装。 我们将设置一个环境变量,该变量将保存 OpenCV 库的构建目录。 我们将在我们的项目中使用这一点。

打开终端并键入以下内容:

C:> setx -m OPENCV_DIR D:OpenCVBuildx64vc14

We are assuming that you have a 64-bit machine with Visual Studio 2015 installed. If you have Visual Studio 2012, replace vc14 with vc11 in the command. The path specified is where we would have our OpenCV binaries, and you should see two folders inside that path called lib and bin. If you are using Visual Studio 2018, you should compile OpenCV from scratch.

让我们继续并将bin文件夹的路径添加到我们的系统路径。 之所以需要这样做,是因为我们将使用动态链接库(DLLs)形式的 OpenCV 库。 实际上,所有的 OpenCV 算法都存储在这里,我们的操作系统只会在运行时加载它们。

为了做到这一点,我们的操作系统需要知道它们的位置。 PATH系统变量包含它可以在其中找到 DLL 的所有文件夹的列表。 因此,我们自然需要将 OpenCV 库的路径添加到此列表中。

我们为什么要做这一切呢? 好的,另一种选择是将所需的 DLL 复制到与应用的可执行文件(.exe文件)相同的文件夹中。 这是一个不必要的开销,特别是当我们在处理许多不同的项目时。

我们需要编辑PATH变量来添加此文件夹。 您可以使用路径编辑器等软件来执行此操作,您可以从此处下载:https://patheditor2.codeplex.com。 安装后,启动它并添加以下新条目(您可以右键单击路径以插入新项目):

%OPENCV_DIR%bin

继续并将其保存到注册表。 我们完了!

Mac OS X Mac OS X

在本节中,我们将了解如何在 Mac OS X 上安装 OpenCV。预编译的二进制文件在 Mac OS X 上不可用,因此我们需要从头开始编译 OpenCV。

在继续之前,我们需要安装 CMake。 如果您还没有安装 CMake,可以从这里下载:https://cmake.org/files/v3.12/cmake-3.12.0-rc1-Darwin-x86_64.dmg。 这是一个.dmg文件,所以下载后,只需运行安装程序即可。

opencv.org下载最新版本的 OpenCV。 当前版本是 4.0.0,您可以从这里下载:https://github.com/opencv/opencv/archive/4.0.0.zip。 将内容解压缩到您选择的文件夹中。

OpenCV 4.0.0 还有一个名为opencv_contrib的新包,其中包含尚未被认为是稳定的用户贡献,以及在所有最新的计算机视觉算法中不能免费用于商业用途的一些算法,这一点值得记住。 安装此软件包是可选的-如果您不安装opencv_contrib,OpenCV 将工作得很好。

因为我们无论如何都要安装 OpenCV,所以最好安装这个软件包,这样您以后就可以试用它了(而不是再次经历整个安装过程)。 这是学习和使用新算法的好方法。 您可以从以下链接下载:https://github.com/opencv/opencv_contrib/archive/4.0.0.zip

将 zip 文件的内容解压缩到您选择的文件夹中。 为方便起见,请将其解压缩到与前面相同的文件夹中,以便opencv-4.0.0opencv_contrib-4.0.0文件夹位于同一主文件夹中。

我们现在就可以构建 OpenCV 了。 打开终端并导航到解压 OpenCV 4.0.0 内容的文件夹。 在命令中替换正确路径后运行以下命令:

$ cd /full/path/to/opencv-4.0.0/ 
$ mkdir build 
$ cd build 
$ cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/full/path/to/opencv-4.0.0/build -D INSTALL_C_EXAMPLES=ON -D BUILD_EXAMPLES=ON -D OPENCV_EXTRA_MODULES_PATH=/full/path/to/opencv_contrib-4.0.0/modules ../ 

现在是安装 OpenCV 4.0.0 的时候了。 转到/full/path/to/opencv-4.0.0/build目录,并在您的终端上运行以下命令:

$ make -j4 
$ make install 

在前面的命令中,**-j4**标志指示它应该使用四个内核来安装它。 这样会更快! 现在,让我们设置库路径。 使用vi ~/.profile命令在您的终端中打开您的~/.profile文件,并添加以下行:

export DYLD_LIBRARY_PATH=/full/path/to/opencv-4.0.0/build/lib:$DYLD_LIBRARY_PATH

我们需要将opencv.pc中的pkgconfig文件复制到/usr/local/lib/pkgconfig,并将其命名为opencv4.pc。 这样,如果您已经安装了 OpenCV 3.x.x,则不会发生冲突。 让我们继续这样做:

$ cp /full/path/to/opencv-4.0.0/build/lib/pkgconfig/opencv.pc /usr/local/lib/pkgconfig/opencv4.pc 

我们还需要更新我们的PKG_CONFIG_PATH变量。 打开您的~/.profile文件并添加以下行:

export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/:$PKG_CONFIG_PATH 

使用以下命令重新加载~/.profile文件:

$ source ~/.profile 

我们完了! 让我们看看它是不是起作用了:

$ cd /full/path/to/opencv-4.0.0/samples/cpp 
$ g++ -ggdb `pkg-config --cflags --libs opencv4` opencv_version.cpp -o /tmp/opencv_version && /tmp/opencv_version

如果您的终端上显示了欢迎使用 OpenCV 4.0.0,您就可以开始使用了。 在本书中,我们将使用 CMake 来构建我们的 OpenCV 项目。 我们将在第 2 章OpenCV 基础简介中更详细地介绍它。

Linux 操作系

让我们来看看如何在 Ubuntu 上安装 OpenCV。 在开始之前,我们需要安装一些依赖项。 让我们通过在您的终端中运行以下命令,使用包管理器安装它们:

$ sudo apt-get -y install libopencv-dev build-essential cmake libdc1394-22 libdc1394-22-dev libjpeg-dev libpng12-dev libtiff5-dev libjasper-dev libavcodec-dev libavformat-dev libswscale-dev libxine2-dev libgstreamer0.10-dev libgstreamer-plugins-base0.10-dev libv4l-dev libtbb-dev libqt4-dev libmp3lame-dev libopencore-amrnb-dev libopencore-amrwb-dev libtheora-dev libvorbis-dev libxvidcore-dev x264 v4l-utils 

现在您已经安装了依赖项,让我们下载、构建并安装 OpenCV:

$ wget "https://github.com/opencv/opencv/archive/4.0.0.tar.gz" -O opencv.tar.gz 
$ wget "https://github.com/opencv/opencv_contrib/archive/4.0.0.tar.gz" -O opencv_contrib.tar.gz 
$ tar -zxvf opencv.tar.gz 
$ tar -zxvf opencv_contrib.tar.gz 
$ cd opencv-4.0.0 
$ mkdir build 
$ cd build 
$ cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/full/path/to/opencv-4.0.0/build -D INSTALL_C_EXAMPLES=ON -D BUILD_EXAMPLES=ON -D OPENCV_EXTRA_MODULES_PATH=/full/path/to/opencv_contrib-4.0.0/modules ../ 
$ make -j4 
$ sudo make install 

让我们将opencv.pc中的pkgconfig文件复制到/usr/local/lib/pkgconfig,并将其命名为opencv4.pc

$ cp /full/path/to/opencv-4.0.0/build/lib/pkgconfig/opencv.pc /usr/local/lib/pkgconfig/opencv4.pc 

我们完了! 我们现在可以使用它从命令行编译我们的 OpenCV 程序。 此外,如果您已经安装了 OpenCV 3.x.x,则不会发生冲突。

让我们检查一下安装是否工作正常:

$ cd /full/path/to/opencv-4.0.0/samples/cpp 
$ g++ -ggdb `pkg-config --cflags --libs opencv4` opencv_version.cpp -o /tmp/opencv_version && /tmp/opencv_version 

如果您的终端上显示了欢迎使用 OpenCV 4.0.0,您应该可以开始使用了。 在接下来的章节中,我们将学习如何使用 CMake 来构建我们的 OpenCV 项目。

简略的 / 概括的 / 简易判罪的 / 简易的

在本章中,我们讨论了人类的视觉系统,以及人类如何处理视觉数据。 我们解释了为什么机器很难做到这一点,以及在设计计算机视觉库时需要考虑的问题。

我们了解了使用 OpenCV 可以做什么,以及可以用来完成这些任务的各种模块。 最后,我们学习了如何在各种操作系统上安装 OpenCV。

在下一章中,我们将讨论如何对图像进行操作,以及如何使用各种函数对其进行操作。 我们还将学习如何为我们的 OpenCV 应用构建项目结构。

二、OpenCV 基础知识简介

第 1 章OpenCV入门中介绍了 OpenCV 在不同操作系统上的安装之后,我们将在本章介绍 OpenCV 开发的基础知识。 它首先展示如何使用 CMake 创建我们的项目。 我们将介绍最基本的图像数据结构和矩阵,以及在我们的项目中工作所需的其他结构。 我们将介绍如何使用 XML/YAML 持久性 OpenCV 函数将变量和数据保存到文件中。

在本章中,我们将介绍以下主题:

  • 使用 CMake 配置项目
  • 从磁盘读取图像/向磁盘写入图像
  • 阅读视频和访问摄像设备
  • 主要图像结构(例如,矩阵)
  • 其他重要和基本的结构(例如,矢量和标量)
  • 基本矩阵运算入门
  • 使用 XML/YAML 持久性 OpenCV API 进行文件存储操作

技术要求

本章要求熟悉基本的 C++ 编程语言。 本章中使用的所有代码都可以从以下 gihub 链接下载:*https://github.com/PacktPublishing/Building-Computer-Vision-Projects-with-OpenCV4-and-CPlusPlus/tree/master/Chapter02。*这些代码可以在任何操作系统上执行,尽管它只在 Ubuntu 上测试过。
查看以下视频了解实际操作的代码:
http://bit.ly/2QxhNBa

基本 CMake 配置文件

要配置和检查项目的所有必需依赖项,我们将使用 CMake,但这不是唯一的方法;我们可以在任何其他工具或 IDE 中配置我们的项目,例如MakefilesVisual Studio,但 CMake 是配置多平台**C++**项目的一种更可移植的方式。

CMake 使用名为CMakeLists.txt的配置文件,其中定义了编译和依赖过程。 对于基于从单个源代码文件构建的可执行文件的基本项目,只需包含三行代码的CMakeLists.txt文件即可。 该文件如下所示:

cmake_minimum_required (VERSION 3.0) 
project (CMakeTest) 
add_executable(${PROJECT_NAME} main.cpp) 

第一行定义了所需的 CMake 的最低版本。 此行在我们的CMakeLists.txt文件中是必需的,并允许我们使用特定版本中定义的 CMake 的功能;在我们的示例中,我们至少需要 CMake 3.0。 第二行定义项目名称。 此名称保存在名为PROJECT_NAME的变量中。

最后一行从main.cpp文件创建一个可执行命令(add_executable()),并为其提供与我们的项目(${PROJECT_NAME})相同的名称,并将源代码编译成名为CMakeTest的可执行文件,这是我们设置为项目名称的名称。 ${}表达式允许访问我们的环境中定义的任何变量。 然后,我们可以使用${PROJECT_NAME}变量作为可执行的输出名称。

创建库

CMake 允许我们创建 OpenCV 构建系统使用的库。 分解多个应用之间的共享代码是软件开发中常见且有用的做法。 在大型应用中,或者在多个应用中共享的公共代码中,这种做法非常有用。 在本例中,我们不创建二进制可执行文件,而是创建一个包含所有函数、类等的编译文件。 然后,我们可以与其他应用共享此库文件,而无需共享源代码。

为此,CMake 包含add_library函数:

# Create our hello library 
    add_library(Hello hello.cpp hello.h) 

# Create our application that uses our new library 
    add_executable(executable main.cpp) 

# Link our executable with the new library 
    target_link_libraries(executable Hello) 

#开头的行添加注释,并被 CMake 忽略。 add_library*(Hello hello.cpp hello.h)命令定义库的源文件及其名称,其中Hello是库名,hello.cpphello.h是源文件。 我们还添加了头文件,以允许诸如 Visual Studio 之类的 IDE 链接到头文件。 此行将生成共享(.so用于 Mac OS X,Unix 或.dll用于 Windows)或静态库(.a用于 Mac OS X,Unix 或.lib用于 Windows)文件,具体取决于我们在库名和源文件之间添加的是SHARED还是STATIC字。 target_link_libraries(executable Hello)是将我们的可执行文件链接到所需库的函数,在我们的例子中,它是Hello库。

管理依赖项

CMake 能够搜索我们的依赖项和外部库,使我们能够根据项目中的外部组件构建复杂的项目,并添加一些需求。

在本书中,最重要的依赖项当然是 OpenCV,我们将把它添加到我们的所有项目中:

    cmake_minimum_required (VERSION 3.0) 
    PROJECT(Chapter2) 
# Requires OpenCV 
    FIND_PACKAGE( OpenCV 4.0.0 REQUIRED ) 
# Show a message with the opencv version detected 
    MESSAGE("OpenCV version : ${OpenCV_VERSION}") 
# Add the paths to the include directories/to the header files
    include_directories(${OpenCV_INCLUDE_DIRS}) 
# Add the paths to the compiled libraries/objects
    link_directories(${OpenCV_LIB_DIR}) 
# Create a variable called SRC 
    SET(SRC main.cpp) 
# Create our executable 
    ADD_EXECUTABLE(${PROJECT_NAME} ${SRC}) 
# Link our library 
    TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${OpenCV_LIBS})

现在,让我们从以下几个方面来了解脚本的工作原理:

cmake_minimum_required (VERSION 3.0) 
cmake_policy(SET CMP0012 NEW) 
PROJECT(Chapter2) 

第一行定义了 CMake 的最低版本,第二行告诉 CMake 使用 CMake 的新行为来帮助识别正确的数字和布尔常量,而无需取消引用具有此类名称的变量;该策略是在 CMake 2.8.0 中引入的,当该策略未从 3.0.2 版开始设置时,CMake 会发出警告。 最后,最后一行定义了项目标题。 定义项目名称后,我们必须定义需求、库和依赖项:

# Requires OpenCV 
    FIND_PACKAGE( OpenCV 4.0.0 REQUIRED ) 
# Show a message with the opencv version detected 
    MESSAGE("OpenCV version : ${OpenCV_VERSION}") 
    include_directories(${OpenCV_INCLUDE_DIRS}) 
    link_directories(${OpenCV_LIB_DIR})

这里是我们搜索 OpenCV 依赖项的地方。 FIND_PACKAGE是一个函数,它允许我们查找依赖项、所需的最低版本以及该依赖项是必需的还是可选的。 在此示例脚本中,我们查找版本 4.0.0 或更高版本的 OpenCV,并声明它是必需的软件包。

The FIND_PACKAGE command includes all OpenCV submodules, but you can specify the submodules that you want to include in the project by executing your application smaller and faster. For example, if we are only going to work with the basic OpenCV types and core functionality, we can use the following command: FIND_PACKAGE(OpenCV 4.0.0 REQUIRED core).

如果 CMake 没有找到它,它会返回一个错误,并且不会阻止我们编译我们的应用。 MESSAGE函数在终端或 CMake GUI 中显示消息。 在我们的示例中,我们显示的 OpenCV 版本如下:

OpenCV version : 4.0.0

${OpenCV_VERSION}是 CMake 存储 OpenCV 包版本的变量。include_directories()link_directories()将指定库的头和目录添加到我们的环境中。 OpenCV CMake 的模块将此数据保存在${OpenCV_INCLUDE_DIRS}${OpenCV_LIB_DIR}变量中。 并非所有平台(如 Linux)都需要这些行,因为这些路径通常位于环境中,但建议使用多个 OpenCV 版本来选择正确的链接并包含目录。 现在是将我们开发的资源包括在内的时候了:

# Create a variable called SRC 
    SET(SRC main.cpp) 
# Create our executable 
    ADD_EXECUTABLE(${PROJECT_NAME} ${SRC}) 
# Link our library 
    TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${OpenCV_LIBS}) 

最后一行创建可执行文件,并将可执行文件与 OpenCV 库链接,正如我们在上一节创建库中所看到的那样。*这段代码中有一个新函数SET;该函数创建一个新变量,并向其添加我们需要的任何值。 在我们的示例中,我们在SRC变量中合并了main.cpp参数的值。 我们可以向同一变量添加越来越多的值,如以下脚本所示:

SET(SRC main.cpp 
        utils.cpp 
        color.cpp
)

使脚本更加复杂

在本节中,我们将展示一个更复杂的脚本,它包含子文件夹、库和可执行文件;总共只有两个文件和几行代码,如此脚本所示。 创建多个CMakeLists.txt文件不是强制性的,因为我们可以在主CMakeLists.txt文件中指定所有内容。 但是,对每个项目子文件夹使用不同的CMakeLists.txt文件更为常见,从而使其更加灵活和可移植。

此示例有一个代码结构文件夹,其中包含一个用于utils库的文件夹和一个根文件夹,根文件夹包含主可执行文件:

CMakeLists.txt 
main.cpp 
utils/ 
   CMakeLists.txt 
   computeTime.cpp 
   computeTime.h 
   logger.cpp 
   logger.h 
   plotting.cpp 
   plotting.h 

然后,我们必须定义两个CMakeLists.txt文件,一个在根文件夹中,另一个在根文件夹中。 CMakeLists.txt根文件夹文件包含以下内容:

    cmake_minimum_required (VERSION 3.0) 
    project (Chapter2) 

# Opencv Package required 
    FIND_PACKAGE( OpenCV 4.0.0 REQUIRED ) 

#Add opencv header files to project 
    include_directories(${OpenCV_INCLUDE_DIR}) 
    link_directories(${OpenCV_LIB_DIR}) 

# Add a subdirectory to the build. 
    add_subdirectory(utils)

# Add optional log with a precompiler definition 
    option(WITH_LOG "Build with output logs and images in tmp" OFF) 
    if(WITH_LOG) 
       add_definitions(-DLOG) 
    endif(WITH_LOG) 

# generate our new executable 
    add_executable(${PROJECT_NAME} main.cpp) 
# link the project with his dependencies 
    target_link_libraries(${PROJECT_NAME} ${OpenCV_LIBS} Utils) 

除了我们将解释的一些函数外,几乎所有的代码行都在前面几节中进行了描述。add_subdirectory()告诉 CMake 分析所需的子文件夹的CMakeLists.txt。 在继续主要的CMakeLists.txt文件解释之前,我们先解释一下utils中的第二个CMakeLists.txt文件。

utils个文件夹的CMakeLists.txt个文件中,我们将编写一个新的库以包括在我们的主项目文件夹中:

# Add new variable for src utils lib 
    SET(UTILS_LIB_SRC 
       computeTime.cpp  
       logger.cpp  
       plotting.cpp 
    ) 
# create our new utils lib 
    add_library(Utils ${UTILS_LIB_SRC}) 
# make sure the compiler can find include files for our library 
    target_include_directories(Utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 

这个 CMake 脚本文件定义了一个变量UTILS_LIB_SRC,我们在其中添加库中包含的所有源文件,使用add_library函数生成库,并使用target_include_directories函数来允许我们的主项目检测所有头文件。 离开utils子文件夹并继续使用根 CMake 脚本,Option 函数将创建一个新变量(在我们的示例中为WITH_LOG),并附上一小段描述。 这个变量可以通过ccmake命令行或 CMake GUI 界面(其中显示说明)以及允许用户启用或禁用此选项的复选框来更改。*此函数非常有用,可让用户决定是否启用或禁用日志、使用 Java 或 Python 支持进行编译(就像 OpenCV 所做的那样)。

在我们的示例中,我们使用此选项在应用中启用记录器。 要启用记录器,我们在代码中使用预编译器定义,如下所示:

#ifdef LOG 
    logi("Number of iteration %d", i); 
#endif 

此对数宏可以通过调用add_definitions函数(-DLOG)在我们的CMakeLists.txt中定义,该函数本身可以由 CMake 变量WITH_LOG运行或隐藏,条件很简单:

if(WITH_LOG) 
   add_definitions(-DLOG) 
endif(WITH_LOG) 

现在我们已经准备好创建我们的 CMake 脚本文件,以便在任何操作系统上编译我们的计算机视觉项目。 然后,在开始示例项目之前,我们将继续学习 OpenCV 基础知识。

图像和矩阵

毫无疑问,计算机视觉中最重要的结构是图像。 计算机视觉中的图像是用数字设备捕获的物理世界的表示。 这张图片只是一个以矩阵格式存储的数字序列(参见下图)。 每个数字都是对所考虑的波长(例如,彩色图像中的红、绿或蓝)或波长范围(对于全色设备)的光强度的测量。 图像中的每个点都称为非像素(对于图片元素),每个像素可以存储一个或多个值,具体取决于它是仅存储一个值的黑白图像(也称为二进制图像),如01,存储两个值的灰度图像,还是存储三个值的彩色图像。 这些值通常在整数0255之间,但您可以使用其他范围,例如浮点数的01,如h****高动态范围成像(HDRI)或热像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-slTa1BpN-1681961622328)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/e4190036-f9b4-48ab-96f5-de9d971b33c4.png)]

图像以矩阵格式存储,其中每个像素都有一个位置,并且可以通过列号和行号来引用。 OpenCV 使用Mat类来实现此目的。 对于灰度图像,使用单个矩阵,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xjHzWaNg-1681961622329)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/9afa1db4-0f94-4d90-8e1b-fb84ea5a7dea.png)]

在彩色图像的情况下,如下图所示,我们使用宽度 x 高度 x 颜色通道数的矩阵:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RKjGRjgH-1681961622329)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/06c0b819-e68c-4843-91fd-08feadf5a698.png)]

但是Mat类不仅用于存储图像;它还允许您存储任何类型和不同大小的矩阵。 您可以将其用作代数矩阵并对其执行运算。 在接下来的几节中,我们将介绍最重要的矩阵运算,例如加法、乘法、对角化。 但是,在此之前,了解矩阵在计算机内存中的内部存储方式很重要,因为访问内存插槽总是比使用 OpenCV 函数访问每个像素效率更高。

在内存中,矩阵保存为按列和行排序的数组或值序列。 下表显示了BGR图像格式的像素序列:

| 第 0 行 | 第 1 行 | 第 2 行 |
| 使用 0 | 使用 1 个 | 使用 2 个 | 使用 0 | 使用 1 个 | 使用 2 个 | 使用 0 | 使用 1 个 | 使用 2 个 |
| 像素ρ1 | 像素 2 | 像素 3 | 像素 4 | 像素 5 | 像素 6 | 像素 7 | 像素 8 | 像素 9 |
| 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 |

按照此顺序,我们可以通过遵循以下公式访问任何像素:

Value= Row_i*num_cols*num_channels + Col_i + channel_i 

OpenCV functions are quite optimized for random access, but sometimes, direct access to the memory (work with pointer arithmetic) is more efficient, for example, when we have to access all pixels in a loop.

读/写图像

在介绍了矩阵之后,我们将从 OpenCV 代码基础开始。 我们首先要学习的是如何读写图像:

#include <iostream> 
#include <string> 
#include <sstream> 
using namespace std; 

// OpenCV includes 
#include "opencv2/core.hpp" 
#include "opencv2/highgui.hpp" 
using namespace cv; 

int main(int argc, const char** argv) 
{ 
   // Read images 
   Mat color= imread("../lena.jpg"); 
   Mat gray= imread("../lena.jpg",CV_LOAD_IMAGE_GRAYSCALE); 

  if(! color.data ) // Check for invalid input
 {
 cout << "Could not open or find the image" << std::endl ;
 return -1;
 }
   // Write images 
   imwrite("lenaGray.jpg", gray); 

   // Get same pixel with opencv function 
   int myRow=color.cols-1; 
   int myCol=color.rows-1; 
   Vec3b pixel= color.at<Vec3b>(myRow, myCol); 
   cout << "Pixel value (B,G,R): (" << (int)pixel[0] << "," << (int)pixel[1] << "," << (int)pixel[2] << ")" << endl; 

   // show images 
   imshow("Lena BGR", color); 
   imshow("Lena Gray", gray); 
   // wait for any key press 
   waitKey(0); 
   return 0; 
} 

现在让我们继续理解代码:

// OpenCV includes 
#include "opencv2/core.hpp" 
#include "opencv2/highgui.hpp" 
using namespace cv; 

首先,我们必须在示例中包含所需函数的声明。 这些函数来自core(基本图像数据处理)和highgui(OpenCV 提供的跨平台 I/O 函数包括corehighui;;第一个函数包括矩阵等基础类,第二个函数包括图形界面读、写和显示图像的函数)。 现在是阅读图片的时候了:

// Read images 
Mat color= imread("../lena.jpg"); 
Mat gray= imread("../lena.jpg",CV_LOAD_IMAGE_GRAYSCALE); 

imread是读取图像的主要功能。 此函数用于打开图像并以矩阵格式存储。 imread接受两个参数。 第一个参数是包含图像路径的字符串,第二个参数是可选的,默认情况下将图像加载为彩色图像。 第二个参数允许以下选项:

  • cv::IMREAD_UNCHANGED:如果设置,则在输入具有相应深度时返回 16 位/32 位图像,否则将其转换为 8 位
  • cv::IMREAD_COLOR:如果设置,则始终将图像转换为彩色图像(BGR,8 位无符号)
  • cv::IMREAD_GRAYSCALE:如果设置,则始终将图像转换为灰度图像(8 位无符号)

要保存图像,我们可以使用imwrite函数,该函数将矩阵图像存储在我们的计算机中:

// Write images 
imwrite("lenaGray.jpg", gray); 

第一个参数是我们要用所需的扩展格式保存图像的路径。 第二个参数是我们要保存的矩阵图像。 在我们的代码示例中,我们创建并存储图像的灰色版本,然后将其另存为.jpg文件。 我们加载的灰度图像将存储在第二个灰度变量中:

// Get same pixel with opencv function 
int myRow=color.cols-1; 
int myCol=color.rows-1;

使用矩阵的.cols.rows属性,我们可以获取图像中的列数和行数,也就是宽度和高度:

Vec3b pixel= color.at<Vec3b>(myRow, myCol); 
cout << "Pixel value (B,G,R): (" << (int)pixel[0] << "," << (int)pixel[1] << "," << (int)pixel[2] << ")" << endl; 

要访问图像的一个像素,我们使用MatOpenCV 类中的模板函数cv::Mat::at<typename t>(row,col)。 模板参数是所需的返回类型。 8 位彩色图像中的一个类型名称是存储三个无符号字符数据(Vec=向量,3=分量数,b=1 字节)的Vec3b类。 对于灰色图像,我们可以直接使用无符号字符,或图像中使用的任何其他数字格式,如uchar pixel= color.at<uchar>(myRow, myCol)。最后,为了显示图像,我们可以使用imshow函数,该函数创建一个窗口,第一个参数是标题,第二个参数是图像矩阵:

// show images 
imshow("Lena BGR", color); 
imshow("Lena Gray", gray); 
// wait for any key press 
waitKey(0); 

If we want to stop the application from waiting, we can use the OpenCV function waitKey, with a parameter of the number of milliseconds we want to wait for a key press. If we set up the parameter to 0, then the function will wait until a key is pressed.

上述代码的结果如下图所示。 左边的图像是彩色图像,右边的图像是灰度图像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2ZFyVhv-1681961622329)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/0b25b775-27e1-472c-89b7-0c5404c14a83.png)]

最后,作为以下示例的示例,我们将创建CMakeLists.txt代码文件,并了解如何使用该文件编译代码。

下面的代码描述了CMakeLists.txt文件:

cmake_minimum_required (VERSION 3.0) 
cmake_policy(SET CMP0012 NEW) 
PROJECT(project) 

# Requires OpenCV 
FIND_PACKAGE( OpenCV 4.0.0 REQUIRED ) 
MESSAGE("OpenCV version : ${OpenCV_VERSION}") 

include_directories(${OpenCV_INCLUDE_DIRS}) 
link_directories(${OpenCV_LIB_DIR}) 

ADD_EXECUTABLE(sample main.cpp) 
TARGET_LINK_LIBRARIES(sample ${OpenCV_LIBS})

要使用此CMakeLists.txt文件编译代码,我们必须执行以下步骤:

  1. 创建一个build文件夹。
  2. build文件夹内,执行 CMake 或在 Windows 中打开 CMake GUI 应用,选择sourcebuild文件夹,然后按配置和生成按钮。
  3. 如果您使用的是 Linux 或 MacOS,请像往常一样生成一个 Makefile,然后使用*make命令编译该项目。 如果您使用的是 Windows,请使用在步骤 2 中选择的编辑器打开项目,然后编译。

最后,在编译我们的应用之后,我们将在 Build 文件夹中拥有一个名为app的可执行文件,我们可以执行该文件。

阅读视频和摄像机

本节使用这个简单的示例向您介绍视频和相机阅读。 在解释如何读取视频或摄像机输入之前,我们想介绍一个新的、非常有用的类,它可以帮助我们管理输入命令行参数。 此新类是在 OpenCV 3.0 版中引入的,是CommandLineParser类:

// OpenCV command line parser functions 
// Keys accepted by command line parser 
const char* keys = 
{ 
   "{help h usage ? | | print this message}" 
    "{@video | | Video file, if not defined try to use webcamera}" 
}; 

对于CommandLineParser,我们必须做的第一件事是定义在常量char向量中需要或允许哪些参数;每行都有以下模式:

"{name_param | default_value | description}"

name_param前面可以有@,它将此参数定义为默认输入。 我们可以使用多个name_param

CommandLineParser parser(argc, argv, keys);

构造函数将获得 Main 函数的输入和先前定义的键常量:

//If requires help show 
if (parser.has("help")) 
{ 
       parser.printMessage(); 
       return 0; 
} 

.has类方法检查参数是否存在。 在示例中,我们检查用户是否添加了参数help?,然后使用类函数printMessage显示所有描述参数:

   String videoFile= parser.get<String>(0);

使用.get<typename>(parameterName)函数,我们可以访问和读取任何输入参数:

   // Check if params are correctly parsed in his variables 
   if (!parser.check()) 
   { 
       parser.printErrors(); 
       return 0; 
   } 

获取所有必需的参数后,我们可以检查这些参数是否正确解析,如果其中一个参数没有解析,则会显示错误消息,例如,添加字符串而不是数字:

VideoCapture cap; // open the default camera 
if(videoFile != "") 
   cap.open(videoFile); 
else 
   cap.open(0); 
if(!cap.isOpened())  // check if we succeeded 
   return -1;  

视频读取和摄像头读取的类是相同的:与先前版本的 OpenCV 一样,属于videoio子模块的是VideoCapture类,而不是属于highgui子模块的VideoCapture类。 创建对象后,我们检查输入命令行参数videoFile是否有路径文件名。 如果它是空的,则我们尝试打开网络摄像机;如果它有文件名,则打开视频文件。 为此,我们使用open函数,给出我们想要打开的视频文件名或索引摄像机作为参数。 如果我们只有一台摄像机,我们可以使用0作为参数。

为了检查是否可以读取视频文件名或摄像头,我们使用isOpened函数:

namedWindow("Video",1); 
for(;;) 
{ 
    Mat frame; 
    cap >> frame; // get a new frame from camera 
    if(frame) 
       imshow("Video", frame); 
    if(waitKey(30) >= 0) break; 
} 
// Release the camera or video cap 
cap.release(); 

最后,我们使用namedWindow函数创建一个窗口来显示帧,并且使用无限循环,使用>>操作抓取每个帧,并且如果我们正确地检索到帧,则使用imshow函数显示该帧。 在本例中,我们不想停止应用,但将等待 30 毫秒,以检查是否有用户想要使用waitKey(30)使用任何键来停止应用执行。

The time required to wait for the next frame using camera access is calculated from the camera speed and our spent algorithm time. For example, if a camera works at 20 fps, and our algorithm spent 10 milliseconds, a great waiting value is 30 = (1000/20) - 10 milliseconds. This value is calculated considering a wait of a sufficient amount of time to ensure that the next frame is in the buffer. If our camera takes 40 milliseconds to take each image, and we use 10 milliseconds in our algorithm, then we only need to stop with waitKey 30 milliseconds, because 30 milliseconds of wait time, plus 10 milliseconds of our algorithm, is the same amount of time for which each frame of the camera is accessible.

当用户想要完成应用时,他们所要做的就是按任意键,然后我们必须使用释放功能释放所有的视频资源。

It is very important to release all resources that we use in a computer vision application. If we do not, we can consume all RAM memory. We can release the matrices using the release function.

前面代码的结果是一个新窗口,显示 BGR 格式的视频或网络摄像机。

其他基本对象类型

我们已经了解了MatVec3b类,但我们还需要学习更多的类。

在本节中,我们将了解大多数项目所需的最基本的对象类型:

  • Vec
  • Scalar
  • Point
  • Size
  • Rect
  • RotatedRect

VEC 对象类型

Vec是一个主要用于数值向量的模板类。 我们可以定义任何类型的向量和分量数量:

Vec<double,19> myVector; 

我们还可以使用任何预定义类型:

typedef Vec<uchar, 2> Vec2b; 
typedef Vec<uchar, 3> Vec3b; 
typedef Vec<uchar, 4> Vec4b; 

typedef Vec<short, 2> Vec2s; 
typedef Vec<short, 3> Vec3s; 
typedef Vec<short, 4> Vec4s; 

typedef Vec<int, 2> Vec2i; 
typedef Vec<int, 3> Vec3i; 
typedef Vec<int, 4> Vec4i; 

typedef Vec<float, 2> Vec2f; 
typedef Vec<float, 3> Vec3f; 
typedef Vec<float, 4> Vec4f; 
typedef Vec<float, 6> Vec6f; 

typedef Vec<double, 2> Vec2d; 
typedef Vec<double, 3> Vec3d; 
typedef Vec<double, 4> Vec4d; 
typedef Vec<double, 6> Vec6d; 

All the following vector operations are also implemented:
v1 = v2 + v3
v1 = v2 - v3
v1 = v2 * scale
v1 = scale * v2
v1 = -v2
v1 += v2

实现的其他扩充操作如下:
v1 == v2, v1 != v2
norm(v1) (euclidean norm)

标量对象类型

Scalar对象类型是从Vec派生的具有四个元素的模板类。 Scalar类型在 OpenCV 中广泛用于传递和读取像素值。

要访问VecScalar值,我们使用[]操作符,它可以从另一个标量、向量或逐值初始化,如以下示例所示:

Scalar s0(0);
Scalar s1(0.0, 1.0, 2.0, 3.0);
Scalar s2(s1);

点对象类型

另一个非常常见的类模板是Point。 此类定义由其坐标xy指定的二维点。

Like Point, there is a Point3 template class for 3D point support.

Vec类一样,为方便起见,OpenCV 定义了以下Point别名:

typedef Point_<int> Point2i; 
typedef Point2i Point; 
typedef Point_<float> Point2f; 
typedef Point_<double> Point2d; 
 The following operators are defined for points:
    pt1 = pt2 + pt3; 
    pt1 = pt2 - pt3; 
    pt1 = pt2 * a; 
    pt1 = a * pt2; 
    pt1 = pt2 / a; 
    pt1 += pt2; 
    pt1 -= pt2; 
    pt1 *= a; 
    pt1 /= a; 
    double value = norm(pt); // L2 norm 
    pt1 == pt2; 
    pt1 != pt2; 

大小对象类型

OpenCV 中另一个非常重要且广泛使用的模板类是用于指定图像或矩形大小的模板类Size。 该类添加了两个成员:width 和 Height,以及有用的函数 Sizearea()。*在下面的示例中,我们可以看到使用 SIZE 的多种方法:

Size s(100,100);
Mat img=Mat::zeros(s, CV_8UC1); // 100 by 100 single channel matrix
s.width= 200;
int area= s.area(); returns 100x200

矩形对象类型

Rect是定义由以下参数定义的 2D 矩形的另一个重要模板类:

  • 左上角的坐标

  • 矩形的宽度和高度

Rect模板类可用于定义图像的感兴趣区域和(ROI),如下所示:

Mat img=imread("lena.jpg");
Rect rect_roi(0,0,100,100);
Mat img_roi=img(r);

RotatedRect 对象类型

最后一个有用的类是一个名为RotatedRect的特定矩形。 此类表示由中心点、矩形的宽度和高度以及旋转角度(以度为单位)指定的旋转矩形:

RotatedRect(const Point2f& center, const Size2f& size, float angle); 

这个类的一个有趣的函数是boundingBox。 此函数返回Rect,其中包含旋转的矩形:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-twxOekOF-1681961622329)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/d2f5d439-a48d-48fe-bc63-8ee6486b8c27.png)]

基本矩阵运算

在本节中,我们将学习一些可以应用于图像或任何矩阵数据的基本且重要的矩阵运算。 我们学习了如何加载图像并将其存储在Mat变量中,但我们可以手动创建Mat。 最常见的构造函数是为矩阵指定大小和类型,如下所示:

Mat a= Mat(Size(5,5), CV_32F); 

You can create a new matrix linking with a stored buffer from third-party libraries without copying data using this constructor: Mat(size, type, pointer_to_buffer).

支持的类型取决于您要存储的号码类型和频道数量。 最常见的类型如下:

CV_8UC1 
CV_8UC3 
CV_8UC4 
CV_32FC1 
CV_32FC3 
CV_32FC4

You can create any type of matrix using CV_number_typeC(n), where the number_type is 8 bits unsigned (8U) to 64 float (64F), and where (n) is the number of channels; the number of channels permitted ranges from 1 to CV_CN_MAX.

初始化不会设置数据值,因此您可能会得到不需要的值。 为避免不需要的值,您可以使用01值以及它们各自的函数来初始化矩阵:

Mat mz= Mat::zeros(5,5, CV_32F); 
Mat mo= Mat::ones(5,5, CV_32F); 

上述矩阵的结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-odDa3Dba-1681961622330)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/94be0666-71d8-4e29-b87e-cf7191d4410e.png)]

特殊矩阵初始化是 EYE 函数,它创建具有指定类型和大小的单位矩阵:

Mat m= Mat::eye(5,5, CV_32F); 

输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QW2Kg64l-1681961622330)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/c9e50aa3-b54a-45ce-af48-2f0c84049749.png)]

OpenCV 的Mat类中允许所有矩阵运算。 我们可以使用+-运算符将两个大小相同的矩阵相加或相减,如以下代码块所示:

Mat a= Mat::eye(Size(3,2), CV_32F); 
Mat b= Mat::ones(Size(3,2), CV_32F); 
Mat c= a+b; 
Mat d= a-b;

上述操作的结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6dbnvlRT-1681961622330)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/17298d68-e4c8-4ee8-8e22-03f41f83fe81.png)]

我们可以使用**运算符乘以标量,或者使用*mul函数计算每个元素的矩阵,并且可以使用**运算符执行矩阵乘法:

Mat m1= Mat::eye(2,3, CV_32F); 
Mat m2= Mat::ones(3,2, CV_32F); 
// Scalar by matrix 
cout << "nm1.*2n" << m1*2 << endl; 
// matrix per element multiplication 
cout << "n(m1+2).*(m1+3)n" << (m1+1).mul(m1+3) << endl; 
// Matrix multiplication 
cout << "nm1*m2n" << m1*m2 << endl; 

上述操作的结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pYyb0T7s-1681961622331)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/5695ad43-0eab-4d4b-85ce-dbd04c17fecb.png)]

其他常见的数学矩阵运算是转置矩阵求逆,分别由函数t()inv()定义。 OpenCV 提供的其他有趣功能是矩阵中的数组操作,例如,对非零元素进行计数。 这对于计算对象的像素或面积很有用:

int countNonZero(src); 

OpenCV 提供了一些统计功能。 通道的平均值和标准偏差可以使用以下函数meanStdDev计算:

meanStdDev(src, mean, stddev); 

另一个有用的统计函数是minMaxLoc。 此函数用于查找矩阵或数组的最小值和最大值,并返回位置和值:

minMaxLoc(src, minVal, maxVal, minLoc, maxLoc); 

这里,src是输入矩阵,minValmaxVal是检测到的双值,minLocmaxLoc是检测到的Point值。

Other core and useful functions are described in detail at: http://docs.opencv.org/modules/core/doc/core.html.

基本数据持久化和存储

在完成本章之前,我们将探索用于存储和读取数据的 OpenCV 函数。 在许多应用中,例如校准或机器学习,当我们完成一些计算时,我们需要保存这些结果,以便在后续操作中检索它们。 为此,OpenCV 提供了一个 XML/YAML 持久层。

写入文件存储

要使用某些 OpenCV 或其他数字数据写入文件,我们可以使用FileStorage类,并使用 STL 流等流<<运算符:

#include "opencv2/opencv.hpp" 
using namespace cv; 

int main(int, char** argv) 
{ 
   // create our writer 
    FileStorage fs("test.yml", FileStorage::WRITE); 
    // Save an int 
    int fps= 5; 
    fs << "fps" << fps; 
    // Create some mat sample 
    Mat m1= Mat::eye(2,3, CV_32F); 
    Mat m2= Mat::ones(3,2, CV_32F); 
    Mat result= (m1+1).mul(m1+3); 
    // write the result 
    fs << "Result" << result; 
    // release the file 
    fs.release(); 

    FileStorage fs2("test.yml", FileStorage::READ); 

    Mat r; 
    fs2["Result"] >> r; 
    std::cout << r << std::endl; 

    fs2.release(); 

    return 0; 
} 

要创建保存数据的文件存储,我们只需调用构造函数,给出所需扩展格式(XML 或 YAML)的路径文件名,并将第二个参数设置为 WRITE:

FileStorage fs("test.yml", FileStorage::WRITE); 

如果我们想要保存数据,我们只需要使用流运算符,在第一阶段给出一个标识符,然后给出我们想要保存的矩阵或值。 例如,要保存int变量,我们只需编写以下代码行:

int fps= 5; 
fs << "fps" << fps; 

否则,我们可以写入/保存mat,如下所示:

Mat m1= Mat::eye(2,3, CV_32F); 
Mat m2= Mat::ones(3,2, CV_32F); 
Mat result= (m1+1).mul(m1+3); 
// write the result 
fs << "Result" << result;

上述代码的结果是 YAML 格式:

%YAML:1.0 
fps: 5 
Result: !!opencv-matrix 
   rows: 2 
   cols: 3 
   dt: f 
   data: [ 8., 3., 3., 3., 8., 3\. ] 

从文件存储中读取以读取先前保存的文件与save功能非常相似:

#include "opencv2/opencv.hpp" 
using namespace cv; 

int main(int, char** argv) 
{ 
   FileStorage fs2("test.yml", FileStorage::READ); 

   Mat r; 
   fs2["Result"] >> r; 
   std::cout << r << std::endl; 

   fs2.release(); 

   return 0; 
} 

第一步是使用适当的参数、路径和FileStorage::READ使用FileStorage构造函数打开保存的文件:

FileStorage fs2("test.yml", FileStorage::READ); 

要读取任何存储的变量,我们只需要使用公共流运算符>>(使用我们的FileStorage对象)和标识符(带有[]运算符):

Mat r; 
fs2["Result"] >> r; 

简略的 / 概括的 / 简易判罪的 / 简易的

在本章中,我们学习了 OpenCV 的基础知识和最重要的类型和操作,图像和视频的访问,以及它们如何存储在矩阵中。 我们学习了存储像素、向量等的基本矩阵运算和其他基本 OpenCV 类。 最后,我们学习了如何将数据保存在文件中,以便在其他应用或其他执行中可以直接读取。

在下一章中,我们将学习如何创建我们的第一个应用,学习 OpenCV 提供的图形用户界面的基础知识。 我们将创建按钮和滑块,并介绍一些图像处理基础知识。

三、学习图形用户界面

第 2 章OpenCV 基础知识中,我们学习了 OpenCV 的基本类和结构,以及最重要的类Mat。 我们学习了如何读取和保存图像和视频,以及图像记忆的内部结构。 我们现在已经准备好使用 OpenCV,但在大多数情况下,我们需要显示图像结果,并使用多个用户界面检索用户与图像的交互。 OpenCV 为我们提供了几个基本的用户界面,帮助我们创建应用和原型。为了更好地理解用户界面是如何工作的,我们将在本章末尾创建一个名为PhotoTool的小型应用。 在本应用中,我们将学习如何使用滤镜和颜色转换。

本章介绍以下主题:

  • OpenCV 基本用户界面
  • OpenCV Qt 界面
  • 滑块和按钮
  • 高级用户界面-OpenGL
  • 颜色转换
  • 基本过滤器

技术要求

本章要求熟悉基本的 C++ 编程语言。 本章使用的所有代码都可以从以下 gihub 链接下载:https://github.com/PacktPublishing/Building-Computer-Vision-Projects-with-OpenCV4-and-CPlusPlus/tree/master/Chapter03。这些代码可以在任何操作系统上执行,尽管它只在 Ubuntu 上测试过。

请查看以下视频以了解代码的实际使用情况:
http://bit.ly/2KH2QXD

OpenCV 用户界面简介

OpenCV 拥有自己的跨操作系统用户界面,允许开发人员创建自己的应用,而无需学习复杂的用户界面库。 OpenCV 用户界面是基本的,但它为计算机视觉开发人员提供了创建和管理其软件开发的基本功能。 所有这些都是原生的,并针对实时使用进行了优化。

OpenCV 提供两个用户界面选项:

  • 基于原生用户界面的基本界面,适用于 Mac OS X 的 Cocoa 或 Carbon,以及适用于 Linux 或 Windows 用户界面的 GTK,在编译 OpenCV 时默认选择。
  • 一个基于 Qt 库的更高级的界面,它是一个跨平台的界面。 在编译 OpenCV 之前,您必须在 CMake 中手动启用 Qt 选项。

在下面的屏幕截图中,您可以看到左侧的基本用户界面窗口和右侧的 Qt 用户界面:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g4Gbwqjb-1681961622331)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/b69bc634-2911-40d0-9411-e81b3669ca1d.png)]

使用 OpenCV 的基本图形用户界面

我们将使用 OpenCV 创建一个基本的用户界面。 OpenCV 用户界面允许我们创建窗口,向其中添加图像,以及移动、调整大小和销毁窗口。 用户界面在 OpenCV 的highui模块中。在下面的代码中,我们将学习如何通过按一个键来显示多个窗口,同时图像在桌面的窗口中移动,从而创建和显示两个图像。

不要担心阅读完整的代码;我们将分成小块进行解释:

#include <iostream> 
#include <string> 
#include <sstream> 
using namespace std; 

// OpenCV includes 
#include <opencv2/core.hpp> 
#include <opencv2/highgui.hpp> 
using namespace cv; 

int main(int argc, const char** argv) 
{ 
   // Read images 
   Mat lena= imread("../lena.jpg"); 
   # Checking if Lena image has been loaded
   if (!lena.data) {
 cout << "Lena image missing!" << enld;
 return -1;
   }
   Mat photo= imread("../photo.jpg"); 
   # Checking if Lena image has been loaded
   if (!photo.data) {
 cout << "Lena image missing!" << enld;
 return -1;
 }

   // Create windows 
   namedWindow("Lena", WINDOW_NORMAL); 
   namedWindow("Photo", WINDOW_AUTOSIZE); 

   // Move window 
   moveWindow("Lena", 10, 10); 
   moveWindow("Photo", 520, 10); 

   // show images 
   imshow("Lena", lena); 
   imshow("Photo", photo);  

   // Resize window, only non autosize 
   resizeWindow("Lena", 512, 512);  

   // wait for any key press 
   waitKey(0); 

   // Destroy the windows 
   destroyWindow("Lena"); 
   destroyWindow("Photo"); 

   // Create 10 windows 
   for(int i =0; i< 10; i++) 
   { 
         ostringstream ss; 
         ss << "Photo" << i; 
         namedWindow(ss.str()); 
         moveWindow(ss.str(), 20*i, 20*i); 
         imshow(ss.str(), photo); 
   } 

   waitKey(0); 
   // Destroy all windows 
   destroyAllWindows(); 
   return 0; 
} 

让我们来了解一下代码:

  1. 为了方便图形用户界面,我们必须执行的第一个任务是导入 OpenCV 的highui模块:
#include <opencv2/highgui.hpp> 
  1. 现在我们已经准备好创建我们的新窗口,我们必须加载一些图像:
// Read images 
Mat lena= imread("../lena.jpg"); 
Mat photo= imread("../photo.jpg"); 
  1. 要创建窗口,我们使用namedWindow函数。 该函数有两个参数;第一个参数是包含窗口名称的常量字符串,第二个参数是我们需要的标志。 第二个参数是可选的:
namedWindow("Lena", WINDOW_NORMAL); 
namedWindow("Photo", WINDOW_AUTOSIZE);
  1. 在我们的示例中,我们创建了两个窗口:第一个窗口称为Lena,第二个窗口称为Photo

默认情况下,Qt 和 NATIVE 有三个标志:

Qt 具有多个附加标志:

If we compile OpenCV with Qt, all the windows that we create are, by default, in the expanded interface, but we can use native interfaces and more basic ones adding the CV_GUI_NORMAL flag. By default, the flags are WINDOW_AUTOSIZE, WINDOW_KEEPRATIO, and WINDOW_GUI_EXPANDED.

  1. 当我们创建多个窗口时,它们是重叠的,但我们可以使用moveWindow函数将窗口移动到桌面的任何区域,如下所示:
// Move window 
moveWindow("Lena", 10, 10); 
moveWindow("Photo", 520, 10); 
  1. 在我们的代码中,我们向左移动Lena窗口10像素,向上移动10像素,向左移动Photo窗口520像素,向上移动10像素:
// show images 
imshow("Lena", lena); 
imshow("Photo", photo);  
// Resize window, only non autosize 
resizeWindow("Lena", 512, 512);
  1. 在显示了我们之前使用imshow函数加载的图像之后,我们调用resizeWindow函数,将Lena窗口的大小调整为512像素。 该函数有三个参数:window namewidthheight

The specific window size is for the image area. Toolbars are not counted. Only windows without the WINDOW_AUTOSIZE flag enabled can be resized.

  1. 在使用waitKey函数等待按键后,我们将使用destroyWindow函数移除或删除我们的窗口,其中窗口名称是唯一必需的参数:
waitKey(0); 

// Destroy the windows 
destroyWindow("Lena"); 
destroyWindow("Photo"); 
  1. OpenCV 有一个功能,可以删除我们在一次调用中创建的所有窗口。 该函数称为destroyAllWindows。 为了演示其工作原理,我们在样例中创建了 10 个窗口,并等待按键。 当用户按任意键时,它会销毁所有窗口:
 // Create 10 windows 
for(int i =0; i< 10; i++) 
{ 
   ostringstream ss; 
   ss << "Photo" << i; 
   namedWindow(ss.str()); 
   moveWindow(ss.str(), 20*i, 20*i); 
   imshow(ss.str(), photo); 
} 

waitKey(0); 
// Destroy all windows 
destroyAllWindows(); 

在任何情况下,OpenCV 都会在应用终止时自动处理所有窗口的销毁,并且没有必要在应用结束时调用此函数。

所有这些代码的结果可以在下面两个步骤的图像中看到。 首先,它显示两个窗口:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W3QhxJmZ-1681961622331)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/8b43e8bc-dfb5-4646-af8c-bdac68791dc7.png)]

按下任意键后,应用将继续并绘制几个改变位置的窗口:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r4390wmX-1681961622331)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/5c09380d-ac52-4546-8bc8-1ecae6e8a71d.png)]

只需几行代码,我们就能够创建和操作窗口并显示图像。 我们现在已经准备好促进用户与图像的交互,并添加用户界面控件。

将滑块和鼠标事件添加到我们的界面

鼠标事件和滑块控制在计算机视觉和 OpenCV 中非常有用。 使用这些控件用户,我们可以直接与界面交互,并更改输入图像或变量的属性。在本节中,我们将介绍用于基本交互的鼠标事件和滑块控件。 为了便于正确理解,我们创建了以下代码,通过这些代码,我们将使用鼠标事件在图像中绘制绿色圆圈,并使用鼠标滑块模糊图像:

// Create a variable to save the position value in track 
int blurAmount=15; 

// Trackbar call back function 
static void onChange(int pos, void* userInput); 

//Mouse callback 
static void onMouse(int event, int x, int y, int, void* userInput); 

int main(int argc, const char** argv) 
{ 
   // Read images 
   Mat lena= imread("../lena.jpg"); 

   // Create windows 
   namedWindow("Lena"); 

   // create a trackbar 
   createTrackbar("Lena", "Lena", &blurAmount, 30, onChange, &lena); 

   setMouseCallback("Lena", onMouse, &lena); 

   // Call to onChange to init 
   onChange(blurAmount, &lena); 

   // wait app for a key to exit 
   waitKey(0); 

   // Destroy the windows 
   destroyWindow("Lena"); 

   return 0; 
} 

让我们来理解一下代码!

首先,我们创建一个变量来保存滑块位置。 我们需要保存滑块位置,以便从其他功能访问:

// Create a variable to save the position value in track 
int blurAmount=15;

现在,我们定义滑块和鼠标事件的回调,这是 OpenCV 函数setMouseCallbackcreateTrackbar所需的:

// Trackbar call back function 
static void onChange(int pos, void* userInput); 

//Mouse callback 
static void onMouse(int event, int x, int y, int, void* userInput); 

在 main 函数中,我们加载一个图像并创建一个名为Lena的新窗口:

int main(int argc, const char** argv) 
{ 
   // Read images 
   Mat lena= imread("../lena.jpg"); 

   // Create windows 
   namedWindow("Lena"); 

现在是创建滑块的时候了。 OpenCV 具有createTrackbar功能,可按顺序生成具有以下参数的滑块:

  1. 轨迹栏名称。
  2. 窗口名称。
  3. 用作值的整数指针;此参数是可选的。 如果设置了该选项,则滑块在创建时将达到此位置。
  4. 滑块上的最大位置。
  5. 滑块位置更改时的回调函数。
  6. 要发送到回调的用户数据。 它可用于在不使用全局变量的情况下将数据发送到回调。

在此代码中,我们为Lena窗口添加了trackbar,并调用了Lena跟踪条,以便模糊图像。 跟踪条的值存储在我们作为指针传递的第一个blurAmount整数中,并将该条的最大值设置为30。 我们将onChange设置为回调函数,并将 Lena Mat 图像作为用户数据发送:

   // create a trackbar 
   createTrackbar("Lena", "Lena", &blurAmount, 30, onChange, &lena);

创建滑块后,当用户单击鼠标左键时,我们将鼠标事件添加到绘制圆圈中。 OpenCV 具有setMouseCallback测试功能。 此函数有三个参数:

  • 我们在其中获取鼠标事件的窗口名称。

  • 当有任何鼠标交互时要调用的回调函数。

  • 用户数据:这是调用回调函数时将发送给它的任何数据。 在我们的示例中,我们将发送整个Lena图像。

使用以下代码,我们可以将鼠标回调添加到Lena窗口,并将onMouse设置为回调函数,将 Lena Mat 图像作为用户数据进行传递:

setMouseCallback("Lena", onMouse, &lena); 

要只完成 Main 函数,我们需要使用与滑块相同的参数来初始化图像。 要执行初始化,我们只需调用参数onChange的回调函数,并在使用destroyWindow*、*关闭窗口之前等待事件,如以下代码所示:

// Call to onChange to init   
onChange(blurAmount, &lena); 

// wait app for a key to exit 
waitKey(0); 

// Destroy the windows 
destroyWindow("Lena"); 

滑块回调使用滑块值作为模糊量对图像应用基本模糊滤镜:

// Trackbar call back function 
static void onChange(int pos, void* userData) { 
    if(pos <= 0) return; 
    // Aux variable for result 
    Mat imgBlur; 
    // Get the pointer input image     
    Mat* img= (Mat*)userInput; 
    // Apply a blur filter 
    blur(*img, imgBlur, Size(pos, pos)); 
    // Show the result 
    imshow("Lena", imgBlur); 
}

此函数使用变量pos检查滑块值是否为0。 在这种情况下,我们不应用筛选器,因为它会生成错误的执行。 我们也不能应用0像素模糊。 在检查滑块值之后,我们创建一个名为imgBlur的空矩阵来存储模糊结果。 要在回调函数中检索通过用户数据发送的图像,我们必须将void* userData值强制转换为正确的图像类型指针Mat*

现在我们有了正确的变量来应用模糊滤镜。 模糊函数将基本中值滤波器应用于输入图像,在我们的示例中为*img;对于输出图像,最后需要的参数是我们要应用的模糊核的大小(核是用于计算核与图像之间的卷积均值的小矩阵)。 在我们的例子中,我们使用的是大小为pos的平方核。 最后,我们只需要使用imshow函数更新图像界面。

鼠标事件回调有五个输入参数:第一个参数定义事件类型;第二个和第三个参数定义鼠标位置;第四个参数定义滚轮移动;第五个参数定义用户输入数据。

鼠标事件类型如下:

| 事件类型 | 描述 / 描写 / 形容 / 类别 |
| EVENT_MOUSEMOVE | 当用户移动鼠标时。 |
| EVENT_LBUTTONDOWN | 当用户单击鼠标左键时。 |
| EVENT_RBUTTONDOWN | 当用户单击鼠标右键时。 |
| EVENT_MBUTTONDOWN | 当用户单击鼠标中键时。 |
| EVENT_LBUTTONUP | 当用户释放鼠标左键时。 |
| EVENT_RBUTTONUP | 当用户释放鼠标右键时。 |
| EVENT_MBUTTONUP | 当用户释放鼠标中键时。 |
| EVENT_LBUTTONDBLCLK | 当用户双击鼠标左键时。 |
| EVENT_RBUTTONDBLCLK | 当用户双击鼠标右键时。 |
| EVENT_MBUTTONDBLCLK | 当用户双击鼠标中键时。 |
| EVENTMOUSEWHEEL | 当用户使用鼠标滚轮执行垂直滚动时。 |
| EVENT_MOUSEHWHEEL | 当用户使用鼠标滚轮执行水平滚动时。 |

在我们的示例中,我们只管理由鼠标左键单击产生的事件,并且丢弃除EVENT_LBUTTONDOWN之外的任何事件。 丢弃其他事件后,我们会通过滑块回调获得类似的输入图片,并使用 OpenCV 函数中的圆圈来获取图片中的圆圈:

//Mouse callback 
static void onMouse(int event, int x, int y, int, void* userInput) 
{ 
   if(event != EVENT_LBUTTONDOWN) 
           return; 

   // Get the pointer input image 
   Mat* img= (Mat*)userInput; 

   // Draw circle 
   circle(*img, Point(x, y), 10, Scalar(0,255,0), 3); 

   // Call on change to get blurred image 
   onChange(blurAmount, img); 

} 

使用 Qt 的图形用户界面

Qt 用户界面为我们提供了更多的控制和选项来处理我们的图像。

该界面分为以下三个主要区域:

  • 工具栏
  • 图像区域
  • 状态栏

我们可以在下图中看到这三个区域。 图片顶部是工具栏,图片是主区域,图片底部可以看到状态栏:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v7ctqjyd-1681961622332)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/3a140d03-40d0-49ff-b28a-52b8de8d1450.png)]

工具栏从左到右有以下按钮:

  • 四个平移按钮
  • 缩放 x1
  • 缩放 x30,显示标签
  • 放大
  • 拉远 / 拉远镜头
  • 保存当前图像
  • 显示属性

这些选项在下图中可以清楚地看到:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cjdYJQGl-1681961622332)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/92ecf1c6-62c1-43d9-8655-d447e784184c.png)]

当我们在图像上按鼠标右键时,图像区域会显示一个图像和一个上下文菜单。 该区域可以使用displayOverlay函数在区域顶部显示覆盖消息。该函数接受三个参数:窗口名称、我们想要显示的文本以及覆盖文本显示的毫秒周期。 如果将此时间设置为0,则文本永远不会消失:

// Display Overlay 
displayOverlay("Lena", "Overlay 5secs", 5000);

我们可以在下图中看到前面代码的结果。 你可以在图片的顶部看到一个小黑框,上面有一句话叠加了 5 秒:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zjq7mIPO-1681961622337)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/aba441bc-2074-4277-81e5-98b24f7cedcb.png)]

最后,状态栏显示窗口的底部,并显示图像中坐标的像素值和位置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xTuI2YTc-1681961622337)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/51ef8c4f-74dc-461b-b9d3-fcce324f3879.png)]

我们可以使用状态栏以覆盖的方式显示消息。 可以更改状态栏消息的函数是displayStatusBar。此函数与覆盖函数具有相同的参数:窗口名称、要显示的文本以及显示它的时间段:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kkDeqZNR-1681961622337)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/73993491-a939-45ba-b3c1-0d0751c71782.png)]

向用户界面添加按钮

在前面的小节中,我们学习了如何创建普通或 Qt 界面,并使用鼠标和滑块与它们交互,但我们也可以创建不同类型的按钮。

Buttons are only supported in Qt windows.

OpenCV Qt 支持的按钮类型如下:

  • 按钮
  • 校验框
  • 无线电箱

这些按钮仅显示在控制面板中。 控制面板是每个程序的一个独立窗口,我们可以在其中附加按钮和轨迹条。要显示控制面板,我们可以按下最后一个工具栏按钮,右键单击 Qt 窗口的任何部分并选择显示属性窗口,或者使用Ctrl+P快捷键。让我们创建一个带有按钮的基本示例。 代码很丰富,我们将先解释 Main 函数,然后再分别解释每个回调函数,以便更好地理解所有内容。 下面的代码向我们展示了生成用户界面的主要代码函数:

Mat img; 
bool applyGray=false; 
bool applyBlur=false; 
bool applySobel=false; 
... 
int main(int argc, const char** argv) 
{ 
   // Read images 
   img= imread("../lena.jpg"); 

   // Create windows 
   namedWindow("Lena"); 

   // create Buttons 
   createButton("Blur", blurCallback, NULL, QT_CHECKBOX, 0); 

   createButton("Gray",grayCallback,NULL,QT_RADIOBOX, 0); 
   createButton("RGB",bgrCallback,NULL,QT_RADIOBOX, 1); 

   createButton("Sobel",sobelCallback,NULL,QT_PUSH_BUTTON, 0); 

   // wait app for a key to exit 
   waitKey(0); 

   // Destroy the windows 
   destroyWindow("Lena"); 

   return 0; 
} 

我们将应用三种类型的滤镜:模糊滤镜、Sobel 滤镜和颜色到灰色的转换。 所有这些都是可选的,用户可以使用我们将要创建的按钮来选择每个选项。 然后,为了获得每个过滤器的状态,我们创建了三个全局布尔变量:

bool applyGray=false; 
bool applyBlur=false; 
bool applySobel=false;

在 Main 函数中,在加载图像并创建窗口之后,我们必须使用createButton函数来创建每个按钮。

OpenCV 中定义了三种按钮类型:

  • QT_CHECKBOX
  • QT_RADIOBOX
  • QT_PUSH_BUTTON

每个按钮都有五个参数,顺序如下:

  1. 按钮名称
  2. 回调函数
  3. 指向传递给回调的用户变量数据的指针
  4. 按钮类型
  5. CheckBox 和 RadioBox 按钮类型使用的默认初始化状态

然后,我们创建一个模糊复选框按钮、两个用于颜色转换的单选按钮和一个用于 Sobel 滤镜的按钮,如以下代码所示:

   // create Buttons 
   createButton("Blur", blurCallback, NULL, QT_CHECKBOX, 0); 

   createButton("Gray",grayCallback,NULL,QT_RADIOBOX, 0); 
   createButton("RGB",bgrCallback,NULL,QT_RADIOBOX, 1); 

   createButton("Sobel",sobelCallback,NULL,QT_PUSH_BUTTON, 0); 

这些是主要功能中最重要的部分。 我们将探索Callback函数。 每个Callback更改其状态变量以调用另一个名为applyFilters的函数,以便将激活的滤镜添加到输入图像:

void grayCallback(int state, void* userData) 
{ 
   applyGray= true; 
   applyFilters(); 
} 
void bgrCallback(int state, void* userData) 
{ 
   applyGray= false; 
   applyFilters(); 
} 

void blurCallback(int state, void* userData) 
{ 
   applyBlur= (bool)state; 
   applyFilters(); 
} 

void sobelCallback(int state, void* userData) 
{ 
   applySobel= !applySobel; 
   applyFilters(); 
} 

applyFilters函数检查每个过滤器的状态变量:

void applyFilters(){ 
   Mat result; 
   img.copyTo(result); 
   if(applyGray){ 
         cvtColor(result, result, COLOR_BGR2GRAY); 
   } 
   if(applyBlur){ 
         blur(result, result, Size(5,5));     
   } 
   if(applySobel){ 
         Sobel(result, result, CV_8U, 1, 1);  
   } 
   imshow("Lena", result); 
} 

要将颜色更改为灰色,我们使用cvtColor函数,该函数接受三个参数:输入图像、输出图像和颜色转换类型。

最有用的颜色空间转换如下:

  • RGB 或 BGR 变为灰色(COLOR_RGB2GRAYCOLOR_BGR2GRAY)
  • RGB 或 BGR 到 YcrCb(或 YCC)(COLOR_RGB2YCrCbCOLOR_BGR2YCrCb)
  • RGB 或 BGR 到 HSV(COLOR_RGB2HSVCOLOR_BGR2HSV)
  • RGB 或 BGR 到 Luv(COLOR_RGB2LuvCOLOR_BGR2Luv)
  • 灰度到 RGB 或 BGR(COLOR_GRAY2RGBCOLOR_GRAY2BGR)

我们可以看到,密码很容易记住。

OpenCV works by default with the BGR format, and the color conversion is different for RGB and BGR, even when converted to gray. Some developers think that R+G+B/3 is true for gray, but the optimal gray value is called luminosity and has the formula 0,21R + 0,72G + 0,07*B.

模糊滤镜已在上一节中进行了描述,最后,如果参数applySobel为真,我们将应用 Sobel 滤镜。 Sobel 滤波器是使用 Sobel 算子获得的图像导数,通常用于检测边缘。 OpenCV 允许我们生成不同的核大小的导数,但最常见的是用 3x3 核来计算x导数或y导数。

最重要的 Sobel 参数如下:

  • 输入图像
  • 输出图像
  • 输出图像深度(CV_8UCV_16UCV_32FCV_64F)
  • 导数x的阶数
  • 导数y的阶数
  • 内核大小(默认为 3)

要生成 3x3 内核和一阶x阶导数,我们必须使用以下参数:

Sobel(input, output, CV_8U, 1, 0);

以下参数用于y阶导数:

Sobel(input, output, CV_8U, 0, 1);      

在我们的示例中,我们同时使用xy导数,覆盖输入。 以下代码片段显示如何同时生成xy导数,在第四个和第五个参数中添加1

Sobel(result, result, CV_8U, 1, 1); 

同时应用xy导数的结果如下图所示,应用于 Lena 图片:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jzpCoQve-1681961622338)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/639c9969-7c38-46c6-ac73-e9d1f88776b1.png)]

OpenGL 支持

OpenCV 包括 OpenGL 支持。 OpenGL 是一个图形库,作为标准集成在几乎所有图形卡中。 OpenGL 允许我们绘制 2D 到复杂的 3D 场景。 OpenCV 包括 OpenGL 支持,因为在许多任务中表示 3D 空间非常重要。 要在 OpenGL 中支持窗口,我们必须在使用namedWindow调用创建窗口时设置WINDOW_OPENGL标志。

下面的代码创建了一个支持 OpenGL 的窗口,并绘制了一个旋转平面,我们将在该平面上显示网络摄像机帧:

Mat frame; 
GLfloat angle= 0.0; 
GLuint texture;  
VideoCapture camera; 

int loadTexture() { 

    if (frame.data==NULL) return -1; 

   glBindTexture(GL_TEXTURE_2D, texture);  
   glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); 
   glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); 
   glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 

   glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, frame.cols, frame.rows,0, GL_BGR, GL_UNSIGNED_BYTE, frame.data); 
   return 0; 

} 

void on_opengl(void* param) 
{ 
    glLoadIdentity();   
    // Load frame Texture 
    glBindTexture(GL_TEXTURE_2D, texture);  
    // Rotate plane before draw 
    glRotatef(angle, 1.0f, 1.0f, 1.0f); 
    // Create the plane and set the texture coordinates 
    glBegin (GL_QUADS); 
        // first point and coordinate texture 
     glTexCoord2d(0.0,0.0);  
     glVertex2d(-1.0,-1.0);  
        // second point and coordinate texture 
     glTexCoord2d(1.0,0.0);  
     glVertex2d(+1.0,-1.0);  
        // third point and coordinate texture 
     glTexCoord2d(1.0,1.0);  
     glVertex2d(+1.0,+1.0); 
        // last point and coordinate texture 
     glTexCoord2d(0.0,1.0);  
     glVertex2d(-1.0,+1.0); 
    glEnd(); 

} 

int main(int argc, const char** argv) 
{ 
    // Open WebCam 
    camera.open(0); 
    if(!camera.isOpened()) 
        return -1; 

    // Create new windows 
    namedWindow("OpenGL Camera", WINDOW_OPENGL); 

    // Enable texture 
    glEnable( GL_TEXTURE_2D );
    glGenTextures(1, &texture); 
    setOpenGlDrawCallback("OpenGL Camera", on_opengl); 
    while(waitKey(30)!='q'){ 
        camera >> frame; 
        // Create first texture 
        loadTexture();     
        updateWindow("OpenGL Camera"); 
        angle =angle+4; 
    } 
    // Destroy the windows 
    destroyWindow("OpenGL Camera"); 
    return 0; 
}

让我们来理解一下代码!

第一个任务是创建所需的全局变量,我们在其中存储视频捕获、保存帧以及控制动画角度平面和 OpenGL 纹理:

Mat frame; 
GLfloat angle= 0.0; 
GLuint texture;  
VideoCapture camera; 

在我们的主要功能中,我们必须创建摄像机捕获以检索摄像机帧:

camera.open(0); 
    if(!camera.isOpened()) 
        return -1; 

如果摄像机正确打开,我们可以使用WINDOW_OPENGL标志创建支持 OpenGL 的窗口:

// Create new windows 
namedWindow("OpenGL Camera", WINDOW_OPENGL);

在我们的示例中,我们希望在平面中绘制来自网络摄像头的图像;然后,我们需要启用 OpenGL 纹理:

// Enable texture 
glEnable(GL_TEXTURE_2D); 

现在,我们已经准备好在窗口中使用 OpenGL 进行绘制,但是我们需要像典型的 OpenGL 应用一样设置一个绘制 OpenGL 回调。 OpenCV 提供了setOpenGLDrawCallback函数,该函数有两个参数-窗口名称和回调函数:

setOpenGlDrawCallback("OpenGL Camera", on_opengl); 

定义了 OpenCV 窗口和回调函数后,我们需要创建一个循环来加载纹理,更新调用 OpenGL 绘图回调的窗口内容,最后更新角度位置。 要更新窗口内容,我们使用 OpenCV 函数 UPDATE WINDOW,并将窗口名称作为参数:

while(waitKey(30)!='q'){ 
        camera >> frame; 
        // Create first texture 
        loadTexture(); 
        updateWindow("OpenGL Camera"); 
        angle =angle+4; 
    } 

当用户按下Q键时,我们就处于循环中。在编译我们的应用示例之前,我们需要定义loadTexture函数和我们的on_opengl回调绘制函数。 loadTexture函数将我们的Mat帧转换为 OpenGL 纹理图像,以便在每个回调绘图中加载和使用。 在将图像作为纹理加载之前,我们必须确保帧矩阵中有数据,并检查数据变量对象是否为空:

if (frame.data==NULL) return -1; 

如果矩阵帧中有数据,则可以创建 OpenGL 纹理绑定并将 OpenGL 纹理参数设置为线性插值:

glGenTextures(1, &texture); 

glBindTexture(GL_TEXTURE_2D, texture); 
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); 
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);

现在,我们必须定义像素在矩阵中的存储方式,并使用 OpenGLglTexImage2D函数生成像素。 需要注意的是,默认情况下,OpenGL 使用 RGB 格式,OpenCV 使用 BGR 格式,我们必须在此函数中设置正确的格式:

glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, frame.cols, frame.rows,0, GL_BGR, GL_UNSIGNED_BYTE, frame.data); 
    return 0; 

现在,当我们在主循环中调用updateWindow时,我们只需要在每次回调时完成绘制平面。 我们使用常用的 OpenGL 函数,然后加载标识 OpenGL 矩阵以重置之前的所有更改:

glLoadIdentity();   

我们还必须将框架纹理带到记忆中:

    // Load Texture 
    glBindTexture(GL_TEXTURE_2D, texture);  

在绘制平面之前,我们将所有变换应用于场景。 在我们的示例中,我们将沿1,1,1轴旋转平面:

    // Rotate plane 
    glRotatef(angle, 1.0f, 1.0f, 1.0f); 

现在我们已经正确设置了绘制平面的场景,我们将绘制四边形的面(具有四个顶点的面),并为此使用glBegin (GL_QUADS)命令:

// Create the plane and set the texture coordinates 
    glBegin (GL_QUADS); 

接下来,我们将绘制一个以第二0,0位置为中心的平面,该平面的大小为 2 个单位。 然后,我们必须使用glTextCoord2DglVertex2D函数定义要使用的纹理坐标和顶点位置:

    // first point and coordinate texture 
 glTexCoord2d(0.0,0.0);  
 glVertex2d(-1.0,-1.0);  
    // seccond point and coordinate texture 
 glTexCoord2d(1.0,0.0);  
 glVertex2d(+1.0,-1.0);  
    // third point and coordinate texture 
 glTexCoord2d(1.0,1.0);  
 glVertex2d(+1.0,+1.0); 
    // last point and coordinate texture 
 glTexCoord2d(0.0,1.0);  
 glVertex2d(-1.0,+1.0); 
    glEnd(); 

This OpenGL code becomes obsolete, but it is appropriated to understand better the OpenCV and OpenGL integration without complex OpenGL code. By way of an introduction to modern OpenGL, read Introduction to Modern OpenGL, from Packt Publishing.

我们可以在下图中看到结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C3r8k2BX-1681961622338)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/b91c5f48-2dd1-4d2d-a3a2-2174123b7983.png)]

简略的 / 概括的 / 简易判罪的 / 简易的

在本章中,我们学习了如何使用 OpenGL 创建不同类型的用户界面来显示图像或 3D 界面。 我们学习了如何创建滑块和按钮,或者如何在 3D 中绘图。 我们也通过原生 OpenCV 学习了一些基本的图像处理过滤器,但是有一些新的开源过滤器可以让我们添加更多功能,比如 CVUI(https://dovyski.github.io/cvui/)或 OpenCVGUI(https://damiles.github.io/OpenCVGUI/)。

在下一章中,我们将构建一个完整的照片工具应用,在其中我们将应用到目前为止所学的所有知识。 通过图形用户界面,我们将学习如何对输入图像应用多个滤镜。

四、深入研究直方图和过滤器

在上一章中,我们学习了 OpenCV 中使用 Qt 库或本机库的用户界面的基础知识;我们还学习了如何使用高级 OpenGL 用户界面。 我们了解了基本的颜色转换和允许我们创建第一个应用的过滤器。 本章将向您介绍以下概念:

  • 直方图和直方图均衡化
  • 查找表
  • 模糊和中间模糊
  • 精明过滤器
  • 图像颜色均衡
  • 了解图像类型之间的转换

在我们了解了 OpenCV 和用户界面的基础知识之后,我们将在本章中创建我们的第一个完整的应用,一个基本的照片工具,并涵盖以下主题:

  • 生成 CMake 脚本文件
  • 创建图形用户界面
  • 计算和绘制直方图
  • 直方图均衡
  • Lomography 相机效果
  • 卡通化效果

这个应用将帮助我们理解如何从头开始创建整个项目,并理解直方图的概念。 我们将结合使用滤镜和查找表,了解如何均衡彩色图像的直方图并创建两种效果。

技术要求

本章要求您熟悉 C++ 编程语言的基础知识。 本章使用的所有代码都可以从以下 giHub 链接下载:https://github.com/PacktPublishing/Building-Computer-Vision-Projects-with-OpenCV4-and-CPlusPlus/tree/master/Chapter04。 该代码可以在任何操作系统上执行,尽管它只在 Ubuntu 上进行了测试。

请查看以下视频,了解实际操作中的代码:
http://bit.ly/2Sid17y

生成 CMake 脚本文件

在开始创建源文件之前,我们将生成CMakeLists.txt文件,以允许我们编译、构造和执行项目。 下面的 CMake 脚本简单而基本,但足以编译和生成可执行文件:

cmake_minimum_required (VERSION 3.0)

PROJECT(Chapter4_Phototool)

set (CMAKE_CXX_STANDARD 11)

# Requires OpenCV
FIND_PACKAGE( OpenCV 4.0.0 REQUIRED )
MESSAGE("OpenCV version : ${OpenCV_VERSION}")

include_directories(${OpenCV_INCLUDE_DIRS})
link_directories(${OpenCV_LIB_DIR})

ADD_EXECUTABLE(${PROJECT_NAME} main.cpp)
TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${OpenCV_LIBS})

第一行表示生成项目所需的最低 CMake 版本,第二行设置我们可以用作${PROJECT_NAME}变量的项目名称,第三行设置所需的 C++ 版本;在我们的示例中,我们需要C++ 11版本,如下面的代码片段所示:

cmake_minimum_required (VERSION 3.0)

PROJECT(Chapter4_Phototool)

set (CMAKE_CXX_STANDARD 11)

此外,我们还需要 OpenCV 库。 首先,我们需要找到该库,然后我们将显示一条关于使用MESSAGE函数找到的 OpenCV 库版本的消息:

# Requires OpenCV 
FIND_PACKAGE( OpenCV 4.0.0 REQUIRED ) 
MESSAGE("OpenCV version : ${OpenCV_VERSION}") 

如果找到最低版本为 4.0 的库,则我们将在项目中包含头文件和库文件:

include_directories(${OpenCV_INCLUDE_DIRS}) 
link_directories(${OpenCV_LIB_DIR})

现在,我们只需要添加要编译并链接到 OpenCV 库的源文件。 项目名称变量用作可执行文件名,我们只使用一个名为main.cpp的源文件:

ADD_EXECUTABLE(${PROJECT_NAME} main.cpp) 
TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${OpenCV_LIBS})

创建图形用户界面

在开始图像处理算法之前,我们先为应用创建主用户界面。 我们将使用基于 Qt 的用户界面来创建单个按钮。 应用接收一个输入参数来加载要处理的图像,我们将创建四个按钮,如下所示:

  • 显示直方图
  • 均衡化直方图
  • 光照相效果
  • 卡通化效果

我们可以在下面的截图中看到四个结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q5UF7Ant-1681961622338)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/166b014e-80cc-4c24-9e9d-c8948fe9d9a4.png)]

让我们开始开发我们的项目吧。 首先,我们将包含 OpenCV 所需的标头,定义一个图像矩阵来存储输入图像,并创建一个常量字符串以使用 OpenCV 3.0 中已有的新命令行解析器;在该常量中,我们只允许两个输入参数help和所需的图像输入:

// OpenCV includes 
#include "opencv2/core/utility.hpp" 
#include "opencv2/imgproc.hpp" 
#include "opencv2/highgui.hpp" 
using namespace cv; 
// OpenCV command line parser functions 
// Keys accepted by command line parser 
const char* keys = 
{ 
   "{help h usage ? | | print this message}" 
    "{@image | | Image to process}" 
}; 

Main 函数从命令行解析器变量开始;接下来,我们设置关于指令并打印帮助消息。 该行设置最终可执行文件的帮助指令:

int main(int argc, const char** argv) 
{ 
   CommandLineParser parser(argc, argv, keys); 
    parser.about("Chapter 4\. PhotoTool v1.0.0"); 
    //If requires help show 
    if (parser.has("help")) 
   { 
       parser.printMessage(); 
       return 0; 
   } 

如果用户不需要帮助,那么我们必须获取imgFile变量字符串中的文件路径图像,并使用parser.check()函数检查是否添加了所有必需的参数:

String imgFile= parser.get<String>(0); 

// Check if params are correctly parsed in his variables 
if (!parser.check()) 
{ 
    parser.printErrors(); 
    return 0; 
}

现在,我们可以使用imread函数读取图像文件,然后使用namedWindow函数创建稍后将在其中显示输入图像的窗口:

// Load image to process 
Mat img= imread(imgFile); 

// Create window 
namedWindow("Input"); 

加载图像并创建窗口后,我们只需要为界面创建按钮,并将它们与回调函数链接起来;每个回调函数都在源代码中定义,我们将在本章后面解释这些函数。 我们将使用createButton函数创建具有QT_PUSH_BUTTON常量的按钮样式:

// Create UI buttons 
createButton("Show histogram", showHistoCallback, NULL, QT_PUSH_BUTTON, 0); 
createButton("Equalize histogram", equalizeCallback, NULL, QT_PUSH_BUTTON, 0); 
createButton("Lomography effect", lomoCallback, NULL, QT_PUSH_BUTTON, 0); 
createButton("Cartoonize effect", cartoonCallback, NULL, QT_PUSH_BUTTON, 0); 

要完成我们的主要功能,我们显示输入图像并等待按键完成我们的应用:

// Show image 
imshow("Input", img); 

waitKey(0); 
return 0; 

现在,我们只需定义每个回调函数,在下一节中,我们将这样做。

绘制直方图

直方图是变量分布的统计图形表示,它使我们能够理解数据的密度估计和概率分布。 直方图是通过将整个变量值范围划分为一个小范围的值,然后计算每个区间内有多少值来创建的。

如果我们把这个直方图概念应用到一幅图像上,看起来很难理解,但实际上,它很简单。 在灰度图像中,我们的变量值的范围是每个可能的灰度值(从0255),密度是具有该值的图像的像素数。 这意味着我们必须计算图像中值为0的像素数、值为1的像素数,依此类推。

显示输入图像直方图的回调函数为showHistoCallback;此函数计算每个通道图像的直方图,并在新图像中显示每个直方图通道的结果。

现在,检查以下代码:

void showHistoCallback(int state, void* userData) 
{ 
    // Separate image in BRG 
    vector<Mat> bgr; 
    split(img, bgr); 

    // Create the histogram for 256 bins 
    // The number of possibles values [0..255] 
    int numbins= 256; 

    /// Set the ranges for B,G,R last is not included 
    float range[] = { 0, 256 } ; 
    const float* histRange = { range }; 

    Mat b_hist, g_hist, r_hist; 

    calcHist(&bgr[0], 1, 0, Mat(), b_hist, 1, &numbins, &histRange); 
    calcHist(&bgr[1], 1, 0, Mat(), g_hist, 1, &numbins, &histRange); 
    calcHist(&bgr[2], 1, 0, Mat(), r_hist, 1, &numbins, &histRange); 

    // Draw the histogram 
    // We go to draw lines for each channel 
    int width= 512; 
    int height= 300; 
    // Create image with gray base 
    Mat histImage(height, width, CV_8UC3, Scalar(20,20,20)); 

    // Normalize the histograms to height of image 
    normalize(b_hist, b_hist, 0, height, NORM_MINMAX); 
    normalize(g_hist, g_hist, 0, height, NORM_MINMAX); 
    normalize(r_hist, r_hist, 0, height, NORM_MINMAX); 

    int binStep= cvRound((float)width/(float)numbins); 
    for(int i=1; i< numbins; i++) 
    { 
        line(histImage,  
                Point( binStep*(i-1), height-cvRound(b_hist.at<float>(i-1) )), 
                Point( binStep*(i), height-cvRound(b_hist.at<float>(i) )), 
                Scalar(255,0,0) 
            ); 
        line(histImage,  
                Point(binStep*(i-1), height-cvRound(g_hist.at<float>(i-1))), 
                Point(binStep*(i), height-cvRound(g_hist.at<float>(i))), 
                Scalar(0,255,0) 
            ); 
        line(histImage,  
                Point(binStep*(i-1), height-cvRound(r_hist.at<float>(i-1))), 
                Point(binStep*(i), height-cvRound(r_hist.at<float>(i))), 
                Scalar(0,0,255) 
            ); 
    } 

    imshow("Histogram", histImage); 

} 

让我们了解如何提取每个通道直方图以及如何绘制它。 首先,我们需要创建三个矩阵来处理每个输入图像通道。 我们使用一个矢量型变量来存储每个变量,并使用splitOpenCV 函数在这三个通道之间划分输入图像:

// Separate image in BRG 
    vector<Mat> bgr; 
    split(img, bgr); 

现在,我们将定义直方图的箱数,在我们的示例中,每个可能的像素值一个:

int numbins= 256; 

让我们定义变量范围并创建三个矩阵来存储每个直方图:

/// Set the ranges for B,G,R 
float range[] = {0, 256} ; 
const float* histRange = {range}; 

Mat b_hist, g_hist, r_hist;

我们可以使用calcHistOpenCV 函数计算直方图。 此函数有几个参数,顺序如下:

  • 输入图像:在我们的示例中,我们使用存储在bgr向量中的一个图像通道
  • 输入中用于计算直方图的图像数量:在我们的示例中,我们只使用1图像
  • 用于计算直方图的数字通道维度:我们在本例中使用[T0
  • 可选的掩码矩阵。
  • 用于存储计算出的直方图的变量。
  • 直方图维度:这是图像(这里是灰色平面)取值的空间维度,在我们的示例中为1
  • 要计算的条柱数量:在我们的示例中为256个条柱,每个像素值一个
  • 输入变量的范围:在我们的例子中,可能的像素值从0255

每个通道的calcHist函数如下所示:

calcHist(&bgr[0], 1, 0, Mat(), b_hist, 1, &numbins, &histRange ); 
calcHist(&bgr[1], 1, 0, Mat(), g_hist, 1, &numbins, &histRange ); 
calcHist(&bgr[2], 1, 0, Mat(), r_hist, 1, &numbins, &histRange ); 

现在我们已经计算了每个通道直方图,我们必须绘制每个通道直方图并将其显示给用户。 为此,我们将创建一个大小为512x300像素的彩色图像:

// Draw the histogram 
// We go to draw lines for each channel 
int width= 512; 
int height= 300; 
// Create image with gray base 
Mat histImage(height, width, CV_8UC3, Scalar(20,20,20)); 

在将直方图值绘制到图像中之前,我们将标准化最小值0和最大值之间的直方图矩阵;最大值与输出直方图图像的高度相同:

// Normalize the histograms to height of image 
normalize(b_hist, b_hist, 0, height, NORM_MINMAX); 
normalize(g_hist, g_hist, 0, height, NORM_MINMAX); 
normalize(r_hist, r_hist, 0, height, NORM_MINMAX);

现在我们必须从 bin0到 bin1画一条线,依此类推。 在每个 bin 之间,我们必须计算有多少像素;然后,通过宽度除以 bin 的数量计算出一个binStep变量。 每条小线从水平位置i-1i绘制;垂直位置是对应i中的直方图值,并使用颜色通道表示法绘制:

int binStep= cvRound((float)width/(float)numbins); 
    for(int i=1; i< numbins; i++) 
    { 
        line(histImage,  
                Point(binStep*(i-1), height-cvRound(b_hist.at<float>(i-1))), 
                Point(binStep*(i), height-cvRound(b_hist.at<float>(i))), 
                Scalar(255,0,0) 
            ); 
        line(histImage,  
                Point(binStep*(i-1), height-cvRound(g_hist.at<float>(i-1))), 
                Point( binStep*(i), height-cvRound(g_hist.at<float>(i))), 
                Scalar(0,255,0) 
            ); 
        line(histImage,  
                Point(binStep*(i-1), height-cvRound(r_hist.at<float>(i-1))), 
                Point( binStep*(i), height-cvRound(r_hist.at<float>(i))), 
                Scalar(0,0,255) 
            ); 
    } 

最后,我们使用imshow函数显示直方图图像:

    imshow("Histogram", histImage); 

这是lena.png图像的结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nTZecb3S-1681961622338)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/88cfc470-843d-45a6-bfc0-6a0e89826d31.png)]

图像色彩均衡

在本节中,我们将学习如何均衡彩色图像。 图像均衡化,或直方图均衡化,试图获得值分布均匀的直方图。 均衡的结果是增加了图像的对比度。 均衡可以使局部对比度较低的区域获得高对比度,从而分散最频繁的亮度。 当图像非常暗或很亮,并且背景和前景之间的差异非常小时,这种方法非常有用。 使用直方图均衡化,我们增加了对比度和曝光过多或曝光不足的细节。 这项技术在医学图像(如 X 射线)中非常有用。

然而,这种方法有两个主要缺点:背景噪声的增加和有用信号的减少。 我们可以在下面的照片中看到均衡的效果,直方图在增加图像对比度时会发生变化和扩散:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nhKQ2xbW-1681961622339)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/cac3607d-9396-4d09-85c8-df67df5609e9.png)]

让我们实现我们的均衡直方图;我们将在用户界面代码中定义的Callback函数中实现它:

void equalizeCallback(int state, void* userData)
{ 
    Mat result; 
    // Convert BGR image to YCbCr 
    Mat ycrcb; 
    cvtColor(img, ycrcb, COLOR_BGR2YCrCb); 

    // Split image into channels 
    vector<Mat> channels; 
    split(ycrcb, channels); 

    // Equalize the Y channel only 
    equalizeHist(channels[0], channels[0]); 

    // Merge the result channels 
    merge(channels, ycrcb); 

    // Convert color ycrcb to BGR 
    cvtColor(ycrcb, result, COLOR_YCrCb2BGR); 

    // Show image 
    imshow("Equalized", result); 
} 

要均衡彩色图像,我们只需均衡亮度通道。 我们可以对每个颜色通道执行此操作,但结果不可用。 或者,我们可以使用分离单个通道中亮度分量的任何其他彩色图像格式,例如HSVYCrCb。 因此,我们选择YCrCb并使用 Y 通道(亮度)进行均衡。 然后,我们遵循以下步骤:

1.使用cvtColor函数将bgr图像转换或输入为YCrCb

Mat result; 
// Convert BGR image to YCbCr 
Mat ycrcb; 
cvtColor(img, ycrcb, COLOR_BGR2YCrCb); 

2.将YCrCb镜像拆分成不同的通道矩阵:

// Split image into channels 
vector<Mat> channels; 
split(ycrcb, channels); 

3.使用只有两个参数(输入和输出矩阵)的equalizeHist函数,仅均衡 Y 通道中的直方图:

// Equalize the Y channel only 
equalizeHist(channels[0], channels[0]); 

4.合并生成的通道,并将其转换为BGR格式,向用户显示结果:

// Merge the result channels 
merge(channels, ycrcb); 

// Convert color ycrcb to BGR 
cvtColor(ycrcb, result, COLOR_YCrCb2BGR); 

// Show image 
imshow("Equalized", result);

应用于低对比度Lena图像的过程将产生以下结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z8Q8yziV-1681961622339)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/643fd55d-4a37-41d4-abad-899710a5805d.png)]

光照相效果

在本节中,我们将创建另一个图像效果,这是一种在不同的移动应用中非常常见的照片效果,例如 Google Camera 或 Instagram。 我们将了解如何使用查找表(LUT)。 我们将在同一节后面介绍 LUT。 我们将学习如何添加一个覆盖图像,在本例中是一个暗晕,以创建我们想要的效果。 实现此效果的函数是lomoCallback回调,它具有以下代码:

void lomoCallback(int state, void* userData) 
{ 
    Mat result; 

    const double exponential_e = std::exp(1.0); 
    // Create Look-up table for color curve effect 
    Mat lut(1, 256, CV_8UC1); 
    for (int i=0; i<256; i++) 
    { 
        float x= (float)i/256.0;  
        lut.at<uchar>(i)= cvRound( 256 * (1/(1 + pow(exponential_e, -((x-0.5)/0.1)) )) ); 
    } 

    // Split the image channels and apply curve transform only to red channel 
    vector<Mat> bgr; 
    split(img, bgr); 
    LUT(bgr[2], lut, bgr[2]); 
    // merge result 
    merge(bgr, result); 

    // Create image for halo dark 
    Mat halo(img.rows, img.cols, CV_32FC3, Scalar(0.3,0.3,0.3) ); 
    // Create circle  
    circle(halo, Point(img.cols/2, img.rows/2), img.cols/3, Scalar(1,1,1), -1);  
    blur(halo, halo, Size(img.cols/3, img.cols/3)); 

    // Convert the result to float to allow multiply by 1 factor 
    Mat resultf; 
    result.convertTo(resultf, CV_32FC3); 

    // Multiply our result with halo 
    multiply(resultf, halo, resultf); 

    // convert to 8 bits 
    resultf.convertTo(result, CV_8UC3); 

    // show result 
    imshow("Lomography", result); 
} 

让我们来看看 Lomography 效果是如何工作的,以及如何实现它。 Lomography 效果分为不同的步骤,但在我们的示例中,我们用两个步骤制作了一个非常简单的 Lomography 效果:

  1. 通过使用查找表将曲线应用于红色通道来实现颜色操纵效果
  2. 通过在图像上应用深色光晕来实现复古效果

第一步是通过应用以下函数使用曲线变换来处理红色:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z24fwYWw-1681961622339)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/eca8d632-7b74-4b6f-8145-3f3b288b1cf5.png)]

此公式生成一条使暗值更暗、亮值更亮的曲线,其中x是可能的像素值(0255),s是我们在示例中设置为0.1的常量。 较低的常量值生成的值低于128的像素非常暗,高于128的像素非常亮。 接近1的值将曲线转换为直线,并且不会产生我们想要的效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3NYAvA0S-1681961622339)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/849ea6dc-dc83-42af-a348-66f7fd1ea47d.png)]

通过应用 LUT 可以非常容易地实现此功能。 LUT 是返回给定值的预处理值以在内存中执行计算的向量或表。 LUT 是一种常用技术,通过避免重复执行代价高昂的计算来节省 CPU 周期。 我们不是为每个像素调用exponential/divide函数,而是对每个可能的像素值只执行一次(256次),并将结果存储在表中。 因此,我们以牺牲一点内存为代价节省了 CPU 时间。 虽然这在图像尺寸较小的标准 PC 上可能不会有太大的不同,但对于 CPU 受限的硬件(如 Raspberry PI)来说,这就是一个巨大的差异。

例如,在我们的示例中,如果要对图像中的每个像素应用函数,则必须进行widthxhigh操作;例如,在 100x100 像素中,将有 10,000 次计算。 如果我们可以为所有可能的输入预先计算所有可能的结果,我们就可以创建 LUT 表。 在图像中,只有个可能的值作为像素值。 如果我们想要通过应用函数来更改颜色,我们可以预计算出 256 个值,并将它们保存在 LUT 向量中。 在我们的示例代码中,我们定义了E变量,并创建了一个由1行和256列组成的lut矩阵。 然后,我们对所有可能的像素值进行循环,方法是应用我们的公式并将其保存到一个lut变量中:

const double exponential_e = std::exp(1.0); 
// Create look-up table for color curve effect 
Mat lut(1, 256, CV_8UC1); 
Uchar* plut= lut.data; 
for (int i=0; i<256; i++) 
{ 
    double x= (double)i/256.0;  
    plut[i]= cvRound( 256.0 * (1.0/(1.0 + pow(exponential_e, -((x-0.5)/0.1)) )) ); 
} 

正如我们在本节前面提到的,我们不会将该函数应用于所有通道;因此,我们需要使用split函数按通道分割输入图像:

// Split the image channels and apply curve transform only to red channel 
vector<Mat> bgr; 
split(img, bgr); 

然后,我们将lut表变量应用于红色通道。 OpenCV 提供了LUT函数,它有三个参数:

  • 输入图像
  • 查找表矩阵
  • 输出图像

然后,我们对LUT函数和红色通道的调用如下所示:

LUT(bgr[2], lut, bgr[2]); 

现在,我们只需合并我们的计算通道:

// merge result 
merge(bgr, result);

第一步已经完成,我们只需要创建黑暗光环就可以完成我们的效果。 然后,我们创建一个内部有一个白色圆圈的灰色图像,具有相同的输入图像大小:

 // Create image for halo dark 
 Mat halo(img.rows, img.cols, CV_32FC3, Scalar(0.3,0.3,0.3)); 
 // Create circle  
 circle(halo, Point(img.cols/2, img.rows/2), img.cols/3, Scalar(1,1,1), -1);  

查看以下屏幕截图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rXUi8qxz-1681961622340)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/09235ae5-f547-479a-a5aa-3c9e6378b4c5.png)]

如果我们将此图像应用于我们的输入图像,我们将获得从深色到白色的强烈变化;因此,我们可以使用blur滤镜函数对我们的圆形光晕图像应用大模糊,以获得平滑的效果:

blur(halo, halo, Size(img.cols/3, img.cols/3)); 

图像将被更改,以提供以下结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uxzkwbia-1681961622340)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/085d480c-7135-4793-b6e1-60a9d870e5ea.png)]

现在,如果我们必须从第一步开始将这个光环应用于我们的图像,一个简单的方法是将两个图像相乘。 但是,我们必须将输入图像从 8 位图像转换为 32 位浮点数,因为我们需要将值在01范围内的模糊图像与具有整数值的输入图像相乘。 下面的代码将为我们做这件事:

// Convert the result to float to allow multiply by 1 factor 
Mat resultf; 
result.convertTo(resultf, CV_32FC3); 

在转换图像之后,我们只需要将每个元素的每个矩阵相乘:

// Multiply our result with halo 
multiply(resultf, halo, resultf); 

最后,我们将浮点图像矩阵结果转换为 8 位图像矩阵:

// convert to 8 bits 
resultf.convertTo(result, CV_8UC3); 

// show result 
imshow("Lomograpy", result); 

这将是结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HFQRgFKD-1681961622340)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/251222ac-208d-43b0-896f-a02570b2b3eb.png)]

卡通化效果

本章的最后一节致力于创建另一种效果,称为卡通化;此效果的目的是创建一个看起来像卡通的图像。 为此,我们将算法分为两个步骤:边缘检测颜色过滤

cartoonCallback函数定义此效果,其代码如下:

void cartoonCallback(int state, void* userData) 
{ 
    /** EDGES **/ 
    // Apply median filter to remove possible noise 
    Mat imgMedian; 
    medianBlur(img, imgMedian, 7); 

    // Detect edges with canny 
    Mat imgCanny; 
    Canny(imgMedian, imgCanny, 50, 150); 

    // Dilate the edges 
    Mat kernel= getStructuringElement(MORPH_RECT, Size(2,2)); 
    dilate(imgCanny, imgCanny, kernel); 

    // Scale edges values to 1 and invert values 
    imgCanny= imgCanny/255; 
    imgCanny= 1-imgCanny; 

    // Use float values to allow multiply between 0 and 1 
    Mat imgCannyf; 
    imgCanny.convertTo(imgCannyf, CV_32FC3); 

    // Blur the edgest to do smooth effect 
    blur(imgCannyf, imgCannyf, Size(5,5)); 

    /** COLOR **/ 
    // Apply bilateral filter to homogenizes color 
    Mat imgBF; 
    bilateralFilter(img, imgBF, 9, 150.0, 150.0); 

    // truncate colors 
    Mat result= imgBF/25; 
    result= result*25; 

    /** MERGES COLOR + EDGES **/ 
    // Create a 3 channles for edges 
    Mat imgCanny3c; 
    Mat cannyChannels[]={ imgCannyf, imgCannyf, imgCannyf}; 
    merge(cannyChannels, 3, imgCanny3c); 

    // Convert color result to float  
    Mat resultf; 
    result.convertTo(resultf, CV_32FC3); 

    // Multiply color and edges matrices 
    multiply(resultf, imgCanny3c, resultf); 

    // convert to 8 bits color 
    resultf.convertTo(result, CV_8UC3); 

    // Show image 
    imshow("Result", result); 

} 

第一步是检测图像最重要的边缘。 在检测边缘之前,我们需要从输入图像中去除噪声。 有几种方法可以做到这一点。 我们将使用中值滤波器来去除所有可能的小噪声,但我们也可以使用其他方法,例如高斯模糊。 OpenCV 函数是medianBlur,它接受三个参数:输入图像、输出图像和内核大小(内核是一个小矩阵,用于对图像应用一些数学运算,如卷积方法):

Mat imgMedian; 
medianBlur(img, imgMedian, 7); 

在去除任何可能的噪声之后,我们使用Canny滤波器检测强边缘:

// Detect edges with canny 
Mat imgCanny; 
Canny(imgMedian, imgCanny, 50, 150); 

Canny过滤器接受以下参数:

  • 输入图像
  • 输出图像
  • 第一阈值
  • 第二阈值
  • 索贝尔大小孔径
  • 布尔值,指示我们是否需要使用更精确的图像渐变幅度

第一阈值和第二阈值之间的最小值用于边缘链接。 最大值用于查找强边缘的初始分段。 Sobel 大小孔径是算法中将使用的 Sobel 滤波器的核大小。 在检测到边之后,我们将应用一个小的扩张来连接破碎的边:

// Dilate the edges 
Mat kernel= getStructuringElement(MORPH_RECT, Size(2,2)); 
dilate(imgCanny, imgCanny, kernel); 

与我们在 Lomography 效果中所做的类似,如果我们需要将边缘的结果图像与彩色图像相乘,则需要像素值在01范围内。 为此,我们将用精明的结果除以256,并将边缘反转为黑色:

// Scale edges values to 1 and invert values 
imgCanny= imgCanny/255; 
imgCanny= 1-imgCanny; 

我们还将把 Canny 8 无符号位像素格式转换为浮点矩阵:

// Use float values to allow multiply between 0 and 1 
Mat imgCannyf; 
imgCanny.convertTo(imgCannyf, CV_32FC3); 

要获得凉爽的结果,我们可以模糊边缘,要获得平滑的结果线,我们可以应用blur滤镜:

// Blur the edgest to do smooth effect 
blur(imgCannyf, imgCannyf, Size(5,5)); 

算法的第一步已经完成,现在我们要处理颜色。 要获得卡通外观,我们将使用bilateral滤镜:

// Apply bilateral filter to homogenizes color 
Mat imgBF; 
bilateralFilter(img, imgBF, 9, 150.0, 150.0); 

bilateral滤波器是一种在保持边缘的同时降低图像噪声的滤波器。 有了适当的参数,我们将在后面讨论,我们可以得到卡通效果。

bilateral过滤器的参数如下:

  • 输入图像

  • 输出图像

  • 像素邻域的直径;如果设置为负值,则根据 sigma 空间值计算

  • 西格玛颜色值

  • 西格玛坐标空间

With a diameter greater than five, the bilateral filter starts to become slow. With sigma values greater than 150, a cartoonish effect appears.

为了创建更强的卡通效果,我们将像素值相乘并除以,将可能的颜色值截断为 10:

// truncate colors 
Mat result= imgBF/25; 
result= result*25; 

最后,我们必须合并颜色和边缘结果。 然后,我们必须创建一个三通道图像,如下所示:

// Create a 3 channles for edges 
Mat imgCanny3c; 
Mat cannyChannels[]={ imgCannyf, imgCannyf, imgCannyf}; 
merge(cannyChannels, 3, imgCanny3c); 

我们可以将颜色结果图像转换为 32 位浮点图像,然后将每个元素的两个图像相乘:

// Convert color result to float  
Mat resultf; 
result.convertTo(resultf, CV_32FC3); 

// Multiply color and edges matrices 
multiply(resultf, imgCanny3c, resultf); 

最后,我们只需要将图像转换为 8 位,然后向用户显示结果图像:

// convert to 8 bits color 
resultf.convertTo(result, CV_8UC3); 

// Show image 
imshow("Result", result); 

在下一个截图中,我们可以看到输入图像(左图)和应用卡通化效果的结果(右图):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ftHUDXI0-1681961622341)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/c3869e51-f5d3-42f9-aa80-9cfe8a73cc6e.png)]

简略的 / 概括的 / 简易判罪的 / 简易的

在本章中,我们了解了如何创建一个完整的项目,通过应用不同的效果来处理图像。 我们还将彩色图像分割成多个矩阵,以便仅将效果应用于一个通道。 我们了解了如何创建查找表、如何将多个矩阵合并为一个、如何使用Cannybilateral过滤器、如何绘制圆以及如何将图像相乘以获得光晕效果。

在下一章中,我们将学习如何进行对象检测,以及如何将图像分割成不同的部分并对这些部分进行检测。

五、自动光学检查、对象分割和检测

第 4 章深入研究直方图和过滤器中,我们了解了直方图和过滤器,它们使我们能够理解图像操作并创建照片应用。

在本章中,我们将介绍目标分割和检测的基本概念。 这意味着隔离图像中出现的对象以供将来处理和分析。

本章介绍以下主题:

  • 去噪
  • 灯光/背景移除基础知识
  • 阈值设置
  • 用于对象分割的连通分量
  • 寻找轮廓以进行对象分割

许多行业使用复杂的计算机视觉系统和硬件。 计算机视觉试图发现问题并将生产过程中产生的错误降至最低,从而提高最终产品的质量。

在此区域中,此计算机视觉任务的名称为自动光学检测(AOI)。 这个名字出现在印刷电路板制造商的检查中,一个或多个摄像头扫描每个电路,以检测关键故障和质量缺陷。 这个术语被用于其他制造业,这样他们就可以使用光学摄像系统和计算机视觉算法来提高产品质量。 如今,光学检测根据需要使用不同的摄像机类型(红外或 3D 摄像机),复杂的算法被用于数千个行业的不同目的,如缺陷检测、分类等。

技术要求

本章要求熟悉基本的 C++ 编程语言。 本章中使用的所有代码都可以从以下 giHub 链接下载:https://github.com/PacktPublishing/Building-Computer-Vision-Projects-with-OpenCV4-and-CPlusPlus/tree/master/Chapter05。 该代码可以在任何操作系统上执行,尽管它只在 Ubuntu 上进行了测试。

请查看以下视频,了解实际操作中的代码:
http://bit.ly/2DRbMbz

隔离场景中的对象

在本章中,我们将介绍 AOI 算法的第一步,并尝试分离场景中的不同部分或对象。 我们将以三种对象类型(螺丝、密封环和螺母)的对象检测和分类为例,在本章和第 6 章学习对象分类中对其进行开发。

假设我们在一家生产这三种产品的公司。 它们都在同一条载带上。 我们的目标是检测载带中的每个物体,并对每个物体进行分类,以便机器人将每个物体放到正确的架子上:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aoDEpnbQ-1681961622341)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/281eaf92-e813-4226-a3af-54f76527aad8.png)]

在本章中,我们将学习如何隔离每个对象并检测其在图像中的位置(以像素为单位)。 在下一章中,我们将学习如何对每个孤立的物体进行分类,以识别它是螺母、螺丝还是密封圈。

在下面的屏幕截图中,我们显示了我们想要的结果,其中左侧图像中有几个对象。 在右图中,我们用不同的颜色绘制了每一个,显示了不同的特征,如面积、高度、宽度和轮廓大小:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PnBtuwdO-1681961622341)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/31c4d62a-34ed-4f72-9f34-d72c2284d9a0.png)]

为了达到这一结果,我们将遵循不同的步骤,使我们能够更好地理解和组织我们的算法。 我们可以在下图中看到这些步骤:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-odF7zejq-1681961622341)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/b92f8c9b-3ba0-415f-9729-ce2eb0ffde38.png)]

我们的申请将分为两章。 在本章中,我们将开发和理解预处理和分割步骤。 在第 6 章学习对象分类中,我们将提取每个分割对象的特征,并训练我们的机器学习系统/算法如何识别每个对象类。

我们的预处理步骤将分为另外三个子集:

  • 噪声消除
  • 光移除
  • 二值化

在分段步骤中,我们将使用两种不同的算法:

  • 轮廓检测
  • 连通分量提取(标记)

我们可以在下图中看到这些步骤和应用流程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rCgZDGJG-1681961622342)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/d2986d6e-caa4-4199-8616-6c27108224d9.png)]

现在,是开始预处理步骤的时候了,这样我们就可以通过去除噪声和光照效果来获得最佳的二值化图像。 这最大限度地减少了任何可能的检测错误。

为 AOI 创建应用

要创建我们的新应用,我们需要一些输入参数。 当用户执行应用时,除了要处理的输入图像外,所有这些都是可选的。 输入参数如下:

  • 要处理的输入图像
  • 光像图案
  • 轻操作,用户可以在差或除操作之间进行选择
  • 如果用户将0设置为值,则应用差值运算
  • 如果用户将1设置为值,则应用除法运算
  • 分段,用户可以在具有或不具有统计数据的连接组件之间进行选择,并查找等高线方法
  • 如果用户将1设置为输入值,则应用分段的连通分量方法
  • 如果用户将2设置为输入值,则应用带有统计区域的连通分量方法
  • 如果用户将3设置为输入值,则会应用查找等值线方法进行分段

要启用此用户选择,我们将使用带有以下键的command line parser类:

// OpenCV command line parser functions 
// Keys accepted by command line parser 
const char* keys = 
{ 
  "{help h usage ? | | print this message}" 
   "{@image || Image to process}" 
   "{@lightPattern || Image light pattern to apply to image input}" 
   "{lightMethod | 1 | Method to remove background light, 0 difference, 1 div }" 
   "{segMethod | 1 | Method to segment: 1 connected Components, 2 connected components with stats, 3 find Contours }" 
}; 

我们将通过检查参数在main函数中使用command line parser类。 在阅读视频和摄像机部分的第 2 章OpenCV基础简介中解释了CommandLineParser

int main(int argc, const char** argv) 
{ 
  CommandLineParser parser(argc, argv, keys); 
  parser.about("Chapter 5\. PhotoTool v1.0.0"); 
  //If requires help show 
  if (parser.has("help")) 
  { 
      parser.printMessage(); 
      return 0; 
  } 

  String img_file= parser.get<String>(0); 
  String light_pattern_file= parser.get<String>(1); 
  auto method_light= parser.get<int>("lightMethod"); 
  auto method_seg= parser.get<int>("segMethod"); 

  // Check if params are correctly parsed in his variables 
  if (!parser.check()) 
  { 
      parser.printErrors(); 
      return 0; 
  } 

解析命令行用户数据后,我们需要检查输入图像是否已正确加载。 然后,我们加载图像并检查其是否包含数据:

// Load image to process 
  Mat img= imread(img_file, 0); 
  if(img.data==NULL){ 
    cout << "Error loading image "<< img_file << endl; 
    return 0; 
  } 

现在,我们准备创建我们的 AOI 细分流程。 我们将从预处理任务开始。

对输入图像进行预处理

本节介绍在对象分割/检测上下文中可以应用于图像预处理的一些最常见的技术。 预处理是我们在开始工作并从中提取所需信息之前对新图像所做的第一个更改。 通常,在预处理步骤中,我们会尽量减少由相机镜头引起的图像噪声、光线条件或图像变形。 这些步骤在检测图像中的对象或片段时将误差降至最低。

去噪

如果我们不去除噪声,我们可以检测到比我们预期更多的目标,因为噪声通常表示为图像中的小点,并且可以被分割为一个目标。 传感器和扫描仪电路通常会产生此噪声。 这种亮度或颜色的变化可以用不同的类型来表示,例如高斯噪声、尖峰噪声和散粒噪声。

可以使用不同的技术来消除噪音。 这里,我们将使用平滑操作,但根据噪声类型的不同,有些比另一些要好。 中值滤波器通常用于去除胡椒噪声;例如,请考虑下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cj105idO-1681961622342)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/5f8547a4-97cd-4b35-ab3f-418c1a044998.png)]

前一幅图像是带有盐和胡椒噪声的原始输入。 如果我们应用中间模糊,我们会得到一个很棒的结果,其中会丢失一些小细节。 例如,我们丢失了螺钉的边缘,但我们保持了完美的边缘。 请参见下图中的结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VcTtMTFR-1681961622342)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/81ba5bdd-4fe2-4096-b876-79a264593bd9.png)]

如果我们应用盒过滤器或高斯过滤器,噪声不会被去除,而是变得平滑,对象的细节也会丢失和平滑。 有关结果,请参见下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wxgHrntD-1681961622342)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/3f63820d-d5bb-4a37-a38a-1417afaa050b.png)]

OpenCV 提供了medianBlur函数,它需要三个参数:

  • 具有134通道图像的输入图像。 当内核大小大于5时,图像深度只能为CV_8U

  • 输出图像,它是应用与输入相同类型和深度的中间模糊的结果图像。

  • 内核大小,它是大于1的奇数孔径大小,例如 3、5、7 等等。

以下代码用于消除噪音:

  Mat img_noise; 
  medianBlur(img, img_noise, 3); 

使用用于分割的光图案去除背景

在这一部分中,我们将开发一个基本算法,使我们能够使用灯光模式删除背景。 这种预处理可以给我们更好的分割效果。 无噪声的输入图像如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m5Cj2HHR-1681961622343)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/d58d7a47-461c-4730-b2db-8b8f42e75ea7.png)]

如果我们应用一个基本阈值,我们将获得如下图像结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UYhz0Hck-1681961622343)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/f094e1c1-a98d-49f4-96ed-a351af611573.png)]

我们可以看到上面的图像伪像有很多白噪声。 如果我们应用光模式和背景去除技术,我们可以得到令人惊叹的结果,我们可以看到在图像的顶部没有像以前的阈值操作那样的伪影,当我们需要分割的时候,我们会得到更好的结果。 我们可以在下图中看到背景去除和阈值处理的结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2zZdOOOC-1681961622343)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/2cbc71ad-c97e-4593-b7f6-89439d40a694.png)]

现在,我们如何才能将光线从我们的图像中移除呢? 这很简单:我们只需要一张没有任何物体的场景照片,从与拍摄其他图像完全相同的位置和照明条件下拍摄;这是 AOI 中的一种非常常见的技术,因为外部条件是受监督和众所周知的。 本例的图像结果类似于下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MFTLDr2V-1681961622343)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/cbcd7c67-e5c7-4b15-889f-9144c8b696b5.png)]

现在,使用一个简单的数学运算,我们可以移除这个光图案。 删除它有两个选项:

  • 差异 / 不同 / 争执
  • 分歧 / 除 / 部分 / 部门

差异选项是最简单的方法。 如果我们具有光图案L和图像画面I,则由此产生的移除R是它们之间的差值:

R= L-I 

这种划分比较复杂,但同时也很简单。 如果我们具有光图案矩阵L和图像画面矩阵I,则结果去除R如下:

R= 255*(1-(I/L)) 

在这种情况下,我们将图像除以光图案,并假设如果我们的光图案是白色的,并且对象比背景载体带更暗,则图像像素值始终等于或低于光像素值。 我们从I/L得到的结果介于01之间。 最后,我们将该除法的结果倒置以得到相同的颜色方向范围,并将其乘以255以得到0-255范围内的值。

在我们的代码中,我们将使用以下参数创建一个名为removeLight的新函数:

  • 用于移除灯光/背景的输入图像
  • 光图案,Mat
  • 一种方法,用0值表示差,1表示除法

结果是一个没有光/背景的新图像矩阵。 下面的代码通过使用灯光图案实现背景的移除:

Mat removeLight(Mat img, Mat pattern, int method) 
{ 
  Mat aux; 
  // if method is normalization 
  if(method==1) 
  { 
    // Require change our image to 32 float for division 
    Mat img32, pattern32; 
    img.convertTo(img32, CV_32F); 
    pattern.convertTo(pattern32, CV_32F); 
    // Divide the image by the pattern 
    aux= 1-(img32/pattern32); 
    // Convert 8 bits format and scale
    aux.convertTo(aux, CV_8U, 255); 
  }else{ 
    aux= pattern-img; 
  } 
  return aux; 
} 

让我们来探讨一下这个问题。 创建aux变量保存结果后,我们选择用户选择的方法并将参数传递给函数。 如果选择的方法是1,则应用除法。

除法需要 32 位浮点型图像,这样我们就可以划分图像,而不是将数字截断为整数。 第一步是将图像和光图案垫转换为 32 位浮点数。 要转换此格式的图像,可以使用Mat类的convertTo函数。 此函数接受四个参数;输出转换的图像和要转换为所需参数的格式,但您可以定义 alpha 和 beta 参数,这些参数允许您缩放和移动下一个函数后面的值,其中O是输出图像,I是输入图像:

O(xy)=cast<Type>(α*I(xy)+β)

下面的代码将图像更改为 32 位浮点:

// Required to change our image to 32 float for division 
Mat img32, pattern32; 
img.convertTo(img32, CV_32F); 
pattern.convertTo(pattern32, CV_32F); 

现在,我们可以对我们的矩阵执行如上所述的数学运算,方法是将图像除以图案并反转结果:

// Divide the image by the pattern 
aux= 1-(img32/pattern32); 

现在,我们有了结果,但需要将其返回到 8 位深度图像,然后像前面一样使用 Convert 函数转换图像的mat,并使用 alpha 参数从0缩放到255

// Convert 8 bits format 
aux.convertTo(aux, CV_8U, 255); 

现在,我们可以将aux变量与结果一起返回。 对于差分方法,开发非常容易,因为我们不需要转换图像;我们只需要应用模式和图像之间的差异并返回它。 如果我们不假设图案等于或大于图像,则需要进行几次检查并截断值,这些值可以小于0或大于255

aux= pattern-img; 

以下图像是将图像灯光图案应用于我们的输入图像的结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ae4BRznc-1681961622343)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/b80f7abd-d551-4741-9dd0-26c786906b7d.png)]

在我们得到的结果中,我们可以检查光线渐变和可能的伪影是如何被去除的。 但是当我们没有灯光/背景图案时会发生什么呢? 有几种不同的技术可以实现这一点;我们将在这里介绍最基本的一种。 使用滤镜,我们可以创建一个可以使用的滤镜,但有更好的算法来了解图像的背景,其中碎片出现在不同的区域。 这项技术有时需要背景估计图像初始化,我们的基本方法可以很好地发挥作用。 这些高级技术将在第 8 章视频监控、背景建模和形态运算中进行探讨。 为了估计背景图像,我们将使用具有较大内核大小的模糊来应用于我们的输入图像。 这是在光学字符识别*(*OCR)中使用的常用技术,其中字母相对于整个文档较薄且较小,允许我们对图像中的光图案进行近似。 我们可以在左手图像中看到灯光/背景图案重建,在右手图像中可以看到地面实况:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eUxxFzCL-1681961622344)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/fe2720f9-e701-42be-8c36-bb70c2085a63.png)]

我们可以看到灯光图案有一些细微的差异,但这一结果足以去除背景。 当使用不同的图像时,我们也可以在下图中看到结果。 在下图中,描述了应用原始输入图像和使用前一种方法计算的估计背景图像之间的图像差的结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zj03SxD0-1681961622344)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/e7f7eccd-0439-4af3-80d6-e7b018363dad.png)]

calculateLightPattern函数创建此灯光图案或背景近似值:

Mat calculateLightPattern(Mat img) 
{ 
  Mat pattern; 
  // Basic and effective way to calculate the light pattern from one image 
  blur(img, pattern, Size(img.cols/3,img.cols/3)); 
  return pattern; 
} 

此基本函数通过使用相对于图像大小较大的内核大小来对输入图像应用模糊。 从代码来看,它是原来宽度和高度的**。**

**# 阈值设置

在去除背景之后,我们只需要对图像进行二值化,以便将来进行分割。 我们要用 Threshold 来做这件事。 Threshold是一个简单的函数,它将每个像素的值设置为最大值(例如 255)。 如果像素的值大于阈值值,或者如果像素的值小于阈值值,则它将被设置为最小值(0):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KFUDkNWl-1681961622344)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/00c05937-dde7-419e-8b58-988c688d97ea.png)]

现在,我们将使用两个不同的threshold值来应用threshold函数:当我们移除灯光/背景时,我们将使用 30threshold值,因为所有不感兴趣的区域都是黑色的。 这是因为我们应用了背景移除。 当我们不使用灯光移除方法时,我们还将使用中值threshold(140),因为我们使用的是白色背景。 最后一个选项用于允许我们在删除和不删除背景的情况下检查结果:

  // Binarize image for segment 
  Mat img_thr; 
  if(method_light!=2){ 
   threshold(img_no_light, img_thr, 30, 255, THRESH_BINARY); 
  }else{ 
   threshold(img_no_light, img_thr, 140, 255, THRESH_BINARY_INV); 
  } 

现在,我们将继续我们应用中最重要的部分:分割。 这里我们将使用两种不同的方法或算法:连通分量和查找轮廓。

分割我们的输入图像

现在,我们将介绍两种分割阈值图像的技术:

  • 连接的组件
  • 查找等高线

使用这两种技术,我们可以提取图像中出现目标对象的每个感兴趣区域(ROI)。 在我们的例子中,这些是螺母、螺丝和环。

连通分量算法

连通分量算法是一种非常常用的算法,用于分割和识别二值图像中的部分。 连通分量是一种迭代算法,其目的是使用八个或四个连通性像素来标记图像。 如果两个像素具有相同的值并且是相邻像素,则这两个像素是相连的。 在图像中,每个像素都有八个相邻像素:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qmVdvfSY-1681961622344)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/8fe23da4-9a99-47f4-8599-6429da876f48.png)]

四连通性意味着,如果2457邻居的值与中心像素相同,则它们只能连接到中心。 通过八个连接,如果12345678邻居的值与中心像素相同,则可以连接它们。 我们可以从四连通性算法和八连通性算法中看出以下示例的不同之处。 我们将把每种算法应用于下一幅二值化图像。 我们使用了一幅小的9x9图像,并放大显示了连接组件的工作原理以及四连接和八连接之间的区别:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y1UzLChF-1681961622345)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/5a81d547-9b10-475e-8550-02f8af6685e2.png)]

四连通性算法检测到两个对象;我们可以在左图中看到这一点。 八连通性算法只检测一个对象(右侧图像),因为两个对角线像素是相连的。 八连通性处理对角线连通性,这是与四连通性相比的主要区别,因为在四连通性中只考虑垂直和水平像素。 我们可以在下图中看到结果,其中每个对象都有不同的灰色值:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5HbhIobb-1681961622345)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/e7f73902-b25a-4f55-bdc9-ec5099c0fff8.png)]

OpenCV 为我们带来了具有两种不同功能的连通分量算法:

  • connectedComponents(图像,标签,连接性=8,类型=CV_32S)
  • connectedComponentsWithStats(图像,标签,统计信息,质心,连接性=8,类型=CV_32S)

这两个函数都返回一个带有检测到的标签数量的整数,其中 Label0表示背景。 这两个函数之间的区别基本上在于返回的信息。 让我们检查一下每一台的参数。 connectedComponents函数为我们提供以下参数:

  • Image:要标记的输入图像。
  • 标签:与输入图像大小相同的输出垫,其中每个像素都有其标签值,其中所有 OS 表示背景,值为1的像素表示第一个连接的组件对象,依此类推。
  • 连接性:表示我们要使用的连接性的两个可能值84
  • 类型:我们要使用的标签图像的类型。 只允许两种类型:CV32_SCV16_U。 默认情况下,这是CV32_S
  • connectedComponentsWithStats函数还定义了两个参数。 以下是统计数据和质心:
    • Stats:这是一个输出参数,为我们提供每个标签的以下统计值(包括背景):
      • CC_STAT_LEFT:连接组件对象最左侧的x坐标
      • CC_STAT_TOP:连接的组件对象的最上面的y坐标
      • CC_STAT_WIDTH:由其边界框定义的连接组件对象的宽度
      • CC_STAT_HEIGHT:由其边界框定义的连接组件对象的高度
      • CC_STAT_AREA:连接组件对象的像素数(面积)
    • 质心:质心指向每个标签的浮动类型,包括考虑用于另一个连接组件的背景。

在我们的示例应用中,我们将创建两个函数,以便可以应用这两个 OpenCV 算法。 然后,我们将在具有基本连通分量算法的带有彩色对象的新图像中向用户显示所获得的结果。 如果我们使用 stats 方法选择连通组件,我们将在每个对象上绘制返回此函数的相应计算区域。

让我们定义连通组件函数的基本绘图:

void ConnectedComponents(Mat img) 
{ 
  // Use connected components to divide our image in multiple connected component objects
     Mat labels; 
     auto num_objects= connectedComponents(img, labels); 
  // Check the number of objects detected 
     if(num_objects < 2 ){ 
        cout << "No objects detected" << endl; 
        return; 
      }else{ 
       cout << "Number of objects detected: " << num_objects - 1 << endl; 
      } 
  // Create output image coloring the objects 
     Mat output= Mat::zeros(img.rows,img.cols, CV_8UC3); 
     RNG rng(0xFFFFFFFF); 
     for(auto i=1; i<num_objects; i++){ 
        Mat mask= labels==i; 
        output.setTo(randomColor(rng), mask); 
      } 
     imshow("Result", output); 
} 

首先,我们调用 OpenCVconnectedComponents函数,该函数返回检测到的对象数量。 如果对象的数量少于两个,这意味着只检测到背景对象,然后我们不需要绘制任何东西,就可以完成。 如果算法检测到多个对象,我们会显示控制台上检测到的对象数量:

  Mat labels; 
  auto num_objects= connectedComponents(img, labels); 
  // Check the number of objects detected 
  if(num_objects < 2){ 
    cout << "No objects detected" << endl; 
    return; 
  }else{ 
    cout << "Number of objects detected: " << num_objects - 1 << endl;

现在,我们要用不同的颜色在新图像中绘制所有检测到的对象。 在此之后,我们需要创建一个具有相同输入大小和三个通道的新黑色图像:

Mat output= Mat::zeros(img.rows,img.cols, CV_8UC3); 

我们将循环遍历除0值之外的每个标签,因为这是背景:

for(int i=1; i<num_objects; i++){ 

要从标签图像中提取每个对象,我们可以使用比较为每个i标签创建一个蒙版,并将其保存在新图像中:

    Mat mask= labels==i; 

最后,我们使用mask为输出图像设置伪随机颜色:

    output.setTo(randomColor(rng), mask); 
  } 

在循环所有图像之后,我们的输出中有所有检测到的不同颜色的对象,我们只需在窗口中显示输出图像:

imshow("Result", output); 

这是使用不同颜色或灰度值绘制每个对象的结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8jnFQuFD-1681961622345)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/328c60fd-3b54-48c2-b3b2-a4e31cbf1871.png)]

现在,我们将解释如何将连通分量与statsOpenCV 算法一起使用,并在生成的图像中显示更多信息。 以下函数实现此功能:

void ConnectedComponentsStats(Mat img) 
{ 
  // Use connected components with stats 
  Mat labels, stats, centroids; 
  auto num_objects= connectedComponentsWithStats(img, labels, stats, centroids); 
  // Check the number of objects detected 
  if(num_objects < 2 ){ 
    cout << "No objects detected" << endl; 
    return; 
  }else{ 
    cout << "Number of objects detected: " << num_objects - 1 << endl; 
  } 
  // Create output image coloring the objects and show area 
  Mat output= Mat::zeros(img.rows,img.cols, CV_8UC3); 
  RNG rng( 0xFFFFFFFF ); 
  for(auto i=1; i<num_objects; i++){ 
    cout << "Object "<< i << " with pos: " << centroids.at<Point2d>(i) << " with area " << stats.at<int>(i, CC_STAT_AREA) << endl; 
    Mat mask= labels==i; 
    output.setTo(randomColor(rng), mask); 
    // draw text with area 
    stringstream ss; 
    ss << "area: " << stats.at<int>(i, CC_STAT_AREA); 

    putText(output,  
      ss.str(),  
      centroids.at<Point2d>(i),  
      FONT_HERSHEY_SIMPLEX,  
      0.4,  
      Scalar(255,255,255)); 
  } 
  imshow("Result", output); 
} 

让我们来理解一下这段代码。 正如我们在非统计函数中所做的那样,我们调用了 Connected Components 算法,但在这里,我们使用stats函数来执行此操作,以检查我们是否检测到多个对象:

Mat labels, stats, centroids; 
  auto num_objects= connectedComponentsWithStats(img, labels, stats, centroids); 
  // Check the number of objects detected 
  if(num_objects < 2){ 
    cout << "No objects detected" << endl; 
    return; 
  }else{ 
    cout << "Number of objects detected: " << num_objects - 1 << endl; 
  }

现在,我们又有了两个输出结果:统计数据和质心变量。 然后,对于每个检测到的标签,我们将通过命令行显示质心和区域:

for(auto i=1; i<num_objects; i++){ 
    cout << "Object "<< i << " with pos: " << centroids.at<Point2d>(i) << " with area " << stats.at<int>(i, CC_STAT_AREA) << endl; 

您可以检查对 stats 变量的调用,以使用列常量stats.at<int>(I, CC_STAT_AREA)提取区域。 现在,像以前一样,我们在输出图像上绘制标有i的对象:

Mat mask= labels==i; 
output.setTo(randomColor(rng), mask); 

最后,在每个分割对象的质心位置,我们希望在生成的图像上绘制一些信息(如面积)。 为此,我们使用putText函数的 STATS 和质心变量。 首先,我们必须创建一个stringstream,以便可以添加统计区域信息:

// draw text with area 
stringstream ss; 
ss << "area: " << stats.at<int>(i, CC_STAT_AREA); 

然后,我们需要使用putText,使用质心作为文本位置:

putText(output,  
  ss.str(),  
  centroids.at<Point2d>(i),  
  FONT_HERSHEY_SIMPLEX,  
  0.4,  
  Scalar(255,255,255)); 

此函数的结果如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a9H5ELcE-1681961622345)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/146a592f-3304-4484-9b59-003a5e038e9f.png)]

FindContours 算法

在分割对象时,findContours算法是最常用的 OpenCV 算法之一。 这是因为此算法是从 1.0 版开始包含在 OpenCV 中的,它为开发人员提供了更多信息和描述符,包括形状、拓扑组织等:

void findContours(InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset=Point()) 

下面我们来解释一下每个参数:

  • 图像:输入二进制图像。
  • 轮廓:轮廓的输出,其中每个检测到的轮廓都是点的矢量。
  • 层次:这是保存等高线层次的可选输出向量。 这是图像的拓扑结构,在这里我们可以得到每个轮廓之间的关系。 层次表示为四个索引的向量,它们是(下一个轮廓、上一个轮廓、第一个子轮廓、父轮廓)。 在给定的等高线与其他等高线没有关系的情况下,给出负指数。 更详细的解释可以在https://docs.opencv.org/3.4/d9/d8b/tutorial_py_contours_hierarchy.html找到。
  • 模式:此方法用于检索轮廓:
    • RETR_EXTERNAL仅检索外部轮廓。
    • RETR_LIST检索所有等高线,而不建立层次。
    • RETR_CCOMP检索具有两个层次(外部和孔)的所有等高线。 如果另一个对象在一个洞内,则将其放在层次的顶部。
    • RETR_TREE检索所有等高线,在等高线之间创建完整层次。
  • 方法:这允许我们使用近似方法检索轮廓的形状:
    • 如果设置了CV_CHAIN_APPROX_NONE,则不会对等高线应用任何近似,并存储等高线的点。
    • CV_CHAIN_APPROX_SIMPLE压缩所有水平、垂直和对角线段,仅存储起点和终点。
    • CV_CHAIN_APPROX_TC89_L1CV_CHAIN_APPROX_TC89_KCOS应用特尔钦近似算法。
  • 偏移:这是一个可选的点值,用于移动所有等高线。 当我们在 ROI 中工作并需要检索全球位置时,这是非常有用的。

Note: The input image is modified by the findContours function. Create a copy of your image before sending it to this function if you need it.

现在我们已经知道了findContours函数的参数,让我们将其应用到我们的示例中:

void FindContoursBasic(Mat img) 
{ 
  vector<vector<Point> > contours; 
  findContours(img, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); 
  Mat output= Mat::zeros(img.rows,img.cols, CV_8UC3); 
  // Check the number of objects detected 
  if(contours.size() == 0 ){ 
    cout << "No objects detected" << endl; 
    return; 
  }else{ 
    cout << "Number of objects detected: " << contours.size() << endl; 
  } 
  RNG rng(0xFFFFFFFF); 
  for(auto i=0; i<contours.size(); i++){ 
    drawContours(output, contours, i, randomColor(rng)); 
    imshow("Result", output); 
  }
} 

让我们逐行解释我们的实现。

在我们的例子中,我们不需要任何层次结构,所以我们只需要检索所有可能对象的外部轮廓。 为此,我们可以使用RETR_EXTERNAL模式,并使用CHAIN_APPROX_SIMPLE方法进行基本轮廓编码:

vector<vector<Point> > contours; 
vector<Vec4i> hierarchy; 
findContours(img, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); 

就像我们之前看到的连通组件示例一样,我们首先检查我们检索到了多少轮廓。 如果没有,则退出我们的函数:

// Check the number of objects detected 
  if(contours.size() == 0){ 
    cout << "No objects detected" << endl; 
    return; 
  }else{ 
    cout << "Number of objects detected: " << contours.size() << endl; 
  }

最后,我们为每个检测到的物体画出轮廓线。 我们用不同的颜色将其绘制在输出图像中。 为此,OpenCV 提供了一个函数来绘制查找等高线图像的结果:

for(auto i=0; i<contours.size(); i++) 
    drawContours(output, contours, i, randomColor(rng)); 
  imshow("Result", output); 
} 

drawContours函数允许以下参数:

  • Image:绘制轮廓的输出图像。
  • 轮廓:轮廓的向量。
  • 等高线索引:指示要绘制的等高线的数字。 如果该值为负,则绘制所有等高线。
  • 颜色:绘制轮廓的颜色。
  • 厚度:如果为负值,则用所选颜色填充轮廓。
  • Line type:这指定我们是要使用抗锯齿绘制,还是要使用其他绘制方法。
  • Hierarchy:这是一个可选参数,只有在要绘制一些等高线时才需要。
  • 最大级别:这是一个可选参数,只有当 Hierarchy 参数可用时才会考虑该参数。 如果设置为0,则只绘制指定的等高线。 如果为1,该函数还会绘制当前等高线和嵌套的等高线。 如果将其设置为2,则算法将绘制所有指定的等高线层次。
  • 偏移量:这是用于移动等高线的可选参数。

我们的示例结果如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ajqo0vKc-1681961622346)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/4556a9b7-4ec8-46bf-8596-62e84032ce7f.png)]

简略的 / 概括的 / 简易判罪的 / 简易的

在本章中,我们探讨了在摄像机拍摄不同对象的受控情况下对象分割的基础知识。 在这里,我们学习了如何去除背景和光线,以便更好地对图像进行二值化,从而将噪声降至最低。 在对图像进行二值化之后,我们了解了三种不同的算法,这些算法可用于分割和分离图像中的每个对象,从而使我们能够隔离每个对象以操作或提取特征。

我们可以在下图中看到整个过程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b83kjRDC-1681961622346)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/build-cv-proj-opencv4-cpp/img/c0f5d22e-3be2-4d0c-af16-5d39839b899e.png)]

最后,我们提取了一幅图像上的所有对象。 您需要这样做才能继续下一章,在下一章中,我们将提取每个对象的特征来训练机器学习系统。

在下一章中,我们将预测图像中任何物体的类别,然后呼叫机器人或任何其他系统来挑选它们中的任何一个,或者检测不在正确载体带上的物体。 然后,我们将研究如何通知一个人来取它。**

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
OpenCV(Open Source Computer Vision Library)是一款开源的计算机视觉库,专门为图像和视频处理任务设计,广泛应用于学术研究、工业应用以及个人项目中。以下是关于OpenCV的详细介绍: 历史与发展 起源:OpenCV于1999年由英特尔公司发起,旨在促进计算机视觉技术的普及和商业化应用。该项目旨在创建一个易于使用、高效且跨平台的库,为开发者提供实现计算机视觉算法所需的基础工具。 社区与支持:随着时间的推移,OpenCV吸引了全球众多开发者和研究人员的参与,形成了活跃的社区。目前,OpenCV由非盈利组织OpenCV.org维护,并得到了全球开发者、研究机构以及企业的持续贡献和支持。 主要特点 跨平台:OpenCV支持多种操作系统,包括但不限于Windows、Linux、macOS、Android和iOS,确保代码能够在不同平台上无缝运行。 丰富的功能:库中包含了数千个优化过的函数,涵盖了计算机视觉领域的诸多方面,如图像处理(滤波、形态学操作、色彩空间转换等)、特征检测与描述(如SIFT、SURF、ORB等)、物体识别与检测(如Haar级联分类器、HOG、DNN等)、视频分析、相机校正、立体视觉、机器学习(SVM、KNN、决策树等)、深度学习(基于TensorFlow、PyTorch后端的模型加载与部署)等。 高效性能:OpenCV代码经过高度优化,能够利用多核CPU、GPU以及特定硬件加速(如Intel IPP、OpenCL等),实现高速图像处理和实时计算机视觉应用。 多语言支持:尽管OpenCV主要使用C++编写,但它提供了丰富的API绑定,支持包括C、Python、Java、MATLAB、JavaScript等多种编程语言,方便不同领域的开发者使用。 开源与免费:OpenCV遵循BSD开源许可证发布,用户可以免费下载、使用、修改和分发库及其源代码,无需担心版权问题。 架构与核心模块 OpenCV的架构围绕核心模块构建,这些模块提供了不同层次的功能: Core:包含基本的数据结构(如cv::Mat用于图像存储和操作)、基本的图像和矩阵操作、数学函数、文件I/O等底层功能。 ImgProc:提供图像预处理、滤波、几何变换、形态学操作、直方图计算、轮廓发现与分析等图像处理功能。 HighGui:提供图形用户界面(GUI)支持,如图像和视频的显示、用户交互(如鼠标事件处理)以及简单的窗口管理。 VideoIO:负责视频的读写操作,支持多种视频格式和捕获设备。 Objdetect:包含预训练的对象检测模型(如Haar级联分类器用于人脸检测)。 Features2D:提供特征点检测(如SIFT、ORB)与描述符计算、特征匹配与对应关系估计等功能。 Calib3d:用于相机标定、立体视觉、多视图几何等问题。 ML:包含传统机器学习算法,如支持向量机(SVM)、K近邻(KNN)、决策树等。 DNN:深度神经网络模块,支持导入和运行预训练的深度学习模型,如卷积神经网络(CNN)。 应用领域 OpenCV广泛应用于: 科研与教育:作为计算机视觉教学和研究的基础工具,OpenCV简化了算法原型开发与验证过程。 工业自动化:在视觉检测、机器人导航、产品质量控制等工业场景中,OpenCV用于实时图像分析与决策。 安防监控:用于人脸识别、行人检测、行为分析等智能监控系统。 医疗影像分析:在医疗领域,OpenCV可用于医学图像处理、病灶检测、诊断辅助等应用。 自动驾驶:在车辆视觉感知系统中,OpenCV用于道路标志识别、障碍物检测、车道线识别等任务。 多媒体应用:如图像编辑软件、AR/VR应用、游戏开发等,利用OpenCV进行图像和视频处理。 物联网与嵌入式系统:在资源受限的嵌入式设备上,OpenCV提供轻量级的计算机视觉解决方案。 学习与社区资源 OpenCV拥有丰富的官方文档、教程、示例代码以及活跃的开发者社区,包括GitHub、StackOverflow、官方论坛等,为学习和使用OpenCV提供了有力支持。此外,有许多书籍、在线课程、博客文章和研讨会专门讲解OpenCV使用计算机视觉技术。 综上所述,OpenCV作为一款功能强大、高效、跨平台且开源的计算机视觉库,为开发者提供了实现各类图像和视频处理任务所需的工具箱,其广泛的应用领域和活跃的社区支持使之成为计算机视觉领域不可或缺的开发工具。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值