Подтвердить что ты не робот

Каков самый быстрый способ создания эскизов изображений в Python?

Я создаю фотогалерею на Python и хочу иметь возможность быстро создавать эскизы для изображений с высоким разрешением.

Какой самый быстрый способ создания высококачественных миниатюр для различных источников изображений?

Должен ли я использовать внешнюю библиотеку, например imagemagick, или есть эффективный внутренний способ сделать это?

Размеры измененных изображений будут (максимальный размер):

120x120
720x720
1600x1600

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

Спасибо.

4b9b3361

Ответ 1

Вы хотите 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)))

Если вам отчаянно нужна скорость. Тогда проденьте это, многопроцессорный или получите другой язык.

Ответ 2

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

Я собрал вместе 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 секунд


Метод 2 - Последовательный 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 - это количество ядер ЦП на вашем компьютере.

#!/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 секунд


Метод 4 - 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

Это должно соответствовать ответу Якоба.

#!/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 с одиночной нагрузкой & последовательное изменение размера

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

#!/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 секунд

Ответ 3

Немного опоздал на вопрос (всего год!), Но я вернусь к ответу "многопроцессорность" в ответе @JakobBowyer.

Это хороший пример смущающей параллельной проблемы, так как основной бит кода не изменяет никакое внешнее по отношению к себе состояние. Он просто читает входные данные, выполняет их вычисления и сохраняет результат.

Python действительно неплохо справляется с такими проблемами благодаря функции map, предоставляемой multiprocessing.Pool.

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, но вместо того, чтобы запускать его в цикле в одном потоке, мы обернули его в функцию, распределив ее по нескольким ядрам с помощью функции многопроцессорной карты.

Ответ 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) 

Здесь более полное пошаговое руководство здесь.

Если вы хотите запустить его параллельно, используйте concurrent.futures в Py3 или futures на Py2.7:

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)

Ответ 5

Если вы уже знакомы с imagemagick, почему бы не придерживаться привязок python?

PythonMagick

Ответ 6

Пользователи Python 2.7, Windows, x64

В дополнение к @JakobBowyer и @Audionautics, PIL довольно старый, и вы можете найти самостоятельно устраните неполадки и ищите правильную версию... вместо этого используйте Pillow из здесь (источник)

обновленный фрагмент будет выглядеть так:

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

полное перечисление script для создания миниатюр:

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

Ответ 7

Еще один ответ, так как (я думаю?) Никто не упомянул качество.

Вот фотография, которую я сделал с iPhone 6S в Олимпийском парке в Восточном Лондоне:

roof of olympic swimming pool

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

Здесь cv2 изменить размер:

$ 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

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

results of downsize to 120 pixels across

(ImageMagick дает те же результаты, что и vipsthumbnail)

По умолчанию cv2 имеет значение BILINEAR, поэтому он имеет фиксированную маску 2x2. Для каждой точки на выходном изображении он вычисляет соответствующую точку на входе и получает среднее значение 2x2. Это означает, что на самом деле только выборка не более 240 точек в каждой строке и просто игнорирование остальных 3750! Это приводит к появлению уродливых алиасов.

vipsthumbnail делает более сложное сокращение в три этапа.

  1. Он использует функцию сжатия при загрузке libjpeg, чтобы уменьшить изображение в 8 раз по каждой оси с помощью блочного фильтра, чтобы превратить 4032 пикселя по всему изображению в 504 x 378 пикселей.
  2. Он сжимает еще один фильтр размером 2 x 2, чтобы получить 252 x 189 пикселей.
  3. Он заканчивается ядром Lanczos3 5 x 5, чтобы получить изображение размером 120 x 90 пикселей.

Предполагается, что это даст эквивалентное качество полноценному ядру Lanczos3, но будет быстрее, поскольку большую часть времени он может фильтровать по блокам.