Вычисление доминантного цвета изображения. Нормализация цветового баланса против баланса белого

Меня как-то спросили, страдаю ли я от похмелий.
Для того, чтобы были похмелья, нужно перестать пить.
Так что данная проблема просто не возникает.
(Приписывают Л. Килмистеру)

Попробуйте задать гуглу вопрос вроде: How to calculate the dominant color of an image? Или что-то вроде того же спросите у stackoverflow, для большей релевантности. В случае с гуглом вы получите результат примерно из более чем 30 миллионов поисковых ответов. 30 миллионов! 30, Клара!

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

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

Баланс белого

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

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

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

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

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

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

Нормализация цветовой гаммы изображения и ББ

И так мы определились, что на самом деле, вместо того, чтобы просто произвести коррекцию баланса белого, нам нужно каким-то образом скорректировать цветовой баланс изображения для последующей обработки автоматизированными фильтрами или даже простыми фильтрами использующими LUT. Назовем для себя это первичной нормализацией гаммы исходного изображения. Но по привычке будем иметь в виду аббревиатуру ББ (баланс белого) или WB (white balance) или в автоматическом смысле: АББ или AWB, чтобы не писать долгие никому не известные слова в коде конкретных приложений.

Далее мы разберем способ автоматизации коррекции ББ прекрасно разобранный в посте Андрея Журавлева. Он может быть просто реализован обычным экшеном в программе Adobe Photoshop. Мы же разберемся как все это, и немножко более, реализовать в виде Metal-ического image-фильтра.

В общих словах мы должны выполнить два простых действия:

  • найти доминантный цвет изображения 
  • применить фильтр коррекции ББ (или в наших терминах нормализацию цветового баланса)

В фильтре ББ:

  • инвертировать доминантный цвет
  • привести яркость инвертированного цвета к среднему значению в каждом канале сложением каналов в режиме смешивания яркостей
  • компенсировать избыточный синий подмешав «серый» со смещением в «желтый»
  • смешать с перекрытием исходное изображения с полученным композитным сигналом

 

Поиск доминантного цвета изображения

Казалось бы ничего сложного реализовать такой поиск нет: взяли все сложили в одну кучу да и поделили. Но как всегда есть нюансы и связаны они, в первую очередь с тем, что поиск среднего придется вести с учетом анализа всей площади изображения, т.е. по факту будет нужно пробежаться по всему массиву NxM RGB триплетов исходной картинки. Сложить суммы R/G/B в какой-то аккумулятор и потом нормализовать до разрядности представления RGB. Все это абсолютно тривиально воспроизводится в терминах линейных алгоритмов на CPU. Но долго. А нам то хочется все делать быстро да еще и в режиме live-view. В общем использовать всю мощь GPU и Metal. Рассмотрим какие есть варианты.

Первое, что приходит на ум, это последовательное сжатие текстуры изображения до точки через интерполяцию средним. Не требует танцев с бубнами вокруг скаттеринга и атомарными операциями. Но все же требует приличного количества операций с чтением и записью текстуры, а так же дополнительным выделением памяти для ping-pong фаз обмена данными вычисляемых текстур на каждом цикле усреднения. Такой подход используется, к примеру, в реализации поиска среднего в Core Image и GPUImage.

Второе, тоже очевидное решение, использовать данные гистограммы, возможно уже нами даже посчитанной для других целей анализа исходного изображения. С точки зрения затрат на вычисления такой подход, очевидно будет более экономичным, поскольку не требует активного обмена с памятью и выделение памяти под промежуточные текстуры. Сложность алгоритма расчета самой гистограммы, как было показано, не превышает O(n*m) плюс накладные расходы на сборку частичных гистограмм средствами DSP.

Суть расчета среднего по гистограмме изображения можно записать так:

M(c) = {\frac{\sum_0^Nh(c)_i\times R_i}{\sum_0^Nh(c)_i}} — среднее значение яркости канала {c} ,

где {N} — ширина гистограммы,
{h(c)_i} — значение бина {i} в канале {c},
{R_{i} = \frac{i}{N-1},\ i \in [0;N)} — монотонно возрастающее распределение яркостей гистограммы, зависит только от размера гистограммы, и в нашем случае почти всегда этот размер 256, для каждого канала

Вот второй вариант возьмем за основу, а в качестве обертки над вычислениями возьмем IMProcessing Framework. В результате получим триплет из значений {M(c)} для каждого из каналов RGB. С точки зрения анализа исходного изображения нам нужно будет получить его гистограмму в объекте класса IMPhistogram, и подключить к нему солвер вычисляющий среднее (в уме помним доминантное…):


// создаем солвер доминантного цвета
dominantColorSolver = IMPHistogramDominantColorSolver()

// создаем анализатор гистограммы
dominantColorAnalayzer = IMPHistogramAnalyzer(context: context)

// добавляем солвер к гистограмме
dominantColorAnalayzer.addSolver(dominantColorSolver)

// мониторим обновление гистограммы изображения
dominantColorAnalayzer.addUpdateObserver { (histogram) - Void in
   // обновляем параметры фильтра коррекции WB по среднему
}

Этот кусок кода ничего не говорит нам о том, как правильно посчитать среднее, просто показывает, что в фреймворке IMProcessing вся эта рутинная работа сделана и осталось только воспользоваться результатами. Но тем неменее, лучше разобрать подробно суть его работы. А код можно расшифровать как получение на «вход» анализатора IMPHistogramAnalyzer провайдера текстуры, который из себя представляет специальный немодифицирующий фильтр изображения, представляющий на выходе его гистограмму в виде объекта: IMPHistogram и запускающий объекты-солверы, дополнительно решающие, что с гистограммой делать, например, замерить средний цвет:

///
/// Солвер доминантного цвета изображения в пространстве RGB(Y)
/// Вычисляет среднее значение интенсивностей каждого канала по гистограмме этих каналов
///
public class IMPHistogramDominantColorSolver: NSObject, IMPHistogramSolver
{
    ///
    /// Доминантный (средний) цвет изображения. Используем векторный тип float4 из фреймворка
    /// для работы с векторными типа данных simd
    ///
    public var color=float4()

    public func analizerDidUpdate(analizer: IMPHistogramAnalyzer, histogram: IMPHistogram, imageSize: CGSize){
        for i in 0...histogram.channels.count-1
        {
            let index = IMPHistogram.ChannelNo(rawValue: i)
// среднее значение канала
            color[i] = histogram.mean(channel: index)
        }
    }
}

Тогда при обновлении «замеров» гистограммы мы можем сдвинуть доминантный цвет фильтра ББ:


...

// мониторим обновление гистограммы изображения
dominantColorAnalayzer.addUpdateObserver { (histogram) - Void in
   // обновляем параметры фильтра коррекции WB по среднему
   self.wbFilter.adjustment.dominantColor = self.dominantColorSolver.color
}

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

  1. один раз получить монотонную дискретную последовательность яркостей от индекса гистограммы
  2. умножить вектор этого монотонного распределения на гистограмму в выбранном канале
  3. получить сумму всех значений распределения

В итоге мы получаем сложность O(n), где n — ширина гистограммы канала, что само по себе немного для 24bit RGB или размера в 256 на канал, но, все же, сэкономим и воспользуемся готовым набором оберток к «хардверным ускорителям», предоставленных нам Apple в виде Accelerate Framework. Всегда хорошо экономить на вычислениях даже по мелочам, согласитесь. В IMPHistogram, поиск среднего реализован так:


    ///
    /// Среднее значение интенсивностей канала с заданным индексом.
    /// Возвращается значение нормализованное к 1.
    ///
    /// - parameter index: индекс канала
    ///
    /// - returns: нормализованное значние средней интенсивности канала
    ///
    public func mean(channel index:ChannelNo) - Float{
        let m = mean(A: channels[index.rawValue], size: channels[index.rawValue].count)
        let denom = sum(A: channels[index.rawValue], size: channels[index.rawValue].count)
        return m/denom
    }

    //
    // Распределение абсолютных значений интенсивностей гистограммы в зависимости от индекса
    //
    private var intensityDistribution:(Int,[Float])!
    //
    // Сборка распределения интенсивностей
    //
    private func createIntensityDistribution(size:Int) - (Int,[Float]){
        let m:Float    = Float(size-1)
        var h:[Float]  = [Float](count: size, repeatedValue: 0)
        var zero:Float = 0
        var v:Float    = 1.0/m

        // Создает вектор с монотонно возрастающими или убывающими значениями
        vDSP_vramp(zero, v, h, 1, vDSP_Length(size))
        return (size,h);
    }

    private func ramp(inout C:[Float], ramp:Range){
        let m:Float    = Float(C.count-1)
        var zero:Float = Float(ramp.startIndex)/m
        var v:Float    = Float(ramp.endIndex-ramp.startIndex)/m
        vDSP_vramp(zero, v, C, 1, vDSP_Length(C.count))
    }

    //
    // Вычисление среднего значения распределения вектора
    //
    private func mean(inout A A:[Float], size:Int) - Float {
        intensityDistribution = intensityDistribution ?? self.createIntensityDistribution(size)
        if intensityDistribution.0 != size {
            intensityDistribution = self.createIntensityDistribution(size)
        }
        if tempBuffer.count != size {
            tempBuffer = [Float](count: size, repeatedValue: 0)
        }
        //
        // Перемножаем два вектора вектор
        //
        vDSP_vmul(A, 1, intensityDistribution.1, 1, tempBuffer, 1, vDSP_Length(size))
        return sum(A: tempBuffer, size: size)
    }

Фильтр баланса белого по доминантному цвету

Сконструируем вторую часть автоматической фильтрации ББ по доминанте изображения:

public class IMPWBFilter:IMPFilter,IMPAdjustmentProtocol{

    public var adjustmentBuffer:MTLBuffer?
    public var kernel:IMPFunction!

    ///  Создать ББ фильтр
    ///
    ///  - parameter context: device context
    ///
    public required init(context: IMPContext) {
        super.init(context: context)
        kernel = IMPFunction(context: self.context, name: "kernel_adjustWB")
        self.addFunction(kernel)
        defer{
            self.adjustment = IMPWBFilter.defaultAdjustment
        }
    }

    public override func configure(function: IMPFunction, command: MTLComputeCommandEncoder) {
        if kernel == function {
            command.setBuffer(adjustmentBuffer, offset: 0, atIndex: 0)
        }
    }

}

И зададим ядро фильтрации на Metal, т.е. фактически реализуем экшен фотошопа из поста Андрея в виде kernel-функции GPGPU:

    inline float4 adjustWB(float4 inColor, constant IMPWBAdjustment adjustment) {

        float4 dominantColor = float4(adjustment.dominantColor);

        // негатив
        float4 invert_color = float4((1.0 - dominantColor.rgb), 1.0);

        constexpr float4 grey128 = float4(0.5,    0.5, 0.5,      1.0);
        constexpr float4 grey130 = float4(0.5098, 0.5, 0.470588, 1.0);

        // компенсировать яркость
        invert_color             = blendLuminosity(invert_color, grey128);
        // компенсировать синий оттенок
        invert_color             = blendOverlay(invert_color, grey130);    

        float4 awb = blendOverlay(inColor, invert_color);

        float4 result = float4(awb.rgb, adjustment.blending.opacity);

        if (adjustment.blending.mode == LUMINOSITY)
            return blendLuminosity(inColor, result);
        else
            return blendNormal(inColor, result);
    }

    kernel void kernel_adjustWB(
                                texture2d inTexture [[texture(0)]],
                                texture2d outTexture [[texture(1)]],
                                constant IMPWBAdjustment &adjustment [[buffer(0)]],
                                uint2 gid [[thread_position_in_grid]]) {

        float4 inColor = sampledColor(inTexture,outTexture,gid);
        outTexture.write(adjustWB(inColor,adjustment),gid);
    }

Дополнительный анализ цветов и коррекция по специфическим весам цветов палитры

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

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

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

Под синтетическими весами палитры изображения будем понимать набор средне-арифметических значений всех пикселов изображения принадлежащих каждой сексте из тонального круга: Reds/Yellows/Greens/Cyans/Blues/Magentas.

Hsv-polar-coord-hue-chroma

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

Ядро подсчета весов будет базироваться на коде расчета гистограммы IMPHistogramAnalyzer: IMPColorWeightsAnalyzer и будет использовать тот же класс для конструирования анализатора, но с новой  kernel-функцией и солвером подсчета весов учитывающего новую структуру данных буфера обмена:

    inline void circle_bin_positionPartial(float3 hsv,
                                           device IMPHistogramBuffer *outArray,
                                           constant IMPColorWeightsClipping   &clipping,
                                           threadgroup atomic_int temp[kIMP_HistogramMaxChannels][kIMP_HistogramSize]
                                           ){
        constexpr uint c = 3;
        float      hue =  hsv.x * 360.0;
        int        bin = 0;

        //
        // Change REDS
        //
        if ((hue>=kIMP_Reds.x && hue<=360.0) || (hue>=0.0 && hue<=kIMP_Reds.w))             bin = 0;                  //         // Change YELLOWS         //         if (hue>=kIMP_Yellows.x && hue<=kIMP_Yellows.w)             bin = 1;                  //         // Change GREENS         //         if (hue>=kIMP_Greens.x && hue<=kIMP_Greens.w)             bin = 2;                  //         // Change CYANS         //         if (hue>=kIMP_Cyans.x && hue<=kIMP_Cyans.w)             bin = 3;                  //         // Change BLUES         //         if (hue>=kIMP_Blues.x && hue<=kIMP_Blues.w)             bin = 4;                           //         // Change MAGENTAS         //         if (hue>=kIMP_Magentas.x && hue<=kIMP_Magentas.w)             bin = 5;                  if (hsv.y > clipping.saturation)
            atomic_fetch_add_explicit(&(temp[c][bin]), 1, memory_order_relaxed);

        if (hsv.y <= clipping.saturation){

            if (hsv.z <= clipping.black)                 //                 // Out of black point                 //                 bin = 253;             else if (hsv.z >= (1.0-clipping.white))
                bin = 254;
            else
                //
                // GRAYS
                //
                bin = 255;
        }
        else
            //
            // COLORED
            //
            bin = 252;

        atomic_fetch_add_explicit(&(temp[c][bin]), 1, memory_order_relaxed);
    }

    ///  @brief Compute color weights
    ///
    kernel void kernel_impColorWeightsPartial(
                                              texture2d   inTexture  [[texture(0)]],
                                              device   IMPHistogramBuffer        *outArray  [[ buffer(0)]],
                                              constant uint                      channels  [[ buffer(1)]],
                                              constant IMPCropRegion             regionIn  [[ buffer(2)]],
                                              constant float                     scale     [[ buffer(3)]],
                                              constant IMPColorWeightsClipping   clipping  [[ buffer(4)]],
                                              uint  tid      [[thread_index_in_threadgroup]],
                                              uint2 groupid  [[threadgroup_position_in_grid]],
                                              uint2 groupSize[[threadgroups_per_grid]]
                                              )
    {
        threadgroup atomic_int temp[kIMP_HistogramMaxChannels][kIMP_HistogramSize];

        uint w      = uint(float(inTexture.get_width())*scale)/groupSize.x;
        uint h      = uint(float(inTexture.get_height())*scale);
        uint size   = w*h;
        uint offset = kIMP_HistogramSize;

        for (uint i=0; i<channels; i++){
            atomic_store_explicit(&(temp[i][tid]),0,memory_order_relaxed);
        }

        threadgroup_barrier(mem_flags::mem_threadgroup);

        for (uint i=0; i<size; i+=offset){                          uint  j = i+tid;             uint2 gid(j%w+groupid.x*w,j/w);                          uint4  rgby = IMProcessing::channel_binIndex(inTexture,regionIn,scale,gid);                          if (rgby.a>0){

                float3 hsv = IMProcessing::rgb_2_HSV(float3(rgby.rgb)/float3(IMProcessing::histogram::Im));

                circle_bin_positionPartial(hsv,outArray,clipping,temp);

                for (uint c=0;
                     c<channels-1; c++){
                    atomic_fetch_add_explicit(&(temp[c][rgby[c]]), 1, memory_order_relaxed);
                }
            }
        }

        threadgroup_barrier(mem_flags::mem_threadgroup);

        for (uint i=0; i<channels; i++){
            outArray[groupid.x].channels[i][tid]=atomic_load_explicit(&(temp[i][tid]), memory_order_relaxed);
        }
    }

В терминах IMProcessing Framework расширение анализа в виде кода будет выглядеть так:

       //
        // Создаем фильтр автоматической коррекции баланса цветов
        //

        filter = IMPAutoWBFilter(context: context)

        //
        // Профилируем обработку через анализ распределения весов на цветовом круге.
        // Снижаем влияние автоматической коррекции в зависимости от сюжета и замысла нашего
        // фильтра. Форма снижения или увеличения opacity, а по сути силы воздействия AWB,
        // может быть произвольной.
        //
        // В конкретном примере:
        // 1. мы снижаем долю насыщенности желтого, если доминантный цвет изображения желто-красный.
        // 2. учитываем вес голубых и синих оттенков для снижения влияния на сюжетах с явно выраженным
        //    участием сине-голубых цветов
        //
        //
        filter.colorsAnalyzeHandler = { (solver, opacityIn, wbFilter, hsvFilter) in

            //
            // Получаем доминантный цвет изображения
            //
            let dominanta = self.filter.dominantColor!

            //
            // Переходим в пространство HSV
            //
            let hsv = dominanta.rgb.tohsv()

            //
            // Получаем тон доминантного цвета
            //
            let hue = hsv.hue * 360

            //
            // Просто выводим для справки
            //
            self.dominantLabel.stringValue = String(format: "%4.0fº", hue)
            self.neutralsWLabel.stringValue = String(format: "%4.0f%", solver.neutralWeights.neutrals*100)

            //
            // Учитываем состав доминантного цвета
            //
            var reds_yellows_weights =
            //
            // количество красного с учетом перекрытия красных оттенков из
            // цветового круга HSV и степенью перекрытия с соседними цветами 1
            //
            hue.overlapWeight(ramp: IMProcessing.hsv.reds, overlap: 1) +
                //
                // к красным добавляем количество желтого
                //
                hue.overlapWeight(ramp: IMProcessing.hsv.yellows, overlap: 1)

            //
            // Веса оставшихся оттенков в доминантном цвете
            //
            let other_colors = solver.colorWeights.cyans +
                solver.colorWeights.greens +
                solver.colorWeights.blues +
                solver.colorWeights.magentas

            reds_yellows_weights -= other_colors
            if (reds_yellows_weights < 0.0 /* 10% */) {
                //
                // Если желто-красного немного - вообще не учитываем в снижении влияния фильтра
                //
                reds_yellows_weights = 0.0; // it is a yellow/red image
            }

            //
            // Снижаем насыщенность желтых оттенков в изображении
            //
            hsvFilter.adjustment.yellows.saturation = -0.1 * reds_yellows_weights

            self.yellowsWLabel.stringValue = String(format: "%4.0f%", reds_yellows_weights*100)

            //
            // Результирующая прозрачность слоя AWB
            //
            var opacity:Float = opacityIn

            //
            // Для сине-голубых оттенков изображения снижаем долю влияния AWB фильтра
            // вычисляем общий вес (по сути площадь)
            //
            let cyan_blues = solver.colorWeights.cyans + solver.colorWeights.blues
            let rest       = 1 - cyan_blues

            self.bluesWLabel.stringValue = String(format: "%4.0f%", cyan_blues*100)

            //
            // Какая-то выдуманная функиця снижения прозрачности AWB от веса сине-голубых оттенков
            //
            opacity    *= rest < cyan_blues ? 1-sqrt(pow(cyan_blues,2) - pow(rest, 2)) : 1

            self.opacityLabel.stringValue = String(format: "%4.0f%", opacity*100)

            return opacity
        }

 

Полностью проект можно забрать, собрать и опробовать из репозитория проекта ImageMetalling: ImageMetalling-09. Для правильной сборки локально должна быть установлена мега-библиотека для работы с файлами формата JPEG (JFIF) libjpeg-turbo. На сегодняшний день это лучшая реализация поддержки этого формата.

Примеры работы фильтра автоматической нормализации цветового баланса (АББ) на различных сюжетах

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

AWB-Compared
Много желтого паразитного оттенка. Убираем. Нейтрализем основной цвет. Профиль LUT Kodachrome 64.
AWB-blue-comparsion
Снижаем ББ. Профиль Kodachrome 64
AWB-green-comparsion
Нормализация оттенков в зеленом чуть снижена.

 

Выводы

Ну работает же! Хоть и просто все как  кирзовый сапог.

 


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

Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s