首页 > Python资料 博客日记

Python 安卓应用构建教程:使用 Kivy 和 AndroidStudio(一)

2024-10-23 03:00:06Python资料围观6

Python资料网推荐Python 安卓应用构建教程:使用 Kivy 和 AndroidStudio(一)这篇文章给大家,欢迎收藏Python资料网享受知识的乐趣

原文:Building Android Apps in Python Using Kivy with Android Studio

协议:CC BY-NC-SA 4.0

一、为 Android 应用开发准备 Kivy

本章介绍了用 Python 构建跨平台应用的 Kivy 框架。它首先向您展示如何在桌面计算机上准备 Kivy 开发环境。我们将创建一个简单的 Kivy 桌面应用。我们还将安装 Buildozer,并使用它从桌面应用构建一个 Android Studio 项目。创建一个 APK 文件,签名后可以安装在 Android 设备上或部署在 Google Play 上。

什么令人失望?

Kivy 是一个跨平台的 Python 框架,用于构建具有自然用户界面(NUI)的应用。由于是跨平台的,Kivy 代码将在 Windows、Linux、Mac、Android 和 IOS 设备上不变地运行。Kivy 的界面是自然的,这意味着任何用户都可以很容易地与界面自然交互。用户不必花费数小时来学习如何使用该界面。交互可以通过带有非触摸屏的光标或使用多点触摸功能来实现。

Kivy 基于许多创建用户界面的低级库。在本书中,将使用简单的直接媒体层(SDL)。SDK 是一个跨平台的库,用于通过 OpenGL 对图形硬件进行底层访问。其他库也是可用的,比如 PyGame。开发人员不会直接与这些库交互,因为 Kivy 是抽象的。它将开发人员从不必要的细节中分离出来,并提供一个简单的界面来访问设备的功能。例如,在 Java 中,开发人员必须编写大量代码才能访问 Android 摄像头并在 Android Studio 中捕捉图像。在 Kivy,这些细节很多都是隐藏的。通过一种非常简单的方式,只需几行代码,用户就可以捕捉图像、上传和播放音频文件、处理触摸事件等等。

Kivy 也是模块化的。这意味着 Kivy 项目可以被组织成许多独立的模块。模块可以在多个项目中使用。在非模块化的库中,没有正在创建的功能的分离,因此我们必须在每个新的应用中为每个特性重复它们。

Kivy 安装

为了构建一个 Kivy 移动应用,最好先构建一个桌面应用。这样,我们至少知道在我们转移到移动应用之前,事情运行良好。这是因为调试桌面应用更容易。因此,本章从在桌面上准备 Kivy 开发环境开始。

我们可以使用 pip 安装程序来安装 Kivy,它从 Python 包索引(PyPI)中获取库来安装最新的稳定版本,或者指定 GitHub 项目的链接来安装其最新的开发版本。我们需要确保 pip 安装正确。如果您打算使用 Python 2,那么在终端中键入pip。对于 Python 3 使用pip3。因为 Python 2 预装在 Ubuntu 中,所以预计pip命令会成功。如果没有,则使用以下命令:

ahmed-gad@ubuntu:-$sudo apt install python-pip

如果您打算使用 Python 3 并且pip3终端命令失败,您必须使用以下命令安装它:

ahmed-gad@ubuntu:-$sudo apt install python3-pip

安装 pip 后,我们可以用它来安装 Kivy。在安装它之前,我们需要确保安装了 Cython。如果之前没有安装 Cython,请使用pippip3安装。以下是安装 Cython for Python 2 的命令:

ahmed-gad@ubuntu:-$pip install cython

之后,我们可以使用下面的命令开始 Kivy 安装。注意,它也安装在 Python 2 中。

ahmed-gad@ubuntu:-$pip install kivy

目前,我们可以在构建移动应用之前构建 Kivy 桌面应用。让我们从构建 hello world 应用开始。应用显示一个显示"Hello World"文本的窗口。

构建简单的桌面 Kivy 应用

构建 Kivy 应用的第一步是创建一个扩展了kivy.app.App类的新类。根据清单 1-1 ,该类被称为TestApp。第二步是覆盖kivy.app.App类中名为build的函数。这个函数放置运行应用时出现在屏幕上的小部件(GUI 元素)。在这个例子中,使用Label类从kivy.uix.label模块创建一个标签,其文本属性被分配给"Hello World"文本。注意,kivy.uix模块保存了 GUI 上出现的所有 Kivy 小部件。最后,定制类被实例化以运行应用。构建这样一个应用的完整代码如清单 1-1 所示。

import kivy.app
import kivy.uix.label

class TestApp(kivy.app.App):

    def build(self):
        return kivy.uix.label.Label(text="Hello World")

app = TestApp()
app.run()

Listing 1-1Build Your First Kivy Application

如果应用保存在名为test.py的文件中,我们可以使用以下命令从终端运行它:

ahmed-gad@ubuntu:-$python test.py

图 1-1 显示了运行应用后出现的窗口。它只是一个简单的窗口,文本在中间。请注意,Test一词显示在窗口的标题栏中。注意,自定义类名是TestApp,由两个字组成,TestApp。当类中有单词App时,Kivy 自动将应用标题设置为它前面的文本,在本例中是Test

图 1-1

清单 1-1 中应用的窗口

可以使用TestApp类构造函数的 title 参数来更改应用的标题。清单 1-2 对清单 1-1 做了两处修改。第一个变化是将应用的标题设置为Hello。第二个变化是我们可以构建一个没有build()功能的应用。删除此函数,类将为空。为此,添加了pass关键字。

import kivy.app

class TestApp(kivy.app.App):
    pass

app = TestApp(title="Hello")
app.run()

Listing 1-2Changing the Application Title

我们目前使用这个build()函数来保存应用小部件,所以通过删除它,应用窗口将是空的,如图 1-2 所示。在接下来的章节中,将讨论 KV 语言在应用中添加窗口小部件。

图 1-2

运行清单 1-2 中的代码后出现的窗口

安装 Buildozer 并创建 buildozer.init 文件

在创建了这样一个简单的桌面应用并确保一切按预期运行之后,我们可以开始构建移动应用了。为此,我们使用了一个名为 Buildozer **,**的项目,它负责自动化打包所需工具的过程,使应用在移动端运行。如前所述,我们可以直接使用 pip 来安装最新的稳定版本,也可以指定 GitHub 链接来安装最新的开发版本。我们可以根据以下命令使用 pip 进行安装:

ahmed-gad@ubuntu:-$pip install buildozer

为了构建 Android 应用,Python 文件必须命名为main.py,并且位于项目的根目录下。Buildozer 使用这个文件作为最终 Java 项目中的主要活动。创建这个文件后,我们需要指定一些关于项目的属性。这些属性对于构建 Android 应用非常重要。这些属性被添加到名为buildozer.spec的文件中。您不需要从头开始创建这个文件,因为您可以使用下面的命令自动创建它。只要确保在main.py所在的同一个路径下执行即可(即在项目根目录下执行)。

ahmed-gad@ubuntu:-$buildozer init

假设保存项目文件的文件夹名为NewApp,发出该命令后,项目目录树如下:

  • 新应用
    • bin

    • .buildozer

    • main.py

    • buildozer.spec

除了需求之外,.buildozer文件夹还包含 Android 项目。在我们成功构建项目之后,bin文件夹保存了生成的 APK 文件。

清单 1-3 中给出了buildozer.spec文件的抽象版本。我们来讨论一下现有的字段,一个一个来。title定义了应用标题,在安装应用后显示给用户。package.namepackage.domain字段很重要,因为它们定义了应用在 Google Play 发布时的 ID。source.dir属性表示 Python 源文件main.py的位置。如果它与buildozer.spec文件位于同一个文件夹中,那么它只是被设置为一个点(.)。

[app]

# (str) Title of your application

title = FirstApp

# (str) Package name

package.name = kivyandroid

# (str) Package domain (needed for android/ios packaging)

package.domain = com.gad

# (str) Source code where the main.py live

source.dir = .

# (list) Source files to include (let empty to include all the files)

source.include_exts = py,png,jpg,kv,atlas,wav

# (list) Source files to exclude (let empty to not exclude anything)

source.exclude_exts = gif

# (list) List of inclusions using pattern matching

#source.include_patterns = assets/∗,img/∗.png

# (list) List of exclusions using pattern matching

#source.exclude_patterns = license,img/∗/∗.jpg

# (list) List of directory to exclude (let empty to not exclude anything)

source.exclude_dirs = bin

# (str) Application versioning (method 1)

version = 0.1

# (list) Application requirements

# comma separated e.g. requirements = sqlite3,kivy

requirements = kivy, numpy

# (str) Custom source folders for requirements

# Sets custom source for any requirements with recipes

requirements.source.numpy = /home/ahmedgad/numpy

# (str) Presplash of the application

presplash.filename = %(source.dir)s/presplash.png

# (str) Icon of the application

icon.filename = %(source.dir)s/logo.png

# (str) Supported orientation (one of landscape, portrait or all)

orientation = landscape

#

# OSX Specific

#

# change the major version of python used by the app

osx.python_version = 3

# Kivy version to use

osx.kivy_version = 1.10.1

#

# Android specific

#

# (bool) Indicate if the application should be fullscreen or not

fullscreen = 1

# (list) Permissions

android.permissions = INTERNET, CAMERA

# (int) Android API to use

android.api = 26

# (int) Minimum API required

android.minapi = 19

# (int) Android SDK version to use

#android.sdk = 27

# (str) Android NDK version to use

#android.ndk = 18b

# (str) Android NDK directory (if empty, it will be automatically downloaded.)

android.ndk_path = /home/ahmedgad/.buildozer/android/platform/android-ndk-r18b

# (str) Android SDK directory (if empty, it will be automatically downloaded.)

android.sdk_path = /home/ahmedgad/.buildozer/android/platform/android-sdk-linux

Listing 1-3Fields Inside the buildozer.spec File

假设我们在项目中使用了一些必须打包在 Android 应用中的资源。这可以用不同的方法来完成。尽管很简单,但如果没有成功完成,可能会浪费几个小时的调试时间。

一种简单的方法是在source.include_exts属性中指定这种资源的扩展。例如,如果所有带有。巴布亚新几内亚。jpg,还有。wav 扩展将被打包在应用中,那么属性将如下所示。如果该字段为空,则将包括根目录中的所有文件。

source.include_exts = png, jpg, wav

source.include_exts属性相反,有一个名为source.exclude_exts的属性定义了要从打包中排除的扩展。如果为空,则不排除任何文件。

还有source.include_patternssource.exclude_patterns分别创建要包含和排除的模式。请注意,它们是使用#进行注释的。

source.exclude_exts类似,有一个名为source.exclude_dirs的属性定义了要排除的目录。例如,bin文件夹是存在的,但是我们对包含它不感兴趣。这减小了 APK 文件的大小。

version属性定义了 Android 应用的版本。当您将应用的新版本上传到 Google Play 时,此属性必须更改为比您之前使用的更高的数字。

requirements属性中,您可以声明在 Python 代码中导入的所需库。例如,如果导入了 NumPy,则 NumPy 必须是该属性中的一项。每个需求将在第一次使用时被下载。

如果您下载了一个需求,并且想要在应用中使用它,而不是下载一个新的,那么您必须在用需求的名称替换了<requirement-name>之后,在requirements.source.<requirement-name>属性中定义这个需求的目录。例如,使用requirements.source.numpy为 NumPy 定义路径。

属性定义了用作应用图标的图像。它可以是 PNG 文件。当加载应用时,会为用户显示一个由presplash.filename属性定义的图像。Kivy 徽标用作默认图像。

orientation属性定义了应用支持的方向。可以设置为landscapeportrait使用一个方向。要根据设备的方向进行设置,请将其设置为all

osx.python_versionosx.kivy_version属性分别定义了正在使用的 Python 和 Kivy 的版本。

如果应用将以全屏模式运行,则将fullscreen属性设置为1。这将在应用运行时隐藏通知栏。

android.permissions属性设置应用运行所需的权限。例如,如果您在应用中访问摄像机,那么必须在该属性中声明CAMERA权限。

最近,谷歌禁止用户上传针对少于 26 个 API 的应用。因此,为了将应用发布到 Google Play,该应用必须面向至少 26 个 API。android.apiandroid.minapi字段定义了要使用的目标和最低 API 版本。重要的是不要将android.api设置为小于 26 的值。

android.sdkandroid.ndk字段设置用于构建应用的 SDK 和 NDK 的版本。如果没有这样的版本,可以下载。您也可以下载这些需求并在android.ndk_path?? 属性中指定它们的路径。

文件中有更多的字段可以帮助你。您可以通过向下滚动到buildozer.spec文件来了解更多信息。从他们的名字可以推断出他们的工作。请注意,系统不会要求您使用文件中的所有字段。只使用你需要的。

Buildozer 模板

注意,使用 Buildozer 构建 Android 应用类似于 Python 和 Android(即 Java)之间的桥梁。开发者创建一个 Python 项目,Buildozer 根据buildozer.spec文件中定义的规范将其转换成 Android Java 项目。为此,Buildozer 拥有根据这些属性中使用的值填充的模板。假设在buildozer.spec文件中指定的包名是kivyandroid,您可以在这里显示的目录中找到模板,因为项目根目录被命名为NewApp

NewApp/.buildozer/android/platform/build/dists/kivyandroid/templates

名为x.y的文件的模板是x.tmpl.y。例如,AndroidManifest.xml文件的模板叫做AndroidManifest.tmpl.xml。用于build.gradle文件的模板叫做build.tmpl.gradle

build.gradle

清单 1-4 中显示了build.tmpl.gradle文件中负责指定目标和最小 API 的部分。minSdkVersion字段保存应用支持的最低 API 级别。targetSdkVersion字段保存目标 API 级别。对于minSdkVersion,如果将变量值{{args.min_sdk_version}}替换为静态值,比如 19,那么无论buildozer.spec file内部的android.minapi属性中指定什么值,最小 API 都将是 19。这个对targetSdkVersion也适用。

android {
   ...
   defaultConfig {
      minSdkVersion {{ args.min_sdk_version }}
      targetSdkVersion {{ android_api }}

      ...
   }
    ...

Listing 1-4Specifying the Target and Minimum APIs in Gradle

AndroidManifest.xml

AndroidManifest.templ.xml文件中,清单 1-5 给出了负责声明在buildozer.spec文件的android.permissions属性中定义的应用权限的部分。第一行允许写入外部存储器。因为这样的权限是绝对的,并且不依赖于在buildozer.spec文件中定义的权限,这意味着即使您没有指定任何权限,这样的权限也是存在的。

    ...
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
{ % for perm in args.permissions %}
{ % if '.' in perm %}
<uses-permission android:name = "{{ perm }}" / >
{ % else %}
<uses-permission android:name = "android.permission.{{ perm }}" />
{ % endif %}
{ % endfor %}
    ...

Listing 1-5Declaring the Application Permission Inside Android Manifest

对于定义的自定义权限,有一个for循环,它遍历android.permissions属性中的每个值。对于每个值,都会为其创建一个<uses-permission>元素。

strings.xml

另一个模板名为strings.tmpl.xml,它负责生成为应用定义字符串资源的strings.xml文件。清单 1-6 显示了这个模板的内容。第一个字符串名为app_name,它定义了应用的名称。通过将{{ args.name }}替换为buildozer.spec文件中title属性的值来检索名称。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">{{ args.name }}</string>
    <string name="private_version">{{ private_version }}</string>
    <string name="presplash_color">{{ args.presplash_color }}</string>
    <string name="urlScheme">{{ url_scheme }}</string>
</resources>

Listing 1-6Specifying the String Resources Inside the strings.xml File

在基于这样的模板准备好所需的文件后,Buildozer 构建 Android 项目。项目的主要活动是PythonActivity.java,它可以在下面的路径中找到。在本书的后面,Android 项目将在 Android Studio 中用于编写一些 Java 功能。

/ NewApp/.buildozer/android/platform/build/dists/kivyandroid/build/src/main/java/org/kivy/android/

在构建应用之前,有一些工具必须可用,如 Android SDK 和 NDK。安装构建 Android 应用所需工具的一个简单方法是使用终端命令,如下所示。它根据buildozer.specandroid.sdkandroid.ndk属性定义的版本下载 SDK 和 NDK。

ahmed-gad@ubuntu:-$buildozer android debug

Buildozer 准备了所有的需求,以保证成功地构建应用。例如,它将所需的 Python 库打包到 APK 中,如果尚未下载,则下载它们。它还获取其他工具,如 SDK、NDK 和 Python-4-Android (P4A)。

如果您的互联网连接速度很快,这个命令会让您的生活更轻松。但是这种方式对于低速互联网连接非常耗时,因此不可靠。如果互联网连接中断,就没有机会继续下载。下载所需的 Python 库并不困难,因为与 SDK 和 NDK 相比,它们的大小并不大。根据我的经验,我浪费了很多时间试图多次下载这样的需求。

更好的解决方案是使用 Buildozer 最小化下载的数据量。这是通过手工(离线)准备大部分需求,然后将它们链接到 Buildozer 来完成的。您可以通过使用支持暂停和恢复下载过程的更可靠的软件来做到这一点。离线准备需求是很有趣的,因为它能够选择您在应用中需要的任何东西的精确版本。离线下载需求的一个有趣的原因是当我们对构建多个共享一些需求的项目感兴趣的时候。每次创建项目时,都会下载相同的需求。或者,我们可以只下载一次需求,并将它们链接到 Buildozer。因此,我们不需要为每个单独的项目下载相同的文件。

准备 Android SDK 和 NDK

从适用于 Linux 的 Android SDK 开始,在指定准确的版本号后,可以从该页面( http://dl.google.com/android/android-sdk_r{{rev}}-linux.tgz )下载。此 URL 使用 SDK 修订版 24 ( http://dl.google.com/android/android-sdk_r24-linux.tgz )。正如扩展名所反映的,它将作为压缩文件下载。

因为下载的 SDK 没有一些组件,包括 SDK 工具、SDK 平台、SDK 平台工具和 SDK 构建工具,所以我们也需要下载它们。SDK 平台用于针对特定的 Android 平台。SDK 平台工具用于支持目标 Android 平台的特性。SDK 构建工具用于构建应用和创建 APK。SDK 工具提供了开发和调试工具。例如,在我们为目标平台构建了 APK 文件之后,这些工具用于运行和调试它。

在指定版本号后,可以从该页面( https://dl-ssl.google.com/android/repository/tools_r{{rev}}-linux.zip )下载 SDK 工具。比如 SDK Tools 22 . 6 . 2 修订版可以从这个页面下载( https://dl-ssl.google.com/android/repository/tools_r22.6.2-linux.zip )。在指定目标平台和版本号后,可以从该页面( https://dl.google.com/android/repository/android-{{platform}}_r{{rev}}.zip )下载 SDK 平台。比如这是下载 SDK 平台 19 修订版 4 的 URL(https://dl.google.com/android/repository/android-19_r04.zip)。SDK 平台工具可以从这个网址( https://dl.google.com/android/repository/platform-tools_r{{rev}}-linux.zip )下载,指定版本号后。比如这是下载 SDK 平台工具修订版 19 的 URL(https://dl.google.com/android/repository/platform-tools_r19.0.1-linux.zip)。

在指定版本号后,可以从这个 URL ( https://dl.google.com/android/repository/build-tools_r{{rev}}-linux.zip )下载 SDK 构建工具。这是 SDK 构建工具 19.1 版本的 URL(https://dl.google.com/android/repository/build-tools_r19.1-linux.zip)。

类似于下载 SDK,NDK 可以从这个网址( http://dl.google.com/android/ndk/android-ndk-r{{rev}}c-linux-x86_64.tar.bz2 )下载。此 URL 对应于 NDK 版本 9 ( http://dl.google.com/android/ndk/android-ndk-r9c-linux-x86_64.tar.bz2 )。

P4A 项目也可以使用 Buildozer 自动下载和安装。但是如果我们对使用项目的开发版本感兴趣,我们不能依赖 Buildozer,而必须从 GitHub 中克隆它( https://github.com/kivy/python-for-android )。

在成功下载需求之后,比如 SDK 及其工具、NDK 和 P4A,还有一个步骤,那就是将它们链接到 Buildozer。这个过程是这样的。

假设安装的 Buildozer 位于/home/ahmedgad/.buildozer/中,压缩后的 Android SDK 和 NDK 会被解压到/home/ahmedgad/.buildozer/android/platform/路径。如果 SDK 和 NDK 文件夹分别命名为android-sdk-linuxandroid-ndk-r9c,那么 SDK 的完整路径就是/home/ahmedgad/.buildozer/android/platform/android-sdk-linux,NDK 的完整路径就是/home/ahmedgad/.buildozer/android/platform/android-ndk-r9c。在 SDK 的 Tools 文件夹中,SDK 工具将被提取出来。下载的 SDK 平台将被解压到 SDK 内的platforms文件夹中。SDK 平台工具将被提取到 SDK 内的platform-tools文件夹中。下载的 SDK 构建工具将被解压到build-tools文件夹中,也在 SDK 中。SDK 的目录树如下,其中...表示父目录中有一些文件和文件夹:

  • android-sdk-linux
    • build-tools

      • 19.1.0
        • . . .
    • platforms

      • android-19
        • . . .
    • platforms-tools

      • . . .
    • tools

      • . . .
    • . . .

请注意,您可以从位于 SDK 工具中的 Android 工具使用 SDK 管理器来管理 SDK。可以使用以下命令访问它:

ahmed-gad@ubuntu:-$. android-sdk-linux/tools/android

Android SDK 管理器如图 1-3 所示。使用管理器,我们可以查看已经安装的工具、可用的更新和可供下载的新工具。确保所需的工具可用。

图 1-3

Android SDK 管理器

之后,我们需要分别使用android.sdk_pathandroid.ndk_path字段在应用的buildozer.spec文件中指定 SDK 和 NDK 的路径。如果克隆的 P4A 项目的路径是/home/ahmedgad/python-for-android **,**它将被分配给buildozer.spec文件中的p4a.source_dir字段。

准备 Python 需求

先说按项目下载需要的 Python 库。通常,我们不必离线下载它们,因为 Buildozer 本身会下载所有需要的 Python 库,并将它们自动链接到项目。它根据buildozer.spec文件的requirements字段知道需要哪些库。例如,如果我们的项目需要 NumPy 和 Kivy,该字段将如下所示:

requirements = kivy,numpy

只需在该字段中指定所需的库,Buildozer 就会为您下载它们。对于每个库,在/NewApp/.buildozer/android/platform/build/packages路径中创建一个带有其名称的文件夹,其中NewApp是项目的根目录。例如,如果 NumPy 是一个必需的库,那么在这个路径中会有一个名为numpy的文件夹。在每个库的文件夹中,库将以压缩文件的形式下载。这样做的目的是缓存这个库,这样就可以只下载一次,而不是每次构建项目时都下载。注意,到目前为止,这个库还没有安装,只是被下载/缓存了。

注意,库不必存在于packages文件夹中。还有一种方法可以告诉 Buildozer 所需的库驻留在哪里。假设先前在packages文件夹外下载了一个库,我们只想告诉 Buildozer。这个库的路径在buildozer.spec文件中指定。在将LIB_NAME替换为库名后,requirements.source.LIB_NAME字段接受这些库的路径。例如,NumPy 的路径在requirements.source.numpy字段中指定。

假设我们的项目需要 NumPy 1.15.2,我们可以从这个页面( https://github.com/numpy/numpy/releases/download/v1.15.2/numpy-1.15.2.zip )下载。如果这个库位于/home/ahmedgad/numpy中,需要添加到buildozer.spec文件中来帮助构建者使用这个库的字段如下。这个过程可以对所有类型的需求重复进行。

requirements.source.numpy = /home/ahmedgad/numpy

当 Buildozer 构建项目时,它会检查所需的库是否被缓存到了packages文件夹中,或者它的路径是使用requirements.source字段显式指定的。如果找到了这些库,将会使用它们。如果没有,Buildozer 必须将它们下载到packages文件夹中。

在 Buildozer 找到所有需要的库之后,它开始安装它们。库被安装到/NewApp/.buildozer/android/platform/build/build/other_builds路径。类似于创建一个文件夹来保存每个下载的库,该路径中也有一个文件夹来保存已安装的库。例如,将有一个名为numpy的文件夹来保存 NumPy 库的安装。

构建和运行简单的 Kivy Android 应用

在我们安装了所需的库之后,buildozer android debug命令将成功完成,APK 文件将被导出到项目的bin目录中。我们可以将这个文件复制到 Android 设备上,安装它,然后运行应用。我们还可以使用这个命令在 Android 设备上自动构建、部署和运行应用:

ahmed-gad@ubuntu:-$buildozer android debug deploy run

这需要使用 USB 电缆将 Android 设备连接到机器,并启用 USB 调试。要在设备中启用 USB 调试,请转到设置,选择列表末尾的开发人员选项项,并将 USB 调试复选框的状态切换为打开。如果“开发人员选项”项不可见,请转到“设置”下的“关于设备”项,并按七次 tab 键。(注意,根据 Android OS 版本,步骤可能会有一些变化。)

我们还可以通过将logcat附加到该命令来查看设备的日志,如下所示:

ahmed-gad@ubuntu:-$buildozer android debug deploy run logcat

图 1-4 显示了应用在 Android 设备上运行后的外观。

图 1-4

在 Android 中运行 Kivy 应用

摘要

到本章结束时,我们已经使用 Kivy 跨平台 Python 库成功创建了一个非常简单的 Android 应用。请记住,相同的 Python 代码用于创建桌面和移动应用。我们通过在台式计算机上准备 Kivy 开发环境来开始应用开发过程。然后,我们创建了一个简单的应用来检查一切是否按预期运行。这个应用只是在一个窗口中显示文本。

成功运行桌面应用后,我们开始移动(Android)应用部署。它是通过安装 Buildozer 为 Kivy 应用生产 Android Studio 项目开始的。Kivy 项目属性在一个名为buildozer.spec的文件中指定。这些属性包括应用标题、请求的权限、所需的资源、Android SDK 和 NDK 的路径等等。该项目包括 Android Studio 所需的所有资源,如 Gradle、Manifest、Strings 等。为这个项目创建了一个 APK 文件,它可以安装在 Android 设备上,甚至可以在签署后部署在 Google Play 上。

二、使用 KV 语言实现逻辑和图形界面的分离

在前一章中,我们准备了 Kivy 开发环境并创建了一个非常简单的桌面应用。然后我们安装了 Buildozer 来构建移动应用。正如我们之前讨论的,成功构建的工具和需求可以使用 Buildozer 自动安装,也可以离线下载并链接。通过准备 Android SDK、NDK 和所需的 Python 库,我们可以成功地构建一个使用与桌面应用相同的 Python 代码的 Android 应用。这是因为 Kivy 是一个跨平台库。

在这一章中,我们将通过在 GUI 上放置更多的小部件来丰富我们的应用,并添加受益于这些小部件接收的数据的逻辑。开始时,GUI 部件将使用 Python 代码添加到kivy.app.App类的build()函数中。我们将创建一个具有三个小部件(按钮、文本输入和标签)的应用。一旦按下按钮,文本输入的数据将显示在文本标签上。随着我们添加更多的小部件,Python 代码将变得复杂,从而更难调试。出于这个原因,我们将使用 KV 语言将 GUI 与 Python 逻辑分开。这样,Python 代码将专用于逻辑。我们开始吧。

将 TextInput 小部件添加到 GUI

在前一章中,我们创建了一个只有一个静态标签部件的应用。在本章中,我们首先添加一个名为TextInput的新部件。从它的名字可以很容易地推断出这个小部件允许用户输入文本。通过实例化kivy.uix.textinput.TextInput类来创建TextInput小部件。这个类的构造函数接收一个名为text的属性,它是小部件中的默认文本。清单 2-1 用这个小部件创建了一个完整的应用。

import kivy.app
import kivy.uix.textinput

class TestApp(kivy.app.App):

    def build(self):
        return kivy.uix.textinput.TextInput(text="Hello World")

app = TestApp()
app.run()

Listing 2-1Adding a TextInput Widget to the GUI

如果运行这个 Python 脚本,将会显示如图 2-1 所示的窗口。整个窗口只是文本输入。您可以编辑输入的文本。

图 2-1

在清单 2-1 中创建的应用的窗口

我们可以在 GUI 中添加一个按钮小部件。点击按钮,文本输入的数据将被接收并打印在print语句中。现在有一个问题。在build()函数中,只返回一个小部件。现在我们需要返回两个小部件(按钮和文本输入)。我们如何做到这一点?解决方案是将这些小部件添加到一个容器中,然后返回这个容器。Kivy 中的容器是布局。

Kivy 中有不同类型的布局,比如框、相对、网格、堆栈等等。每种布局都有其排列内部子部件的方式。例如,box 布局垂直或水平排列子元素。网格布局将窗口分割成由行和列组成的矩阵,然后在矩阵单元中插入小部件。默认情况下,窗口平均分配给所有小部件。

通过在 Python 中添加更多小部件来丰富 GUI 应用

清单 2-2 中的代码创建了一个应用,其中按钮和文本输入被插入到一个框布局中。应用的 GUI 是在build()函数中创建的。首先,按钮被创建为来自kivy.uix.button.Button类的一个实例。它接受参数text,该参数接受显示在按钮上的文本。按钮和文本输入都保存在变量中以备后用。按钮部件保存在my_button变量中,而文本输入保存在text_input变量中。

盒子布局是作为来自kivy.uix.boxlayout.BoxLayout类的实例创建的。它保存在box_layout变量中。为了指定小部件是垂直还是水平添加到布局中,在类构造函数中指定了orientation参数。其默认值为horizontal **,**表示小部件将从左向右水平插入。在这个例子中,方向被设置为垂直,所以小部件将从上到下插入,其中插入的第一个元素将被放置在窗口的顶部,插入的最后一个元素将被放置在窗口的底部。通过指定要添加的微件的名称,使用add_widget功能将微件插入布局。最后,布局返回。

import kivy.app
import kivy.uix.textinput
import kivy.uix.button
import kivy.uix.boxlayout

class TestApp(kivy.app.App):

    def build(self):
        my_button = kivy.uix.button.Button(text="Click me")
        text_input = kivy.uix.textinput.TextInput(text="Data inside TextInput")

        box_layout = kivy.uix.boxlayout.BoxLayout(orientation="vertical")
        box_layout.add_widget(widget=button)
        box_layout.add_widget(widget=textInput)

        return box_layout

app = TestApp()
app.run()

Listing 2-2Adding More Than One Widget to the Application

运行应用后,将出现如图 2-2 所示的窗口。它被垂直分为两个大小相等的部分,并根据布局中的放置顺序进行放置。

图 2-2

在清单 2-2 中创建的应用窗口

到目前为止,单击该按钮不会导致任何操作。使用bind()功能来处理按钮动作。它接受反映动作的参数。要处理按钮按下动作,使用on_press参数。该参数被分配给一个函数,该函数在动作被触发时被调用。

处理按钮按压

清单 2-3 中的代码创建了一个名为button_press()的函数,当按钮被按下时该函数被调用。它接受触发动作的小部件作为参数。该功能附加在按钮上,以便在按下按钮时执行。该函数在每次按下按钮时打印一条消息,根据press_count变量显示按钮被按下的次数。它在函数调用结束时递增。

import kivy.app
import kivy.uix.textinput
import kivy.uix.button
import kivy.uix.boxlayout

class TestApp(kivy.app.App):

    press_count = 1
    def button_press(self, button_pressed):
        print("Button Pressed", TestApp.press_count, "Times")
        TestApp.press_count = TestApp.press_count + 1

    def build(self):
        my_button = kivy.uix.button.Button(text="Click me")
        my_button.bind(on_press=TestApp.button_press)
        text_input = kivy.uix.textinput.TextInput(text="Data inside TextInput")

        box_layout = kivy.uix.boxlayout.BoxLayout(orientation="vertical")
        box_layout.add_widget(widget=my_button)
        box_layout.add_widget(widget=text_input)

        return box_layout

app = TestApp()
app.run()

Listing 2-3Handling a Button Press

按下按钮四次后打印的信息如图 2-3 所示。

图 2-3

每次按下按钮时都会打印一条消息

从文本输入接收数据

可以修改应用,以便打印插入到TextInput小部件中的文本。如前所述,当按钮被按下时,它的回调函数button_press()将被调用。在这个函数中,可以返回并打印TextInput小部件中的文本。为了能够访问该函数中的TextInput小部件,该小部件存储在关键字self引用的当前对象中。新应用的代码如清单 2-4 所示。

import kivy.app
import kivy.uix.textinput
import kivy.uix.button
import kivy.uix.boxlayout

class TestApp(kivy.app.App):

    def button_press(self, button_pressed):
        input_data = self.text_input.text
        print(input_data)

    def build(self):
        my_button = kivy.uix.button.Button(text="Click me")
        my_button.bind(on_press=self.button_press)
        self.text_input = kivy.uix.textinput.TextInput(text="Data inside TextInput")

        box_layout = kivy.uix.boxlayout.BoxLayout(orientation="vertical")
        box_layout.add_widget(widget=my_button)
        box_layout.add_widget(widget=self.text_input)

        return box_layout

app = TestApp()
app.run()

Listing 2-4Receiving Text from the TextInput upon a Button Press

按下按钮后,将使用text属性获取TextInput小部件中的当前文本,并打印到终端。

在文本标签上显示文本

在当前状态下,我们必须打开窗口,按下按钮,然后到终端上查看打印出来的消息。如果再次按下按钮,我们必须到终端查看输出,等等。我们可以通过在窗口内部的标签部件上打印消息来使生活变得更容易。因此,我们根本不需要打开终端。与上一个应用相比,所做的更改包括使用kivy.uix.label.Label类创建一个新的标签小部件,将其添加到框布局中,将其附加到当前对象(self)以便在button_press()函数中访问它,并根据从TextInput小部件接收的输入更改其文本。新的应用如清单 2-5 所示。

import kivy.app
import kivy.uix.label
import kivy.uix.textinput
import kivy.uix.button
import kivy.uix.boxlayout

class TestApp(kivy.app.App):

    def button_press(self, button_pressed):
        self.text_label.text = self.text_input.text

    def build(self):
        self.text_input = kivy.uix.textinput.TextInput(text="Data inside TextInput")
        my_button = kivy.uix.button.Button(text="Click me")
        my_button.bind(on_press=self.button_press)
        self.text_label = kivy.uix.label.Label(text="Waiting for Button Press")

        box_layout = kivy.uix.boxlayout.BoxLayout(orientation="vertical")
        box_layout.add_widget(widget=self.text_label)
        box_layout.add_widget(widget=my_button)
        box_layout.add_widget(widget=self.text_input)

        return box_layout

app = TestApp()
app.run()

Listing 2-5Adding a Label Widget to the Application

应用窗口如图 2-4 所示。当按下按钮时,TextInput小工具内的文本显示在标签上。

图 2-4

按下按钮后,在标签小部件中显示 TextInput 中的文本

嵌套小部件

对于最后一个应用,在 box 布局中只添加了三个小部件作为子部件。因为此布局的方向设置为垂直,所以窗口高度将在三个子窗口中平均分配,但每个子窗口将占据窗口的整个宽度。换句话说,每个孩子将占用窗口高度的三分之一,但会扩展窗口的整个宽度。调试清单 2-5 中的代码非常容易,因为只有几个小部件。通过添加更多的小部件,代码变得更加复杂,难以调试。我们可以在清单 2-6 中的下一个应用中添加更多的小部件。在前面的应用中,每个小部件占据了整个窗口的宽度。在这个应用中,窗口的宽度分为两个小部件。

在清单 2-6 的应用代码中,窗口将有一个名为box_layout的垂直方向的根框布局。这个布局将有三个子布局。这个布局的顶层子元素是一个名为text_label的标签。剩下的两个子节点是名为box_layout1box_layout2的盒子布局(子节点本身就是布局)。每个子框布局的方向都是水平的(也就是说,子框是从左向右插入的)。每个子布局将有两个子部件(按钮和文本输入)。当每个子布局的按钮被按下时,同级TextInput小部件内的文本将显示在标签上。

import kivy.app
import kivy.uix.label
import kivy.uix.textinput
import kivy.uix.button
import kivy.uix.boxlayout

class TestApp(kivy.app.App):
    def button1_press(self, button_pressed):
        self.text_label.text = self.text_input1.text

    def button2_press(self, button_pressed):
        self.text_label.text = self.text_input2.text

    def build(self):
        self.text_label = kivy.uix.label.Label(text="Waiting for Button Press")

        self.text_input1 = kivy.uix.textinput.TextInput(text="TextInput 1")
        my_button1 = kivy.uix.button.Button(text="Click me")
        my_button1.bind(on_press=self.button1_press)

        self.text_input2 = kivy.uix.textinput.TextInput(text="TextInput 2")
        my_button2 = kivy.uix.button.Button(text="Click me")
        my_button2.bind(on_press=self.button2_press)

        box_layout = kivy.uix.boxlayout.BoxLayout(orientation="vertical")

        box_layout1 = kivy.uix.boxlayout.BoxLayout(orientation="horizontal")
        box_layout1.add_widget(widget=self.text_input1)
        box_layout1.add_widget(widget=my_button1)

        box_layout2 = kivy.uix.boxlayout.BoxLayout(orientation="horizontal")
        box_layout2.add_widget(widget=self.text_input2)
        box_layout2.add_widget(widget=my_button2)

        box_layout.add_widget(self.text_label)
        box_layout.add_widget(box_layout1)
        box_layout.add_widget(box_layout2)

        return box_layout

app = TestApp()
app.run()

Listing 2-6Creating Nested Widgets

图 2-5 显示了运行清单 2-6 中的应用后的窗口。每个按钮都有一个回调函数。例如,button1_press()与第一个按钮(my_button1)关联。当按下给定盒子布局中的一个按钮时,来自同一个盒子布局中的TextInput小部件的文本显示在标签上。

图 2-5

嵌套小部件

在添加了更多的 widget 之后,显然很难推导出应用的 widget 树。例如,确定给定父母的子女并不容易。因此,接下来我们将使用 KV 语言,它以结构化的方式构建 GUI 的小部件树。

使用 KV 语言

KV 语言(kvlang 或 Kivy 语言)以可读的方式创建了一个小部件树,帮助我们调试应用的 GUI。它使用缩进来标记给定父级的子级。它还使用缩进来标记给定小部件的属性。使用 KV 语言的另一个好处是 Python 逻辑与 GUI 的分离。微件树创建在扩展名为.kv的文件中。因此,我们可以独立于 Python 代码来修改小部件树。注意,我们不必将模块导入 KV 文件来使用小部件。比如为了用一个盒子布局,我们就写BoxLayout

在 KV 文件中,总是有一个没有任何缩进的小部件。这是根小部件,它对应于清单 2-6 中代码所示的box_layout小部件。这个小部件的属性和子部件缩进四个空格。清单 2-7 显示了清单 2-6 中先前应用的 KV 文件的内容。

BoxLayout:
    orientation: "vertical"
    Label:
        text: "Waiting for Button Press"
        id: text_label
    BoxLayout:
        orientation: "horizontal"
        TextInput:
            text: "TextInput 1"
            id: text_input1
        Button:
            text: "Click me"
            on_press: app.button1_press()
    BoxLayout:
        orientation: "horizontal"
        TextInput:
            text: "TextInput 2"
            id: text_input2
        Button:
            text: "Click me"
            on_press: app.button2_press()

Listing 2-7Using the KV Language to Separate the Python Logic from the GUI

根据所需的顺序将小部件添加到树中,以产生与上一个应用相同的结果。值得一提的是,Python 代码中需要引用的字段都被赋予了 id。它们是TextInputLabel小部件。此外,使用on_press属性将on_press动作附加到按钮上,该属性被分配给一个使用关键字app调用的函数。kvlang 中的这个关键字指的是使用这个 KV 文件的 Python 文件。因此,app.button1_press()意味着调用链接到这个 KV 文件的 Python 文件内部名为button1_press的函数。这里的问题是如何将 Python 文件链接到 KV 文件。这很容易。

在 Python 文件中创建的类被命名为TestApp。Kivy 提取单词App之前的文本,即Test。在将文本转换成小写(Test变成了test)之后,Kivy 在 Python 文件的同一个文件夹中搜索一个名为test.kv的 KV 文件。如果找到这样的文件,Kivy 会将它隐式链接到 Python 文件。如果没有找到,应用将启动,但会有一个空白窗口。请注意,build()功能被删除。如果 Python 代码中存在这个函数,而 Kivy 没有找到 KV 文件,那么应用将不会运行。

在 KV 文件中创建小部件树之后,Python 代码如清单 2-8 所示。现在 Python 代码调试起来非常简单。无论小部件树是使用 Python 还是 KV 语言创建的,应用的工作方式都是一样的。

需要注意的是如何在 Python 代码中访问 KV 文件中创建的小部件。一旦小部件被赋予一个 ID,您就可以使用root.ids字典来引用它。关键字root指的是 KV 文件中的根框布局小部件。通过按所需小部件的 ID 索引字典,它将被返回,因此我们能够访问它的属性并覆盖它。

import kivy.app

class TestApp(kivy.app.App):

    def button1_press(self):
        self.root.ids['text_label'].text = self.root.ids['text_input1'].text

    def button2_press(self):
        self.root.ids['text_label'].text = self.root.ids['text_input2'].text

app = TestApp()
app.run()

Listing 2-8Python Code for the Application in Listing 2-6 After Defining the GUI in a KV File

使用 load_file()调用 KV

假设 KV 文件的名称不是test.kv而是test1.kv,Kivy 将无法隐式定位 KV 文件。在这种情况下,我们必须显式指定来自kivy.lang.Builder类的load_file()函数内的文件路径,如清单 2-9 所示。这个函数的结果由build()函数返回。

import kivy.app
import kivy.lang

class TestApp(kivy.app.App):

    def button1_press(self):
        self.root.ids['text_label'].text = self.root.ids['text_input1'].text

    def button2_press(self):
        self.root.ids['text_label'].text = self.root.ids['text_input2'].text

    def build(self):
        return kivy.lang.Builder.load_file("test1.kv")

app = TestApp()
app.run()

Listing 2-9Explicitly Specifying the Path of the KV File

使用 load_string()调用 KV

还可以使用kivy.lang.Builder类中的load_string()函数在 Python 文件中编写 KV 语言代码。代码用三重引号括起来,如清单 2-10 所示。请注意,不建议使用这种方式,因为它没有将逻辑与可视化分开。

import kivy.app
import kivy.lang

class TestApp(kivy.app.App):

    def button1_press(self):
        self.root.ids['text_label'].text = self.root.ids['text_input1'].text

    def button2_press(self):
        self.root.ids['text_label'].text = self.root.ids['text_input2'].text

    def build(self):
        return kivy.lang.Builder.load_string(

"""

BoxLayout:

    orientation: "vertical"
    Label:
        text: "Waiting for Button Press"
        id: text_label
    BoxLayout:
        orientation: "horizontal"
        TextInput:
            text: "TextInput 1"
            id: text_input1
        Button:
            text: "Click me"
            on_press: app.button1_press()
    BoxLayout:
        orientation: "horizontal"
        TextInput:
            text: "TextInput 2"
            id: text_input2
        Button:
            text: "Click me"
            on_press: app.button2_press()
""")

app = TestApp()
app.run()

Listing 2-10Adding the KV Language Code Within the Python File

注意,本章并没有创建 Android 应用;重点是添加更多的小部件和构建应用。不要担心根据本章讨论的内容构建 Android 应用,因为它非常简单。准备好buildozer.spec文件后,按照第一章中讨论的步骤使用 Buildozer 构建 APK 文件。

摘要

既然我们已经到了本章的末尾,让我们快速回顾一下前两章讨论的内容。在前一章中,我们为开发桌面应用准备了 Kivy 环境。然后我们安装了 Buildozer 来开发 Android 应用。我们简单地从创建一个单标签小部件显示文本的例子开始。之后,使用布局将更多的小部件添加到应用中。使用on_press处理按钮按压动作。由于我们在小部件树中嵌套了更多的小部件,调试应用变得更加困难。出于这个原因,引入了 KV 语言,这样我们就可以构建小部件树,并将 GUI 从 Python 逻辑中分离出来。

在下一章中,我们将介绍相机小部件,这样我们就可以非常容易地访问相机。在确保成功创建桌面应用之后,我们将转到构建相应的 Android 应用,并看看如何使用 Kivy 非常直观地访问 Android 相机小部件。因为访问 Android 摄像头需要权限,下一章讨论在buildozer.spec文件内部添加权限。这些权限将反映在 Google Play 中,供任何用户在安装应用之前查看。下一章还讨论了 Kivy 的一个比较重要的特性,就是 Canvas。Canvas 用于在小部件上绘图和进行转换。

三、将安卓相机共享给一个 HTTP 服务器

在前两章中,我们为开发桌面应用准备了 Kivy 环境。在确保一切按预期运行之后,我们安装了 Buildozer 来构建 Android 应用。我们在书中创建的第一个应用非常简单;一个标签小部件显示了一些文本。之后,使用布局将更多的小部件添加到应用中。使用on_press处理按钮按压动作。创建了一个嵌套的小部件树,这使得调试变得更加复杂。出于这个原因,KV 语言被引入来构建小部件树,并将 GUI 与 Python 逻辑分离开来。

本章讨论如何访问和使用 Android 摄像头来捕捉图像并与 HTTP 服务器共享图像。Camera小部件用于访问 Android 摄像头。在确保桌面上的一切都正常工作后,我们使用 Buildozer 来构建 Android 应用。适当的权限在buildozer.init文件中指定。默认情况下,Android 摄像头旋转 90 度,Kivy 画布用于处理这个问题。将讨论三个画布实例— canvascanvas.beforecanvas.after。为了将一条给定指令的效果限制在某些小部件上,我们讨论了PushMatrixPopMatrix指令。

在适当的角度预览相机后,将捕获图像,以便将它们上传到 HTTP 服务器。服务器是使用 Flask 创建的,并在台式计算机上运行。使用服务器的 IPv4 地址和端口号,对 Python 库的请求将使用 Kivy Android 应用和 HTTP POST消息上传捕获的图像。

本章最后在服务器的网络浏览器中创建一个 Android 摄像头的实时预览。为了节省时间,图像以字节数组的形式保存在设备存储器中,而不是保存在设备存储器中。这样的字节数组然后被上传到服务器。然后,服务器解释这些字节数组,以便在 web 浏览器的 HTML 页面上显示图像。

绝望的相机小部件

Python 中有不同的库可以访问相机,比如 OpenCV 和 PyGame。Kivy 还支持一个名为Camera的小部件来访问相机。它更简单,因为它不要求使用库。使用小部件的 APK 文件比打包库的 APK 文件小。

清单 3-1 显示了在BoxLayout根小部件中有一个Camera小部件的 KV 文件。使用resolution属性指定首选分辨率。如果可能,以 1280x720 的分辨率捕捉图像。请注意Camera小部件大小和分辨率之间的差异。小部件大小设置应用 GUI 上小部件的大小,而分辨率定义捕获图像的像素密度。

play属性指定是否在应用启动后播放摄像机。如果设置为True,相机将在应用启动后播放。这个简单的 KV 文件是访问摄像机所需的最少代码。

BoxLayout:
    Camera:
        resolution: 1280,720
        play: True

Listing 3-1Adding the Camera Widget to the Widget Tree

与这个 KV 文件相关的 Python 代码也非常简单,如清单 3-2 所示。只需创建一个扩展了kivy.app.App类的类,并覆盖它的build()函数。因为这个函数是空的,所以将 Python 代码链接到 KV 文件的唯一方法是将其命名为test.kv

import kivy.app

class TestApp(kivy.app.App):

    def build(self):
        pass

app = TestApp()
app.run()

Listing 3-2Python File Associated with the KV File in Listing 3-1

应用窗口如图 3-1 所示。

图 3-1

使用相机小部件访问相机

访问 Android 摄像头

至此,我们已经创建了一个访问摄像机的桌面应用。让我们开始构建 Android 应用。

如第一章所述,应用的 Python 文件必须命名为main.py。为了获得访问 Android 摄像头的权限,android.permissions字段必须指定如下权限:

android.permissions=Camera

之后,可以根据下面的命令使用 Buildozer 构建 Android 应用。请记住,该命令在调试模式下构建 APK 文件,将其部署到 USB 连接的 Android 设备,并在安装后运行应用。

ahmed-gad@ubuntu:-$buildozer android debug deploy run

图 3-2 显示了使用 Android 应用捕获的图像之一。逆时针旋转 90 度。要解决这个问题,小部件必须顺时针旋转 90 度。因为顺时针旋转使用负角度,所以需要旋转-90 度。Kivy 支持将变换应用于其小部件的画布。

图 3-2

使用通过相机小部件访问的 Android 相机捕捉图像

不鼓励的画布

我们绘制的区域通常被称为画布。在 Kivy 中,画布是定义小部件图形表示的指令容器,而不是绘图区域。Kivy 中有三个画布实例— canvascanvas.beforecanvas.after。因此,可以为每个小部件分配这三个不同的实例。

在这三个实例中,画布可以执行两种类型的指令— contextvertex。顶点指令画在部件上。例如,如果要在一个小部件上绘制一个矩形或一条直线,它就是一个顶点指令。上下文指令不画任何东西,只是改变事物在屏幕上的显示方式。例如,上下文指令可以通过改变小部件的旋转、平移和缩放来变换小部件。

在添加 canvas 指令之前,必须将一个canvas实例附加到感兴趣的小部件。之后,我们可以添加说明。例如,清单 3-3 中的代码将canvas实例附加到一个Label小部件,并使用Rectangle顶点指令绘制一个矩形。

BoxLayout:
    Label:
        canvas:
            Rectangle:
                pos: 0,0
                size: 200, 200

Listing 3-3Adding Canvas to the Label Widget to Draw a Rectangle

矩形放置在像素(0,0)处,该像素是左下角对应的 Kivy 坐标系的原点(除了RelativeLayoutScatterLayout)。矩形的宽度和高度设置为 200 像素。因此,矩形从左下角开始,在水平和垂直方向延伸 200 个像素。图 3-3 为矩形。

图 3-3

在标签小部件上绘制一个矩形

我们可以使用Color上下文指令改变矩形的颜色。该指令使用rgb属性接受 RGB 颜色,其中每个通道被赋予一个介于 0 和 1 之间的值。在清单 3-4 中,红色是指定的颜色。

理解上下文指令应用于窗口小部件和它们下面的顶点指令是非常重要的。如果在上下文指令之前添加了小部件或顶点指令,则不会应用上下文指令。在本例中,如果在Rectangle顶点指令之后添加了Color顶点指令,矩形将被涂成红色。

BoxLayout:
    Label:
        canvas:
            Color:
                rgb: 1, 0, 0
            Rectangle:
                pos: root.pos
                size: 200,200

Listing 3-4Using the Color Context Instruction to Change the Rectangle Color

使用清单 3-4 中的 KV 文件运行应用后,结果如图 3-4 所示。矩形根据Color指令着色。

图 3-4

在标签小部件上绘制一个红色矩形

我们可以根据清单 3-5 为第二个标签小部件重复前面的指令。应用的颜色是绿色而不是红色,矩形位于窗口的中心。

BoxLayout:
    Label:
        canvas:
            Color:
                rgb: 1, 0, 0
            Rectangle:
                pos: root.pos
                size: 200,200
    Label:
        canvas:
            Color:
                rgb: 0, 1, 0
            Rectangle:
                pos: root.width/2-100, root.height/2-100
                size: 200,200

Listing 3-5Two Label Widgets Assigned to Two Canvas Instances to Draw Two Rectangles

应用窗口如图 3-5 所示。

图 3-5

使用两条矩形指令绘制两个矩形

使用完顶点指令后,我们可以开始使用第二类指令,也就是上下文。非常重要的是要注意,在确定应该在哪里应用指令之前,必须应用上下文指令。假设我们旋转一个使用Rectangle vertex 指令创建的矩形。在这种情况下,必须在Rectangle指令之前添加旋转矩形的上下文指令。如果上下文指令添加在Rectangle指令之后,矩形将不会改变。这是因为上下文指令仅在渲染绘图之前有效。渲染绘图后,上下文指令不起作用。

旋转小部件的上下文指令称为Rotate。根据清单 3-6 ,这个上下文指令加在Rectangle顶点指令之前旋转矩形。使用Rotate指令的angle属性,旋转到-45,顺时针旋转。旋转轴(或多个轴)可以使用axis属性定义。值 0,0,1 表示绕 Z 轴旋转。

默认情况下,旋转相对于坐标系的原点(0,0)。在这个例子中,我们对围绕(0,0)点旋转不感兴趣,而是围绕窗口中心点旋转。使用origin属性,我们可以改变旋转原点到窗口的中心。

BoxLayout:
    Label:
        canvas:
            Color:
                rgb: 1, 0, 0
            Rectangle:
                pos: root.pos
                size: 200,200
    Label:
        canvas:
            Color:
                rgb: 0, 1, 0
            Rotate:
                angle: -45
                axis: 0,0,1
                origin: root.width/2, root.height/2
            Rectangle:
                pos: root.width/2, root.height/2
                size: 200,200

Listing 3-6Using the Rotation Context Instruction to Rotate the Rectangle

图 3-6 显示了旋转矩形后的结果。

图 3-6

使用旋转上下文指令旋转矩形

在前面的例子中,为了影响绘图,上下文指令如ColorRotate必须在顶点指令如Rectangle之前添加到画布实例中。顶点指令必须写入 KV 文件中放置目标小部件的行之前的一行。例如,如果小部件位于第 5 行,那么顶点指令必须位于第 5 行之前,而不是之后。在前面的例子中,我们能够控制上下文指令在顶点指令之前的位置。在某些情况下,这是不可能的。

让我们考虑清单 3-7 中所示的应用,其中我们想要旋转一个按钮。

BoxLayout:
    Button:
        text: "Rotate this Button"

Listing 3-7A Button To Be Rotated

如果根据清单 3-8 将 canvas 实例添加到Button小部件中,那么 canvas 实例中的Rotate上下文指令将被添加到我们想要旋转的Button小部件之后,而不是之前。因此Rotate上下文指令不会影响小部件。我们需要在小部件之前而不是之后添加上下文指令。我们将讨论这个问题的两种解决方案。

BoxLayout:
    Button:
        text: "Rotate this Button"
        canvas:
            Rotate:
                angle: 45
                origin: root.width/2, root.height/2

Listing 3-8The Rotate Context Instruction Has Been Added After the Button Widget and Thus Does Not Affect It

对于给定的父部件,添加到 canvas 实例中的指令不仅会应用到父部件,还会应用到子部件。基于这个特征,我们可以找到我们的第一个解决方案。如果我们想在给定的小部件上执行上下文指令,我们可以将该指令添加到其父部件的 canvas 实例中。这样的指令将影响父部件和它的子部件。清单 3-9 实现了这个解决方案。注意,上下文指令不仅影响使用顶点指令如Rectangle绘制的内容,还会影响小部件。

BoxLayout:
    canvas:
        Rotate:
            angle: 45
            origin: root.width/2, root.height/2
    Button:
        text: "Rotate this Button"

Listing 3-9Placing the Canvas Instance Inside the Parent Widget in Order to Affect Its Children

结果如图 3-7 所示。我们成功地解决了这个问题,但是还有一个问题。

图 3-7

将画布添加到其父级后,按钮小部件旋转成功

之前的解决方案不仅旋转了按钮,还旋转了它的父按钮。如果有另一个孩子而不是按钮,它也会被旋转。清单 3-10 中显示的 KV 文件有一个不应该旋转的Label小部件。不幸的是,它会被旋转,如图 3-8 所示。

图 3-8

旋转上下文指令影响按钮和标签小部件

BoxLayout:
    canvas:
        Rotate:
            angle: 45
            origin: root.width/2, root.height/2
    Label:
        text: "Do not Rotate this Label"
    Button:
        text: "Rotate this Button"

Listing 3-10Adding the Context Instruction to the Parent Widget Affects All of its Children

画布.之前

前面的解决方案是将上下文指令添加到父部件,这会影响所有子部件。没有办法只将这种效果应用于特定的孩子。为了解决这个问题,这个解决方案将根据清单 3-11 中的 KV 文件使用canvas.before实例而不是canvas。该小部件中的指令将在呈现小部件之前执行。因此,如果在其中添加了Rotate内容指令,Button小部件将会成功旋转。

BoxLayout:
    Label:
        text: "Do not Rotate this Label"
    Button:
        text: "Rotate this Button"
        canvas.before:
            Rotate:
                angle: 45
                origin: root.width/2, root.height/2

Listing 3-11Using canvas.before Rather Than canvas to Rotate the Button Widget

应用窗口如图 3-9 所示。仅旋转Button小部件;Label保持不变。

图 3-9

使用 canvas.before 实例仅旋转一个子级

在前面的例子中,有一个技巧。我们要旋转的小部件被添加到小部件树的末尾,这就是为什么Label不受旋转的影响。如果在Button之后添加了Label,那么ButtonLabel小部件将被旋转。修改后的代码如清单 3-12 所示,应用窗口如图 3-10 所示。为什么Label widget 被旋转了?

图 3-10

在 canvas.before 被添加到 Label 之前后,按钮和标签小部件被旋转

BoxLayout:
    Button:
        text: "Rotate this Button"
        canvas.before:
            Rotate:
                angle: 45
                origin: root.width/2, root.height/2
    Label:
        text: "Do not Rotate this Label"

Listing 3-12Placing the canvas.before Instruction Before the Label Widget

Kivy 中的画布指令不限于它们被添加到的小部件。一旦一个指令被添加到任何一个小部件中,它就会影响到其他的小部件,直到有什么东西取消了这个指令的效果。例如,如果Button小部件旋转 45 度,那么它后面的小部件也将旋转 45 度。如果Label小部件出现在Button之后,我们不希望它被旋转,我们可以将Label小部件旋转-45 度,以便将其恢复到初始状态。取消Label小部件旋转的 KV 文件如清单 3-13 所示。应用窗口如图 3-11 所示。请注意,标签首先旋转 45 度,然后旋转-45 度。如果在Button之后有一个以上的小工具,那么将它们全部旋转以返回到它们的初始状态将是令人厌烦的。更好的解决方案是将Rotate上下文指令的效果限制在Button小部件上。

图 3-11

向左旋转-45 度后,左侧小部件保持不变

BoxLayout:
    Button:
        text: "Rotate this Button"
        canvas.before:
            Rotate:
                angle: 45
                origin: root.width/2, root.height/2
    Label:
        text: "Do not Rotate this Label"
        canvas.before:
            Rotate:
                angle: -45
                origin: root.width/2, root.height/2

Listing 3-13Rotating the Label by -45 Degrees to Cancel the Effect of the Button Rotation

canvas.after、PushMatrix 和 PopMatrix

为了避免将Rotate指令应用到Button小部件下面的小部件,并将效果限制到Button小部件,Kivy 提供了PushMatrixPopMatrix指令。想法是保存由旋转、平移和缩放表示的当前上下文状态。保存状态后,我们可以对Button小部件应用旋转。旋转后的Button小部件渲染成功后,我们可以恢复保存的上下文状态。因此,只有Button小部件将被旋转,所有其他小部件将保留它们的上下文状态。

清单 3-14 显示了使用PushMatrixPopMatrix的 KV 文件。生成的窗口与图 3-11 所示的窗口相同。

BoxLayout:
    Button:
        text: "Rotate this Button"
        canvas.before:
            PushMatrix:
            Rotate:
                angle: 45
                origin: root.width/2, root.height/2
        canvas.after:
            PopMatrix:
    Label:
        text: "Do not Rotate this Label"

Listing 3-14Using PushMatrix and PopMatrix to Limit the Effect of the Context Instructions

注意,PushMatrix指令被插入到canavs.before实例中,而PopMatrix指令被插入到canvas.after实例中。在canvas.after内增加了PopMatrix指令,确保只有在Button旋转成功后才会执行。如果该指令添加到canvas.before,则按钮不会旋转。事实上,按钮将根据Rotate指令旋转,然后在呈现旋转后的按钮之前恢复上下文状态。因此,我们不会感觉到旋转的影响。

相机旋转

在理解了画布和它们的指令是如何工作的之后,我们可以旋转Camera小部件,这是我们最初的目标。我们可以使用清单 3-15 中所示的 KV 文件构建应用。该文件使用前面讨论过的指令(canvas.beforecanvas.afterPushMatrixPopMatrix)。在进一步发展之前,熟悉它们是很重要的。请注意,我们正在更改 KV 文件,而没有更改 Python 代码。

BoxLayout:
    Camera:
        resolution: 1280, 720
        play: True
        canvas.before:
            PushMatrix:
            Rotate:
                angle: -90
                axis: 0,0,1
                origin: root.width/2, root.height/2
        canvas.after:
            PopMatrix:

Listing 3-15The KV File That Rotates the Camera Widget

为了展示完整的想法,清单 3-2 中使用的 Python 代码在清单 3-16 中重复出现。

记住将 KV 文件的名称设置为test.kv,以便在将它们转换成小写字母后匹配类名中单词App之前的字符。还要记得将CAMERA添加到buildozer.spec文件的android.permissions字段,以获得使用相机的权限。

import kivy.app

class TestApp(kivy.app.App):

    def build(self):
        pass

app = TestApp()
app.run()

Listing 3-16Python Code Associated with the KV File in Listing 3-15

图 3-12 显示了在 Android 设备上运行的应用。小部件放置在正确的角度。

图 3-12

相机部件被正确地放置在 Android 应用中

在本章开始之前,让我们快速回顾一下本书到目前为止我们所讨论的内容。我们为构建桌面和 Android 应用准备了 Kivy 开发环境。所需的主要工具是 Kivy 和 Buildozer。我们首先创建了一个简单的应用,其中只使用了一个小部件。为了向应用窗口添加多个 Kivy 小部件,我们讨论了 Kivy BoxLayout容器。为了允许用户与应用交互,我们讨论了如何设置和获取文本小部件,如TextInputLabel。使用on_press处理按钮按下动作。

Camera小工具也用于使用 Kivy 访问相机。因为使用 Android 相机捕获的图像默认是逆时针旋转 90 度,所以我们必须将Camera小部件顺时针旋转-90 度。Kivy 中的 canvas 实例允许我们使用上下文指令对小部件进行转换。此外,画布有顶点指令,用于在小部件上绘制形状,如矩形。讨论了其他画布实例,它们是canvas.beforecanvas.after。为了将画布指令的效果限制在选定的小部件上,我们讨论了PushMatrixPopMatrix指令。

在本章的下一节,我们将扩展之前创建的应用,以便查看摄像机、捕捉图像并将其上传到 HTTP 服务器。我们构建了一个应用,不仅可以查看相机,还可以捕捉图像。HTTP 服务器是使用运行在 PC 上的 Flask API 创建的。服务器有一个基于其 IPv4 地址和端口号的开放套接字,它等待请求上传文件的请求。使用requests Python 库,Kivy 应用使用 HTTP POST消息将捕获的图像上传到服务器。一旦服务器收到来自 Kivy 应用的 HTTP POST消息,它就将文件上传到一个选定的目录。我们开始吧。

使用 Kivy 捕捉和保存图像

我们现在想要修改清单 3-15 和 3-16 中编写的应用,以便在按钮按下时捕获并保存图像。为此,一个Button小部件将被添加到窗口的末尾,如清单 3-17 所示。问题是,我们如何捕捉相机图像?一般来说,当一个 widget 调用export_to_png()函数时,就会抓取一个 widget 的图像(即截图)并以 PNG 文件的形式保存到指定的目录下。如果Camera小部件调用了这个函数,相机图像将被保存为一个 PNG 文件。

为了更好地理解清单 3-17 中所示的 KV 文件,有一些注释。BoxLayout的方向被设置为vertical以确保小部件垂直排列,这样按钮就可以被添加到布局的末尾。在Button小部件之前添加了Camera小部件,这样按钮就被添加到了窗口的末尾。

当按钮被按下时,Camera小部件被赋予一个 IDcamera以在 Python 代码中访问它,从而调用export_to_png()函数。当on_press动作被触发时,Python 代码中的capture()函数将被调用。

BoxLayout:
    orientation: "vertical"
    Camera:
        id: camera
        size_hint_y: 18
        resolution: (1280, 720)
        play: True
        canvas.before:
            PushMatrix:
            Rotate:
                angle: -90
                origin: root.width/2, root.height/2
        canvas.after:
            PopMatrix:
    Button:
        text: "Capture"
        size_hint_y: 1
        on_press: app.capture()

Listing 3-17Adding a Button that Captures an Image When Pressed

最后要注意的是,size_hint_y属性被添加到了CameraButton小部件中。微件的尺寸根据BoxLayout自动计算。该属性提示布局使给定小部件的高度变大、变小或等于另一个小部件的高度。例如,如果相机的size_hint_y设置为 2,按钮设置为 1,那么Camera小工具的高度将是按钮高度的两倍。如果相机设置为 3,按钮设置为 1,那么相机高度将是按钮高度的三倍。如果两者都设置为相同的数字,那么两个小部件的高度将相等。在这个例子中,我们给Camera小部件分配了一个大值,给按钮分配了一个小值,这样就不会在屏幕上隐藏太多的区域。

size_hint_ y property类似,还有一个size_hint_x property控制小部件的宽度。

准备好 KV 文件后,我们需要讨论清单 3-18 中所示的 Python 文件。注意,这个类被命名为PycamApp。因此,KV 文件应该被命名为pycam.kv,以便隐式地使用它。在capture()函数中,Camera小部件根据其 ID 被提取到camera变量中。该变量调用export_to_png()函数,该函数接受保存捕获图像的路径。您可以更改此路径来自定义应用。

from kivy.app import App

class PycamApp(App):

    def capture(self):
        camera = self.root.ids["camera"]
        camera.export_to_png("/storage/emulated/0/captured_image_kivy.png")

    def build(self):
        pass

app = PycamApp()
app.run()

Listing 3-18Python File Associated with KV File in Listing 3-17 that Captures an Image on Button Press

构建并运行 Android 应用后,其窗口如图 3-13 所示。按下按钮,摄像机图像将保存在指定的目录中。这个图像将被发送到服务器。所以,让我们开始构建服务器。

图 3-13

按下按钮时捕捉图像

使用 Flask 构建 HTTP 服务器

保存捕获的图像后,我们将其发送到 HTTP 服务器。服务器是使用运行在 PC 上的 Flask API 创建的。因为 Flask 超出了本书的范围,所以我们不会详细讨论它。你可以在我的书《CNN使用深度学习的实际计算机视觉应用》的第七章中阅读更多关于 Flask 的内容(Apress,2018)。

清单 3-19 列出了构建 HTTP 服务器的 Flask 代码。让服务器监听文件上传请求。首先,使用flask.Flask类创建一个应用实例。该类的构造函数接受import_name参数,该参数被设置为包含 Flask Python 文件的文件夹名称。

import flask
import werkzeug

app = flask.Flask(import_name="FlaskUpload")

@app.route('/', methods = ['POST'])
def upload_file():
    file_to_upload = flask.request.files['media']
    file_to_upload.save(werkzeug.secure_filename(file_to_upload.filename))
    print('File Uploaded Successfully.')
    return 'SUCCESS'

app.run(host="192.168.43.231", port=6666, debug=True)

Listing 3-19Building the HTTP Server Using Flask

在这段代码的结尾,应用使用 IPv4 地址 192.168.43.231 和端口号 6666 监听请求。

如果您不知道您的 IPv4 地址,请使用ifconfig终端命令。如果没有找到该命令,使用该命令安装net-tools:

ahmed-gad@ubuntu:-$sudo apt install net-tools

之后,可以执行ifconfig命令,如图 3-14 所示。

图 3-14

使用 ifconfig 终端命令确定 IPv4 地址

debug参数控制是否激活调试模式。当调试模式打开时,对服务器的更改将自动应用,而无需重新启动它。

装饰器告诉 Flask 当一个 URL 被访问时执行哪个函数。URL 作为参数被添加到装饰器中,函数被添加到装饰器的下面,在这个例子中是upload_file()。URL 设置为/ **,**表示服务器的根目录。因此,当用户访问http://192.168.43.231/:6666时,与服务器根目录相关联的route()装饰器将接收这个请求并执行upload_file()功能。

route()装饰器接受一个名为methods的参数,该参数接收函数将作为列表响应的 HTTP 消息的类型。因为我们只对POST HTTP 消息感兴趣,所以它将被设置为['POST']

要上传的数据作为键和值的字典发送。密钥是文件的 ID,值是文件本身。在upload_file()函数中,使用flask.request.files字典获取要上传的文件。它接收引用要上传的文件的密钥。使用的钥匙是media。这意味着当我们准备 Kivy 应用时,要上传的图像的键必须设置为media。文件被返回到一个变量中,在我们的例子中是file_to_upload

如果我们对根据文件的原始名称保存文件感兴趣,可以使用filename属性返回它的名称。因为一些文件被命名为欺骗服务器并执行非法操作,所以使用werkzeug.secure_filename()函数返回安全文件名。返回安全文件名后,使用save()功能保存文件。文件保存成功后,控制台上会出现一条打印消息,服务器会向客户端发送一个单词SUCCESS作为响应。

请注意,服务器接受任何文件扩展名。你可以阅读更多关于 Flask 的内容,了解如何上传带有特定扩展名的文件。

使用将文件上传到 HTTP 服务器的请求

在修改 Android 应用来上传文件之前,我们可以构建一个客户端作为桌面应用,它使用requests库来上传文件。它将比移动应用更容易调试。该应用不包括 Kivy 代码,因此我们将使用终端命令与服务器交互。客户端代码如清单 3-20 所示。

import requests

files = {'media': open('/home/ahmedgad/Pictures/test.png', 'rb')}
try:
    requests.post('http://192.168.43.231:6666/', files=files)
except requests.exceptions.ConnectionError:
    print("Connection Error! Make Sure Server is Active.")

Listing 3-20Using Requests to Upload the Captured Image to the Server

准备好保存要上传的文件的字典。它只有一个键值对。如前所述,这个键将被设置为media,因为服务器正在等待这个键。使用接收其路径的open()函数打开文件。rb参数指定文件在二进制模式下以只读方式打开。

requests.post()函数接收两个参数。第一个是 URL,在指定其套接字详细信息(IPv4 地址和端口)后,将它定向到服务器的根目录。第二个是字典。try and catch 语句用于检查连接建立是否有问题。这确保了即使在建立连接时出现错误,应用也不会崩溃。

在构建了服务器和客户机之后,我们可以开始运行服务器了。服务器没有 GUI,因此它的界面是终端。因为服务器是一个常规的 Python 文件,我们可以根据使用的 Python 版本,在关键字pythonpython3后键入 Python 文件名来运行它。如果它的名字是FlaskServer.py,它将使用下面的命令执行。记住使用正确的路径来定位 Python 文件。

ahmed-gad@ubuntu:~/Desktop/FlaskUpload$python3 FlaskServer.py

我们将看到指示服务器正在成功运行的信息性消息,如图 3-15 所示。

图 3-15

运行 Flask HTTP 服务器

运行客户端应用并按下按钮后,服务器接收到 HTTP POST消息,并根据图 3-16 中控制台上显示的消息成功上传文件。响应的 HTTP 状态代码是 200,这意味着请求成功完成。

图 3-16

Kivy 应用捕获的图像被成功上传到 Flask HTTP 服务器

使用 Kivy Android 应用上传相机拍摄的图像

在确定桌面客户端应用运行良好后,我们就可以准备 Android 应用了。与清单 3-18 相比,变化将出现在capture()函数内部,如清单 3-21 所示。

import kivy.app
import requests

class PycamApp(kivy.app.App):

    def capture(self):
        camera = self.root.ids['camera']
        im_path = '/storage/emulated/0/'
        im_name = 'captured_image_kivy.png'
        camera.export_to_png(im_path+im_name)
        files = {'media': open(im_path+im_name, 'rb')}

        try:
            self.root.ids['capture'].text = "Trying to Establish a Connection..."
            requests.post('http://192.168.43.231:6666/', files=files)
            self.root.ids['capture'].text = "Capture Again!"
        except requests.exceptions.ConnectionError:
            self.root.ids['capture'].text = "Connection Error! Make Sure Server is Active."

    def build(self):
        pass

app = PycamApp()
app.run()

Listing 3-21Capturing and Uploading Images to the Server Using the Android Application

因为服务器可能脱机,我们需要向用户反映这一点。单击 Capture 按钮后,应该会显示一条消息,通知用户连接正在建立。如果出现连接错误,还会显示一条消息来反映这一情况。

为了能够更改按钮的文本,我们必须在 Python 文件中访问它。为了访问小部件,它必须有一个 ID。清单 3-17 中之前的 KV 文件没有给按钮分配 ID。清单 3-22 中修改后的 KV 文件为Button小部件分配一个 ID。分配的 ID 是capture

BoxLayout:
    orientation: "vertical"
    id: root_widget
    Camera:
        id: camera
        size_hint_y: 18
        resolution: (1280, 720)
        play: True
        canvas.before:
            PushMatrix:
            Rotate:
                angle: -90
                origin: root.width/2, root.height/2
        canvas.after:
            PopMatrix:
    Button:
        id: capture
        text: "Capture"
        size_hint_y: 1
        on_press: app.capture()

Listing 3-22Assigning an ID to the Button to Access it Inside the Python File

因为与之前的应用相比,小部件的排列没有改变,所以应用窗口将与之前的应用相同。

请求中的动态 IP 地址

现在,应用依赖于服务器的静态 IPv4 地址。如果服务器使用动态主机配置协议(DHCP),服务器的 IPv4 地址可能会改变,因此我们必须用新地址重建应用。为了使这个过程动态,我们可以使用一个TextInput小部件,其中可以输入服务器的 IPv4 地址。在将字典发送到服务器之前,获取来自小部件的文本以构建 URL。修改后的 KV 文件如清单 3-23 所示。为了访问这个小部件,它被分配了 ID ip_address

BoxLayout:
    orientation: "vertical"
    id: root_widget
    Camera:
        id: camera
        size_hint_y: 18
        resolution: (1280, 720)
        play: True
        canvas.before:
            PushMatrix:
            Rotate:
                angle: -90
                origin: root.width/2, root.height/2
        canvas.after:
            PopMatrix:
    TextInput:
        text: "192.168.43.231"
        id: ip_address
        size_hint_y: 1
    Button:
        id: capture
        text: "Capture"
        size_hint_y: 1
        on_press: app.capture()

Listing 3-23Adding a TextInput Widget to Enter the IPv4 Address of the Server

使用TextInput小部件后的 Python 代码如清单 3-24 所示。

import kivy.app
import requests

class PycamApp(kivy.app.App):

    def capture(self):
        camera = self.root.ids['camera']
        im_path = '/storage/emulated/0/'
        im_name = 'captured_image_kivy.png'
        camera.export_to_png(im_path+im_name)

        ip_addr = self.root.ids['ip_address'].text
        url = 'http://'+ip_addr+':6666/'
        files = {'media': open(im_path+im_name, 'rb')}

        try:
            self.root.ids['capture'].text = "Trying to Establish a Connection..."
            requests.post(url, files=files)
            self.root.ids['capture'].text = "Capture Again!"
        except requests.exceptions.ConnectionError:
            self.root.ids['capture'].text = "Connection Error! Make Sure Server is Active."

    def build(self):
        pass

app = PycamApp()
app.run()

Listing 3-24Fetching the IPv4 Address from the TextInput Widget

应用窗口如图 3-17 所示。请注意,按钮的文本被更改为“再次捕获!”这意味着根据 Python 代码成功上传了文件。记得在按下按钮之前运行服务器。尝试使用不同的 IPv4 地址,并注意按钮文本如何变化以反映存在连接错误。

图 3-17

使用 TextInput 小部件输入服务器的 IPv4 地址后的图像捕获应用

Flask 服务器日志消息也会出现在终端上,如图 3-18 所示。这些消息反映了服务器从 IPv4 地址为 192.168.43.1 的客户端收到了类型为POST的消息。

图 3-18

收到文件后记录来自服务器的消息

此时,我们成功地创建了一个 Kivy Android 应用,该应用将使用相机捕获的单个图像共享到 HTTP 服务器。这是通过将图像保存到设备存储器,然后上传它。

本章的剩余部分通过在服务器的 web 浏览器中实时预览 Android 摄像头来扩展该应用。这是通过捕获相机图像并将其作为字节数组存储在设备内存中来实现的。为了节省时间,图像不会作为文件存储在设备存储器中。使用 HTML POST消息将字节数组上传到 Flask 服务器,如本章前面所述。服务器接收每个图像,并使用<img>元素将其显示在 HTML 页面上。我们通过连续捕捉图像、将图像上传到服务器以及更新和刷新 HTML 页面来创建实时 Android 摄像头预览。

捕获相机图像并将其存储到存储器中

在本章前面,使用 Kivy export_to_png()函数将Camera小部件图像捕获并保存为 PNG 文件。当一个小部件调用这个函数时,会从小部件中截取一个屏幕截图,并以 PNG 文件的形式保存到函数中指定的目录中。使用 HTTP POST消息将保存的 PNG 文件上传到服务器。

如果我们希望使用 Android 相机连续预览捕获的图像,我们必须捕获Camera小部件,将每个捕获的图像保存为 PNG 文件,并将它们发送到服务器。每次捕获图像时保存文件非常耗时。此外,不需要将文件保存在存储器中。我们只需要将捕获的图像尽快发送到服务器。没有必要延迟传输过程。出于这些原因,我们可以使用工具来捕获图像并将像素保存在内存中,而不是保存在文件中。我们可以使用 OpenGL 中的glReadPixels()函数,或者使用get_region()函数返回小部件的纹理。这两种功能都可以捕获图像并将其保存到设备内存中,而不是保存为文件。这加快了进程。

glReadPixels()功能在kivy.graphics.opengl模块中可用。其签名如下:

kivy.graphics.opengl.glReadPixels(x, y, width, height, format, type)

该函数的工作原理是从 Kivy 应用窗口中捕获一个区域的图像。该区域从使用xy参数定位的左下角开始。使用widthheight参数指定区域的宽度和高度。使用前四个参数,成功地指定了区域。该区域从(x, y)开始,水平向左延伸等于width的值,垂直向上延伸等于height的值。因为我们对捕获放置Camera小部件的区域感兴趣,所以我们可以返回这个小部件的坐标,并将它们分配给四个参数。

在返回该区域的像素之前,需要指定一些其他参数来控制像素如何保存到内存中。

format参数指定像素数据的格式。它有不同的值,如GL_REDGL_GREENGL_BLUEGL_RGBGL_RGBA等等。这些值存在于kivy.graphics.opengl模块中。我们对捕捉 RGB 图像感兴趣,因此将使用GL_RGB值。

type参数指定像素数据的类型。它有不同的值,如GL_UNSIGNED_BYTEGL_BYTEGL_UNSIGNED_SHORTGL_SHORTGL_UNSIGNED_INT等等。这些值存在于kivy.graphics.opengl模块中。我们对将图像保存为字节数组感兴趣,因此使用了GL_UNSIGNED_BYTE参数。

该函数还有另外两个可选参数,名为arrayoutputType,我们不想编辑它们。

另一种捕获小部件图像的方法是使用kivy.graphics.texture.Texture类的get_region()方法。对于任何有纹理的小部件,我们可以调用这个函数来返回它的纹理。它有四个参数,如下所示。它们与glReadPixels()函数中的前四个参数相同。

get_region(x, y, width, height)

您可以使用这些函数中的任何一个从Camera小部件中捕捉图像,并将结果保存在内存中。对于get_region(),它与有纹理的部件一起工作。一些小部件没有纹理,比如TextInput,因此我们不能使用get_region()。另一方面,glReadPixels()捕捉图像时不关心小工具是否有纹理。

为了使事情更简单,我们可以使用get_region()。清单 3-25 中显示了使用get_region()捕捉图像并将其保存到内存中的完整代码。

我们将开始构建一个桌面应用来简化调试。在本章的最后,我们可以构建 Android 应用。

使用 KF 文件中的 ID 获取Camera小部件,以确定其左下方的位置(camera.xcamera.y)。这是对其分辨率(camera.resolution)的补充,以返回捕获图像的大小,其中camera.resolution[0]是宽度,camera.resolution[1]是高度。这四个值被分配给get_region()方法中的四个参数。

get_region()方法返回一个TextureRegion类的实例。为了返回纹理的像素,我们可以使用pixels属性。它以像素的形式返回小部件的纹理,以无符号字节数组的形式显示RGBA格式。这个数组保存在内存中。在这个例子中,字节数组保存在pixels_data变量中。这个变量中的数据稍后将被发送到服务器。

import kivy.app
import PIL.Image

class PycamApp(kivy.app.App):

    def capture(self):
        camera = self.root.ids['camera']
        print(camera.x, camera.y)

        pixels_data = camera.texture.get_region(x=camera.x, y=camera.y, width=camera.resolution[0], height=camera.resolution[1]).pixels

        image = PIL.Image.frombytes(mode="RGBA",size=(int(camera.resolution[0]), int(camera.resolution[1])), data=pixels_data)
        image.save('out.png')

    def build(self):
        pass

app = PycamApp()
app.run()

Listing 3-25Capturing a Camera Image Using get_region()

现在,我们可以调试应用以确保一切按预期运行。这是通过使用 Python 图像库( PIL )保存捕获的图像。因为get_region().pixels返回一个 RGBA 格式的字节数组,我们需要从该数组中构造图像。PIL 的frombytes()函数支持从字节数组构建图像。该函数通过指定图像模式来接受模式参数,在本例中是字符串“RGBA”。我们还在size参数中将图像大小指定为一个元组,并在数据参数中指定原始字节数据。

注意,这个函数接受整数形式的大小。最好将Camera小部件的宽度和高度转换成整数。这是因为Camera小部件返回的宽度和高度可能是float。使用save()功能保存frombytes()功能返回的图像。

清单 3-26 中显示了与之前的 Python 代码一起用于构建桌面应用的 KV 文件。除了删除了TextInput小部件之外,这个文件与上一个例子中最后一个应用使用的文件相同,因为我们目前没有兴趣联系服务器。

BoxLayout:
    orientation: "vertical"
    Camera:
        id: camera
        size_hint_y: 18
        resolution: (1280, 720)
        play: True
    Button:
        id: capture
        text: "Capture"
        size_hint_y: 1
        on_press: app.capture()

Listing 3-26KV File for the Application in Listing 3-25

运行应用后,窗口如图 3-19 所示。

图 3-19

使用 get_region()捕获并保存到设备内存中的图像

如果你点击按钮,get_region().pixels将捕获Camera小部件的区域并保存在内存中。

到目前为止,我们已经使用get_region()方法成功捕获了一幅图像,并将其保存到设备内存中。下一步是将这个图像发送到 Flask 服务器。

使用 HTTP POST 消息将捕获的图像发布到 Flask 服务器

在前面的例子中,图像以字节数组的形式保存在内存中,并准备好发送给服务器。清单 3-27 显示了将数组发送到 Flask 服务器的 Python 代码。

import kivy.app
import requests

class PycamApp(kivy.app.App):

    def capture(self):
        camera = self.root.ids['camera']
        print(camera.x, camera.y)

        pixels_data = camera.texture.get_region(x=camera.x, y=camera.y, width=camera.resolution[0], height=camera.resolution[1]).pixels

        ip_addr = self.root.ids['ip_address'].text
        url = 'http://'+ip_addr+':6666/'
        files = {'media': pixels_data}

        try:
            self.root.ids['capture'].text = "Trying to Establish a Connection..."
            requests.post(url, files=files)
            self.root.ids['capture'].text = "Capture Again!"
        except requests.exceptions.ConnectionError:
            self.root.ids['capture'].text = "Connection Error! Make Sure Server is Active."

    def build(self):
        pass

app = PycamApp()
app.run()

Listing 3-27Uploading the Bytes Array to the Server

get_region()方法返回的字节数组被插入到将被发送到服务器的字典中。注意,字典被分配给了requests.post()函数的files参数。这意味着字节数组将作为文件在服务器上接收。

代码中的其他部分都按照前面的例子中讨论的那样工作。请注意,我们对在客户端使用 PIL 不再感兴趣。

在添加了TextInput小部件之后,客户端应用的 KV 文件如清单 3-28 所示。

BoxLayout:
    orientation: "vertical"
    Camera:
        id: camera
        size_hint_y: 18
        resolution: (1280, 720)
        play: True
    TextInput:
        text: "192.168.43.231"
        id: ip_address
        size_hint_y: 1
    Button:
        id: capture
        text: "Capture"
        size_hint_y: 1
        on_press: app.capture()

在向服务器发送图像之前

在服务器端,我们将接收上传的字节数组文件。该文件将被读取,以便使用PIL.Image.frombytes()函数将其内容转换成图像,如前所述。为了使用这个函数将字节数组转换成图像,它在size参数中接收返回图像的大小。使用不同的尺寸而不是正确的尺寸可能会降低图像质量。因此,我们需要知道服务器端的图像大小。我们如何做到这一点?

从客户端到服务器的每个POST消息都包含要上传的文件。我们还可以在该消息中发送图像大小。不幸的是,这将在每条消息中发送更多的数据,因为每次上传图像时都会发送图像大小。因为图像大小是固定的,所以我们不需要发送多次。

更好的解决方案是在发送任何图像之前向服务器发送一条POST HTTP 消息。此消息告诉服务器它将在下一条消息中接收的图像的大小。当服务器在新邮件中收到上传的图像时,它可以使用以前收到的图像大小。由于这些原因,一个新的Button小部件被添加到小部件树的末尾。当按下时,Camera小部件的大小将被获取并通过POST HTTP 消息上传到服务器。

清单 3-28 显示了客户端 Kivy 应用修改后的 KV 文件。新按钮被分配了 ID cam_size。当按下此按钮时,将执行 Python 代码中的cam_size()函数。

BoxLayout:
    orientation: "vertical"
    Camera:
        id: camera
        size_hint_y: 18
        resolution: (1280, 720)
        play: True
    TextInput:
        text: "192.168.43.231"
        id: ip_address
        size_hint_y: 1
    Button:
        id: capture
        text: "Capture"
        size_hint_y: 1
        on_press: app.capture()
    Button:
        id: cam_size
        text: "Configure Server"
        size_hint_y: 1
        on_press: app.cam_size()

Listing 3-28KV File for the Client-Side Application

添加cam_size()函数后,客户端 Kivy 应用的 Python 代码如清单 3-29 所示。创建一个字典来保存要上传的图像的宽度和高度(即Camera小部件)。这些数据将作为参数发送到服务器的/camSize目录中,因此使用了requests.post()函数的params参数。如果消息成功发送到服务器,那么新添加的Button widget 就没用了。因此,将使用delete_widget()功能将其从窗口小部件树中删除。

import kivy.app
import requests

class PycamApp(kivy.app.App):

    def cam_size(self):
        camera = self.root.ids['camera']
        cam_width_height = {'width': camera.resolution[0], 'height': camera.resolution[1]}

        ip_addr = self.root.ids['ip_address'].text
        url = 'http://'+ip_addr+':6666/camSize'

        try:
            self.root.ids['cam_size'].text = "Trying to Establish a Connection..."
            requests.post(url, params=cam_width_height)
            self.root.ids['cam_size'].text = "Done."
            self.root.remove_widget(self.root.ids['cam_size'])
        except requests.exceptions.ConnectionError:
            self.root.ids['cam_size'].text = "Connection Error! Make Sure Server is Active."

    def capture(self):
        camera = self.root.ids['camera']
        print(camera.x, camera.y)

        pixels_data = camera.texture.get_region(x=camera.x, y=camera.y, width=camera.resolution[0], height=camera.resolution[1]).pixels

        ip_addr = self.root.ids['ip_address'].text
        url = 'http://'+ip_addr+':6666/'
        files = {'media': pixels_data}

        try:
            self.root.ids['capture'].text = "Trying to Establish a Connection..."
            requests.post(url, files=files)
            self.root.ids['capture'].text = "Capture Again!"
        except requests.exceptions.ConnectionError:
            self.root.ids['capture'].text = "Connection Error! Make Sure Server is Active."

    def build(self):
        pass

app = PycamApp()
app.run()

Listing 3-29Informing the Server by the Width and Height of the Captured Images

图 3-20 显示了客户端应用窗口。

图 3-20

添加服务器配置按钮后的应用窗口

准备好客户端 Kivy 应用后,下一步是准备服务器端 Flask 应用。

在服务器上处理接收到的图像

在客户端成功构建 Kivy 应用后,下一步是准备服务器端 Flask 应用。它首先接收上传图像的尺寸(宽度和高度),然后接收上传的图像。应用的 Python 代码如清单 3-30 所示。

有称为cam_widthcam_height的变量是在任何函数之外定义的。这些变量保存图像的宽度和高度。当 KV 文件中 ID 为cam_size的按钮被按下时,URL 为/camSize的路由装饰器执行cam_size()功能。在这个函数中,使用flask.request.args字典从客户端接收Camera小部件的宽度和高度作为参数。它们被分配给先前创建的两个变量。为了使用这些变量而不是创建新的变量,我们在函数的开头将它们定义为global

在分配数据之前,记得将接收数据的类型转换为integerint(float())变量保证转换没有错误。

import flask
import PIL.Image

app = flask.Flask(import_name="FlaskUpload")

cam_width = 0
cam_height = 0

@app.route('/camSize', methods = ['POST'])
def cam_size():
    global cam_width
    global cam_height

    cam_width = int(float(flask.request.args["width"]))
    cam_height = int(float(flask.request.args["height"]))

    print('Width',cam_width,'& Height',cam_height,'Received Successfully.')

    return "OK"

@app.route('/', methods = ['POST'])
def upload_file():
    global cam_width
    global cam_height

    file_to_upload = flask.request.files['media'].read()

    image = PIL.Image.frombytes(mode="RGBA", size=(cam_width, cam_height), data=file_to_upload)
    image.save('out.png')

    print('File Uploaded Successfully.')

    return 'SUCCESS'

app.run(host="192.168.43.231", port=6666, debug=True)

Listing 3-30Restoring Images from the Received Bytes Arrays at the Server

upload_file()功能类似于本章前面使用的功能。它使用flask.request.files字典接收上传的文件。使用read()功能读取上传的文件。使用PIL.Image.frombytes()功能将接收到的文件转换成图像。仅出于调试目的,在开发客户端应用时,图像将保存到 PNG 文件中。

准备好客户端和服务器应用后,我们可以根据图 3-21 进行测试。通过运行客户端 Kivy 应用并按下 ID 为cam_size的按钮,图像大小(宽度和高度)将被发送到服务器,该按钮将被删除。按下另一个按钮后,图像将被捕获并作为字节数组文件发送到服务器。该文件在服务器上被读取,并返回字节数组。使用PIL.Image.frombytes()函数将该数组转换成图像。图 3-21 显示一切正常。

图 3-21

上传字节数组后的图像尺寸被发送到服务器

此时,Kivy 客户端和 Flask 服务器应用都可以很好地相互协作,以便上传单个图像。为了从客户机向服务器连续发送图像,我们可以构建一个 HTML 页面来显示接收到的图像。

使用 HTML 页面保存和显示接收到的图像

到目前为止,我们成功地构建了客户端和服务器端应用。客户端应用发送图像,其宽度和高度也被发送到服务器。客户端将图像作为字节数组发送给服务器。服务器使用接收到的宽度和高度将数组保存为 PNG 文件。

因为我们对显示接收到的图像感兴趣,所以我们将构建一个非常简单的 HTML 页面,该页面包含一个<img>元素,其中要显示的上传图像的路径被分配给了src属性。在接收并保存上传的图像为 PNG 文件后,服务器应用根据上传图像的路径更新src属性后创建 HTML 文件。然后,使用webbrowser模块的open()功能在网络浏览器中打开 HTML 页面。该函数接受页面 URL 作为参数。更新后的服务器应用如清单 3-31 所示。

import flask
import PIL.Image
import webbrowser

app = flask.Flask(import_name="FlaskUpload")

cam_width = 0
cam_height = 0

@app.route('/camSize', methods = ['POST'])
def cam_size():
    global cam_width
    global cam_height

    cam_width = int(float(flask.request.args["width"]))
    cam_height = int(float(flask.request.args["height"]))

    print('Width',cam_width,'& Height',cam_height,'Received Successfully.')

    return "OK"

@app.route('/', methods = ['POST'])
def upload_file():
    global cam_width
    global cam_height

    file_to_upload = flask.request.files['media'].read()

    image = PIL.Image.frombytes(mode="RGBA", size=(cam_width, cam_height), data=file_to_upload)
    image.save('out.png')

    print('File Uploaded Successfully.')

    html_code = '<html><head><title>Displaying Uploaded Image</title></head><body><h1>Displaying Uploaded Image</h1><img src="out.png" alt="Uploaded Image at the Flask Server"/></body></html>'

    html_url = "/home/ahmedgad/Desktop/FlaskUpload/test.html"
    f = open(html_url,'w')
    f.write(html_code)
    f.close()

    webbrowser.open(html_url)

    return 'SUCCESS'

app.run(host="192.168.43.231", port=6666, debug=True)

Listing 3-31Displaying the Restored Images on the Server on an HTML Page

HTML 代码作为文本写在html_code变量中。清单 3-32 中显示了更好的可视化格式代码。除了<img>元素之外,<h1>元素在它上面打印一个标题。HTML 代码根据html_url变量中指定的路径写入 HTML 文件。

<html>
<head>
<title>Displaying Uploaded Image</title>
</head>
<body>
<h1>Uploaded Image to the Flask Server</h1>
<img src="out.png" alt="Uploaded Image at the Flask Server"/>
</body>
</html>

Listing 3-32HTML Page to Display the Images

在客户端捕获图像,上传到服务器,更新并显示 HTML 页面后,结果如图 3-22 所示。请注意,应用会在浏览器中为每个上传的图像打开一个新的选项卡。这将是一个麻烦,当我们试图不断上传图像。

图 3-22

将上传的图像显示到服务器上的 HTML 页面

显示接收的图像而不保存

在客户端应用中,为了避免保存每个上传的图像,我们使用了get_region()方法。我们需要对服务器端应用做同样的事情。

目前,服务器接收字节数组,使用 PIL 将其保存到 PNG 文件,并在 web 浏览器中显示。我们需要删除将图像保存为 PNG 文件的中间步骤。因此,我们需要将上传的图像以字节数组的形式直接显示在 web 浏览器上。这是通过将图像的字节数组内联到作为 base64 编码图像的<img> HTML 元素的src属性中来实现的。

为了将字节数组编码为 base64,使用 base64 Python 模块。确保它安装在您的机器上。清单 3-33 显示了更新后的服务器端应用。

请注意,我们不再需要使用 PIL。这是因为我们对将字节数组转换成图像或保存图像都不感兴趣。

import flask
import webbrowser
import base64

app = flask.Flask(import_name="FlaskUpload")

cam_width = 0
cam_height = 0

@app.route('/camSize', methods = ['POST'])
def cam_size():
    global cam_width
    global cam_height

    cam_width = int(float(flask.request.args["width"]))
    cam_height = int(float(flask.request.args["height"]))

    print('Width',cam_width,'& Height',cam_height,'Received Successfully.')

    return "OK"

@app.route('/', methods = ['POST'])
def upload_file():
    global cam_width
    global cam_height

    file_to_upload = flask.request.files['media'].read()

    print('File Uploaded Successfully.')

    im_base64 = base64.b64encode(file_to_upload)

    html_code = '<html><head><meta http-equiv="refresh" content="1"><title>Displaying Uploaded Image</title></head><body><h1>Uploaded Image to the Flask Server</h1><img src="data:;base64,' + im_base64.decode(
    'utf8') + '" alt="Uploaded Image at the Flask Server"/></body></html>'

    html_url = "/home/ahmedgad/Desktop/FlaskUpload/test.html"
    f = open(html_url,'w')
    f.write(html_code)
    f.close()

    webbrowser.open(html_url)

    return 'SUCCESS'

app.run(host="192.168.43.231", port=6666, debug=True)

Listing 3-33Inlining the Bytes Array Into the src Attribute of the <img> HTML Tag

下面显示了使用b64encode()函数将图像转换为 base64 编码的代码行。该函数接受一个字节数组,因此它由file_to_upload变量中上传的数据提供。

im_base64 = base64.b64encode(file_to_upload)

im_base64变量保存 base64 编码的图像。这个变量中的值作为一个数据 URL 被分配给<img>元素的src属性。使用的网址是data:;base64,。注意,URL 不直接接受字节数组,而是在使用encode('utf8')函数将其转换成字符串之后接受。你可以阅读更多关于数据 URL 的内容。

记住,我们必须将上传的字节数组图像转换成 PIL 图像,以便旋转它。然后,PIL 图像被转换回字节数组,以便使用 base64 进行编码。通过这样做,我们不必将图像保存为外部文件。

不断上传图像到服务器

以前,单个图像被上传到服务器。现在,我们想不断上传图像到服务器。要做到这一点,客户端和服务器端的应用都将发生变化。

点击客户端 Kivy 应用的捕获按钮后,应用进入一个无限的while循环。在每次迭代中,一个摄像机图像被捕获并通过POST HTTP 消息发送到服务器。更新后的 Kivy 应用如列表 3-34 所示。

import kivy.app
import requests

class PycamApp(kivy.app.App):

    def cam_size(self):
        camera = self.root.ids['camera']
        cam_width_height = {'width': camera.resolution[0], 'height': camera.resolution[1]}

        ip_addr = self.root.ids['ip_address'].text
        url = 'http://'+ip_addr+':6666/camSize'

        try:
            self.root.ids['cam_size'].text = "Trying to Establish a Connection..."
            requests.post(url, params=cam_width_height)
            self.root.ids['cam_size'].text = "Done."
            self.root.remove_widget(self.root.ids['cam_size'])
        except requests.exceptions.ConnectionError:
            self.root.ids['cam_size'].text = "Connection Error! Make Sure Server is Active."

    def capture(self):
        while True:
            camera = self.root.ids['camera']

            pixels_data = camera.texture.get_region(x=camera.x, y=camera.y, width=camera.resolution[0], height=camera.resolution[1).pixels

            ip_addr = self.root.ids['ip_address'].text
            url = 'http://'+ip_addr+':6666/'
            files = {'media': pixels_data}

            try:
                self.root.ids['capture'].text = "Trying to Establish a Connection..."
                requests.post(url, files=files)
                self.root.ids['capture].text = "Capture Again!"
            except requests.exceptions.ConnectionError:
                self.root.ids['capture'].text = "Connection Error! Make Sure Server is Active."

    def build(self):
        pass

app = PycamApp()
app.run()

Listing 3-34Client-Side Application for Continuously Capturing and Uploading Images to the Server

在服务器端 Flask 应用中,为每个上传的图像打开一个新的浏览器选项卡。当我们想要持续上传图像时,这是一个问题。为了解决这个问题,我们使用一个名为html_opened的标志变量。默认设置为False,表示不打开标签页。上传第一张图片后,它将被设置为True,因此应用将不会打开任何其他标签。清单 3-35 中显示了更新后的 Flask 应用。

import flask
import base64
import webbrowser

app = flask.Flask(import_name="FlaskUpload")

cam_width = 0
cam_height = 0

html_opened = False

@app.route('/camSize', methods = ['GET', 'POST'])
def cam_size():
    global cam_width
    global cam_height

    cam_width = int(float(flask.request.args["width"]))
    cam_height = int(float(flask.request.args["height"]))

    print('Width',cam_width,'& Height',cam_height,'Received Successfully.')

    return "OK"

@app.route('/', methods = ['POST'])
def upload_file():
    global cam_width
    global cam_height
    global html_opened

    file_to_upload = flask.request.files['media'].read()

    print('File Uploaded Successfully.')

    im_base64 = base64.b64encode(file_to_upload)

    html_code = '<html><head><meta http-equiv="refresh" content="0.5"><title>Displaying Uploaded Image</title></head><body><h1>Uploaded Image to the Flask Server</h1><img src="data:;base64,'+im_base64.decode('utf8')+'" alt="Uploaded Image at the Flask Server"/></body></html>'

    html_url = "/home/ahmedgad/Desktop/FlaskUpload/templates/test.html"
    f = open(html_url,'w')
    f.write(html_code)
    f.close()

    if html_opened == False:
        webbrowser.open(html_url)
        html_opened = True

    return "SUCCESS"

app.run(host="192.168.43.231", port=6666, debug=True)

Listing 3-35Server-Side Application for Continuously Receiving the Uploaded Images and Displaying Them in the Web Browser

服务器应用的另一个变化是使用了一个<meta>标签,每 0.5 秒刷新一次 HTML 页面。

使用时钟控制图像上传速率

前面的应用使用 UI 线程将图像上传到服务器。这会挂起应用,并阻止用户与其小部件进行交互。

最好在另一个线程中而不是 UI 线程中执行耗时的操作。在我们的应用中,这种解决方案是不可行的。这是因为如果我们创建了一个新的线程,它仍然需要在每次捕获图像时从 UI 线程访问Camera小部件。

另一个解决方案是在一个新的线程而不是 UI 线程中将图像上传到服务器。这使得应用的 UI 比以前响应更快。此外,我们可以通过控制上传图像到服务器的速度来减缓这个过程。

使用kivy.clock.Clock对象,我们可以安排一个函数调用在将来执行。因为我们对将来多次执行该函数感兴趣,所以kivy.clock.Clock.schedule_interval()函数是一个不错的选择。它接受要执行的函数以及两次执行之间的秒数。Kivy 应用的修改代码如清单 3-36 所示。间隔设置为 0.5 秒。记得匹配schedule_interval()函数中上传图片和<meta>标签中刷新 HTML 页面的秒数。

import kivy.app
import requests
import kivy.clock
import kivy.uix.screenmanager
import threading

class Configure(kivy.uix.screenmanager.Screen):
    pass

class Capture(kivy.uix.screenmanager.Screen):
    pass

class PycamApp(kivy.app.App):
    num_images = 0

    def cam_size(self):
        camera = self.root.ids['camera']
        cam_width_height = {'width': camera.resolution[0], 'height': camera.resolution[1]}

        ip_addr = self.root.ids['ip_address'].text
        port_number = self.root.ids['port_number'].text
        url = 'http://' + ip_addr + ':' + port_number + '/camSize'

        try:
            self.root.ids['cam_size'].text = "Trying to Establish a Connection..."
            requests.post(url, params=cam_width_height)
            self.root.ids['cam_size'].text = "Done."
            self.root.current = "capture"
        except requests.exceptions.ConnectionError:
            self.root.ids['cam_size'].text = "Connection Error! Make Sure Server is Active."

    def capture(self):
        kivy.clock.Clock.schedule_interval(self.upload_images, 0.5)

    def upload_images(self, ∗args):
        self.num_images = self.num_images + 1
        print("Uploading image", self.num_images)

        camera = self.root.ids['camera']

        print("Image Size ", camera.resolution[0], camera.resolution[1])
        print("Image corner ", camera.x, camera.y)

        pixels_data = camera.texture.get_region(x=camera.x, y=camera.y, width=camera.resolution[0], height=camera.resolution[1]).pixels

        ip_addr = self.root.ids['ip_address'].text
        port_number = self.root.ids['port_number'].text
        url = 'http://' + ip_addr + ':' + port_number + '/'
        files = {'media': pixels_data}

        t = threading.Thread(target=self.send_files_server, args=(files, url))
        t.start()

    def build(self):
        pass

    def send_files_server(self, files, url):
        try:
            requests.post(url, files=files)
        except requests.exceptions.ConnectionError:
            self.root.ids['capture'].text = "Connection Error! Make Sure Server is Active."

app = PycamApp()
app.run()

Listing 3-36Uploading the Images in a New Thread

在这个例子中,创建了一个名为upload_images()的新函数来保存负责捕获和上传每张图片的代码。该函数为每个上传的图像增加一个名为num_images的变量。在该功能中,仅使用camera.texture.get_region()捕捉图像。为了上传它,在这个函数的末尾创建了一个新线程。

使用threading模块中的Thread类,我们可以创建新的线程。在该类的构造函数中,指定了线程target,它可以是线程运行后调用的函数。如果该函数接受参数,我们可以使用构造函数的args参数传递它们。

在我们的应用中,创建了一个名为send_files_server()的回调函数,它接受上传到服务器的图像以及服务器 URL。

运行 Kivy 和 Flask 应用后,打印到终端的消息表明执行成功。

Kivy 应用的终端执行如图 3-23 所示。

图 3-23

客户端应用的终端执行

图 3-24 显示了烧瓶应用的输出。

图 3-24

服务器端 Flask 应用的终端执行

我们现在已经创建了一个桌面 Kivy 应用,它访问相机,连续捕获图像,将它们上传到 Flask 服务器,并在 web 浏览器中显示捕获的图像。下一步是构建 Android 应用。

构建实时摄像头预览 Android 应用

我们需要对客户端桌面 Kivy 应用进行一些更改,以使其适合作为 Android 应用。

我们必须使用Rotate上下文指令将Camera小部件旋转-90 度,因为 Android 摄像头默认旋转 90 度。这在本章前面已经讨论过了。旋转小部件的 KV 文件如清单 3-37 所示。

BoxLayout:
    orientation: "vertical"
    Camera:
        id: camera
        size_hint_y: 18
        resolution: (1280, 720)
        play: True
        canvas.before:
            PushMatrix:
            Rotate:
                angle: -90
                origin: root.width/2, root.height/2
        canvas.after:
            PopMatrix:
    TextInput:
        text: "192.168.43.231"
        id: ip_address
        size_hint_y: 1
    Button:
        id: capture
        text: "Capture"
        size_hint_y: 1
        on_press: app.capture()
    Button:
        id: cam_size
        text: "Configure Server"
        size_hint_y: 1
        on_press: app.cam_size()

Listing 3-37Rotating the Image 90 Degrees for the Android Application

请注意,旋转Camera小部件并不意味着上传到服务器的图像也会被旋转。这个操作只是旋转显示摄像机图像的Camera小部件。捕获的图像仍然旋转 90 度。因此,我们需要修改 Flask 应用,以便在 web 浏览器中显示之前将每个捕获的图像旋转-90°。如清单 3-38 所示。字节数组被转换成旋转 90 度的 PIL 图像。最后,旋转后的图像被转换回字节数组,以便根据 base64 进行编码。

import flask
import base64
import PIL.Image
import webbrowser

app = flask.Flask(import_name="FlaskUpload")

cam_width = 0
cam_height = 0

html_opened = False

@app.route('/camSize', methods = ['GET', 'POST'])
def cam_size():
    global cam_width
    global cam_height

    cam_width = int(float(flask.request.args["width"]))
    cam_height = int(float(flask.request.args["height"]))

    print('Width',cam_width,'& Height',cam_height,'Received Successfully.')

    return "OK"

@app.route('/', methods = ['POST'])
def upload_file():
    global cam_width
    global cam_height
    global html_opened

    file_to_upload = flask.request.files['media'].read()

    image = PIL.Image.frombytes(mode="RGBA", size=(cam_width, cam_height), data=file_to_upload)
    image = image.rotate(-90)

    print('File Uploaded Successfully.')

    im_base64 = base64.b64encode(image.tobytes())

    html_code = '<html><head><meta http-equiv="refresh" content="0.5"><title>Displaying Uploaded Image</title></head><body><h1>Uploaded Image to the Flask Server</h1><img src="data:;base64,'+im_base64.decode('utf8')+'" alt="Uploaded Image at the Flask Server"/></body></html>'

    html_url = "/home/ahmedgad/Desktop/FlaskUpload/templates/test.html"
    f = open(html_url,'w')
    f.write(html_code)
    f.close()

    if html_opened == False:
        webbrowser.open(html_url)
        html_opened = True

    return "SUCCESS"

app.run(host="192.168.43.231", port=6666, debug=True)

Listing 3-38Rotating the Captured Images at the Server by 90 Degrees

准备好客户端和服务器端应用后,我们可以根据下面的终端命令构建 Android 应用。

ahmedgad@ubuntu:~/Desktop/NewApp$ buildozer android debug deploy run logcat

确保将路径更改为应用根目录,其中存在buildozer.spec文件,并激活虚拟环境(如果您在虚拟环境中准备了开发环境)。

Android 应用的窗口如图 3-25 所示。

图 3-25

Android 应用的窗口,用于持续上传图像

点击捕获按钮后,Android 应用会连续捕获图像并上传到服务器,在服务器上以 HTML 页面显示。图 3-26 显示了显示的图像之一。

图 3-26

在服务器的网络浏览器中显示的上传图像

摘要

本章讨论了通过Camera小部件访问 Android 摄像头。在构建 Android 应用之前,我们创建了一个桌面应用来确保一切按预期运行。我们使用 Buildozer 构建了 Android 应用。为了获得访问 Android 摄像头的许可,我们必须更新buildozer.init文件中的android.permissions字段。因为安卓摄像头默认旋转 90 度,所以必须旋转回来。这是使用基维画布完成的。讨论了三个画布实例— canvascanvas.beforecanvas.after。为了将给定指令的效果限制在某些小部件上,我们讨论了PushMatrixPopMatrix指令。

在适当的角度预览相机后,图像被捕获,以便上传到 HTTP 服务器。服务器是使用 Flask 创建的,并在台式计算机上运行。使用服务器的 IPv4 地址和端口号,requests Python 库将使用 Kivy Android 应用和 HTTP POST消息上传捕获的图像。

在本章的最后,我们在服务器的网络浏览器中预览了 Android 摄像头。为了节省时间,图像以字节数组的形式保存在设备存储器中,而不是保存在设备存储器中。这样的字节数组然后被上传到服务器。然后,服务器解释这些字节数组,并通过网页浏览器中的 HTML 页面显示图像。

在下一章中,通过将按钮分离到不同的屏幕中,为实时预览项目创建了一个更方便的设计。Kivy 支持用于构建屏幕的Screen类和用于管理此类屏幕的ScreenManager类。我们可以从一个屏幕导航到另一个屏幕。为了理解如何创建一个有多个屏幕的应用,下一章从讨论如何创建定制的小部件开始。


版权声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐