Какой алгоритм стоит за функцией Gimp "Color to Alpha"?

Для тех, кто еще не знаком с функцией "Color to Alpha" в Gimp, есть страница из документации Gimp: Color to Alpha. Это действительно хорошая работа, и мне очень интересно, как именно Gimp делает это с точки зрения цветовой манипуляции, в каком цветовом пространстве могут быть цвета. Спасибо большое за любые подсказки.

РЕДАКТИРОВАТЬ 1: Генерация информации о прозрачности для пикселя на основе его сходства с цветом ключа (тот, который вы выбираете в диалоговом окне "Цвет к альфа-каналу"), как предлагали некоторые люди перед тем, как по какой-то причине удалить свой ответ, звучит как хорошее понимание, но я предполагаю, что это более сложно, чем это. Давайте предположим, что мы оцениваем цветовое сходство в диапазоне единиц от 0, 0 до 1, 0, и у нас есть пиксель, цвет которого, например, 0,4, похож, скажем, на белый цвет (как вы бы выбрали белый цвет в "Color to "Альфа" и, следовательно, пиксель получает значение альфа-канала 0,6, тогда как бы вы изменили фактический цвет пикселя, чтобы компенсировать потерю яркости / яркости / насыщенности, когда результирующий пиксель отображается на белом фоне с альфа-каналом 0,6?

РЕДАКТИРОВАТЬ 2: На самом деле обновление: ответ на подвопрос, связанный с первым редактированием, в разделе Как изменить альфа-точку пикселя, не меняя результирующий цвет? но это, вероятно, не полная история, потому что то, что происходит в исходном коде Gimp для функции "Color to Alpha", не так просто и, кажется, основано на конкретном алгоритме, а не на формуле.

6 ответов

Решение

Вам нужно придумать механизм сравнения сходства цветов. Есть множество цветовых пространств, в которых вы можете сделать это. RGB часто не лучший для такого рода вещей. Но вы можете использовать HSV, YCbCr или другое пространство яркости / цветности. Часто расстояние в одном из этих пространств даст вам лучший ответ, чем евклидово расстояние в RGB. Если у вас есть расстояние, вы можете разделить его на максимальное расстояние, чтобы получить процент. Этот процент будет инверсией альфа, который вы хотите использовать, как одна из возможностей.

Если вы хотите узнать, как это делает GIMP, вы можете посмотреть на источник. Например, вот одно недавнее изменение кода для этого плагина.

Я взглянул на исходный код, и его основной смысл - функция colortoalpha. Параметры от *a1 до *a4 - это ввод / вывод красного, зеленого, синего и альфа соответственно, а c1 до c3 - цвет для создания альфа.

Когда вы комбинируете два цвета c1 и c2 с определенной альфа a (0 ≤ a ≤ 1), результат

y = a * c1 + (1-a) * c2

Здесь мы делаем обратную операцию: мы знаем конечный результат y и цвет фона c2 и хотим выяснить c1 и a. Поскольку это недоопределенное уравнение, существует бесконечное количество решений. Однако диапазоны 0 ≤ c1 ≤ 255 и 0 ≤ a ≤ 1 добавляют границы к решению.

Плагин Gimp работает так, что для каждого пикселя он минимизирует альфа-значение (то есть максимизирует прозрачность). И наоборот, это означает, что для каждого получающегося в результате пикселя, который не является полностью прозрачным (т. Е. Был не совсем цвет фона), один из компонентов RGB имеет значение 0 или 255.

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

Стоит отметить, что весь процесс выполняется в цветовом пространстве RGB, но может выполняться и в других, если операция объединения выполняется в одном цветовом пространстве.

Я реализовал этот фильтр в моем редакторе http://www.photopea.com/ разделе "Фильтр - Другое - Цвет в альфа- канал". Результаты на 100% идентичны GIMP. Алгоритм предельно прост.

Идея: цвет фона B был объединен с цветом переднего плана F с использованием значения прозрачности A, чтобы получить новый цвет N:

N = A * F  +  (1 - A) * B;

Вам известны N (фактический цвет изображения) и B (параметр фильтра), и вы хотите восстановить цвет переднего плана F и его прозрачность A.

Сделай это так:

A = max( abs(N.r - B.r), abs(N.g - B.g), abs(N.b - B.b)  )  

Теперь вы знаете N, B, A. Просто используйте формулу выше, чтобы вычислить F.

Так что я посмотрел на исходный код GIMP... э-э! Я сделал это универсальным и читабельным. Все еще довольно быстро, хотя. Математическое объяснение смотрите в ответе Сампо. Вот реализация C# (легко конвертируемая в C / C++):

static class PixelShaders {

    /// <summary>
    /// Generic color space color to alpha.
    /// </summary>
    /// <param name="pA">Pixel alpha.</param>
    /// <param name="p1">Pixel 1st channel.</param>
    /// <param name="p2">Pixel 2nd channel.</param>
    /// <param name="p3">Pixel 3rd channel.</param>
    /// <param name="r1">Reference 1st channel.</param>
    /// <param name="r2">Reference 2nd channel.</param>
    /// <param name="r3">Reference 3rd channel.</param>
    /// <param name="mA">Maximum alpha value.</param>
    /// <param name="mX">Maximum channel value.</param>
    static void GColorToAlpha(ref double pA, ref double p1, ref double p2, ref double p3, double r1, double r2, double r3, double mA = 1.0, double mX = 1.0) {
        double aA, a1, a2, a3;
        // a1 calculation: minimal alpha giving r1 from p1
        if (p1 > r1) a1 = mA * (p1 - r1) / (mX - r1);
        else if (p1 < r1) a1 = mA * (r1 - p1) / r1;
        else a1 = 0.0;
        // a2 calculation: minimal alpha giving r2 from p2
        if (p2 > r2) a2 = mA * (p2 - r2) / (mX - r2);
        else if (p2 < r2) a2 = mA * (r2 - p2) / r2;
        else a2 = 0.0;
        // a3 calculation: minimal alpha giving r3 from p3
        if (p3 > r3) a3 = mA * (p3 - r3) / (mX - r3);
        else if (p3 < r3) a3 = mA * (r3 - p3) / r3;
        else a3 = 0.0;
        // aA calculation: max(a1, a2, a3)
        aA = a1;
        if (a2 > aA) aA = a2;
        if (a3 > aA) aA = a3;
        // apply aA to pixel:
        if (aA >= mA / mX) {
            pA = aA * pA / mA;
            p1 = mA * (p1 - r1) / aA + r1;
            p2 = mA * (p2 - r2) / aA + r2;
            p3 = mA * (p3 - r3) / aA + r3;
        } else {
            pA = 0;
            p1 = 0;
            p2 = 0;
            p3 = 0;
        }
    }

}

Реализация GIMP ( здесь) использует цветовое пространство RGB, использует альфа-значение как float с диапазоном от 0 до 1 и R, G, B как float от 0 до 255.

Реализация RGB неэффективна, когда изображение имеет артефакты JPEG, потому что они означают незначительные ощутимые отклонения цвета, но довольно значительные абсолютные отклонения R, G, B. Использование цветового пространства LAB должно помочь в этом случае.

Если вы хотите просто удалить сплошной фон с изображения, алгоритм преобразования цвета в альфа-канал не является оптимальным вариантом. Я получил хорошие результаты, когда вычислял расстояние цветового пространства для каждого пикселя, используя цветовое пространство LAB. Рассчитанное расстояние затем было применено к альфа-каналу исходного изображения. Основное различие между этим и цветом в альфа-цвет пикселей не будет изменено. Удаление фона просто устанавливает альфа (непрозрачность) на разницу цветов. Это хорошо работает, если цвет фона не появляется на переднем плане изображения. Если это так, либо фон не может быть удален, либо должен использоваться алгоритм BFS для обхода только внешних пикселей (что-то вроде использования выбора волшебной палочки в GIMP, затем удаления выделения).

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

Я перевел метод colortoalpha с gimp на C# как мог. Проблема в том, что значения RGBA берутся как байты для каждого канала в библиотеке, такой как ImageSharp. Некоторые конверсии теряют данные во время конверсии, но я старался изо всех сил, чтобы сохранить как можно больше. Это использует ImageSharp для мутации изображения. ImageSharp полностью управляется, поэтому он будет работать на разных платформах. Это также быстро. Весь этот метод работает в течение ~10 мс (менее 10 мс).

Вот код для реализации C#:

public static unsafe void ColorToAlpha(this Image<Rgba32> image, Rgba32 color)
    {
        double alpha1, alpha2, alpha3, alpha4;
        double* a1, a2, a3, a4;

        a1 = &alpha1;
        a2 = &alpha2;
        a3 = &alpha3;
        a4 = &alpha4;

        for (int j = 0; j < image.Height; j++)
        {
            var span = image.GetPixelRowSpan(j);

            for (int i = 0; i < span.Length; i++)
            {
                ref Rgba32 pixel = ref span[i];

                // Don't know what this is for
                // *a4 = pixel.A;

                if (pixel.R > color.R)
                    *a1 = (pixel.R - color.R) / (255.0 - color.R);
                else if (pixel.R < color.R)
                    *a1 = (color.R - pixel.R) / color.R;
                else
                    *a1 = 0.0;

                if (pixel.G > color.G)
                    *a2 = (pixel.G - color.G) / (255.0 - color.G);
                else if (pixel.G < color.G)
                    *a2 = (color.G - pixel.G) / color.G;
                else
                    *a2 = 0.0;

                if (pixel.B > color.B)
                    *a3 = (pixel.B - color.B) / (255.0 - color.B);
                else if (pixel.B < color.B)
                    *a3 = (color.B - pixel.B) / color.B;
                else
                    *a3 = 0.0;

                if (*a1 > *a2)
                    *a4 = *a1 > *a3 ? *a1 * 255.0 : *a3 * 255.0;
                else
                    *a4 = *a2 > *a3 ? *a2 * 255.0 : *a3 * 255.0;

                if (*a4 < 1.0)
                    return;

                pixel.R = (byte)Math.Truncate((255.0 * (*a1 - color.R) / *a4 + color.R));
                pixel.G = (byte)Math.Truncate((255.0 * (*a2 - color.G) / *a4 + color.G));
                pixel.B = (byte)Math.Truncate((255.0 * (*a3 - color.B) / *a4 + color.B));

                pixel.A = (byte)Math.Truncate(*a4);
            }
        }
    }

Я столкнулся с той же проблемой и реализовалcolortoalphaв Python, так что я могу легко создавать сценарии пакетных операций. Кажется, он идеально соответствует выходным данным GIMP!

      import PIL.Image
import numpy as np

# Color to make transparent.
BG_COLOR_RGB = [255, 255, 255] # white

# 8 bits for every channel including alpha: 24-bit RGB input, 32-bit RGBA output
MAX_CHANNEL = 255
MAX_ALPHA = 255

# Open file and convert the data to NumPy
input_image = PIL.Image.open('input.jpg')
input = np.asarray(input_image)
bg_color = np.array(BG_COLOR_RGB, dtype=np.uint8)

# Inversing the formula: Img = alpha * Fg + (1-alpha) * Bg
alpha_per_channel = (0
                    + np.greater(input, bg_color)
                    * np.divide(input - bg_color, MAX_CHANNEL - bg_color,
                                where=np.greater(MAX_CHANNEL - bg_color, 0))
                    + np.less(input, bg_color)
                    * np.divide(bg_color - input, bg_color,
                                where=np.greater(bg_color,0)))
alpha = np.amax(alpha_per_channel, axis=2, keepdims=True)
foreground = np.divide(input - (1-alpha) * bg_color, alpha,
                       where=np.greater(alpha, 1e-4))

# Output has 4 channels (RGBA)
output = np.zeros([input.shape[0],input.shape[1],4], dtype=np.uint8)
# Set RGB channels
output[:, :, 0:3] = foreground
# Set alpha channel
output[:, :, 3:4] = alpha * MAX_ALPHA

# Save the file preserving DPI settings
PIL.Image.fromarray(output).save('output.png', dpi=input_image.info.get('dpi'))

Вы можете найти полный исходный код здесь: https://github.com/unicilindro/color2alpha/blob/main/color2alpha.py

Другие вопросы по тегам