2017-05-19 94 views
6

Próbuję znaleźć najszybsze podejście, aby przeczytać kilka obrazów z katalogu w tablicy numpy. Moim celem końcowym jest obliczenie statystyk takich jak maksymalny, minimalny i n-ty percentyl pikseli ze wszystkich tych obrazów. Jest to proste i szybkie, gdy piksele ze wszystkich obrazów znajdują się w jednej dużej tablicy typu numpy, ponieważ mogę korzystać z wbudowanych metod tablicowych, takich jak .max i .min oraz funkcja np.percentile.Najszybsze podejście do czytania tysięcy obrazów w jedną dużą tablicę numpy

Poniżej przedstawiono przykładowe czasy z 25 obrazami tiff (512 x 512 pikseli). Te testy porównawcze są z używania %%timit w notatniku Juwatera. Różnice są zbyt małe, by mieć praktyczne implikacje dla zaledwie 25 zdjęć, ale zamierzam przeczytać tysiące obrazów w przyszłości.

# Imports 
import os 
import skimage.io as io 
import numpy as np 
  1. Dołączanie do listy

    %%timeit 
    imgs = []  
    img_path = '/path/to/imgs/' 
    for img in os.listdir(img_path):  
        imgs.append(io.imread(os.path.join(img_path, img)))  
    ## 32.2 ms ± 355 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) 
    
  2. Korzystanie ze słownika

    %%timeit  
    imgs = {}  
    img_path = '/path/to/imgs/'  
    for img in os.listdir(img_path):  
        imgs[num] = io.imread(os.path.join(img_path, img))  
    ## 33.3 ms ± 402 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) 
    

dla listy i słownik podejść wyżej Próbowałem zastępując pętlę z odpowiednie zrozumienie z symulacją ilar wyniki w czasie. Próbowałem również wstępnego przydzielenia kluczy słownika bez znaczącej różnicy w czasie wykonania. Aby uzyskać obrazy z listy do dużej tablicy, użyłbym np.concatenate(imgs), który zajmuje tylko ~ 1 ms.

  1. Preallocating do numpy szeregu wzdłuż pierwszego wymiaru

    %%timeit  
    imgs = np.ndarray((512*25,512), dtype='uint16')  
    img_path = '/path/to/imgs/'  
    for num, img in enumerate(os.listdir(img_path)):  
        imgs[num*512:(num+1)*512, :] = io.imread(os.path.join(img_path, img))  
    ## 33.5 ms ± 804 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) 
    
  2. Preallocating do numpy wzdłuż trzeciego wymiaru

    %%timeit  
    imgs = np.ndarray((512,512,25), dtype='uint16')  
    img_path = '/path/to/imgs/'  
    for num, img in enumerate(os.listdir(img_path)):  
        imgs[:, :, num] = io.imread(os.path.join(img_path, img))  
    ## 71.2 ms ± 2.22 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) 
    

początkowo przemyślanej Podejrzane podejścia do wstępnej alokacji byłyby szybsze, ponieważ w pętli nie ma dynamicznej ekspansji zmiennej, ale wydaje się, że tak nie jest. Podejście, które uważam za najbardziej intuicyjne, to ostatnie, w którym każdy obraz zajmuje osobny wymiar wzdłuż trzeciej osi tablicy, ale jest on również najwolniejszy. Dodatkowy czas nie jest związany z samą alokacją, która trwa tylko ~ 1 ms.

mam trzy pytanie o to:

  1. Dlaczego preallocation numpy zbliża się nie szybciej niż słownik i lista rozwiązań?
  2. Jaka jest najszybsza metoda odczytywania tysięcy obrazów w jedną dużą tablicę?
  3. Czy mogę odnieść korzyści z patrzenia poza numpy i obraz scikit, aby jeszcze szybszy moduł do czytania w obrazach? Próbowałem plt.imread(), ale moduł scikit-image.io jest szybszy.
+1

Czy próbowałeś zainicjować tablicę '(25, 512, 512)'? Pierwszy wymiar to zewnętrzny. 'np.array (imgs)' z pierwszego podejścia listy tworzy ten kształt. Większość z tego 33 ms czasu to ładunek, a nie magazyn. Aby to sprawdzić, próbuj ładować bez gromadzenia tablic. – hpaulj

+0

Dzięki @hpaulj! Twoje wskazówki na temat zewnętrznych wymiarów i to, że większość czasu spędziłem z góry, były pomocne. Próbowałem z 300 tiffami o wyższej rozdzielczości (1024x1024 pikseli) i numpy podejście z zewnętrznym wymiarem (albo [300, 1024, 1024] albo [1024, 300, 1024]) jest teraz najszybsze (~ 1s). Drugi to lista i rozwiązania słownikowe (~ 1,7 s), a numpy wymiar wewnętrzny (?) [1024, 1024, 300] jest martwy ostatni (~ 4.6s). Jeśli dodasz odpowiedź, mogę dodać te testy porównawcze i je zaakceptować. –

+0

@hpaulj Jeśli mógłbyś dodać link do bardziej szczegółowych informacji na temat tego, co oznacza, że ​​pierwsze (i drugie?) Wymiary są zewnętrzne, naprawdę bym to docenił. Nie mogę znaleźć niczego istotnego w moich wyszukiwaniach. –

Odpowiedz

1

Część A: Uzyskiwanie dostępu i przypisywanie tablic numpy

Przechodzenie przez sposób elementy są przechowywane w wierszu-major Aby tablic numpy, robisz dobrą rzeczą podczas przechowywania tych elementów wzdłuż ostatniej osi za iterację.Będą one zajmować ciągłe lokalizacje pamięci i jako takie będą najbardziej wydajne w uzyskiwaniu dostępu i przypisywaniu wartości. Tak więc inicjalizacje takie jak np.ndarray((512*25,512), dtype='uint16') lub np.ndarray((25,512,512), dtype='uint16') działałyby najlepiej, jak również wspomniane w komentarzach.

Po kompilacji tych jako funcs dla testów na czasy i karmienia w przypadkowych tablic zamiast obrazów -

N = 512 
n = 25 
a = np.random.randint(0,255,(N,N)) 

def app1(): 
    imgs = np.empty((N,N,n), dtype='uint16') 
    for i in range(n): 
     imgs[:,:,i] = a 
     # Storing along the first two axes 
    return imgs 

def app2(): 
    imgs = np.empty((N*n,N), dtype='uint16') 
    for num in range(n):  
     imgs[num*N:(num+1)*N, :] = a 
     # Storing along the last axis 
    return imgs 

def app3(): 
    imgs = np.empty((n,N,N), dtype='uint16') 
    for num in range(n):  
     imgs[num,:,:] = a 
     # Storing along the last two axes 
    return imgs 

def app4(): 
    imgs = np.empty((N,n,N), dtype='uint16') 
    for num in range(n):  
     imgs[:,num,:] = a 
     # Storing along the first and last axes 
    return imgs 

taktowania -

In [45]: %timeit app1() 
    ...: %timeit app2() 
    ...: %timeit app3() 
    ...: %timeit app4() 
    ...: 
10 loops, best of 3: 28.2 ms per loop 
100 loops, best of 3: 2.04 ms per loop 
100 loops, best of 3: 2.02 ms per loop 
100 loops, best of 3: 2.36 ms per loop 

czasy te potwierdzają teorię wydajności proponowanej na początku, choć Spodziewałem się, że czasy ostatniej konfiguracji będą mierzyć czasy pomiędzy tymi dla app3 i app1, ale może efekt przejścia od ostatniej do pierwszej osi dostępu i przypisania nie jest liniowy. Więcej badań na ten temat może być interesujące (follow up question here).

do claify schematycznie, za które są przechowywane macierzy obrazu, oznaczoną x (obraz 1) i o (Rysunek 2), to mamy:

APP1:

[[[x 0] 
    [x 0] 
    [x 0] 
    [x 0] 
    [x 0]] 

[[x 0] 
    [x 0] 
    [x 0] 
    [x 0] 
    [x 0]] 

[[x 0] 
    [x 0] 
    [x 0] 
    [x 0] 
    [x 0]]] 

Zatem w pamięci, będzie to: [x,o,x,o,x,o..] następujące rząd-główne zamówienie.

Apl2:

[[x x x x x] 
[x x x x x] 
[x x x x x] 
[o o o o o] 
[o o o o o] 
[o o o o o]] 

Zatem w przestrzeni pamięci, byłoby: [x,x,x,x,x,x...o,o,o,o,o..].

App3:

[[[x x x x x] 
    [x x x x x] 
    [x x x x x]] 

[[o o o o o] 
    [o o o o o] 
    [o o o o o]]] 

Zatem w przestrzeni pamięci, to będzie taki sam jak poprzedni.


Część B: Odczyt obrazu z dysku jako tablice

Teraz część na czytanie obraz, widziałem OpenCV na imread się znacznie szybciej.

Jako test, Pobrałem obraz Mony Lisy ze strony wiki i testowane działanie na zdjęciu czytanie -

import cv2 # OpenCV 

In [521]: %timeit io.imread('monalisa.jpg') 
100 loops, best of 3: 3.24 ms per loop 

In [522]: %timeit cv2.imread('monalisa.jpg') 
100 loops, best of 3: 2.54 ms per loop 
+0

Dzięki za odpowiedź! Czy możesz wyjaśnić, dlaczego szybciej jest czytać '[x, x, x, ... o, o, o, ...]' niż czytać '[x, o, x, o, x, o ... ] '? 'x' i' o' byłyby tym samym typem obiektu i całkowitą intensywnością pikseli, prawda? –

+0

Co więcej, czy można rozwinąć "robisz właściwe zachowanie podczas przechowywania tych elementów wzdłuż ostatniej osi w iteracji"? Czy twoje przykłady nie pokazują, że najszybszą alternatywą jest przechowywanie elementów wzdłuż pierwszej osi/wymiaru, tj. 'X' w' np.ndarray ([x, y, z]) '? Czy też "elementy" nie odnoszą się do oddzielnych obrazów? –

+0

@JoelOstblom Do pierwszego zapytania - Jak już wskazałem w poście i powtarzam ponownie: "Będą zajmować ciągłe lokalizacje pamięci i jako takie będą najskuteczniejsze w uzyskiwaniu dostępu i przypisywaniu wartości. ', ponieważ dla' [x, x, x, ... o, o, o, ...] 'przechowujemy image1, to jest' x ', a następnie 'image2', to znaczy' o's' next. Do drugiego zapytania - Edytowane kody z komentarzami. – Divakar

3

W tym przypadku większość czasu zostanie wydane czyta pliki z dysku, a ja wouldn nie martw się zbytnio o czas wypełnienia listy.

W każdym razie, jest to skrypt porównujący cztery metody, bez narzutu na odczyt rzeczywistego obrazu z dysku, ale po prostu odczytanie obiektu z pamięci.

import numpy as np 
import time 
from functools import wraps 


x, y = 512, 512 
img = np.random.randn(x, y) 
n = 1000 


def timethis(func): 
    @wraps(func) 
    def wrapper(*args, **kwargs): 
     start = time.perf_counter() 
     r = func(*args, **kwargs) 
     end = time.perf_counter() 
     print('{}.{} : {} milliseconds'.format(func.__module__, func.__name__, (end - start)*1e3)) 
     return r 
    return wrapper 


@timethis 
def static_list(n): 
    imgs = [None]*n 
    for i in range(n): 
     imgs[i] = img 
    return imgs 


@timethis 
def dynamic_list(n): 
    imgs = [] 
    for i in range(n): 
     imgs.append(img) 
    return imgs 


@timethis 
def list_comprehension(n): 
    return [img for i in range(n)] 


@timethis 
def numpy_flat(n): 
    imgs = np.ndarray((x*n, y)) 
    for i in range(n): 
     imgs[x*i:(i+1)*x, :] = img 

static_list(n) 
dynamic_list(n) 
list_comprehension(n) 
numpy_flat(n) 

Wyniki pokazują:

__main__.static_list : 0.07004200006122119 milliseconds 
__main__.dynamic_list : 0.10294799994881032 milliseconds 
__main__.list_comprehension : 0.05021800006943522 milliseconds 
__main__.numpy_flat : 309.80870099983804 milliseconds 

Oczywiście najlepiej jest lista ze zrozumieniem, jednak nawet podczas wypełniania tablicy numpy, jej zaledwie 310 ms do 1000 obrazów (odczyt z pamięci). Więc znowu, narzut będzie czytany na dysku.

Dlaczego numpy jest wolniejsze?

To jest sposób w jaki numpy zapisuje tablicę w pamięci. Jeśli zmodyfikujemy funkcje listy Pythona w celu przekonwertowania listy na tablicę numpy, czasy są podobne.

Zmodyfikowane funkcje zwracają wartości:

@timethis 
def static_list(n): 
    imgs = [None]*n 
    for i in range(n): 
     imgs[i] = img 
    return np.array(imgs) 


@timethis 
def dynamic_list(n): 
    imgs = [] 
    for i in range(n): 
     imgs.append(img) 
    return np.array(imgs) 


@timethis 
def list_comprehension(n): 
    return np.array([img for i in range(n)]) 

oraz wyniki Czas:

__main__.static_list : 303.32892100022946 milliseconds 
__main__.dynamic_list : 301.86925499992867 milliseconds 
__main__.list_comprehension : 300.76925699995627 milliseconds 
__main__.numpy_flat : 305.9309459999895 milliseconds 

Więc to tylko numpy rzeczą, która to zajmuje więcej czasu i jest to wartość stała w stosunku do tablicy rozmiar ...

Powiązane problemy