Сегодня я не буду подробно описывать как с помощью Metal Framework и Metal Shading Language запрограммировать фильтр. Т.е. не буду останавливаться на деталях реализации. В целом, предыдущего поста должно было быть достаточно для уверенного понимания как варится программа с использованием этих средств.
Сегодня мы сосредоточимся на идее нового фильтра с точки зрения разработчика этого фильтра (инженера или математика) и покажем как от идеи до воплощения в коде заставить железку работать на благо, например, эстетики. Подумаем как сделать изображение более «привлекательным» в некоторых, специальных случаях.
В общем начнем простыми и местами научными методами учить наш iPhone или iPad быстро и качественно корректировать тени несколько недоэкспонированных или высококонтрастных фотографий и, возможно, потерявших непростительно много деталей в тенях. Предлагаемый способ можно назвать универсальным алгоритмом коррекции теней произвольных изображений, в отличие, скажем, от версии Adobe реализованной в инструменте Shadows/Highlights алгоритм, который я опишу сегодня, нельзя воспроизвести средствами фоторедакторов, по крайней мере пока.
Этот алгоритм используется как часть одного большого фильтра изображений в новой версии фото-приложения Degradr. И надо сказать существенно влияет на конечный результат всей работы.
Если по чесноку, и вот без этого кривляния, основная цель сегодняшнего поста показать как легко можно сосредоточиться на идее и не тратить время на прикладную часть: сбивании пальцев рук в ацкие мозоли при набивании текста программы фильтра. Metal Framework и Metal Shading Language — это все, что вам нужно для реализации вашей идеи. Сосредоточьтесь на главном: на реализации.
Идея
Основная задача при коррекции теней изображения состоит в «вытягивании» пропавших деталей в сильно контрастных изображениях. При этом средние тона и светлоты не должны участвовать в осветлении.
Возьмем исходную картинку создадим две копии, сложим эти копии в режиме смешения Screen, который как известно осветляет изображение в целом. По сути, это будет гамма-коррекция со значением гаммы 2, примененной к инвертированной версии исходной картинки и затем восстановленной из негатива.
— функция смешивания слоев яркостных каналов изображения,
где — функция смешивание в режиме Screen
Выглядеть такая штука (смешивание изображения с самим собой) будет так:
На графике хорошо видно, что это частный случай функции гамма-коррекции. Или как если бы мы применили инструмент «Кривые» к изображению в Photoshop-е аккуратно притянув сплайны к нужным точкам. Но кривые, и сплайны — это слишком грубая работа для пытливого ума. Не наш, в общем метод…
Нетрудно догадаться, что функцию можно применить дважды и получить еще более высветляющую кривую:
Вот эту функцию, которая более, и возьмем за основу «высветляющей» части фильтра — это будет гамма коррекция негатива изображения с гаммой равной 4 и последующим «отпечатком» в исходный позитив (поэтому я назвал коррекцию «пленочной» — работаем с «негативом»):
Однако полученное изображение будет светлым по всему полю, а нам нужно только в тенях. Чтобы избежать лишнего осветления применим к результату свертку функций смешивания исходного изображения и полученного в нормальном режиме. Где свертка будет функцией смешивания в альфа канале с усилением значения яркостного канала в тенях и ослаблением смешивания в светах. Свертку можно придумать и будет она примерно такой:
Свертку можно придумать любую, вы можете свою, даже может быть еще лучше. Но смысл должен быть тут ясен: нам нужна плавная монотонно-убывающая функция. Хотя подойдет и просто линейная, но работать она будет хуже. При желании, вы сможете это проверить после сборки проекта, заменив мою версию на свою.
Напомню как смешиваются каналы в нормальном режиме с учетом альфа-композиции двух исходных изображений:
Или результирующее выражение свертки для нашего случая:
Вес осветленного изображения в зависимости от значения яркостного канала исходного изображения подобрать несложно:
,
- где — представление изображения в цветовом пространстве RGB для utput,nput, creen соответственно
— яркостный канал исходного изображения
— яркостный канал осветленного изображения
— вес осветленного изображения
— коэффициент наклона свертки
— тональная ширина теней, т.е. на сколько захватываем в яркостном канале тени
Небольшой update по следам дискуссии
Результирующая функция-свертка от функции гамма-коррекции инвертированного изображения и веса альфа канала получается такой (синяя кривая — поправили, исправляю: «магическая некривая линия коррекции теней»):
Куём математику в железо: проверяем гипотезу
// // IMPFilter.metal // ImageMetalling-00 // // Created by denis svinarchuk on 27.10.15. // Copyright © 2015 ImageMetalling. All rights reserved. // #include <metal_stdlib> #include <simd/simd.h> using namespace metal; inline float4 blendNormal(float4 c2, float4 c1) { // // from: https://github.com/BradLarson/GPUImage // float4 outputColor; float a = c1.a + c2.a * (1.0 - c1.a); float alphaDivisor = a + step(a, 0.0); outputColor.r = (c1.r * c1.a + c2.r * c2.a * (1.0 - c1.a))/alphaDivisor; outputColor.g = (c1.g * c1.a + c2.g * c2.a * (1.0 - c1.a))/alphaDivisor; outputColor.b = (c1.b * c1.a + c2.b * c2.a * (1.0 - c1.a))/alphaDivisor; outputColor.a = a; return clamp(outputColor, float4(0.0), float4(1.0)); } typedef struct{ packed_float4 shadows; // [level, weight, tonal width, slop] } IMPShadows; // // Прямое переложение функции расчета веса светов в якростном канале сигнала // inline float luminance_weight(float Li, float W, float Wt, float Ks){ return W / exp( 6 * Ks * Li / Wt) * (Wt * 0.5); } // // РЕзультирующая функция коррекции теней // inline float4 adjustShadows(float4 source, constant IMPShadows &adjustment) { float3 rgb = source.rgb; // // выучите эту строчку наизусть, используется почти везде // можно запомнить как 3/6/1 // // почитать можно тут: https://en.wikipedia.org/wiki/Relative_luminance // исходная формула относительной яркости в колорометрии: // Y = 0.2126 R + 0.7152 G + 0.0722 B // но мы работаем не с колорметрически измеренным значением RGB, а с представлением // rgb в виде sRGB цветового пространства. Так случилось, что быстрое преобразование: // L(rgb)= (r,g,b)(0.299, 0.587, 0.114)', для наших целей подходит лучше // и подтверждается рядом экспериментов с большим набором изображений. // float luminance = dot(rgb, float3(0.299, 0.587, 0.114)); // // Распаковываем выходной буфер, прилетевший из памяти приложения в память GPU // подразумеваем: // 1. x - уровень воздействия фильтра // 2. y - коэффициент нормализации фильтра (по умолчанию = 1 и мы его не трогаем) // 3. z - тональная ширина охвата фильтра, т.е. насколько далеко мы восстанавливаем тени от черной точки // 4. w - коэффициент наклона (slope) кривой фильтра, т.е. скорость сниения воздействия в зависимости от // яркости // float4 shadows(adjustment.shadows); float weight = luminance_weight(luminance, shadows.y, shadows.z, shadows.w); // // Альфа канал - функция уровня воздействия фильтра и вес от яркости // float a(shadows.x * weight); // // Функция смешивания в режиме screen 2 раза или // гаммакорекция негатива с гаммой == 4 // float3 c(1.0 - pow((1.0 - rgb),4)); // // Результат смешиваем в нормальном режиме с учетом композиции в альфа канале // return blendNormal (source, float4 (c , a)); } kernel void kernel_adjustSHL( texture2d<float, access::sample> inTexture [[texture(0)]], texture2d<float, access::write> outTexture [[texture(1)]], constant IMPShadows &adjustment [[buffer(0)]], uint2 gid [[thread_position_in_grid]] ) { float4 inColor = inTexture.read(gid); outTexture.write(adjustShadows(inColor, adjustment), gid); }
Весь остальной код, с некоторыми изменениями, копируем из предыдущего проекта. В итоге получаем: аппликуху магически «улучшающую» любые фотографии. Скачать исходный код можно поссылке. Cобирать проект в папке: ImageMetalling-01.
Добавлю небольшие пояснения по коду MSL. В нем можно увидеть вот такой кусок:
typedef struct{ packed_float4 shadows; // [level, weight, tonal width, slop] } IMPShadows; ... constant IMPShadows &adjustment [[buffer(0)]], ....
Он означает, что мы передаем в память GPU не готовую структуру данных, а упакованную, т.е. без побайтного выравнивания. Разработчики Metal позаботились о возможности упаковать произвольную структуру данных в основном коде приложения и передать её в программу исполняемую на GPU. Поскольку мы не знаем о природе выравнивания данных в памяти GPU, мы должны использовать такую нотацию для многомерных структур. Очевидно, что в нашем случаем можно сконструировать свою структуру для параметризации функции фильтра, но помня о главном принципе программиста: не плодить лишних сущностей, использовали готовый вектор класса float4 из комплекта Metal Framework.
В проект можно подгрузить любую свою фотографию, можно даже переделать код для чтения изображений из Camera Roll и запись обратно. Я украл взял фотоку Павла Косенко с каким-то экстремальным случаем теней, и поэкспериментировал с ней:
Бинго! Работает. Можно нас поздравить: наша теория работает в железе, и скорее всего она верна.
Что еще можно сказать?..
Совсем не трудно догадаться, что операция затемнения светов строго симметрична осветлению теней. Единственное отличие, которое можно себе вообразить: нужно инвертировать свертку и смешивать изображения в режиме Multiply.
Notice (отчего-то спрашивают): По тексту, когда мы говорим об изображении, всегда имеется в виду один пиксел. Мы продолжаем помнить о параллелизме вычислений: функция фильтра вычисляется для каждого пиксела отдельно и сразу. Я думаю, вы привыкните к этому скоро, если еще не имели дело с вычислениями на графических сопроцессорах.
Если заметили ашипку, если написал глупость, если что-то не понятно: комментируйте или пишите на: imagemetalling [*] gmail.com .
Хоть и в кавычках, но все же почему «пленочной»?
НравитсяНравится
Назвать можно как угодно, в том числе и выбрать слово для привлечения внимания.
Почему «пленочной» отчасти объясняется в самом посте:
1. работаем с «негативом», т.е. с инверсией изображения
2. если изучить как работает эмульсия негатива, то можно заметить, что этот нелинейный процесс работает несколько отлично от процесса цифрового, особенно в тенях, которые на самом деле света. И в этом примере, отчасти конечно, этот процесс воспроизводится. Модель исключительно эмпирическая и нестрогая, но пусть уж называется как назвал.
НравитсяНравится 1 человек
Если для привлечение вниманиея, то да, понятно.
НравитсяНравится
Павел перенаправил сюда, поэтому спрошу и здесь. Чем отличается (принципиально) такой подход от попытки смешать два слоя (осветленный и оригинальный) по маске теней в фотошопе (кроме того, что я не могу там маску экспонентой задать)?
НравитсяНравится
Принципиально тут нечего сравнивать — это абсолютно два разных решения, хоть и одной и той же задачи. Смешивание слоев в фотошопе композиция двух сигналов с учетом альфа канала и все. Причем второй (осветляющий сигнал нужно еще нарисовать фактически).
В этой модели композиция используется только для того, что бы показать воздействие алгоритма.
Ядро алгоритма работает со сверткой двух функций: сигнала исходного слоя и характеристической кривой «негатива». Эту тему я развернуто не объяснил в посте (можно углубиться слишком далеко в теоретический базис), тут просто нужно понимать, что почти все вещи природного характера подчиняются в основном нормальным распределениям, и свертка конструируется здесь через одну из форм гауссовых кривых (видимо вы подразумевали по этим экспоненту). Поэтому, если внимательно посмотреть на решение то и смешивать будет не нужно по большому счету: конечная функция работает в рамках вычисления свертки.
В фотошопе же мы можем фактически только нарисовать маску или несколько масок, либо автоматически либо в ручную, и какой-то осветляющий слой: кривые, смешивание и т.п.
В предлагаемом решение происходит эмуляция процессов экспонирования и проявки негативной пленки.
Используя оба подхода, можно получить абсолютно идентичный результат. Только в фотошопе придется доводить руками и смотреть глазами, поскольку, в лоб, без обратного восприятия задачу средствами фотошопа не решить — придется подгонять результат под желаемый. Тут же предлагается некая универсальная модель работающая почти всегда: ничего не надо смотреть глазами, просто приложить фильтр.
Ну и совсем принципиально: фотошоп-версия штука дико накладная для вычислений даже с распараллеливанием на GPU. Предлагаемый алгоритм решает задачу более элегантно, как алгоритмически так и программно: можно, к примеру, легко обрабатывать видео поток на лету.
НравитсяНравится 1 человек
Ден имел в виду, что этот алгоритм работает более перцептуально (в соответствии с восприятием человека), то есть в той или иной стпени близок к аналоговым процессам.
НравитсяНравится 1 человек
Насколько я понимаю, яркость результирующей точки зависит исключительно от яркости исходной. Раз так, то это значит, что то, что вы изобрели, — это банальная кривая, несколько приподнятая в тенях и переходящая в прямую у=х ближе к светам. Уж по каким формулам она приподнята — другой вопрос. В вычислительном смысле такое преобразование делается в ОДНО ДЕЙСТВИЕ по таблице, расчет которой делается заранее при ничтожных затратах.
НравитсяНравится
Сергей, вы абсолютно правы: результирующая функция может быть преобразованием почти в одной действие. Более, того я прямым текстом отсылаю к кривым, которыми эта функция может быть реализована, точнее осветляющая, её часть.
Но целью этого поста было не показать изобретение, которого здесь нет, а как использовать новое инструментальное средство Apple под названием Metal Framework и Metal Shading Language для реализации конкретной функции.
Как сделать почти в одно действие данное преобразование: я планирую показать в следующем посте именно для фильтра из этого поста. Вы вероятно с этим приемом хорошо знакомы и, скорее всего, вам этот пост будет не интересен.
Но я, пожалуй, категорически не соглашусь с утверждением «…какими формулами она будет поднята — другой вопрос…».
Очевидно, что предметом изучения и практическим результатом исследований в конкретной прикладной области — обработке цвета (света) фотографий, является аналитический, эмпирический или доказательный способы поиска функций отображения одного цветового (светового) пространства изображения. В данном примере используется один из миллиона случаев, который удовлетворяет конкретно моей цели. Вы можете предложить свой вариант, и он, несомненно будет точно таким же решением ровно той же задачи.
Кстати, будет весьма интересно увидеть предложенное решение и подискутировать, на эту тему.
И спасибо за проявленный интерес!
НравитсяНравится 1 человек
Кривые они и есть кривые. Это по поводу пожалуй, «категорически не соглашусь с утверждением Никаких находок по части обработки цвета тут не предвидится. Так, в
В Вашем случае поднятие теней приведет к потере контрастности в средних тонах.
Еще мне кажется, что то, что Вы называете сверткой, таковой не является:
https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D1%91%D1%80%D1%82%D0%BA%D0%B0_(%D0%BC%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9_%D0%B0%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7)
А то, что предлагаете выучить наизусть:
// выучите эту строчку наизусть, используется почти везде
//
float luminance = dot(rgb, float3(0.299, 0.587, 0.114));
это неправильный способ вычисления светлоты.
Упражнения с Metal любопытны.
НравитсяНравится 1 человек
Сорри, что обозвал гауссиану… Но все таки:
blendNormal (source, float4 (c , a)) — это и есть «композиция двух сигналов с учетом альфа канала…», это возвращаясь к цитате про фотошоп. Отдельная задача в фотошопе получить оствеляющий сигнал (достаточно просто) и маску(немного сожнее).
Немного туплю (точнее не хватает бекграунда в фунциональном анализе), но причем тут свертка двух функций? Ну, в смысле, что я знаю что такое свертка функций в контексте конструирования 1D/2D фильтров, но тут что-то совсем не понял.
НравитсяНравится
Никаких противоречий в своем утверждении не вижу.
Форма кривых определяет результат, поиск формы кривой: цель подобных задач. На мой взгляд — это самоочевидно, вопрос именно в форме. В конкретном примере, форма вот такая. Доказательством этого простого утверждения, является бесчисленное количество методик обработки фотографий с помощью кривых, бесчисленное количество LUT-для постпродакшена фильмов, бесчисленное количество публикаций обработки медицинских снимков и т.п. — это все база поиска условно «хорошей» кривой для каких-то целей. В развлекательных целях можно найти еще с пару 10ков пока не надоест. То, что это будут какие-то кривые — не предмет дискуссии. Очевидно, это будут кривые.
Не думал, что часть про свертку может вызвать недопонимание, поскольку является, достаточно стереотипным подходом: в данном случае функция f=Lo, g=W(Li), сверткой будет результирующая функция от этих двух (если так удобнее воспринимать), которую вот в Metal и выливаем. Эта свертка удовлетворяет всем основным свойствам: коммутативности, линейности и т.п.
Степень воздействия на контраст в средних можно уменьшить сужая ядро W(Li): ws. Задачу «не снижать» контраст в средних этот вариант фильтра не решает. И я бы вообще, с точки зрения «гармонизации изображения», разделял эти фильтры.
НравитсяНравится 1 человек
Прошу прощения, что таки немного не по теме, но для f=Lo, g=W(Li) — сверткой вроде как должно быть что-то в виде интеграла/суммы: SUM(f(x-y)*g(y))?
НравитсяНравится
Не совсем так — это частный случай для интегрируемых функций. Свертка — это любая функция от двух других, удовлетворяющая свойствам коммутативности, ассоциативности, линейности и непрерывности на отрезках непрерывных для исходных функций. В приведенном примере из поста — это простая свертка последовательностей.
НравитсяНравится 1 человек
Если следовать последнему вашему определению, тогда ясно. Но такого определения я нигде не нашел. Во всех источниках — интеграл/сумма. А у нас по факту просто произведение двух функций. Что тоже, как я понимаю, можно называть сверткой?
НравитсяНравится
Не совсем — это не произведение двух функций, это функция произведений двух последовательностей, которая является функцией двух исходных в строгом смысле. Эту цепочку я опустил при описании, алгоритма. Иначе пришлось бы статью увеличить раза в 3:) Поскольку нужно раскрыть, что произведение двух последовательностей должно быть произведением двух одно размерных или дополненных последовательностей, произведения последовательностей производятся в симметрично отображенных индексах последовательностей. Потом раскрыть, что мы делаем трюк — изначально переворачивая гауссиан и отрезаем от него отрицательную часть, и что расчет ведется в дискретной области, а не непрерывной и поэтому мы можем рассматривать свертку последовательностей и т.п.
Вот всю это чепуху я опустил и сразу приступил к выводам. Пока вижу, что вызвало больше вопросов.
НравитсяНравится
Круть. На пальцах, не вдаваясь в теоретические подробности, казалось бы, все просто: перемножили две картинки и добавили к исходной картике… Но по сути, вся эта теория применима и к смешиванию слоев в фотошопе для normal blending.
НравитсяНравится
Абсолютно. Тут главное не стесняться насиловать и математику и железо и иногда здравый смысл;)
Рад, что вам понравилось — это мотивирует продолжать бложить на эту тему.
НравитсяНравится
Про свертку понял — не достаточно раскрыл тему, постараюсь исправиться в следующих постах, раз тема вызывает вопросы. Добавил в текст статьи картинку с результирующей кривой.
НравитсяНравится 1 человек
Нет, на самом деле, если есть возможность, то здорово было использовать «глубокую» математизацию там, где без этого иначе нельзя. В данном конкретном выражении вполне можно было обойтись и достаточно простой формой: Y(x,y) = A(x,y) + B(x,y) * M(x,y). Во-первых, интерпретация такого выражения для «фотографа» достаточно проста: A — исходное изображение, B — осветленная его версия, M — маска воздействия. Опять таки, в данном случае становится совершенно ясной аналогия с использованием слоев в фотошопе, но с какими-то ограничениями или недостатками. Во-вторых, это никак не противоречит цели статьи — пояснить принципы построения фильтров для выполнения на железе. Это мое субъективное мнение.
Еще хотел бы добавить про аналогии с «пленочностью» при обработке цифровых изображений. Это весьма тонкая материя. Часто субъективная. Неплохо было бы как-то прийти к общему знаменателю с аналоговыми процессами при проявке/печати. Таким знаменателем, например, может быть общая система координат. Если проводить аналогию с пленкой, то неплохо было использовать логарифмическую систему координат — привычную систему для ХК пленок.
Спасибо за ваши ответы и полезную, по крайней мере для мнея, дискуссию.
НравитсяНравится
Не считаю пленку тонкой материей — это такой же предмет выбора одного из решений, причем достаточной ограниченный и чем дальше тем труднее нам его сделать — эпоха закончилась. В наших руках более мощный инструментарий. Надо просто привести в порядок цифровую эстетику — приблизить её к пленочной.
По поводу реализации, конечно можно и так решить. Это решение очевидное. Однако хотелось показать как можно использовать смешивание как функцию другой. Раз.
А два, если немного заглянуть вперед, мы можем написать некую библиотеку фильтров одним из общих параметров, которых, будет выбор режима смешивания, тогда уже не важно какой режим мы выбираем: нормальный, света, перекрытия и т.п. — мы мыслим в рамках функции альфа канала, а режим выбираем как параметр фильтра: так проще организовать цепочки их применения, без создания дополнительных специальных смешивающих фильтров, как, к примеру это организовано в Core Image или GPUimage (это просто лишние текстуры и накладные расходы на обмен в памяти GPU/CPU. В этом есть некоторое преимущество перед мышлением в терминах маски.
Но это уже не относится к теме совсем. Тут можно и так и так поступать — не принципиально.
НравитсяНравится
С результирующей кривой недоразумение. Надо к ней еще x прибавить, то есть сложить с графиком y=x — диагональю
НравитсяНравится
Не надо — это функция используется для вычисления веса альфа канала для композиционного смешивания — это тоже отмечено в посте. Сложение будет другой функцией, в конкретном случае нам не интересной.
НравитсяНравится
Тогда не называйте это «кривой», поскольку в обсуждаемом контексте «кривую» понимают, скорее, в «фотошоповском» смысле. И не показывайте на графике, где по оси абсцисс input, а по оси ординат output.
НравитсяНравится
Сергей, как вам угодно. Давайте называть кривую некривой. Мне нравится: «магическая некривая линия осветления теней». Подходит? Правда, я немного теряюсь мы сейчас про какую из функций?
НравитсяНравится 1 человек
Да мне наплевать, если честно. Но стоит как-то поточнее формулировать. И поменьше магии.
НравитсяНравится
Сергей, мне хотелось, что бы вы расслабились. Вы очень серьезно восприняли совершенно неформальный пост. Быть предельно точным я в этом блоге не собираюсь, и в одном из первых постов честно предупредил: обращаться с формулировками и формулами буду несколько вольно.
И да, плевать в чужом блоге, все же несколько не воспитанно. За дискуссию в целом спасибо.
Почему яркостный канал в этом примере и других считается не по колорометрическому вектору: 0.2126, 0.7152, 0.0722, а по линейному, объясню чуть позже (критиковали чуть выше), хотя это не сильно принципиально. В первом ~ так: 3/6/1 запомнить проще и лежит все в пределах допустимого: 1. перцептуальной погрешности, 2. разброса калибровок мониторов.
НравитсяНравится