Манипуляция пикселями — это процесс изменения цветовых данных изображения непосредственно на уровне отдельных пикселей перед их отображением на карте. В OpenLayers такая возможность используется для создания визуальных эффектов, тематического отображения растровых данных, анализа изображений, выделения объектов и динамической обработки картографической информации в браузере.
Основой подобных операций выступает доступ к массиву пиксельных данных, где каждый пиксель представлен набором значений каналов:
Каждый канал хранится в диапазоне от 0 до 255.
Например, пиксель полностью красного цвета выглядит следующим образом:
[255, 0, 0, 255]
Полностью прозрачный пиксель:
[0, 0, 0, 0]
В OpenLayers манипуляция пикселями обычно применяется при работе с
растровыми слоями и механизмом растровой обработки
(ol/source/Raster).
Для изменения пикселей используется источник данных RasterSource.
Схема работы выглядит следующим образом:
Пример создания растрового источника:
import RasterSource from 'ol/source/Raster.js';
import OSM from 'ol/source/OSM.js';
const raster = new RasterSource({
sources: [
new OSM()
],
operationType: 'pixel',
operation: function(pixel) {
return pixel;
}
});
В данном случае операция ничего не изменяет и возвращает исходный пиксель.
OpenLayers поддерживает два режима выполнения операций.
Операция вызывается для каждого пикселя отдельно.
operationType: 'pixel'
Функция получает массив:
function(pixel) {
return pixel;
}
Структура:
[
red,
green,
blue,
alpha
]
Этот режим наиболее удобен для цветовых преобразований.
Операция получает сразу всё изображение.
operationType: 'image'
Пример:
function(inputs) {
return inputs[0];
}
Такой подход подходит для сложных вычислений и анализа соседних пикселей.
Одной из самых простых операций является инверсия.
Исходный принцип:
новый цвет = 255 - старый цвет
Реализация:
const raster = new RasterSource({
sources: [osmSource],
operation: function(pixel) {
pixel[0] = 255 - pixel[0];
pixel[1] = 255 - pixel[1];
pixel[2] = 255 - pixel[2];
return pixel;
}
});
Результат:
Градации серого часто используются для анализа картографических изображений.
Наиболее распространённая формула:
Gray =
0.299 × R +
0.587 × G +
0.114 × B
Реализация:
const raster = new RasterSource({
sources: [osmSource],
operation: function(pixel) {
const gray =
0.299 * pixel[0] +
0.587 * pixel[1] +
0.114 * pixel[2];
pixel[0] = gray;
pixel[1] = gray;
pixel[2] = gray;
return pixel;
}
});
После обработки карта отображается как чёрно-белое изображение.
Для увеличения яркости к каждому каналу добавляется некоторое значение.
const raster = new RasterSource({
sources: [osmSource],
operation: function(pixel) {
pixel[0] += 40;
pixel[1] += 40;
pixel[2] += 40;
return pixel;
}
});
Однако значения не должны превышать 255.
Безопасный вариант:
function(pixel) {
pixel[0] = Math.min(255, pixel[0] + 40);
pixel[1] = Math.min(255, pixel[1] + 40);
pixel[2] = Math.min(255, pixel[2] + 40);
return pixel;
}
Затемнение выполняется аналогично.
function(pixel) {
pixel[0] = Math.max(0, pixel[0] - 40);
pixel[1] = Math.max(0, pixel[1] - 40);
pixel[2] = Math.max(0, pixel[2] - 40);
return pixel;
}
Чем больше величина вычитания, тем темнее становится изображение.
Контрастность влияет на различимость светлых и тёмных областей.
Формула:
newValue =
(value - 128) * factor + 128
Пример:
const factor = 1.5;
function(pixel) {
pixel[0] = (pixel[0] - 128) * factor + 128;
pixel[1] = (pixel[1] - 128) * factor + 128;
pixel[2] = (pixel[2] - 128) * factor + 128;
return pixel;
}
При значениях:
Альфа-канал расположен в четвёртом элементе массива.
pixel[3]
Полупрозрачность:
function(pixel) {
pixel[3] = 128;
return pixel;
}
Полная прозрачность:
function(pixel) {
pixel[3] = 0;
return pixel;
}
Полная непрозрачность:
function(pixel) {
pixel[3] = 255;
return pixel;
}
Иногда требуется оставить только объекты определённого цвета.
Например, выделение зелёных областей.
function(pixel) {
const red = pixel[0];
const green = pixel[1];
const blue = pixel[2];
if (green > 120 &&
green > red &&
green > blue) {
return pixel;
}
return [0, 0, 0, 0];
}
В результате остаются только зелёные пиксели.
Подобная техника применяется для:
Сепия часто используется для стилизации карт.
Формулы преобразования:
newRed =
0.393R + 0.769G + 0.189B
newGreen =
0.349R + 0.686G + 0.168B
newBlue =
0.272R + 0.534G + 0.131B
Реализация:
function(pixel) {
const r = pixel[0];
const g = pixel[1];
const b = pixel[2];
pixel[0] =
0.393 * r +
0.769 * g +
0.189 * b;
pixel[1] =
0.349 * r +
0.686 * g +
0.168 * b;
pixel[2] =
0.272 * r +
0.534 * g +
0.131 * b;
return pixel;
}
Получается эффект старой фотографии.
Постеризация уменьшает количество цветовых уровней.
Пример:
function(pixel) {
pixel[0] =
Math.floor(pixel[0] / 64) * 64;
pixel[1] =
Math.floor(pixel[1] / 64) * 64;
pixel[2] =
Math.floor(pixel[2] / 64) * 64;
return pixel;
}
Изображение приобретает характерный ступенчатый стиль.
В RasterSource можно передавать дополнительные параметры.
Создание:
const raster = new RasterSource({
sources: [osmSource],
operation: function(pixel, data) {
pixel[0] *= data.redFactor;
pixel[1] *= data.greenFactor;
pixel[2] *= data.blueFactor;
return pixel;
}
});
Передача параметров:
raster.set('redFactor', 1.2);
raster.set('greenFactor', 1.0);
raster.set('blueFactor', 0.8);
Подготовка данных:
raster.on('beforeoperations', function(event) {
event.data.redFactor =
raster.get('redFactor');
event.data.greenFactor =
raster.get('greenFactor');
event.data.blueFactor =
raster.get('blueFactor');
});
Такой механизм позволяет изменять обработку без пересоздания слоя.
Событие вызывается непосредственно перед запуском вычислений.
Пример:
raster.on(
'beforeoperations',
function(event) {
event.data.threshold = 100;
}
);
Получение данных:
function(pixel, data) {
if (
pixel[0] > data.threshold
) {
pixel[0] = 255;
}
return pixel;
}
Событие удобно использовать для передачи настроек пользовательского интерфейса.
После завершения обработки вызывается событие:
raster.on(
'afteroperations',
function(event) {
console.log(
'Обработка завершена'
);
}
);
Обычно применяется для:
RasterSource способен работать сразу с несколькими входными слоями.
const raster = new RasterSource({
sources: [
source1,
source2
],
operationType: 'pixel',
operation: function(pixels) {
const p1 = pixels[0];
const p2 = pixels[1];
return [
(p1[0] + p2[0]) / 2,
(p1[1] + p2[1]) / 2,
(p1[2] + p2[2]) / 2,
255
];
}
});
Подобный подход используется при:
Пороговая обработка преобразует изображение в чёрно-белое.
Пример:
function(pixel) {
const gray =
(
pixel[0] +
pixel[1] +
pixel[2]
) / 3;
if (gray > 128) {
return [255, 255, 255, 255];
}
return [0, 0, 0, 255];
}
Такой алгоритм применяется для:
Пиксельная обработка способна выполнять миллионы операций за один кадр. Поэтому необходимо учитывать ряд особенностей.
Минимизировать количество вычислений
Плохо:
Math.pow(value, 2)
Лучше:
value * value
Избегать создания лишних объектов
Плохо:
return [
pixel[0],
pixel[1],
pixel[2],
pixel[3]
];
Лучше:
return pixel;
если значения не изменяются.
Не выполнять тяжёлые запросы внутри операции
Недопустимо:
fetch('/data');
или
console.log(pixel);
для каждого пикселя.
Передавать параметры через beforeoperations
Так исключается повторное получение данных внутри миллионов вызовов функции обработки.
Манипуляция пикселями в OpenLayers используется для решения широкого круга задач:
Гибкость RasterSource позволяет реализовывать как простые цветовые фильтры, так и сложные алгоритмы обработки изображений непосредственно в браузере, используя стандартные возможности JavaScript и инфраструктуру OpenLayers.