Проблемы с использованием грубого алгоритма серого?

Поэтому я разрабатываю несколько программ для редактирования фотографий в python с использованием PIL и один из них конвертирует изображение в оттенки серого (я избегаю использования любых функций из PIL).

Используемый мною алгоритм прост: для каждого пикселя (глубина цвета 24) я вычислил среднее значение R, G и B и установил значения RGB в этом среднем состоянии.

Моя программа создавала полутоновые изображения, которые казались точными, но мне было интересно, использовал ли я правильный алгоритм, и я наткнулся на этот ответ на вопрос, где кажется, что "правильный" алгоритм должен вычислять 0.299 R + 0.587 G + 0.114 B

Я решил сравнить свою программу с этим алгоритмом. Я создал изображение в оттенках серого, используя мою программу, и другую (используя тот же ввод) с веб-сайта в Интернете (верхний результат Google для 'image to grayscale'.

Моим невооруженным взглядом казалось, что они были точно такими же, и если бы было какое-то изменение, я не мог этого видеть. Тем не менее, я решил использовать этот сайт (верхний результат Google для 'compare two images online'), чтобы сравнить мои изображения в оттенках серого. Оказалось, что в глубине пикселей они имели небольшие вариации, но ни один из них не был воспринят человеческим глазом на первый взгляд (различия могут быть замечены, но обычно только тогда, когда изображения накладываются друг на друга или переключаются между ними в миллисекундах),

Мои вопросы (первый - главный вопрос):

  1. Есть ли недостатки в использовании моего "грубого" алгоритма серого?
  2. У кого-нибудь есть какие-либо входные изображения, где мой алгоритм с оттенками серого создаст видимое изображение, которое будет "правильным"?
  3. Существуют ли комбинации цветов /RBG, для которых мой алгоритм не будет работать?

Моя ключевая часть кода (при необходимости):

def greyScale(pixelTuple):
    return tuple([round(sum(pixelTuple) / 3)] * 3)

"Правильный" алгоритм (который, кажется, сильно весит зеленый):

def greyScale(pixelTuple):
    return tuple([round(0.299 * pixelTuple[0] + 0.587 * pixelTuple[1] + 0.114 * pixelTuple[2])] * 3)

Мое входное изображение: My input image

Изображение в оттенках серого, созданное моим алгоритмом: The greyscale image my algorithm produces

Изображение в оттенках серого, которое является "правильным": The greyscale image which is 'correct'

Когда изображения в оттенках серого сравниваются онлайн (выделены красным цветом различия, используя 10% пуха): When the greyscale images are compared online (highlighted red are the differences, using a fuzz of 10%)

Несмотря на различия в выделенных выше пикселях, изображения в серой шкале выше отображаются как почти одинаковые (по крайней мере, для меня).

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

EDIT:

В ответ на ответ @Szulat мой алгоритм фактически производит это изображение вместо этого (игнорируйте плохую обрезку, исходное изображение имеет три круга, но мне нужен только первый):

This is what my algorithm **actually** produces

В случае, если люди задаются вопросом, в чем причина преобразования в оттенки серого (так как кажется, что алгоритм зависит от цели), я просто делаю некоторые простые инструменты для редактирования фотографий на python чтобы у меня был мини-Photoshop, t нужно полагаться на Интернет для применения фильтров и эффектов.

Причина для Bounty: Различные ответы здесь охватывают разные вещи, которые являются релевантными и полезными. Это затрудняет выбор того, какой ответ принять. Я начал щедрость, потому что мне нравятся несколько ответов, перечисленных здесь, но также потому, что было бы хорошо иметь один ответ, который покрывает все, что мне нужно для этого вопроса.

Ответ 1

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

enter image description here

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

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

Ответ 2

Наиболее очевидный пример:

  1. оригинал

  2. Desaturated in Gimp (режим Lightness - это то, что делает ваш алгоритм)

  3. Desaturated in Gimp (режим освещенности - это то, что делают наши глаза)

gimp desaturate: lightness vs luminosity

Итак, не средние RGB. Усреднение RGB просто неправильно!

(Хорошо, вы правы, усреднение может быть действительно в некоторых неясных приложениях, хотя оно не имеет физического или физиологического значения, когда значения RGB рассматриваются как цвет. Кстати, "обычный" способ взвешивания усреднения также неверен более тонким способом из-за гамма. sRGB должен быть сначала линеаризован, а затем окончательный результат преобразован обратно в sRGB (что было бы эквивалентно извлечению L-компонента в цветовом пространстве Lab))

Ответ 3

Вы можете использовать любое уравнение преобразования, масштаб, линейность. Тот, который вы нашли:

I = 0.299 R + 0.587 G + 0.114 B

основано на средней чувствительности восприятия основного цвета (R, G, B) человеческого глаза (по крайней мере, для периода времени и популяции /HW, который был создан, учитывайте, что эти стандарты были созданы до LED, TFT и т.д. экраны).

Есть несколько проблем, с которыми вы боретесь:

  1. наши глаза не то же самое

    Все люди не воспринимают цвет одинаково. Существуют значительные расхождения между полами и меньшими и между регионами; Роль и поколения, и возраста. Поэтому даже среднее значение должно обрабатываться как "среднее".

    У нас есть различная чувствительность к интенсивности света по видимому спектру. Самый чувствительный цвет зеленый (следовательно, самый высокий вес на нем). Но пики кривой XYZ могут быть на разных длинах волн для разных людей (как и я, я их немного сдвинул, вызывая разницу в распознавании определенных длин волн, например, некоторых оттенков Aqua - некоторые считают их зелеными как синие, даже если ни у кого из них нет нарушения цветовой слепоты или что-то еще).

  2. мониторы не используют одни и те же длины волн или спектральную дисперсию

    Поэтому, если вы возьмете 2 разных монитора, они могут использовать несколько разные длины волн для R, G, B или даже разные ширины спектрального фильтра (просто используйте спектроскоп и посмотрите). Да, они должны быть "нормализованы" HW, но это не то же самое, что использовать нормированные длины волн. Это похоже на проблемы с использованием источников света RGB и White Noise.

  3. линейность монитора

    Люди не видят в линейном масштабе: мы обычно логарифмичны/экспоненциальны (зависит от того, как вы на это смотрите), так что да, мы можем нормализовать это с помощью HW (или даже SW), но проблема в том, что мы линеаризуем для одного человека, а значит, это для другого.

Если вы возьмете все это вместе, вы можете либо использовать средние значения, либо специальное (и дорогое) оборудование для измерения/нормализации относительно какого-либо стандарта или против откалиброванного человека (в зависимости от отрасли).

Но это слишком много, чтобы справляться с домашними условиями, поэтому оставляйте все для промышленности и используйте весы для "среднего", как и в большинстве стран мира... К счастью, наш мозг может справиться с этим, поскольку вы не можете видеть разницу, если не начнете сравнивать оба изображения бок о бок или в анимации :). Поэтому я (буду):

I = 0.299 R + 0.587 G + 0.114 B
R = I
G = I
B = I

Ответ 4

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

Поскольку мы не видим в оттенках серого, "лучший" метод несколько зависит от приложения и несколько в глазах зрителя.

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

Ответ 5

Существует множество формул для яркости, в зависимости от праймеров цвета R, G, B:

Rec.601/NTSC: Y = 0.299*R + 0.587*G + 0.114*B , 

Rec.709/EBU:  Y = 0.213*R + 0.715*G + 0.072*B , 

Rec.2020/UHD: Y = 0.263*R + 0.678*G + 0.059*B . 

Это все потому, что наши глаза менее чувствительны к синему, чем к красному, чем к зеленому.

При этом вы, вероятно, рассчитываете Luma, а не Luminance, поэтому формулы все не так. Для постоянной яркости вы должны преобразовать в линейный свет

R = R' ^ 2.4 , G = G' ^ 2.4 , B = B' ^ 2.4 , 

применить формулу Luminance и преобразовать обратно в гамма-домен

Y' = Y ^ (1/2.4) . 

Также учтите, что преобразование трехмерного цветового пространства в 1D-количество теряет 2/3 информации, что может укусить вас на следующих этапах обработки. В зависимости от проблемы, иногда другая формула лучше, например V = MAX (R, G, B) (из цветового пространства HSV).

Откуда мне знать? Я последователь и друг доктора Пойнтона.

Ответ 6

Достаточно ответов, но я хочу обсудить немного по этому вопросу по-другому.

Поскольку я интересовался цифровой живописью, я чаще использую HSV.

Это намного более управляемо для использования HSV во время покраски, но держите его коротким, главное - S: Saturation, отделяющее концепцию цвета от света. И превращение S в 0, уже является "компьютерной" серой шкалой изображения.

from PIL import Image
import colorsys

def togrey(img):
    if isinstance(img,Image.Image):
        r,g,b = img.split()
        R = []
        G = []
        B = [] 
        for rd,gn,bl in zip(r.getdata(),g.getdata(),b.getdata()) :
            h,s,v = colorsys.rgb_to_hsv(rd/255.,gn/255.,bl/255.)
            s = 0
            _r,_g,_b = colorsys.hsv_to_rgb(h,s,v)
            R.append(int(_r*255.))
            G.append(int(_g*255.))
            B.append(int(_b*255.))
        r.putdata(R)
        g.putdata(G)
        b.putdata(B)
        return Image.merge('RGB',(r,g,b))
    else:
        return None

a = Image.open('../a.jpg')
b = togrey(a)
b.save('../b.jpg')

Этот метод действительно зарезервировал "яркий" оригинальный цвет. Однако, не учитывая, как человеческий глаз обрабатывает данные.

Ответ 7

В ответе на ваш главный вопрос есть недостатки в использовании какой-либо одной меры серого. Это зависит от того, что вы хотите от своего изображения. Например, если у вас есть цветной текст на белом фоне, если вы хотите выделить текст, вы можете использовать минимум значений r, g, b в качестве вашей меры. Но если у вас есть черный текст на цветном фоне, вы можете использовать максимум значений для одного и того же результата. В моем программном обеспечении я предлагаю вариант максимального, минимального или медианного значения для пользователя. Также освещаются результаты непрерывных тоновых изображений. В ответ на комментарии, требующие более подробной информации, код для пикселя ниже (без каких-либо защитных мер).

int Ind0[3] = {0, 1, 2};                 //all equal
int Ind1[3] = {2, 1, 0};                 // top, mid ,bot from mask...
int Ind2[3] = {1, 0, 2};
int Ind3[3] = {1, 2, 0};
int Ind4[3] = {0, 2, 1};
int Ind5[3] = {2, 0, 1};
int Ind6[3] = {0, 1, 2};
int Ind7[3] = {-1, -1, -1};              // not possible
int *Inds[8] = {Ind0, Ind1, Ind2, Ind3, Ind4, Ind5, Ind6, Ind7};
void grecolor(unsigned char *rgb, int bri, unsigned char *grey)
{                         //pick out bot, mid or top according to bri flag
    int r = rgb[0];
    int g = rgb[1];
    int b = rgb[2];
    int mask = 0;
    mask |= (r > g);
    mask <<= 1;
    mask |= (g > b);
    mask <<= 1;
    mask |= (b > r);
    grey[0] = rgb[Inds[mask][2 - bri]];  // 2, 1, 0 give bot, mid, top
}