npm имеет удобный пакет для извлечения пиксельных данных из данного изображения, но он не дает нам никакого (очевидного) способа извлечь пиксели из данного сектора изображения с определенным ширина и высота. Сделать это на стороне клиента легко, поскольку у нас есть такие инструменты, как элемент холста HTML5, но на стороне сервера мы как бы застряли.

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

Во-первых, давайте настроим наш модуль так, чтобы он требовал get-пикселей:

var getPixels = require(‘get-pixels’);

Затем мы создадим функцию, которая может вызывать getPixels для данного изображения. Для иллюстрации представим, что свойство req.body.imagePath содержит путь к нашему изображению:

var pixelGetter = function(req, res){
  getPixels(req.body.imagePath, function(err, pixels){
    if (err){
      console.log(err);
      return;
    } else {
      console.log(pixels.data);
    }
  });
}

pixel.data возвращает буферный объект со свойством length, и мы можем перебирать его аналогично тому, как мы перебираем индексы массива. Первые четыре индекса будут значениями RGBA, соответственно, для первого пикселя; вторые четыре индекса будут значениями RGBA для второго пикселя и так далее. Вот как это может выглядеть:

[255, 255, 235, 0, 253, 143, 32, 0…]

Представьте, что у нас есть изображение .png шириной 5 пикселей и высотой 5 пикселей. Когда getPixels вызывается для этого изображения, он возвращает буферный объект со 100 элементами: 5 пикселей на 5 пикселей всего 25 пикселей, каждый с компонентом R, компонентом G, компонентом B и компонентом A - 25 пикселей с 4 свойства, каждое дает нам 100 элементов.

Наша проблема в том, что при доступе к файлам pixel.data все эти элементы существуют в виде полностью плоского массива! К счастью, итерация по массиву, хранящемуся в свойстве .data, - не единственный для нас способ получить доступ к пикселям в изображении, поскольку get-pixel фактически возвращает ndarray. n в данном случае относится к количеству измерений массива - у нашего - 3, и он может выглядеть примерно так:

Думайте о красном, синем, зеленом и сером квадратах как о значениях RGBA для данного пикселя, и что данные для данного пикселя состоят из четырех элементов «глубиной».

Теперь предположим, что у нас есть ndarray, представляющий изображение размером 5 пикселей на 5 пикселей (как визуальное представление массива ndarray слева), и нам просто нужны пиксели для верхнего левого сегмента изображения размером 3 на 3 пикселя. Это несложно сделать, используя пару вложенных циклов for:

var flatArray = [];
for (var y = 0; y < 3; i++){
  for (var x = 0; x < 3; j++){
    for (var z = 0; z < 4; k++){
      flatArray.push(pixels.get(x, y, z));
    }
  }
}

Давайте посмотрим на все, что мы сделали в приведенном выше фрагменте кода, начиная с кода, содержащегося в самом внутреннем цикле for.

Pixel.get принимает столько аргументов, сколько размеров, и они появляются в том порядке, в котором мы ожидаем - в нашем случае у нас есть x для ширины, y. для высоты и z для «глубины». (В анимированных гифках есть даже четвертое измерение, представляющее пиксели в каждом кадре. Правильно - время!) Измерение z можно метафорически понимать как «глубину» - и это имеет смысл, если вы подумаете о каждом из них. пиксель имеет глубину 4 элемента из-за того, что он имеет значения r, g, b и a соответственно. Вот снова наше изображение:

Наша итерация начинается с красного квадрата в верхнем левом углу куба - мы перемещаемся по красной грани нашего куба слева направо, сверху вниз, и на каждой остановке на красном квадрате мы ныряем внутрь. и возьмите для пикселя значения красного, зеленого, синего и альфа-канала. При построении плоского массива мы хотим передать каждое из этих значений r, g, b и a (какими бы они ни были) в массив, прежде чем перейти к пикселю справа от него (это происходит, когда мы перебираем x loop). И, конечно же, как только мы достигли 5-го пикселя в горизонтальном ряду пикселей (x = 4), мы увеличиваем y один раз, чтобы перейти к строке непосредственно под ним, сбрасываем x на ноль и повторяем.

Если бы мы хотели получить кусок размером 3x13 пикселей в правом верхнем квадранте нашего изображения, наш код выглядел бы очень похоже:

var flatArray = [];
for (var y = 2; i < 5; i++){
  for (var x = 2; j < 5; j++){
    for (var z = 0; k < 4; k++){
      flatArray.push(pixels.get(x, y, z));
    }
  }
}

В любом случае наша переменная flatArray будет выглядеть примерно так:

[235, 216, 130, 0, 253, 143, 32, 0…]

Превратить это обратно в ndarray - тривиально. Сначала установите ndarray с помощью npm:

npm install ndarray

Теперь потребуйте его в своем модуле узла, чтобы мы могли получить доступ ко всем его изящным методам:

var ndarray = require(‘ndarray’);

Создать ndarray с желаемыми размерами так же просто, как вызвать следующую команду в нашей переменной flatArray:

var pixelArray = ndarray(new Float64Array(flatArray), [10,10,4])

Теперь у нас есть ndarray, который позволяет нам получить доступ к значениям пикселей rgba с помощью удобных методов получения и установки, которые поставляются в комплекте с модулем.

Но есть одна хитрость - вышеизложенное помогает понять, что происходит под капотом, но оказывается, что у ndarrays есть методы, которые позволяют нам абстрагироваться от большой части этой математики.

Фактически мы можем разрезать этот массив по трем измерениям, используя .hi и .lo. Думайте о .lo как о щелчке и перетаскивании верхнего левого угла нашего изображения в нижний правый угол изображения, чтобы захватить несколько пикселей. Используя модель 5x5 пикселей, показанную выше, мы могли бы получить значения R для верхнего левого угла 3x3 нашего изображения, вызвав это (помните, что массивы в JavaScript начинаются с нуля):

pixelArray.lo(2, 2, 0);

В результате будет выделен массив ndarray, который выглядит как оранжевый сегмент нашей модели ниже:

Мы можем связать .lo с .hi, чтобы выбрать только часть, полученную в результате перекрытия каждого метода. Если бы нам нужно было только значение R пикселя, расположенного в (2, 2), мы могли бы написать код, который выглядел бы так:

pixelArray.lo(2, 2, 0).hi(2, 2, 0);

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

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

pixelArray.get(2, 2, 0);

Однако есть много случаев, когда мы хотели бы использовать эту цепочку методов.

Допустим, мы хотели выделить нижний левый сектор изображения размером 2x2. Наш код будет выглядеть так:

pixelArray.lo(2, 4, 0).hi(4, 2, 0);

Вот как это будет выглядеть на нашей модели:

Мы еще не исследовали глубину захвата. Если бы мы хотели захватить и красную, и зеленую части этого фрагмента пикселей 2x2, все, что нам нужно было бы сделать, это изменить наш код, чтобы он выглядел так:

pixelArray.lo(2, 4, 1).hi(4, 2, 1);

Опять же, вот как это будет выглядеть на нашей модели:

Вот и все! ndarrays чрезвычайно удобны для захвата элементов в многомерном массиве и особенно удобны, когда мы хотим захватить и выполнить анализ определенных пикселей в изображении.