使用粗糙的灰度算法有问题吗?

Problems with using a rough greyscale algorithm?

所以我正在设计一些使用 PILpython 中编辑照片的程序,其中之一是将图像转换为灰度(我避免使用 python 中的任何功能=13=]).

我采用的算法很简单:对于每个像素(颜色深度为 24),我计算了 RGB 的平均值值并将 RGB 值设置为此平均值。

我的程序正在生成看起来准确的灰度图像,但我想知道我是否使用了正确的算法,我遇到了一个问题 this answer,似乎 'correct'算法就是计算0.299 R + 0.587 G + 0.114 B.

我决定将我的程序与该算法进行比较。我使用我的程序生成了一个灰度图像,另一个(使用相同的输入)来自 a website online'image to grayscale' 的顶部 Google 结果。

肉眼看来,它们是一模一样的,如果有什么变化,我是看不出来的。但是,我决定使用 this website'compare two images online' 的前 Google 结果)来比较我的灰度图像。结果发现,在像素深处,它们有细微的变化,但none人眼一眼就能察觉(可以发现差异,但通常只有在图像相互重叠或切换时才能发现)毫秒之间)。

我的问题(第一个是正题)

  1. 使用我的 'rough' 灰度算法有什么缺点吗?
  2. 有没有人有任何输入图像,其中我的灰度算法会产生与 'correct' 明显不同的图像?
  3. 是否有任何 colours/RBG 组合我的算法无法正常工作?

我的关键代码(如果需要的话):

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

'correct'算法(看起来偏重绿色):

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

我的输入图片:

我的算法生成的灰度图像:

灰度图'correct':

在线对比灰度图时(红色高亮为差异,使用10%的fuzz):

尽管上面突出显示的像素有所不同,但上面的灰度图像看起来几乎完全相同(至少对我而言)。

另外,关于我的第一个问题,如果有人感兴趣,this site已经对不同的灰度转换算法做了一些分析,也有一些自定义算法。

编辑:

为了回应@Szulat 的回答,我的算法 实际上 生成了这张图片(忽略裁剪不当,原始图片有三个圆圈,但我只需要第一个):

如果有人想知道转换为灰度的原因是什么(因为算法似乎取决于目的),我只是在 python 中制作一些简单的照片编辑工具,以便我可以有一个迷你 Photoshop,不需要依赖互联网来应用滤镜和效果。

悬赏原因:这里的不同答案涵盖了不同的内容,这些都是相关且有用的。这使得选择接受哪个答案变得相当困难。我已经开始悬赏,因为我喜欢这里列出的一些答案,但也因为有一个涵盖我对这个问题所需的一切的答案会很好。

有许多不同的方法可以转换为灰度,它们确实会给出不同的结果,尽管使用不同的输入彩色图像可能更容易看出差异。

正如我们在灰度中看不到的那样,"best" 方法在某种程度上取决于应用程序并且在某种程度上在旁观者的眼中。

您提到的替代公式是基于人眼对绿色调的变化更敏感,因此赋予它们更大的权重 - 类似于相机中的拜耳阵列,每个红色有 2 个绿色像素和蓝色的。 Wiki - Bayer array

这些图像看起来非常相似,但您的眼睛可以分辨出不同之处,特别是如果您将一个替换另一个:

例如,您可以注意到背景中的花朵在平均转换中看起来更亮。

并不是说三个通道的平均 "bad" 本质上有什么。该公式的原因是我们对红色、绿色和蓝色的感知不同,因此它们对灰度图像强度的贡献不应该相同;因为我们更强烈地感知绿色,所以绿色像素在灰度上应该看起来更亮。然而, 没有唯一完美的灰度转换,因为我们看到的是颜色,而且无论如何每个人的视觉都略有不同,所以任何公式都会尝试做出近似值,使像素强度感觉 "right" 对大多数人来说。

您可以使用任何转换方程、标度、线性度。您找到的那个:

I = 0.299 R + 0.587 G + 0.114 B

基于普通人眼 "average" 原色(R、G、B)感知敏感度(至少在 population/HW 它创建的时间段内;记住这些标准是在 LED、TFT 等屏幕之前创建的)。

您遇到的几个问题:

  1. 我们的眼睛不一样

    所有人感知颜色的方式不同。性别之间存在很大差异,地区之间也存在较小差异;甚至世代和年龄也有影响。因此,即使是平均值也应处理为 "average"。

    我们对可见光谱中的光强度有不同的敏感度。最敏感的颜色是绿色(因此它的权重最高)。但是 XYZ curve 峰值对于不同的人来说可能处于不同的波长(像我一样,我让它们移动了一点,导致对某些波长的识别有所不同,比如 Aqua 的某些阴影 - 有些人认为它们是绿色,有些人认为它们是蓝色,即使 none 他们中有任何色盲残疾或其他)。

  2. 监视器不使用相同的波长或光谱色散

    因此,如果您使用 2 台不同的显示器,它们可能会使用略微不同的 R、G、B 波长,甚至可能会使用不同宽度的光谱滤波器 (just use a spectroscope and see)。是的,硬件应该 "normalized" 但这与使用归一化波长不同。它类似于使用 RGB 与白噪声光谱光源的问题。

  3. 监测线性度

    人类看不到线性尺度:我们通常是 logarithmic/exponential(取决于你如何看待它)所以是的,我们可以用 HW(甚至 SW)将其归一化,但问题是如果我们线性化对一个人来说意味着我们为另一个人破坏它。

如果你把所有这些放在一起,你可以使用平均值......或特殊(且昂贵)的设备来 measure/normalize 针对某些标准或针对经过校准的人(取决于行业)。

但这在家庭条件下处理起来太多了,所以把所有这些都留给工业,并像世界上大多数人一样使用 "average" 的权重......幸运的是我们的大脑可以处理它,因为你看不到差异,除非你开始并排比较两个图像或在动画中:)。所以我(会)做:

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

最明显的例子:

  1. 原创

  2. 在 Gimp 中去饱和(亮度模式 - 这是您的算法所做的)

  3. 在 Gimp 中去饱和(亮度模式 - 这就是我们的眼睛所做的)

所以,不要平均 RGB。平均 RGB 是完全错误的!

(好吧,你是对的,平均在一些晦涩的应用程序中可能是有效的,即使当 RGB 值被视为颜色时它没有物理或生理意义。顺便说一句,"regular" 由于 gamma,加权平均的方式在更微妙的方面也是不正确的。sRGB 应该首先线性化,然后将最终结果转换回 sRGB(这相当于检索 Lab 颜色中的 L 分量 space))

亮度有很多公式,具体取决于 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 , 

应用亮度公式,并转换回伽玛域

Y' = Y ^ (1/2.4) . 

此外,请考虑将 3D 颜色 space 转换为 1D 数量会丢失 2/3 的信息,这可能会在接下来的处理步骤中困扰您。根据问题的不同,有时不同的公式会更好,例如 V = MAX(R,G,B)(来自 HSV 颜色 space)。

我怎么知道?我是 Poynton 博士的追随者和朋友。

在回答您的主要问题时,使用任何单一的灰度度量都有缺点。这取决于你想从你的形象中得到什么。例如,如果您在白色背景上有彩色文本,如果您想让文本脱颖而出,您可以使用 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
}

提供的答案已经足够了,但我想以不同的方式就此主题进行更多讨论。

自从学了数码绘画后,更多的时候是用HSV。

绘画时使用HSV更可控,但要简短,重点是S:Saturation将颜色与光线的概念分开。而将S变为0,就已经是图像的'computer'灰度了。

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')

此方法真正保留了原色的'bright'。然而,没有考虑人眼如何处理数据