
一 背景
二 原理篇

1 wasm内存布局

2 Javascript与Python的互访
const arr_pyproxy = pyodide.globals.get('arr') // arr是python里的一个全局对象
import jsfrom js import foo # foo是Javascript里的一个全局对象
【自动转换】对于简单类型,如数字、字符串、布尔等,会被自动拷贝内存值,此时产生的就不是Proxy、而是最终的值了。
【半自动转换】非简单的内置类型,都需要通过to_js()、to_py()方式来显式转换:
对于Python内置的list、dict、numpy.ndarray等对象,不属于简单类型,不会自动转换类型,必须通过pyodide.to_js()来转,相应的会被转成JS的list、map、TypedArray类型
反过来也类似,通过to_py()方法,JS的TypedArray转为memoryview,list、map转为list、dict
【手动转换】各种class、function和用户自定义类型,因为对方的语言没有对应的现成类型,所以只能以proxy的形式存在,需要通过运算符来间接操纵,就像操纵提线木偶一样。为了达到方便操纵的目的,pyodide对两种语言进行了语法模拟,用一种语言里的操作符模拟另一种语言的类似行为。例如:JS中的let a=new XXX(),在Python中就变为a=XXX.new()。
import pyodideimport js.cv as cv2print(dir(cv2))
三 实践篇
1 初始化python
function loadJS( url, callback ){var script = document.createElement('script'),fn = callback || function(){};script.type = 'text/javascript';script.onload = function(){fn();};script.src = url;document.getElementsByTagName('head')[0].appendChild(script);}// 加载opencvloadJS('https://test-bucket-duplicate.oss-cn-hangzhou.aliyuncs.com/public/opencv/opencv.js', function(){console.log('js load ok');});// 加载推理引擎onnxruntime.js。当然也可以使用其他推理引擎loadJS('https://test-bucket-duplicate.oss-cn-hangzhou.aliyuncs.com/public/onnxruntime/onnx.min.js', function(){console.log('js load ok');});// 初始化python运行环境loadJS('https://test-bucket-duplicate.oss-cn-hangzhou.aliyuncs.com/public/pyodide/0.18.0/pyodide.js', function(){console.log('js load ok');});pyodide = await loadPyodide({ indexURL : "https://test-bucket-duplicate.oss-cn-hangzhou.aliyuncs.com/public/pyodide/0.18.0/"});await pyodide.loadPackage(['micropip']);


2 加载pypi包
pypi.org上的纯python包可以用micropip.install() 直接安装
含有C语言扩展(编译为动态链接库)的wheel包,需要对照官方已编译包的列表
在列表中的直接用micropip.install()安装
不在这个列表里的,就需要自己手动编译后发布到服务器后再用micropip.install()安装。

设置允许跨域
对于wasm格式的文件请求,响应Header里应当带上:"Content-type": "application/wasm"
location ~ ^/wasm/ {add_header 'Access-Control-Allow-Origin' "*";add_header 'Access-Control-Allow-Credentials' "true";root path/to/wasm_dir;header_filter_by_lua 'uri = ngx.var.uriif string.match(uri, ".js$") == nil thenngx.header["Content-type"] = "application/wasm"end';}
await pyodide.runPythonAsync(`import micropipmicropip.install(["numpy", "Pillow"])`);await pyodide.runPythonAsync(`import pyodideimport js.cv as cv2import js.onnx as onnxruntimeimport numpy as np`);
3 opencv的使用
await pyodide.runPythonAsync(`# 构造一个1080p图片h,w = 1080,1920img = np.arange(h * w * 3, dtype=np.uint8).reshape(h, w, 3)# 使用cv2.resize将其缩小为1/10# 原python代码:small_img = cv2.resize(img, (h_small, w_small))# 改成调用opencv.js:h_small,w_small = 108, 192mat = cv2.matFromArray(h, w, cv2.CV_8UC3, pyodide.to_js(img.reshape(h * w * 3)))dst = cv2.Mat.new(h_small, w_small, cv2.CV_8UC3)cv2.resize(mat, dst, cv2.Size.new(w_small, h_small), 0, 0, cv2.INTER_NEAREST)small_img = np.asarray(dst.data.to_py()).reshape(h_small, w_small, 3)`);
await pyodide.runPythonAsync(`# 使用cv2.findContours来检测轮廓。假设mask为二维numpy数组,只有0、1两个值# 原python代码:contours = cv2.findContours(mask, cv2.RETR_CCOMP,cv2.CHAIN_APPROX_NONE)# 改成调用opencv.js:contours_jsproxy = cv2.MatVector.new() # cv2.Mat数组,对应opencv.js中的 contours = new cv.MatVector()语句hierarchy_jsproxy = cv2.Mat.new()mat = cv2.matFromArray(mask.shape[0], mask.shape[1], cv2.CV_8UC1, pyodide.to_js(mask.reshape(mask.size)))cv2.findContours(mat, pyodide.to_js(contours_jsproxy), pyodide.to_js(hierarchy_jsproxy), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)# contours js格式转python格式contours = []for i in range(contours_jsproxy.size()):c_jsproxy = contours_jsproxy.get(i)c = np.asarray(c_jsproxy.data32S.to_py()).reshape(c_jsproxy.rows, c_jsproxy.cols, 2)contours.append(c)`);
4 推理引擎的使用
await pyodide.runPythonAsync(`model_url="onnx模型的地址"session = onnxruntime.InferenceSession.new()session.loadModel(model_url)session.run(......)`);
5 挂载持久存储文件系统
// 创建挂载点pyodide.FS.mkdir('/mnt');// 挂载文件系统pyodide.FS.mount(pyodide.FS.filesystems.IDBFS, {}, '/mnt');// 写入一个文件pyodide.FS.writeFile('/mnt/test.txt', 'hello world');// 真正的保存文件到持久文件系统pyodide.FS.syncfs(function (err) {console.log(err);});
// 创建挂载点pyodide.FS.mkdir('/mnt');// 挂载文件系统pyodide.FS.mount(pyodide.FS.filesystems.IDBFS, {}, '/mnt');// 写入一个文件pyodide.FS.writeFile('/mnt/test.txt', 'hello world');// 真正的保存文件到持久文件系统pyodide.FS.syncfs(function (err) {console.log(err);});
6 打wheel包
micropip.install("https://foo.com/bar-1.2.3-xxx.whl")from bar import ...
四 存在的缺陷
Python运行环境加载和初始化时间有点儿长,视网络情况,几秒到几十秒都有可能。
pypi包支持的不完整。虽然pypi.org上的纯python包都可以直接使用,但涉及到C扩展写的包,如果官方还没编译出来,那就需要自己动手编译了。
个别很常用的包,例如opencv,还没成功编译出来;模型推理框架一个都没有。不过还好可以通过相应的JS库来弥补。
如果python中调用了js库的话:
可能会产生一定的内存拷贝开销(从wasm内存到JS内存的来回拷贝)。尤其是大数组作为参数或返回值,在速度要求高的场合下,额外的内存拷贝开销就不能忽视了。
python库的方法接口可能跟其对应的js库的接口参数、返回值格式不一致,有一定的适配工作量。
五 总结
https://test-bucket-duplicate.oss-cn-hangzhou.aliyuncs.com/public/pyodide/test.html
2、文档:
https://pyodide.org/en/stable/usage/type-conversions.html
https://emscripten.org/docs/api_reference/Filesystem-API.html
持久化储存训练营
“Kubernetes 难点攻破训练营系列”的初心是和开发者们一起应对学习和使用 K8s的挑战。这一次,我们从容器持久化存储开始。
点击阅读原文查看详情~
文章转载自阿里技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




