Сегодня я хотел бы поговорить о манипулировании картинками. Не 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