Leer y procesar de forma asíncrona una imagen en python

Contexto

A menudo me encontré en la siguiente situación:

  • Tengo una lista de nombres de archivos de imágenes que necesito procesar
  • Leo cada imagen secuencialmente, por ejemplo, scipy.misc.imread
  • Luego hago algún tipo de procesamiento en cada imagen y devuelvo un resultado.
  • Guardo el resultado a lo largo del nombre de archivo de la imagen en un estante

El problema es que la simple lectura de la imagen lleva una cantidad de tiempo no despreciable, en algún momento comparable o incluso más prolongada que el procesamiento de la imagen.

Pregunta

Así que pensé que idealmente podría leer la imagen n + 1 mientras procesaba la imagen n. ¿O incluso es mejor procesar y leer varias imágenes a la vez de una manera óptima determinada automáticamente?

He leído sobre multiprocesamiento, subprocesos, trenzado, gevent y similares, pero no puedo averiguar cuál usar y cómo implementar esta idea. ¿Alguien tiene una solución a este tipo de problema?

Ejemplo minimo

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

La respuesta de Philip es buena, pero solo creará un par de procesos (una lectura, una computadora) que difícilmente alcanzarán el máximo de un sistema moderno de 2 núcleos. Aquí hay una alternativa que utiliza multiprocessing.Pool (específicamente, su método de mapa) que crea procesos que realizan los aspectos de lectura y cálculo, pero que deberían hacer un mejor uso de todos los núcleos que tiene disponibles (asumiendo que hay más archivos que núcleos).

 #!/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() 

Si incremento el número de imágenes a 500, y uso el argumento processes=N para Pool , obtengo

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

en mi i7 de cuatro núcleos hyperthreaded.

Si tiene casos de uso más realistas (es decir, diferentes imágenes reales), es posible que sus procesos pasen más tiempo esperando que los datos de la imagen se carguen desde el almacenamiento (en mis pruebas, se cargan de forma casi instantánea desde el disco almacenado en caché) y luego puede ser vale la pena crear explícitamente más procesos que núcleos para obtener un poco más de superposición de cálculo y carga. Sin embargo, solo sus propias pruebas de escalabilidad en una carga realista y HW pueden decirle qué es lo mejor para usted.

El paquete de multiprocesamiento es bastante fácil de usar. Mira el ejemplo de colas para una guía. Estarás siguiendo el modelo de consumidor productor. Desea que uno (o más) procesos de producción de lectura, y uno (o más) procesos de consumidor que procesan la imagen.

Tu ejemplo se vería así:

 from multiprocessing import Process, Queue import scipy def process_images(q): while not q.empty: im = q.get() # Do stuff 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() 

Esto es un poco más simple que tu idea original. En este ejemplo, el productor se sum a la cola tan rápido como puede, en lugar de simplemente mantenerse al frente del consumidor. Eso podría ser un problema si el productor se adelanta tanto que no tiene suficiente memoria para mantener la cola. Si surgen problemas, puede profundizar en los documentos de multiprocesamiento, pero esto debería ser suficiente para comenzar.