PIL 的 Image.convert() 函数在模式 'P' 下如何工作?

6
我有一组24位的png文件,想将它们转换成8位的png文件。我使用了PIL的Image.convert()方法来解决这个问题。然而,在使用参数'mode=P'后,我发现具有相同RGB值的像素可能会被不同地转换。
我将一个示例图像转换为numpy数组,并且原始的24位png文件像这样具有值:

RGB数组

   ..., 
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119], 
   ...

在使用模式为“P”的转换函数后,图像的值变成了如下形式:

8位数组

   ..., 98, 98, 134, 98, 98, 98, 134, 98, 98, 98, 134, ...

代码示例:

from PIL import Image
import numpy as np
img = Image.open("path-to-file.png")
p_img = img.convert("P")

我希望相同RGB值的像素被以相同的方式转换。我知道像素被转换为调色板索引,但这仍然对我没有意义。我不熟悉PIL库。可以有人解释一下为什么会发生这种情况吗?提前感谢您。
根据 Mark 的示例实现了一个小功能。
import numpy as np
from PIL import Image
#from improc import GenerateNColourImage

# Set image height and width
N    = 6
h, w = 100, 100

# Generate repeatable random Numpy image with N^3 unique colours at most
n = np.random.randint(N, size=(h, w, 3), dtype=np.uint8)
# Work out indices of diagonal elements
diags = np.diag_indices(h)

# Intentionally set all diagonal elements same shade of blue
n[diags] = [10,20,200]
# Make Numpy image into PIL Image, palettise, convert back to Numpy array and check diagonals
a0 = Image.fromarray(n)

unique_colors = np.unique(n.reshape(-1, n.shape[2]), axis=0).shape
print(unique_colors)   #e.g. print (217, 3)
a1 = a0.convert('P')
a2 = np.array(a1)

# Look at diagonals - should all be the same
print(a2[diags])
print(' %s %d' % ("Number of unique colors:  ", np.unique(a2).shape[0]))

对角像素的值已打印
... 154 154 154 154 154 154 124 154 160 160 160 154 160 ...

在模式“P”下,8位图像包含125个唯一的调色板索引。似乎无论如何PIL都会执行抖动处理。
2个回答

2
这是将图像转换为P颜色模式时显示的正常行为。调色板模式的工作方式是创建一个映射表,将范围在0-255之间的索引对应于较大颜色空间(如RGB)中的离散颜色。例如,图像中的RGB颜色值(0, 0, 255) (纯蓝色) 获得索引1 (仅是假设的例子)。同样的过程会处理原始图像中每个唯一的像素值(但在映射过程中表的大小不应超过256)。因此,numpy数组(或常规列表)具有以下值:
   ..., 98, 98, 134, 98, 98, 98, 134, 98, 98, 98, 134, ...

这些像素值对应于映射表中的索引,而不是实际的颜色值本身。因此,您可以将它们解释为索引,读取图像时会将其转换为存储在该索引中的实际颜色值。

但是这些像素值并不总是意味着图像是彩色模式P。例如,如果您查看灰度图像(L)的像素数据,则值看起来与调色板模式相同,但实际上对应于真实的颜色值(或灰度级别),而不是索引。

Translated Content:

这些像素值对应于映射表中的索引,而不是实际的颜色值本身。因此,您可以将它们解释为索引。当读取图像时,它们会被转换为存储在该索引中的实际颜色值。

但是,这些像素值并不总是表示图像是彩色模式P。例如,如果您查看灰度图像(L)的像素数据,则这些值看起来与调色板模式相同,但实际上对应于真实的颜色值(或灰度级别),而不是索引。


Translated Title: "最初的回答"

嗨Vasu,感谢您的回复。我知道RGB值会被映射到调色板索引中,但我不确定为什么相同的RGB值可能具有不同的索引。正如Mark在他的答案中所说,这可能是抖动,以获得更好的可视化性能。此外,离开这个话题,您知道一张图片如何告诉它是灰度图像还是调色板图像吗? - VicXue
不,我的意思是如果_L_和_P_模式的PNG文件都是8位格式,图像查看器如何区分它们并以不同的方式可视化它们? - VicXue
@VicXu 你永远不知道。调色板图像可能与灰度图像相同。但反之则不然。我的意思是,如果在视觉层面上看到一个“P”图像,其映射表具有灰度阴影,则会类似于该图像的灰度版本。原因是因为灰度样本空间属于0-255范围(或256个唯一值),而“P”图像模式也能够存储那么多唯一颜色。因此,灰度图像可以很好地转换为“P”,差异几乎可以忽略不计。 - Vasu Deo.S
@VicXue 反之则不然,灰度图像无法保存颜色值(不像调色板图像)。因此,至少在视觉层面上可以将其用作区分的标志,即如果图像包含任何形式的RGB颜色(或超出灰色范围的颜色),则它肯定是调色板图像(如果只存在两个图像,一个是调色板图像,另一个是灰度图像)。但是,如果使用灰度图像,则区分过程并不那么容易。(至少在视觉层面上)因为两个图像之间的差异是难以察觉的。 - Vasu Deo.S
1
@Mark在这个话题上给出了非常好的答案。它可能与您所问的不同,但仍然包含了很多关于此事的信息。 - Vasu Deo.S
显示剩余3条评论

2
问题在于PIL/Pillow正在进行“抖动”处理。简单来说,如果图像中有超过256种颜色(调色板可以容纳的最大值),则图像中必然存在调色板中不存在的颜色。因此,PIL会累积误差(原始颜色和调色板颜色之间的差异),并且不时地插入一个略微不同颜色的像素,以使图像从远处看起来更或多或少正确。这基本上是“误差扩散”。因此,如果你的颜色被卷入其中,它有时会出现不同的情况。
一种避免这种情况的方法是将图像量化到少于256种颜色,那么就不会有误差扩散的问题了。
"Original Answer" 翻译成 "最初的回答"
# Quantise to 256 colours
im256c = = image.quantize(colors=256, method=2)

请注意,这并不意味着您的蓝色阴影在每个图像中都将映射到相同的调色板索引,它只是意味着在任何一个给定的图像中,所有具有您的蓝色阴影的像素都将具有相同的调色板索引。
以下是一个示例:
注意:原始答案 → "最初的回答"
#!/usr/bin/env python3

import numpy as np
from PIL import Image
from improc import GenerateNColourImage

# Set image height and width
h, w = 200, 200
N    = 1000

# Generate repeatable random Numpy image with N unique colours
np.random.seed(42)
n = GenerateNColourImage(h,w,N) 

# Work out indices of diagonal elements
diags = np.diag_indices(h)

# Intentionally set all diagonal elements same shade of blue
n[diags] = [10,20,200]

# Make Numpy image into PIL Image, palettise, convert back to Numpy array and check diagonals
a0 = Image.fromarray(n)
a1 = a0.convert('P')
a2 = np.array(a1)

# Look at diagonals - should all be the same
print(a2[diags])

输出

[154 154 154 154 154 154 154 154 160 154 160 154 154 154 154 154 160 154
 154 154 160 154 154 154 160 160 154 154 154 160 154 154 154 154 154 154
 154 154 160 154 154 154 154 154 154 154 154 160 160 154 154 154 154 154
 154 154 154 154 154 154 154 154 154 160 154 154 154 154 154 154 154 160
 154 160 154 154 154 154 154 154 154 154 154 154 154 160 154 160 160 154
 154 160 154 154 154 160 154 154 154 154 154 160 154 154 154 154 155 154
 154 160 154 154 154 154 154 154 154 154 154 160 154 154 154 160 154 154
 154 154 160 154 154 154 154 154 154 154 154 154 154 154 154 154 154 160
 154 160 154 160 154 160 154 160 160 154 154 154 154 154 154 154 154 154
 154 154 154 154 161 154 154 154 154 154 154 154 154 154 154 160 154 160
 118 154 160 154 154 154 154 154 154 154 154 154 160 154 154 160 154 154
 154 154]

哎呀,里面有154、118和160这些值...


现在再用相同的Numpy数组,但使用quantise()来重新执行最后4行:

(注:原文中的"Original Answer"我已经翻译成了中文)

# Make Numpy image into PIL Image, quantise, convert back to Numpy array and check diagonals
b0 = Image.fromarray(n)
b1 = b0.quantize(colors=256,method=2)
b2 = np.array(b1)

# Look at diagonals - should all be the same
print(b2[diags])

输出

[64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64]

这样就好多了 - 但是仍然一样!

我应该补充一点,如果你将图像保存为256色或更少的PNG格式,PIL/Pillow会自动保存调色板图像。

Original Answer翻译成"最初的回答"


我了解 Floyd-Steinberg 抖动算法(在将图像转换为 1 时使用的算法)。它被用来营造出一种感觉,让人觉得图像包含了更多的颜色,尽管实际上并没有(这种方法被用于黑白图像中,从远处看起来就像是灰度图像)。那么误差扩散抖动算法也是一样的吗? - Vasu Deo.S
其次,您能否详细解释一下 PIL累计误差(原始颜色和调色板颜色之间的差异),然后不时插入略微不同颜色的像素 这一点?我无法理解超出256调色板范围的颜色是如何被存储/插入的。 - Vasu Deo.S
第三,这个问题不是所有使用颜色索引的格式都会遇到吗?那么为什么说 PIL/Pillow 的问题在于“抖动”,几乎所有颜色索引格式都会以某种方式使用它来补偿调色板范围之外的颜色呢? - Vasu Deo.S
顺便说一下,在我的情况下,quantise()方法确实非常有效。非常感谢你。 - VicXue
@Mark 这是一个示例图片的链接 link 或许你也可以看一下我上面的代码示例?谢谢。 - VicXue
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接