第1章 OpenCV Android开发框架

在开始本书内容之前,笔者假设大家已经有了面向对象语言编程的基本概念,了解了Java语言的基本语法与特征,并且尝试过Android平台上的应用程序开发。本章将主要介绍OpenCV的历史与发展、各个模块的功能说明、如何使用Android Studio IDE来建立OpenCV的开发环境,以及如何整合配置并成功运行和调用OpenCV中的函数实现一个最简单的OpenCV程序演示。如果没有特别说明,那么这里使用的OpenCV版本都是基于OpenCV 3.3 Android SDK。

作为使用最为广泛的计算机视觉开源库,OpenCV在开源社区与英特尔、谷歌等大公司的共同努力之下,发展到今天,已经吸引了全世界各地的开发者编译和使用它实现各种应用程序。而伴随着人工智能时代的到来,作为人工智能眼睛的计算机视觉必然会进一步释放活力,满足市场需要。OpenCV作为计算机视觉开源框架,其在移动端支持Android系统的特性必将进一步深入到移动开发的各种应用场景之中,下面就来开启一段OpenCV学习旅程。

1.1 OpenCV是什么

OpenCV的中文全称是源代码开放的计算机视觉库(Open Source Computer Vision Library),是基于C/C++编写的,是BSD开源许可的计算机视觉开发框架,其开源协议允许在学术研究与商业应用开发中免费使用它。OpenCN支持Windows、Linux、Mac OS、iOS与Android操作系统上的应用开发。在笔者动笔写这本书的时候,其最新版本3.3刚刚发布不久。

1.1.1 OpenCV的历史与发展

在OpenCV孕育发展的过程中,Intel公司做出了巨大的贡献,OpenCV最初是Intel公司的内部项目,随着时间的推移、OpenCV的功能算法得到不断的优化与增强,不过是短短十几年的时间,其已经席卷整个业界,得到众多著名IT公司的大力支持,其中包括大名鼎鼎的机器人公司Willow Garage与搜索引擎公司Google。下面的时间节点对OpenCV的发展都产生过重要影响,具体如下。


❑ 1999年,OpenCV正式立项,那个时候Android智能手机的春天还没有到来。

❑ 2000年,在IEEE的计算机视觉与模式识别大会上OpenCV正式发布Alpha版本。

❑ 2001年~2005年,Intel公司陆续发布了最初的5个Beta测试版本。

❑ 2006年,OpenCV1.0版本正式发布,基于C语言接口SDK调用。

❑ 2008年,OpenCV获得了当时发展如日中天的机器人公司Willow Garage的支持、得到了进一步推广,然而不幸的是,作为机器人业界的传奇公司Willow Garage却在2014宣布倒闭。

❑ 2009年,OpenCV2.0版本正式发布,这是OpenCV发展史上的一个重要里程碑,早期的OpenCV是基于C语言实现的,在2.0的版本中又添加了C++接口,并且对原来的C语言代码进行了优化和整合,以期吸引更多的开发者用户。

❑ 2012年,Intel公司决定把OpenCV开发者社区正式交给开源社区opencv.org运营与维护。

❑ 2014年,OpenCV3.0版本发布。

❑ 2016年,OpenCV3.1与OpenCV3.2版本相继发布,其扩展模块支持集成Google TensorFlow深度学习框架。

❑ 2017年,OpenCV3.3.x版本发布,在Release开发包中增加了DNN(深度神经网络)模块支持。


OpenCV支持Java语言开发的Android SDK最早是始于2010年。在OpenCV3.x版本中,OpenCV更加强调对移动端与嵌入式设备的支持。

(1)编程语言

OpenCV中的这些模块大多数都是基于C/C++完成的,少量的SDK接口模块使用Java、Python等语言开发。在最新开发的OpenCV的核心模块中,C++替代C成为了开发语言。

(2)应用领域

OpenCV自从1.0版本发布以来,立刻吸引了许多公司的目光,被广泛应用于许多领域的产品研发与创新上,相关应用包括卫星地图与电子地图拼接,医学中图像噪声处理、对象检测,安防监控领域的安全与入侵检测、自动监视报警,制造业与工业中的产品质量检测、摄像机标定,军事领域的无人机飞行、无人驾驶与水下机器人等众多领域。

1.1.2 OpenCV模块介绍

OpenCV分为正式的发布版本与扩展模块,Android SDK所对应的是OpenCV的发布版本,其扩展模块的功能可以通过源代码编译的方式进行集成与开发,关于扩展模块的编译与使用已经超出了本书的讨论范围,这里就不再赘述了。下面以OpenCV3.3为例,OpenCV正式发布版本中包含的核心功能模块具体如下。


❑ 二维与三维特征工具箱

❑ 运动估算

❑ 人脸识别

❑ 姿势识别

❑ 人机交互

❑ 运动理解

❑ 对象检测

❑ 移动机器人

❑ 分割与识别

❑ 视频分析

❑ 运动跟踪

❑ 图像处理

❑ 机器学习

❑ 深度神经网络


除上所述的核心功能模块之外,其扩展模块更加的庞大与繁杂。OpenCV Android SDK可以从其官方主页上下载获得,下载地址为:http://opencv.org/opencv-3-3.html,在最下面就可以发现Android SDK的下载链接,点击就可以直接去相关页面上下载最新的Android SDK。

1.1.3 OpenCV Android SDK

OpenCV Android SDK本质上是使用Java语言接口通过JNI技术调用OpenCV C/C++代码完成的算法模块。OpenCV4Android本身并不是一个纯Java语言的计算机视觉库,而是基于OpenCVC++本地代码、通过Java语言接口定义,基于JNI技术实现调用C++本地方法的SDK开发包。所以当你下载好OpenCV Android SDK之后,在它的SDK目录下可以看到如图1-1所示的目录结构。

图1-1

其中,etc目录里面有两个文件夹,里面都是一些XML数据文件,这些XML数据是训练好的HAAR与LBP级联分类器数据;java目录里面是Android SDK相关文件;native里面则是基于C/C++编译好的OpenCV Android平台支持的本地库文件、JNI层开发所需要的头文件及cmake文件,其中库文件大多数以*.a和*.so结尾。而在与SDK同层级的samples目录中则包含了OpenCV Android SDK的一些应用案例教程,以供初学者参考,但是很不幸的是,直到今天为止,这些教程仍然还是基于Eclipse开发环境来演示OpenCV功能,不得不说这是一个小小的缺憾,希望OpenCV社区在后面的Open CV版本中能够更新这些教程,使其基于Android Studio来演示。

此外,OpenCV Android SDK的功能与OpenCV对应发布版本中的功能完全相同,唯一不同的是因为Java语言的关系,Java层封装的接口的参数传递和方法调用,与C++的接口相比有一些差异,这些都是为了更适应Java语言的特性而做出的改动,使得Android开发者更加容易学习与使用OpenCV来解决问题。

1.2 OpenCV Android开发环境搭建

当OpenCV遇到Android时,两者就通过Java SDK或者Android NDK很好地结合在一起了,可是对于广大Android开发者或者OpenCV开发者来说,要想成功地在Android Studio上运行一个类似于Hello World的OpenCV程序,还需要做一些工作,下面就一起来完成这些工作,实现开发环境的搭建。

1.2.1 软件下载与安装

在搭建开发环境之前,首先需要下载和安装如下几个软件开发包。


❑ OpenCV Android SDK 3.3版本

❑ JDK8:64位

❑ Android Studio

❑ Android SDK与NDK开发包


这里需要特别说明一下的是,首先应该安装好JDK,之后再下载安装其他的软件开发包,全部下载安装完毕之后,就可以打开Android Studio——Android集成开发环境(IDE),配置好Android SDK的路径之后,Android Studio(IDE)工具就可以正常使用了。

所下载的OpenCV Android SDK 3.3是一个安装包,只要解压缩到指定磁盘即可,双击解压缩好的目录就可以看到1.1.3节中提到的几个目录与层次结构。在本书最后的案例开发中会涉及与使用NDK的开发包,这里暂时只要将其安装好即可。


版本问题

使用Android Studio与Android SDK、NDK开发时,同样的代码在不同的版本上运行可能会出现一些兼容性问题。因为项目实际需要或者个人偏好,大家使用的版本可能不尽相同,这里说一下本书所用的版本,具体如下。


❑ Android Studio 3.0

❑ Android SDK 26

❑ Android NDK r13b

1.2.2 环境搭建

环境搭建的整个过程可以分为如下四步。

1.新建Android项目

打开Android Studio IDE,选择【File】→【New Project... 】,结果如图1-2所示。

图1-2

把项目默认名称修改为OpencvDemo,然后点击【Next】按钮,结果如图1-3所示。

图1-3

这里支持的最小版本是Android 14(Android 4.0版本),继续点击【Next】按钮,结果如图1-4所示。

图1-4

默认选择Emtpy Activity,点击【Next】→【Finish】,之后就会得到一个新建的默认Android版本的Hello World程序,如果一切顺利的话,就可以真机运行,查看效果。

2.导入OpenCV Android SDK依赖项

选择【File】→[New...]→【Import Module... 】,打开对话框之后,选择解压缩好的OpenCV Android SDK目录中的sdk\java,模块名称会自动显示出当前OpenCV的版本信息,如图1-5所示。

图1-5

点击【Next】→【Finish】,完成导入。然后再选择【File】→【Project Structure...】打开依赖项添加对话框,选择最右侧的【+】按钮,完成添加之后如图1-6所示。

图1-6

点击【OK】按钮,结束。

3.复制本地依赖项OpenCV库文件

把目录结构导航从【Android】切换到【Projects】,如图1-7所示。

图1-7

选择app下面的libs,然后把OpenCV Android SDK目录native\libs下面的所有文件与文件夹全部复制到libs中去,最后删除所有以*.a结尾的文件。

4.修改Gradle脚本与编译

在Android Studio中双击打开如下两个Gradle脚本,如图1-8所示。

图1-8

把两个脚本中的minSdkVersion修改为14、targetSdkVersion修改为26,然后保存,如图1-9所示。

图1-9

在Module:app对应的build.gradle脚本中添加如下内容:

        task  nativeLibsToJar(type:  Jar,  description:  'create  a  jar  archive  of  the
    native libs') {
            destinationDir file("$buildDir/native-libs")
            baseName 'native-libs'
            from fileTree(dir: 'libs', include: '**/*.so')
            into 'lib/'
        }

        tasks.withType(JavaCompile) {
            compileTask -> compileTask.dependsOn(nativeLibsToJar)
        }

然后在编译片段添加如下代码:

        implementation  fileTree(dir:  "$buildDir/native-libs",  include:  'native-libs.
    jar')

保存最终修改好的Gradle文件即可。选择【build】→【clean project】,之后再选择【rebuild project】就完成了整个环境变量的配置与编译。可是环境变量配置得是否正确,我们还不能做到心中有数,所以下面通过一个简单的测试程序来验证一下环境配置。

1.2.3 代码测试

为了验证Android Studio的环境配置是否正确,需要调用一下OpenCV的相关API,把一张彩色图像转换为灰度图像,借此来验证Android平台上的OpenCV SDK是否可以正确调用。因此首先要实现图像显示,在Android中,可以通过XML(activity_main. xml)配置文件选择ImageView元素来实现,还需要一个按钮来响应用户操作,这可以通过添加Button元素来实现。在RelativeLayout中,两个元素对应的XML显示如下:

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/process_btn"
            android:text="灰度"/>
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="fitCenter"
            android:id="@+id/sample_img"
            android:src="@drawable/lena"
            android:layout_centerInParent="true"/>

在代码层面实现图像资源的加载,然后交给OpenCV处理,之后的返回结果显示可以由如下3个步骤来实现。

1)首先加载OpenCV的本地库,代码如下:

        private void iniLoadOpenCV() {
            boolean success = OpenCVLoader.initDebug();
            if(success) {
                Log.i(CV_TAG, "OpenCV Libraries loaded...");
            } else {
                Toast.makeText(this.getApplicationContext(),
                "WARNING: Could not load OpenCV Libraries! ",
                Toast.LENGTH_LONG).show();
            }
        }

2)对按钮加上OnClickListener事件响应,代码如下:

        Button processBtn = (Button)this.findViewById(R.id.process_btn);
        processBtn.setOnClickListener(this);

3)在事件响应方法进行处理并显示结果,代码如下:

        @Override
        public void onClick(View v) {
            Bitmap bitmap = BitmapFactory.decodeResource(
                            this.getResources(), R.drawable.lena);
            Mat src = new Mat();
            Mat dst = new Mat();
            Utils.bitmapToMat(bitmap, src);
            Imgproc.cvtColor(src, dst, Imgproc.COLOR_BGRA2GRAY);
            Utils.matToBitmap(dst, bitmap);
            ImageView iv = (ImageView)this.findViewById(R.id.sample_img);
            iv.setImageBitmap(bitmap);
            src.release();
            dst.release();
        }

需要注意的是,在MainActivity中,需要完成接口View.OnClickListener,这样才能保证按钮事件的正确响应。可以在手机上看到运行完整代码的效果。

注意:书中所有完整的源代码都可以在Github上下载,强烈建议运行每个源代码实例,将源代码看作本书的一部分。

1.3 构建演示APP

本节将尝试构建一个用来演示本书所讲内容的APP,希望在其中可以按章节来索引各章节的相关功能演示。在UI设计层面,首先需要设计一个流程,实现从主界面选择各章,然后到对应的各节的代码演示。该流程如图1-10所示。

图1-10

根据上述的流程可知,我们所需要的界面功能与元素如表1-1所示。

表 1-1

根据表1-1所述的UI设计与程序流程,在启动程序之后,首先选择所在章节,然后选择相关的演示程序进行查看,这样的APP结构有利于集成每章相关的演示程序。根据1.2节的内容,我们首先需要在layout目录下创建两个XML文件作为界面2与界面3,然后还需要将activity_main.xml文件中添加的ImageView与Button元素移到界面3中,在activity_main.xml中添加一个ListView元素作为界面1。这一切做好之后,再来看一下activity_main.xml中的ListView XML显示:

        <ListView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/chapter_listView"/>

做好了XML界面编程之后,需要创建如图1-11所示的几个类与包,它们之间的关系通过类图显示。

图1-11

图1-11中各个类的功能说明具体如下。


❑ MainActivity:第一个界面,显示本书的10个章标题列表。

❑ SectionsActivity:第二个界面,显示各章对应的演示程序。

❑ CharpteFrist1Activity:第三个界面,这里是第1章的演示程序,以后各章会创建属于自己的演示程序。

❑ ItemDto:列表的每个条目对应的数据类。

❑ ChapterUtils:工具类,可以获取章节列表。

❑ AppConstants:常量接口,定义各个章节与演示程序的名称。

❑ SectionsListViewAdaptor:Android ListView元素对应的数据模型。

首先要实现ListView的显示且选择事件响应,在事件响应中实现View跳转到指定页面。其中ListView的初始化代码如下:

        private void initListView() {
            ListView listView = (ListView) findViewById(R.id.chapter_listView);
            final SectionsListViewAdaptor commandAdaptor = new SectionsListViewAdaptor
    (this);
            listView.setAdapter(commandAdaptor);
            commandAdaptor.getDataModel().addAll(ChapterUtils.getChapters());
            listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<? > parent, View view, int position,
    long id) {
                    ItemDto dot = commandAdaptor.getDataModel().get(position);
                    goSectionList(dot);
                }
            });
            commandAdaptor.notifyDataSetChanged();
        }

以第一个界面到第二个界面的跳转为例,实现跳转的代码如下:

        private void goSectionList(ItemDto dto) {
            Intent intent = new Intent(this.getApplicationContext(), SectionsActivity.
    class);
            intent.putExtra(AppConstants.ITEM_KEY, dto);
            startActivity(intent);
        }

实现从第二个Activity(SectionActivity)跳转到各个演示程序的功能,首先需要在onCreate方法中对每章内容的演示程序实现ListView显示与选择监听,这部分的代码如下:

        private void initListView(ItemDto dto) {
            ListView listView = (ListView) findViewById(R.id.secction_listView);
            final SectionsListViewAdaptor commandAdaptor = new SectionsListViewAdaptor
    (this);
            listView.setAdapter(commandAdaptor);
              commandAdaptor.getDataModel().addAll(ChapterUtils.getSections((int)dto.
    getId()));
            listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<? > parent, View view, int position,
    long id) {
                    String command = commandAdaptor.getDataModel().get(position).
    getName();
                    goDemoView(command);
                }
            });
            commandAdaptor.notifyDataSetChanged();
        }

然后在goDemoView方法中根据章节内容跳转到不同的演示程序Activity中,跳转到第1章的演示程序代码如下:

        if(command.equals(AppConstants.CHAPTER_1TH_PGM_01)) {
              Intent  intent  =  new  Intent(this.getApplicationContext(),
  CharpteFrist1Activity.class);
            startActivity(intent);
        }

以后针对各章的内容,在此处添加相关的代码即可,这样就实现了代码的集成。本章完整的源代码可以参见上述提到的源代码文件。

1.4 拍照与图像选择

1.3节中,我们成功地构建了一个演示本书内容的APP框架,这里还需要对其进行进一步的细化,因为我们发现该框架还没有拍照或者图像选择功能,无法提供测试图像来演示OpenCV代码的功能,所以本节在1.3节所示代码的基础之上,再加上拍照与图像选择的功能。在Android系统中显示图像,早期一直有一个很大的问题,尤其是对于大的Bitmap对象,常常会因为DVM内存的问题导致OOM(Out of Momery)错误,开玩笑地说,做Java与Android开发如果没有遇到类似的问题,你出门都不好意思跟人打招呼。谷歌官方的做法是通过降采样使用Bitmap的微缩版图像,这里对选择的图像或者拍照所得图像的显示与处理依然沿用此策略。

1.拍照

调用Android拍照功能,拍照并返回图像,代码如下:

        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        fileUri = Uri.fromFile(getSaveFilePath());
        intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
        startActivityForResult(intent, REQUEST_CAPTURE_IMAGE);

2.图像选择

调用Android图片浏览功能,选择一张图片,自动返回图像并显示,代码如下:

        Intent intent = new Intent();
        intent.setType("image/*");
        intent.setAction(Intent.ACTION_GET_CONTENT);
        startActivityForResult(Intent.createChooser(intent,  "图像选择..."),  REQUEST_
    CAPTURE_IMAGE);

3.加载大小合适的图像

首先获取图像的大小,然后得到图像的降采样版本,显示在ImageView对象元素中,杜绝OOM问题的发生,代码如下:

        if(fileUri == null) return;
        ImageView imageView = (ImageView)this.findViewById(R.id.sample_img);
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(fileUri.getPath(), options);
        int w = options.outWidth;
        int h = options.outHeight;
        int inSample = 1;
        if(w > 1000 || h > 1000) {
            while(Math.max(w/inSample, h/inSample) > 1000) {
                inSample *=2;
            }
        }
        options.inJustDecodeBounds = false;
        options.inSampleSize = inSample;
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        Bitmap bm = BitmapFactory.decodeFile(fileUri.getPath(), options);
        imageView.setImageBitmap(bm);

4.处理与显示

调用OpenCV4Android的API对图像进行有目的的处理,处理之后返回处理后的图像并显示。这里我简单地改写一下前面的OpenCV测试程序,使用OpenCV的imread功能来读取图像,完成灰度转换并显示,代码如下:

        Mat src = Imgcodecs.imread(fileUri.getPath());
        if(src.empty()) {
            return;
        }
        Mat dst = new Mat();
        Imgproc.cvtColor(src, dst, Imgproc.COLOR_BGR2GRAY);
        Bitmap bitmap = grayMat2Bitmap(dst);
        ImageView iv = (ImageView)this.findViewById(R.id.sample_img);
        iv.setImageBitmap(bitmap);
        src.release();
        dst.release();

在拍照或者选择图像结束之后,回调onActivityResult()方法,对于照片,可以直接获得文件路径,而对于选择图像,则需要在回调处理中根据图像ID得到相关图像的正确文件路径。此外,这里还涉及后续章节中要详细解释的Mat对象与Bitmap对象。

本书后续章节的每个演示程序基本上都是基于这个顺序来实现的。而且如果没有特别的说明与支持,那么本书所有的图像都是RGB色彩空间、相关的拍照与图像选择代码会在本书后续章节中重复使用,后续章节针对这部分代码将不再重复讲述,主要的篇幅会放在OpenCV4Android相关知识的讲解与学习上。

注意:这里把加载OpenCV库文件放到了主界面对应的onCreate方法中去调用,这样就避免了到处加载OpenCV库文件的烦恼,同时有助于减少程序的代码量。

1.5 小结

本章主要介绍了OpenCV的历史与发展、相关功能模块、开发环境搭建,以及为了后续更好地学习本书知识所做的一些必要的Android知识铺垫、测试程序框架设计与编码实现。有了这些基础,读者才可以更好地学习后续章节所讲的每个知识点,并通过程序与代码来演示应用。

同时,通过本章的学习,即使是之前没有接触过OpenCV与Android编程的读者也对二者会有基本的感性认知,对OpenCV、Android SDK、如何集成配置Android Studio、如何开发与运行代码都有一定的了解与接触,为后续熟练使用IDE来开发使用OpenCV4Android打下牢固基础。希望大家通过本章的学习,可以顺利搭建好开发环境,运行好测试程序,为后续学习打下坚实基础。