Современные браузеры поддерживают воспроизведение видео через элемент <video>
. Большинство браузеров также могут получить доступ к камере через API MediaDevices.getUserMedia(). Но даже если эти две вещи объединить, мы не сможем напрямую получить доступ к этим пикселям и манипулировать ими.
К счастью, в браузерах есть Canvas API, который позволяет нам рисовать графику с помощью JavaScript. На самом деле мы можем рисовать изображения в <canvas>
из самого видео, что позволяет нам манипулировать этими пикселями и отображать их.
То, что вы узнаете здесь о том, как манипулировать пикселями, даст вам основу для работы с изображениями и видео любого типа и источника, а не только с холстом.
Добавить изображение на холстПрежде чем мы начнем видео, давайте посмотрим, как добавить изображение на холст.
<img src><div> <canvas id=Canvas class=video></canvas></div>
Мы создаем элемент изображения, представляющий изображение, которое будет нарисовано на холсте. Альтернативно мы можем использовать объект Image в JavaScript.
var Canvas;var context;function init() { var image = document.getElementById('SourceImage'); Canvas = document.getElementById('Canvas'); context = Canvas.getContext('2d'); // Или // var image = new Image(); // image.onload = function () { // drawImage(image); // image.src = 'image.jpg';}function drawImage(image) { // Устанавливаем холст такой же ширины и высоты, как и изображение. Canvas.width = image.width; Canvas.height = image.height; context.drawImage(image, 0, 0);}window.addEventListener('load', init);
Приведенный выше код рисует все изображение на холсте.
Посмотрите изображение «Рисование на холсте», созданное Веллингом Гузманом (@wellingguzman) на CodePen.
Теперь мы можем начать играть с этими пикселями!
Обновить данные изображенияДанные изображения на холсте позволяют нам манипулировать пикселями и изменять их.
Атрибут данных — это объект ImageData, который имеет три свойства — ширину, высоту и данные/все из которых представляют что-то на основе исходного изображения. Все эти свойства доступны только для чтения. Нас интересуют данные — одномерный массив, представленный объектом Uint8ClampedArray, содержащим данные для каждого пикселя в формате RGBA.
Несмотря на то, что свойство данных доступно только для чтения, это не означает, что мы не можем изменить его значение. Это означает, что мы не можем присвоить этому свойству другой массив.
// Получаем данные изображения холста. imageData = context.getImageData(0, 0, Canvas.width, Canvas.height);image.data = new Uint8ClampedArray(); // НЕПРАВИЛЬНО.data[1] = 0;
Вы можете спросить, какое значение представляет объект Uint8ClampedArray? Вот описание из MDN:
Массив типа Uint8ClampedArray представляет собой массив 8-битных целых чисел без знака, которые ограничены значениями от 0 до 255. Если вы укажете значение вне диапазона [0,255], будет установлено 0 или 255, если вы укажете нецелое число, ближайшее; Целое число будет установлено. Содержимое инициализируется значением 0. После установки на элементы массива можно ссылаться, используя методы объекта или используя стандартный синтаксис индексации массива (т.е. используя обозначение скобок).
Короче говоря, этот массив хранит значения в диапазоне от 0 до 255 в каждом месте, что делает его идеальным решением для формата RGBA, поскольку каждая часть представлена значением от 0 до 255.
Цвет RGBAЦвет может быть представлен в формате RGBA, который представляет собой комбинацию красного, зеленого и синего. A представляет альфа-значение непрозрачности цвета.
Каждая позиция в массиве представляет значение цветового (пиксельного) канала.
Если у вас изображение 2х2, то перед нами 16-битный массив (2х2 пикселя по 4 значения в каждом).
Изображение 2x2 уменьшено
Массив будет выглядеть так:
// КРАСНЫЙ ЗЕЛЕНЫЙ СИНИЙ БЕЛЫЙ[255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255]Изменить данные пикселей
Одна из самых быстрых вещей, которые мы можем сделать, — это сделать все пиксели белыми, изменив все значения RGBA на 255.
// Используйте кнопку для запуска эффектаvar button = document.getElementById('Button');button.addEventListener('click', onClick);functionchangeToWhite(data) { for (var i = 0; i < data.length; i++) { data[i] = 255; }}function onClick() { var imageData = context.getImageData(0, 0, Canvas.width, Canvas.height); ChangeToWhite(imageData.data); // Обновляем холст новыми данными context.putImageData(imageData, 0, 0);}
Данные будут переданы как ссылка, что означает, что любые изменения, которые мы вносим в них, изменят значение переданного параметра.
Инвертировать цветаХороший эффект, не требующий особых вычислений, — инвертирование цветов изображения.
Значения цвета можно инвертировать с помощью оператора XOR (^) или этой формулы 255 — значение (значение должно быть в диапазоне 0–255).
function invertColors(data) { for (var i = 0; i < data.length; i+= 4) { data[i] = data[i] ^ 255; // Инвертировать красный data[i+1] = data[i] +1] ^ 255; // Инвертируем зеленый data[i+2] = data[i+2] ^ 255 // Инвертируем синий }}function onClick() { var imageData =; context.getImageData(0, 0, Canvas.width, Canvas.height); invertColors(imageData.data); // Обновляем холст новыми данными context.putImageData(imageData, 0, 0);}
Мы увеличиваем цикл на 4 вместо 1, как мы делали раньше, поэтому мы можем заполнить 4 элемента массива от пикселя к пикселю, каждый пиксель.
Значение альфа не влияет на инвертирование цветов, поэтому мы его пропускаем.
Яркость и контрастностьЯркость изображения можно настроить по следующей формуле: новое значение = текущее значение + 255 * (яркость / 100).
Factor = (259 * (контраст + 255)) / (255 * (259 - контраст))color = GetPixelColor(x, y)newRed = Truncate(factor * (Red(color) - 128) + 128)newGreen = Truncate( коэффициент * (Зеленый(цвет) - 128) + 128)newBlue = Truncate(фактор * (Синий(цвет) - 128) + 128)
Основной расчет – получить коэффициент контрастности, который будет применяться к каждому значению цвета. Усечение — это функция, которая обеспечивает сохранение значения в диапазоне от 0 до 255.
Давайте напишем эти функции на JavaScript:
функция applyBrightness(данные, яркость) { for (var i = 0; i < data.length; i+= 4) { data[i] += 255 * (яркость data[i+1] += 255 *); (яркость/100); данные[i+2] += 255 * (яркость/100 }}function truncateColor(value) { if); (значение < 0) { значение = 0; } else if (значение > 255) { значение = 255; } возвращаемое значение;} function applyContrast (data, контраст) { var коэффициент = (259,0 * (контраст + 255,0)) / ( 255,0 * (259,0 - контраст)); for (var i = 0; i < data.length; i+= 4) { data[i] = truncateColor(фактор * (данные[i] - 128,0) + 128,0); данные[i+1] = truncateColor(фактор * (данные[i+1] - 128,0) + 128,0); данные[i+2] = truncateColor( коэффициент * (данные[i+2] - 128,0) + 128,0 }};
В этом случае вам не нужна функция truncateColor, поскольку Uint8ClampedArray усекает значения, а нужно преобразовать добавленный в нее алгоритм.
Следует помнить, что если вы применяете яркость или контрастность, данные изображения перезаписываются и не могут быть возвращены в предыдущее состояние. Если мы хотим вернуться в исходное состояние, исходные данные изображения должны храниться отдельно для справки. Сохранение переменной изображения доступной для других функций будет полезно, поскольку вы можете использовать изображение для перерисовки холста и исходного изображения.
var image = document.getElementById('SourceImage'); function redrawImage() { context.drawImage(image, 0, 0);}Используйте видео
Чтобы заставить его работать с видео, мы возьмем исходный скрипт изображения и HTML-код и внесем некоторые незначительные изменения.
HTMLИзмените элемент Image элемента видео, заменив следующую строку:
<источник изображения>
... с этим:
<источник видео></видео>
JavaScript
Замените эту строку:
var image = document.getElementById('SourceImage');
... добавьте эту строку:
вар видео = document.getElementById('SourceVideo');
Чтобы начать обработку видео, нам нужно дождаться, пока видео будет готово к воспроизведению.
video.addEventListener('canplay', function () { // Устанавливаем холст такой же ширины и высоты, что и видео Canvas.width = video.videoWidth; Canvas.height = video.videoHeight; // Воспроизведение видео video.play( ); // начинаем рисовать кадры drawFrame(video);});
Воспроизведение события воспроизводится как минимум в течение нескольких кадров, если имеется достаточно данных для воспроизведения мультимедиа.
Мы не можем видеть ни одного видео, отображаемого на холсте, поскольку показываем только первый кадр. Нам приходится выполнять drawFrame каждые n миллисекунд, чтобы поддерживать частоту кадров видео.
Внутри drawFrame мы снова вызываем drawFrame каждые 10 мс.
функция drawFrame(видео) { context.drawImage(video, 0, 0); setTimeout(function () { drawFrame(video); }, 10);}
После выполнения drawFrame мы создаем цикл, который выполняет drawFrame каждые 10 мс — достаточно времени, чтобы видео оставалось синхронизированным с холстом.
Добавляем эффекты к видеоМы можем использовать ту же функцию, которую мы создали ранее, для инвертирования цветов:
function invertColors(data) { for (var i = 0; i < data.length; i+= 4) { data[i] = data[i] ^ 255; // Инвертировать красный data[i+1] = data[i] +1] ^ 255; // Инвертировать зеленый data[i+2] = data[i+2] ^ 255 // Инвертировать синий }}
и добавьте это в функцию drawFrame:
функция drawFrame(видео) { context.drawImage(video, 0, 0); var imageData = context.getImageData(0, 0, Canvas.width, Canvas.height); invertColors(imageData.data); context.putImageData(imageData, 0, 0); setTimeout(функция () { drawFrame(видео); }, 10);}
Мы можем добавить кнопку и переключать эффект:
функция drawFrame(видео) { context.drawImage(video, 0, 0); if (applyEffect) { var imageData = context.getImageData(0, 0, Canvas.width, Canvas.height); invertColors(imageData.data); .putImageData(imageData, 0, 0); } setTimeout(function () { drawFrame(video); }, 10);}использовать камеру
Мы сохраним тот же код, который использовали для видео, с той лишь разницей, что мы будем использовать MediaDevices.getUserMedia для изменения видеопотока из файла в поток камеры.
MediaDevices.getUserMedia — это новый API, устаревший API MediaDevices.getUserMedia(). Браузеры по-прежнему поддерживают старые версии, а некоторые браузеры не поддерживают более новые версии, и нам приходится прибегать к полифилам, чтобы убедиться, что браузер поддерживает одну из них.
Сначала удалите атрибут src из видеоэлемента:
<video><code></pre><pre><code>// Устанавливаем источник видео для камерыstreamfunction initCamera(stream) { video.src = window.URL.createObjectURL(stream);}if (navigator .mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({video: true, audio: false}) .then(initCamera) .catch(console.error) );}
Живая демо-версия
ЭффектВсе, что мы рассмотрели до сих пор, является основой, которая нам нужна для создания различных эффектов для видео или изображений. Мы можем использовать множество различных эффектов, конвертируя каждый цвет независимо.
Оттенки серогоПреобразование цветов в оттенки серого можно выполнить разными способами, используя разные формулы/методы. Чтобы не углубляться в проблему, я собираюсь показать вам пять формул, основанных на инструменте обесцвечивания GIMP и Luma:
Серый = 0,21R + 0,72G + 0,07B // LuminosityGray = (R + G + B) ÷ 3 // Средняя яркость Gray = 0,299R + 0,587G + 0,114B // Rec601 StandardGray = 0,2126R + 0,7152G + 0,0722B / /ITU-RBT.709 стандартный серый = 0,2627R + 0,6780G + 0,0593B // стандарт ITU-R BT.2100
С помощью этих формул мы хотим найти уровень яркости каждого цвета пикселя. Значение варьируется от 0 (черный) до 255 (белый). Эти значения создадут эффект оттенков серого (черно-белого).
Это означает, что самый светлый цвет будет ближе всего к 255, а самый темный цвет будет ближе всего к 0.
Живая демо-версия
два тонаРазница между эффектом двухцветного изображения и эффектом оттенков серого заключается в том, что используются два цвета. В оттенках серого у вас есть градиент от черного к белому, тогда как в дуплексе у вас есть градиент от любого цвета к любому другому цвету (от синего к розовому).
Используя значение интенсивности оттенков серого, мы можем заменить его значением градиента.
Нам нужно создать градиент от ColorA к ColorB.
function createGradient(colorA, colorB) { // Значения градиента от цвета A до цвета B var градиент = [] // максимальное значение цвета — 255 var maxValue = 255 // Преобразование шестнадцатеричных значений цвета в RGB; object var from = getRGBColor(colorA); var to = getRGBColor(colorB); // Создает 256 цветов от цвета A до цвета B for (var i = 0; i); <= maxValue; i++) { // IntensityB изменится от 0 до 255 // IntensityA изменится с 255 до 0 // IntensityA уменьшит интенсивность, а интенсивность B увеличится // Это означает, что ColorA сначала станет сплошным и медленно превратится в ColorB // Если посмотреть на это по-другому, прозрачность цвета A увеличится, а прозрачность цвета B уменьшится var интенсивностиB = i; var интенсивностиA = maxValue - интенсивностиB; приведенная ниже формула объединяет два цвета на основе их интенсивности // (IntensityA * ColorA + IntensityB * ColorB) / maxValuegradient[i] = { r: (intensityA*from.r + интенсивностиB*to.r) / maxValue, g: ( интенсивностьA*from.g + интенсивностьB*to.g) / maxValue, b: (intensityA*from.b + интенсивностьB*to.b) / maxValue }; градиент;}// Вспомогательная функция для преобразования 6-значных шестнадцатеричных значений в объект цвета RGB. function getRGBColor(hex) { var colorValue; if (hex[0] === '#') { hex = hex.substr(1); } colorValue = parseInt(hex, 16); return { r: colorValue >> 16, g: (colorValue >> 8) & 255, b: цветовое значение и 255 }}
Короче говоря, мы создаем набор значений цвета, начиная с цвета A, уменьшая интенсивность, переходя к цвету B и увеличивая интенсивность.
От #0096ff до #ff00f0
var градиенты = [ {r: 32, g: 144, b: 254}, {r: 41, g: 125, b: 253}, {r: 65, g: 112, b: 251}, {r: 91 , г: 96, б: 250}, {р: 118, г: 81, б: 248}, {r: 145, г: 65, б: 246}, {r: 172, г: 49, б: 245}, {r: 197, г: 34, б: 244}, {r: 220, г: 21 , б: 242}, {р: 241, г: 22, б: 242},];
Представление масштабирования цветовых переходов
Выше приведен пример градиента с 10 значениями цвета от #0096ff до #ff00f0.
Представление цветовых переходов в оттенках серого
Теперь, когда у нас есть представление изображения в оттенках серого, мы можем использовать его для сопоставления его со значением двухтонового градиента.
Двухцветный градиент имеет 256 цветов, а оттенки серого — 256 цветов в диапазоне от черного (0) до белого (255). Это означает, что значение цвета в оттенках серого будет сопоставлено с индексом элемента градиента.
vargradientColors = createGradient('#0096ff', '#ff00f0');var imageData = context.getImageData(0, 0, Canvas.width, Canvas.height);applyGradient(imageData.data);for (var i = 0; i < data.length; i += 4) { // Получаем значение цвета каждого канала var redValue = data[i]; data[i+1]; var blueValue = data[i+2]; // Сопоставление значений цвета с индексом градиента // Замена значения цвета в градациях серого цветом для двухцветного градиента data[i] =gradientColors[ redValue].r; data[i+1] =gradientColors[greenValue].g; data[i+2] =gradientColors[blueValue].b; data[i+3] = 255;}
Живая демо-версия
в заключениеЭта тема могла бы быть более углубленной или объяснять больше последствий. Домашнее задание для вас — найти различные алгоритмы, которые можно применить к этим скелетным примерам.
Понимание структуры пикселей на холсте позволит вам создавать неограниченное количество эффектов, таких как сепия, смешение цветов, эффекты зеленого экрана, блики/сбои изображения и т. д.
Вы даже можете создавать эффекты на лету, не используя изображения или видео.
Выше приведено все содержание этой статьи. Я надеюсь, что она будет полезна для изучения всеми. Я также надеюсь, что все поддержат сеть VeVb Wulin.