使用Java、OpenCV与Quarkus进行图像处理
如果你对计算机视觉感兴趣,你可能已经熟悉OpenCV。这是一个出色的库,几乎包含进行2D和3D处理所需的一切功能。手势识别、人脸检测、运动跟踪,任何与图像处理相关的需求,OpenCV都可以成为你的首选。它基于BSD许可证,因此你可以直接下载并开始使用。
OpenCV是用C语言编写的,也有很好的Java绑定。如果你像我一样是Java开发者,不想涉及所有加载和构建本地绑定的工作,那么请继续阅读。在本文中,我将展示如何将OpenCV与流行的新框架Quarkus一起使用,而无需担心安装库或重新加载整个应用程序。
Quarkus是一个容器优先、Kubernetes原生的框架,由Red Hat发起,完全专注于开发者的愉悦体验。在我看来,最有趣的部分是实时编码功能,我们将在应用程序中看到这一点。即使使用JNI,我也不需要不断构建和重新加载应用程序,只需轻松地继续编码。我真的很喜欢这一点!它让生活更简单。
设置
假设你有一个可用的Java环境,包括Java运行时和Maven或Gradle构建系统。你可能认为需要下载OpenCV库才能使JNI工作等等,但猜猜看,你不需要!Quarkus OpenCV扩展将处理这一切。Quarkus扩展是为核心Quarkus框架启用更多功能的一种方式。Quarkiverse是存放所有扩展的中心。因此,如果你正在寻找一个优秀的框架来生成文档,或使用hibernate-search进行模糊搜索,或集成Amazon服务等等,请自行查看这里。
我们将使用Quarkus-OpenCV扩展。为此,我们首先从code.quarkus.io创建一个简单的Java CLI项目。有几种开始的方式。可以前往code.quarkus.io并在那里创建项目,或运行本地Maven命令,或使用Quarkus CLI。我将采用我最熟悉的方法。让我们从Maven开始。
1
2
3
4
5
6
7
8
|
mvn io.quarkus.platform:quarkus-maven-plugin:2.7.5.Final:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=getting-started \
-Dextensions="resteasy"
cd getting-started
./mvnw quarkus:add-extension -Dextensions="io.quarkiverse.opencv:quarkus-opencv"
|
第一条命令从code.quarkus.io下载启动项目,项目名为getting-started。第二条命令切换到新创建的项目目录。最后,我将Quarkiverse的OpenCV扩展添加到我们的项目中。
现在我们应该有一个可以开始工作的项目了。让我们在IDE中打开它。然后检查pom.xml是否具有正确的依赖项。如果你正在跟随操作,请确保pom.xml中的依赖项列表如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.opencv</groupId>
<artifactId>quarkus-opencv</artifactId>
<version>LATEST</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
|
接下来,由于我删除了rest-easy依赖项,我还将删除REST端点,即org.acme.GreetingResource.java及其测试。现在,我们有一个干净的项目可以开始了。
创建CLI主程序
Quarkus提供了创建命令行应用程序的选项,这非常酷,因为你还可以将这些编译成本地代码。在本博客中,我们只是为了演示目的制作一个简单的应用程序。但如果你正在寻找创建一些严肃的CLI应用程序的涅槃,请查看quarkus.io上使用picocli或JBang的指南。
在我的应用程序中,我尽量保持简单。让我们创建基础的QMain类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package org.acme;
import io.quarkus.runtime.QuarkusApplication;
import io.quarkus.runtime.annotations.QuarkusMain;
@QuarkusMain
public class QMain implements QuarkusApplication {
@Override
public int run(String... args) throws Exception {
// TODO Auto-generated method stub
return 0;
}
}
|
我现在将在终端中运行此应用程序。命令应启动实时编码会话,因为应用程序已启动。
启动实时编码
1
2
|
~/demos/getting-started mvn quarkus:dev
Quarkus started
|
什么是实时编码?只需继续编码,不用担心重新启动应用程序,或添加新的依赖项和配置,或重新构建等等。你几乎不需要每次都重新启动应用程序!
我将让终端保持运行,并返回到QMain中的编码。我将添加两个类级别的属性,一个用于读取图像,另一个用于保存图像。Quarkus通过Microprofile API提供@ConfigProperty。这使我能够将配置属性注入到我的应用程序中。我是这样做的。
1
2
3
4
5
6
7
|
// 在application.properties中设置这些
@ConfigProperty(name = "cli.sourceImagePath")
String testImage;
// 在application.properties中设置这些
@ConfigProperty(name = "cli.targetImagePath")
String targetImage;
|
在我的application.properties中,我添加了这些文件的路径。
1
2
|
cli.sourceImagePath=images/testImage.jpg
cli.targetImagePath=images/testImageDetected-output.jpg
|
一个是cli.sourceImagePath,必须存在才能进行处理。而cli.targetImagePath不需要存在,因为我将把图像保存到那里。在我的情况下,我将图像添加到项目的根目录中,因此路径是images/。
加载和保存图像
现在回到QMain。我将为这些路径添加保存和加载方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/**
* 加载图像
*/
public Mat loadImage(String imagePath) {
Imgcodecs imageCodecs = new Imgcodecs();
return imageCodecs.imread(imagePath);
}
/**
* 将图像保存到targetPath
*/
public void saveImage(Mat imageMatrix, String targetPath) {
Imgcodecs imgcodecs = new Imgcodecs();
imgcodecs.imwrite(targetPath, imageMatrix);
}
|
Mat有两个部分:图像头和数据矩阵。头是常量,但矩阵的大小取决于图像。Mat用于从路径加载图像,并形成我们执行图像操作的基本结构。
定义过滤器接口
为了处理多个过滤器来处理图像,我将定义一个接口,该接口将Mat作为源,处理它,然后返回Mat。这样,我可以在保存图像之前对其应用多个过滤器。
1
2
3
4
5
6
7
8
9
|
package org.acme;
import org.opencv.core.Mat;
public interface Filter {
public Mat process(Mat src);
}
|
连接点
最后,让我们尝试通过将方法调用添加到QMain的run方法来连接结构。并点燃一些乐趣!
1
2
3
4
5
6
7
|
@Override
public int run(String... args) throws Exception {
Mat m = loadImage(testImage);
saveImage(m, targetImage);
return 0;
}
|
很简单,上面我所做的只是加载图像并保存图像。还没有进行处理。如何测试?记住我们运行mvn quarkus:dev的终端。按空格键,程序将继续执行。多么酷啊!无需重建,所有配置、新类等一切都能正常工作。这就是Quarkus带来的开发者乐趣!
检查日志。它声明检测到application.properties的文件更改,此外还列出了已更改的类。刚刚发生了什么?Quarkus运行时刚刚执行了CLI应用程序。这意味着如果我转到cli.targetImagePath,我应该在那里看到一个创建的图像。此时,它将看起来与源图像相同,因为我还没有对其进行任何处理。猜猜看?这正是我计划现在要做的事情。
继续,我今天使用的图像由Unsplash上的Shuttergames提供。
高斯模糊
我创建的第一个过滤器是GaussianBlurfilter。它是一个低通滤波器,意味着它会衰减高频信号。整体的视觉效果将是一个平滑的模糊。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package org.acme;
import org.opencv.core.Mat;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;
public class GaussianBlur implements Filter {
@Override
public Mat process(Mat src) {
if(src != null) {
int MAX_KERNEL_LENGTH = 45;
Mat dst = new Mat();
for (int i = 1; i < MAX_KERNEL_LENGTH; i = i + 2) {
Imgproc.GaussianBlur(src, dst, new Size(i, i), 0, 0);
}
return dst;
}
else throw new IllegalArgumentException("Looking for Mat nothing found!, try passing org.opencv.core.Mat to process");
}
}
|
上面我们定义了一个内核大小。内核是一个矩阵,定义了锚定像素及其周围像素如何根据提供的函数进行更改。内核将定义卷积的大小、权重和位于中心的锚点。process方法获取一个Mat对象,应用具有内核大小的高斯模糊过滤器,最后返回Mat对象。
RGB2Grey
将以下内容添加到QMain的run方法中,以便我可以调用我的过滤器。
1
2
3
4
5
6
7
8
9
|
@Override
public int run(String... args) throws Exception {
Mat m = loadImage(testImage);
m = new GaussianBlur().process(m);
saveImage(m, targetImage);
return 0;
}
|
完美,现在如果我在终端再次按空格键,它应该执行我所有的更改。结果如下所示的模糊。模糊是平滑的,几乎就像覆盖了一个镜头。
接下来,让我们制作另一个过滤器。这次我如何使图像变灰。为此,OpenCV已经提供了一个简单的函数。再次,我使用Filter接口,因此传递一个Mat并接收一个返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package org.acme;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;
public class RGB2Grey implements Filter{
@Override
public Mat process(Mat src) {
if(src != null) {
Mat dst = src;
Imgproc.cvtColor(src, dst, Imgproc.COLOR_RGB2GRAY);
return dst;
}
else throw new IllegalArgumentException("Looking for Mat nothing found!, try passing org.opencv.core.Mat to process");
}
}
|
上面的代码使用ImgProc提供的操作将所有像素从RGB移动到灰度。
1
|
m = new RGB2Grey().process(m);
|
将上述内容添加到我的run方法中,并在终端再次按空格键应再次执行我所有的更改。瞧!我有一个灰度图像。
希望你喜欢这个关于如何一起使用Quarkus和OpenCV的演练。更多示例,请查看带有代码示例的GitHub存储库。