Сегодня я хотел бы поговорить о манипулировании картинками. Не Direct2D, который я использовал в предыдущей статье, а чистый 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>
Общий процесс применения эффекта тона сепии требует, чтобы вы вычислили новое значение для каждого пикселя изображения:
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);
Чтобы сделать его более реалистичным, я добавил в формулу немного случайного, чтобы окончательный код JavaScript, применяемый к каждому пикселю, был:
function noise() {
return Math.random() * 0.5 + 0.5;
};
function colorDistance(scale, dest, src) {
return (scale * dest + (1 - scale) * src);
};
var processSepia = function (pixel) {
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);
};
Брутальная сила
Очевидно, самое первое решение может состоять в использовании брутального подхода с функцией, которая применяет предыдущий код к каждому пикселю.
Чтобы получить доступ к пикселям, вы можете использовать контекст холста со следующим кодом:
var source = document.getElementById("source");
source.onload = function () {
var canvas = document.getElementById("target");
canvas.width = source.clientWidth;
canvas.height = source.clientHeight;
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");
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(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 мс и, очевидно, использует только один процессор:
Добавление веб-работников
Лучшее, что вы можете сделать, когда имеете дело с SIMD (одна инструкция с несколькими данными), это использовать подход распараллеливания. Особенно, если вы хотите работать с недорогим оборудованием (например, телефонными устройствами) с ограниченными ресурсами.
С JavaScript, чтобы насладиться мощью распараллеливания, вы должны использовать Web Workers (мой друг Дэвид Руссет написал отличную статью на эту тему: http://blogs.msdn.com/b/davrous/archive/2011/07/ 15 / введение-в-html5-веб-работники-javascript-многопоточность-подход.aspx ).
Обработка изображений является действительно хорошим кандидатом для распараллеливания, потому что (в случае тона сепии) каждая обработка независима, и поэтому возможен следующий подход:
Для этого прежде всего необходимо создать файл 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);
}
};
Функция processSepia будет применена к каждому фрагменту изображения выделенным работником. Код каждого работника включен в файл pictureprocessor.js :
importScripts("tools.js");
self.onmessage = function (e) {
var canvasData = e.data.data;
var binaryData = canvasData.data;
var l = e.data.length;
var index = e.data.index;
processSepia(binaryData, l);
self.postMessage({ result: canvasData, index: index });
};
Суть в том, что данные холста (на самом деле их часть в соответствии с текущим блоком для обработки) клонируются JavaScript и передаются рабочему. Работник работает не с исходным источником, а с его копией (используя указанный алгоритм: алгоритм структурированного клонирования ). Сама копия действительно быстрая и ограничена определенной частью изображения.
Главная страница клиента ( default.js ) должна создать 4 рабочих и дать им правую часть изображения. Затем каждый работник будет вызывать функцию в главном потоке, используя API обмена сообщениями ( postMessage / onmessage ), чтобы вернуть результат:
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;
// 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
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
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
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 });
}
};
Используя эту технику, весь процесс длится всего 80 мс (из 150 мс) на моем компьютере и, очевидно, использует 4 процессора:
На моем недорогом оборудовании (на основе двухъядерной системы) этот процесс падает до 500 мс (с 900 мс).
Окончательный код доступен здесь: http://www.catuhe.com/msdn/pictureworkers.zip
И живая версия прямо здесь: http://www.catuhe.com/msdn/workers/default.html
(Для сравнения, версия для веб-работников отсутствует: http://www.catuhe.com/msdn/workers/defaultnoworker.html ).
Важно отметить, что на современных компьютерах разница может быть незначительной или даже в пользу кода без работников. Накладные расходы на копию памяти должны быть сбалансированы сложным кодом, используемым работниками. Тон сепии может быть недостаточно в некоторых случаях.
Тем не менее, веб-работники действительно будут полезны на низком оборудовании
Портирование на Windows 8
Наконец, я не смог устоять перед удовольствием портировать свой код JavaScript для создания приложения для Windows 8. Мне понадобилось около 10 минут, чтобы создать пустой проект JavaScript и скопировать / вставить код JavaScript внутри (почувствуйте всю мощь собственного кода JavaScript для Windows 8!)
Так что не стесняйтесь захватить код приложения для Windows 8 здесь: http://www.catuhe.com/msdn/Win8PictureWorkers.zip



