专栏首页IMWeb前端团队C++ 编写 WebAssembly初探(二)

C++ 编写 WebAssembly初探(二)

本文作者:IMWeb llunnn 原文出处:IMWeb社区 未经同意,禁止转载

上一篇(环境搭建,简单接入):C++编写WebAssembly初探

这一次,我们尝试使用WebAssembly来做简单的图片处理。

我们选取一种最基本的图像处理——高斯模糊来尝试实现。原理可参考高斯模糊和卷积滤波简介

js向wasm传递数组

与传递number不同,传递数组时,需要js将数组拷贝到wasm内存中,并通过传递指针(数据在内存中的位置),让wasm通过访问内存的具体位置,来获取或修改数组。

另外,不同于js,wasm的内存管理由开发者进行控制,我们需要手动分配和释放内存。

这里的过程是,首先我们获得表示图片像素的数组,将这个数组复制到wasm内存,再调用wasm模块处理这些像素数据,处理完后js重新读取这块内存,并将处理过的图片画到canvas上。

// 被处理的图片
const srcImg =  document.getElementById('srcImg');
srcImg.onload = () => {
  // onload时将图片画到canvas上,以获得像素数据
  const { clientWidth = 0, clientHeight = 0 } = srcImg;

  var canvas = document.getElementById("drawerCanvas");
  canvas.width = clientWidth;
  canvas.height = clientHeight;
  var ctx = canvas.getContext("2d");
  ctx.drawImage(srcImg, 0, 0, clientWidth, clientHeight);

  // 获得像素数据
  const imageData = ctx.getImageData(0, 0, clientWidth, clientHeight);

  // 处理数据
  const resImageData = wasmProcess(imageData, clientWidth, clientHeight);

  // 将处理后的图片数据画到canvas上
  ctx.putImageData(resImageData, 0, 0);
}
// 将js的typedarray复制到wasm的内存
function copyToHeap(typedArray) {
  const numBytes = typedArray.byteLength;
  const ptr = Module._malloc(numBytes);
  const heapBytes = new Uint8Array(Module.HEAPU8.buffer, ptr, numBytes);
  heapBytes.set(new Uint8Array(typedArray.buffer));
  return heapBytes;
}
// 释放一块wasm内存
function freeHeap(heapBytes) {
  Module._free(heapBytes.byteOffset);
}
// 图片处理的函数
function wasmProcess(imgData, width, height) {
  const heapBytes = copyToHeap(imgData.data);
  // 调用c++暴露的方法。其中heapBytes.byteoffset传递的是wasm内存中数组的指针
  Module.ccall(
    'easyBlur',
    'number',
    ['number', 'number', 'number', 'number', 'number'],
    [heapBytes.byteOffset, width, height, 3, 3]
  );
  // 从wasm内存读取出处理后的数据
  const newData = new Uint8ClampedArray(heapBytes);
  // 释放wasm内存
  freeHeap(heapBytes);
  const newImageData = new ImageData(newData, width, height);
  return newImageData;
}

简单的高斯模糊算法实现

这里取最简单的滤波器,即矩阵所有项都相等的滤波器。要使得滤波器的各项和为1,则每一项的值为1 / (cw*ch).

如一个3*3的滤波器为 [0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11].我们可以简单地通过改变cw和ch来调整模糊的强度,cw和ch越大,扩散程度越大,则模糊强度也越大。

另外我们需要观察ctx.getImageData()得到的数组格式:获得的data是一个一维数组,按照从从左到右,从上到下的顺序记录了图片每个像素的值。其中每4个值为一组,分别代表同一个像素的r, g, b, a四个通道的数值。我们模糊时对每个通道进行单独处理。

我的代码:

#include <cstdint>
#include <cmath>

// 卷积操作,传入imageData像素数组的指针,imageData宽高,滤波器及滤波器宽高。
void conv(uint8_t *ptr, int width, int height, float* filter, int cw, int ch) {
  for (int i = ch / 2; i < height - ceil((float)ch / 2) + 1; i++) {
    for (int j = cw / 2; j < width - ceil((float)cw / 2) + 1; j++) {
      // rgba取前3个通道进行处理
      for (int k = 0; k < 3; k++) {
        float sum = 0;
        int count = 0;
        for (int x = -ch / 2; x < ceil((float)ch / 2); x++) {
          for (int y = -cw / 2; y < ceil((float)cw / 2); y++) {
            sum += filter[count] * (float)ptr[((i+x)*width+(y+j))*4+k];
            count++;
          }
        }
        ptr[(i*width+j)*4+k] = (uint8_t)sum;
      }
    }
  }
}

#ifdef __cplusplus
extern "C"
{
#endif

  // 供js调用的函数,传入像素数组的指针,宽高,以及滤波器的宽高
  // 这里为了简单,默认滤波器矩阵每一项的值相同,即1/(cw*ch)。
  void easyBlur(uint8_t *ptr, int width, int height, int cw = 3, int ch = 3) {
    float* filter = new float[cw * ch];
    float value = 1 / (float)(cw * ch);
    for (int i = 0; i < cw * ch; i++) {
      filter[i] = value;
    }
    conv(ptr, width, height, filter, cw, ch);
    delete [] filter;
  }

#ifdef __cplusplus
}
#endif

效果预览

对于宽度200px左右的图片,使用长宽为5的滤波器效果如下:

瓶颈

使用js以相同的方法重新实现了一次,发现在图片较小时js处理的耗时更短,而图片较大时wasm虽然速度快于js,但处理的时间也非常长,是不能忍受的。

问题的原因很可能是:

  • js调用C时有一定的执行代价
  • 将数据在js内存和wasm内存之间复制消耗大量的时间,影响性能。 所以这种数据量非常大的场景下,wasm虽然优化了计算的时间,但因为传递的的时间大大增加,反而成为了性能的瓶颈。

另外,对于前端来说,自己实现相关的处理算法,性能远不如线上一些库优化得好。这里有更多前端可用的图片处理库可以参考。

Ref

Emscripten: Pointers and Pointers

ArrayBuffer - ECMAScript 6入门

[译]WebAssembly 中的Memory

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • react组件性能优化探索实践

    React本身就非常关注性能,其提供的虚拟DOM搭配上Diff算法,实现对DOM操作最小粒度的改变也是非常的高效。然而其组件渲染机制,也决定了在对组件进行更新时...

    IMWeb前端团队
  • 通过ffi在node.js中调用动态链接库(.so/.dll文件)

    ? 概述 为什么要在node.js中调用动态链接库 由于腾讯体系下的许多公共的后台服务(L5, CKV, msgQ等)已经有了非常成熟的C/C++编写的API...

    IMWeb前端团队
  • react组件性能优化探索实践

    React本身就非常关注性能,其提供的虚拟DOM搭配上Diff算法,实现对DOM操作最小粒度的改变也是非常的高效。然而其组件渲染机制,也决定了在对组件进行更新时...

    IMWeb前端团队
  • 表达式求值问题

       最近在学习表达式求值问题,想使用C++或C语言实现一个带圆括号的十进制正整数的表达式求值控制台程序。这个问题可以通过栈或者二叉树遍历来解决。记得以前在学...

    ccf19881030
  • Android Service重启恢复(Service进程重启)原理解析

    Android系统中,APP进程被杀后,等一会经常发现进程又起来了,这个现象同APP中Service的使用有很大关系,本文指的Service是通过startSe...

    看书的小蜗牛
  • 10:判决素数个数

    10:判决素数个数 查看 提交 统计 提问 总时间限制: 1000ms 内存限制: 65536kB描述 输入两个整数X和Y,输出两者之间的素数个数(包括X和Y...

    attack
  • 1.5编程基础之循环控制44:第n小的质数

    #include<iostream> #include<cmath> using namespace std; int main() { int n; ci...

    attack
  • Android 自定义流布局。使用开源库SimpleFlowLayout

    实际项目中需要实现一个 热门搜索 的栏目,类似下图: 由于 子项(子view) 中的文字是可变的,一行能显示的 子项 的个数也无法确定。需要支持自动换行和计算...

    zhangyunfeiVir
  • CodeForces 242C King's Path(bfs+map)

          题意就是走地图,输入起点终点,然后输入某行的某些点可以走,然后问你最后能不能走到终点,能的话输出最少步数。仔细读一下题会发现地图的范围太大了,用二维...

    Ch_Zaqdt
  • PTA 甲级 1139

    https://pintia.cn/problem-sets/994805342720868352/problems/994805344776077312

    ShenduCC

扫码关注云+社区

领取腾讯云代金券