在Python中生成图像缩略图的最快方法是什么?

40

我正在使用Python构建一个照片库,并希望能够快速为高分辨率图像生成缩略图。

生成各种图像源的高质量缩略图的最快方法是什么?

我应该使用像ImageMagick这样的外部库,还是有一种有效的内部方法来实现这一点呢?

缩放后的图像尺寸将为(最大尺寸):

120x120
720x720
1600x1600

质量是一个问题,因为我希望尽可能保留原来的颜色,并最小化压缩产生的图像失真。

谢谢。


你可以使用 Python Wand,它调用 Imagemagick 来实现。我不能说它是最快的。Python OpenCV 可能更快。 - fmw42
缩略图是最方便的工具之一。您可以在自动化脚本中使用Python API或CLI。 - Artyom Vancyan
8个回答

66

我想寻找一些乐趣,所以我对上面提出的各种方法以及我自己的一些想法进行了基准测试。

我收集了1000张iPhone 6S的高分辨率12MP图像,每张图像都是4032x3024像素,并使用了一台8核iMac电脑。

以下是各种技术和结果-每个都在自己的部分中。


方法1 -顺序ImageMagick

这是简单化、未优化的代码。每个图像都被读取并产生缩略图。然后再次读取并产生不同大小的缩略图。

#!/bin/bash

start=$SECONDS
# Loop over all files
for f in image*.jpg; do
   # Loop over all sizes
   for s in 1600 720 120; do
      echo Reducing $f to ${s}x${s}
      convert "$f" -resize ${s}x${s} t-$f-$s.jpg
   done
done
echo Time: $((SECONDS-start))

结果:170 秒


方法二 - 顺序 ImageMagick,单次加载和连续调整大小

这仍然是顺序的,但略微更加智能。每个图像只被读取一次,然后加载的图像缩小三次并保存在三种分辨率下。 改进之处在于每个图像仅被读取一次,而不是3次。

#!/bin/bash

start=$SECONDS
# Loop over all files
N=1
for f in image*.jpg; do
   echo Resizing $f
   # Load once and successively scale down
   convert "$f"                              \
      -resize 1600x1600 -write t-$N-1600.jpg \
      -resize 720x720   -write t-$N-720.jpg  \
      -resize 120x120          t-$N-120.jpg
   ((N=N+1))
done
echo Time: $((SECONDS-start))

结果:76秒


方法3 - GNU Parallel + ImageMagick

这种方法是在之前的方法基础上,使用GNU Parallel并行处理N张图片,其中N是您计算机上的CPU核心数。

#!/bin/bash

start=$SECONDS

doit() {
   file=$1
   index=$2
   convert "$file"                               \
      -resize 1600x1600 -write t-$index-1600.jpg \
      -resize 720x720   -write t-$index-720.jpg  \
      -resize 120x120          t-$index-120.jpg
}

# Export doit() to subshells for GNU Parallel   
export -f doit

# Use GNU Parallel to do them all in parallel
parallel doit {} {#} ::: *.jpg

echo Time: $((SECONDS-start))

结果:18秒


方法四 - GNU Parallel + vips

这与先前的方法相同,但它在命令行中使用vips而不是ImageMagick

#!/bin/bash

start=$SECONDS

doit() {
   file=$1
   index=$2
   r0=t-$index-1600.jpg
   r1=t-$index-720.jpg
   r2=t-$index-120.jpg
   vipsthumbnail "$file"  -s 1600 -o "$r0"
   vipsthumbnail "$r0"    -s 720  -o "$r1"
   vipsthumbnail "$r1"    -s 120  -o "$r2"
}

# Export doit() to subshells for GNU Parallel   
export -f doit

# Use GNU Parallel to do them all in parallel
parallel doit {} {#} ::: *.jpg

echo Time: $((SECONDS-start))

结果:8秒


方法5 - PIL顺序处理

这意味着与Jakob的答案相对应。

#!/usr/local/bin/python3

import glob
from PIL import Image

sizes = [(120,120), (720,720), (1600,1600)]
files = glob.glob('image*.jpg')

N=0
for image in files:
    for size in sizes:
      im=Image.open(image)
      im.thumbnail(size)
      im.save("t-%d-%s.jpg" % (N,size[0]))
    N=N+1

结果:38秒


方法6 - 顺序PIL,单次加载和连续调整大小

这是对Jakob答案的改进,其中图像仅加载一次,然后将其缩小三次,而不是每次重新加载以产生新分辨率。

#!/usr/local/bin/python3

import glob
from PIL import Image

sizes = [(120,120), (720,720), (1600,1600)]
files = glob.glob('image*.jpg')

N=0
for image in files:
   # Load just once, then successively scale down
   im=Image.open(image)
   im.thumbnail((1600,1600))
   im.save("t-%d-1600.jpg" % (N))
   im.thumbnail((720,720))
   im.save("t-%d-720.jpg"  % (N))
   im.thumbnail((120,120))
   im.save("t-%d-120.jpg"  % (N))
   N=N+1

结果:27秒


方法7 - 并行PIL

这个方法旨在对应Audionautics的答案,因为它使用了Python的多进程。它还省去了为每个缩略图大小重新加载图像的需要。

#!/usr/local/bin/python3

import glob
from PIL import Image
from multiprocessing import Pool 

def thumbnail(params): 
    filename, N = params
    try:
        # Load just once, then successively scale down
        im=Image.open(filename)
        im.thumbnail((1600,1600))
        im.save("t-%d-1600.jpg" % (N))
        im.thumbnail((720,720))
        im.save("t-%d-720.jpg"  % (N))
        im.thumbnail((120,120))
        im.save("t-%d-120.jpg"  % (N))
        return 'OK'
    except Exception as e: 
        return e 


files = glob.glob('image*.jpg')
pool = Pool(8)
results = pool.map(thumbnail, zip(files,range((len(files)))))

结果:6秒


方法8-并行OpenCV

这个方法旨在改进bcattle的答案,因为它使用了OpenCV,但它也避免了重新加载图像以生成每个新分辨率输出的需要。

#!/usr/local/bin/python3

import cv2
import glob
from multiprocessing import Pool 

def thumbnail(params): 
    filename, N = params
    try:
        # Load just once, then successively scale down
        im = cv2.imread(filename)
        im = cv2.resize(im, (1600,1600))
        cv2.imwrite("t-%d-1600.jpg" % N, im) 
        im = cv2.resize(im, (720,720))
        cv2.imwrite("t-%d-720.jpg" % N, im) 
        im = cv2.resize(im, (120,120))
        cv2.imwrite("t-%d-120.jpg" % N, im) 
        return 'OK'
    except Exception as e: 
        return e 


files = glob.glob('image*.jpg')
pool = Pool(8)
results = pool.map(thumbnail, zip(files,range((len(files)))))

结果:5秒钟


2
不错的比较,马克。 - fmw42
3
这个答案比以上所有的(并被接受的)答案都要优秀。 - Beracah
1
@Austin Vanilla PIL。 - Mark Setchell
嗨,马克,另一个要点可能是vips和imagemagick使用自适应Lanczos3算法,因此对于1600输出情况下的16个点内核。OpenCV resize()默认使用简单的双线性插值,因此它具有速度优势,但会产生明显较差的质量输出。我预计会出现糊纹效果,特别是在较小的尺寸下。 - jcupitt
PIL/Pillow 会释放 GIL 吗?如果是这样,我想知道线程如何运行,因为它的开销比 multiprocessing 低。使用 multiprocessing.dummy 将会很容易。 - Justin Winokur
显示剩余7条评论

31

你需要PIL,它可以轻松完成这个任务。

from PIL import Image
sizes = [(120,120), (720,720), (1600,1600)]
files = ['a.jpg','b.jpg','c.jpg']

for image in files:
    for size in sizes:
        im = Image.open(image)
        im.thumbnail(size)
        im.save("thumbnail_%s_%s" % (image, "_".join(size)))

如果你迫切需要速度,那么就使用多线程、多进程或选择另一种语言。


3
PIL 的最新版本不再支持 import Image,你应该使用 from PIL import Image - Joakim
1
此外,这段代码只会保存3个缩略图,尽管它会生成所有9个缩略图(你可能需要使用thumbnail_%s_%s-%s" % (image, size[0], size[1]))。 - Matt3o12
从磁盘加载相同的高分辨率图像3次以生成3个缩略图似乎很奇怪。为什么不加载高分辨率图像一次,缩小到1600,写入,缩小到720,写入,缩小到120并写入?肯定会更快。 - Mark Setchell

14

虽然问题有点晚了(已经过去一年了!),但我会借鉴@JakobBowyer答案中的“多进程处理”部分。

这是一个很好的例子,说明了一个尴尬并行问题,因为主要代码不会改变任何外部状态。它只是读取输入,执行计算并保存结果。

由于multiprocessing.Pool提供的map函数,Python实际上在这些类型的问题上表现得相当不错。

from PIL import Image
from multiprocessing import Pool 

def thumbnail(image_details): 
    size, filename = image_details
    try:
        im = Image.open(filename)
        im.thumbnail(size)
        im.save("thumbnail_%s" % filename)
        return 'OK'
    except Exception as e: 
        return e 

sizes = [(120,120), (720,720), (1600,1600)]
files = ['a.jpg','b.jpg','c.jpg']

pool = Pool(number_of_cores_to_use)
results = pool.map(thumbnail, zip(sizes, files))

代码的核心与@JakobBowyer完全相同,但我们将其包装在一个函数中,并通过多进程映射函数将其分散到多个核心上,而不是在单个线程中循环运行。


2
你不想要笛卡尔积而不是 zip 吗? - Mechanical snail
“zip”指的是此函数,而非压缩文件格式。 - Nick
如果将核心数设置为1,上述脚本是否会增加任何好处? - avi
1
@Nick 查找笛卡尔积。这将生成第一张图片的缩略图为120x120,第二张为720x720,最后一张为1600x1600。 - The Tahaan

4
另一种选择是使用Python绑定来使用OpenCV。这可能比PIL或Imagemagick更快。
import cv2

sizes = [(120, 120), (720, 720), (1600, 1600)]
image = cv2.imread("input.jpg")
for size in sizes:
    resized_image = cv2.resize(image, size)
    cv2.imwrite("thumbnail_%d.jpg" % size[0], resized_image) 

这里有一份更完整的步骤说明链接

如果你想要并行运行它,在Py3上使用concurrent.futures或在Py2.7上使用futures包:

import concurrent.futures
import cv2

def resize(input_filename, size):
    image = cv2.imread(input_filename)
    resized_image = cv2.resize(image, size)
    cv2.imwrite("thumbnail_%s%d.jpg" % (input_filename.split('.')[0], size[0]), resized_image)

executor = concurrent.futures.ThreadPoolExecutor(max_workers=3)
sizes = [(120, 120), (720, 720), (1600, 1600)]
for size in sizes:
    executor.submit(resize, "input.jpg", size)

4

我再补充一个答案,因为(我想?)没有人提到质量。

这是我在东伦敦奥林匹克公园用 iPhone 6S 拍摄的照片:

roof of olympic swimming pool

屋顶由一组木板制成,除非你缩小得相当仔细,否则会产生非常严重的莫尔纹效应。我不得不大幅压缩图像才能上传到 stackoverflow --- 如果您有兴趣,原始文件在这里

这里是 cv2 resize 的结果:

$ python3
Python 3.7.3 (default, Apr  3 2019, 05:39:12) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import cv2
>>> x = cv2.imread("IMG_1869.JPG")
>>> y = cv2.resize(x, (120, 90))
>>> cv2.imwrite("cv2.png", y)
True

这里是vipsthumbnail

$ vipsthumbnail IMG_1869.JPG -s 120 -o vips.png

下图展示了vipsthumbnail在左侧的两张缩小后并放大了x2的图片:

缩小至120像素后的结果

(ImageMagick的结果与vipsthumbnail相同)

cv2默认使用双线性插值,因此它有一个固定的2x2掩码。对于输出图像中的每个点,它都会计算输入图像中相应的点并取2x2平均值。这意味着它在每行最多只采样240个点,并简单地忽略其他3750个点!这会产生丑陋的混叠现象。

vipsthumbnail进行了一个更复杂的三级缩小过程。

  1. 它使用libjpeg的缩放加载功能,使用盒状滤波器将4032像素跨度的图像缩小了8倍,得到504 x 378像素的图像。
  2. 它进一步进行了2 x 2盒形滤波器缩小,得到252 x 189像素的图像。
  3. 最后使用5 x 5 Lanczos3核心来获得输出的120 x 90像素图像。

这被认为与完整的Lanczos3核心具有相同的质量,但速度更快,因为它可以大部分时间使用盒状滤波器。


1
是的,当缩小图像的比例很大时,默认参数下的cv2.resize表现非常糟糕。这是许多其他图像库共有的问题,它们忽略了适当调整重采样核心的重要性。但您不必使用默认值,您可以添加interpolation=cv2.INTER_AREA,从而获得与vipsthumbnail相当的结果。 - Mark Ransom

3
如果您已经熟悉ImageMagick,为什么不继续使用Python绑定?请参考PythonMagick

谢谢 - 这比一些内置的Python方法更快吗? - ensnare
1
哪些内置方法?如果您指的是PIL,我不能确定,但ImageMagick更像是瑞士军刀而不是赛马。尽管我从未抱怨过性能,但我仍然享受其令人难以置信的功能。我不知道是否有其他具有类似功能的库。 - Don Question

2

Python 2.7,Windows,x64用户

除了@JakobBowyer@Audionautics之外,PIL相当老旧,你可能会发现自己需要解决问题并寻找正确的版本……相反,使用这里Pillow来源

更新后的代码段如下:

im = Image.open(full_path)
im.thumbnail(thumbnail_size)
im.save(new_path, "JPEG")

生成缩略图的完整枚举脚本:
import os
from PIL import Image

output_dir = '.\\output'
thumbnail_size = (200,200)

if not os.path.exists(output_dir):
    os.makedirs(output_dir)

for dirpath, dnames, fnames in os.walk(".\\input"):
    for f in fnames:
        full_path = os.path.join(dirpath, f)
        if f.endswith(".jpg"):
            filename = 'thubmnail_{0}'.format(f) 
            new_path = os.path.join(output_dir, filename)
            
            if os.path.exists(new_path):
                os.remove(new_path)
                
            im = Image.open(full_path)
            im.thumbnail(thumbnail_size)
            im.save(new_path, "JPEG")

0

将OpenCV图像转换为PIL/Pillow格式非常容易,参见将opencv图像格式转换为PIL图像格式? - Mark Ransom

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