Я много лет работал видеоредактором, и большую часть этого времени я потратил на «цветокоррекцию», то есть «на то, чтобы сделать видеоматериал похожим на фильм». Это выглядит примерно так:
Достойные профессиональные (или промышленные / компьютерные) камеры стараются улавливать широкий диапазон тонов «точно» и без излишнего применения фильтров. Это дает вам как можно больше информации для работы, но может выглядеть довольно скучно. Таким образом, цель цветокоррекции обычно состоит в том, чтобы (1) выделить лучшее в изображении, чтобы оно выглядело «лучше» (это полностью субъективно!) И (2) в некоторых случаях фактически подтолкнуть изображение еще дальше, чтобы оно выглядело «лучше». на определенный «взгляд» (каким бы неестественным он ни был) в творческих целях.
Для предстоящего проекта, созданного с использованием библиотеки C ++ для творческого кодирования openFrameworks, я поставил перед собой задачу добиться некоторых интересных эффектов оценки в реальном времени с высоким разрешением и частотой кадров 60 кадров в секунду.
Цветовая коррекция в Photoshop с помощью кривых
Один из наиболее распространенных методов достижения градаций цвета, особенно на неподвижных изображениях, - это независимая настройка кривых RGB. Photoshop предоставляет удобный интуитивно понятный инструмент для этого:

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

Чтобы добиться этого в Photoshop, обычно применяют немного разные кривые к каждому из трех цветовых каналов.
Так, например, на камеру можно было получить действительно скучное «плоское» изображение:

А с творческой настройкой красных, зеленых и синих кривых можно добиться более интересного вида:

Это удобный способ работы с фотографией, в котором используются инструменты, знакомые дизайнерам, редакторам и цифровым фотографам.
Поэтому я подумал, могу ли я «взглянуть», разработанный в Photoshop, и применить точно так же к живому видеоизображению в интерактивном приложении?
Вам нужна мощность графического процессора
Такие фреймворки, как openFrameworks (и подобные им, например, Processing) предоставляют вам набор инструментов для чтения и управления пикселями. Проблема в том, что такого рода чтение, вычисление и запись (манипулируемых пиксельных данных) все выполняется на ЦП, а это означает, что его почти никогда не бывает достаточно быстро, чтобы делать в реальном времени движущееся изображение.
В моем случае мне пришлось взять канал с камеры USB 3.0 Basler Ace, которая имела более высокое разрешение, чем Full HD, и работала со скоростью 60 кадров в секунду. Более того, мне приходилось выполнять всевозможные преобразования (неискажение) и фильтрацию в реальном времени и выводить их на дисплей 4K (3840x2160). И все это при сохранении прекрасных плавных 60 кадров в секунду.
Даже с быстрым процессором (четырехъядерный i7 с тактовой частотой 4,0 ГГц) это не сработало. Так что я знал, что мне нужно будет воспользоваться выделенным графическим процессором для применения этих эффектов. А в моем случае для этого требуется программирование шейдеров OpenGL!
Если вам интересно узнать больше, я настоятельно рекомендую LearnOpenGL, в котором подробно рассказывается о создании графики с использованием OpenGL (и, в частности, о программировании шейдеров).
Карта - это не территория
При применении «цветовой градации» к изображению нам нужна карта - что-то, что сообщает нашему шейдеру графического процессора, как сдвинуть красный, зеленый и синий компоненты каждого пикселя в source таким образом, что соответствующие пиксели в выходном изображении смещаются так же, как если бы мы применили кривую, которую мы разработали в Photoshop.
Давайте представим наихудший способ сделать это: экспортировать кривую из Photoshop и каким-то образом интерпретировать проприетарный двоичный файл .acv в некую формулу (хранятся ли в нем контрольные точки для кривой?) И применить эту уникальную формулу к каждому пикселю. Не очень практично.
Но давайте посмотрим на это по-другому. В изображении с 8-битными каналами (что обычно) существует только 256 возможных значений красного, зеленого и синего для каждого пикселя. Таким образом, вы также можете представить себе, как взять кривую Photoshop для каждого цветового канала, разделить ее на 256 столбцов, а затем прочитать «высоту» кривой как значение от 0 до 255 для каждого столбца. Во всяком случае, разве не этим занимается Photoshop?

Теперь представьте, что превратите это в 3 большие таблицы для красного, зеленого и синего, каждая ровно 256 столбцов в длину и только 1 строку в высоту, со всеми целевыми (т. Е. «Переназначенными») значениями для каждого заданного входного значения. Это выглядело бы примерно так:

Это чрезвычайно полезная таблица. Вам понадобится 3 из них - по одному для красного, зеленого и синего - при условии, что у вас есть своя кривая для каждого канала.
Но не только таблицы содержат строки и столбцы! А как насчет самих изображений? По сути, цифровое представление изображения - это просто таблица значений «яркости», где строки представляют положение x, а столбцы представляют y позиция. Так почему бы не превратить каждую таблицу в изображение размером 256x1, по одному для каждого красного, зеленого и синего каналов. Это может выглядеть так:

И поскольку мы можем кодировать значения красного, зеленого и синего цветов в каждый пиксель, почему бы просто не объединить информацию в одно изображение? Затем мы можем просто прочитать информацию о «отображении» для каждого канала на любом пикселе:

Готовы выполнить переназначение ... Для любого пикселя из входного изображения мы просто считываем его значения красного, зеленого и синего, затем переходим в нашу таблицу поиска (которая на самом деле является изображением) и ищем соответствующую позицию по оси x, чтобы найти, какое значение он должен быть сопоставлен.
Например, если у нас есть пиксель [R: 213, G: 100, B: 97], мы могли бы использовать изображение выше, чтобы найти позицию 213 (координаты в нашем изображении высотой 1 пиксель будут просто [x: 213, y: 0], и мы прочитаем найденный там пиксель: [R: 200, G: 213, B: 210]. Это означает, что новый (переназначенный) пиксель должен иметь значение красного цвета. из 200. Сделайте это для каждого пикселя изображения, и у вас будет применена кривая RGB.К счастью, графические процессоры действительно хорошо справляются с такими вычислениями очень быстро.
Но ваш следующий вопрос, вероятно ...
Но как создать рампу?
И вот в чем хитрость!
Просто создайте прямоугольник с градиентом 256x1 (вы можете использовать Sketch, Photoshop, что угодно) и поместите его в изображение, которое хотите использовать, чтобы проверить «внешний вид» цветовой кривой. Теперь примените настройки кривой , чтобы кривая повлияла на изображение и наклон. (Я бы порекомендовал использовать корректирующий слой наверху, поскольку он применяет фильтр неразрушающим образом ко всем слоям, находящимся под ним.)

В конце этого процесса будет экспортироваться только часть изображения размером 256x1, содержащая рампу. Я позволю вам найти эффективные способы достижения этой цели самостоятельно.
Этот метод позволяет вам поиграть с разными цветовыми градациями на реальных изображениях в знакомом инструменте (Photoshop) и создать «справочную таблицу» в виде изображения, что позволит шейдеру графического процессора воспроизводить точно такую же градацию. на любом целевом изображении.
Красиво, да?
Пример реализации в OpenFrameworks
Я не совсем понял, как загрузить рампу изображения в качестве вторичной одномерной текстуры в шейдер (пока), поэтому я выполняю небольшую разовую обработку на стороне процессора, чтобы извлечь значения поиска из изображения рампы. Это нормально, потому что это происходит только при загрузке рампы, а не в каждом кадре:
ofPixels curvesPix = lutRampsImages[newIndex].getPixels();
for (int i = 0; i <= 255; i++) {
ofFloatColor c = curvesPix.getColor(i, 0);
redLUT[i] = c.r;
greenLUT[i] = c.g;
blueLUT[i] = c.b;
}
Затем я загружаю их в свой шейдер - в моем случае я применяю его к текстуре, поступающей из камеры:
videoFbo.begin();
cam.begin();
camTex.bind();
curveRGBshader.begin();
curveRGBshader.setUniform1fv("red", &redLUT[0], 256);
curveRGBshader.setUniform1fv("green", &greenLUT[0], 256);
curveRGBshader.setUniform1fv("blue", &blueLUT[0], 256);
plane.draw();
curveRGBshader.end();
cam.end();
camTex.unbind();
videoFbo.end();
Вот фрагментный шейдер, который отвечает за настоящую магию:
#version 410
// receive the main texture
uniform sampler2DRect mainImage;
// receive the LUT arrays
uniform float red [256];
uniform float green [256];
uniform float blue [256];
in vec2 varyingtexcoord; // from vertex shader
out vec4 outputColor;
void main() {
vec4 InColor = texture(mainImage, varyingtexcoord);
vec4 OutColor;
OutColor.r = red[int(InColor.r*255)];
OutColor.g = green[int(InColor.g*255)];
OutColor.b = blue[int(InColor.b*255)];
OutColor.a = 1.0;
outputColor = OutColor;
}
Посмотрите, как значение канала (например, InColor.r) используется в качестве индекса для массива для поиска соответствующего целевого значения (например, red [int (InColor.r * 255)]).
Простой вершинный шейдер необходим только для того, чтобы выровнять текстуру с плоскостью, на которой мы рисуем:
#version 410
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat4 textureMatrix;
uniform mat4 modelViewProjectionMatrix;
in vec4 position;
in vec4 color;
in vec4 normal;
in vec2 texcoord;
out vec2 varyingtexcoord;
void main()
{
// move the texture coordinates
varyingtexcoord = vec2(texcoord.x, texcoord.y);
// send the vertices to the fragment shader
gl_Position = modelViewProjectionMatrix * position;
}
И это все!