Самый лучший селективный HSV-adjustment фильтр ever.

Мы не заблудились, мы просто не знаем где мы находимся.
(South Park)

Наверное не будет преувеличением утверждение: каждый фотограф или человек занимающийся фотографией хоть раз в жизни использовал инструмент называющийся «Hue/Saturation» или вроде того. С этим весьма популярным инструментом есть одна интересная история: в природе не существует более-менее адекватной реализации доступной в исходных кодах. Каждый разработчик либо конструирует все самостоятельно, либо использует готовый код из свободного Gimp либо городит безумные конструкции вокруг расширений ImageMagic. Те же, кто знают тайну, попросту жмутся, не делятся исходниками или же выкладывают в сеть что-то вроде пресловутого Gimp. Пробовали с ним работать? Вот то-то и оно.

А давайте уже нарушим эту монополию, сорвем покровы с реализации этого инструмента в виде Metal-апического кода, но не под iOS, а, например, под OSX. Так удобнее крутить параметры слайдерами и видеть изменения в реальном времени. А еще будет удобно рисовать гистограмму и наблюдать как меняется представление картинки в «частотном домене».

Одно из полезных свойств инструмента Hue/Saturation в Photoshope, к примеру (да и во всех семействах редакторов Adobe) — это возможность селективного изменение выбранного диапазона цветов. Строго говоря это главная особенность этого инструмента: выбрать цвета, изменить оттенок этих цветов, яркость и насыщенность. По сути это основная история при работе с цветом. Без нее любая реализация такого инструмента не имеет смысла. Поэтому разберем на практике как эту возможность повторить в виде кода и возможно чуть улучшить вариант используемый в продуктах Adobe. В итоге мы получим готовое приложение с возможностью цветокоррекции произвольных изображений с отображением результата в режиме реального времени при коррекции параметров фильтра и возможностью сохранения результата в файл формата jpeg.

ImageMetalling-07-App
Приложение использующее фильтр селективной коррекции цветов изображения

И да, написать это приложение теперь можно за 20-30 минут с тестированием и отладкой кода.

HSV против HSL

На самом деле никакого противостояния нет. Adobe в своем инструменте Hue/Saturation использует цветовое пространство HSL, мы будем использовать HSV, по двум тривиальным причинам: 1. преобразование RGB<->HSV быстрее; 2. HSL позволяет вытянуть светлоты изображения до белой точки, а нам этого не нужно, более того, мне представляется это избыточным и «нефотографичным».

Что такое «селективное» изменение цветов

На расшифровке селективности в контексте этого поста надо немного остановиться, чтобы организовать некоторое терминологическое поле для работы с кодом.

В целом, никто не мешает разработчику взять произвольное цветовое пространство, произвольный набор цветов и изменить отображение исходного набора цветов в новое. Например так: CLUT-ы в Metal… Или взять кривые и поканально в пространстве RGB произвести цветокоррекцию. Или произвести пересчет карты в пространстве CIELAB. Или придумать свое пространство и манипулировать цветами в нем. Все это будет «селективно» в какой-то степени. Но загадочно для подавляющего количества конечных пользователей.

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

В нашем примере мы будем работать с традиционным набором из 6-ти оттенков цветового круга: Красными, Желтыми, Зелеными, Голубыми, Синими и Пурпурными ( Reds, Yellows, Greens, Cyans, Blues, Magentas: RGB, CMY). Количество оттенков можно расширить, если по каким либо соображениями 6-ти будет не достаточно (но на самом деле достаточно).

Таким образом под селективным фильтром HSV мы будем понимать действие по изменению цветовой карты изображения только в выбранном диапазоне тонов из цветового круга пространства HSV.

hsv-selective
Селективная замена красных оттенков зелеными.

«Перекрытие» цветовых оттенков

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

hsv-test-gimp
Артефакты рефлексов на глянцевом белом покрытии при замене красных цветов при использовании резкой функции перекрытия. (Gimp)

 

Что-бы снизить вероятность появления таких искажений преобразования неплохо бы ввести дополнительную функцию смешивания значений пикселов в зависимости от их тонального веса в конкретной области сектора круга HSV.

gimp-overlap-weights.jpg
Так учитывает Gimp тональный вес оттенка при смешивании цветов селективной коррекции в HSV
photoshop-overlap-weights
Adobe Photoshop смешивает цвета похожих оттенков в таких пропорциях

Как видно, в функциях перекрытия цветов популярных фоторедакторов есть резкие границы. Достаточно очевидно, что в «фотографическом» смысле работа фильтра в пограничных тональных областях будет иметь резкие переходы. Избавиться от этого достаточно просто: заменив функцию на плавную. Первое, что приходит на ум, использовать один из видов нормального распределения. Его, в нашем варианте селективного Hue/Saturation, и реализуем.

imp-overlap-weights
Реализация функции перекрытия тональных оттенков в реализации IMProcessing Framework

 

Код утилиты для вычисления веса перекрытия цвета в ядре Metal может выглядеть так:


inline float gaus_distribution(float x, float fi, float mu,  float sigma){
    return fi * exp(- pow( (x-mu),2.0) / (2.0 * pow(sigma,2.0)));
}

inline float normal_distribution(float x, float mu, float sigma, float denom, float4 ramp){
	float fi = 1.0/(sigma*sqrt(2.0*3.1415926));
	float maxval = gaus_distribution(mu,fi,mu,sigma);
	float mux = mix(mu, mu+1.0, clamp(sign(x*360.0-ramp.w*2.0),0.0,1.0));
    return clamp(gaus_distribution(x,fi,mux,sigma)/maxval/denom,0.0,1.0);
}

inline float overlapWeight(float x, float4 ramp){

    const float te    = 1.0/360.0;
    const float width = 1.0;

    float where = clamp(sign(ramp.w-ramp.x), 0.0, 1.0);

    float denom = te*width;
    float sigma = mix((ramp.y-360.0-ramp.z)*denom, (ramp.z-ramp.y)*denom, where);
    highpfloat mu    = mix((ramp.x-360.0+ramp.w)*0.5, (ramp.w+ramp.x)*0.5, where);

    return hue_normal_distribution(x*te, mu*te, sigma, 1.0, ramp);
}

Практическую пользу такого подхода можно продемонстрировать сравнив результат применения похожих «экстремальных» сдвигов в оригинальном варианте и в варианте Photoshop:

HSV-comparsion (1)
Как видно плавная функция, как и ожидалось дает более плавное смешивание перекрытия близких цветов.

Совершенно точно никто не использует таким образом этот инструмент в Photoshop-е на практике. Тем не менее, и тут даже обсуждать нечего, всегда неплохо иметь бОльший диапазон инструментального воздействия на изображение.

Проблемы производительности фильтра Hue/Saturation

Из всех популярных гомогенных операций селективное HSV преобразование наверное самое медленное. Как нетрудно видеть, параметры преобразования задаются для каждого из секторов. Получается, что при выполнении ядра фильтра на GPU, нам как минимум придется пробежаться по всем секторам во всех каналах HSV, т.е. 18 раз на селективные цвета + 3 раза на каждый канал для мастер-цвета (неселективное преобразование). Что эквивалентно производительности видео/GPU тракта ~2Gb/s для частоты обновления 60FPS. Это несущественно для постобработки изображений на современных CPU/GPU, но в целом, все еще дорого для обработки видео в реальном времени на смартфонах. Поэтому прежде чем приступить к реализации рассмотрим варианты предварительной алгоритмической оптимизации.

Для начала видно, что вычисление весов перекрытий выглядит весьма затратной операцией. Как водится дорогостоящий расчет заменим интерполяцией через предрасчитанные значения размещенные в массиве одномерных текстур:

   ///  @brief Получить семплированное значение веса из кривых весов перекрытия близких цветов круга HSV
    ///
    ///  @param hue       текущее значение тона цвета для определения веса перекрытия
    ///  @param weights   кривая весов тональной палитры перекрытий близких цветов.
    ///                   Веса расчтывается как массив одномерых текстур
    ///                   каждого тонального сектора цветового круга HSV.
    ///  @param index     текущий индекс сектора цветового круга HSV
    ///
    ///  @return вес перекрытия пиксела в цветовом пространстве hsv для заданного hue
    ///
     inline float overlapWeight(float hue, texture1d_array&amp;lt;float, access::sample&amp;gt;  weights, uint index){
        constexpr sampler s(address::clamp_to_edge, filter::linear, coord::normalized);
        return weights.sample(s, hue, index).x;
    }

Сам расчет текстуры выполним один раз только в момент конструирования фильтра или при изменение параметров на CPU. Тогда все вычисления в ядре сведутся к операции интерполяции (через семплирование текстуры) и применению полученного веса к операции сдвига в каждом канале HSV:

    ///  @brief Сдвинуть на -1..+1 значение яркостного канала HSV.
    ///
    ///  @param hsv        входное значение в пространстве HSV
    ///  @param levelOut   сдвиг
    ///  @param hue        текущее значение тона цвета для определения веса перекрытия
    ///  @param weights    кривая весов тональной палитры перекрытий близких цветов
    ///  @param index      текущий индекс сектора цветового круга HSV
    ///
    ///  @return новое значение пиксела в цветовом пространстве hsv
    ///
    inline float3 adjust_lightness(float3 hsv, float levelOut, float hue, texture1d_array&amp;lt;float, access::sample&amp;gt;  weights, uint index)
    {
        //
        // Значение сдвига яркостного канала с перекрытием близких цветов
        // рассматриваем не только как функцию значения сдвига но и функцию значениея каналы
        // насыщенности.
        //
        float v = 1.0 + levelOut * overlapWeight(hue,weights,index) * hsv.y;
        hsv.z = clamp(hsv.z * v, 0.0, 1.0);
        return hsv;
    }

    ///  @brief Сдвинуть на -1..+1 значение канала насыщенности HSV.
    ///
    ///  @param hsv        входное значение в пространстве HSV
    ///  @param levelOut   сдвиг
    ///  @param hue        текущее значение тона цвета для определения веса перекрытия
    ///  @param weights    кривая весов тональной палитры перекрытий близких цветов
    ///  @param index      текущий индекс сектора цветового круга HSV
    ///
    ///  @return новое значение пиксела в цветовом пространстве hsv
    ///
     inline float3 adjust_saturation(float3 hsv, float levelOut, float hue, texture1d_array&amp;lt;float, access::sample&amp;gt;  weights, uint index)
    {
        float v = 1.0 + levelOut * overlapWeight(hue,weights,index);
        hsv.y = clamp(hsv.y * v, 0.0, 1.0);
        return hsv;
    }

    ///  @brief Сдвинуть на -1..+1 значение канала тона HSV.
    ///
    ///  @param hsv        входное значение в пространстве HSV
    ///  @param levelOut   сдвиг
    ///  @param hue        текущее значение тона цвета для определения веса перекрытия
    ///  @param weights    кривая весов тональной палитры перекрытий близких цветов
    ///  @param index      текущий индекс сектора цветового круга HSV
    ///
    ///  @return новое значение пиксела в цветовом пространстве hsv
    ///
     inline float3 adjust_hue(float3 hsv, float levelOut, float hue, texture1d_array&amp;lt;float, access::sample&amp;gt;  weights, uint index){

        //
        // hue rotates with overlap ranages
        //
        hsv.x  = hsv.x + 0.5 * levelOut * overlapWeight(hue,weights,index);
        return hsv;
    }

Основная утилита контроля селективной коррекции будет выглядеть как пошаговая модификация HSV представления пиксела в каждом селективном пространстве с учетом предрасчитанных весов перекрытия. Запишем основную функцию и ядро преобразования:

 ///  @brief Установить новое значение поксела в соответствиями с параметрами сдвигов
    ///  каналов в каналах в каждом тональном секторе цветового пространства HSV.
    ///
    ///  @param input_color входной RGBA
    ///  @param hueWeights  веса перекрытий
    ///  @param adjust      параметры преображования
    ///
    ///  Параметры преобразования задаются объектом имеющим структуру уровней:
    ///
    ///  typedef struct{
    ///    float hue;
    ///    float saturation;
    ///    float value;
    ///  }IMPHSVLevel;
    ///
    ///  И настройки для каждом из секторов и мастер уровня
    ///  typedef struct {
    ///    IMPHSVLevel   master;
    ///    IMPHSVLevel   levels[kIMP_Color_Ramps];
    ///    IMPBlending   blending;
    ///  } IMPHSVAdjustment;
    ///
    ///
    ///  Сектора задются из фиксированного набора с исходными значениями перекрытий взятых из
    ///  Adobe Photoshop CC2015.
    ///
    ///  #define  kIMP_Color_Ramps  6
    ///
    ///  static constant metal_float4 kIMP_Reds        = {315.0, 345.0, 15.0,   45.0};
    ///  static constant metal_float4 kIMP_Yellows     = { 15.0,  45.0, 75.0,  105.0};
    ///  static constant metal_float4 kIMP_Greens      = { 75.0, 105.0, 135.0, 165.0};
    ///  static constant metal_float4 kIMP_Cyans       = {135.0, 165.0, 195.0, 225.0};
    ///  static constant metal_float4 kIMP_Blues       = {195.0, 225.0, 255.0, 285.0};
    ///  static constant metal_float4 kIMP_Magentas    = {255.0, 285.0, 315.0, 345.0};
    ///
    ///
    ///  @return новое значение пиксела в RGBA
    ///
    inline float4 adjustHSV(float4 input_color,
                            texture1d_array&amp;lt;float, access::sample&amp;gt;  hueWeights,
                            constant IMPHSVAdjustment     &amp;amp;adjust
                            ){

        float3 hsv = IMProcessing::rgb_2_HSV(input_color.rgb);

        float  hue = hsv.x;

        //
        // Для каждого из каналов сдвигаем значения в каждом мз секторов
        //
        // Сдвигаем яркости
        for (uint i = 0; i&amp;lt;kIMP_Color_Ramps; i++){
            hsv = adjust_lightness(hsv, adjust.levels[i].value,    hue, hueWeights, i);
        }

        // Сдвигаем насыщенности
        for (uint i = 0; i&amp;lt;kIMP_Color_Ramps; i++){
            hsv = adjust_saturation(hsv, adjust.levels[i].saturation,    hue, hueWeights, i);
        }

        //
        // Сдвигаем тона
        //
        for (uint i = 0; i&amp;lt;kIMP_Color_Ramps; i++){
            hsv = adjust_hue(hsv, adjust.levels[i].hue,    hue, hueWeights, i);
        }

        //
        // Устанавливаем мастер значение
        //
        hsv.z = clamp(hsv.z * (1.0 + adjust.master.value), 0.0, 1.0);
        hsv.y = clamp(hsv.y * (1.0 + adjust.master.saturation), 0.0, 1.0);
        hsv.x  = hsv.x + 0.5 * adjust.master.hue;

        float3 rgb(IMProcessing::HSV_2_rgb(hsv));

        //
        // Традиционно выбираем одно из смешиваний
        //
        if (adjust.blending.mode == 0)
            return IMProcessing::blendLuminosity(input_color, float4(rgb, adjust.blending.opacity));
        else
            return IMProcessing::blendNormal(input_color, float4(rgb, adjust.blending.opacity));
    }

    ///
    ///  @brief Ядро прямого преобразования.
    ///
     kernel void kernel_adjustHSVExample(texture2d&amp;lt;float, access::sample&amp;gt;  inTexture         [[texture(0)]],
                                 texture2d&amp;lt;float, access::write&amp;gt;   outTexture        [[texture(1)]],
                                 texture1d_array&amp;lt;float, access::sample&amp;gt;  hueWeights  [[texture(2)]],
                                 constant IMPHSVAdjustment               &amp;amp;adjustment  [[buffer(0)]],
                                 uint2 gid [[thread_position_in_grid]]){

        float4 input_color   = inTexture.read(gid);

        float4 result =  adjustHSV(input_color, hueWeights, adjustment);

        outTexture.write(result, gid);
    }

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

И так, второе важное допущение, которое мы можем использовать для ускорения расчета: интерполяция основного преобразования через преобразование суженной цветовой карты в пространстве RGB. Грубо говоря, для того что-бы не гонять 21 раз в ядре каждый пиксел по всему изображению, сузим процедуру до гомогенных преобразований LUT. Т.е. вычислим ограниченный LUT по 3D RGB пространству с учетом параметров фильтра. При этом вычисления будут производится только при изменении этих параметров. И сам LUT сделаем размерности не 256x256x256, что тоже не кисло, а к примеру, 64x64x64, что уже не так дорого, но все еще удовлетворяет некоторой точности преобразования, допустимой в рамках live-view режима, или режима предпросмотра действий результата фильтра, когда предельная точность преобразований не нужна.

Обычно такая операция выполняется на CPU и затем используется фильтр вроде разработанного нами для лучшего из фото-приложений Metalagram. Т.е. 3D текстура заполняется в цикле 64x64x64 и затем используется как LUT-мапинг в GPU. Но мы не будем так делать: у нас есть же все для практически мгновенного заполнения текстуры непосредственно в GPU. Т.е. мы просто используем уже разработанную утилиту селективной цветокоррекции для ядра Metal, но применим к 3D текстуре, каждый пиксел, которой, равен значению координаты:

    ///
    /// @brief Ядро предварительного расчета LUT преобразования
    ///
    ///
    ///  @param hsv3DLut  3D текстура новой таблицы
    ///  @param hueWeights  веса перекрытий
    ///  @param adjust      параметры преображования
    ///
        kernel void kernel_adjustHSV3DLutExample(
                                      texture3d&amp;lt;float, access::write&amp;gt;         hsv3DLut     [[texture(0)]],
                                      texture1d_array&amp;lt;float, access::sample&amp;gt;  hueWeights   [[texture(1)]],
                                      constant IMPHSVAdjustment               &amp;amp;adjustment  [[buffer(0) ]],
                                      uint3 gid [[thread_position_in_grid]]){

        //
        // Вычисляем входной пиксел в 3D пространстве.
        //
        float4 input_color  = float4(float3(gid)/(hsv3DLut.get_width(),hsv3DLut.get_height(),hsv3DLut.get_depth()),1);

        //
        // Преобразовываем его в соостветсвие с параметрами сдвигов
        //
        float4 result       = IMProcessingExample::adjustHSV(input_color, hueWeights, adjustment);

        //
        // Пишем LUT
        //
        hsv3DLut.write(result, gid);
    }

Вот собственно и все танцы. Теперь приступим к практической камасутре.

Молодой, но бодрый IMProcessing Framework

За последние 3-4 года Apple, если не фундаментально, то весьма существенно, проапгрейдила парадигму разработки на своих платформах. Фактически отказавшись от дальнейшего развития не только OpenGL, но и Objective-C, глубоко въевшегося во все аспекты жизнедеятельности компании и программистского комьюнити вокруг нее. Что можно сказать по поводу этих ключевых технологий позапрошлого века? Туда им и дорога. Swift и Metal — будущее компании. Поэтому мы подумали, и я решил: хватит кормить кауказ клавиатуру своими мозолями. Перехожу, не только на Metal, но и на Swift. А посему, практически с нуля начинаю портирование основных фильтров необходимых для ежедневного развлечения, работы, работе на работе и вне, и работы над работой для работы над этим блогом, т.е. DPCore3 используемая в качестве топлива для первых постов этого блога больше не будет использоваться.

Текущая версия IMProcessing уже содержит ядро позволяющее скрыть основной слой взаимодействия не только с железом, но и, простихоспоти, с Metal Framework. По мере развития движка я буду пытаться поддерживать в актуальном состоянии документацию по нему и публиковать примеры использования.

Пока же, для объяснения кода примера реализации селективного HSV фильтра, достаточно разъяснения основных шагов выполняемых при использовании этого конструктора:

  1. Создание контекста исполнения фильтров — создается контейнер IMPContext, оборачивающий и скрывающий однотипные действия инициализации устройства, инициализации исполняемого кода кернел или графических функций, инициализации очереди команд в контексте которой будет происходит поток взаимодействия с памятью устройства ускорителя.
  2. IMPFunction — загрузка потока исполнения кода ядер или графических функций.   По сути это оболочка к шейдерам Metal на Metal Shading Language.
  3. Создание инстанса IMPFilter, или его расширение или создание нового класса фильтров с использованием IMPFilter как базового. Является менеджером потока исполнения одной или нескольких функций или других фильтров организованных в виде последовательной цепочки операций над изображением. Также дополняет синхронную или асинхронную обработку событий исполнения фильтрации. Что можно использовать для простой организации анализа, пост- или пре-процессинга изображений, а так же для организации реакции пользовательского интерфейса или операций ввода-вывода результатов работы фильтров.
  4. Создание IMPimageProvider — абстрактная привязка провайдера изображения (текстуры) к фильтру в качестве источника и результата исполнения фильтра. Расширяя IMPImageProvider можно подключать произвольные источники изображений, к примеру URL, или читалку LUT-файлов. В текущей версии поддерживается чтение из файлов с форматами jpeg/tiff/png и 3D LUT формата Cube.

Такой концепт позволяет сосредоточится на реализации функциональной части фильтров, быстро создавать новые и встраивать в произвольные приложении без лишнего оверкодинга.

Вот на базе этого фреймворка и реализуем практический инструмент селективной цветокоррекции.

Создание проекта

Для использования IMProcessing в самостоятельных приложениях достаточно создать проект в XCode либо под iOS либо под OSX и добавить к проекту Podfile. В моем случае проект будет называться IMProcessing-07, а pod будет выглядеть так:


platform :osx, '10.11'

use_frameworks!

target 'ImageMetalling-07' do
    pod 'IMProcessing', :git =&amp;amp;gt; 'https://bitbucket.org/degrader/improcessing.git'
end

В качестве репозитария, так же можно указать зеркало проекта на github: https://github.com/dnevera/IMProcessing.git, если удобно отслеживать фреймворк там.

В проект обязательно нужно добавить файл с произвольным именем с исходным текстом Metal Shading Language. Пусть будет, к примеру IMPMain_metal.metal:

Снимок экрана 2015-12-27 в 21.46.26

Для довершения настройки, необходимо рассказать системе сборки Metal, где искать уже готовые ядра и заголовочные файлы Metal из IMProcessing:

Снимок экрана 2015-12-27 в 21.47.37

Т.е. при создании нового проекта всегда указываем путь к $(PODS_ROOT)/Headers/Private/IMProcessing — каталогу металических шейдеров из фреймворка и размещенных в этом каталоге системой cocoapods.

Код главного шейдера приложения должен содержать как минимум одно включение:


//
//  IMPMetal_main.metal
//  ImageMetalling-07
//
//  Created by denis svinarchuk on 15.12.15.
//  Copyright © 2015 IMetalling. All rights reserved.
//

#include &quot;IMPStdlib_metal.h&quot;

В проекте: ImageMetalling-07 реализующий фильтр и приложение по мотивам этого поста все эти шаги выполнены. Нужно только не забыть выполнить команду $pod install перед сборкой проекта.

Конструирование фильтра

Создадим в проекте файл: IMPHSVExampleFilter.swift и заполним его осмысленным кодом. Для начала определим параметры настроек фильтра: сдвиги уровней каналов в каждом из выбранных диапазонов цветов. По умолчанию все коррекции должны быть нулевыми. Изменение параметров должно приводить к обновлению либо буферов обмена структуры параметров и в случае «оптимизированного» инстанса класса, обновлению 3D-текстуры LUT преобразования. В этом случае дополнительно запускаются ядра расчета LUT на GPU.

    ///
    /// Значения сдвигов по умолчанию
    ///
    public static let defaultAdjustment = IMPHSVAdjustment(
        master:   IMPHSVLevel(hue: 0.0, saturation: 0, value: 0),
        levels:  (
            IMPHSVLevel(hue: 0.0, saturation: 0, value: 0),
            IMPHSVLevel(hue: 0.0, saturation: 0, value: 0),
            IMPHSVLevel(hue: 0.0, saturation: 0, value: 0),
            IMPHSVLevel(hue: 0.0, saturation: 0, value: 0),
            IMPHSVLevel(hue: 0.0, saturation: 0, value: 0),
            IMPHSVLevel(hue: 0.0, saturation: 0, value: 0)),
        blending: IMPBlending(mode: IMPBlendingMode.NORMAL, opacity: 1)
    )

    ///
    /// Текущие значения корректирующих сдвигов
    ///
    public var adjustment:IMPHSVAdjustment!{
        didSet{

            if self.optimization == .HIGH {
                //
                // в режиме предварительного расчета LUT используем буфер для передачи в ядро
                // предварительно подготовленный LUT преобразования
                //
                adjustmentLut.blending = adjustment.blending
                self.updateBuffer(&amp;amp;adjustmentLut, size:sizeof(IMPAdjustment))
            }

            //
            // обнвовляем буфер обмена с ядром заполненной структурой корректировок
            //
            updateBuffer(&amp;amp;adjustmentBuffer, context:context, adjustment:&amp;amp;adjustment, size:sizeof(IMPHSVAdjustment))

            if self.optimization == .HIGH {
                //
                // В режиме оптимизации исполняем ядро вычисления корректирующего LUT
                //
                applyHsv3DLut()
            }

            dirty = true
        }
    }

 

Конструктор инстанса фильтра

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

   ///  Сконструировать HSV фильтра.
    ///
    ///  - .HIGH используется для уменьшения вычислений в в ядрах.
    ///     Вместо расчета по каждому пикселу создается интерполирующий 64x64x64 LUT. Такой подход
    ///     может привести к появлению артефактов на изображении, но снижение количества вычислений
    ///     может быть использовано в вариантах использования фильтра в режиме live-view телефонов,
    ///     или превью изображений, когда понижение точности расчета не существенно влияет на результат.
    ///
    ///  - .Normal используется как основной режим с повышенной точностью расчетов
    ///
    ///  - parameter context:      контекст устройства
    ///  - parameter optimization: режим потимизации
    ///
    public required init(context: IMPContext, optimization:optimizationLevel) {

        super.init(context: context)

        self.optimization = optimization

        if self.optimization == .HIGH {
            //
            // ядро отображения 3D LUT
            //
            kernel = IMPFunction(context: self.context, name: &amp;quot;kernel_adjustLutD3D&amp;quot;)

            //
            // ядро подготовки HSV 3D LUT
            //
            kernel_hsv3DLut = IMPFunction(context: self.context, name: &amp;quot;kernel_adjustHSV3DLutExample&amp;quot;)

            //
            // глубина расчета HSV 3D LUT
            //
            hsv3DlutTexture = hsv3DLut(64)
        }
        else{
            kernel = IMPFunction(context: self.context, name: &amp;quot;kernel_adjustHSVExample&amp;quot;)
        }

        //
        // Добавляем к фильтру функцию
        //
        addFunction(kernel)

        //
        // веса перекрытия по умолчанию
        //
        hueWeights = IMPHSVFilter.defaultHueWeights(self.context, overlap: IMProcessing.hsv.hueOverlapFactor)

        defer{
            //
            // Умолчательные коррекции
            //
            adjustment = IMPHSVFilter.defaultAdjustment
        }
    }

    //
    // По умолчанию создаем фильтра с &amp;quot;нормальным&amp;quot; режимом оптимизации
    //
    public convenience required init(context: IMPContext) {
        self.init(context: context, optimization:.NORMAL)
    }

Доставка параметров фильтрации до ядер устройства:

    ///  Перегружаем функицю конфигурации передачи параметров в ядра
    ///
    ///  - parameter function: текущее ядро
    ///  - parameter command:  текущий командный энкодер
    public override func configure(function: IMPFunction, command: MTLComputeCommandEncoder) {
        if self.optimization == .HIGH {
            command.setTexture(hsv3DlutTexture, atIndex: 2)
            command.setBuffer(adjustmentLutBuffer, offset: 0, atIndex: 0)
        }
        else{
            command.setTexture(hueWeights, atIndex: 2)
            command.setBuffer(adjustmentBuffer, offset: 0, atIndex: 0)
        }
    }

Заполнение 3D-текстуры LUT для оптимизированных вычислений

   //
    // Вычисления LUT по настройкам HSV коррекции.
    //
    private func applyHsv3DLut(){

        self.context.execute({ (commandBuffer) -&amp;gt; Void in

            let width  = self.hsv3DlutTexture!.width
            let height = self.hsv3DlutTexture!.height
            let depth  = self.hsv3DlutTexture!.depth

            //
            // Запускаем ядра в гриде заданной размерности
            //
            let threadgroupCounts = MTLSizeMake(self.kernel_hsv3DLut.groupSize.width, self.kernel_hsv3DLut.groupSize.height,  self.kernel_hsv3DLut.groupSize.height);

            let threadgroups = MTLSizeMake(
                (width  + threadgroupCounts.width ) / threadgroupCounts.width ,
                (height + threadgroupCounts.height) / threadgroupCounts.height,
                (depth + threadgroupCounts.height) / threadgroupCounts.depth);

            let commandEncoder = commandBuffer.computeCommandEncoder()

            commandEncoder.setComputePipelineState(self.kernel_hsv3DLut.pipeline!)

            commandEncoder.setTexture(self.hsv3DlutTexture, atIndex:0)
            commandEncoder.setTexture(self.hueWeights, atIndex:1)
            commandEncoder.setBuffer(self.adjustmentBuffer, offset: 0, atIndex: 0)

            commandEncoder.dispatchThreadgroups(threadgroups, threadsPerThreadgroup:threadgroupCounts)
            commandEncoder.endEncoding()
        })
    }

 

Имплементация фильтра в основное приложение

...
import IMProcessing
...
class ViewController: NSViewController {

...

    //
    // Создания контекста GPU устройства
    //
    let context = IMPContext()

    //
    // Основной фильтр
    //
    var mainFilter:IMPTestFilter!

    //
    // Превью изображения
    //
    var imageView: IMPView!

...
   override func viewDidLoad() {
        super.viewDidLoad()
...

        //
        // Создаем окно превью изображения
        //
        imageView = IMPView(frame: scrollView.bounds)

        //
        // Создаем фильтр
        //
        mainFilter = IMPTestFilter(context: self.context)

        //
        // Связываем фильтр с превью
        //
        imageView.filter = mainFilter

...
        //
        // добавляем окно как документ скролируемого контента
        //
        scrollView.documentView = imageView

...
        //
        // Хендлер меню загрузки файла
        //
        IMPDocument.sharedInstance.addDocumentObserver { (file, type) -&amp;amp;gt; Void in
            if type == .Image {

                //
                // Читаем изображение в стандартный контейнер IMPIMage,
                // для OSX IMPImage определен как алиас к NSImage
                // public typealias IMPImage = NSImage, для iOS соответственно к UIImage
                //
                //
                if let image = IMPImage(contentsOfFile: file){

                    //
                    // Обновляем размеры содержимого окна
                    //
                    self.imageView.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)

                    //
                    // Обновляем источник фильтра. Свойство .source IMPView определяется как alias IMPFilter.source
                    // После обновления источника происходит автоматическое исполнения фильтра и содержимого окна превью
                    //
                    self.imageView.source = IMPImageProvider(context: self.imageView.context, image: image)

                    self.asyncChanges({ () -&amp;amp;gt; Void in
                        self.zoomOne()
                    })
                }
            }
        }

...
  }  

Полный код исходный код готового проекта можно забрать из ImageMetallingImageMetalling-07.

Инжой и мерикрисмас!


Авторы блога не преследуют задачи быть предельно корректным, но если заметили явную ашипку, если написали явную глупость, если что-то не понятно: комментируйте или пишите на: imagemetalling [*] gmail.com .

 

Самый лучший селективный HSV-adjustment фильтр ever.: 4 комментария

    1. Если мы хотим получить технический инструмент, например для какого-то анализа, то да, должны. Иначе мы не сможем адекватно рассчитать воздействие сдвигов. Но если мы хотим получить инструмент «фотографический», более перцептуально работающий с изображением, то можно использовать любую функцию. В данном случае обоснование простое: нормальное распределение наше вообще все:) А можно поиграться с функциями откликов, к примеру, причем делать эти функции адаптивными к тональному сегменту и т.п.

      Нравится 1 человек

Оставьте комментарий