惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
Jina AI
Jina AI
月光博客
月光博客
博客园_首页
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
IT之家
IT之家
Hugging Face - Blog
Hugging Face - Blog
T
Tailwind CSS Blog
V
Visual Studio Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
大猫的无限游戏
大猫的无限游戏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
阮一峰的网络日志
阮一峰的网络日志
量子位
博客园 - 【当耐特】
The Cloudflare Blog
宝玉的分享
宝玉的分享
博客园 - 聂微东
博客园 - 叶小钗
美团技术团队
G
Google Developers Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
小众软件
小众软件

jdhao's digital space

Conversion between base64 and OpenCV or PIL Image 腾讯云对象存储博客图床开启 CDN 加速(不需要购买额外域名) Search and Replace in Multiple Files in Vim/Neovim Change Table Column Width in LaTeX Image or Table Side by Side in LaTeX LaTeX 并排显示图像或表格 Firenvim: Neovim inside Your Browser Content inside HTML tags missing in Latest Hugo? Creating Markdown Front Matter with Ultisnips Labelme JSON 标注格式转 voc XML 格式 Nifty Nvim Techniques That Make My Life Easier -- Series 6 macOS 下如何为视频制作字幕 Running Command Asynchronously inside Neovim Resolving Merge Conflict after Git Stash Pop Pylint: command not found? A Hands-on Experience with Neovim's Built-in LSP Support How to Convert PDF to Images with Imagemagick 互联网上常用缩略语集锦 File Backup in Neovim Converting PDF Pages to Images with Poppler Nifty Nvim Techniques That Make My Life Easier -- Series 5 Neovim Configuration for System-wide Use How to sort a list of tuple or list in Python -- lambda or itemgetter? Building A Vim Statusline from Scratch 人类第一颗原子弹爆炸始末 Distributed Training in PyTorch with Horovod Learning Expect Programming Essential Knowledge about SSH Nifty LaTeX Techniques -- Series 1 更改 Adsense 邮寄地址,重新寄送 PIN
Pillow/PIL 缩放索引图像时的一个问题
2020-11-18 · via jdhao's digital space

今天遇到了一个有趣的问题,有人在 v2ex 上说自己的图片,经过 PIL 缩放到 800x600 以后,非常模糊,有什么办法可以解决。

现象与问题#

我想这不可能啊,我把他的原图下载到本地,然后使用 PIL 进行缩放保存,原始的代码如下:

from PIL import Image


def main():
    img = Image.open('big_image.png')
    im_resize = img.resize((800, 600), resample=Image.LANCZOS)
    im_resize.save('big-resized-pil.png', quality=90)


if __name__ == "__main__":
    main()

发现保存的图片中,文字部分的确很模糊,Image.LANCZOS 已经是最好的滤波器了,按理说不应该出现这种问题。我又把 PIL 提供的其他几种滤波器都试了一下,发现出来的结果没区别,都很差。

我又用 OpenCV 试了一下,发现使用 OpenCV 缩放得到的图片效果还可以,下面是我的代码:

 import cv2

 def main():
     im = cv2.imread('big_image.png')
     # https://docs.opencv.org/master/da/d54/group__imgproc__transform.html#ga47a974309e9102f5f08231edc7e7529d
     im_resize = cv2.resize(im, (800, 600), interpolation=cv2.INTER_LANCZOS4)
     write_param = [cv2.IMWRITE_PNG_COMPRESSION, 5]
     cv2.imwrite('big-resized.png', im_resize, write_param)


 if __name__ == "__main__":
     main()

缩放以后的图片明显比 PIL 缩放出来的效果好了很多,上面的 Interpolation 参数类似于 PIL 中的 resample 参数,用来控制图像缩放时候使用的滤波器,不光 cv2.INTER_LANCZOS4 出来的效果可以,甚至 cv2.INTER_AREA 出来的效果也不错。

即便 OpenCV 和 PIL 实现某个滤波器的细节有差异,缩放出来的结果差异也不应该如此明显,这样巨大的差异很难用实现细节不同来解释清楚。

原因#

后面那个问题又有人回复,说可能是因为提问者给的图是索引图 (index image,也叫 palette image) (索引图读取以后,使用 print(im.mode) 会输出 'P')。如果图片本身是索引图,PIL 在读取图片并不会自动转换为 RGB 图片,因此返回的 Image 对象仍然是索引图,这就带来了问题。

如果我们仔细阅读 Image.resize() 方法的文档,其中对 resample 参数的解释提到:

If the image has mode “1” or “P”, it is always set to PIL.Image.NEAREST

这样就解释了为什么我们最初使用 Image.LANCZOS 滤波器不管用的问题:因为这个图像是索引图像,PIL 只会使用 Image.NEAREST 滤波器,我们自己设定的滤波器压根没生效,所以才会出现糟糕的压缩效果。

知道了这个原因,解决方法也很简单,那就是读取图像的时候,使用 Image.convert() 把图像转为 RGB 模式:

    img = Image.open('big_image.png').convert('RGB')

然后对图像进行缩放,得到的结果就与 OpenCV 缩放以后的结果基本一致了。

什么是索引图#

我们常见的 RGB 图片,图片的每个像素分别用 R, G, B 三个颜色分量表示,一般取值是在 0-255 之间,每个颜色分量占 1 byte,所以一个像素占 3 byte 空间,如果图像大小为 MxN,那么图像占的内存空间大小约为 MxNx3 byte。

索引图是一种特殊的图 ,它的存在主要是为了节省空间,索引图附带了一个 color palette/table 或者叫 color map,对应了 256 种颜色(所以 color table 大小为 256x3)),然后图像像素每个位置值在 0-255 之间,数值代表该处像素在 color table 对应的颜色的索引值,实际展示该图片的时候,我们利用这个索引就能在 color table 中找到真正要展示的颜色。从这个描述可以看出,索引图一个像素只需要 1 比特,所以索引图占的空间大小约为 RGB 图的 1/3 ,大大减少了存储占用1

我们在 PIL 中可以轻松创建索引图,一个改编例子如下:

from PIL import Image
from PIL import ImageDraw


im = Image.new("P", (400, 400), 0)
palette_color = im.getpalette()
print(palette_color)

im.putpalette([
    0, 0, 0,  # black background
    255, 0, 0,  # index 1 is red
    255, 255, 0,  # index 2 is yellow
    255, 153, 0,  # index 3 is orange
])

# It seems that the image palette is automatically filled with numbers to make
# it have size 256*3 (768), if you do not provide that much number to
# putpaletter() method.
palette_color = im.getpalette()
print(palette_color)

d = ImageDraw.ImageDraw(im)
d.polygon((0, 0, 0, 400, 400, 400), fill=1)
d.rectangle((100, 100, 300, 300), outline=2)
d.ellipse((120, 120, 280, 280), outline=3)

im.save("out.gif")

在 PIL 中,如果图像是一个索引图,我们可以使用 Image.getpalette() 方法得到它的 color palette,该 palette 是一个 768 元素的 list,按照 [r, g, b, r, g, b, ...] 这种顺序排布。为了方便找到某个 index 对应的颜色,我们可以把它变形:

palette_color = np.asarray(palette).reshape(256, 3)

参考#