深度学习和 CNN 计算机视觉应用实践指南(四)

原文:Practical Computer Vision Applications Using Deep Learning with CNNs

协议:CC BY-NC-SA 4.0

七、部署预训练模型

在构建 DL 模型的过程中,创建模型是最难的一步,但它不是终点。为了从创建的模型中获益,用户应该远程访问它们。用户的反馈将有助于改进模型性能。

本章讨论如何在线部署预训练模型以供互联网用户访问。使用 Flask micro web framework,使用 Python 创建 web 应用。使用 HTML(超文本标记语言)、CSS(级联样式表)和 JavaScript,构建简单的网页,以允许用户向服务器发送和接收 HTTP(超文本传输协议)请求。用户使用 web 浏览器访问应用,并能够将图像上传到服务器。基于部署的模型,图像被分类,并且其类别标签被返回给用户。此外,还创建了一个 Android 应用来访问 web 服务器。本章假设读者对 HTML、CSS、JavaScript 和 Android 有基本的了解。读者可以按照此链接中的说明安装烧瓶( http://flask.pocoo.org/docs/1.0/installation/ )。

应用概述

图 7-1 总结了本章的目标应用,它扩展了第六章中的步骤:使用 TF 构建 CNN 的数据流图,然后使用 CIFAR10 数据集对其进行训练;最后,保存训练好的模型,为部署做好准备。使用 Flask,可以创建一个监听来自客户端的 HTTP 请求的 web 应用。客户端从使用 HTML、CSS 和 JavaScript 创建的网页访问 web 应用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-1

应用概述

服务器加载保存的模型,打开一个会话,并等待来自客户端的请求。客户端使用 web 浏览器打开网页,该网页允许将图像上传到服务器进行分类。服务器根据大小确保映像属于 CIFAR10 数据集。之后,将图像输入模型进行分类。由模型预测的标签在对客户端的响应中被返回。最后,客户端在网页上显示标签。为了针对 Android 设备进行定制,创建了向服务器发送 HTTP 请求并接收分类标签的 Android 应用。

在这一章中,我们将介绍应用中涉及的每个步骤,直到成功完成为止。

烧瓶简介

Flask 是一个用于构建 web 应用的微框架。尽管是微型的,但它不支持其他框架所支持的一些功能。它被称为“微”,因为它具有构建应用所需的核心需求。稍后使用扩展,您可以添加所需的功能。Flask 让用户决定使用什么。例如,它不附带特定的数据库,而是让用户自由选择使用哪个数据库。

Flask 使用 WSGI (Web 服务器网关接口)。WSGI 是服务器处理来自 Python web 应用的请求的方式。它被视为服务器和应用之间的通信通道。服务器收到请求后,WSGI 处理请求,并将其发送给用 Python 编写的应用。WSGI 接收应用的响应,并将其返回给服务器。然后,服务器响应客户端。Flask 使用 Werkzeug,这是一个用于实现请求和响应的 SWGI 实用程序。Flask 还使用 jinja2,这是用于构建模板网页的模板引擎,这些网页随后会动态填充数据。

为了开始使用 Flask,让我们根据清单 7-1 讨论最小 Flask 应用。构建 Flask 应用要做的第一件事是从 Flask 类创建一个实例。使用类构造函数创建了app实例。构造函数的强制import_name参数非常重要。它用于定位应用资源。如果在FlaskApp\firstApp.py中找到该应用,则将该参数设置为FlaskApp。例如,如果在应用目录下有一个 CSS 文件,则此参数用于定位该文件。

import flask

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

@app.route(rule="/")
def testFunc():
    return "Hello"

app.run()

Listing 7-1Minimal Flask Application

Flask 应用由一组函数组成,每个函数都与一个 URL(统一资源定位器)相关联。当客户端导航到一个 URL 时,服务器向应用发送请求以响应客户端。应用使用与该 URL 相关联的查看功能进行响应。视图函数的返回是呈现在网页上的响应。这就留下了一个问题:我们如何将一个函数与一个 URL 关联起来?幸运的是,答案很简单。

route()装饰器

首先,该函数是一个可以接受参数的常规 Python 函数。在清单 7-1 中,函数被调用testFunc(),但是还不接受任何参数。它返回字符串Hello。这意味着当客户端访问与该函数相关联的 URL 时,字符串Hello将呈现在屏幕上。使用route()装饰器将 URL 与函数相关联。它被称为“路由”,因为它的工作方式类似于路由器。路由器接收输入消息并决定遵循哪个输出接口。此外,装饰器接收一个输入 URL 并决定调用哪个函数。

route()装饰器接受一个名为rule的参数,该参数表示与装饰器下面的视图函数相关联的 URL。根据清单 7-1,route()装饰器将代表主页的 URL /关联到名为testFunc()的视图函数。

完成这个简单的应用后,下一步是通过使用 Flask 类的run()方法运行脚本来激活它。运行应用的结果如图 7-2 所示。根据输出,服务器默认监听 IP(互联网协议)地址127.0.0.1,这是一个环回地址。这意味着服务器只是在端口 5000 上侦听来自本地主机的请求。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-2

运行第一个 Flask 应用后的控制台输出

当使用网络浏览器访问位于127.0.0.1:5000/地址的服务器时,将调用testFunc()函数。其输出呈现在网络浏览器上,如图 7-3 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-3

访问与testFunc()功能相关的 URL

我们可以使用run()方法的hostport参数覆盖 IP 和端口的默认值。覆盖这些参数的默认值后,run()方法如下:

app.run(host="127.0.0.5", port=6500)

图 7-4 显示了将主机设置为127.0.0.5并将端口号设置为 6500 后的结果。只要确保没有应用正在使用选定的端口。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-4

通过覆盖run()方法的默认值来监听不同的主机和端口

对于服务器收到的每个请求,HTTP 请求的方法、URL 和响应代码都打印在控制台上。例如,当访问主页时,请求返回 200,这意味着页面被成功定位。访问一个不存在的页面比如127.0.0.5:6500/abc会返回 404 作为响应代码,意味着没有找到这个页面。这有助于调试应用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-5

服务器收到的请求

run()方法的另一个有用的参数名为debug。它是一个布尔参数,用于决定是否打印调试信息。它默认为 False。当这样一个参数被设置为 True 时,我们就不必为代码中的每一个变化重新启动服务器。这在应用的开发中非常有用。每次更改后只需保存应用的 Python 文件,服务器就会自动重新加载。如图 7-6 ,服务器开始使用端口号 6500。更改为 6300 后,服务器会自动重新加载以监听新端口。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-6

当调试处于活动状态时,每次更改后自动重新加载服务器

添加规则 url 方法

之前,URL 已经使用route()装饰器绑定到函数。装饰器在 Flask 类内部调用add_url_rule()方法。这个方法和其他的装饰器做同样的工作。我们可以根据清单 7-2 直接使用这个方法。它像以前一样接受了rule参数,但是增加了view_func参数。它指定哪个视图功能与该规则相关联。它被设置为函数名,即testFunc。当我们使用route()装饰器时,函数是隐式已知的。函数正好在装饰器的下面。请注意,对该方法的调用不必正好在函数下面。运行这段代码会返回与之前相同的结果。

import flask

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

def testFunc():
    return "Hello"
app.add_url_rule(rule="/", view_func=testFunc)

app.run(host="127.0.0.5", port=6300, debug=True)

Listing 7-2Using the add_url_rule() Method

可变规则

以前的规则是静态的。可以向规则中添加可变部分。它被视为一个参数。可变部分添加在两个尖括号<>之间规则的静态部分之后。我们可以修改前面的代码,根据清单 7-3 接受一个表示名称的变量参数。主页的规则现在是/<name>,而不仅仅是/。如果客户端导航到 URL 127.0.0.5:6300/Gad,那么name被设置为Gad

import flask

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

def testFunc(name):
    return "Hello : " + name
app.add_url_rule(rule="/<name>", view_func=testFunc, endpoint="home")

app.run(host="127.0.0.5", port=6300, debug=True)

Listing 7-3Adding Variable Part to the Rule

请注意,view 函数中必须有一个参数来接受 URL 的可变部分。因此,testFunc()被修改为接受一个与规则中定义的名称相同的参数。函数的返回被修改为也返回参数name的值。图 7-7 显示了使用变量法则后的结果。改变变量部分,访问主页,就会改变输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-7

在规则中使用可变部分

可以在规则中使用多个可变部分。根据清单 7-4 ,该规则接受两个参数,代表由-分隔的名和姓。

import flask

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

def testFunc(fname, lname):
    return "Hello : " + fname + " " + lname
app.add_url_rule(rule="/<fname>-<lname>", view_func=testFunc, endpoint="home")

app.run(host="127.0.0.5", port=6300, debug=True)

Listing 7-4Using More Than One Variable Part

访问 URL 127.0.0.5:6300/Ahmed-Gad会将fname设置为Ahmed,将lname设置为Gad。结果如图 7-8 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-8

规则中有多个可变部分

端点

add_url_rule()方法接受名为endpoint的第三个参数。它是规则的标识符,有助于多次重用同一规则。注意,这个论点也存在于route()装饰器中。默认情况下,端点的值被设置为 view 函数。这里有一个场景,其中endpoint很重要。

假设网站有两个页面,每个页面分配一个规则。第一条规则是/,第二条规则是/addNums/<num1>-<num2>。第二页有两个代表两个数字的参数。这些数字加在一起,结果返回到主页进行渲染。清单 7-5 给出了创建这些规则及其视图功能的代码。给testFunc()视图函数一个等于homeendpoint值。

add_func()视图函数接受两个参数,它们是与之相关的规则的可变部分。因为这些参数的值是字符串,所以使用int()函数将它们的值转换成整数。然后它们被一起加入到num3变量中。这个函数的返回不是数字,而是使用redirect()方法重定向到另一个页面。这种方法接受重定向位置。

import flask

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

def testFunc(result):
    return "Result is : " + result
app.add_url_rule(rule="/<result>", view_func=testFunc, endpoint="home")

def add_func(num1, num2):
    num3 = int(num1) + int(num2)
    return flask.redirect(location=flask.url_for("home", result=num3))
app.add_url_rule(rule="/addNums/<num1>-<num2>", view_func=add_func)

app.run(host="127.0.0.5", port=6300, debug=True)

Listing 7-5Using Endpoint to Redirect Between Pages

我们可以简单地使用端点来返回 URL,而不是对 URL 进行硬编码。使用from_url()方法从端点返回 URL。除了规则接受的任何变量之外,它还接受规则的端点。因为主页规则接受一个名为result的变量,所以我们必须在from_url()方法中添加一个名为result的参数,并为其赋值。分配给这种变量的值是num3。通过导航到 URL 127.0.0.5:6300/addNums/1-2,数字 1 和 2 相加,结果是 3。该函数然后重定向到主页,在这里规则的result变量被设置为等于 3。

使用端点比硬编码 URL 更容易。我们可以简单地将redirect()方法的location参数赋给规则/,但是不推荐这样做。假设主页的 URL 从/更改为/home,那么我们必须在对主页的每个引用中应用这一更改。而且,假设 URL 很长,比如127.0.0.5:6300/home/page1。每次我们需要引用这个 URL 时,都要键入它,这很烦人。端点被视为 URL 的抽象。

证明使用端点重要性的另一个例子是,站点管理员可能决定更改页面的地址。如果页面通过复制和粘贴它的 URL 被多次引用,那么我们必须到处改变 URL。使用端点可以避免这个问题。端点不像 URL 那样频繁更改,因此即使页面的 URL 发生变化,站点也将保持活动状态。请注意,不使用端点进行重定向会使向规则传递可变部分变得困难。

清单 7-5 中的代码接受从 URL 添加的输入数字。我们可以创建一个简单的 HTML 表单,允许用户输入这些数字。

HTML 表单

add_url_rule()方法(当然还有route()装饰器)接受另一个名为methods的参数。它接受指定规则响应的 HTTP 方法的列表。该规则可以响应多种类型的方法。

有两种常见的 HTTP 方法:GET 和 POST。GET 方法是默认方法,它发送未加密的数据。POST 方法用于将 HTML 表单数据发送到服务器。让我们创建一个简单的表单,接受两个数字,并将它们发送到 Flask 应用进行添加和呈现。

清单 7-6 给出了创建一个表单的 HTML 代码,该表单除了一个提交类型的输入外,还有两个数字类型的输入。表单方法设置为post。海东的网址是 http://127.0.0.5:6300/form 。该操作表示表单数据将被发送到的页面。有一个规则将该 URL 与一个视图函数相关联,该函数从表单中获取数字,将它们相加,并呈现结果。表单元素的名称非常重要,因为在提交表单后,只有带有 name 属性的元素才会被发送到服务器。元素名称被用作标识符来检索 Flask 应用中的元素数据。

<html>
<header>
<title>HTML Form</title>
</header>
<body>

<form method="post" action="http://127.0.0.5:6300/form">
<span>Num1 </span>
<input type="number" name="num1"><br>
<span>Num2 </span>
<input type="number" name="num2"><br>
<input type="submit" name="Add">
</form>

</body>
</html>

Listing 7-6
HTML Form

HTML 表单如图 7-9 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-9

带有两个数字输入的 HTML 表单

提交表单后,清单 7-7 中的 Flask 应用检索表单数据。规则/formhandle_form()函数相关联。该规则只响应 POST 类型的 HTTP 消息。在函数内部,使用flask.request.form字典返回表单元素。每个 HTML 表单元素的名称被用作该对象的索引,以便返回它们的值。例如,名称为num1的第一个表单元素的值通过使用flask.request.form["num1"]返回。

import flask

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

def handle_form():
    num1 = flask.request.form["num1"]
    num1 = int(num1)
    num2 = flask.request.form["num2"]
    num2 = int(num2)

    result = num1 + num2
    result = str(result)

    return "Result is : " + result
app.add_url_rule(rule="/form", view_func=handle_form, methods=["POST"])

app.run(host="127.0.0.5", port=6300, debug=True)

Listing 7-7Flask Application to Retrieve the HTML Form Data

因为索引flask.request.form对象返回的值是一个字符串,所以必须使用int()函数将它转换成一个整数。将两个数相加后,它们的结果存储在result变量中。这个变量被转换成一个字符串,以便将其值与一个字符串连接起来。连接的字符串由handle_form视图函数返回。渲染结果如图 7-10 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-10

两个数字 HTML 表单元素相加的结果

文件上传

在 Flask 中上传文件非常简单,除了一些变化之外,与前面的例子相似。在 HTML 表单中创建一个类型为file的输入。此外,表单加密类型属性enctype被设置为multipart/form-data。用于上传文件的 HTML 表单的代码如清单 7-8 所示。表格的截图如图 7-11 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-11

用于上传文件的 HTML 表单

<html>
<header>
<title>HTML Form</title>
</header>
<body>

<form method="post" enctype="multipart/form-data" action="http://127.0.0.5:6300/form">
<span>Select File to Upload</span><br>
<input type="file" name="fileUpload"><br>
<input type="submit" name="Add">
</form>

</body>
</html>

Listing 7-8HTML Form for Uploading a File

选择要上传的图像后,它将被发送到根据清单 7-9 创建的 Flask 应用。该规则再次设置为仅响应 POST 类型的 HTTP 消息。之前,我们使用了flask.request.form对象来检索数据字段。现在,我们使用flask.request.files返回要上传的文件的详细信息。表单输入的名称fileUpload被用作该对象的索引,以返回要上传的文件。注意,flask.request是一个全局对象,它从客户机 web 页面接收数据。

为了保存文件,使用filename属性检索其名称。不建议根据用户提交的文件名保存文件。一些文件名被设置成伤害服务器。为了安全保存文件,使用了werkzeug.secure_filename()功能。记得导入werkzeug模块。

import flask, werkzeug

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

def handle_form():
    file = flask.request.files["fileUpload"]
    file_name = file.filename
    secure_file_name = werkzeug.secure_filename(file_name)
    file.save(dst=secure_file_name)

    return "File uploaded successfully."
app.add_url_rule(rule="/form", view_func=handle_form, methods=["POST"])

app.run(host="127.0.0.5", port=6300, debug=True)

Listing 7-9Flask Application to Upload Files to the Server

安全文件名返回到secure_file_name变量。最后,通过调用save()方法永久保存文件。这种方法接受保存文件的目标位置。因为只使用了文件名,所以它将保存在 Flask 应用 Python 文件的当前目录中。

HTML 内部烧瓶应用

前一个视图函数的返回输出只是一个显示在 web 页面上的文本,没有任何格式。Flask 支持在 Python 代码中生成 HTML 内容,这有助于更好地呈现结果。清单 7-10 给出了一个例子,其中tesFunc()视图函数的返回结果是 HTML 代码,其中

元素呈现结果。数字

7-12 shows the result.

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-12

使用 HTML 代码格式化视图函数的输出

import flask, werkzeug

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

def testFunc():
    return "<html><body><h1>Hello</h1></body></html>"
app.add_url_rule(rule="/", view_func=testFunc)

app.run(host="127.0.0.5", port=6300, debug=True)

Listing 7-10Generating HTML Inside Python

在 Python 代码中生成 HTML 使得调试代码变得困难。最好把 Python 和 HTML 分开。这就是为什么 Flask 支持使用 Jinja2 模板引擎的模板。

烧瓶模板

不是在 Python 文件中键入 HTML 代码,而是创建一个单独的 HTML 文件(即模板)。使用render_template()方法在 Python 中呈现这样的模板。HTML 文件被称为模板,因为它不是静态文件。该模板可多次用于不同的数据输入。

为了在 Python 代码中定位 Flask 模板,创建了一个名为templates的文件夹来保存所有的 HTML 文件。假设 Flask Python 文件命名为firstApp.py,HTML 文件命名为hello.html,项目结构如图 7-13 所示。在清单 7-11 中,创建了hello.html文件来打印与清单 7-10 中完全相同的 Hello 消息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-13

使用模板后的项目结构

<html>
<header>
<title>HTML Template</title>
</header>
<body>

<h1>Hello</h1>

</body>
</html>

Listing 7-11Template to Print Hello Message

清单 7-12 中给出了呈现该模板的 Python 代码。与主页关联的视图函数的返回结果是render_template()方法的输出。这个方法接受一个名为template_name_or_list的参数来指定模板文件名。请注意,该参数可以接受单个名称或一系列名称。当用多个名称指定一个列表时,将呈现现有的第一个模板。该示例的渲染结果与图 7-12 相同。

import flask, werkzeug

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

def testFunc():
    return flask.render_template(template_name_or_list="hello.html")
app.add_url_rule(rule="/", view_func=testFunc)

app.run(host="127.0.0.5", port=6300, debug=True)

Listing 7-12Python Code to Render an HTML Template

动态模板

模板目前是静态的,因为它们每次都以相同的方式呈现。我们可以通过使用可变数据使它们动态化。Jinja2 支持在模板内部添加占位符。在渲染模板时,这些占位符会被评估 Python 表达式的输出所替换。在要打印表达式输出的地方,用{{...}}将表达式括起来。清单 7-13 给出了使用变量name的 HTML 代码。

<html>
<header>
<title>HTML Template with an Expression</title>
</header>
<body>

<h1>Hello {{name}}</h1>

</body>
</html>

Listing 7-13HTML Code with an Expression

下一步是在根据清单 7-14 为变量name传递值之后呈现模板。要呈现的模板中的变量作为参数与它们的值一起被传递到render_template中。访问主页的结果如图 7-14 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-14

用表达式呈现模板的结果

import flask, werkzeug

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

def testFunc():
    return flask.render_template(template_name_or_list="hello.html", name="Ahmed")
app.add_url_rule(rule="/", view_func=testFunc)

app.run(host="127.0.0.5", port=6300, debug=True)

Listing 7-14Rendering Flask Template with an Expression

变量name的值是静态类型的,但它可以使用变量规则或 HTML 形式动态生成。清单 7-15 给出了用于创建接受名称的变量规则的代码。根据规则的变量部分,视图函数必须有一个名为的参数。然后这个参数的值被分配给render_template()方法的 name 参数。然后根据图 7-15 将该值传递给要渲染的模板。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-15

将从变量规则接收的值传递给 Flask 模板

import flask, werkzeug

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

def testFunc(name):
    return flask.render_template(template_name_or_list="hello.html", name=name)
app.add_url_rule(rule="/<name>", view_func=testFunc)

app.run(host="127.0.0.5", port=6300, debug=True)

Listing 7-15Variable Rule to Pass Value to Flask Template

我们还可以在 HTML 代码中插入 Python 语句、注释和行语句,每个语句都有不同的占位符。语句用{% ... %}括起来,注释用{# ... #}括起来,行语句用# ... ##括起来。清单 7-16 给出了一个例子,其中插入了一个 Python for 循环来打印从 0 到 4 的五个数字,每个数字都在< h1 > HTML 元素中。循环中的每条语句都用{%...%}括起来。

Python 使用缩进来定义块。因为 HTML 内部没有缩进,for循环的结尾用endfor标记。该文件的渲染结果如图 7-16 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-16

使用 Python 循环呈现模板

<html>
<header>
<title>HTML Template with Expression</title>
</header>
<body>

{%for k in range(5):%}
<h1>{%print(k)%}</h1>
{%endfor%}

</body>
</html>

Listing 7-16Embedding a Python Loop Inside Flask Template

静态文件

CSS 和 JavaScript 文件等静态文件用于样式化网页并使其动态化。与模板类似,创建了一个文件夹来存储静态文件。文件夹名为static。如果我们要创建一个名为style.css的 CSS 文件和一个名为simpeJS.js的 JavaScript 文件,项目结构将如图 7-17 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-17

包含模板和静态文件的项目结构

Python 代码与清单 7-15 中的代码相同,只是没有使用规则的变量部分。清单 7-17 显示了hello.html文件的内容。值得一提的是 HTML 文件是如何链接到 JavaScript 和 CSS 文件的。通常,使用的属性是text/javascript。此外,使用标签添加 CSS 文件,其中rel属性被设置为stylesheet。新的是如何定位这些文件。

<html>
<header>
<title>HTML Template with Expression</title>
<script type="text/javascript" src="{{url_for(endpoint='static', filename='simpleJS.js')}}"></script>
<link rel="stylesheet" href="{{url_for(endpoint='static', filename='style.css')}}">
</header>
<body>

{%for k in range(5):%}
<h1 onclick="showAlert({{k}})">{%print(k)%}</h1>
{%endfor%}
</body>
</html>

Listing 7-17HTML File Linked with CSS and JavaScript Files

<script><link>标签中,url_for()方法被用在一个表达式中来定位文件。该方法的endpoint属性被设置为 static,这意味着您应该查看项目结构下名为static的文件夹。该方法接受另一个名为filename的参数,它引用静态文件的文件名。

CSS 文件的内容在清单 7-18 中给出。它只针对任何<h1>元素,并通过在其上下添加虚线来修饰它们的文本。

h1 {
text-decoration: underline overline;
}

Listing 7-18Content of the CSS File

清单 7-19 给出了 JavaScript 文件的内容。它有一个名为showAlert的函数,该函数接受一个连接到字符串并打印在警报中的参数。当 HTML 模板中代表五个数字的任何一个<h1>元素被点击时,这个函数被调用。与元素相关的数字作为参数传递给函数,以便打印出来。

function showAlert(num){
alert("Number is " + num)
}

Listing 7-19Content of the JavaScript File

当点击带有文本1的数字<h1>元素时,输出如图 7-18 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-18

点击带有文本1的第二个

元素的结果

至此,我们已经有了对 Flask 的介绍,这足以让我们开始部署预训练模型。在接下来的部分中,针对 Fruits 360 和 CIFAR10 数据集的预训练模型将被部署到 web 服务器,以便 Flask 应用能够访问它们,从而对客户端上传的图像进行分类。

使用 Fruits 360 数据集部署训练模型

我们要部署的第一个模型是第五章中使用 Fruits 360 数据集训练的模型,并使用 GA 优化。Flask 应用由两个主要页面组成。

第一页是主页。它有一个 HTML 表单,允许用户选择图像文件。该文件被上传到服务器。第二页完成了大部分工作。它遵循第章和第章中的相同步骤。它在上传到服务器后读取图像,提取其特征,使用 STD 过滤特征,使用预训练的 ANN 预测图像类别标签,最后允许用户返回主页选择另一个图像进行分类。该应用具有图 7-19 中定义的结构。下面详细讨论一下应用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-19

水果 360 识别应用结构

清单 7-20 开始了构建应用的第一步。导入整个应用所需的所有模块。创建 Flask 类的实例时,将构造函数的import_name参数设置为父目录的名称,即FruitsApp。到目前为止,只创建了一个规则。该规则将主页/的 URL 绑定到查看功能homepage。应用使用主机127.0.0.5、端口号6302和主动调试模式运行。

import flask, werkzeug, skimage.io, skimage.color, numpy, pickle

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

def homepage():
    return flask.render_template(template_name_or_list="home.html")
app.add_url_rule(rule="/", view_func=homepage, endpoint="homepage")

app.run(host="127.0.0.5", port=6300, debug=True)

Listing 7-20Basic Structure of the Fruits 360 Recognition Application

当用户访问主页http://127.0.0.5:6302时,视图函数homepage()使用render_template()方法呈现home.html模板。使用的关联端点是homepage,它与视图函数的名称相同。注意,省略这个端点不会改变任何东西,因为默认端点实际上等于视图函数名。清单 7-21 中给出了home.html页面的内容。

<html>
<header>
<title>Select Image</title>
<link rel="stylesheet" href="{{url_for(endpoint='static', filename='style.css')}}">
</header>
<body>

<h1>Select an Image from the Fruits 360 Dataset</h1>
<form enctype="multipart/form-data" action="{{url_for(endpoint='extract')}}" method="post">
<input type="file" name="img"><br>
<input type="submit">
</form>

</body>
</html>

Listing 7-21Implementation of the home.html Page

该页面创建一个 HTML 表单,表单中有一个名为img的输入,表示要上传的文件。记住表单的加密类型属性enctype被设置为multipart/form-data,方法为post。该操作表示表单数据将提交到的页面。提交表单后,其数据被发送到另一个页面,以对上传的图像文件进行分类。为了避免对 URL 进行硬编码,目标规则的端点被设置为extract,用于通过url_for()方法获取其 URL。为了能够在 HTML 页面中运行这个表达式,它被包含在{{...}}之间。

在页面头中,样式表静态文件style.css通过使用接受endpointurl_for()方法的filename参数的表达式链接到页面。记住,静态文件的endpoint被设置为staticfilename参数被设置为目标静态文件名。CSS 文件的内容将在后面讨论。图 7-20 显示选择图像文件后的主页屏幕。在提交表单之后,所选择的文件细节被发送到视图函数extractFeatures,该函数与端点extract相关联,用于进一步的处理。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-20

上传水果 360 数据集图片的主页截图

清单 7-22 给出了与/extract规则相关联的extractFeatures视图函数的代码。请注意,该规则只监听 POST HTTP 方法。extractFeatures视图函数响应之前提交的表单。它使用字典flask.request.files返回上传的图像文件。使用图像文件的filename属性返回文件名。为了使保存文件更加安全,使用secure_filename()函数返回安全文件名,该函数接受原始文件名并返回一个安全名称。根据这个安全名称保存图像。

def extractFeatures():
    img = flask.request.files["img"]
    img_name = img.filename
    img_secure_name = werkzeug.secure_filename(img_name)
    img.save(img_secure_name)
    print("Image Uploaded successfully.")

img_features = extract_features(image_path=img_secure_name)
    print("Features extracted successfully.")

    f = open("weights_1000_iterations_10%_mutation.pkl", "rb")
    weights_mat = pickle.load(f)
    f.close()
    weights_mat = weights_mat[0, :]

    predicted_label = predict_outputs(weights_mat, img_features, activation="sigmoid")

    class_labels = ["Apple", "Raspberry", "Mango", "Lemon"]
    predicted_class = class_labels[predicted_label]
    return flask.render_template(template_name_or_list="result.html", predicted_class=predicted_class)
app.add_url_rule(rule="/extract", view_func=extractFeatures, methods=["POST"], endpoint="extract")

Listing 7-22Python Code for the extractFeatures View Function

上传图像到服务器后,使用清单 7-23 中定义的extract_features函数提取其特征。它接受图像路径,并遵循第三章的 Fruits 360 数据集特征挖掘一节中的步骤,从读取图像文件,提取色调通道直方图,使用 STD 过滤特征,最后返回过滤后的特征集。基于对训练数据进行的实验,根据所选元素的索引来过滤特征。这些元素的数量是 102。然后将特征向量返回到形状为 1×102 的行数量向量中。这就为矩阵乘法做好了准备。返回特征向量后,我们可以继续执行extractFeatures视图功能。

def extract_features(image_path):
    f = open("select_indices.pkl", "rb")
    indices = pickle.load(f)
    f.close()

    fruit_data = skimage.io.imread(fname=image_path)
    fruit_data_hsv = skimage.color.rgb2hsv(rgb=fruit_data)
    hist = numpy.histogram(a=fruit_data_hsv[:, :, 0], bins=360)
    im_features = hist[0][indices]
    img_features = numpy.zeros(shape=(1, im_features.size))
    img_features[0, :] = im_features [:im_features.size]
    return img_features

Listing 7-23Extracting Features from the Uploaded Image

根据清单 7-23 ,将特征向量接收到img_features变量中的下一步是恢复使用遗传算法训练的人工神经网络所学习的一组权重。权重返回到weights_mat变量。请注意,这些权重表示在最后一代之后返回的总体的所有解。我们只需要找到群体中的第一个解。这就是为什么索引 0 是从weights_mat变量返回的。

根据清单 7-24 ,在准备好图像特征和学习到的权重后,下一步是将它们应用于人工神经网络,以使用predict_outputs()函数产生预测标签。它接受权重、特征和激活函数。激活功能与我们之前实现的功能相同。predict_outputs()函数通过一个循环,在输入和 ANN 中每层的权重之间执行矩阵乘法。到达输出层的结果后,返回预测的类索引。对应的是分数最高的班级。该索引由该函数返回。

def predict_outputs(weights_mat, data_inputs, activation="relu"):
    r1 = data_inputs
    for curr_weights in weights_mat:
        r1 = numpy.matmul(a=r1, b=curr_weights)
        if activation == "relu":
            r1 = relu(r1)
        elif activation == "sigmoid":
            r1 = sigmoid(r1)
    r1 = r1[0, :]
    predicted_label = numpy.where(r1 == numpy.max(r1))[0][0]
    return predicted_label

Listing 7-24Predicting the Class Label for the Uploaded Image

返回预测的类索引后,我们回到清单 7-22 。然后,返回的索引被转换成相应类的字符串标签。所有标签都保存到class_labels列表中。预测的类标签被返回给predicted_class变量。extractFeatures视图函数最后使用render_template()方法呈现result.html模板。它将预测的类标签传递给这样的模板。清单 7-25 中提供了该模板的代码。

<html>
<header>
<title>Predicted Class</title>
<link rel="stylesheet" href="{{url_for(endpoint='static', filename='style.css')}}">
</header>
<body>

<h1>Predicted Label</h1>
<h1>{{predicted_class}}</h1>
<a href="{{url_for(endpoint='homepage')}}">Classify Another Image</a>
</body>
</html>

Listing 7-25Content of the result.html Template

该模板创建了一个表达式,能够在<h1>元素中呈现预测的类标签。创建一个锚点,让用户返回主页对另一个图像进行分类。主页的 URL 是基于其端点返回的。打印完类别标签后的result.html文件屏幕如图 7-21 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-21

对上传的图像进行分类的结果

注意,该应用只有一个名为style.css的静态文件,根据清单 7-26 实现。它只是改变了<input><a>元素的字体大小。它还通过在文本上方和下方添加一行来为<h1>元素的文本添加装饰。

a, input{
font-size: 30px;
color: black;
}

h1 {
text-decoration: underline overline dotted;
}

Listing 7-26Static CSS File for Adding Styles

在讨论了应用的每个部分之后,清单 7-27 中提供了完整的代码。

import flask, werkzeug, skimage.io, skimage.color, numpy, pickle

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

def sigmoid(inpt):
    return 1.0/(1.0+numpy.exp(-1*inpt))

def relu(inpt):
    result = inpt
    result[inpt<0] = 0
    return result

def extract_features(image_path):
    f = open("select_indices.pkl", "rb")
    indices = pickle.load(f)
    f.close()

    fruit_data = skimage.io.imread(fname=image_path)
    fruit_data_hsv = skimage.color.rgb2hsv(rgb=fruit_data)
    hist = numpy.histogram(a=fruit_data_hsv[:, :, 0], bins=360)
    im_features = hist[0][indices]
    img_features = numpy.zeros(shape=(1, im_features.size))
    img_features[0, :] = im_features[:im_features.size]
    return img_features

def predict_outputs(weights_mat, data_inputs, activation="relu"):
    r1 = data_inputs

    for curr_weights in weights_mat:
        r1 = numpy.matmul(a=r1, b=curr_weights)
        if activation == "relu":
            r1 = relu(r1)
        elif activation == "sigmoid":
            r1 = sigmoid(r1)
    r1 = r1[0, :]
    predicted_label = numpy.where(r1 == numpy.max(r1))[0][0]
    return predicted_label

def extractFeatures():
    img = flask.request.files["img"]
    img_name = img.filename
    img_secure_name = werkzeug.secure_filename(img_name)
    img.save(img_secure_name)
    print("Image Uploaded successfully.")

    img_features = extract_features(image_path=img_secure_name)
    print("Features extracted successfully.")

    f = open("weights_1000_iterations_10%_mutation.pkl", "rb")
    weights_mat = pickle.load(f)
    f.close()

    weights_mat = weights_mat[0, :]

    predicted_label = predict_outputs(weights_mat, img_features, activation="sigmoid")

    class_labels = ["Apple", "Raspberry", "Mango", "Lemon"]
    predicted_class = class_labels[predicted_label]
    return flask.render_template(template_name_or_list="result.html", predicted_class=predicted_class)
app.add_url_rule(rule="/extract", view_func=extractFeatures, methods=["POST"], endpoint="extract")

def homepage():
    return flask.render_template(template_name_or_list="home.html")
app.add_url_rule(rule="/", view_func=homepage)

app.run(host="127.0.0.5", port=6302, debug=True)

Listing 7-27Complete Code of Flask Application for Classifying Fruits 360 Dataset Images

使用 CIFAR10 数据集部署训练模型

我们讨论的部署使用 Fruits 360 数据集训练的模型的步骤将会重复,但对于使用使用 CIFAR10 数据集训练的 TensorFlow 创建的模型。与以前的应用相比,有一些增强。应用的结构如图 7-22 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-22

使用 CIFAR10 数据集部署预训练模型的应用结构

我们将在后面讨论应用的每个部分。让我们从清单 7-28 中的代码开始。导入整个应用所需的库。优选在单独的模块中进行预测步骤。这就是使用CIFAR10Predict模块的原因。它具有从 CIFAR10 数据集预测图像的类别标签所需的所有功能。这使得 Flask 应用的 Python 文件关注于视图函数。

import flask, werkzeug, os, scipy.misc, tensorflow
import CIFAR10Predict

app = flask.Flask("CIFARTF")

def redirect_upload():
    return flask.render_template(template_name_or_list="upload_image.html")
app.add_url_rule(rule="/", endpoint="homepage", view_func=redirect_upload)

if __name__ == "__main__":
    prepare_TF_session(saved_model_path='\\AhmedGad\\model\\')
    app.run(host="localhost", port=7777, debug=True)

Listing 7-28Preparing a Flask Application for Deploying the Pretrained Model Using CIFAR10 Dataset

在运行应用之前,最好确保它是执行的主文件,而不是从另一个文件引用的。如果该文件作为主文件运行,其内部的__name__变量将等于__main__。否则,__name__变量被设置为调用该文件的模块。只有当该文件是主文件时,它才应该运行。这就是使用if语句的原因。

创建一个 TF 会话,以便使用根据清单 7-29 实现的prepare_TF_session功能恢复预训练模型。该函数接收已保存模型的路径,以便恢复图形,并通过在进行预测之前初始化图形中的变量来准备会话。

def prepare_TF_session(saved_model_path):
    global sess
    global graph

    sess = tensorflow.Session()

    saver = tensorflow.train.import_meta_graph(saved_model_path+'model.ckpt.meta')
    saver.restore(sess=sess, save_path=saved_model_path+'model.ckpt')

    sess.run(tensorflow.global_variables_initializer())

    graph = tensorflow.get_default_graph()
    return graph

Listing 7-29Restoring the Pretrained TF Model

准备好会话后,应用以localhost作为主机、端口号7777和活动调试模式运行。

创建了一个将主页 URL /绑定到视图功能redirect_upload()的规则。这条规则有端点homepage。当用户访问主页http://localhost:777时,view 函数使用render_template()方法渲染清单 7-30 中定义的upload_image.html模板。

<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="{{url_for(endpoint='static', filename='project_styles.css')}}">
<meta charset="UTF-8">
<title>Upload Image</title>
</head>
<body>
<form enctype="multipart/form-data" method="post" action="http://localhost:7777/upload/">
<center>
<h3>Select CIFAR10 image to predict its label.</h3>
<input type="file" name="image_file" accept="img/*"><br>
<input type="submit" value="Upload">
</center>
</form>
</body>

</html>

Listing 7-30HTML File for Uploading an Image from the CIFAR10 Dataset

这个 HTML 文件创建了一个表单,允许用户选择要上传到服务器的图像。该页面截图如图 7-23 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-23

用于上传 CIFAR10 映像的 HTML 页面的屏幕截图

这个页面非常类似于为 Fruits 360 应用创建的表单。提交表单后,数据将被发送到与由action属性指定的规则相关联的页面,该属性是/upload。清单 7-31 中给出了该规则及其查看功能。

def upload_image():
    global secure_filename
    if flask.request.method == "POST"
        img_file = flask.request.files["image_file"]
        secure_filename = werkzeug.secure_filename(img_file.filename
        img_file.save(secure_filename)
        print("Image uploaded successfully.")
        return flask.redirect(flask.url_for(endpoint="predict"))
    return "Image upload failed."
app.add_url_rule(rule="/upload/", endpoint="upload", view_func=upload_image, methods=["POST"])

Listing 7-31Uploading a CIFAR10 Image to the Server

/upload规则被赋予一个名为upload的端点,它只响应 POST 类型的 HTTP 消息。它与upload_image视图功能相关联。它从原始文件名中检索安全文件名,并将图像保存到服务器。如果图像上传成功,那么它使用redirect()方法将应用重定向到与predict端点相关联的 URL。该端点属于/predict规则。清单 7-32 中给出了规则及其视图功能。

def CNN_predict():
    global sess
    global graph

    global secure_filename

    img = scipy.misc.imread(os.path.join(app.root_path, secure_filename))

    if(img.ndim) == 3:
        if img.shape[0] == img.shape[1] and img.shape[0] == 32:
            if img.shape[-1] == 3:
                predicted_class = CIFAR10Predict.main(sess, graph, img)
                return flask.render_template(template_name_or_list="prediction_result.html", predicted_class=predicted_class)
            else:
                return flask.render_template(template_name_or_list="error.html", img_shape=img.shape)
        else:
            return flask.render_template(template_name_or_list="error.html", img_shape=img.shape)
    return "An error occurred."
app.add_url_rule(rule="/predict/", endpoint="predict", view_func=CNN_predict)

Listing 7-32View Function to Predict the Class Label for CIFAR10 Image

该函数读取图像文件,并根据其形状和大小检查它是否已经属于 CIFAR10 数据集。这种数据集中的每个图像都有三维;前两个维度大小相等,都是 32。此外,图像是 RGB,因此第三维具有三个通道。如果没有找到这些规范,那么应用将被重定向到根据清单 7-33 实现的error.html模板。

<!DOCTYPE html>
<html lang="en">
<head>
<link type="text/css" rel="stylesheet" href="{{url_for(endpoint='static', filename='project_styles.css')}}">
<meta charset="UTF-8">
<title>Error</title>
</head>
<body>
<center>
<h1 class="error">Error</h1>
<h2 class="error-msg">Read image dimensions {{img_shape}} do not match the CIFAR10 specifications (32x32x3).</h2>
<a href="{{url_for(endpoint='homepage')}}"><span>Return to homepage</span>.</a>
</center>
</body>
</html>

Listing 7-33Template

for Indicating That the Uploaded Image Does Not Belong to the CIFAR10 Dataset

除了 CIFAR10 数据集的标准大小之外,它还使用表达式打印上传图像的大小。上传不同 shapeCIFAR10 数据集的图像:形状和大小,上传的图像,错误如图 7-24 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-24

上传与 CIFAR10 图像形状或大小不同的图像时出错

如果上传图像的形状和大小与 CIFS ar 10 图像的形状和大小相匹配,则很可能是一个 CIFS ar 10 图像,其标签将使用模块CIFAR10Predict进行预测。如清单 7-34 所示,它有一个名为main的函数,接受读取后的图像并返回其类标签。

def main(sess, graph, img):

    patches_dir = "\\AhmedGad\\cifar-10-python\\cifar-10-batches-py\\"
    dataset_array = numpy.random.rand(1, 32, 32, 3)
    dataset_array[0, :, :, :] = img

    softmax_propabilities = graph.get_tensor_by_name(name="softmax_probs:0")
    softmax_predictions = tensorflow.argmax(softmax_propabilities, axis=1)
    data_tensor = graph.get_tensor_by_name(name="data_tensor:0")
    keep_prop = graph.get_tensor_by_name(name="keep_prop:0")

    feed_dict_testing = {data_tensor: dataset_array, keep_prop: 1.0}

    softmax_propabilities_, softmax_predictions_ = sess.run([softmax_propabilities, softmax_predictions], feed_dict=feed_dict_testing)

    label_names_dict = unpickle_patch(patches_dir + "batches.meta")
    dataset_label_names = label_names_dict[b"label_names"]
    return dataset_label_names[softmax_predictions_[0]].decode('utf-8')

Listing 7-34Predicting the Class Label of the Image

该函数恢复所需的张量,这些张量有助于根据它们的名称返回预测标签,例如softmax_predictions张量。一些其他张量被恢复以覆盖它们的值,它们是keep_prop以避免在测试阶段丢失任何神经元,以及data_tensor张量以提供上传的图像文件的数据。然后运行该会话以返回预测的标签。标签只是一个数字,是类的标识符。数据集提供了一个元数据文件,其中有一个包含所有类名称的列表。通过索引列表,标识符被转换成类字符串标签。

预测完成后,CNN_predict()视图函数将预测的类发送给prediction_result.html模板进行渲染。该模板的实现如清单 7-35 所示。这很简单。它只是使用一个表达式在一个<span>元素中打印预测的类。该页面提供了基于端点返回主页的链接,以选择另一个图像进行分类。上传图片后的渲染页面如图 7-25 所示。

<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="{{url_for(endpoint='static', filename='project_styles.css')}}">
<script type="text/javascript" src="{{url_for(endpoint='static', filename='result.js')}}"></script>
<meta charset="UTF-8">
<title>Prediction Result</title>
</head>
<body onload="show_alert('{{predicted_class}}')">
<center><h1>Predicted Class Label : <span>{{predicted_class}}</span></h1>
<br>
<a href="{{url_for(endpoint='homepage')}}"><span>Return to homepage</span>.</a>
</center>
</body>
</html>

Listing 7-35Rendering Predicted Class

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-25

预测类别标签后的渲染结果

注意,当加载清单 7-35 的元素时,有一个名为show_alert()的 JavaScript 函数调用。它接受预测的类标签并显示警告。其实现如清单 7-36 所示。

function show_alert(predicted_class){
alert("Processing Finished.\nPredicted class is *"+predicted_class+"*.")
}

Listing 7-36JavaScript Alert Showing the Predicted Class

既然已经讨论了应用的各个部分,清单 7-37 中给出了完整的代码。

import flask, werkzeug, os, scipy.misc, tensorflow
import CIFAR10Predict#Module for predicting the class label of an input image.

#Creating a new Flask Web application. It accepts the package name.
app = flask.Flask("CIFARTF")

def CNN_predict():
    """
    Reads the uploaded image file and predicts its label using the saved pretrained CNN model.
    :return: Either an error if the image is not for the CIFAR10 dataset or redirects the browser to a new page to show the prediction result if no error occurred.
    """

    global sess

    global graph

# Setting the previously created 'secure_filename' to global.
# This is because to be able to invoke a global variable created in another function, it must be defined global in the caller function.

    global secure_filename
    #Reading the image file from the path it was saved in previously.
    img = scipy.misc.imread(os.path.join(app.root_path, secure_filename))

# Checking whether the image dimensions match the CIFAR10 specifications.
# CIFAR10 images are RGB (i.e. they have 3 dimensions). Its number of dimensions was not equal to 3, then a message will be returned.

    if(img.ndim) == 3:

# Checking if the number of rows and columns of the read image matched CIFAR10 (32 rows and 32 columns).

        if img.shape[0] == img.shape[1] and img.shape[0] == 32:

# Checking whether the last dimension of the image has just 3 channels (Red, Green, and Blue).

            if img.shape[-1] == 3:

# Passing all preceding conditions, the image is proved to be of CIFAR10.
# This is why it is passed to the predictor.

                predicted_class = CIFAR10Predict.main(sess, graph, img)

# After predicting the class label of the input image, the prediction label is rendered on an HTML page.
# The HTML page is fetched from the /templates directory. The HTML page accepts an input which is the predicted class.

                return flask.render_template(template_name_or_list="prediction_result.html", predicted_class=predicted_class)
            else:
                # If the image dimensions do not match the CIFAR10 specifications, then an HTML page is rendered to show the problem.
                return flask.render_template(template_name_or_list="error.html", img_shape=img.shape)

        else:
            # If the image dimensions do not match the CIFAR10 specifications, then an HTML page is rendered to show the problem.
            return flask.render_template(template_name_or_list="error.html", img_shape=img.shape)
    return "An error occurred."#Returned if there is a different error other than wrong image dimensions.

# Creating a route between the URL (http://localhost:7777/predict) to a viewer function that is called after navigating to such URL.
# Endpoint 'predict' is used to make the route reusable without hard-coding it later.

app.add_url_rule(rule="/predict/", endpoint="predict", view_func=CNN_predict)

def upload_image():
    """
    Viewer function that is called in response to getting to the 'http://localhost:7777/upload' URL.
    It uploads the selected image to the server.
    :return: redirects the application to a new page for predicting the class of the image.
    """
    #Global variable to hold the name of the image file for reuse later in prediction by the 'CNN_predict' viewer functions.
    global secure_filename

    if flask.request.method == "POST":#Checking of the HTTP method initiating the request is POST.
        img_file = flask.request.files["image_file"]#Getting the file name to get uploaded.
        secure_filename = werkzeug.secure_filename(img_file.filename)#Getting a secure file name. It is a good practice to use it.
        img_file.save(secure_filename)#Saving the image in the specified path.
        print("Image uploaded successfully.")

# After uploading the image file successfully, next is to predict the class label of it. The application will fetch the URL that is tied to the HTML page responsible for prediction and redirects the browser to it.
# The URL is fetched using the endpoint 'predict'.

        return flask.redirect(flask.url_for(endpoint="predict"))
    return "Image upload failed."

# Creating a route between the URL (http://localhost:7777/upload) to a viewer function that is called after navigating to such URL.
# Endpoint 'upload' is used to make the route reusable without hard-coding it later. The set of HTTP method the viewer function is to respond to is added using the ‘methods’ argument. In this case, the function will just respond to requests of the methods of type POST.

app.add_url_rule(rule="/upload/", endpoint="upload", view_func=upload_image, methods=["POST"])

def redirect_upload():
    """
    A viewer function that redirects the Web application from the root to an HTML page for uploading an image to get classified.
    The HTML page is located under the /templates directory of the application.
    :return: HTML page used for uploading an image. It is 'upload_image.html' in this example.
    """
    return flask.render_template(template_name_or_list="upload_image.html")

# Creating a route between the homepage URL (http://localhost:7777) to a viewer function that is called after getting to such a URL.
# Endpoint 'homepage' is used to make the route reusable without hard-coding it later.

app.add_url_rule(rule="/", endpoint="homepage", view_func=redirect_upload)

def prepare_TF_session(saved_model_path):
    global sess
    global graph

    sess = tensorflow.Session()

    saver = tensorflow.train.import_meta_graph(saved_model_path+'model.ckpt.meta')
    saver.restore(sess=sess, save_path=saved_model_path+'model.ckpt')

    #Initializing the variables.
    sess.run(tensorflow.global_variables_initializer())

    graph = tensorflow.get_default_graph()
    return graph

# To activate the web server to receive requests, the application must run.
# A good practice is to check whether the file is called from an external Python file or not.
# If not, then it will run.

if __name__ == "__main__":

# In this example, the app will run based on the following properties:
# host: localhost

# port: 7777
# debug: flag set to True to return debugging information.

    #Restoring the previously saved trained model.
    prepare_TF_session(saved_model_path='\\AhmedGad\\model\\')
    app.run(host="localhost", port=7777, debug=True)

Listing 7-37Complete Flask Application for CIFAR10 Dataset

八、跨平台数据科学应用

当前的 DL 库中有一些版本支持为移动设备构建应用。例如,TensorFlowLite、Caffe Android 和 Torch Android 都是分别来自 TF、Caffe 和 Torch 的版本,以支持移动设备。这些释放是基于他们的父母。为了使原始模型在移动设备上工作,必须有一个中间步骤。例如,创建使用 TensorFlowLite 的 Android 应用的过程有以下总结步骤:

  1. 准备 TF 模型。

  2. 将 TF 模型转换为 TensorFlowLite 模型。

  3. 创建一个 Android 项目。

  4. 在项目中导入 TensorFlowLite 模型。

  5. 在 Java 代码中调用模型。

为构建一个适合在移动设备上运行的模型而经历这些步骤是令人厌倦的。具有挑战性的步骤是第二步。

TensorFlowLite 是与移动设备兼容的版本。因此,与它的祖先 TF 相比,它被简化了。这意味着它不支持其父库中的所有内容。到目前为止,TensorFlowLite 还不支持 TF 中的一些操作,如 tanh、image.resize_bilinear 和 depth_to_space。当准备在移动设备上工作的模型时,这增加了限制。此外,模型开发人员必须使用语言来创建运行经过训练的 CNN 模型的 Android 应用。使用 Python,将使用 TF 创建模型。在使用 TF 优化转换器(TOCO)优化模型之后,使用 Android Studio 创建一个项目。在这样的项目中,将使用 Java 调用模型。因此,这个过程并不简单,创建应用也很有挑战性。有关使用 TensorFlowLite 构建移动应用的更多信息,请阅读此链接的文档( www.tensorflow.org/lite/overview )。在这一章中,我们将使用 Kivy (KV)以最小的努力构建跨平台运行的应用。

Kivy 是一个抽象和模块化的开源跨平台 Python 框架,用于创建自然用户界面(ui)。它通过使用后端库对图形硬件进行低级访问并处理音频和视频,将开发人员从复杂的细节中分离出来。它只是为开发人员提供简单的 API 来完成任务。

本章使用一些简单的例子来介绍 Kivy,帮助解释它的基本程序结构、UI 小部件、使用 KV 语言构造小部件以及处理动作。Kivy 支持在 Window、Linux、Mac 以及移动设备上执行相同的 Python 代码,这使得它具有跨平台性。使用 Buildozer 和 Python-4-Android (P4A),Kivy 应用被转换成一个 Android 包。不仅执行原生 Python 代码;Kivy 还支持一些在移动设备上执行的库,比如 NumPy 和 PIL (Python Image Library)。在本章结束时,使用 NumPy 构建了一个跨平台应用来执行第五章中实现的 CNN。本章使用 Ubuntu 是因为 Buildozer 目前可以在 Linux 上使用。

Kivy 简介

在本节中,将基于一些示例详细讨论 Kivy 基础知识。这有助于我们着手构建自己的应用。记得从第七章开始,Flask 应用通过实例化 Flask 类开始创建应用;然后应用通过调用run()方法来运行。Kivy 类似,但有一些变化。我们可以假设Flask类对应于 Kivy 中的App类。Kivy 和 Flask 内部都有一个叫run()的方法。Kivy 应用不是通过实例化 App 类创建的,而是通过实例化一个扩展 App 类的子类创建的。然后,应用通过使用从子类创建的实例调用run()方法来运行。

Kivy 用于构建一个 UI,该 UI 由一组称为小部件的可视元素组成。在实例化类和运行它之间,我们必须指定使用哪些小部件以及它们的布局。App 类支持一个名为build()的方法,该方法返回包含 UI 中所有其他小部件的布局小部件。可以从父 App 类中重写此方法。

使用 BoxLayout 的基本应用

让我们通过讨论清单 8-1 中的一个基本 Kivy 应用来让事情变得更清楚。首先,从 Kivy 导入所需的模块。kivy.app包含了 App 类。这个类被用作我们定义的类FirstApp的父类。第二个语句导入kivy.uix.label,它有一个标签小部件。这个小部件只在 UI 上显示文本。

build()方法中,标签小部件是使用kivy.uix.label.Label类创建的。该类构造函数接受一个名为text的参数,它是要在 UI 上显示的文本。返回的标签保存为FirstApp对象的属性。与将小部件保存在单独的变量中相比,将小部件作为属性添加到类对象中可以更容易地在以后检索它们。

import kivy.app
import kivy.uix.label
import kivy.uix.boxlayout

class FirstApp(kivy.app.App):
    def build(self):
        self.label = kivy.uix.label.Label(text="Hello Kivy")
        self.layout = kivy.uix.boxlayout.BoxLayout()
        self.layout.add_widget(widget=self.label)
        return self.layout

firstApp = FirstApp()
firstApp.run()

Listing 8-1Basic Kivy Application

Kivy 中的小部件被分组到一个根小部件中。在清单 8-1 中,BoxLayout被用作根小部件,它包含所有其他小部件。这就是为什么kivy.uix.boxlayout是进口的。基于kivy.uix.label.BoxLayout类的构造函数,BoxLayout对象被保存为FirstClass对象的属性。创建标签和布局对象后,使用add_widget()方法将标签添加到布局中。这个方法有一个名为widget的参数,它接受要添加到布局中的小部件。将标签添加到根小部件(布局)后,布局由build()方法返回。

在创建了子类FirstApp并准备好它的build()方法之后,就创建了该类的一个实例。然后该实例调用run()方法,应用窗口根据图 8-1 显示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-1

带有文本标签的简单 Kivy 应用

Kivy 应用生命周期

只需运行应用,build()方法中定义的小部件就会呈现在屏幕上。请注意,Kivy 生命周期如图 8-2 所示。它类似于 Android 应用的生命周期。生命周期从使用run()方法运行应用开始。之后,执行build()方法,返回要显示的小部件。执行on_start()方法后,应用成功运行。此外,应用可能会暂停或停止。如果暂停了,那么调用on_pause()方法。如果应用恢复,那么调用on_resume()方法。如果没有恢复,应用就会停止。应用可能会在没有暂停的情况下直接停止。如果是这种情况,就调用on_stop()方法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-2

Kivy 应用生命周期

图 8-1 顶部的标题有First字样。那是什么?子类被命名为FirstApp。当类以单词App结尾命名时,Kivy 使用它前面的工作作为应用标题。给班级取名MyApp,那么题目就是My。注意App这个词必须以大写字母开头。如果类被命名为Firstapp,那么标题也将是Firstapp。注意,我们能够使用类构造函数的title参数来设置自定义名称。构造函数还接受一个名为icon的参数,它是一个图像的路径。

清单 8-2 将应用标题设置为自定义标题,并且还实现了on_start()on_stop()方法。窗口如图 8-3 所示。当应用启动时,调用on_start()方法来打印消息。对于on_stop()方法也是如此。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-3

改变应用标题

import kivy.app
import kivy.uix.label
import kivy.uix.boxlayout

class FirstApp(kivy.app.App):
    def build(self):
        self.label = kivy.uix.label.Label(text="Hello Kivy")
        self.layout = kivy.uix.boxlayout.BoxLayout()
        self.layout.add_widget(widget=self.label)
        return self.layout

    def on_start(self):
        print("on_start()")

    def on_stop(self):
        print("on_stop()")

firstApp = FirstApp(title="First Kivy Application.")
firstApp.run()

Listing 8-2Implementing Life Cycle Methods

我们可以在BoxLayout中添加多个小部件。这个布局小部件垂直或水平排列其子部件。它的构造函数有一个名为orientation的参数来定义排列。它有两个值:horizontalvertical。默认为horizontal

如果方向设置为垂直,则小部件堆叠在彼此之上,其中第一个添加的小部件出现在窗口的底部,最后一个添加的小部件出现在顶部。在这种情况下,窗口高度在所有子小部件中平均分配。

如果方向是水平的,那么小部件是并排添加的,其中第一个添加的小部件是屏幕上最左边的小部件,而最后一个添加的小部件是屏幕上最右边的小部件。在这种情况下,窗口的宽度在所有子部件之间平均分配。

清单 8-3 使用了五个按钮部件,它们的文本设置为Button 1Button 2,直到Button 5。这些小部件被水平添加到一个BoxLayout小部件中。结果如图 8-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-4

BoxLayout小工具的水平方向

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

class FirstApp(kivy.app.App):
    def build(self):
        self.button1 = kivy.uix.button.Button(text="Button 1")
        self.button2 = kivy.uix.button.Button(text="Button 2")
        self.button3 = kivy.uix.button.Button(text="Button 3")
        self.button4 = kivy.uix.button.Button(text="Button 4")
        self.button5 = kivy.uix.button.Button(text="Button 5")
        self.layout = kivy.uix.boxlayout.BoxLayout(orientation="horizontal")
        self.layout.add_widget(widget=self.button1)
        self.layout.add_widget(widget=self.button2)
        self.layout.add_widget(widget=self.button3)
        self.layout.add_widget(widget=self.button4)
        self.layout.add_widget(widget=self.button5)
        return self.layout

firstApp = FirstApp(title="Horizontal BoxLayout Orientation.")
firstApp.run()

Listing 8-3Kivy Application using BoxLayout as the Root Widget with Horizontal Orientation

小部件大小

BoxLayout将屏幕平均分配给所有的小工具。添加五个小部件,然后它将屏幕在宽度和高度上分成五个相等的部分。它给每个部件分配一个大小相等的部分。我们可以使用小部件的size_hint参数使分配给小部件的零件尺寸变大或变小。它接受一个具有两个值的元组,这两个值定义了相对于窗口大小的宽度和高度。默认情况下,所有小部件的元组都是(1,1)。这意味着大小相等。如果小部件的此参数设置为(2,1),则小部件的宽度将是默认宽度的两倍。如果设置为(0.5,1),那么小部件宽度将是默认宽度的一半。

清单 8-4 更改了一些小部件的size_hint参数。图 8-5 显示了每个按钮的文本反映其相对于窗口大小的宽度的结果。请注意,小部件向父小部件发出提示,希望其大小符合由size_hint参数指定的值。家长可以接受或拒绝请求。这就是为什么它的参数名中有hint这个词。例如,设置小部件的col_force_defaultrow_force_default属性会使父部件完全忽略size_hint参数。注意,size_hint是小部件构造函数的一个参数,也可以作为小部件实例的一个属性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-5

使用 size_hint 参数改变小部件的宽度

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

class FirstApp(kivy.app.App):
    def build(self):
        self.button1 = kivy.uix.button.Button(text="2", size_hint = (2, 1))
        self.button2 = kivy.uix.button.Button(text="1")
        self.button3 = kivy.uix.button.Button(text="1.5", size_hint = (1.5, 1))
        self.button4 = kivy.uix.button.Button(text="0.7", size_hint = (0.7, 1))
        self.button5 = kivy.uix.button.Button(text="3", size_hint = (3, 1))
        self.layout = kivy.uix.boxlayout.BoxLayout(orientation="horizontal")
        self.layout.add_widget(widget=self.button1)
        self.layout.add_widget(widget=self.button2)
        self.layout.add_widget(widget=self.button3)
        self.layout.add_widget(widget=self.button4)
        self.layout.add_widget(widget=self.button5)
        return self.layout

firstApp = FirstApp(title="Horizontal BoxLayout Orientation.")
firstApp.run()

Listing 8-4Using the size_hint Argument with theIf added “withnot OK, please clarify listing caption. Widgets to Change Their Relative 
Size

网格布局

也有BoxLayout以外的布局。例如,GridLayout根据指定的行数和列数将屏幕分成网格。根据清单 8-5 ,创建了一个两行三列的网格布局,其中添加了六个按钮。行数和列数分别根据rowscols属性设置。添加的第一个小部件出现在左上角,而添加的最后一个小部件出现在右下角。结果如图 8-6 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-6

两行三列的网格布局

import kivy.app
import kivy.uix.button
import kivy.uix.gridlayout

class FirstApp(kivy.app.App):
    def build(self):
        self.button1 = kivy.uix.button.Button(text="Button 1")
        self.button2 = kivy.uix.button.Button(text="Button 2")
        self.button3 = kivy.uix.button.Button(text="Button 3")
        self.button4 = kivy.uix.button.Button(text="Button 4")
        self.button5 = kivy.uix.button.Button(text="Button 5")
        self.button6 = kivy.uix.button.Button(text="Button 6")
        self.layout = kivy.uix.gridlayout.GridLayout(rows=2, cols=3)
        self.layout.add_widget(widget=self.button1)
        self.layout.add_widget(widget=self.button2)
        self.layout.add_widget(widget=self.button3)
        self.layout.add_widget(widget=self.button4)
        self.layout.add_widget(widget=self.button5)
        self.layout.add_widget(widget=self.button6)
        return self.layout

firstApp = FirstApp(title="GridLayout with 2 rows and 3 columns.")
firstApp.run()

Listing 8-5Dividing the Window into a Grid of Size 2×3 Using GridLayout

另一种适合移动设备的布局是PageLayout。它实际上在同一个布局中构建了几个页面。在页面边框处,用户可以向左或向右拖动页面,以便导航到另一个页面。创建这样的布局很简单。只需创建一个kivy.uix.pagelayout.PageLayout类的实例,这类似于我们之前所做的。然后,将小部件添加到布局中,就像我们使用add_widget()方法一样。

更多小部件

UI 中有多个小部件可以使用。例如,Image小部件用于根据图像的来源显示图像。TextInput小部件允许用户将输入输入到应用中。其他还有CheckBoxRadioButtonSlider等等。

清单 8-6 给出了一个带有ButtonLabelTextInputImage小部件的例子。TextInput类的构造函数有一个名为hint_text的属性,它在小部件中显示一条提示消息,帮助用户知道要输入什么。image 小部件使用source属性来指定图像路径。图 8-7 显示了结果。稍后,我们将处理这些小部件的动作,比如按钮点击、改变标签文本等等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-7

带有LabelTextInputButtonImage小部件的垂直BoxLayout

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

class FirstApp(kivy.app.App):
    def build(self):
        self.label = kivy.uix.label.Label(text="Label")
        self.textinput = kivy.uix.textinput.TextInput(hint_text="Hint Text")
        self.button = kivy.uix.button.Button(text="Button")
        self.image = kivy.uix.image.Image(source="im.png")
        self.layout = kivy.uix.boxlayout.BoxLayout(orientation="vertical")
        self.layout.add_widget(widget=self.label)
        self.layout.add_widget(widget=self.textinput)
        self.layout.add_widget(widget=self.button)
        self.layout.add_widget(widget=self.image)
        return self.layout

firstApp = FirstApp(title="BoxLayout with Label, Button, TextInput, and Image")
firstApp.run()

Listing 8-6BoxLayout with Label, TextInput, Button, and Image Widgets

小部件树

在前面的例子中,有一个根小部件(布局),有几个子部件直接连接到它。列表 8-6 的 widget 树如图 8-8 所示。这棵树只有一层。我们可以创建一个更深的树,如图 8-9 所示,其中垂直方向的根BoxLayout部件有两个子布局。第一个是一个有两行两列的GridLayout小部件。第二个子窗口是水平方向的水平BoxLayout窗口小部件。这些子GridLayout部件有自己的子部件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-9

具有嵌套布局的小部件树

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-8

清单 8-6 中 Kivy 应用的部件树

清单 8-7 中给出了带有图 8-9 中定义的小部件树的 Kivy 应用。应用创建每个父节点,然后创建其子节点,最后将这些子节点添加到父节点中。该应用的渲染窗口如图 8-10 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-10

嵌套小部件

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

class FirstApp(kivy.app.App):
    def build(self):
        self.gridLayout = kivy.uix.gridlayout.GridLayout(rows=2, cols=2)
        self.image1 = kivy.uix.image.Image(source="apple.jpg")
        self.image2 = kivy.uix.image.Image(source="bear.jpg")
        self.button1 = kivy.uix.button.Button(text="Button 1")
        self.button2 = kivy.uix.button.Button(text="Button 2")
        self.gridLayout.add_widget(widget=self.image1)
        self.gridLayout.add_widget(widget=self.image2)
        self.gridLayout.add_widget(widget=self.button1)
        self.gridLayout.add_widget(widget=self.button2)

        self.button3 = kivy.uix.button.Button(text="Button 3")
        self.button4 = kivy.uix.button.Button(text="Button 4")

        self.boxLayout = kivy.uix.boxlayout.BoxLayout(orientation="horizontal")
        self.textinput = kivy.uix.textinput.TextInput(hint_text="Hint Text.")
        self.button5 = kivy.uix.button.Button(text="Button 5")
        self.boxLayout.add_widget(widget=self.textinput)
        self.boxLayout.add_widget(widget=self.button5)

        self.rootBoxLayout = kivy.uix.boxlayout.BoxLayout(orientation="vertical")
        self.rootBoxLayout.add_widget(widget=self.gridLayout)
        self.rootBoxLayout.add_widget(widget=self.button3)
        self.rootBoxLayout.add_widget(widget=self.button4)
        self.rootBoxLayout.add_widget(widget=self.boxLayout)

        return self.rootBoxLayout

firstApp = FirstApp(title="Nested Widgets.")
firstApp.run()

Listing 8-7Kivy Application with Nested Widgets in the Widget Tree

处理事件

我们可以使用bind()方法处理 Kivy 小部件生成的事件。此方法接受指定要处理的目标事件的参数。该参数被赋予一个函数或方法,用于处理此类事件。例如,当按下按钮时,触发on_press事件。因此,bind()方法使用的参数将被命名为on_press。假设我们想要使用一个叫做handle_press的方法来处理这个事件,那么bind()方法的on_press参数将被赋予这个方法名。请注意,处理事件的方法接受一个参数,该参数表示触发事件的小部件。让我们看看清单 8-8 中的应用是如何工作的。

这个应用有两个TextInput小部件,一个Label和一个Button。用户在每个TextInput小部件中输入一个数字。当按钮被按下时,数字被取出并相加,然后结果被呈现在Label上。基于前面的例子,应用中的一切都是我们熟悉的,除了调用bind()方法来使用add_nums()方法处理 press 事件。

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

class FirstApp(kivy.app.App):

    def add_nums(self, button):
        num1 = float(self.textinput1.text)
        num2 = float(self.textinput2.text)
        result = num1 + num2
        self.label.text = str(result)

    def build(self):
        self.boxLayout = kivy.uix.boxlayout.BoxLayout(orientation="horizontal")
        self.textinput1 = kivy.uix.textinput.TextInput(hint_text="Enter First Number.")
        self.textinput2 = kivy.uix.textinput.TextInput(hint_text="Enter Second Number.")
        self.boxLayout.add_widget(widget=self.textinput1)
        self.boxLayout.add_widget(widget=self.textinput2)

        self.label = kivy.uix.label.Label(text="Result of Addition.")
        self.button = kivy.uix.button.Button(text="Add Numbers.")
        self.button.bind(on_press=self.add_nums)

        self.rootBoxLayout = kivy.uix.boxlayout.BoxLayout(orientation="vertical")
        self.rootBoxLayout.add_widget(widget=self.label)
        self.rootBoxLayout.add_widget(widget=self.boxLayout)
        self.rootBoxLayout.add_widget(widget=self.button)

        return self.rootBoxLayout

firstApp = FirstApp(title="Handling Actions using Bind().")
firstApp.run()

Listing 8-8Application for Adding Two Numbers and Showing Their Results on a Label

按钮调用bind()方法,这是任何小部件的属性。为了处理on_press事件,该方法将使用它作为一个参数。该参数被设置为等于用名称add_nums创建的自定义函数。这意味着每次触发on_press事件时,都会执行add_nums()方法。on_press本身就是一个方法。因为默认情况下它是空的,所以我们需要给它添加一些逻辑。那个逻辑可能是我们在 Python 文件中定义的方法,比如add_nums方法。注意,我们创建了一个方法,而不是一个处理事件的函数来访问对象中的所有小部件。如果使用了函数,那么我们必须传递处理事件所需的小部件的属性。

add_nums()方法中,使用text属性将两个TextInput小部件中的文本返回到num1num2变量中。因为text属性返回的结果是一个字符串,所以我们要把它转换成一个数字。这是使用float()功能完成的。两个数相加,结果返回到result变量。将两个数相加将返回一个数。因此,result变量的数据类型是数字。因为text属性只接受字符串,我们必须使用str()函数将result变量转换成字符串,以便在标签上显示它的值。图 8-11 显示了将两个数相加并将结果呈现在Label小工具上后的应用 UI。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-11

将两个数字相加并在Label小部件上显示结果的应用 UI

KV 语言

通过添加更多小部件来扩大小部件树会使 Python 代码更难调试。类似于我们在第七章中所做的,将 HTML 代码从 Flask 应用的逻辑中分离出来,在这一章中,我们将把 UI 代码从应用逻辑中分离出来。

UI 将使用一种叫做 KV language (kvlang 或 Kivy language)的语言创建。这种语言创建扩展名为.kv的文件来保存 UI 小部件。因此,将有一个用于处理事件等应用逻辑的.py文件,以及另一个用于保存应用 UI 的.kv文件。KV 语言以一种简单的方式构建小部件树,与将它添加到 Python 代码中相比,这种方式更容易理解。KV 语言使得调试 UI 变得容易,因为它清楚地表明了给定的父节点属于哪个子节点。

KV 文件由一组规则组成,这些规则类似于定义小部件的 CSS 规则。规则由小部件类和一组属性及其值组成。在小部件类名后添加一个冒号,表示小部件内容的开始。给定小部件下的内容是缩进的,就像 Python 定义块的内容一样。属性名与其值之间有一个冒号。例如,清单 8-9 创建了一个构建按钮小部件的规则。

按钮小部件后跟一个冒号。冒号后缩进的所有内容都属于该小部件。缩进空间的数量并不固定为四个。它类似于 Python,我们可以使用任意数量的空格。我们发现有三个属性是缩进的。第一个是text属性,它使用冒号与值分开。转到一个新的缩进行,我们可以写入新的属性background_color,使用冒号将其与值分开。顺便说一下,颜色是使用 RGBA 颜色空间定义的,其中 A 表示 alpha 通道。颜色值介于 0.0 和 1.0 之间。对于第三个属性,重复相同的过程,用冒号将它的名称和值分开。color属性定义了文本的颜色。

Button:
    text: "Press Me."
   background_color: (0.5, 0.5, 0.5, 1.0)
    color: (0,0,0,1)

Listing 8-9Preparing the Button Widget with Some Properties Using KV Language

我们可以创建一个简单的 Kivy 应用,它使用 KV 文件来构建 UI。假设我们想要构建一个 UI,以BoxLayout小部件作为垂直方向的根。这个根小部件有三个子部件(ButtonLabelTextInput)。注意,KV 语言只有一个根小部件,它是通过不加任何缩进地键入来定义的。这个根小部件的子部件将被同等缩进。清单 8-10 中给出了 KV 语言文件。在根小部件之后,ButtonLabelTextInput小部件缩进四个空格。根小部件本身可以有属性。每个子部件的属性都缩进在它们的部件后面。这很简单,但是我们如何在 Python 代码中使用这个 KV 文件呢?

BoxLayout:
    orientation: "vertical"
    Button:
        text: "Press Me."
        color: (1,1,1,1)
    Label:
        text: "Label"
    TextInput:
        hint_text: "TextInput"

Listing 8-10Simple UI Created Using KV Language

在 Python 代码中加载 KV 文件有两种方式。第一种方法是在kivy.lang.builder.Builder类的load_file()方法中指定文件的路径。这个方法使用它的filename参数来指定文件的路径。该文件可以位于任何位置,不需要与 Python 文件位于同一目录中。清单 8-11 展示了如何以这种方式定位 KV 文件。

以前,build()方法的返回是在 Python 文件中定义的根小部件。现在它返回load_file()方法的结果。将 Python 文件中的逻辑与表示分离后,Python 代码更加清晰,现在表示在 KV 文件中。

import kivy.app
import kivy.lang.builder

class FirstApp(kivy.app.App):

    def build(self):
        return kivy.lang.builder.Builder.load_file(filename='ahmedgad/FirstApp/first.kv')

firstApp = FirstApp(title="Importing UI from KV File.")
firstApp.run()

Listing 8-11Locating the LV File Using Its Path

使用第二种加载 KV 文件的方式可以使代码更加清晰。这种方式依赖于继承 App 类的子类的名称。如果这个类被命名为FirstApp,那么 Kivy 将寻找一个名为first.kv的 KV 文件。也就是说,App这个词被删除,剩下的文本First被转换成小写。如果 Python 文件所在的目录下有一个名为first.kv的文件,那么这个文件将被自动加载。

当使用这个方法时,Python 代码将如清单 8-12 所示。代码现在比以前更清晰,调试也更简单。在FirstApp类中添加了pass语句,以避免让它为空。注意,如果 Kivy 找不到根据first.kv命名的文件,应用仍将运行,但会显示一个空白窗口。

import kivy.app

class FirstApp(kivy.app.App):
    pass

firstApp = FirstApp(title="Importing UI from KV File.")
firstApp.run()

Listing 8-12Loading the KV File Named According to the Child Class Name

我们可以将清单 8-8 中的 UI 从 Python 代码中分离出来,并将事件处理程序绑定到 KV 文件中的按钮。KV 文件在清单 8-13 中给出。

还有几点值得一提。可以使用id属性在 KV 文件中给小部件一个 ID。它的值不需要用引号括起来。ID 可用于检索 KV 文件和 Python 文件中的小部件的属性。根据代码,id 被赋予元素Label和两个TextInput小部件。原因是这些是我们希望根据其属性检索或更改的小部件。

BoxLayout:
    orientation: "vertical"
    Label:
        text: "Result of Addition."
        id: label
    BoxLayout:
        orientation: "horizontal"
        TextInput:
            hint_text: "Enter First Number."
            id: textinput1
        TextInput:
            hint_text: "Enter Second Number."
            id: textinput2
    Button:
        text: "Add Numbers."
        on_press: app.add_nums(root)

Listing 8-13UI of Listing 8-8 for Adding Two Numbers Separated into KV File

按钮小部件具有on_press属性。它用于将事件处理程序绑定到on_press事件。事件处理程序是清单 8-14 中 Python 代码内的add_nums()方法。因此,我们想从 KV 文件中调用一个 Python 方法。我们如何做到这一点?

KV 语言有三个很有帮助的关键词:app,指应用实例;root,指 KV 文件中的根 widget 还有self,指的是当前的小部件。从 Python 代码中调用方法的合适的关键字是app关键字。因为它引用了整个应用,所以它将能够引用 Python 文件中的方法。因此,我们可以用它来调用使用app.add_nums()add_nums()方法。

import kivy.app

class FirstApp(kivy.app.App):

    def add_nums(self, root):
        num1 = float(self.root.ids["textinput1"].text)
        num2 = float(self.root.ids["textinput2"].text)
        result = num1 + num2
        self.root.ids["label"].text = str(result)

firstApp = FirstApp(title="Importing UI from KV File.")
firstApp.run()

Listing 8-14Kivy Python File for Handling the on_press Event

在这个方法中,我们想要引用TextInput和 label 小部件,以便获取输入的数字并在标签上打印结果。因为self参数指的是调用它的对象,也就是关于整个应用的实例,所以我们可以用self.root用它来指根小部件。这将返回小部件的根,可用于根据它们的 id 访问它的任何子小部件。

KF 文件中的所有 id 都保存在ids字典中。我们可以使用这个字典来检索我们想要的任何小部件,只要它有一个 ID。在检索小部件本身之后,我们可以获取它的属性。这样,我们可以返回在TextInput小部件中输入的数字,将它们的值从字符串转换为浮点,将它们相加,并将转换为字符串后的结果赋给Label小部件的text属性。

P4A

至此,我们对 Kivy 有了一个很好的概述。我们可以使用 Kivy 构建 Android 应用。我们将从打包清单 8-13 和清单 8-14 中的 Kivy 应用开始。

之前的应用不做任何改动,打包后就可以在 Android 上运行了。将 Kivy 应用转换为 Android 应用的简化步骤如图 8-12 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-12

从 Kivy 应用构建 Android 应用的步骤

完成 Kivy Python 应用后,Buildozer 工具将准备创建 APK 文件所需的工具。最重要的工具叫做 P4A。Buildozer 工具在转换成 Android 应用之前为每个 Kivy 应用创建一个名为buildozer.spec的文件。该文件保存了应用的详细信息,稍后将在第节准备 buildozer.spec 文件中讨论。让我们从安装 Buildozer 工具开始。

安装推土机

本节使用 Buildozer 工具将 Kivy 应用打包成 Android 应用。安装完成后,Buildozer 会自动完成构建 Android 应用的过程。它根据所有需求准备环境,以便成功地构建应用。这些需求包括 P4A、Android SDK 和 NDK。在安装 Buildozer 之前,需要一些依赖项。可以使用以下 Ubuntu 命令自动下载和安装它们:

ahmed-gad@ubuntu:~$ sudo pip install --upgrade cython==0.21
ahmed-gad@ubuntu:~$ sudo dpkg --add-architecture i386
ahmed-gad@ubuntu:~$ sudo apt-get update
ahmed-gad@ubuntu:~$ sudo apt-get install build-essential ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386

成功安装这些依赖项后,可以根据以下命令安装 Buildozer:

ahmed-gad@ubuntu:~$ sudo install --upgrade buildozer

如果您的机器上当前安装了 Buildozer,那么- upgrade 选项可以确保它被升级到最新版本。成功安装 Buildozer 后,让我们准备好buildozer.spec文件,以便构建 Android 应用。

正在准备 buildozer.spec 文件

图 8-13 给出了要打包成 Android 应用的项目结构。有一个文件夹名为FirstApp,里面有三个文件。第一个文件名为main.py,这是之前名为FirstApp.py的 Kivy 应用。之所以改名,是因为在构建 Android 应用的时候,必须有一个名为main.py的文件,它是应用的入口。这不会改变应用中的任何内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-13

项目结构

在继续下一步之前,最好检查 Kivy 应用是否运行成功。只需在你的机器上激活 Kivy 虚拟环境,并根据图 8-14 运行main.py Python 文件。预计其工作方式如图 8-11 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-14

激活 Kivy 虚拟环境以运行 Kivy 应用

至此,已经成功创建了一个 Kivy 桌面应用。我们现在可以开始准备丢失的文件buildozer.spec并构建一个 Android 应用。

使用 Buildozer 可以简单地自动生成buildozer.spec文件。打开 Ubuntu 终端并导航到应用 Python 和 KV 文件所在的FirstApp目录后,发出以下命令:

ahmed-gad@ubuntu:~/ahmedgad/FirstApp$ buildozer init

发出该命令后,出现确认信息,如图 8-15 所示。该文件的一些重要字段在清单 8-15 中列出。例如,title代表应用标题;source目录是指main.py文件所在的应用的根目录,这里设置为当前目录;app 版本;Python 和 Kivy 版本;orientation,即应用是否全屏出现;和应用requirements,这只是设置为 kivy。如果我们使用 P4A 支持的库,比如 NumPy,那么我们需要将它列在 kivy 旁边,以便将其加载到应用中。permissions属性表示应用请求的权限。如果您的计算机上已经存在 SKD 和 NDK 的路径,您也可以对它们进行硬编码,以节省下载时间。注意,行前的#字符表示它是一个注释。presplash.filename属性用于指定启动前加载应用时出现的图像路径。icon.filename属性被赋予用作应用图标的图像的文件名。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-15

成功创建buildozer.spec文件

这些字段位于规范文件的[app]部分。您还可以编辑规范文件,以更改您认为值得修改的任何字段。默认情况下,package.domain属性被设置为org.test,这仅用于测试,不用于生产。如果该值保持不变,它将阻止应用的构建。

[app]
title = Simple Application
package.name = firstapp
package.domain = gad.firstapp
source.dir = .
source.include_exts = py,png,jpg,kv,atlas
version = 0.1
requirements = kivy
orientation = portrait
osx.python_version = 3
osx.kivy_version = 1.10.1
fullscreen = 0
presplash.filename = presplash.png
icon.filename = icon.png
android.permissions = INTERNET
android.api = 19
android.sdk = 20
android.ndk = 9c
android.private_storage = True
#android.ndk_path =
#android.sdk_path =

Listing 8-15Some Important Fields from the buildozer.spec File

在准备好构建 Android 应用所需的文件之后,下一步是使用 Buildozer 构建它。

使用推土机构建 android 应用

准备好所有的项目文件后,Buildozer 使用它们来生成 APK 文件。对于开发,我们可以使用以下命令生成应用的调试版本:

ahmed-gad@ubuntu:~/ahmedgad/FirstApp$ buildozer android release

图 8-16 显示了输入命令时的响应。第一次构建应用时,Buildozer 必须下载所有必需的依赖项,比如 SDK、NDK 和 P4A。Buildozer 通过自动下载和安装它们节省了很多精力。根据您的互联网连接,该过程可能需要一段时间才能全部启动和运行;耐心点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-16

安装 Buildozer 构建 Android 应用所需的依赖项

安装成功完成后,会创建两个文件夹。第一个名为.buildozer;它表示 Buildozer 下载的构建应用所需的所有文件。第二个文件夹名为bin;它存储构建应用后生成的 APK 文件。我们可以将 APK 文件转移到 Android 设备上进行安装和测试。Android 应用的屏幕如图 8-17 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-17

运行 Android 应用

如果机器连接并识别了一个 Android 设备,Buildozer 可以根据以下命令生成 APK 文件并在机器上安装它:

ahmed-gad@ubuntu:~/ahmedgad/FirstApp$ buildozer android debug deploy run

在基于 Python Kivy 应用构建了基本的 Android 应用之后,我们可以开始构建更高级的应用。并非所有运行在桌面上的 Kivy 应用都可以直接在移动设备上运行。某些库可能不支持打包到移动应用中。例如,P4A 只支持一组可以在 Android 应用中使用的库。如果您使用了不支持的库,应用会崩溃。

P4A 支持 Kivy,它可以构建与我们之前讨论的完全一样的应用 UI。P4A 还支持其他库,如NumPyPILdateutilOpenCVPyiniusFlask等等。使用 Python 构建 Android 应用的限制是只能使用 P4A 支持的库集。在下一节中,我们将讨论如何从第三章中创建的应用构建一个 Android 应用,用于识别水果 360 数据集图像。

Android 上的图像识别

第三章创建的应用从 Fruits 360 数据集提取特征,用于训练人工神经网络。在第七章中,创建了一个 Flask 应用来从网络上访问它。在这一章中,我们将讨论如何将它打包到一个离线运行的 Android 应用中,并在设备上提取功能。

首先要考虑的是这个应用中使用的库是否受 P4A 支持。使用的库如下:

  • scikit-image用于读取原始 RGB 图像并将其转换为 HSV。

  • NumPy用于提取特征(即色调直方图),构建人工神经网络层,并进行预测。

  • pickle用于恢复使用遗传算法和所选特征元素的指数训练的网络的最佳权重。

从使用的库中,P4A 只支持 NumPy。不支持scikit-imagepickle。因此,我们必须找到 P4A 支持的替代库来取代这两个库。替换scikit-image的选项有OpenCVPIL。我们只需要一个库来读取图像文件,并将其转换为 HSV,仅此而已。OpenCV比所需的两个功能更多。将这个库打包到 Android 应用中会增加它的大小。为此,使用PIL是因为它更简单。

关于pickle,我们可以用NumPy来代替。NumPy可以在扩展名为.npy的文件中保存和加载变量。因此,权重和所选元素指数将保存到.npy文件中,以便使用NumPy读取。

项目结构如图 8-18 所示。Fruits.py文件包含从测试图像中提取特征并预测其标签所需的函数。除了使用NumPy而不是picklePIL而不是scikit-image之外,这些功能与第三章中的功能几乎相同。清单 8-16 中给出了该文件的实现。

extract_features()函数有一个代表图像文件路径的参数。它使用 PIL 读取它,并使用convert方法将其转换到 HSV 颜色空间。这个方法接受指定图像要被转换成 HSV 的HSV字符串。然后,extract_features()方法提取特征,根据所选索引的.npy文件过滤特征元素,最后返回。使predict_outputs()函数接受权重.npy文件路径,然后使用NumPy读取它,基于 ANN 对图像进行分类,并返回分类标签。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-18

Android 上水果 360 数据集图像识别项目架构

import numpy
import PIL.Image

def sigmoid(inpt):
    return 1.0/(1.0+numpy.exp(-1*inpt))

def relu(inpt):
    result = inpt
    result[inpt<0] = 0
    return result

def predict_output(weights_mat_path, data_inputs, activation="relu"):
    weights_mat = numpy.load(weights_mat_path)
    r1 = data_inputs
    for curr_weights in weights_mat:
        r1 = numpy.matmul(a=r1, b=curr_weights)
        if activation == "relu":
            r1 = relu(r1)
        elif activation == "sigmoid":
            r1 = sigmoid(r1)
    r1 = r1[0, :]
    predicted_label = numpy.where(r1 == numpy.max(r1))[0][0]
    return predicted_label

def extract_features(img_path):
    im = PIL.Image.open(img_path).convert("HSV")
    fruit_data_hsv = numpy.asarray(im, dtype=numpy.uint8)

    indices = numpy.load(file="indices.npy")

    hist = numpy.histogram(a=fruit_data_hsv[:, :, 0], bins=360)
    im_features = hist[0][indices]
    img_features = numpy.zeros(shape=(1, im_features.size))
    img_features[0, :] = im_features[:im_features.size]
    return img_features

Listing 8-16Fruits.py Module for Extracting Features and Classifying Images

清单 8-17 中给出了负责构建应用 UI 的 KV 文件first.kv。值得一提的是,标签和按钮小部件的字体大小都是使用font_size属性增加的。另外,调用classify_image()方法来响应按钮部件on_press事件。

BoxLayout:
    orientation: "vertical"
    Label:
        text: "Predicted Class Appears Here."
        font_size: 30
        id: label
    BoxLayout:
        orientation: "horizontal"
        Image:
            source: "apple.jpg"
            id: img
    Button:
        text: "Classify Image."
        font_size: 30
        on_press: app.classify_image()

Listing 8-17KV File of the Fruits Recognition Application

根据清单 8-18 ,在main.py文件中可以找到classify_image()方法的实现。该方法从 image 小部件的 source 属性加载要分类的图像的路径。该路径作为参数传递给水果模块中的extract_features()函数。predict_output()函数接受提取的特征、人工神经网络权重和激活函数。它在每一层的输入和它的权重之间的矩阵乘法之后返回分类标签。然后标签被打印在标签小部件上。

import kivy.app
import Fruits

class FirstApp(kivy.app.App):
    def classify_image(self):
        img_path = self.root.ids["img"].source

        img_features = Fruits.extract_features(img_path)

        predicted_class = Fruits.predict_output("weights.npy", img_features, activation="sigmoid")

        self.root.ids["label"].text = "Predicted Class : " + predicted_class

firstApp = FirstApp(title="Fruits 360 Recognition.")
firstApp.run()

Listing 8-18Implementation of the main.py File of the Fruits Recognition Application

在开始构建 APK 文件之前,我们可以通过运行 Kivy 应用来确保一切正常。运行应用并按下按钮后,图像被分类;结果如图 8-19 所示。在确保应用成功运行之后,我们可以开始构建 Android 应用了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-19

分类图像后运行 Kivy 应用的结果

在使用 Buildozer 构建应用之前,必须生成buildozer.spec文件。您可以使用buildozer init命令自动创建它。值得注意的是,在应用内部,我们使用两个.npy文件来表示过滤后的元素索引和权重。我们需要把它们放进 APK 的档案里。我们如何做到这一点?在buildozer.spec文件中,有一个名为source.include_exts的属性。它接受我们需要包含到 APK 文件中的所有文件的扩展名,用逗号分隔。这些文件位于应用的根目录下。例如,添加扩展名为pynpykvpngjpg的文件,属性如下:

source.include_exts = py,png,jpg,kv ,npy

成功执行应用的两个关键步骤是使用 PIL 将 RGB 图像转换为 HSV,以及使用 NumPy 中的matmul()函数进行矩阵乘法。注意使用提供这些功能的库版本。

关于从 RGB 到 HSV 的转换,请确保使用新版本的 PIL 枕头。它只是 PIL 的一个扩展,可以毫无区别地导入和使用。关于矩阵乘法,只有 NumPy 1 . 10 . 0 版及更高版本支持。注意不要使用较低的版本。这留下了一个额外的问题,即如何告诉 P4A 我们需要使用特定版本的库。一种方法是在对应于 NumPy 的 P4A 配方中指定所需的版本。这些方法位于 Buildozer 安装目录下的 P4A 安装目录中。例如,根据图 8-20 使用版本 1.10.1。基于指定的版本,库将从 Python 包索引(PyPI)下载,并在构建应用时自动安装。注意,为 Android 准备 Kivy 的环境比它的使用更难。我们生活在一个准备开发环境比开发本身更难的时代。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-20

指定要安装的 NumPy 版本

现在我们已经准备好构建 Android 应用了。我们可以使用命令buildozer android debug deploy run在连接到开发机器的 Android 设备上构建、安装和运行应用。我们还可以使用logcat工具来打印设备的调试信息。只要在命令的末尾加上这个词。构建成功后,Android 应用 UI 将如图 8-21 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-21

Android 应用的用户界面,用于对水果 360 数据集的图像进行分类

安卓上的 CNN

在第五章的第节中,我们创建了一个使用NumPy从头构建 CNN 的项目。在本节中,这个项目将被打包到一个 Android 应用中,以便在设备上执行 CNN。项目结构如图 8-22 所示。numpycnn.py文件包含第五章中讨论的用于构建 CNN 层的所有函数。名为main.py的主应用文件有一个名为NumPyCNNApp的子类。这就是 KV 文件应该命名为numpycnn.kv的原因。buildozer.spec文件类似于我们之前讨论过的。我们将简单地讨论主文件和它的 KV 文件。根据本章前面的讨论,预计项目这一部分的大部分内容会很清楚。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-22

在 Android 上运行 CNN 的项目结构

我们将从清单 8-19 中的 KV 文件开始。根小部件是一个垂直的BoxLayout,它有两个子部件GridLayout。第一个GridLayout小部件显示原始图像和 CNN 最后一层的结果。它被平均分配来容纳两个垂直的子BoxLayout窗口小部件。每个布局都有标签和图像小部件。标签只是让它指示原始图像和结果图像的位置。

根小部件的第二个子部件GridLayout,有三个小部件。第一个是一个Button,当它被按下时,通过调用主 Python 文件中的start_cnn()方法来执行 CNN。第二个是一个Label,打印执行完所有 CNN 图层后结果的大小。最后,第三个子组件是一个TextInput小部件,它允许用户以文本形式指定 CNN 的架构。例如,conv2,pool,relu表示网络由三层组成:第一层是具有四个过滤器的 conv 层,第二层是平均池层,第三层是 ReLU 层。当应用运行时,其用户界面如图 8-23 所示。

BoxLayout:
    orientation: "vertical"
    GridLayout:
        size_hint_y: 8
        cols: 3
        spacing: "5dp", "5dp"
        BoxLayout:
            orientation: "vertical"
            Label:
                id: lbl1
                size_hint_y: 1
                font_size: 20
                text: "Original"
                color: 0, 0, 0, 1
            Image:
                source: "input_image.jpg"
                id: img1
                size_hint_y: 5
                allow_stretch: True
        BoxLayout:
            orientation: "vertical"
            Label:
                id: lbl2
                size_hint_y: 1
                font_size: 20
                text: ""
                color: 0, 0, 0, 1
            Image:
                id: img2
                size_hint_y: 5
                allow_stretch: True
    GridLayout:
        cols: 3
        size_hint_y: 1
        Button:
            text: "Run CNN"
            on_press: app.start_cnn()
            font_size: 20
            id: btn
        Label:
            text: "Click the button & wait."
            id: lbl_details
            font_size: 20
            color: 0, 0, 0, 1
        TextInput:
            text: "conv4,pool,relu"
            font_size: 20
            id: cnn_struct

Listing 8-19KV File of the CNN Kivy Application

清单 8-20 中给出了main.py文件的实现。这个文件的入口点是start_cnn()方法。它从Image小部件中读取图像路径,并使用我们在前面的例子中讨论过的 PIL 来读取它。为简单起见,使用convert()方法将图像转换为灰色。字符L将图像转换成灰色。按下Button小部件后,该函数运行一个后台线程,该线程根据TextInput中指定的结构执行 CNN。最后一层的结果返回给refresh_GUI()方法。此方法在 UI 窗口上显示结果的第一个矩阵。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-23

执行 CNN 的 Kivy 应用的主窗口

import kivy.app
import PIL.Image
import numpy
import numpycnn
import threading
import kivy.clock

class NumPyCNNApp(kivy.app.App):

    def run_cnn_thread(self):
        layers = self.root.ids["cnn_struct"].text.split(",")
        self.root.ids["lbl_details.text"] = str(layers)
        for layer in layers:
            if layer[0:4] == "conv":
                if len(self.curr_img.shape) == 2:
                    l_filter = numpy.random.rand(int(layer[4:]), 3, 3)
                else:
                    l_filter = numpy.random.rand(int(layer[4:]), 3, 3, self.curr_img.shape[-1])
                self.curr_img = numpycnn.conv(self.curr_img, l_filter)
                print("Output Conv : ", self.curr_img.shape)
            elif layer == "relu":
                self.curr_img = numpycnn.relu(self.curr_img)
                print("Output RelU : ", self.curr_img.shape)
            elif layer == "pool":
                self.curr_img = numpycnn.avgpooling(self.curr_img)
                print("Output Pool : ", self.curr_img.shape)
            elif layer[0:2] == "fc":
                num_outputs = int(layer[2:])
                fc_weights = numpy.random.rand(self.curr_img.size, num_outputs)
                print("FC Weights : ", fc_weights.shape)
                self.CNN_FC_Out = numpycnn.fc(self.curr_img, fc_weights=fc_weights, num_out=num_outputs)
                print("FC Outputs : ", self.CNN_FC_Out)
                print("Output FC : ", self.CNN_FC_Out.shape)
            else:
                self.root.ids["lbl_details"].text = "Check input."
                break
        self.root.ids["btn.text"] = "Try Again."
        self.refresh_GUI()

    def start_cnn(self):
        img1 = self.root.ids["img1"]#Original Image
        im = PIL.Image.open(img1.source).convert("L")
        img_arr = numpy.asarray(im, dtype=numpy.uint8)
        self.curr_img = img_arr

        im_size = str(self.curr_img.shape)
        self.root.ids["lbl_details"].text = "Original image size " + im_size

        threading.Thread(target=self.run_cnn_thread).start()
        self.root.ids["btn"].text = "Wait."

    @kivy.clock.mainthread
    def refresh_GUI(self):
        im = PIL.Image.fromarray(numpy.uint8(self.curr_img[:, :, 0]))
        layer_size = str(self.curr_img.shape)
        im.save("res.png")
        self.root.ids["img2"].source = "res.png"
        self.root.ids["lbl2"].text = "Last Layer Result"
        self.root.ids["lbl_details"].text = "Out size "+layer_size

if __name__ == "__main__":
    NumPyCNNApp().run()

Listing 8-20Implementation of the Main File of the Kivy Application Executing CNN

线程执行run_cnn_thread()方法。该方法从分割从TextInput中检索的文本开始,分别返回每个层。基于 if 语句,从numpycnn.py文件中调用合适的函数来构建指定的 CNN 层。例如,如果当前字符串是relu,那么relu函数将被调用。追加到conv字符串的数字用作指定过滤器数量的参数。所有过滤器的形状都是 3×3。它们是随机填充的。如果有未识别的字符串,应用会在Label上显示一条消息,指出输入有问题。该函数执行完毕后,返回到refresh_GUI()方法。它显示返回的第一个矩阵,并在Label上打印其大小。

此应用的修改版本允许运行所有三个连续的 conv、池和 ReLU 层,并显示所有这些层返回的结果。基于前三层(两个过滤器,带有两个过滤器的 conv 层,接着是池化,然后是 ReLU),所有返回的结果如图 8-24 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-24

基于三层 CNN (conv2,pool,relu)的所有层的结果

在确保应用在桌面上运行良好之后,构建应用剩下的唯一文件是buildozer.spec文件。可以根据我们之前的讨论来准备。成功创建之后,我们可以像前面一样使用 Buildozer 开始构建它。在 Android 设备上运行应用后的用户界面如图 8-25 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-25

运行 Kivy 应用在 Android 设备上执行 CNN

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值