带你读《Java图像处理:基于OpenCV与JVM》之一:基于JavaVM的OpenCV

简介: 本书包含了各种先进的图像处理技术,如图像平滑化、卡通化、素描化,以及使用掩膜对图像的部分区域进行修改。 你将看到如何使用OpenCV解决图像分析的问题,如边缘检测、形状检测等。 最后,本书还介绍了处理网络摄像头以及各种视频流的方法,并提供相应的代码用于实时视频分析。

华章程序员书库

Java图像处理:基于OpenCV与JVM
Java Image Processing Recipes: With OpenCV and JVM

image.png


[法] 尼古拉斯·莫德奇克(Nicolas Modrzyk) 著
魏 兰 潘婉琼 译

第1章

基于JavaVM的OpenCV
几年前,在去上海的旅途中,一位好友送给我一本很厚的书,是介绍OpenCV的。书中包含了海量的图像处理方法、实时视频分析例子和引人入胜的深度解析,于是我迫不及待地配置好环境来测试书中的程序。
众所周知,OpenCV是开源计算机视觉(Open Source Computer Vision)的英文简写。作为一个开源库,OpenCV提供可直接使用的高级图像处理算法,既包括简单易用的高级图像操作,也包括形状识别以及实时视频监测和分析功能。
OpenCV中最核心的内容是多维矩阵对象,叫作Mat。通过本书的学习,Mat将成为我们最熟悉的朋友。在许多攻略中,输入的对象是Mat,处理的内容是Mat,输出的结果也是Mat。
虽然Mat即将成为我们的好朋友,但是作为一个C++对象,它并不是很好相处。你必须重新编译、安装和小心地配置任何使用Mat的新环境。
但是Mat可以被打包。
Mat虽然在本地运行,但它可以被神不知鬼不觉地加载到Java虚拟机中运行。
第1章将通过介绍Java虚拟机中的多种语言让你开始上手使用OpenCV,当然包括Java语言,也包括通俗易懂的Scala语言和谷歌最爱的Kotlin语言。
为了使用同样的方法来运行不同的语言,你会首先(重新)认识一种Java编译工具,叫作Leiningen,之后利用它来运行简单的OpenCV函数。
第1章是第2章的入门基础。第2章的内容是相似的基于JVM的Clojure语言,可以为富有创造性的OpenCV代码带来即时的视觉反馈。

1.1 初识Leiningen

问题定义
有一句名言是“一次编写,随处运行”,也就是说,在不同的机器上,可以用同样简单便捷的方法来编译和运行Java程序。当然,你总是可以使用最原始的javac命令来编译Java代码,然后使用单纯的Java在命令行中运行编译过的代码,但现在已经是21世纪了,我们应该寻找更有效的方法。
无论使用何种编程语言,手动配置工作环境都是一项大工程。而且当你完成配置之后,很难与他人分享胜利果实。
使用编译工具,可以用简单的方法定义项目所需的依赖,同时也可帮助其他用户更快地上手。
接下来,我们介绍一个简单易用的编译工具。
解决方法
Leiningen 是(主要)面向JavaVM的编译工具。它与一些知名的工具有些相似,例如Ant、Maven和Gradle。
当Leiningen命令行安装完成之后,就可以基于模板来创建JavaVM项目,并且毫无顾虑地运行程序了。
本攻略将介绍如何快速安装Leiningen,以及如何使用它运行你的第一个Java程序。
工作原理
首先,把Leiningen安装在你需要的地方,然后用它来创建一个空的Java项目。
注意: 安装Leiningen之前,需要在你的电脑上安装Java 8。由于Java 9通过破坏现有方法来解决旧的问题,我们目前还是选择使用Java 8。
安装Leiningen
Leiningen的网站主页是https://leiningen.org/
在主页上方,可以找到手动安装Leiningen的四个简单步骤。
在MacOS和Unix环境中:
1.下载lein脚本。https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein
2.把它放在你的$PATH变量中,以便shell可以找到它(例如~/bin)。
3.设置脚本为可运行(chmod a+x ~/bin/lein)。
4.在终端运行lein,然后它会下载一个安装包。
在Windows环境中:
1.下载lein.bat批处理脚本。https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein.bat
2.用管理员权限把它放在你的C:/Windows/System32文件夹下。
3.打开命令提示符运行lein,然后它会下载一个安装包。
在Unix环境中,你可以使用包管理器。在MacOS中,Brew有Leiningen的包。
在Windows中,也有一个很好的安装器在https://djpowell.github.io/leiningen-win-installer/
如果你是一个Chocolatey粉丝,Windows也有Chocolatey的包:https://chocolatey.org/packages/Lein
如果你在终端或命令提示符中安装成功,那么你就可以看到已安装工具的版本号。在第一次运行的时候,Leiningen会下载它的内部依赖(internal dependecy),但是之后的运行通常会非常快。

image.png

用Leiningen创建新的包含OpenCV的Java项目
Leiningen通常使用一个文本文件,叫作project.clj,里面有一个简单的图,存储着元数据(metadata)、依赖(dependencies)、插件(plug-ins)和配置(settings)。
当你通过调用lein命令来运行项目时,lein会去project.clj文件中查找该项目的相关信息。
Leiningen自带可直接使用的项目模板,但是为了更好地理解,我们来一步步地学习第一个例子。
对于一个Leiningen Java项目,你需要两个文件:
■一个用来描述项目的project.clj文件
■一个包含Java代码的.java文件,例如Hello.java
第一个项目的目录结构看起来是这个样子的:
image.png

一个目录,两个文件。
为了简化问题,第一个Java例子十分简单。

image.png

接下来,让我们来看一看project.clj文件中的细节:

image.png

这其实是Clojure代码,不过我们把它看作一种领域专用语言(Domain Specific Language,DSL),即使用特定术语描述项目的语言。
为了方便,每种术语在表1-1中有解释。

image.png
image.png

接下来,请创建对应的文件夹和文件目录,复制并粘贴每个文件对应的内容。
完成之后,就可以运行你的第一个Leiningen命令:

image.png

这个命令会根据你的环境,在你的终端或控制台中生成以下内容:

image.png

太棒啦!我们的旅程开始啦!但是,等一等,刚才到底发生了什么呢?
其中包含了一点魔法。Leiningen的run命令会让Leiningen运行一个编译过的Java类main函数。这个被运行的类定义在项目的元数据中,你应该记得,叫作Hello。
在运行Java类之前,我们需要先编译它。默认情况下,Leiningen会在运行run之前进行编译,这也解释了“Compiling...”是从哪里来的。
之后,你可能会注意到,在你的项目中创建了一个叫作target的文件夹,里面包含了一个叫作classes的文件夹和一个Hello.class文件。

image.png

target和classes文件夹是编译过的Java字节码(bytecode)的默认存储地址,这个target文件夹之后会被加入Java运行时的类路径(classpath)。
紧跟着是“lein run”触发的执行阶段,Hello类的主函数中的代码块被执行,并输出信息。

image.png

你可能会问:“如果我有多个Java文件,并且想运行非主函数之外的其他函数呢?”
这是一个非常合理的问题,在第1章中编写和运行不同代码时,你会经常使用这个技巧。
假如你在同样的Java文件夹中写了第二个Java类,叫作Hello2.Java,其中包含更新的旅途内容。

image.png

为了运行Hello2.java的主函数,在调用lein run时,需要加上-m选项,这里m代表着主函数,之后跟着需要运行的Java类的名称。

image.png

这个命令输出以下内容:

image.png

太好啦!根据这些指导,你可以勇往直前,运行你的第一个OpenCV Java程序了。

1.2 编写你的第一个OpenCV Java程序

问题定义
在通过Leiningen设置的Java项目中,直接使用OpenCV库。
在利用OpenCV运行Java代码之前,有一个令人头疼的问题(自己编译OpenCV封装时),希望这一步能够越简单越好。
解决方法
1.1节介绍了Leiningen的基本环境配置。这一节介绍如何添加对OpenCV C++库和Java封装的依赖。
工作原理
第一个OpenCV例子将使用Leiningen项目模板来配置,其中project.clj文件和文件夹已经创建完毕。Leiningen项目模板不需要单独下载,可以通过在创建新项目时使用Leiningen的集成命令new来调用。
为了在你的机器上创建该项目,在命令行中运行lein命令。
无论是在Windows还是Mac中,这个命令都会输出:

image.png


以上命令做了两件事情:
1.创建了一个新的项目文件夹,叫作hellocv。
2.根据名为jvm-opencv的模板,在相关的文件夹中创建了目录和文件。
命令运行完之后,相对简单的项目文件就自动生成了。

image.png

这个看起来好像不是很令人印象深刻,但是这些文件和上一个攻略中的两个文件基本一样,都是一个项目描述文件和一个Java文件。
project.clj文件的内容与之前有所不同:

image.png

也许你会马上注意到有三行从未见过的内容。
首先是repositories区域,这是一个新的区域,用于查找依赖。这里填写的内容是作者存储OpenCV构建文件的公用地址。
OpenCV的核心依赖以及本地依赖都已被编译好,并且上传至该公共区域供你使用。
这两个依赖分别是:
■opencv
■opencv-native
你也许会问,为什么需要两个依赖呢?
opencv-native是OpenCV针对不同平台的C++代码,例如MacOS、Windows或Linux的平台相关依赖。opencv是平台无关的Java封装,用来调用不同平台的C++代码。
当你编译OpenCV时,这也是OpenCV代码传送的方式。
为了方便起见,打包好的opencv-native中包含针对Windows、Linux和MacOS的所有源代码。
HelloCv.java文件中的Java代码位于Java文件夹中,是一个类似于helloworld的简单例子,会直接加载OpenCV的源代码库。内容如下所示:

image.png
image.png

这段代码做了什么呢?
①它告诉Java运行时利用loadLibrary来加载native opencv库。这是使用OpenCV的必要步骤,每次运行你的应用时都要调用一次。
②通过Java对象,创建了一个Mat对象。Mat本质上是一个图像存储器,像矩阵一样,这里我们设置它的尺寸为3×3:高度为3个像素,宽度为3个像素。每个像素的类型是8UC1,这个奇怪的名字代表着包含8个位的无符号(8U)单通道(C1)整数。
③最终输出Mat(矩阵)对象中的内容。
和之前一样,这个项目是可以直接被运行的,无论你使用什么平台,lein run命令都可以完成任务。

image.png


该命令输出以下内容:

image.png

这里的1和0代表着创建的矩阵对象的实际内容。

1.3 自动编译和运行代码

问题定义
虽然lein命令非常通用,你可能还是想在后台启动你的程序,并且在更新代码的时候让你的代码自动运行。
解决方法
Leiningen配有自动插件。启用后,该插件会监视文件模式的变化并触发命令。让我们来试试吧!
工作原理
当你用jvm-opencv模板创建项目时(请参阅1.2节),你会注意到project.clj文件的内容略长于本书中显示的内容。它实际上看起来更像这样:

image.png

多出来的两行被高亮显示出来。一行是项目元数据在:plugins部分增加了lein-auto的插件。
另一行(即:auto部分)定义要监视变化的文件模式,这里所有以Java结尾的文件的变化都会激活自动刷新的子命令。
回到命令行,现在我们将在通常的run命令前添加auto命令,你需要编写下面这样的命令:

image.png

第一次运行它时,它将提供与之前相同的输出,但是会添加一些额外的行:

image.png

不错,请注意,Leiningen命令尚未完成运行,它实际上是在监听文件的变化。
从现在开始,你可以随意修改HelloCv的Java代码中Mat对象的大小。将以下行

image.png

替换为

image.png

更新的代码表示Mat对象现在是5×5矩阵,每个像素仍然由一个字节的整数表示。
然后查看Leiningen命令所在的终端或控制台,你会看到以下正在更新的输出:

image.png


注意这次打印出的Mat对象是由5行5列组成的。

1.4 使用更好的文本编辑器

问题定义
到目前为止,你可能一直在使用你自己的文本编辑器输入代码,但是想要一个更好一些的OpenCV工作环境。
解决方法
虽然这未必是最好的方案,可能有其他的环境让你觉得效率更高,但我发现通过很简单的设置,Github上的Atom编辑器就非常高效。这款编辑器在敲代码时非常好用。
享受使用Atom工作的主要原因之一是图片加载非常快,所以在做与图像有关的项目时,更新的图像可以非常快地自动反映到你的屏幕上。据我所知,这是唯一支持图像显示的文本编辑器。让我们看看它是如何工作的!
工作原理
安装基本的Atom编辑器很简单,你只需到下面的网站下载安装程序即可:
https://atom.io/
Atom不仅是一个很好的编辑器,而且你可以很容易地安装很多新的插件,以使它更符合你的工作风格。
对于OpenCV,我们想添加三个插件:
■一个通用的集成开发环境(Integrated Development Environment,IDE)插件
■一个Java语言插件,它将使用下面的插件
■用于编辑器内终端的插件
这三个插件如图1-1~1-3所示。

image.png

在底部打开的终端会让你输入相同的"lein auto run"命令,因此你不需要额外的命令提示符或者另外的终端窗口来执行Leiningen的自动运行函数。这样你将能让所有的代码都在一个窗口中编写。
理想情况下,Atom布局看起来如图1-4或图1-5所示。

image.png

image.png


请注意,现在针对Java语言的自动补全功能也已经通过Atom的Java插件得到支持了,因此当你输入函数名的时候会看到一个下拉菜单列出可用的函数,如图1-6所示。

image.png

最后,对图像进行的更新,虽然不能被实时地显示出来,但在保存文件时可以看到。如果你在后台打开文件,会看到文件在每次保存都会被刷新,保存是通过OpenCV的imwrite函数完成的。
所以,由于有leiningen auto run在后台一直运行,保存文件时,compilation/run 循环会被触发并更新图像。
图1-7显示了即使没有保存文件外的用户行为,屏幕上的图像是如何在视觉上更新的。


image.png

在本章后续部分,你会看到现在作为参考的内容,即使用submat函数更改Mat对象中部分区域的颜色,这里先把代码片段展示出来。

image.png

现在你可以开始享受使用OpenCV的所有功能了。我们来使用吧。

1.5 学习OpenCV矩阵对象基础知识

问题定义
Mat(矩阵)对象是OpenCV框架的核心,掌握它你可以更加得心应手地使用OpenCV。
解决方法
让我们通过几个核心示例来看看如何创建矩阵对象并查看它们的内容。
工作原理
此攻略需要你完成与前几节相同的配置。
要创建一个每个“点”只有一个通道的简单矩阵,通常用到Mat类中以下三个静态函数中的一个:zeros,eye,ones。
通过表1-2可以更清楚地看到这三个函数的用途。

image.png

如果你之前使用过OpenCV(如果还没有,请相信我),你会记得CV_8UC1是OpenCV对8位无符号字的称呼,每个像素一个通道,所以最终有3×3即9个值。
正如你所料,它的“堂兄”CV_8UC3给每个像素分配了三个通道,因此1×1的Mat对象就具有三个值。在处理RGB图像时你将经常使用三通道的Mat。它也是加载图像时的默认格式。
第一个例子简单地显示了加载每个像素为单通道的Mat对象的三种方法,以及加载每个像素包含三个通道的Mat对象的一种方法。

image.png
image.png


最后一个Mat对象mat4每个像素包含三个通道。如果尝试打印该对象的信息,你将看到一个包含三个0的数组。
CV_8UC1和CV_8UC3是两种常见的像素格式,在CvType类中还定义了许多其他的像素格式。
当进行矩阵之间的计算时,可能还需要每个通道为浮点数的矩阵。以下是实现方式:

image.png

输出矩阵:

image.png

在许多情况下,你可能并不会从头创建矩阵,而是从文件中加载图像。

1.6 从文件加载图像

问题定义
加载图像文件,并把它转换为Mat对象以进行数字操作。
解决方法
OpenCV有一个名为imread的简单函数,用以从文件中读取图像。它通常只需要图像在本地文件系统上的文件路径,但同时这个函数还带有一个缺省的类型参数。让我们看看如何使用不同形式的imread。
工作原理
imread函数位于Imgcodecs类的同名包中。
它的标准用法是简单地给出文件的路径。假设你已从Google搜索下载了猫咪图像并将它存储在images/kittenjpg路径下(如图1-8所示),如下代码给出了如何加载这个图像:

image.png

image.png


如果OpenCV可以找到并正确加载猫咪图像,则输出以下消息到控制台中:

image.png

需要注意的是,如果找不到该文件,OpenCV也不会抛出任何异常或者报告任何错误信息,而是显示加载的Mat对象为空,所以没有行和列:

image.png

你可以根据自己的编码方式,尝试封装检查Mat大小的代码,以确保可以找到图像并正确解码。
这个函数也可以加载灰度图像(如图1-9所示),这是通过传递另外一个参数控制的。

image.png

image.png

这个参数取自同一个Imgcodecs类。
在这里,我们使用IMREAD_GRAYSCALE将图像强制转换为灰度图像并加载到Mat对象中。
除了使用IMREAD_GRAYSCALE外,还可以向imread函数传递其他选项来得到特定的处理通道和图像深度,其中最有用的如表1-3所示。

image.png
image.png

图1-10显示了使用REDUCED_COLOR_8加载得到的图像。

image.png


你可能已经注意到,使用imread加载图像时不需要提供图像的格式。OpenCV会根据文件的扩展名以及文件中的二进制信息自动完成相应的图像解码工作。

1.7 保存图像到文件

问题定义
使用OpenCV保存图像。
解决方法
OpenCV有一个同imread函数相对应的用来写入文件的函数,函数名是imwrite,也在Imgcodecs类中定义。通常情况下,该函数仅使用本地文件系统里指向图像存储位置的文件路径作为参数,但它也可以使用一些参数来修改图像存储的方式。
工作原理
imwrite函数同imread函数工作原理相似,不同之处是它除了路径,还需要一个Mat对象来存储图像。
第一个代码片段简单地实现将以彩色形式加载的猫咪图像存储到文件中。

image.png

图1-11展示了输出的.jpg图片的内容。

image.png

现在,当保存Mat对象时,你也可以仅通过使用一个不同的扩展名来改变存储格式。例如,想要保存为便携式网络图形(Portable Network Graphic,PNG)格式,仅需调用imwrite函数时,使用一个不同的扩展名即可。

image.png

不需要进行图像编码和令人发狂的字节操作,你输出的文件确实是PNG格式。
可以向imwrite函数传递参数,最常见的参数是压缩参数。
例如,按照官方文档:
■对于JPEG,可以使用CV_IMWRITE_JPEG_QUALITY参数,参数值范围为0~100(值越大图像质量越高)。默认值是95。
■对于PNG,可以使用0~9作为压缩程度的参数值,值越大表示图像越小且压缩时间越长。默认值是3。
可以通过使用另一个叫作MatOfInt的OpenCV对象来实现使用压缩参数压缩输出文件,MatOfInt是一个整型矩阵,或者是一个更简单的形式,即数组。

image.png


上段代码实现PNG图片压缩。同时,通过查看文件大小,实际上你可以发现这个PNG文件大小至少减少了10%。

1.8 利用子矩阵修剪图像

问题定义
只保存图像指定的子区域。
解决方法
这篇简短的攻略的主要目标是介绍submat函数。submat的返回值是一个矩阵对象,内容是原图的子矩阵或子区域。
工作原理
读入一张猫咪图片,通过submat来截取我们想要的那部分内容。这个例子使用的猫咪图片如图1-12所示。

image.png

当然,可以使用任何一张你喜欢的猫咪图片。现在,让我们使用imread来读取这个文件。

image.png

根据观察可知,println输出了矩阵对象本身的一些信息。它的大部分信息与内存有关,所以你可以直接访问内存,同时它也显示了这个矩阵对象是否是一个子矩阵。在这个例子中,由于这个矩阵对象是原始图片,所以它的isSubmat值是false。

image.png


如图1-13所示,Atom编辑器中的自动补全功能会向你提示不同版本的submat函数。

image.png

现在我们使用submat函数的第一种形式,输入参数是每一行和每一列的起始和终止值。

image.png

输出的对象显示新创建的矩阵对象确实是一个子矩阵。

image.png


你可以像处理普通矩阵对象那样来处理这个新建的子矩阵,例如可以尝试保存它。

image.png


由于边界值是根据原始猫咪图片精心挑选的,我们可以得到图1-14中的漂亮结果。

image.png

有一件很好的事情是,当你对子矩阵进行了操作之后,原始矩阵也会受到同样的影响。例如,你对子矩阵中猫咪的脸进行了模糊处理,并且保存了整个矩阵(不是子矩阵),那么就只有猫咪的脸会变得模糊。具体操作如下所示:

image.png

blur是org.opencv.imgproc.Imgproc类中的一个核心函数,它的输入参数是size对象,用来指明每个像素模糊区域大小,size越大,模糊的效果也越强。
模糊的结果如图1-15所示,当你仔细看的时候会发现,只有猫咪的脸部被模糊了,这也是我们之前保存的子矩阵的位置。

image.png

你之前也见到过submat函数的其他定义,还有两种方法可以获得子矩阵。
一种是采用两个Range参数,第一个代表行(y或高度)的范围,第二个代表列(x或宽度)的范围,都是使用Range类来创建的。

image.png

另一种方法是使用矩形,首先给出左上角的坐标,然后是矩形的大小。
image.png

后一种方法最常用,因为它最自然。同时,当在图片中检测物体时,你可以用该物体的包围框,它的类型是Rect对象。
值得注意的是,修改子矩阵会破坏原矩阵的效果。如果你想把子矩阵改成蓝色:

image.png

submat3_2.png和submat3_3.png都会变成如图1-16所示的蓝色猫咪脸。

image.png

同时原矩阵也会被变成如图1-17所示的样子!

image.png

这里想表达的观点是,无论在何时何地使用submat函数,一定要小心谨慎,通常情况下,它是一个强有力的图像处理工具。

1.9 从子矩阵生成矩阵

问题定义
让我们来学习如何手动地通过多个子矩阵生成一个完整的矩阵。
解决方法
setTo和copyTo是OpenCV中两个非常重要的函数。setTo可以将一个矩阵中的所有像素设置为指定的颜色,而copyTo可以将一个已有的矩阵复制到另一个矩阵之中。当使用setTo或者copyTo时,你经常需要与子矩阵打交道,即只对矩阵中的一部分进行处理。
为了使用setTo,我们会用到OpenCV的Scalar对象来定义颜色,这里会使用RGB颜色空间的一组值来创建。让我们来看一下具体是怎么工作的。
工作原理
第一个例子使用setTo将多个子矩阵合成一个矩阵,每个子矩阵有不同的颜色。
从彩色子矩阵生成矩阵
首先我们通过RGB值来定义颜色。之前提到过,颜色是通过Scalar对象创建出来的,包含三个整数值,每个值的范围是0~255。
第一个颜色值代表蓝色的深度,第二个值代表绿色的深度,最后一个值代表红色的深度。为了得到红色、绿色或者蓝色,可以把对应的颜色值设为最高值,即255,其他值设为0。
下面的例子介绍了如何得到红色、绿色和蓝色。

为了定义蓝绿色、品红色和黄色,我们把这些颜色当作RGB的补充色。因此把其他通道设置为最大值255,主通道设置为0。
蓝绿色是红色的补充色,所以红色值通道被设为0,而另外两个通道为255:
image.png

品红是绿色的补充色,黄色是蓝色的补充色,它们的值如下所示:

image.png

我们把颜色都设置好了,现在使用这些对象来创建一个包含所有颜色的矩阵。接下来的setColors方法把输入的矩阵中的一行填充为主颜色RGB或补充色CMY。
我们来看一下如何使用setTo将子矩阵设置为给定的Scalar颜色。

image.png
image.png

接下来,我们创建一个包含三个颜色通道的矩阵,并且填充它的第一行和第二行。

image.png

结果是一个包含两行的矩阵,如图1-18所示,每一行都包含不同颜色的子矩阵。

image.png

从图片子矩阵生成矩阵
颜色很棒,但是你也许更希望能处理图像。第二个例子介绍如何使用图像填充子矩阵。
首先创建一个大小为200×200的矩阵和两个子矩阵:一个是主矩阵的上部,一个是主矩阵的下部。

image.png
image.png

然后加载一个图片以创建另一个小矩阵,并把它的大小调整为上部(或下部)的子矩阵大小。这里会引入Imgproc类中的resize函数。

image.png

当然,你可以任意选择其他的图像。这里,假设加载的图像如图1-19所示。

image.png

这个猫咪矩阵被复制到上部子矩阵和下部子矩阵。
请注意,之前设置大小的步骤很关键。复制能够成功,是因为小矩阵和子矩阵的大小是完全相同的,因此复制的时候没有出现任何问题。

image.png

生成的matofpictures.jpg文件包含两只猫咪,如图1-20所示。

image.png

如果你忘了调整小矩阵的大小,那么复制会彻底失败,结果可能会是如图1-21所示的样子。

image.png

1.10 高亮显示图像中的物体

问题定义
一张图片中包含一组物体、动物或者形状,也许是因为你想得到图像中物体的个数,想把它们高亮显示出来。
解决方法
OpenCV提供了一个非常有名的函数叫作Canny,它可以高亮显示图像中的线条。本章的后几节会详细介绍Canny的用法。我们先使用Java来实现一些简单的操作。
OpenCV的Canny函数可以检测灰度矩阵中的轮廓。我们需要做的只是把输入的矩阵转换为灰度图像,剩下的工作将由Canny完成。
通过Core类中的cvtColor函数,OpenCV可以很容易地改变颜色空间。
工作原理
假设你有一张工具图片,如图1-22所示。

image.png

和往常一样,我们把图片加载到矩阵中。

image.png

接下来,使用cvtColor函数来进行颜色转换,它的输入包含源矩阵、目标矩阵和目标颜色空间。颜色空间的常量可以在Imgproc类中找到,它们的名字以COLOR_为前缀。
使用颜色常量COLOR_RGB2GRAY,可以把矩阵变成黑白两色。

image.png

这个黑白图像可以被直接送入Canny中。Canny函数包含以下参数:
■源矩阵
■目标矩阵
■低阈值,使用150.0
■高阈值,通常是低阈值的2倍或3倍
■光圈,3~7之间的一个奇数,我们使用3。光圈值越大,被检测到的轮廓越多
■L2梯度,暂时设置为true
对每一个像素,Canny使用一个卷积矩阵包含一个核心像素和它的邻居像素,得到一个梯度值。如果梯度值大于高阈值,那么它就被检测为边界。如果梯度值在高阈值和低阈值之间,并且有个高阈值和它连接,那么它也会被保留。
接下来,我们来调用Canny函数。

image.png

输出的图片如图1-23所示。

image.png

为了保护眼睛、节省打印机油墨和树木资源,有些时候把矩阵中的白色变成黑色、黑色变成白色会让物体更容易辨认。反色操作可以通过Core类中的bitwise_not函数实现。

image.png

当然,也可以把Canny函数用在更多的猫咪图片中。图1-25~1-27展示了同样的Canny函数用在猫咪图片中的效果。

image.png

image.png

1.11 使用Canny结果作为掩膜

问题定义
Canny的边缘检测非常棒,它的输出还可以被作为掩膜(mask),用于生成一个精美的艺术化图片。
让我们来尝试把Canny的结果画在另一张图片上。
解决方法
当进行复制操作时,可以使用一个叫作掩膜的参数。掩膜是一个单通道的矩阵,值只包含0和1。
当使用掩膜进行复制时,如果掩膜中的像素值是0的话,源矩阵中的像素就不会被复制,如果值是1的话,源像素就会被复制到目标矩阵中。
工作原理
在1.10节攻略中,根据bitwise_not函数输出的结果,我们得到了一个新的矩阵对象。

image.png

如果你决定把kittens输出的话(也许不是一个好主意,因为文件很大),你会看到一堆0和1,这就是掩膜的制作方法。
现在有了掩膜,我们来创建一个叫作target的白色矩阵,作为copy函数的目标参数。

image.png

然后为copy函数加载一个源矩阵,你应该记得,我们需要确定它的大小和copy函数的目标矩阵(也就是target矩阵)的大小一致。
让我们来调整背景对象的大小。

image.png

这样我们就准备好进行复制操作了。

image.png

输出的矩阵如图1-28所示。

image.png

接下来你可以回答这个问题:为什么猫咪是白色的?
正确答案其实是,底层的矩阵在初始化时是纯白色的,参照new Mat(..., WHITE)声明。当掩膜阻碍了一个像素的复制,也就是说掩膜中这个像素对应的值是0时,矩阵原来的颜色就会显示出来,这里是白色,这也是图1-28中的猫咪是白色的原因。你当然可以尝试一个黑色背景的源矩阵,或者是自己选择一个图片。
在接下来的章节,我们将看到更多的例子。

1.12 使用轮廓进行边缘检测

问题定义
在Canny操作的结果中,希望找到一组可绘制的轮廓,并把它们绘制在矩阵中。
解决方法
OpenCV中有两个函数常与Canny函数一同使用:findContours和drawContours。
findContours读入一个矩阵,并在这个矩阵中查找边缘,或者说定义形状的边界。因为原图像可能包含许多颜色和亮度的噪声,你通常需要一个经过预处理的图片,即一个由Canny处理过的黑白矩阵。
drawContours读入findContours的结果,也就是一组轮廓对象,并允许你用具体的特征来绘制这些轮廓,例如绘制线条的粗细和颜色。
工作原理
如同在解决方法中提到的,OpenCV的findContours函数输入一个预处理过的图片,包含以下参数:
1.预处理过的矩阵
2.用于接收轮廓对象的空队列(MatOfPoint)
3.一个分层矩阵,你目前可以忽略它,并把它设置为空矩阵
4.轮廓追踪模式,例如是否建立轮廓之间的关系或返回所有内容
5.存储轮廓的近似类型,例如是绘制所有的点还是只绘制一些关键点
第一步,我们把预处理图片和追踪轮廓一起放在自定义的find_contours函数中。

image.png

该函数返回一组检测到的轮廓,每个轮廓包含一组像素点,用OpenCV的话说,就是一个MatOfPoint对象。
接下来,我们定义一个draw_contours函数,读入源矩阵来找出第一步中得到的每个轮廓的大小,输入还包括我们希望用来绘制边缘的线条粗度。
在OpenCV中绘制轮廓,通常需要一个for循环,并把要绘制的轮廓索引给drawContours函数。

image.png

太棒啦,该攻略最核心的部分已经完成,现在你可以运行它了。可以和之前一样使用猫咪的照片来作为基准输入图像。

image.png

draw-contours的结果如图1-2所示。

image.png

接下来换一种粗度来绘制轮廓,例如,当粗度是3时,结果会有些许不同,如图1-30所示,线条更细一些。

image.png

从现在开始,我们可以使用结果矩阵作为掩膜进行背景复制。
下面的代码取自1-11节。该函数读入一个掩膜,并且用这个掩膜进行复制。

image.png

图1-31显示了掩膜复制的结果,其中轮廓绘制时的粗度为3。

image.png

值得注意的是,第3章将介绍更酷的使用掩膜和背景的方法,用于生成艺术图片,这一节攻略暂时告一段落。

1.13 处理视频流

问题定义
你希望使用OpenCV来对视频流进行实时的图像处理。
解决方法
Java版本的OpenCV提供了一个videoio包,以及一个特定的VideoCapture对象,它提供了多种方法来直接从连接的视频设备中读取矩阵对象。
首先,你会看到如何从视频设备中获取一个特定大小的矩阵对象,然后将矩阵存入文件中。
通过使用帧(frame),你将看到如何将之前学习到的预处理代码应用在实时获取到的图像中。
工作原理
拍摄静止图片
首先介绍do_still_captures函数。它的输入参数是一组需要抓取的帧、每帧间隔的时间以及从哪个camera_id读入图像。
camera_id是连接到你机器的捕获设备索引。通常你会使用0,但是如果你还有其他外接设备的话,就要选择对应的camera_id。
首先创建一个 VideoCapture对象,camera_id作为参数。
然后创建一个空的矩阵对象,把它传入camera.read()函数来读取数据。
这里的矩阵对象是你熟悉的标准OpenCV矩阵Mat,于是你也可以应用那些之前学过的变换。
到目前为止,我们先把每一帧存储好,用时间戳作为文件名。
完成后,你可以通过VideoCapture对象中的release函数来把相机设置回待机模式。
看看以下代码是怎么实现的。

image.png
image.png


调用新建的函数只需填入所需参数,接下来从ID为0的设备中读取10张图片,每间隔1秒拍摄1次。

image.png

如图1-32所示,这10张图片被创建在该项目的video文件夹中。确实,时间过得飞快,现在已经是深夜了。

image.png

实时处理
好吧,坏消息是OpenCV的Java封装不包含将矩阵转为BufferedImage的明确方法,BufferedImage是Java的graphic包中处理图像的对象。
这里不介绍太多细节,假设你需要一个MatToBufferedImage函数来实时处理Java帧,通过把矩阵对象转换为BufferedImage,即可将它渲染为标准的Java GUI对象。
让我们快速地写一个函数,将矩阵转换为标准的Java BufferedImage。

image.png


当你有了这段代码之后,事情就变得简单了起来。但你仍然需要另外一段代码:一个自定义的panel,它继承了Java的Panel类JPanel。
这个自定义的panel,称为MatPanel,包含一个需要绘制的矩阵对象。MatPanel继承Java的JPanel类的方法是,在paint() 函数中直接调用你刚刚见过的函数:MatToBufferedImage。

image.png

好了,标准OpenCV包中缺少的代码已经被实现了,你可以直接创建JFrame来接收矩阵对象。

image.png

本攻略的最后一步是使用一段与do_still_captures函数类似的代码,但并不在几帧之后停下,你将会写一个无限循环来处理视频流。

image.png

图1-33展示了一个日本房间在凌晨1点钟的实时景象,通过JFrame实时渲染。
显然,目标是实时处理矩阵对象,对于你来说一个很好的练习是试着生成图1-34所示的屏幕截图效果。
    

image.png

答案如下所示,你也应该猜到了,这段代码只是将Canny函数应用在视频读取的矩阵对象中。

image.png

1.14 用Scala写OpenCV代码

问题定义
既然你已经可以使用Java写一些OpenCV代码了,并且开始享受它,但此刻你想要使用Scala来减少样板代码。
解决方法
到目前为止,你使用的当前OpenCV设置可以很容易运行任何为JavaVM编译的类。因此,如果你能够编译Scala类,并且正好有Leiningen插件,那么剩下的工作就十分相似了。
那意味着通过到目前为止已经使用的Leiningen设置,你仅需要更新project.clj文件中的项目元数据,该文件存放于几个地方来确保运行正常。
该工作需要两步。第一步,添加Scala编译器和库;第二步,更新目录,使Scala代码文件可以被找到。
工作原理
基本设置
project.clj文件需要在如下重点陈述的几个地方被更新。
**■项目名称,当然那是可选的。
■主类,你可以使用同样的名称,但如果那样做,确保使用lein clean命令删除旧的Java代码。
■接下来添加lein-zinc插件,这是一个集多能于一体的Leiningen插件。
■lein-zinc插件需要在lein执行编译前触发,因此我们需要在项目元数据中的prep-tasks键中添加一步。prep-tasks键负责定义在相似命令执行前需执行的任务。
■最后,将Scala库依赖加入到依赖键中。**
更新的project.clj文件如下。

image.png
image.png

你为Scala建立的新项目文件结构应该看上去如图1-35所示。

image.png


就像你看到的,同Java设置相比没有太大改变,但是需确保你的源文件现在是在scala文件夹中。
为了确保所有的文件都在正确的位置且设置正确,让我们再一次尝试一个简单的OpenCV例子,但这一次使用Scala。
你将像在前面Java示例中做的一样,加载OpenCV本地库。如果你在scala对象定义中的任何地方都会调用loadLibrary,它将被JVM当作静态调用,并且在加载使用Scala最新写的SimpleOpenCV类时加载库。
其余的代码更像是Java代码的直译。

image.png

当编译上述代码时,Scala源代码会在目标文件夹中生成一些Java字节码,就像Java代码生成的方式一样。
因此,你可以像在Java中做的一样来运行Scala代码,或者通过命令行运行:

image.png

在屏幕上,控制台输出预期的OpenCV的3x3矩阵。

image.png

图1-36展示了Scala更新设置元素的全景图。

image.png

模糊
第一个Scala示例的确显得有点太简单了,那么现在让我们在Scala中试试OpenCV的模糊效果。

image.png

就像你看到的,模糊效果在一行中被连续调用多次,可以在同一个矩阵对象上增加模糊效果。
图1-37中这只无聊猫咪被模糊成了图1-38中的模糊无聊猫咪。

image.png

你一定已经在本地机器上尝试了,并且发现Scala设置中两件十分友好的事情。
编译时间缩短了一些,并且实际上可以更快地看到你的OpenCV代码执行。Scala编译器似乎通过增量代码变化确定需要的编译步骤。
此外,尽管静态导入在Java中已存在,但在Scala中它似乎集成得更加自然。
Canny效果
在更多地减少样板代码的尝试中,Scala使导入类和方法变得更简单。
Scala攻略中第三个示例将展示在改变加载的OpenCV矩阵的颜色空间后,如何使用Canny变换。
下面的代码十分整洁,唯一不足的部分是OpenCV的vconcat函数需要java.util.Array并且无法使用本地Scala对象作为参数,因此你将需要使用名为Arrays.asList的Java函数来替代。

image.png

代码中使用了Canny参数以在这个简单的艺术空间中输出一些结果,但这一次并没有很有效地找出边缘。图1-39和图1-40展示了在加载的猫咪图像上使用Canny效果处理前/处理后的结果。
为Java编写的画轮廓示例也被引入到Scala中并且提供了源码,位于本书提供的案例源码库中。现在,这个示例留给读者作为一个简单的练习题。

    
<p style="text-align:center">![image.png](https://ucc.alicdn.com/pic/developer-ecology/0d89c010c260455bbe9970ac10d2ee4b.png)</p>

1.15 用Kotlin写OpenCV代码

问题定义
使用Scala写OpenCV变换程序令人兴奋,但现在谷歌正在力推Kotlin语言,你将会非常喜欢使用Kotlin写OpenCV代码。
解决方法
当然,Leiningen中有Kotlin插件。就像Scala设置一样,你需要再一次在project.clj文件中更新元数据。
你最需要做的是添加Kotlin插件以及访问Kotlin源文件的路径。
工作原理
基本设置
在project.clj文件中需要更新的地方同那些在Scala设置需要更新的地方十分相似,并且已在接下来的代码片段中被高亮标出。

image.png
image.png

因为Kotlin类是通过插件显式地编译到JavaVM字节码中,你可以参考那些到目前为止你已经完成编译的类。
显而易见,第一个测试是检验你是否可以加载一个矩阵对象并且打印它的0和1值。
下面十分简短的Kotlin代码片段实现了上述功能。

image.png


在你执行通常的Leiningen运行命令之前,需要将First.kt文件放到Kotlin文件夹中。

image.png

这个命令输出同样是必要的,展示了正确创建的OpenCV对象并且将它打印到控制台中。

image.png

image.png


这是一个简单的示例。让我们使用Kotlin和OpenCV来完成更加复杂一点的事情吧。
颜色映射
下面这个新示例展示出如何使用Imgproc类中的applyColorMap函数完成不同颜色映射之间的变换,这个示例完全用Kotlin代码实现。

image.png

就像你掌握的那样,Kotlin的构造函数调用不需要使用明确的new关键字,而且就像在Scala中一样,可以使用静态引入方法。
现在你可以从图1-41中的原始输入图像开始看这段代码的运行效果。
你会看到程序创建了三个文件,如图1-42、图1-43和图1-44所展示的三个输出文件所示。

image.png


在Kotlin中,合适的类型转换看上去有一些挑战,但是代码同样是非常紧凑的,就像在Scala中移除了一些样板代码。
用户接口
你想要使用Kotlin的一个主要原因是它那不可思议的tornadofx库,这个库使在GUI框架JavaFX下的JVM中编写简单的用户接口变得更容易。
这样的小应用对于给用户创造调整OpenCV参数的机会并且伪实时地看到结果,是非常有用的。
Kotlin设置
tornadofx库可以被添加到存在于依赖部分的project.clj文件中,如下面提取出的片段所示。

image.png
image.png

由于本攻略的目的是培养你创造性的想法,因此我们不再深入学习如何编写Kotlin程序以及使用tornadofx库编写Kotlin程序。但是你将很快学习到一些如何将这些方法集成到OpenCV中的Kotlin示例。
下面的第一个示例将向你介绍如何引导你的Kotlin代码显示一帧中的一副图像。
仿制用户接口
一个简单的tornadofx应用基本遵循了一个结构,即给定的启动器(Launcher)→应用→视图,如图1-45中的流程图所示。

image.png

有了这张图的概念,我们需要创建三个类。
**■HelloWorld0:UI应用的主视图
■MyApp0:用来发送给JavaFX启动器的JavaFX应用对象
■World0:主类,只会被创建一次,因此使用对象代替类来定义它,以此启动基于JVM的应用**
一个tornadofx中的视图由一个根面板(Root Panel)组成,你可以按照自己的意愿定制JavaFX小部件作为根面板。
**■下面的代码创建一个单一视图,该视图由嵌入在imageview小部件中的图像组成。
■imageview中图像的尺寸由定义小部件的模块设置。
■视图初始化由init{......}模块完成,而且由于根对象无法再一次初始化,因此使用神奇的with函数完成。**

image.png

这段代码的其余部分是标准的tornadofx/javafx样板模板,以此正确启动基于JavaFX的应用。

image.png

如同到目前为止你所完成的那样,通过如下命令使用Leiningen自动模式运行上述代码。

image.png

你的屏幕上将会出现图形化的一帧(图1-46)。

image.png

实际上,这段代码和这一帧有一些不同。在根模块中,通过在合适的地方插入下面的代码片段设置了一个标题。你会找到这是在哪里插入的。

image.png

反馈按钮的用户接口
接下来的示例基于前述示例并且增加了一个按钮,当按下按钮,内部计数器会增加,而且计数器的值会实时显示在屏幕上。
反馈值可以通过SimpleIntegerProperty建立,或者通过javafx.beans包中的Simple-
XXXProperty建立。
该反馈值可以绑定到小部件上,在接下来的示例中,将会绑定到一个标签上,因此标签值与属性值相同。
按钮是你可以用来定义一个处理句柄的UI小部件。句柄代码存在于模块内部或者一个不同的Kotlin函数中。
根据上述目标和解释,让我们开始介绍下面的代码片段。

image.png
image.png

运行计数器应用的结果如图1-47所示。
在点击这个漂亮的按钮几次之后,你会得到如图1-48所示内容。

image.png

模糊应用
这些应用很酷,但这看上去像是创建GUI的课程,而且同OpenCV没有太大关系。
确实如此。
因此,最后一个Kotlin应用基于上述两个示例,介绍如何建立一个模糊应用,其中模糊程度由反馈属性设置。
你需要在Java环境下的图像对象和OpenCV环境下的Mat对象来回转换。下面的示例介绍一种快速转换的方法,通过使用OpenCV的imencode函数实现,该函数将Mat对象编码为字节而无须将它们存储到文件中。
这个模糊应用使用了SimpleObjectProperty类型的变量,该变量随着它的图像化视图更新而变化。
较长的导入列表有些烦人,但你可能不必为自己自定义的应用加入更多的导入。

image.png
image.png

通常情况下,Leiningen在文件改变后为你自动完成全部Kotlin编译工作,模糊应用效果如图1-49所示。

image.png

当你点击增加(increment)按钮后,猫咪图像变得越来越模糊;当你点击减小(decrement)按钮后,它变得越来越清晰。
在本书的代码样本中有更多的tornadofx示例,因此无须犹豫,找出它们来练习。你可能会通过OpenCV方法获得更多的UI。例如一个图像拖拽面板,图像可以根据你的意愿被模糊处理。那听起来不再是无法实现的,是吗?

image.png

第1章写满了攻略,从在基于JavaVM的OpenCV中建立一个小项目开始,逐渐学习更加复杂的图像操作示例,最开始使用Java,最终熟练使用JavaVM运行环境,以使用Scala代码以及含有令人印象深刻的tornadofx库的Kotlin代码。
介绍origami库的大门已经打开,该库是为OpenCV设计的Clojure封装。该环境带给你更加简洁的代码并且更具有交互性,以此来尝试新事物并且变得更加有创造性。是时候兴奋起来了。
**我对未来有兴奋的感觉,而我不知道那看上去会是什么样。但是无论如何,未来将会是我创造的样子。
——Amanda Lindhout**

相关文章
|
8天前
|
Oracle Java 关系型数据库
java体系结构和jvm
java体系结构和jvm
|
1月前
|
Java 编译器 测试技术
滚雪球学Java(03):你知道JDK、JRE和JVM的不同吗?看这里就够了!
【2月更文挑战第12天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!!
103 4
|
2月前
|
算法 Java 关系型数据库
掌握这3个技巧,你也可以秒懂JAVA性能调优和jvm垃圾回收
JVM 是一个虚拟化的操作系统,类似于 Linux 和 Window,只是他被架构在了操作系统上进行接收 class 文件并把 class 翻译成系统识别的机器码进行执行,即 JVM 为我们屏蔽了不同操作系统在底层硬件和操作指令的不同。
21 0
|
1月前
|
存储 Java 数据安全/隐私保护
【JVM】Java虚拟机栈(Java Virtual Machine Stacks)
【JVM】Java虚拟机栈(Java Virtual Machine Stacks)
35 0
|
19天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
57 0
|
1月前
|
存储 缓存 安全
[Java基础]——JVM内存模型
[Java基础]——JVM内存模型
|
1月前
|
算法 Java UED
【JVM】分代收集算法:提升Java垃圾回收效率
【JVM】分代收集算法:提升Java垃圾回收效率
17 0
|
1月前
|
Java
【JVM】深入理解Java引用类型:强引用、软引用、弱引用和虚引用
【JVM】深入理解Java引用类型:强引用、软引用、弱引用和虚引用
83 0
|
1月前
|
存储 安全 Java
【JVM】Java堆 :深入理解内存中的对象世界
【JVM】Java堆 :深入理解内存中的对象世界
49 0
|
1月前
|
存储 安全 前端开发
什么是Java虚拟机(JVM),它的作用是什么?
什么是Java虚拟机(JVM),它的作用是什么?