Асинхронно считывает и обрабатывает изображение в python

Контекст

Я часто оказывался в следующей ситуации:

  • У меня есть список имен файлов изображений, которые мне нужно обработать
  • Я читаю каждое изображение последовательно, используя, например, scipy.misc.imread
  • Затем я делаю какую-то обработку на каждом изображении и возвращаю результат
  • Я сохраняю результат по имени файла изображения в полке

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

Вопрос

Итак, я думал, что в идеале я мог бы читать изображение n + 1 во время обработки изображения n. Или даже лучше обрабатывать и считывать сразу несколько изображений автоматически оптимальным образом?

Я читал о многопроцессорности, потоках, витой, gevent и т.д., но я не могу понять, какой из них использовать и как реализовать эту идею. У кого-нибудь есть решение этой проблемы?

Минимальный пример

# generate a list of images
scipy.misc.imsave("lena.png", scipy.misc.lena())
files = ['lena.png'] * 100

# a simple image processing task
def process_image(im, threshold=128):
    label, n = scipy.ndimage.label(im > threshold)
    return n

# my current main loop
for f in files:
    im = scipy.misc.imread(f)
    print process_image(im)

Ответ 1

Ответ Филиппа - это хорошо, но создаст только пару процессов (одно чтение, одно вычисление), которое вряд ли позволит создать современную систему с двумя ядрами. Здесь используется альтернатива multiprocessing.Pool (в частности, ее метод карты), который создает процессы, которые выполняют как аспекты чтения, так и вычисления, но которые должны лучше использовать все доступные вам ядра (при условии, что больше файлов, чем ядер).

#!/usr/bin/env python

import multiprocessing
import scipy
import scipy.misc
import scipy.ndimage

class Processor:
    def __init__(self,threshold):
        self._threshold=threshold

    def __call__(self,filename):
        im = scipy.misc.imread(filename)
        label,n = scipy.ndimage.label(im > self._threshold)
        return n

def main():
    scipy.misc.imsave("lena.png", scipy.misc.lena())
    files = ['lena.png'] * 100

    proc=Processor(128)
    pool=multiprocessing.Pool()
    results=pool.map(proc,files)

    print results

if __name__ == "__main__":
    main()

Если я увеличиваю количество изображений до 500 и использую аргумент processes=N для Pool, тогда я получаю

Processes   Runtime
   1         6.2s
   2         3.2s
   4         1.8s
   8         1.5s

на моем четырехъядерном гиперпотоке i7.

Если вы попали в более реалистичные варианты использования (то есть фактические разные изображения), ваши процессы могли бы тратить больше времени, ожидая, пока данные изображения загрузятся из хранилища (в моем тестировании они загружаются практически мгновенно с кэшированного диска), а затем возможно, стоит явно создать больше процессов, чем ядра, чтобы получить больше перекрытий вычислений и нагрузки. Только ваше собственное тестирование масштабируемости на реалистичной нагрузке и HW может рассказать вам, что действительно лучше для вас.

Ответ 2

Многопроцессорный пакет довольно прост в использовании. Посмотрите на пример очереди для руководства. Вы будете следовать за моделью потребителя производителя. Требуется, чтобы один (или более) производитель обрабатывал изображения для чтения, а один (или более) потребительские процессы выполняли обработку изображений.

Ваш пример будет выглядеть примерно так:

from multiprocessing import Process, Queue
import scipy

def process_images(q):
    while not q.empty():
        im = q.get()
        # Do stuff with item from queue

def read_images(q, files):
    for f in files:
        q.put(scipy.misc.imread(f))

if __name__ == '__main__':
    q = Queue()

    producer = Process(target=read_images, args=(q, files))
    producer.start()
    consumer = Process(target=process_images, args=(q, ))
    consumer.start()

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