Статьи

Использование веб-работников для повышения производительности манипуляции изображениями

Сегодня я хотел бы поговорить о манипулировании картинками. Не 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 внутри Sourire(почувствуйте всю мощь собственного кода JavaScript для Windows 8!)

Так что не стесняйтесь захватить код приложения для Windows 8 здесь: http://www.catuhe.com/msdn/Win8PictureWorkers.zip