找回密码
 会员注册
查看: 16|回复: 0

WebAssembly在数字图像处理中的应用

[复制链接]

7

主题

0

回帖

22

积分

新手上路

积分
22
发表于 2024-9-30 07:15:59 | 显示全部楼层 |阅读模式
1. 前言数字图像处理的本质就是对图片的再次加工,这其中又分好几个层次,如下图 1 所示:图 1. 图像处理分类1.1 低层次的图像处理低层次图像处理主要对图象进行各种加工以改善图象的视觉效果、或突出有用信息,并为自动识别打基础,或通过编码以减少对其所需存储空间、传输时间或传输带宽的要求。简单来说,我们以 x,y 分别表示图片横纵坐标,那么这就是(x, y) => f(x, y)的过程,即对输入的图片的所有像素进行处理,并生成一个新的图片,如下图 2 所示。图 2. 低层次的滤镜处理1.2 中层次的图像分析中层次图像分析主要对图像中感兴趣的目标进行检测(或分割)和测量,以获得它们的客观信息,从而建立对图像中目标的描述,是一个从图像到数值或符号的过程。最终输出具有特征的数据,如下图 3 所示。图 3. 中层次的关键点识别1.3 高层次的图像理解高层次图像理解在中级图像处理的基础上,进一步研究图像中各目标的性质和它们之间相互的联系,并得出对图像内容含义的理解(对象识别)及对原来客观场景的解释(计算机视觉),从而指导和规划行动。其特点是基于人类对客观世界的认知,结合图像数据,最终产出的是对图像的理解,如下图 4 所示。图 4. 高层次的特征理解底层的图像处理能力为上层能力提供基本的数学工具,而上层的分析或者理解结果又能进一步优化底层图像处理效果,比如最近很火的“抖音 AI 作画”,就是基于输入图片的特征,绘制一副新的图画,如下图 5 所示。图 5. AI 识别原图特征并作画这里我们会优先重点介绍常见的基本图像处理方法——滤镜,以及基于伟大的计算机视觉库 OpenCV 做一些扩展的说明。2. 常见滤镜实现效果说明滤镜是一种经典的像素处理函数,大家知道图片是由大量纵横排列的像素构成的,每个像素在计算机中使用[红色,绿色,蓝色,不透明度]表示,所以滤镜的本质就是一个纯函数:我们以这张图片作为原始图片,列举一些常见的滤镜函数算法:图 6. 原始图片2.1 灰度效果按照 0.3 * R + 0.59 * G + 0.11 * B 的计算公式得出结果统一赋值给 R、G、B。图 7. 灰度图片2.2 黑白效果判断 R、G、B 的平均值是否超过了 100(阈值),超过就是纯白 255,否则纯黑 0。图 8. 黑白图片2.3 反色效果分别用 255 减去原始的 R、G、B 值,得到的就是反色值。图 9. 反色图片2.4 镜像效果把图片上 Y 对称轴左边的每一个像素,转移到对称点 (x' = width - x) 即可。图 10. 镜像图片2.5 浮雕效果按个扫描图像单元,分别用后一个图像单元的 R、G、B 减去前一个单元,得到的结果再进行灰度处理(不然会有色彩残留),此算法原理就是相当于绘制 R、G、B 相差特别大的轮廓。图 11. 浮雕图片2.6 卡通效果按照如下公式计算,会使得图片色彩变得更加鲜艳图 12. 卡通图片2.7 怀旧效果图 13. 怀旧图片2.8 熔铸效果图 14. 熔铸图片2.9 毛玻璃效果遍历每一个图像单元,然后取其附近的单元(如随机数取左右 1-8 个中任意一个),将这个随机单元数据填充到原始单元中,就会造成一种毛毛糙糙的感觉。图 15. 毛玻璃图片2.10 高斯模糊效果高斯模糊首先要确定模糊半径,原理就是让每个像素挨个去计算半径内其他像素的平均值,色彩平均了那就看起来是模糊的效果。具体原理可见阮一峰的博客图 16. 高斯模糊图片2.11 素描效果这里需要两个图层,A 图层是原图经过一次灰度效果后形成。然后拷贝 A 图层,应用反色处理生成 B 图层,再对 B 图层应用高斯模糊。A、B 图层合并的时候,分别对 R、G、B 采用如下公式计算。图 17. 素描图片3. 一个简单的图片处理站点我设计了一个前端处理站点,它可以接受摄像机或者用户上传的图片作为输入,支持多种类型的处理函数,最终输出图像元素。这并不是一个最好的实践,但是可以帮助大家更多了解一些基本知识。图 18. 简单的图片站点图片的输入源可以是上传的图片,也可以是通过浏览器提供的摄像机 API 进行拍照,我们将输入图片的像素数据统一转化为二进制数据格式,在中间中转层来进行滤镜处理。中转层可以支持多种运行方式,比如放在纯前端(JavaScript、WebAssembly),或者把数据传输给后端,使用相同的逻辑处理后再返回给前端。一方面可以验证各服务实现效果是否一致,另一方面也很容易进行对比总结。3.1 数据类型所有的图片都是按照像素来处理的,也就是说无论调用摄像头 API 向 Canvas 绘制数据的,或者读取用户上传的图片数据,以及最后绘制的结果,都是以 Uint8ClampedArray 作为数据格式传输的。这是一种特定类型的数组,它的元素限制为 8 位无符号整型,也就是值在 0-255 区间,如果赋值超过此区间,则按照最近的值处理,比如赋值 1000 会修改为 255,赋值-23 会修改为 0。如果使用Uint8Array类型,则只是读取前 8 位,-23 因为补码形式的存在,会被修改为 233。不过,当数据需要传递给服务端的时候,往往需要编码为占用空间更小的 Base64 字符串,后端进行解码并处理后,再编码为 Base64 返回。3.2 AssemblyScript大家知道,WebAssembly[1] 是一种紧凑的二进制程序格式,它不是一种高级语言,难以用人能表达清楚的方式直接编写,所以一般都会有专门的工具提供转译的手段。AssemblyScript 是一种类似 TypeScript,且对前端相对友好的编译语言,我们简单看一下它的代码:exportfunctionadd(a:i32,b:i32):i32{returna+b;}相比 TypeScript,看起来只是多了一个更细的类型定义。不过如果只是这么简单的场景,根本没必要使用 WebAssembly 绕一圈,我们完整的介绍一下流程。先写一个 AssemblyScript 函数//导出灰度函数,语法与TS高度类似,为了节省空间,我们直接修改入参并返回结果exportfunctiongray(data:Uint32Array):Uint32Array{const{length}=data;for(leti=0;i();使用官方工具编译为 WebAssembly 格式的文件ascfilter.ts--outFilefilter.wasm--optimize在 Web Browser 引入,使用官方封装好的工具实例化更方便一些import{instantiate}from'@assemblyscript/loader';//orrequireconstinstance=instantiate(fetch('yourwasm.wasm'),{/*importObject*/}).catch(console.error);使用的时候,要特别注意数据是需要通过指针的形式传入的,需要关注指针的生命周期//首先拿到wasm实例constwasm=awaitinstance.then();//通过上述的loader加载和编译设置,wasm.export提供了所有导出的函数和工具函数//__newArray用来生成wasm内部数组,__getArray用来将wasm//__pin是一个指针指向wasm内部数据,__unpin则是释放这个指针触发垃圾回收//Uint32Array_ID是上面导出的标识,gray是上文自定义的灰度函数const{__newArray,__getArray,__pin,__unpin,Uint32Array_ID,gray}=wasm.exports;//将js中的canvasData传入__newArray函数中,在wasm中生成对应的数组内容//再用__pin获取指向这个数据的指针constarrayPtr=__pin(__newArray(Uint32Array_ID,canvasData));//调用wasm生成的函数进行数据处理(传入上一步wasm指针),返回一个wasm内部IDconstresult=gray(arrayPtr);//通过__getArray将内部ID解析为JS数组constresultArray=__getArray(result);//最后需要释放指针引用,触发垃圾回收__unpin(arrayPtr);在之前的测试中,因为 JS 和 WebAssembly 的数据通信需要大量的拷贝,加上 AssemblyScript 对代码的优化不如 Rust 等编译器做的好,再加上 Chrome V8 TurboFan 强大的 JIT 优化能力,WebAssembly 的处理速度往往不一定能超过原生的 JavaScript,不过 AssemblyScript 仍然是前端同学开发 WebAssembly 最方便的途径之一,值得推荐。3.3 RustRust[2] 可以算是当今最“网红”的编程语言了,不过它确实配的上,Rust 通过把在运行时可能会遇到的问题提前到编译时进行,相当于用开发时间换取运行效率。Rust 可能是支持 WebAssembly 最好的语言了,不仅有非常完整体系的工具链,性能更好,服务端运行时 (WASI) 也提供相当完备的生态。我们这里只介绍一下 Rust 编译 WebAssembly 在前端的应用:安装 Rust 环境和 wasm-pack 包,初始化工程curlhttps://sh.rustup.rs-sSf|shcargoinstallwasm-packcargonew--libhello-wasm接着我们书写逻辑代码,比如一个可调用的函数参考如下,注意我们要包含 wasm 相关的工具链和 json 处理工具externcratewasm_bindgen;externcrateserde_json;usewasm_bindgen::prelude::*;#[macro_use]externcrateserde_derive;#[wasm_bindgen]pubfndesaturate(js_objects:&JsValue)->String{letelements:Vec=js_objects.into_serde().unwrap();letmuti=0;letdata_len=data.len();whileiWebAssembly 成熟的运行环境了图 19. FaaS 概念示意基于此服务,我们可以很轻松的做到:serverless,无需关注服务器的申请,部署,运维等等WebAssembly 天然自带冷启动快、资源隔离的优势部署在边缘,成本更低,网络消耗更低4. 更专业的工具 —— OpenCV上面讲到的是低层次的图像处理,其实我们可以基于一个成熟的库来尝试往更多方向进行探索。OpenCV 是计算机视觉(Computer vision)领域最受欢迎的开源库,它最初是由 intel 使用 C 和 C++进行开发的,如今已经演化为一个免费的、跨平台的、且高度优化过的库。借助 WebAssembly 特性,OpenCV 能很方便的提供能力给前端或者 Node.js 开发者。4.1 构建OpenCV 官方已经封装好了基于 Emscripten 的工具链[3],大概有下面这些方式:构建 asm.js 风格的产物emcmakepython./opencv/platforms/js/build_js.pybuild_jsasm.js 是一种带有特定类型声明的 JavaScript,在早期 WebAssembly 未诞生时作为一种内置的优化手段构建 wasm 产物emcmakepython./opencv/platforms/js/build_js.pybuild_wasm--build_wasm//往往需要配合loader扩展多线程、SIMD能力emcmakepython./opencv/platforms/js/build_js.pybuild_js--build_loader使用官方文件官方会提供 opencv.js 格式的文件[4][5],一般建议项目自己拷贝一份下来,避免海外网络拥塞导致系统不稳定。使用封装库[推荐]NPM 仓库有很多人封装过了 OpenCV,并添加了或多或少的辅助工具。我使用的是 opencv-ts[6],不仅不需要打包,还提供了 TypeScript 语法提示能力,使用起来是比较方便的。4.2 基本用法官方已经提供比较完整的说明文档,这里不详细介绍具体的 API 了,我们简单说一下用法。注意:所有的代码应该写在初始化函数onRuntimeInitialized中。cv 提供了imread(imageid | HTMLImageElement | HTMLCanvasElement) 的函数签名,这可以从指定的源读取图片内容,并返回一个cv.Mat类型的对象,这是一个基本的存储二维空间的数据结构——矩阵,这里用来存储图片的像素结构,后面很多操作都是基于Mat类型来操作的。输出图片也非常方便,直接调用cv.imshow(canvasid | HTMLCanvasElement, cv.Mat对象) 就可以把图像绘制在屏幕上。需要记住的是 WebAssembly 不会帮助你回收内存,所以记得 cv.Mat 对象用完后及时调用delete()方法释放内存。5. 写一个实时视频转化工具有没有想过可以用少量代码写一个实时转化视频的案例?图 20. 实时视频滤镜我们先创建两个 DOM 节点,分别放置视频和画布其实,视频本质上可以当做一帧又一帧的图像轮播,OpenCV 内置了对视频流的截取能力constcapture=newcv.VideoCapture(videoElement);constframeMat=newcv.Mat(videoElement.height,videoElement.width,cv.CV_8UC4);因此我们尽量保证在每一帧进行一次渲染计算即可constonPaint=()=>{//把图像信息绘制在矩阵中capture.read(frameMat);//调用绘制函数,下面会讨论实现render(frameMat);//触发下一次帧渲染requestAnimationFrame(onPaint);}onPaint();那么现在问题就到了如何基于 OpenCV 进行线条化滤镜的绘制了,以下面这张图为例图 21. 原图总体过程可以分为 5 步:5.1 灰度化OpenCV 自带了灰度处理函数,使用常量 cv.COLOR_RGBA2GRAY 表示constgrayMat=newcv.Mat();cv.cvtColor(frameMat,grayMat,cv.COLOR_RGBA2GRAY);图 22. 灰度化处理5.2 高斯模糊上面也提到了高斯模糊算法,需要一个像素半径作为指标即可,OpenCV 自带了高斯模糊方法constksize=newcv.Size(5,5);constblurMat=newcv.Mat();cv.GaussianBlur(grayMat,blurMat,ksize,0,0,cv.BORDER_CONSTANT);图 23. 模糊处理5.3 黑白化黑白化(专业名词叫二值化),即像素若大于某个值则为最大值,小于某个值即为最小值letthresholdMat=newcv.Mat();cv.adaptiveThreshold(blurMat,thresholdMat,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,cv.THRESH_BINARY,5,2);图 24. 黑白化处理接下来分别进行一次高斯模糊和黑白化,图 25. 第二次模糊处理图 26. 第二次黑白处理可以让线条轮廓更加明显一点,当然还有其他优化效果的算法,这里不赘述了,最后,千万记得释放内存。OpenCV 是一个非常强大的工具库,上面举得只是一个非常简单的例子。结合机器学习模型,可以把实时的人脸识别、车牌 OCR、美颜相机、闯红灯识别等直接放在前端来完成,不得不说 WebAssembly 为前端同学提供了一个很好融合其它学科精华的机会。6. 总结正如上面所说,WebAssembly 可以方便的整合成熟的工具库,让它们以高性能在浏览器环境上运行。图 27. WebAssembly 应用其实可以这样盲下结论,理论上可在浏览器上运行的,最终都可以在浏览器端运行。WebAssembly 另一个巨大的应用前景就是浏览器外,轻量级、高效率、天然隔离性让它在容器化、特别是在低端设备上有很大的发展潜力。然而,WebAssembly 还处于发展的初始阶段,很多能力都亟待进一步的提高。Web 端性能V8 的 TurboFan 提供了 JIT 能力,能把热点代码直接转化为机器码执行,能达到 C 语言效率级别,另一方面 WebAssembly 和 JavaScript 通信需要一定的上下文开销,单纯的浏览器场景下,如果 WebAssembly 效率不能稳定优于 JavaScript,那其实大部分业务场景下没有绝对的必要。开发不友好Emscripten 和 WASM-PACK 可以说是里程碑式的工具,它们让 WebAssembly 走进前端变成了可能,不过像周边的生态,特别是调试、断点、部署等还是比较麻烦的。浏览器外可靠的虚拟机WebAssembly 需要一个可靠的运行时才能在浏览器外稳定运行,目前像字节码联盟或者一些边缘云公司 (fastly) 会提供一些运行时,这些运行时各有自身适合的场景,区别于 Docker 几乎成为容器的事实标准,百花齐放恰恰也说明了 WebAssembly 的生态还有很长的路要探索。7. 参考文献[1]. AssemblyScript: https://www.assemblyscript.org/introduction.html[2]. Rust: https://rustwasm.github.io/[2]. Build OpenCV.js: https://docs.opencv.org/4.x/d4/da1/tutorial_js_setup.html[3]. Using OpenCV.js: https://docs.opencv.org/4.6.0/d0/d84/tutorial_js_usage.html[4]. opencv.js: https://docs.opencv.org/4.6.0/opencv.js[5]. opencv-ts: https://www.npmjs.com/package/opencv-ts点击上方关注 · 技术干货不迷路点击左下方“阅读原文”,或扫描上方二维码,进入专栏阅读《走进 WebAssembly 的世界》完整版。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-15 21:04 , Processed in 0.615568 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表