利用网络worker提高图像操作的性能

分享于 

13分钟阅读

Web开发

  繁體

在10天内开发 Windows 8应用程序。

今天我想用纯Javascript来讨论HTML5中的图片操作。

测试案例

测试应用程序很简单。 在左侧,要操作的图片和右侧的更新结果( 应用了棕褐色的色调效果):

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

<!DOCTYPEhtml><html><head><metacharset="utf-8"/><title>PictureWorker</title><linkhref="default.css"rel="stylesheet"/></head><bodyid="root"><divid="sourceDiv"><imgid="source"src="mop.jpg"/></div><divid="targetDiv"><canvasid="target"></canvas></div><divid="log"></div></body></html>

要应用棕褐色色调效果,需要为现有源图片的每个像素计算一个新的RGB值,然后用 id="目标"在 <画布> 标签上显示它。 below 是我们用来从一个像素的现有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);

为了更加真实,我增加了一点随机性我的黑褐色公式。 i的值从 0.5到 1,确定最终像素输出与上面的公式匹配的程度,与它保持原始RGB值的程度相对应。

function noise() {
 //Returns a value between 0.5 and 1return 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 valuereturn (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);
};

蛮力

显然,第一个解决方案是使用强力,用一个函数在每个像素上应用先前的代码。 若要访问像素,可以使用画布上下文使用下面的代码,创建源dbo和目标画布的指针:

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 对象包含每个像素的array,可以用来快速地将数据读或者写到画布上。 记住这一点,我们可以用下面的代码来应用整个效果:

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 objectvar 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极端处理器( 12内核) 上,主流程需要 150ms,明显只使用了一个处理器:

输入网络worker

当处理 SIMD ( 单指令多数据) 时,最好的事情是使用并行化方法。 特别是当你希望使用有限资源的低端硬件( 例如电话设备) 时。

在JavaScript中,为了享受并行化的能力,你必须使用网络worker。 我的朋友 David Rousset在这个主题上写了一。

图片处理对于并行化来说是一个很好的候选者,因为( 在我们的sepia功能的情况下) 每个处理都是独立的。 因此,以下方法是可能的:

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

// add the below functions to tools.jsfunction 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);
 }
};

本脚本的主要部分是一部分画布数据,即当前 block 要处理的部分由JavaScript克隆并传递给工作人员。 工人没有在初始源上工作,而是在它的( 使用结构化克隆算法 ) 副本上工作。 副本本身非常快速,只限于图片的特定部分。

主客户端页面( 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 timevar start = new Date();
 var canvas = document.getElementById("target");
 canvas.width = source.clientWidth;
 canvas.height = source.clientHeight;
 // Testing canvas supportif (!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 usualif (!window.Worker) {
 // Getting all the canvas datavar 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 上面, one for each web-workervar workersCount = 4;
 var finished = 0;
 var segmentLength = len/workersCount; // This is the length of array sent to the workervar blockSize = canvas.height/workersCount; // Height of the picture chunck for every worker// Function called when a job is finishedvar onWorkEnded = function (e) {
 // Data is retrieved using a memory clone operationvar 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 workerfor (var index = 0; index < workersCount; index++) {
 var worker = new Worker("pictureProcessor.js");
 worker.onmessage = onWorkEnded;
 // Getting the picturevar 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";

使用这里技术,完整的过程仅在我的计算机上保持 80ms ( 从 150毫秒),并且显然使用 4处理器:

在低端硬件( 基于双核心系统) 上,进程时间降低到 500ms ( 从 900毫秒)。

可以在这里下载 final 代码,并在这里发布一个工作示例( )。 作为比较,这里是相同的代码 ,没有网络worker的代码。

注意,最重要的一点是最近计算机上的差异可能很小,甚至在没有工人的情况下也是如此。 内存副本的开销必须由工作人员使用的复杂代码平衡。 在某些情况下,可以能不足以在某些情况下切换到网络工作人员的上面 示例转换。 然而,网络worker实际上在具有多个内核的低端硬件上非常有用。

迁移到 Windows 8

finally 无法抵抗移植我的JavaScript代码以创建 Windows 8应用程序的乐趣。 创建一个空白JavaScript项目并复制/粘贴JavaScript代码 inside 花费了我大约 10分钟的时间。 你可以在这里获取 Windows 应用程序代码 ,并感觉到本机JavaScript代码的强大功能,例如 Windows。

本文是来自 IE 团队的HTML5技术系列的一部分。 在本文中试用 3个月的免费 BrowserStack 跨浏览器 测试 @ http://modern.IE

David是微软法国的技术倡导者,专攻HTML5和网络开发。 本文最初出现在他的MSDN博客上,EternalCoding在 2012年09月20日 中,. You 可以跟随他的@deltakosh 在 Twitter 上。


WEB  IMP  MAN  图像  PERF  Using  
相关文章