使用Web Workers提高图像处理性能






4.25/5 (4投票s)
使用Web Workers提高图像处理性能。
今天我想讨论一下在 HTML5 中使用纯 Javascript 进行图像处理。
测试用例
测试应用程序很简单。左边是要处理的图片,右边是更新后的结果(应用了棕褐色调效果)。
页面本身也很简单,如下所述:
<!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 值,然后使用 id="target" 的 <canvas> 标签将其渲染出来。以下是我们使用从现有像素的 RGB 值创建新的 RGB 值的公式:
finalRed= (red * 0.393) + (green * 0.769) + (blue * 0.189);
finalGreen = (red * 0.349) + (green * 0.686) + (blue * 0.168);
finalBlue= (red * 0.272) + (green * 0.534) + (blue * 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 上下文使用以下代码,该代码创建一个指向源图像和目标 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
对象包含每个像素的数组,可用于快速读取或写入数据到 canvas。 考虑到这一点,我们可以使用以下代码应用整个效果:
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 核)上,主进程需要 150 毫秒,并且显然只使用一个处理器。
进入 Web Workers
在处理 SIMD(单指令多数据)时,你能做的最好的事情是使用并行化方法。特别是当你想使用资源有限的低端硬件(例如手机设备)时。
在 JavaScript 中,要享受并行化的强大功能,你必须使用 Web Workers。我的朋友 David Rousset 写了一篇关于这个主题的精彩文章。
图像处理是并行化的一个非常好的候选者,因为(在我们的棕褐色调函数的情况下)每个处理都是独立的。 因此,以下方法是可行的:
为此,首先你必须创建一个 *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);
}
};
该脚本的主要点是 canvas 数据的某个部分,即当前要处理的块所需的部分,由 JavaScript 克隆并传递给 worker。 worker 不是在初始源上工作,而是在它的副本上工作(使用结构化克隆算法)。 副本本身非常快,并且仅限于图片的特定部分。
主客户端页面(*default.js*)必须创建 4 个 worker,并为它们提供图片的正确部分。 然后,每个 worker 将使用消息传递 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 个处理器。
在我的低端硬件(基于双核系统)上,处理时间降至 500 毫秒(从 900 毫秒)。
最终代码可以从这里下载,并且一个可用的示例发布在这里。 为了比较,这里是相同的代码没有 web workers。
一个需要注意的重要点是,在最近的计算机上,差异可能很小,甚至有利于没有 worker 的代码。 内存复制的开销必须通过 worker 使用的复杂代码来平衡。 上述棕褐色调转换的示例在某些情况下可能不足以保证切换到 web worker。 但是,web worker 在具有多个核心的低端硬件上将非常有用。
移植到 Windows 8
最后,我还是忍不住将我的 JavaScript 代码移植以创建一个 Windows 8 应用程序。 我花大约 10 分钟创建了一个空白的 JavaScript 项目并将 JavaScript 代码复制/粘贴到其中。 你可以从这里获取 Windows 应用程序代码,并感受 Windows 8 本地 JavaScript 代码的强大功能!
本文是 Internet Explorer 团队 HTML5 技术系列的一部分。通过 @ http://modern.IE 免费获得 3 个月的 BrowserStack 跨浏览器测试,亲身体验本文中的概念。
David Catuhe 是微软法国的技术推广主管,专门从事 HTML5 和 Web 开发。 本文最初出现在他的 MSDN 博客 EternalCoding 上,发表于2012 年 9 月 20 日。你可以在 Twitter 上关注他@deltakosh。