数字旗手

电气化、自动化、数字化、智能化、智慧化

0%

开源深度学习计算平台ImJoy解析:8 -- 使用python编写计算插件

上一篇着重介绍了如何使用JavaScript库来编写插件的前端UI和后端计算逻辑,这一节会介绍如何将计算后端切换为python语言,即计算逻辑完全使用python编写,充分利用python庞大的计算生态。
使用python开发计算插件有两种:
(1)web-python:即python运行在浏览器中,其原理实际是应用了Pyodide这一工具,将python代码编译在浏览器中,但其缺点也很明显,首先是加载速度非常慢,因为第一次运行时需要将所用的python库都下载下来;然后其也无法应用整个python深度学习生态。
(2)native-python:该类型插件会链接一个本地的jupyter插件引擎,可以充分发挥python的最大价值,本篇也将着重介绍该种插件的编写。

web-python的hello world

先看一个使用web-python编写的hello world例子,它会完全在浏览器中运行Python代码。
注意,当运行以下插件时,会需要一段时间,因为它需要将python库加载到浏览器中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<docs lang="markdown">
[TODO: write documentation for this plugin.]
</docs>
<config lang="json">
{
"name": "Untitled Plugin",
"type": "web-python",
"version": "0.1.0",
"description": "[TODO: describe this plugin with one sentence.]",
"tags": [],
"ui": "",
"cover": "",
"inputs": null,
"outputs": null,
"flags": [],
"icon": "extension",
"api_version": "0.1.8",
"env": "",
"permissions": [],
"requirements": [],
"dependencies": []
}
</config>
<script lang="python">
from imjoy import api
class ImJoyPlugin():
def setup(self):
api.log('initialized')
def run(self, ctx):
api.alert('hello world.')
api.export(ImJoyPlugin())
</script>

实测速度非常慢,所以并不推荐使用web-python这种方式来编写插件。

native-python开发插件

如果想充分利用python的深度学习生态,唯一使用的方式就是native-python这种开发模式。
此模式的使用可以有三种组合方式:
(1)ImJoy官方部署+MyBinder插件引擎;
(2)ImJoy官方部署+本地Jupyter插件引擎;
(3)本地部署+本地Jupyter插件引擎。
第一种因为使用MyBinder这一免费的Jupyter托管方案,其性能会较弱,通常只用于demo用途,因此不推荐;
第二种会使用ImJoy的官方web app来作为应用入口,因此可能会受限于其官网的可连接性,快速开发时推荐使用;
第三种web app和Jupyter都是在本地部署,因此有最大的灵活性。本部分将对第三种的环境搭建做一介绍。

本地部署web app

ImJoy的主web app程序也在GitHub上进行了开源,见这里
(1)clone该仓库:

1
git clone git@github.com:imjoy-team/ImJoy.git

(2)安装依赖包
进入web文件夹,然后:
1
npm install

这一步需要安装nodejs,此处不详细介绍,可以移步这里
(3)编译运行:
有两种编译和运行方式,一种是开发模式:
1
npm run serve

或者生产模式:
1
npm run build

(4)访问app:
上一步运行该app后,就会生成可访问的链接,通常是:
1
http://localhost:8001

搭建本地Jupyter插件引擎

搭建本地Jupyter插件引擎有两种方法:
(1)安装Jupyter notebook(通过pip install jupyter),然后安装imjoy-jupyter-extension
(2)可以通过 pip install imjoy 安装这个ImJoy-Engine库。
推荐使用后者,因为这样可以对Jupyter服务器做一些对ImJoy有用的设置,并且不需要单独安装imjoy-jupyter-extension。
具体的搭建流程如下:
(1)下载并安装conda环境:推荐使用python3.7版本的Anaconda。
(2)安装引擎:

1
pip install -U imjoy[jupyter]

(3)启动引擎:
1
imjoy --jupyter

然后在终端就会得到形如:
1
http://localhost:8888/?token=caac2d7f2e8e0...ad871fe

的链接。这就是插件引擎的地址。
(4)连接web app
在前面开启的web app页面上http://localhost:8001/#/app,点击右上角的小火箭图标,然后点击Add Jupyter-Engine,将上面插件引擎的地址填入即可。

native-python的hello world

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<docs lang="markdown">
[TODO: write documentation for this plugin.]
</docs>
<config lang="json">
{
"name": "Untitled Plugin",
"type": "native-python",
"version": "0.1.0",
"description": "[TODO: describe this plugin with one sentence.]",
"tags": [],
"ui": "",
"cover": "",
"inputs": null,
"outputs": null,
"flags": [],
"icon": "extension",
"api_version": "0.1.8",
"env": "",
"permissions": [],
"requirements": [],
"dependencies": []
}
</config>
<script lang="python">
from imjoy import api

class ImJoyPlugin():
def setup(self):
api.log('initialized')
def run(self, ctx):
api.alert('hello world.')
api.export(ImJoyPlugin())
</script>

用python写图像处理插件

这一部分尝试将构建基于Web的图像分析插件这一篇中的opencv.js功能用python版的opencv实现一遍。
在此例中,有两个插件:UI插件和compute插件。一般来说,有两种方法可以连接它们:
(1)首先用api.createWindow(...)从compute插件实例化UI插件,然后与返回的窗口对象进行交互;
(2)也可以直接启动UI插件,然后通过api.getPlugin()来获取compute插件提供的api。
两种方法到底用哪一种取决于应用程序的实际需要,这里推荐第一种方式用于Python插件的编写,因为它可以更轻松地在Jupyter笔记本中调试。

这里的插件是用Python重写计算功能、JavaScript仍然是前端,因此涉及到两种语言对图像格式的转译,需要进行编码和解码以使它们交叉兼容。最简单的方法是将图像编码为“base64”字符串。
因此,整个插件的流程为(本节末尾会给出所有代码,这里是将代码分解):
(1)从UI插件(即图像查看器)的canvas画布中得到图像的base64编码:

1
2
3
4
const canvas = document.getElementById('canvas-id')

// get `base64` encoded image from a canvas
const base64String = canvas.toDataURL()

(2)在UI插件中调用compute插件中的函数,并传递上面的base64编码:
UI插件能调用compute插件中的python函数,是通过插件中的ctx变量来得到它,形如:
1
2
3
4
5
6
7
8
// the run funciton of the image viewer
async run(ctx){
// check if there is a process function passed in
if(ctx.data && ctx.data.process){
// show an additional "Process in Python" button
// and set the call back to use this process function
}
}

相对应地,在Python插件中就可以执行await api.createWindow(type="Image Viewer", data={"process": self.process})来传给JS插件(假设已经在插件中定义了一个名为 process的函数)。
在调用api.createWindow 时,有两种方法可以引用另一个窗口插件:
(a)将type键设置为窗口插件名称,例如如果UI插件名为My Window Plugin,就将其设置为type。注意,这个名称是从 <config> 块中的 name 定义中获得的。
(b)如果UI插件是源代码的形式或者由公共服务器提供,可以设置src作为插件源代码或者插件URL,比如name="Kaibu",src="https://kaibu.org/#/app"。在这种情况下,插件将被动态填充。例如,它允许将窗口插件存储为Python中的字符串,甚至可以根据模板动态生成窗口插件。

另外一个需要注意的是,如果是使用await api.createWindow(type="Image Viewer", data={"process": self.process}),此时会发现,如果第二次单击该按钮,它将不再起作用,并且如果转到浏览器控制台,将看到一条错误消息,提示Callback function can only called once, if you want to call a function for multiple times, please make it as a plugin api function.。这是因为在第一次调用后从窗口中删除了process函数。为了明确地告诉窗口保留process函数,可以将一个特殊的键_rintf设置为True,即把上面的代码改成data={"process": self.process, "_rintf": True}
(3)在python插件中解码base64,并读取为numpy类型数组:

1
2
3
4
5
6
7
8
9
10
11
12
import re
import base64
import io
import imageio

def base64_to_image(base64_string, format=None):
'''This function takes a base64 string as input
and decode it into an numpy array image
'''
base64_string = re.sub("^data:image/.+;base64,", "", base64_string)
image_file = io.BytesIO(base64.b64decode(base64_string.encode('ascii')))
return imageio.imread(image_file, format)

(4)在python插件中编写图像处理算法:
这里仍然使用的是opencv,不过要用的是它的python版本:

1
2
3
4
5
6
7
"requirements": ["opencv-python"]

import cv2

def process_image(src):
dst = cv2.cvtColor(src, cv2.COLOR_RGBA2GRAY)
return dst

(5)将numpy数组类型的处理结果编码为base64并返回:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def image_to_base64(image_array):
'''This function takes a numpy image array as input
and encode it into a base64 string
'''
buf = io.BytesIO()
imageio.imwrite(buf, image_array, "PNG")
buf.seek(0)
img_bytes = buf.getvalue()
base64_string = base64.b64encode(img_bytes).decode('ascii')
return 'data:image/png;base64,' + base64_string


async def process(self, base64string):
img = base64_to_image(base64string)
dst = process_image(img)
base64dst = image_to_base64(dst)
return base64dst

(6)在JS插件中接收base64编码,并在画布中显示为图像:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// draw a `base64` encoded image to the canvas
const drawImage = (canvas, base64Image)=>{
return new Promise((resolve, reject)=>{
const img = new Image()
img.crossOrigin = "anonymous"
img.onload = function(){
const ctx = canvas.getContext("2d");
canvas.width = Math.min(this.width, 512);
canvas.height= Math.min(this.height, parseInt(512*this.height/this.width), 1024);
// draw the img into canvas
ctx.drawImage(this, 0, 0, canvas.width, canvas.height);
resolve(canvas);
}
img.onerror = reject;
img.src = base64Image;
})
}

整个插件的处理逻辑如上,结果与完全JS作为前端和后端的结果相同,如下图:
pythonbackend

完整代码如下:
对于UI插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<config lang="json">
{
"name": "Image Viewer",
"type": "window",
"tags": [],
"ui": "",
"version": "0.1.0",
"cover": "",
"description": "This is a demo plugin for displaying image",
"icon": "extension",
"inputs": null,
"outputs": null,
"api_version": "0.1.8",
"env": "",
"permissions": [],
"requirements": [
"https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css",
"https://use.fontawesome.com/releases/v5.14.0/js/all.js"],
"dependencies": []
}
</config>

<script lang="javascript">
const drawImage = (canvas, base64Image)=>{
return new Promise((resolve, reject)=>{
const img = new Image()
img.crossOrigin = "anonymous"
img.onload = function(){
const ctx = canvas.getContext("2d");
canvas.width = Math.min(this.width, 512);
canvas.height= Math.min(this.height, parseInt(512*this.height/this.width), 1024);
// draw the img into canvas
ctx.drawImage(this, 0, 0, canvas.width, canvas.height);
resolve(canvas);
}
img.onerror = reject;
img.src = base64Image;
})
}
const readImageFile = (file)=>{
return new Promise((resolve, reject)=>{
const U = window.URL || window.webkitURL;
if(U.createObjectURL){
resolve(U.createObjectURL(file))
}
else{
const fr = new FileReader();
fr.onload = function(e) {
resolve(e.target.result)
};
fr.onerror = reject
fr.readAsDataURL(file);
}
})
}

class ImJoyPlugin{
async setup(){
const fileInput = document.getElementById("file-input");
const canvas = document.getElementById("input-canvas");
const outputcanvas = document.getElementById("output-canvas");
fileInput.addEventListener("change", async ()=>{
const img = await readImageFile(fileInput.files[0]);
await drawImage(canvas, img);
}, true);
await api.log("plugin initialized")
const selectButton = document.getElementById("select-button");
selectButton.addEventListener("click", async ()=>{
fileInput.click()
}, true);
}
async run(ctx){
if(ctx.data && ctx.data.process){
const canvas = document.getElementById("input-canvas");
const outputcanvas = document.getElementById("output-canvas");
const btn = document.getElementById('process-button')
btn.disabled = false;
btn.addEventListener("click", async ()=>{
const base64String = canvas.toDataURL()
const base64dst = await ctx.data.process(base64String)
await drawImage(outputcanvas, base64dst)
}, true);
}
}
}
api.export(new ImJoyPlugin())
</script>

<window>
<div>
<input id="file-input" accept="image/*" capture="camera" type="file"/>
<nav class="panel">
<p class="panel-heading">
<i class="fas fa-eye" aria-hidden="true"></i> My Image Viewer with Python backend
</p>
<div class="panel-block">
<button id="select-button" class="button is-link is-outlined is-fullwidth">
Open an image
</button>
<button id="process-button" disabled class="button is-link is-outlined is-fullwidth">
RGB to Gray
</button>
</div>
<div class="panel-block">
<canvas id="input-canvas" style="width: 100%; object-fit: cover;"></canvas>
<canvas id="output-canvas" style="width: 100%; object-fit: cover;"></canvas>
</div>
<div class="panel-block">
<button id="predict-button" class="button is-link is-outlined is-fullwidth">
Predict
</button>
</div>
</div>
</window>

<style>
#file-input{
display: none;
}
h1{
color: pink;
}
</style>

对于compute插件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<config lang="json">
{
"type": "native-python",
"name": "my-python-plugin",
"id": "9l3fewe7l",
"namespace": "9l3fewe7l",
"lang": "python",
"window_id": "code_9l3fewe7l",
"api_version": "0.1.8",
"description": "[TODO: describe this plugin with one sentence.]",
"tags": [],
"version": "0.1.0",
"ui": "",
"cover": "",
"icon": "extension",
"inputs": null,
"outputs": null,
"env": "",
"permissions": [],
"requirements": ["opencv-python"],
"dependencies": []
}
</config>

<script lang="python">
from imjoy import api
import re
import base64
import io
import imageio
import cv2

def image_to_base64(image_array):
'''This function takes a numpy image array as input
and encode it into a base64 string
'''
buf = io.BytesIO()
imageio.imwrite(buf, image_array, "PNG")
buf.seek(0)
img_bytes = buf.getvalue()
base64_string = base64.b64encode(img_bytes).decode('ascii')
return 'data:image/png;base64,' + base64_string

def base64_to_image(base64_string, format=None):
'''This function takes a base64 string as input
and decode it into an numpy array image
'''
base64_string = re.sub("^data:image/.+;base64,", "", base64_string)
image_file = io.BytesIO(base64.b64decode(base64_string.encode('ascii')))
return imageio.imread(image_file, format)

def process_image(src):
dst = cv2.cvtColor(src, cv2.COLOR_RGBA2GRAY)
return dst

class ImJoyPlugin():
async def setup(self):
pass
async def process(self, base64string):
img = base64_to_image(base64string)
dst = process_image(img)
base64dst = image_to_base64(dst)
return base64dst
async def run(self, ctx):
await api.createWindow(
type="Image Viewer",
data={
"process": self.process,
"_rintf": True})

api.export(ImJoyPlugin())
</script>

在调试上述插件时,因为涉及到了base64的编码和解码,我频繁用到了如下debug方法,推荐尝试:
(1)使用api.log(base64string)将base64编码结果显示在控制台中;
(2)使用这个网站Base64 to Image将base64编码可视化,以查看结果正不正确。

使用python深度学习库

如上,我们使用了opencv-python进行了简单的图像处理,验证了native python插件的可行性。
而除了opencv-python,python背后还有着更为广阔的深度学习生态,如tensorflow、pytorch、mxnet、paddlepaddle等深度学习框架,以及这些框架可调用的GPU资源,因此可以说整个python计算生态都可以被ImJoy的native python插件所调用,这就提供了非常广阔的应用空间。
该部分不再介绍native python怎样调用python深度学习库,而是在后面的具体应用中详细解析。