墨奇科技博客 | 计算机视觉在前端应用中的实践Ⅰ_Moqi_AI的博客-程序员ITS301_计算机视觉前端

技术标签: c++  计算机视觉  缓存  opencv  

墨奇科技在业务中有很多应用需要在网页中对于图像进行处理、特征识别,让我们对于计算机视觉在前端应用中的实践有了一些自己的认识和了解,希望能够借此机会与大家进行分享,尤其是如何搭建基于 WebAssembly 的技术方案。

一、站在巨人的肩膀上

如何使用 OpenCV.js 进行图像处理

OpenCV 作为计算机视觉领域最为成熟的开源库,在实际业务中经常被运用于进行图像和视频处理。OpenCV 使用 C/C++ 进行编写,并且提供了 Python、Java、C#、Go、Javascript 等接口。在前端领域,当遇到需要在浏览器端进行图像处理时,使用 OpenCV.js 可以在多数业务场景中满足产品需求,提高开发速度,调高运行效率。

1.OpenCV.js 是什么?


OpenCV.js 是将 C++ 版的 OpenCV 通过 Emscripten 编译为 WebAssembly 版本的 OpenCV.js 库。常年来 Web 都只能运行 Javascript,而 Javascript 的语言特性从而决定了它在执行一些类似于图像处理等计算密集型操作时性能较差,即使在 V8 引擎的加持下仍然会有性能瓶颈。WebAssembly 赋予了 Web 端运行 C++、Rust 等语言编写的代码的能力,实现多数情况下更高效的计算性能,达到接近于 Native 的计算水平

图片来源互联网


2.如何引入 OpenCV.js 到运行环境


如果希望在前端 Javascript 代码中使用 OpenCV.js 需要经过以下几个步骤:

  • 第一步:获得 OpenCV.js 文件。参考官方教程,我们可以直接获得一个预编译的版本,或者当我们需要进行一些编译的配置时,也可以自己进行编译。如果自行编译的推荐可以使用 docker 的方式,方便快捷稳定。

  • 第二步:在代码中引入 OpenCV.js 文件。当我们获得了文件后,可以通过 script 标签进行引入。一个 OpenCV.js 标准库的大小在 10MB 左右,所以建议使用 async 属性让文件异步载入。

  • 第三步:确保 OpenCV.js 已经载入到内存中。使用 async 进行异步载入时,可以在标签的 onLoad 回调函数中执行后续操作,此时 OpenCV.js 文件已经下载完毕。虽然此时文件已经下载完毕,但是相关的函数和变量可能还没有完全加载到内存中,所以建议书写一个 cvFuncLoaded 函数用于判断运行环境中 OpenCV 相关的函数是否都已载入到内存中。


3.引入额外的 OpenCV 库


OpenCV 提供的预编译的版本只有标准库的内容,当我们希望使用 opencv_contrib 中的扩展模块内容时,就需要我们将我们所需要的扩展模块内容添加到编译选项中,具体可以参考官方提供的教程。


4.优化包体积


OpenCV 提供的预编译的版本或者默认编译下的 OpenCV.js 文件通常都比较大,通常需要 10MB 左右,如果引入了额外的 opencv_contrib 中的内容的话,整体体积会更大。这对于前端项目来说是难以接受的,每个用户在第一次打开该页面时需要加载 10MB 以上的资源,将会耗费较长时间,所以我们需要对于包体积进行优化。 

OpenCV 包含了非常丰富的各种图像处理的工具,但是在我们实际中可能并不会用到其中大部分的内容,所以可以在编译时对于需要编译的内容进行裁剪,只编译实际业务中所需要用到的部分。

以我们实际中的业务场景为例,主要用到的是标准库的一些图像旋转、缩放、透视变换以及扩展模块中的 Aruco 相关部分,通过不断裁剪将编译文件体积减小了近 90%,极大地缩短了 OpenCV.js 的载入时间。

二、自己动手,丰衣足食

搭建基于 WebAssembly 的技术方案

基于 OpenCV.js 的方案便于上手。作为成熟的开源类库,OpenCV 使得从 C++ -> WebAssembly 的开发、编译流程非常顺滑,而大量成熟的 CV 算子可供直接调用,能够覆盖多数应用场景。然而在实际使用过程中,这套方案存在 2 个不足:

  • OpenCV 的体积较大。适当裁剪 modules 可以优化,但在简单的场景中,module 的颗粒度依然过大。例如,在一个仅需要做图像风格转换的业务中,我们也许只需要维护一个数组,不需要完整的 cv::Mat 作为数据结构,很多内部逻辑显得冗余。又例如,如果业务只需要处理 uint8 类型的灰度图,裁剪掉 3 & 4 通道以及浮点数输入的逻辑是比较困难的。

  • 基于 OpenCV 的定制化开发并不少见。但对于不熟悉 CV 的前端工程师,在相对复杂的 OpenCV 实现中找到对应逻辑,修改、重新编译并测试的成本较高。尤其是出于通用性及性能优化,OpenCV 相当多的实现基于模板元编程,并且与硬件架构相关。在前端场景下,考虑这两点的收益并不大。

1.技术方案的选择

因而,我们认为在上下文明确、功能单一的业务场景中,一套轻量级的 CV 方案是必要的。前端在处理如 CV 的计算密集型任务时,有以下 3 个较主流的方案:

  • WebAssembly

  • WebGL

  • WebGPU

在选择技术方案时,我们有 3 个考虑方向:

  • 业务场景:我们处理的是哪类任务?兼容性要求是什么?

  • 性能指标:我们希望于达成什么级别的计算性能,相比于现有方案带来多大提升?

  • 开发体验:工程师完成技术实现的成本如何?

(1)业务场景

我们的业务场景面向企业用户,以 PC 端为主。CV 实现的功能类型单一,以风格转换、图像增强为主。系统需要兼容的最低操作系统为 Windows XP,对应的浏览器版本也较低(e. g. Chrome 49 / Firefox 52)。这意味着我们不可能完全依赖一套非 JS 方案,而需要做降级兼容,并且尽量选择覆盖率高,且在各版本浏览器中表现稳定的技术。具体地看:

  • WebAssembly 无法支持 Chrome 57 以下的版本,但可以覆盖所有 Firefox 52 以上版本。

  • WebGL 的硬件加速无法在 Windows XP 上开启,且我们实测即使对 Windows 7 系统,Chrome 60 以下版本中的表现也并不稳定。

  • WebGPU 标准还不成熟,落地风险大。

(2)性能指标

现有业务系统有一套基础的 JS 实现,该实现存在的问题是,对大尺寸图像,高计算复杂度的图像操作可能需要秒级的响应时间,用户等待时间过长。我们第一次上线前的目标如下:

  • 技术层面,首先过滤出所有高复杂度的计算函数,对业务系统中可能出现的最大分辨率图像,经过 Wasm 加速的此类 CV 函数,处理时延能控制在 350ms 以下。

  • 用户实际感知上,所有的操作都应是准实时的。

 
(3)开发体验

团队中有 C++ 经验的工程师不多,搭建、维护 C++ 项目的难度大。

综上,权衡之后,我们决定以 Rust 作为开发语言,并搭建一套 JS + WebAssembly 的复合方案。
 

2.技术方案的两个维度

我们将技术方案拆分为 2 个维度:开发流水线与运行时模型。



(1)开发流水线

开发流水线可参考下图:

计算函数的实现:Rust

我们常常听说 Rust 是一门特性比较独特(e. g. ownership)的编程语言,存在较高上手成本。但事实上,Rust 具有非常优质的官方教程且设计体系有理可寻(e. g.  Affine type system  vs ownership / move)。

计算类函数的实现难点,大多是在算法的理解,而不依赖对高级语言特性的使用。因而,在较短的适应期后,对于单个计算函数,从理解算法、使用 Rust 实现,到完成基本的正确性和性能测试,我们的前端工程师能将开发周期控制在 5-6 小时之内(取决于算法自身的复杂度),在项目推进中,并没有成为时间瓶颈。

相比于 C++Rust 额外的优势还在于其较现代的包管理体系及对内存安全的重视

  • 对于习惯了基于 Yarn / npm 进行依赖管理的前端工程师而言,使用 Cargo 远比使用 CMake 手动引入头文件路径及链接依赖库更易上手。不需要手动管理内存或学习智能指针也降低了心理负担。

  • 对于习惯了编写如 JavaScript 的动态类型语言的工程师而言,Rust 严格的编译期类型检查最初会让人不习惯,但这确实规避了不少运行期错误。例如,在计算操作中,整型、浮点数之间的转换(例如双线性 resize 中将像素点位置代入计算,获得浮点系数),大量乘加操作造成的溢出(例如 uint8 的卷积)都可能对精度产生影响,Rust 能显式地让我们意识到这些问题。

Fallback 实现:  JavaScript

由于 WebAssembly 无法覆盖所有目标浏览器,对每一个计算函数,我们都会实现一个对应的 JS 版本作为 fallback。



从 Rust -> WebAssembly: 编译与打包

从 C++ 到 WebAssembly,Emscripten 确实是一套较为成熟的工具,但使用中仍需要工程师对 C++ 编译、链接流程有所了解,编写 CMakeLists.txt 时需要额外注意不少针对 WebAssembly 的选项(e. g.  TOTAL_STACK  / INITIAL_MEMORY),细节较多。

相比之下,Rust 官方提供的工具 wasm-pack 使整个过程更为透明:在编写时,为需要暴露给 JS 调用的函数、类等添加 wasm-bindgen 的 attribute,按官方示例配置好 cargo.toml(optimization level 与 wee_alloc 是 2 个对 WebAssembly 而言比较重要的设置),只需执行简单的命令 wasm pack build,在绝大多数情况下,即可得到 wasm 产物。

值得一提的是,wasm-pack 事实上更像是一个工具集的抽象。例如,其通过 wasm-bindgen 完成对 attribute 的解析、语法树的 transform,并通过 wasm-opt 完成对 wasm 生成代码的后优化。



JavaScript / TypeScript 侧调用

Wasm-pack 生成的是一对 js / wasm 产物。使用时,只需在业务组件中引入 js 文件,获得实例,并通过实例调用函数即可,与一般的 JS 函数调用并无二致。具体设计可见下章。

(2)运行时模型

我们将计算函数想象成一个黑盒,其接受一个输入、返回一个输出。运行时模型,指这个盒子的初始化及内部处理逻辑。

初始化检查服务

尽管相比于 WebGL 与 WebGPU,WebAssembly 对于我们的用户群体覆盖率更好,但依然无法完成对所有目标浏览器的兼容。初始化检查服务所做的,便是判断当前浏览器环境,决定 WebAssembly 的可用性。



惰性加载

在我们的前端系统中,WebAssembly Module 只有在当前页面需要使用,且检查服务通过时才会被加载。



调用分发器

基于检查服务的结果,我们将调用分发器决定调用 WebAssembly 或是 JS 函数。



WebAssembly 实例

我们使用一个实例对象作为所用 WebAssembly 函数的 callee。如此,WebAssembly 相关的逻辑可以被较好地隔离开。



内存管理

维护 JS 中对象到 WebAssembly 的内存空间是值得额外注意的。我们可以选择每次返回一个新的 buffer 并重新构建 TypedArray,或是直接采用拷贝。仅维护一个映射到 WebAssembly 内存地址的 buffer 是不安全的:在诸如 grow memory 这样的操作后,旧有区间可能被整体复制,那么原先的地址及对应的 buffer 也就失效了。
 

3.上线效果

覆盖率

目前,所有业务系统中的计算函数均实现了 WebAssembly + JS 的方案。

性能

  • 对于 profiling 后过滤出的高耗时计算函数(如:锐化、颜色混合、自定义均衡等)在测试浏览器环境(Chrome 57 / Chrome 60 / Chrome 70 / Chrome 79 / Firefox 52 / Firefox 72)中,平均性能达到了 4-5 倍的提升

    对于个别浏览器环境中的个别函数,如 Firefox 72 中的颜色混合,提升大于 20 倍。对于长宽均超过 2000 像素的大尺寸图像,此类函数在大多数测试环境中,均可在 350ms 内完成计算。

  • 对于低耗时计算函数(如:亮度、对比度、反相),测试浏览器环境中的性能提升也基本达到了 4 倍。在 Chrome 79 / Firefox 72 中,对于长宽均超过 2000 像素的大尺寸图像,此类操作的时延能控制在 ~10ms。


下图即为某次 release 前,Chrome 79 上各 CV 函数的 JS/WASM 时延指标对比。处理的输入图大小为 2040x2040:

反思

当然,性能优化时并不乏“意外”。我们不得不承认,受制于开发初期的不确定性,最初的指标制定、profiling 规则都不够细致。我们也确实对 Rust 的实现进行了额外的优化以达到预期的指标。同时,从结果上看,确实存在部分函数,在一些低版本的测试环境中,提升效果不尽如人意的情况。技术层面,在高版本的浏览器中,没有 web worker 和 SIMD 加持的 WebAssembly 相比于 WebGL 也未有性能优势。性能优化中的经验、反思、迭代,我们不在此篇文章中做深入展开。
 

总结

本文第一部分主要介绍了如何引入 OpenCV.js 到运行环境,讲解了在前端应用中如何使用现有的成熟 OpenCV 库,以及如何优化引入的包体积。第二部分主要介绍了在我们需要自己实现一个计算机视觉库的时候,如何搭建基于 WebAssembly 的技术方案,以及我们在实际业务场景中带来的性能提升。本系列的下一篇文章是《计算机视觉在前端应用中的实践(二) — 性能优化》,敬请期待。

墨奇科技有良好的工作环境和各种福利,让你能够充分的发挥自己的技术,不断挑战自我。墨奇全栈组在持续热招前端、后端、Android、C++、嵌入式等开发岗位↓

点我直接进入内推通道

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Moqi_AI/article/details/123898816

智能推荐

Codec_kong-kong的博客-程序员ITS301

https://github.com/redisson/redisson/wiki/2.-Configuration#Redis data codec. Used during read and write Redis data. Several implementations are available:Codec class name Description org.redi...

修改Content Server管理员密码 - [Documentum 实施开发记录]_anhai6033的博客-程序员ITS301

ag:版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明http://fanlb.blogbus.com/logs/59357766.html修改documentum content server 管理员用户dmadmin的密码 (1qaz2wsx)1. 停止服务 Documentum Java Method Server2. 停止服务 Documentu...

Autosar AP – ARA_小火球2.0的博客-程序员ITS301

AP AUTOSAR的ARA(Runtime For Adaptive Application)由一系列的Function Clusters(FCs)组成。每个FC都有他们各自的功能,Foundation与Service部分都有各自包含的FC,软件平台概览如下图所示■ Foundation部分中包含的FC及其主要描述如下表所示: Foundation FCs 缩写 描述 Operating System Interfa

springboot整合RabbitMQ_hayhead的博客-程序员ITS301

RabbitMQ的安装安装好RabbitMQ和Erlang后,在浏览器中输入:localhost:15672,进入rabbitmq的登陆界面, 登陆账号密码都是guest创建交换机创建队列消息推送、接收的流程交换机的四种类型Direct      直连型交换机,根据消息携带的路由键将消息投递给对应队列(点对点式)Fanout      扇型交换机,这个交换机没有路

Linux 2.6.10内核下PCI Express Native热插拔框架的实现机制_查尔斯.褚的博客-程序员ITS301

Linux 2.6.10内核下PCI Express Native热插拔框架的实现机制[日期:2008-7-22]来源:IBM  作者:王兵 国防科学技术大学计算机学院软件所[字体:大 中小]  PCI热插拔技术,可以有效避免由更换外设引起的服务器系统停机,对于提高服务器系统可用性和可扩展性意义重大。本文讨论了PCI Express热插拔所涉

随便推点

系统的硬件组成_Gaodes的博客-程序员ITS301

典型的系统硬件组成1.总线:贯穿系统的是一组电子管道,称为总线,它是携带信息字节并且负责在各个部件间传递。通常总线被设计成传送定长的字节块,也就是字。字中的字节数是一个基本的系统参数,各个系统中都不尽相同。现在大多数机器字长要嘛是4个字节(32位),要嘛是8个字节(64位)。2.I/O(输入/输出)设备是系统与外部世界的联系通道。主要是作为用户输入的键盘和鼠标,作用用户输出的显示器,长期存储数据和程序的磁盘。3.主存:主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序。4.处理器

【面试必备】Redis最全面试题_csdn大数据的博客-程序员ITS301

出自:https://github.com/CyC2018/CS-Notes程序员乔戈里整理01概述Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存...

base64 InputStream互转_世界这么大我想去看看的博客-程序员ITS301_base64图片转inputstream

1、InputStream转base64public static String getBase64FromInputStream(InputStream in) { // 将图片文件转化为字节数组字符串,并对其进行Base64编码处理 byte[] data = null; // 读取图片字节数组 try { ByteArrayOutputStream swapStream = new ByteArrayOutputStream(); ...

QT应用编程: 界面自适应屏幕分辨率_DS小龙哥的博客-程序员ITS301

一、环境介绍操作系统:win10 64位QT版本:QT5.12.6编译器:MinGW 32二、实现代码每次程序打开之后,根据当前屏幕分辩率进行计算缩放系数,然后设置界面上的控件尺寸,不管有没有使用布局器都可以设置。/**************************************************作者: DS小龙哥环境: win10 QT5.12.6 VS2017 32位 Release功能: 自适应工具栏按钮大小****************...

面试必备算法|图解插入排序(Python)_二哥不像程序员的博客-程序员ITS301

插入排序插入排序的思想​ 插入排序通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。图解插入排序插入排序的性质最优时间复杂度:O(n)O(n)O(n) (升序排列,序列已经处于升序状态)最坏时间复杂度:O(n2)O(n^2)O(n2)稳定性:稳定插入排序的代码实现lst = list(map(int, input().split(',')))d

WEB自动化测试总结篇_闯的博客-程序员ITS301_web自动化测试value值怎么看

一、初识WEB-selenium自动化测试针对bing网站的搜索功能进行自动化测试# 从谷歌公司的一个项目selenium导入webdriver这段代码来驱动浏览器chrome=webdriver.Chrome()# 2、打开bing网站chrome.get('http://cn.bing.com/')# 3、输入关键词chrome.find_element_by_id('sb_form_q').send_keys('51tesing')# 4、点击搜索按钮chrome.find..

推荐文章

热门文章

相关标签