使用Web Workers改善图像处理性能

今天,我想谈谈使用纯JavaScript的HTML5中的图片处理。

测试用例

测试应用程序很简单。 左侧是要处理的图片,右侧是更新的结果(已应用棕褐色调效果):

图1

该页面本身很简单,描述如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>PictureWorker</title>

    <link href="default.css" rel="stylesheet" />
</head>
<body id="root">
    <div id="sourceDiv">
        <img id="source" src="mop.jpg" />
    </div>
    <div id="targetDiv">
        <canvas id="target"></canvas>
    </div>
    <div id="log"></div>
</body>
</html>

应用棕褐色效果的整个过程需要您为现有源图片的每个像素计算一个新的RGB值,然后将其呈现在<canvas>标记上,并带有id =“ target”。 以下是用于根据像素的现有RGB值创建新RGB值的公式:

finalRed =(红色* 0.393)+(绿色* 0.769)+(蓝色* 0.189);
finalGreen =(红色* 0.349)+(绿色* 0.686)+(蓝色* 0.168);
finalBlue =(红色* 0.272)+(绿色* 0.534)+(蓝色* 0.131);

为了使其更加真实,我在棕褐色配方中添加了一些随机性。 我创建了一个范围从0.5到1的噪声值,该值确定我的最终像素输出与通过上述公式计算出的RGB值匹配的程度,以及其保留原始RGB值的程度。

function noise() {
    //Returns a value between 0.5 and 1
    return Math.random() * 0.5 + 0.5;
};

function colorDistance(scale, dest, src) {
    // returns a red, blue or green value for the 'sepia' pixel
    // which is a weighted average of the original value and the calculated value
    return (scale * dest + (1 - scale) * src);
};

var processSepia = function (pixel) {
    // takes a given pixel and updates its red, blue and green values
    // using a randomly weighted average of the initial and calculated red/blue/green values
    pixel.r = colorDistance(noise(), (pixel.r * 0.393) + (pixel.g * 0.769) + (pixel.b * 0.189), pixel.r);
    pixel.g = colorDistance(noise(), (pixel.r * 0.349) + (pixel.g * 0.686) + (pixel.b * 0.168), pixel.g);
    pixel.b = colorDistance(noise(), (pixel.r * 0.272) + (pixel.g * 0.534) + (pixel.b * 0.131), pixel.b);
};

蛮力

显然,第一个解决方案是使用蛮力,其功能是将先前的代码应用于每个像素。 要访问像素,可以将canvas上下文与以下代码结合使用,该代码创建指向源img和目标canvas的指针:

var source = document.getElementById("source");

    source.onload = function () {
        var canvas = document.getElementById("target");
        canvas.width = source.clientWidth;
        canvas.height = source.clientHeight;

  // ... tempContext is the 2D context of canvas
        tempContext.drawImage(source, 0, 0, canvas.width, canvas.height);

        var canvasData = tempContext.getImageData(0, 0, canvas.width, canvas.height);
        var binaryData = canvasData.data;
    }

此时,binaryData对象包含每个像素的数组,可用于快速将数据直接读取或写入画布。 考虑到这一点,我们可以使用以下代码来应用整个效果:

var source = document.getElementById("source");

    source.onload = function () {
        var start = new Date();

        var canvas = document.getElementById("target");
        canvas.width = source.clientWidth;
        canvas.height = source.clientHeight;

        if (!canvas.getContext) {
            log.innerText = "Canvas not supported. Please install a HTML5 compatible browser.";
            return;
        }

        var tempContext = canvas.getContext("2d");
        // len is the number of items in the binaryData array
        // it is 4 times the number of pixels in the canvas object
        var len = canvas.width * canvas.height * 4;

        tempContext.drawImage(source, 0, 0, canvas.width, canvas.height);

        var canvasData = tempContext.getImageData(0, 0, canvas.width, canvas.height);
        var binaryData = canvasData.data;

        // processSepia is a variation of the previous version. See below
        processSepia(binaryData, len);

        tempContext.putImageData(canvasData, 0, 0);
        var diff = new Date() - start;
        log.innerText = "Process done in " + diff + " ms (no web workers)";

     }

processSepia函数只是前一个函数的变体:

var processSepia = function (binaryData, l) {
    for (var i = 0; i < l; i += 4) {
        var r = binaryData[i];
        var g = binaryData[i + 1];
        var b = binaryData[i + 2];

        binaryData[i] = colorDistance(noise(), (r * 0.393) + (g * 0.769) + (b * 0.189), r);
        binaryData[i + 1] = colorDistance(noise(), (r * 0.349) + (g * 0.686) + (b * 0.168), g);
        binaryData[i + 2] = colorDistance(noise(), (r * 0.272) + (g * 0.534) + (b * 0.131), b);
    }
};

使用此解决方案,在我的Intel Extreme处理器(12核)上,主进程耗时150ms,并且显然仅使用一个处理器:

图2

输入网络工作者

处理SIMD(单指令多数据)时,最好的方法是使用并行化方法,尤其是当您要使用资源有限的低端硬件(例如电话设备)时。

在JavaScript中,要享受并行化的强大功能,您必须使用Web Workers。 我的朋友戴维·罗塞特(David Rousset) 在这个问题上写了一篇出色的论文。

图片处理非常适合并行化,因为(如棕褐色调功能)每个处理都是独立的。 因此,可以采用以下方法:

图3

为此,首先必须创建一个tools.js文件,以供其他脚本用作参考。

// add the below functions to tools.js
function noise() {
    return Math.random() * 0.5 + 0.5;
};

function colorDistance(scale, dest, src) {
    return (scale * dest + (1 - scale) * src);
};

var processSepia = function (binaryData, l) {
    for (var i = 0; i < l; i += 4) {
        var r = binaryData[i];
        var g = binaryData[i + 1];
        var b = binaryData[i + 2];

        binaryData[i] = colorDistance(noise(), (r * 0.393) + (g * 0.769) + (b * 0.189), r);
        binaryData[i + 1] = colorDistance(noise(), (r * 0.349) + (g * 0.686) + (b * 0.168), g);
        binaryData[i + 2] = colorDistance(noise(), (r * 0.272) + (g * 0.534) + (b * 0.131), b);
    }
};

该脚本的要点是,画布数据的一部分(即当前块要处理的部分)由JavaScript克隆并传递给工作程序。 工作人员不在初始源上工作,而是在其副本上工作(使用结构化克隆算法 )。 副本本身确实很快,并且仅限于图片的特定部分。

主客户端页面(default.js)必须创建四个工作程序,并为他们提供图片的正确部分。 然后,每个工作人员都将使用消息传递API( postMessage / onmessage )在主线程中回调一个函数,以返回结果:

var source = document.getElementById("source");

source.onload = function () {

    // We use var start at the beginning of the code and stop at the end to measure turnaround time

    var start = new Date();

    var canvas = document.getElementById("target");
    canvas.width = source.clientWidth;
    canvas.height = source.clientHeight;

    // Testing canvas support
    if (!canvas.getContext) {
        log.innerText = "Canvas not supported. Please install a HTML5 compatible browser.";
        return;
    }

    var tempContext = canvas.getContext("2d");
    var len = canvas.width * canvas.height * 4;

    // Drawing the source image into the target canvas
    tempContext.drawImage(source, 0, 0, canvas.width, canvas.height);

    // If workers are not supported
    // Perform all calculations in current thread as usual
    if (!window.Worker) {
        // Getting all the canvas data
        var canvasData = tempContext.getImageData(0, 0, canvas.width, canvas.height);
        var binaryData = canvasData.data;

        // Processing all the pixel with the main thread
        processSepia(binaryData, len);

        // Copying back canvas data to canvas
        tempContext.putImageData(canvasData, 0, 0);

        var diff = new Date() - start;
        log.innerText = "Process done in " + diff + " ms (no web workers)";

        return;
    }

    // Let say we want to use 4 workers
    // We will break up the image into 4 pieces as shown above, one for each web-worker
    var workersCount = 4;
    var finished = 0;
    var segmentLength = len / workersCount; // This is the length of array sent to the worker
    var blockSize = canvas.height / workersCount; // Height of the picture chunck for every worker

    // Function called when a job is finished
    var onWorkEnded = function (e) {
        // Data is retrieved using a memory clone operation
        var canvasData = e.data.result; 
        var index = e.data.index;

        // Copying back canvas data to canvas
        // If the first webworker  (index 0) returns data, apply it at pixel (0, 0) onwards
        // If the second webworker  (index 1) returns data, apply it at pixel (0, canvas.height/4) onwards, and so on
        tempContext.putImageData(canvasData, 0, blockSize * index);

        finished++;

        if (finished == workersCount) {
            var diff = new Date() - start;
            log.innerText = "Process done in " + diff + " ms";
        }
    };

    // Launching every worker
    for (var index = 0; index < workersCount; index++) {
        var worker = new Worker("pictureProcessor.js");
        worker.onmessage = onWorkEnded;

        // Getting the picture
        var canvasData = tempContext.getImageData(0, blockSize * index, canvas.width, blockSize);

        // Sending canvas data to the worker using a copy memory operation
        worker.postMessage({ data: canvasData, index: index, length: segmentLength });
    }
};

source.src = "mop.jpg";

使用此技术,整个过程在我的计算机上仅持续80毫秒(从150毫秒开始),并且显然使用了四个处理器:

图4

在我的低端硬件(基于双核系统)上,处理时间从500ms减少到500ms。

可以在此处下载最终代码,并在此处发布一个工作示例。 为了进行比较,这是不带web worker的相同代码。

需要注意的重要一点是,在最近的计算机上,差异可能很小,甚至支持没有工作人员的代码。 内存副本的开销必须由工作人员使用的复杂代码来平衡。 上面的棕褐色调转换示例在某些情况下可能不足以保证切换到网络工作者。

但是,网络工作者在具有多核的低端硬件上确实很有用。

移植到Windows 8

最终,我无法忍受移植我的JavaScript代码以创建Windows 8应用程序的乐趣。 我花了大约10分钟的时间创建了一个空白的JavaScript项目并复制/粘贴了其中的JavaScript代码。 您可以在此处获取Windows应用程序代码,并体验Windows 8的本机JavaScript代码的强大功能!

From: https://www.sitepoint.com/using-web-workers-to-improve-image-manipulation-performance/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值