Статьи

Частичное управление изображениями с помощью Canvas и Webworkers

В моей предыдущей статье я показал, как использовать box2d-web и deviceorientation, чтобы перемещаться по кругу, используя стандартные HTML5 API. Моей первоначальной целью этой статьи было загрузить изображение, перевести его в серию отдельных кругов и позволить вам поиграть с этим. Но это на потом. Сейчас я покажу вам, как вы можете использовать canvas вместе с веб-работниками для разгрузки тяжелых вычислительных функций в фоновый поток.

Для этого примера мы возьмем входное изображение и будем манипулировать изображением в блоках 10х10 пикселей. Для каждого блока мы вычислим доминирующий цвет и отобразим прямоугольник на нашей цели в этом цвете. Поскольку пример говорит более тысячи слов, мы собираемся создать это:

manip_example

Для тех, кто интересуется, это моя дочь очень задумывается о чем-то. Вы можете увидеть это демо в действии здесь .

Начиная

Для реализации этого примера нам не нужно много делать. Мы просто должны сделать следующие шаги:

  1. Подождите, пока изображение не загрузится.
  2. Разделите изображение на отдельные части, готовые к обработке.
  3. Настройте веб-работника для начала обработки при получении сообщения
  4. Рассчитайте доминирующий цвет из нашего образца изображения.
  5. Нарисуйте прямоугольник на нашем целевом холсте

Давайте начнем с простого, и посмотрим на код загрузки изображения.

Подождите, пока изображение загрузится

Прежде чем мы начнем обрабатывать изображение, мы должны убедиться, что оно полностью загружено. Для этого мы используем следующий фрагмент кода:

// start processing when the document is loaded.
    $(document).ready(function () {
 
        // handles rendering the elements
        setupWorker();
 
        // wait for the image to be loaded, before we start processing it.
        $("#source").load(function () {
 
            // determine size of image
            var imgwidth = $(this).width();
            var imgheight = $(this).height();
 
            // create a canvas and make context available
            var targetCanvas = createTargetCanvas(imgwidth, imgheight);
            targetContext = targetCanvas.getContext("2d");
 
            // render elements
            renderElements(imgwidth, imgheight, $(this).get()[0]);
        });
    });

Как вы можете видеть здесь, мы сначала регистрируем функцию готовности jquery. Это сработает, когда будет загружен весь документ. Это, однако, не означает, что изображения также уже были загружены. Чтобы убедиться, что изображение готово к обработке, мы добавляем функцию загрузки jquery к нашему исходному изображению (имеет идентификатор #source). Когда изображение загружено, мы определяем требуемый размер нашего целевого холста, на котором мы рендерим результат, и запускаем рендеринг, используя функцию renderElements. Функция renderElements разделяет изображение и огни веб-работников.

Разделите изображение на отдельные части, готовые к обработке

Цель этого примера — создать эффект низкого пикселя на исходном изображении. Мы делаем это, выбирая часть изображения, вычисляем доминирующий цвет и рисуем квадрат на целевом холсте Следующий код показывает, как вы можете использовать временный холст для выбора части изображения.

    // process the image by splitting it in parts and sending it to the worker
    function renderElements(imgwidth, imgheight, image) {
        // determine image grid size
        var nrX = Math.round(imgwidth / bulletSize);
        var nrY = Math.round(imgheight / bulletSize);
 
        // iterate through all the parts of the image
        for (var x = 0; x < nrX; x++) {
            for (var y = 0; y < nrX; y++) {
                // create a canvas element we use for temporary rendering
                var canvas2 = document.createElement('canvas');
                canvas2.width = bulletSize;
                canvas2.height = bulletSize;
                var context2 = canvas2.getContext('2d');
                // render part of the image for which we want to determine the dominant color
                context2.drawImage(image, x * bulletSize, y * bulletSize, bulletSize, bulletSize, 0, 0, bulletSize, bulletSize);
 
                // get the data from the image
                var data = context2.getImageData(0, 0, bulletSize, bulletSize).data
                // convert data, which is a canvas pixel array, to a normal array
                // since we can't send the canvas array to a webworker
                var dataAsArray = [];
                for (var i = 0; i < data.length; i++) {
                    dataAsArray.push(data[i]);
                }
 
                // create a workpackage
                var wp = new workPackage();
                wp.colors = 5;
                wp.data = dataAsArray;
                wp.pixelCount = bulletSize * bulletSize;
                wp.x = x;
                wp.y = y;
 
                // send to our worker.
                worker.postMessage(wp);
            }
        }
    } 

В этой функции мы сначала определяем, сколько строк и столбцов мы собираемся разделить изображение. Мы перебираем каждый из этих элементов и визуализируем эту конкретную часть изображения на временном холсте. С этого холста мы получаем данные, используя функцию getImageData. На данный момент у нас есть вся информация, необходимая нашему работнику для расчета доминирующего цвета (это дорогостоящая операция). Мы храним информацию в «рабочем пакете»:

 function workPackage() {
        this.data = [];
        this.pixelCount = 0;
        this.colors = 0;
        this.x = 0;
        this.y = 0;
 
        this.result = [0, 0, 0];
    }

Это удобный класс, который служит сообщением для нашего веб-работника. Обратите внимание, что нам нужно преобразовать результат вызова getImageData в обычный массив. Информация для веб-работника копируется, и Chrome по крайней мере не может скопировать результирующий массив из операции getImageData. Все идет нормально. Теперь у нас есть хорошие рабочие пакеты для каждой части нашего экрана, которые мы передаем веб-работнику с помощью операции worker.postMessage. Но как выглядит этот работник и как мы его настраиваем?

  • Настройте веб-работника для начала обработки при получении сообщения
  • Мы создаем работника в операции setupWorker, которая вызывается при загрузке нашего документа.

       function setupWorker() {
            worker = new Worker('extractMainColor.js');
            worker.addEventListener('message', function (event) {
     
                // the workpackage contains the results
                var wp = event.data;
     
                // get the colors
                var colors = wp.result;
     
                drawRectangle(targetContext, wp.x, wp.y, bulletSize, colors[0]);
                //drawCircle(targetContext, wp.x, wp.y, bulletSize, colors[0]);
     
            }, false);
        }

    Как видите, создать работника очень просто. Просто укажите работнику на JavaScript, который он должен выполнить. Обратите внимание, что существуют все виды ограничений в отношении ресурсов и объектов, к которым имеет доступ работник. Хорошее введение в то, что можно, а что нет, можно найти в этой статье .
    Как только мы определили работника, мы добавляем eventListener. Этот слушатель вызывается, когда работник использует операцию postMessage. В нашем примере это используется для передачи результата обратно в тот же рабочий пакет. Основываясь на этом результате, мы рисуем прямоугольник (или другую фигуру) на нашем целевом холсте. Сам работник очень прост:

    importScripts('quantize.js' , 'color-thief.js');
     
    self.onmessage = function(event) {
     
        var wp = event.data;
        var foundColor = createPaletteFromCanvas(wp.data,wp.pixelCount, wp.colors);
        wp.result = foundColor;
        self.postMessage(wp);
     
    };

    Этот работник использует два внешних скрипта для расчета и возврата доминирующего цвета. Это достигается путем получения необходимой информации из рабочего пакета, вычисления доминирующего цвета и возврата результата в рабочий пакет с использованием postMessage. Вычислить доминирующий цвет не так просто. Я наткнулся на огромную библиотеку по имени color-thief , которая делает это для вас. Очевидно, вам нужно принимать во внимание больше, чем просто значения RGB, если вы сделаете это, то получите просто набор коричневых цветов.

    Рассчитайте доминирующий цвет из нашего образца изображения.

    Я упомянул, что я использовал библиотеку Color-Thief для расчета доминирующего цвета. Я делаю это с помощью этого кода:

    createPaletteFromCanvas(wp.data,wp.pixelCount, wp.colors);

    Это, однако, непосредственно не предоставлено вором цвета. Color-thief предполагает, что вы хотите использовать его непосредственно на элементе изображения на вашей странице. Мне пришлось расширить библиотеку color-thief следующей простой операцией, чтобы она могла работать напрямую с двоичными данными.

    function createPaletteFromCanvas(pixels, pixelCount, colorCount) {
     
        // Store the RGB values in an array format suitable for quantize function
        var pixelArray = [];
        for (var i = 0, offset, r, g, b, a; i < pixelCount; i++) {
            offset = i * 4;
            r = pixels[offset + 0];
            g = pixels[offset + 1];
            b = pixels[offset + 2];
            a = pixels[offset + 3];
            // If pixel is mostly opaque and not white
            if (a >= 125) {
                if (!(r > 250 && g > 250 && b > 250)) {
                    pixelArray.push([r, g, b]);
                }
            }
        }
     
        // Send array to quantize function which clusters values
        // using median cut algorithm
     
        var cmap = MMCQ.quantize(pixelArray, colorCount);
        var palette = cmap.palette();
     
        return palette;
    }

    Это возвращает массив наиболее доминирующих цветов (так же, как это делают обычные функции вора цветов), но может работать непосредственно с данными нашего работника.

    Нарисуйте прямоугольник на нашем целевом холсте

    И это в значительной степени так. На данный момент мы разделили наше изображение на массив субизображений. Каждая часть отправляется веб-работнику для обработки. Веб-работник обрабатывает изображение и передает результат обратно нашему обработчику событий. В обработчике событий мы выбираем наиболее доминирующий цвет и можем использовать его для рисования на холсте. На рисунке в начале этой статьи я использовал прямоугольники:

    Софи прямоугольники

    Используя этот JavaScript (и с размером маркера 15):

      // draw a rectangle on the supplied context
        function drawRectangle(targetContext, x, y, bulletSize, colors) {
            targetContext.beginPath();
            targetContext.rect(x * bulletSize, y * bulletSize, bulletSize, bulletSize);
            targetContext.fillStyle = "rgba(" + colors + ",1)";
            targetContext.fill();
        }

    Но мы могли бы так же легко сделать круги:

    Софи круги

    Используя это:

     // draw a circle on the supplied context
        function drawCircle(targetContext, x, y, bulletSize, colors) {
            var centerX = x * bulletSize + bulletSize / 2;
            var centerY = y * bulletSize + bulletSize / 2;
            var radius = bulletSize / 2;
     
            targetContext.beginPath();
            targetContext.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
            targetContext.fillStyle = "rgba(" + colors + ",1)";
            targetContext.fill();
        }

    Как вы можете видеть, веб-работники действительно просты в использовании, а canvas дает нам много возможностей для работы с imagedata. В этом примере я использовал только одного веб-работника, более интересным было бы добавить очередь, в которой несколько работников прослушивали бы реально обрабатываемые элементы параллельно. Демо и полный код этой статьи можно найти здесь .