【譯】用Python讀寫海量圖片的方法

翻譯:老齊python


爲何必需要了解更多用Python存儲和訪問圖像的方法?若是你的業務只用到少許圖片,好比根據圖像的色彩分類,,或者用OpenCV實現人臉識別,這時徹底不用擔憂這個問題了。即便藉助Python的PIL,也能輕鬆處理幾百張照片,把圖像以.png.jpg文件的形式存儲在磁盤上,簡單方便又恰當。算法

然而,現實的任務不都如此,好比卷積神經網絡(CNN)等算法能夠處理包含大量圖像的數據集,還能夠從中學習。若是你對此感興趣,能夠申請加入本文的微信公衆號「老齊教室」提供的在線《機器學習案例》,在真實的案例項目中去體驗。數據庫

注:關注微信公衆號:老齊教室,回覆「姓名+手機號+'案例'」,申請得到《機器學習案例集》,本文的代碼和數據,都已經收集到此案例集。數組

WechatIMG6

ImageNet是一個著名的公共圖像數據庫,能夠用於對象分類、識別等任務的模型訓練,它包含超過1400萬張圖像。瀏覽器

想想要花多長時間才能把它們分批地、成百上千次地裝入內存中進行訓練。若是你用常規方法來讀取這些圖片,應該在開始讀取以後,離開電腦去作點別的事情,回來後還不必定完成。可是,若是你但願去谷歌或英偉達工做,就不能這樣玩。緩存

在本文中,你將瞭解:bash

  • 將圖像做爲.png文件存儲在磁盤上
  • 將圖像存儲到LMDB(lightning memory-mapped databases,閃電般的內存映射數據庫)
  • 將圖像存儲到HDF5格式的文件中

咱們還將探索如下內容:微信

  • 爲何替代存儲方法值得考慮
  • 當你讀、寫單個圖像時,這三種方法的性能有什麼不一樣
  • 當你讀、寫多個圖像時,這三種方法的性能有什麼不一樣
  • 這三種方法在磁盤使用方面的比較

若是沒有一種存儲方法聽起來耳熟,不要擔憂:對於這篇文章,你所須要的只是一些基本的Python語言知識以及對圖像(它們其實是由多維數組組成的)、內存的基本理解,好比10MB和10GB之間的差別。網絡

咱們開始吧!併發

安裝程序

下面的項目中,須要一個圖像數據集,以及一些Python包。本文的微信公衆號「老齊教室」對下述全部代碼均提供了在線實驗平臺,請按照前面提示申請使用《機器學習案例集》

數據集

案例中的數據集來自衆所周知的CIFAR-10,它由60000個32x32像素的彩色圖像組成,這些圖像屬於不一樣的對象類別,如狗、貓和飛機。相對而言,CIFAR不是一個很大的數據集,可是若是咱們使用完整的TinyImages數據集,就須要大約400GB的可用磁盤空間,對於學習而言,這太奢侈了。

此數據集已經上傳到本文的微信公衆號「老齊教室」的《機器學習案例集》,能夠按照前述方式申請得到。

如下代碼將從數據集文件中讀取圖像數據,並加載到NumPy數組中:

import numpy as np
import pickle
from pathlib import Path

# Path to the unzipped CIFAR data
data_dir = Path("data/cifar-10-batches-py/")

# Unpickle function provided by the CIFAR hosts
def unpickle(file):
    with open(file, "rb") as fo:
        dict = pickle.load(fo, encoding="bytes")
    return dict

images, labels = [], []
for batch in data_dir.glob("data_batch_*"):
    batch_data = unpickle(batch)
    for i, flat_im in enumerate(batch_data[b"data"]):
        im_channels = []
        # Each image is flattened, with channels in order of R, G, B
        for j in range(3):
            im_channels.append(
                flat_im[j * 1024 : (j + 1) * 1024].reshape((32, 32))
            )
        # Reconstruct the original image
        images.append(np.dstack((im_channels)))
        # Save the label
        labels.append(batch_data[b"labels"][i])

print("Loaded CIFAR-10 training set:")
print(f" - np.shape(images) {np.shape(images)}")
print(f" - np.shape(labels) {np.shape(labels)}")
複製代碼

全部的圖像用images變量引用,對應的元數據保存在labels中。接下來,你能夠安裝如下的三個Python包。

在磁盤上存儲圖像

你須要爲從磁盤上保存和讀取這些圖像的默認方法設置環境。本文假設你的系統上安裝了Python 3.x,並將使用Pillow進行圖像處理:

$ pip install Pillow
複製代碼

或者,若是你願意,可使用Anaconda安裝它:

$ conda install -c conda-forge pillow
複製代碼

注意:PIL是Pillow的原始版本,目前它已經再也不維護,而且與Python 3.x不兼容。若是你先前安裝了PIL,請在安裝Pillow以前卸載它,由於它們彼此。

如今你能夠存儲和讀取磁盤上的圖像了。

LMDB入門

LMDB,有時被稱爲「閃電數據庫」,意味着像閃電般那麼快的內存映射數據庫,因而可知,它速度快,而且使用內存映射文件。它以鍵值對存儲,不是關係數據庫。

在實現方面,LMDB是一個B+樹,這基本上意味着它是存儲在內存中的樹狀圖結構,其中每一個鍵值對都是一個節點,節點能夠有許多子節點。同一級別的節點相互連接以進行快速遍歷。

關鍵在於,B+樹的關鍵組件被設置爲與主機操做系統的文件相對應。當訪問數據庫中的任何鍵值對時,實現效率最大化。因爲LMDB的高性能在很大程度上依賴於這一點,LMDB的效率已經被證實依賴於底層文件系統及其實現。

LMDB效率的另外一個關鍵緣由是:它是內存映射的。這意味着它返回指向鍵和值的內存地址的直接指針,而不須要像大多數其餘數據庫那樣複製內存中的任何內容。

若是你對B+樹不感興趣,別擔憂。後面的操做中,咱們不須要爲了使用LMDB,你不須要了解它們的內部實現。咱們將使用Python的LMDB C,用pip安裝:

$ pip install lmdb
複製代碼

你還能夠選擇經過Anaconda安裝:

$ conda install -c conda-forge python-lmdb
複製代碼

而後在Python交互模式中,用import lmdb檢查,不報錯,就OK了。

HDF5入門

HDF5表明分層數據格式,這種文件格式被稱爲HDF4或HDF5。咱們不須要擔憂HDF4,由於HDF5是當前維護的版本。

有趣的是,HDF起源於(美國)國家超級計算應用中心,是一種便攜式、緊湊的科學數據格式。若是你想知道它是否被普遍使用,請查看美國宇航局的地球數據項目中關於HDF5的簡介。

HDF文件由兩種類型的對象組成:

  • 數據集
  • 羣組

數據集是多維數組,羣組由數據集或其餘組組成。任何大小和類型的多維數組均可以存儲爲數據集,但數據集中的維度和類型必須統一。每一個數據集必須包含一個同構的N維數組。也就是說,由於組和數據集多是嵌套的,因此你仍然能夠得到可能須要的異構性:

$ pip install h5py
複製代碼

與其餘庫同樣,你能夠經過Anaconda安裝:

$ conda install -c conda-forge h5py
複製代碼

若是你import h5py不報錯,那也說明一切都將正確設置。

存儲單個圖像

如今,你已經對這些方法有了一個大體的瞭解,讓咱們直接進入主題:讀、寫文件各須要多長時間,以及將佔用多少內存。經過這些示例,也能夠了解每種方法的基本工做原理。

當我提到文件時,一般指的是不少文件。可是,因爲有些方法可能針對不一樣的操做和文件數量進行了優化,所以進行區分是很重要的。

爲了便於實驗,咱們能夠比較讀取不一樣數量的文件的性能,把圖片的數量按10的倍數從1張增至10萬張。因爲咱們的五批CIFAR-10總共有50000個圖像,所以能夠每一個圖像能夠用兩次,總共得到100000個圖像。

爲了準備實驗,你須要爲每一個方法建立一個文件夾,其中包含全部數據庫文件或圖像:

from pathlib import Path

disk_dir = Path("data/disk/")
lmdb_dir = Path("data/lmdb/")
hdf5_dir = Path("data/hdf5/")
複製代碼

Path不會自動爲你建立文件夾,除非你明確地要求它這樣作:

disk_dir.mkdir(parents=True, exist_ok=True)
lmdb_dir.mkdir(parents=True, exist_ok=True)
hdf5_dir.mkdir(parents=True, exist_ok=True)
複製代碼

在接下來的代碼中,可使用Python標準庫中timeit模塊來對程序計時。

存儲到磁盤

下面的實驗中,輸入是一個單獨的圖像image,當前做爲NumPy數組存儲在內存中。首先要將其做爲.png圖像保存到磁盤上,並使用惟一的圖像ID image_id對其命名。這個步驟可使用以前安裝的Pillow完成:

from PIL import Image
import csv

def store_single_disk(image, image_id, label):
    """ Stores a single image as a .png file on disk. Parameters: --------------- image image array, (32, 32, 3) to be stored image_id integer unique ID for image label image label """
    Image.fromarray(image).save(disk_dir / f"{image_id}.png")

    with open(disk_dir / f"{image_id}.csv", "wt") as csvfile:
        writer = csv.writer(
            csvfile, delimiter=" ", quotechar="|", quoting=csv.QUOTE_MINIMAL
        )
        writer.writerow([label])
複製代碼

這樣能夠保存圖像。在全部實際的應用程序中,你還要關心附加到圖像的元數據。在咱們的示例數據集中,元數據是圖像標籤。將圖像存儲到磁盤時,有幾中不一樣的保存元數據的方式。

一種是將標籤編碼爲圖像名稱。這樣作的好處是不須要任何額外的文件。

可是,它也有一個很大的缺點,即:不管什麼時候處理標籤,都會強迫你處理全部文件。將標籤存儲在一個單獨的文件中能夠容許你單獨處理標籤,而沒必要加載圖像。在上面的代碼中,我已經爲這個實驗將標籤存儲在一個單獨的.csv文件中。

如今讓咱們繼續使用LMDB執行徹底相同的任務。

存儲到LMDB

首先,LMDB是一個鍵值存儲系統,其中每一個條目都保存爲一個字節數組。所以在咱們的例子中,鍵將是每一個圖像的惟一標識符,值將是圖像自己。鍵和值都應該是字符串,此一般的用法是將值序列化爲字符串,而後在讀取時反序列化。

你可使用pickle進行序列化。任何Python對象均可以序列化,所以你也能夠在數據庫中包含圖像元數據。這就避免了從磁盤加載數據集時將元數據附加回圖像數據的麻煩。

你能夠爲圖像及其元數據建立一個基本的Python類:

class CIFAR_Image:
    def __init__(self, image, label):
        # Dimensions of image for reconstruction - not really necessary 
        # for this dataset, but some datasets may include images of 
        # varying sizes
        self.channels = image.shape[2]
        self.size = image.shape[:2]

        self.image = image.tobytes()
        self.label = label

    def get_image(self):
        """ Returns the image as a numpy array. """
        image = np.frombuffer(self.image, dtype=np.uint8)
        return image.reshape(*self.size, self.channels)
複製代碼

其次,由於LMDB是內存映射的,因此新的數據庫須要知道它們將消耗多少內存。這在咱們這裏相對簡單,但在其餘的案例中多是一個巨大的麻煩。LMDB以map_size表示與內存相關的參數。

最後,在transactions中用LMDB執行讀寫操做。你能夠把它們看做相似於傳統數據庫,由數據庫上的一組操做組成。這看起來可能已經比磁盤版本複雜得多,可是請堅持讀下去!

考慮到這三點,讓咱們看看將單個圖像保存到LMDB的代碼:

import lmdb
import pickle

def store_single_lmdb(image, image_id, label):
    """ Stores a single image to a LMDB. Parameters: --------------- image image array, (32, 32, 3) to be stored image_id integer unique ID for image label image label """
    map_size = image.nbytes * 10

    # Create a new LMDB environment
    env = lmdb.open(str(lmdb_dir / f"single_lmdb"), map_size=map_size)

    # Start a new write transaction
    with env.begin(write=True) as txn:
        # All key-value pairs need to be strings
        value = CIFAR_Image(image, label)
        key = f"{image_id:08}"
        txn.put(key.encode("ascii"), pickle.dumps(value))
    env.close()
複製代碼

如今能夠將圖像保存到LMDB。最後,讓咱們看看最後一種方法:HDF5。

存儲到HDF5

記住,HDF5文件能夠包含多個數據集。在這種狀況下,你能夠建立兩個數據集,一個用於圖像,一個用於圖像的元數據:

import h5py

def store_single_hdf5(image, image_id, label):
    """ Stores a single image to an HDF5 file. Parameters: --------------- image image array, (32, 32, 3) to be stored image_id integer unique ID for image label image label """
    # Create a new HDF5 file
    file = h5py.File(hdf5_dir / f"{image_id}.h5", "w")

    # Create a dataset in the file
    dataset = file.create_dataset(
        "image", np.shape(image), h5py.h5t.STD_U8BE, data=image
    )
    meta_set = file.create_dataset(
        "meta", np.shape(label), h5py.h5t.STD_U8BE, data=label
    )
    file.close()
複製代碼

h5py.h5t.STD_U8BE指定將要存儲在數據集中的數據類型,在本例中是無符號8位整數。

注意:數據類型的選擇將強烈影響HDF5的運行時間和存儲要求,所以最好選擇最低要求。

如今,咱們已經回顧了保存單個圖像的三種方法。讓咱們進入下一個步驟。

存儲單個圖像的實驗

你能夠把用於保存單個圖像的全部三個函數放入字典中,該字典能夠在稍後的計時代碼中使用:

_store_single_funcs = dict(
    disk=store_single_disk, lmdb=store_single_lmdb, hdf5=store_single_hdf5
)
複製代碼

萬事俱備只欠東風。讓咱們嘗試保存CIFAR中的第一個圖像及其相應的標籤,並以三種不一樣的方式存儲它:

from timeit import timeit

store_single_timings = dict()

for method in ("disk", "lmdb", "hdf5"):
    t = timeit(
        "_store_single_funcs[method](image, 0, label)",
        setup="image=images[0]; label=labels[0]",
        number=1,
        globals=globals(),
    )
    store_single_timings[method] = t
    print(f"Method: {method}, Time usage: {t}")
複製代碼

注意:在使用LMDB時,可能會看到MapFullError: mdb_txn_commit: MDB_MAP_FULL: Environment mapsize limit reached錯誤。LMDB不重寫預先存在的值,即便它們具備相同的鍵。這有助於加快寫入時間,但也意味着:若是針對同一個LMDB文件進行寫入,則會增長映射數量。若是執行上述函數,請務必先刪除任何預先存在的LMDB文件。

請記住,咱們對運行時間(以毫秒爲單位顯示)以及內存使用狀況感興趣:

Method Save Single Image + Meta Memory
Disk 1.915 ms 8 K
LMDB 1.203 ms 32 K
HDF5 8.243 ms 8 K

這裏有兩個要點:

  • 全部的方法都很是快速。
  • 在磁盤使用方面,LMDB佔用更多。

顯然,儘管LMDB在性能上略有領先,但咱們並無說服任何人爲何不將圖像存儲在磁盤上。畢竟,這是一種人類可讀的格式,你能夠從任何文件系統瀏覽器打開和查看它們!好吧,是時候看看更多的圖片了…

存儲多個圖像

前面已經演示了使用幾種存儲單個圖像的方法,如今要繼續調整代碼,以保存多個圖像,而後運行計時實驗。

調整代碼以用於多個圖像

將多個圖像保存爲.png文件,就像屢次調用store_single_method()同樣簡單。但對於LMDB或HDF5,狀況並不是如此,由於你不但願每一個圖像都有不一樣的數據庫文件。相反,你但願將全部圖像放入一個或多個文件中。

你須要稍微修改代碼並建立三個接受多個圖像的新函數:store_many_disk()store_many_lmdb()store_many_hdf5

def store_many_disk(images, labels):
    """ Stores an array of images to disk Parameters: --------------- images images array, (N, 32, 32, 3) to be stored labels labels array, (N, 1) to be stored """
    num_images = len(images)

    # Save all the images one by one
    for i, image in enumerate(images):
        Image.fromarray(image).save(disk_dir / f"{i}.png")

    # Save all the labels to the csv file
    with open(disk_dir / f"{num_images}.csv", "w") as csvfile:
        writer = csv.writer(
            csvfile, delimiter=" ", quotechar="|", quoting=csv.QUOTE_MINIMAL
        )
        for label in labels:
            # This typically would be more than just one value per row
            writer.writerow([label])

def store_many_lmdb(images, labels):
    """ Stores an array of images to LMDB. Parameters: --------------- images images array, (N, 32, 32, 3) to be stored labels labels array, (N, 1) to be stored """
    num_images = len(images)

    map_size = num_images * images[0].nbytes * 10

    # Create a new LMDB DB for all the images
    env = lmdb.open(str(lmdb_dir / f"{num_images}_lmdb"), map_size=map_size)

    # Same as before — but let's write all the images in a single transaction
    with env.begin(write=True) as txn:
        for i in range(num_images):
            # All key-value pairs need to be Strings
            value = CIFAR_Image(images[i], labels[i])
            key = f"{i:08}"
            txn.put(key.encode("ascii"), pickle.dumps(value))
    env.close()

def store_many_hdf5(images, labels):
    """ Stores an array of images to HDF5. Parameters: --------------- images images array, (N, 32, 32, 3) to be stored labels labels array, (N, 1) to be stored """
    num_images = len(images)

    # Create a new HDF5 file
    file = h5py.File(hdf5_dir / f"{num_images}_many.h5", "w")

    # Create a dataset in the file
    dataset = file.create_dataset(
        "images", np.shape(images), h5py.h5t.STD_U8BE, data=images
    )
    meta_set = file.create_dataset(
        "meta", np.shape(labels), h5py.h5t.STD_U8BE, data=labels
    )
    file.close()
複製代碼

用上面的修改以後的代碼,就能夠將多個文件存儲到磁盤,在這個代碼中,能夠遍歷列表中的每一個圖像。對於LMDB,還須要經過循環,將每一個圖像及其元數據加載到一個CIFAR_Image對象中。

修改幅度最小是用HDF5方法,其實,幾乎沒有任何調整!除了外部限制或數據集大小外,HFD5文件對文件大小沒有限制,所以全部圖像都像之前同樣被填充到一個數據集中,最終存儲爲一個文件。

接下來,你須要經過增大數據集來爲實驗準備數據集。

準備數據集

再次運行這些實驗以前,讓咱們首先將數據集大小增長一倍,這樣咱們就可使用多達100000個圖像進行測試:

cutoffs = [10, 100, 1000, 10000, 100000]

# Let's double our images so that we have 100,000
images = np.concatenate((images, images), axis=0)
labels = np.concatenate((labels, labels), axis=0)

# Make sure you actually have 100,000 images and labels
print(np.shape(images))
print(np.shape(labels))
複製代碼

既然有了足夠的圖像,如今是實驗的時候了。

測試存儲多個圖像的程序

正如前面那樣,能夠建立一個字典來處理帶有store_many_的全部函數並運行實驗:

_store_many_funcs = dict(
    disk=store_many_disk, lmdb=store_many_lmdb, hdf5=store_many_hdf5
)

from timeit import timeit

store_many_timings = {"disk": [], "lmdb": [], "hdf5": []}

for cutoff in cutoffs:
    for method in ("disk", "lmdb", "hdf5"):
        t = timeit(
            "_store_many_funcs[method](images_, labels_)",
            setup="images_=images[:cutoff]; labels_=labels[:cutoff]",
            number=1,
            globals=globals(),
        )
        store_many_timings[method].append(t)

        # Print out the method, cutoff, and elapsed time
        print(f"Method: {method}, Time usage: {t}")
複製代碼

運行代碼,須要你在懸念中靜坐片刻,等待111,110個圖像以三種不一樣的格式分別存儲到磁盤上三次,固然,你還須要消耗大約2GB的磁盤空間。

如今是見證奇蹟的時刻了!全部這些儲存須要多長時間?一幅圖表賽過千言萬語:

第一個圖顯示程序所獲得的存儲時間未經變換的比較,從圖中可知,存儲到「.png」文件和LMDB或HDF5之間的耗時差別較大。

第二張圖對存儲時間進行了對數變換,從圖中可知,HDF5的在開始階段速度比LMDB慢,可是,隨着圖像數量的增長,它的速度略微領先。

準確的測試結果可能會因機器而異,這就是爲何LMDB和HDF5值得考慮的緣由。下面是生成上述圖形的代碼:

import matplotlib.pyplot as plt

def plot_with_legend(
    x_range, y_data, legend_labels, x_label, y_label, title, log=False
):
    """ Displays a single plot with multiple datasets and matching legends. Parameters: -------------- x_range list of lists containing x data y_data list of lists containing y values legend_labels list of string legend labels x_label x axis label y_label y axis label """
    plt.style.use("seaborn-whitegrid")
    plt.figure(figsize=(10, 7))

    if len(y_data) != len(legend_labels):
        raise TypeError(
            "Error: number of data sets does not match number of labels."
        )

    all_plots = []
    for data, label in zip(y_data, legend_labels):
        if log:
            temp, = plt.loglog(x_range, data, label=label)
        else:
            temp, = plt.plot(x_range, data, label=label)
        all_plots.append(temp)

    plt.title(title)
    plt.xlabel(x_label)
    plt.ylabel(y_label)
    plt.legend(handles=all_plots)
    plt.show()

# Getting the store timings data to display
disk_x = store_many_timings["disk"]
lmdb_x = store_many_timings["lmdb"]
hdf5_x = store_many_timings["hdf5"]

plot_with_legend(
    cutoffs,
    [disk_x, lmdb_x, hdf5_x],
    ["PNG files", "LMDB", "HDF5"],
    "Number of images",
    "Seconds to store",
    "Storage time",
    log=False,
)

plot_with_legend(
    cutoffs,
    [disk_x, lmdb_x, hdf5_x],
    ["PNG files", "LMDB", "HDF5"],
    "Number of images",
    "Seconds to store",
    "Log storage time",
    log=True,
)
複製代碼

下面,再演示讀取圖片。

讀取單個圖像

首先,讓咱們考慮讀入單個圖像的三種方法。

從磁盤讀取

在這三種方法中,因爲序列化的緣由,當從內存中讀取圖像文件時,LMDB須要的工做量最大。讓咱們瀏覽一下這些函數,它們分別爲三種存儲格式讀取單個圖像。

首先,從.png.csv文件中讀取單個圖像及其元數據:

def read_single_disk(image_id):
    """ Stores a single image to disk. Parameters: --------------- image_id integer unique ID for image Returns: ---------- image image array, (32, 32, 3) to be stored label associated meta data, int label """
    image = np.array(Image.open(disk_dir / f"{image_id}.png"))

    with open(disk_dir / f"{image_id}.csv", "r") as csvfile:
        reader = csv.reader(
            csvfile, delimiter=" ", quotechar="|", quoting=csv.QUOTE_MINIMAL
        )
        label = int(next(reader)[0])

    return image, label
複製代碼

從LMDB讀取

接下來,演示從LMDB讀取圖像的方法:

def read_single_lmdb(image_id):

    """ Stores a single image to LMDB. Parameters: --------------- image_id integer unique ID for image Returns: ---------- image image array, (32, 32, 3) to be stored label associated meta data, int label """

    # Open the LMDB environment

    env = lmdb.open(str(lmdb_dir / f"single_lmdb"), readonly=True)


    # Start a new read transaction

    with env.begin() as txn:

        # Encode the key the same way as we stored it

        data = txn.get(f"{image_id:08}".encode("ascii"))

        # Remember it's a CIFAR_Image object that is loaded

        cifar_image = pickle.loads(data)

        # Retrieve the relevant bits

        image = cifar_image.get_image()

        label = cifar_image.label

    env.close()


    return image, label
複製代碼

注意這兩行:

  • env = lmdb.open(str(lmdb_dir / f"single_lmdb"), readonly=True),其中readonly=True是指在事務完成以前,不容許對LMDB文件進行寫操做。在數據庫術語中,它至關於獲取一個讀鎖。
  • image = cifar_image.get_image()中的get_image()用處是返回CIFAR_Image對象,這也是反序列化的過程。

這就結束了從LMDB讀取圖像的過程。最後,你還須要對HDF5執行相同的操做。

從HDF5讀取

從HDF5讀取圖像與寫入過程很是類似。下面是打開和讀取HDF5文件並解析相同圖像和元數據的代碼:

def read_single_hdf5(image_id):
    """ Stores a single image to HDF5. Parameters: --------------- image_id integer unique ID for image Returns: ---------- image image array, (32, 32, 3) to be stored label associated meta data, int label """
    # Open the HDF5 file
    file = h5py.File(hdf5_dir / f"{image_id}.h5", "r+")

    image = np.array(file["/image"]).astype("uint8")
    label = int(np.array(file["/meta"]).astype("uint8"))

    return image, label
複製代碼

注意,在文件目錄後用/,後面是表示圖片文件名的變量,這樣能夠訪問文件中的各類數據集。與之前同樣,你能夠建立一個包含全部讀取函數的字典:

_read_single_funcs = dict(
    disk=read_single_disk, lmdb=read_single_lmdb, hdf5=read_single_hdf5
)
複製代碼

準備好,就能夠開始實驗了。

讀取單個圖像的實驗

你可能會認爲:讀取單個圖像的的時間確定會很短,下面是實驗代碼:

from timeit import timeit

read_single_timings = dict()

for method in ("disk", "lmdb", "hdf5"):
    t = timeit(
        "_read_single_funcs[method](0)",
        setup="image=images[0]; label=labels[0]",
        number=1,
        globals=globals(),
    )
    read_single_timings[method] = t
    print(f"Method: {method}, Time usage: {t}")
複製代碼

下面是讀取單個圖像的實驗結果:

Method Read Single Image + Meta
Disk 1.61970 ms
LMDB 4.52063 ms
HDF5 1.98036 ms

直接從磁盤讀取.png.csv文件稍微快一點,但這三種方法執行起來都很快。咱們接下來要作的實驗更有趣。

讀取多個圖像

繼續修改代碼,實現讀取多個圖像的功能,固然,性能很重要。

修改代碼以用於讀取多個圖像

修改前面的函數(注: 詳見本系列第二部分),你可使用read_many_做爲前綴命名函數,而後將它們用於隨後的實驗。與以前同樣,在讀取不一樣數量的圖像時,比較性能是頗有趣的。這些圖像在下面的代碼中重複,以供參考:

def read_many_disk(num_images):
    """ Reads image from disk. Parameters: --------------- num_images number of images to read Returns: ---------- images images array, (N, 32, 32, 3) to be stored labels associated meta data, int label (N, 1) """
    images, labels = [], []

    # Loop over all IDs and read each image in one by one
    
    for image_id in range(num_images):
        images.append(np.array(Image.open(disk_dir / f"{image_id}.png")))

    with open(disk_dir / f"{num_images}.csv", "r") as csvfile:
        reader = csv.reader(
            csvfile, delimiter=" ", quotechar="|", quoting=csv.QUOTE_MINIMAL
        )
        for row in reader:
            labels.append(int(row[0]))
    return images, labels

def read_many_lmdb(num_images):
    """ Reads image from LMDB. Parameters: --------------- num_images number of images to read Returns: ---------- images images array, (N, 32, 32, 3) to be stored labels associated meta data, int label (N, 1) """
    images, labels = [], []
    env = lmdb.open(str(lmdb_dir / f"{num_images}_lmdb"), readonly=True)

    # Start a new read transaction
    
    with env.begin() as txn:
        # Read all images in one single transaction, with one lock
        # We could split this up into multiple transactions if needed
        for image_id in range(num_images):
            data = txn.get(f"{image_id:08}".encode("ascii"))
            # Remember that it's a CIFAR_Image object 
            # that is stored as the value
            cifar_image = pickle.loads(data)
            # Retrieve the relevant bits
            images.append(cifar_image.get_image())
            labels.append(cifar_image.label)
    env.close()
    return images, labels

def read_many_hdf5(num_images):
    """ Reads image from HDF5. Parameters: --------------- num_images number of images to read Returns: ---------- images images array, (N, 32, 32, 3) to be stored labels associated meta data, int label (N, 1) """
    images, labels = [], []

    # Open the HDF5 file
    
    file = h5py.File(hdf5_dir / f"{num_images}_many.h5", "r+")

    images = np.array(file["/images"]).astype("uint8")
    labels = np.array(file["/meta"]).astype("uint8")

    return images, labels

_read_many_funcs = dict(
    disk=read_many_disk, lmdb=read_many_lmdb, hdf5=read_many_hdf5
)
複製代碼

把讀函數和寫函數一塊兒放在字典中,就能夠進行實驗了。

讀取多個圖像的實驗

如今能夠運行程序來讀取多個圖像:

from timeit import timeit

read_many_timings = {"disk": [], "lmdb": [], "hdf5": []}

for cutoff in cutoffs:
    for method in ("disk", "lmdb", "hdf5"):
        t = timeit(
            "_read_many_funcs[method](num_images)",
            setup="num_images=cutoff",
            number=1,
            globals=globals(),
        )
        read_many_timings[method].append(t)

        # Print out the method, cutoff, and elapsed time
        print(f"Method: {method}, No. images: {cutoff}, Time usage: {t}")
複製代碼

如前所述,你能夠將讀取的實驗結果繪製成圖表:

上面的圖表顯示了正常的、未經調整的讀取時間,由圖示可知,從.png文件讀取圖像的時間與從LMDB或HDF5讀取之間存在巨大差別。

相反,下面的圖形是對時間進行對數變換以後的顯示,突顯了它們之間的相對差別。

在實踐中,寫入時間一般比讀取時間更不重要。想象一下,你正在開發一個關於圖像的深層神經網絡,而整個圖像數據集只有一半能夠同時放入內存。神經網絡訓練的每一個階段都須要整個數據集,而模型須要幾百個階段才能收斂。實際上,每一個階段都會將數據集的一半讀入內存。

此時,人們會使用一些技巧,好比訓練pseudo-epochs,使其稍微好一點。

如今,再次看一看上面的圖,40秒和4秒之間的差距忽然就成了6個小時和40分鐘的區別!

若是咱們在同一張圖表上查看讀寫時間,有如下圖示:

當你將圖像存儲爲.png文件時,寫入時間和讀取時間有很大的區別。然而,對於LMDB和HDF5,這種差異就不那麼明顯了。總的來講,即便讀取時間比寫入時間更重要,使用LMDB或HDF5存儲圖像也有充分的理由。

既然你已經看到了LMDB和HDF5的性能優點,那麼讓咱們來看看另外一個重要的指標:磁盤使用率。

磁盤使用狀況

速度並非你惟一感興趣的性能指標。咱們已經在處理很是大的數據集,因此磁盤空間也是一個很是有效和相關的問題。

假設你有一個3TB的圖像數據集。與咱們的CIFAR示例不一樣,你可能已經將它們放在磁盤上的某個位置,所以經過使用另外一種存儲方法,你其實是在備份,而這些備份也必須存儲。這樣作,將在你使用圖像時,爲你帶來巨大的性能優點,但你須要確保有足夠的磁盤空間。

各類存儲方法使用多少磁盤空間?如下是每種方法用於每一數量的圖像所佔的磁盤空間:

HDF5和LMDB都比使用普通的.png圖像存儲佔用更多的磁盤空間。須要注意的是,LMDB和HDF5對磁盤的使用和性能在很大程度上取決於各類因素,包括操做系統。更重要的是,存儲數據的大小。

LMDB經過緩存來提升效率,你不須要了解它的內部工做原理,但請注意:對於較大的圖像,LMDB使用的磁盤空間會明顯增長,由於圖像不適合以LMDB的分支形式存儲,會有許多溢出。上面圖表中的LMDB柱就要從圖表中冒出了。

與你可能使用的普通圖像相比,32x32x3像素圖像相對較小,而且它們容許最佳的LMDB性能。

雖然咱們不會在這裏進行實驗性的探索,但根據我本身對256x256x3或512x512x3像素圖像的體驗,HDF5在磁盤使用方面一般比LMDB稍微高效一些。這是進入最後一節的一個很好的過渡,在最後一節裏定性討論各方法之間的差別。

討論

LMDB和HDF5還有其餘值得了解的顯著特性,簡要討論對這兩種方法的一些評論也是很重要的。

並行存取

在上面的實驗中,咱們沒有測試的一個關鍵比較是並行讀和寫。一般,對於如此大的數據集,你可能但願經過並行化來加快操做速度。

在大多數狀況下,你不會對同時讀取同一圖像的部份內容感興趣,但你會但願同時讀取多個圖像。有了這個並行定義,將.png文件存儲到磁盤實際上容許併發。只要圖像名稱不一樣,沒有什麼能夠阻止你從不一樣的線程一次讀取多個圖像,或者一次寫入多個文件。

LMDB怎麼樣?在一個LMDB環境中能夠同時有多個讀取器,可是隻有一個寫入器,而且寫入器不會阻塞讀取器。你能夠在LMDB技術網站上看到更多關於這方面的信息。

多個應用程序能夠同時訪問同一個LMDB數據庫,同一進程的多個線程也能夠同時訪問LMDB進行讀取。這使讀取過程變得更快:若是將全部CIFAR劃分爲10個集合,那麼能夠爲一個集合中的每一個讀取設置10個進程,至關於把加載時間除以10。

HDF5還提供並行I/O,容許並行讀寫。可是,在實現中,除非你有一個並行文件系統,不然會保持寫鎖,而且訪問是按順序的。

若是你正在處理這樣一個系統,有兩個主要選項。在本文中,關於並行IO的HDF組將更深刻地討論這兩個主要選項。它可能變得至關複雜,最簡單的選擇是智能地將數據集拆分爲多個HDF5文件,這樣每一個進程能夠獨立處理一個.h5文件。

文檔

若是你用谷歌搜索lmdb,至少在英國,第三個搜索結果是IMDb(互聯網電影數據庫)。那不是你要找的!

實際上,LMDB的Python包甚至尚未達到0.94以上的版本,但它被普遍使用並被認爲是穩定的。

至於LMDB技術自己,LMDB技術網站上有更詳細的文檔。除非你從他們的入門頁面開始,不然這種感受有點像在二年級學習微積分。

對於HDF5,在h5py文檔站點上有很是清晰的文檔,還有Christopher Lovell的一篇有用的博客文章,這篇文章很是全面地介紹瞭如何使用h5py包。O'Reilly book、Python和HDF5也是一個很好的入門途徑。

雖然不像初學者所但願的那樣有文檔記錄,但LMDB和HDF5都有很大的用戶社區,所以更深刻的Google搜索一般會產生有用的結果。

更具批判性地看待實現

在存儲系統中沒有烏托邦,LMDB和HDF5都有各自的陷阱。

理解LMDB的一個關鍵點是:在不覆蓋或移動現有數據的狀況下寫入新數據。這是一個設計決策,它容許你很是快速地讀取,就像在咱們的實驗中所看到的那樣,而且還保證數據的完整性和可靠性,而無需保存事務日誌。

不過,請記住,在寫入新數據庫以前,須要定義用於內存分配的map_size參數。這就是LMDB的麻煩所在。假設你已經建立了一個LMDB數據庫,而且一切都很好。你耐心地等待着你的龐大數據集被打包到LMDB中。

而後,接下來,你會記住你須要添加新的數據。即便你在map_size中指定了緩衝區,也可能很容易看到lmdb.MapFullError。除非你想用更新後的map_size從新編寫整個數據庫,不然你必須將新數據存儲在單獨的LMDB文件中。即便一個事務能夠跨越多個LMDB文件,但擁有多個文件仍然是一件麻煩的事。

此外,有些系統對一次能夠佔用多少內存有限制。以我本身的經驗,在使用HPC系統時,就遇到了使人很是沮喪的事,這讓我不得不使用HDF5而不是LMDB。

對於LMDB和HDF5,一次只能將請求的項讀入內存。使用LMDB,鍵值對被逐個讀入內存,而用HDF5,能夠像訪問Python數組那樣訪問dataset對象,索引爲dataset[i],切片爲dataset[i:j]dataset[i:j:interval]

由於系統是優化的,並且取決於你的操做系統,對於數據的訪問順序可能會影響性能。

根據個人經驗,一般狀況下,對於LMDB,按照鍵的順序(鍵值對按照鍵的字母數字順序保存在內存中)訪問時可能會得到更好的性能;而對於HDF5,訪問大範圍將比使用如下方法逐個讀取數據集的每一個元素執行得更好:

# Slightly slower

for i in range(len(dataset)):
    # Read the ith value in the dataset, one at a time
   
讀取數據集中的第i個值,一次一個
   
    do_something_with(dataset[i])

# This is better

data = dataset[:]
for d in data:
    do_something_with(d)
複製代碼

與其餘庫的集成

若是你處理的是很是大的數據集,你極可能會對它們作一些重要的事情。值得考慮的是深度學習庫以及它與LMDB和HDF5的集成類型。

首先,只要將圖像轉換爲預期格式的NumPy數組,全部的庫都支持從磁盤讀取的.png文件。這對全部方法都適用,並且咱們在上面已經看到,將圖像做爲數組讀取相對簡單。

如下是幾個最受歡迎的深度學習庫及其LMDB和HDF5集成:

  • Caffe有一個穩定的、支持良好的LMDB集成,而且它透明地處理讀取步驟。LMDB層也能夠很容易地替換爲HDF5數據庫。
  • Keras使用HDF5格式保存和恢復模型。這意味着TensorFlow也能夠。
  • TensorFlow有一個內置的類LMDBDataset,它提供了一個接口,用於從LMDB文件中讀取輸入數據,而且能夠批量生成迭代器和張量。TensorFlow沒有針對HDF5的內置類,可是能夠編寫繼承Dataset的類。我我的使用了一個自定義的類,它是根據我構造HDF5文件的方式爲優化讀取訪問而設計的。
  • Theano不支持任何特定的文件格式或數據庫,但如前所述,只要它做爲N維數組被讀入,就可使用任何內容。

雖然這遠遠不夠全面,但但願經過一些關鍵的深度學習庫,讓你對LMDB/HDF5的集成有所瞭解。

關於用Python存儲圖像的一些我的看法

在我本身的平常工做中,分析萬億字節的醫學圖像時,我同時使用了LMDB和HDF5,而且瞭解到,對於任何存儲方法,事前籌劃都是相當重要的。

一般,模型須要使用k-fold交叉驗證進行訓練,這涉及到將整個數據集拆分爲k個集(k一般爲10)和正在訓練的k個模型,每一個模型使用不一樣的k個集做爲測試集。這能夠確保模型不會過分擬合數據集,或者,換句話說,沒法對未看到的數據進行良好的預測。

上述的數據集,要保存到單獨的HDF5數據集裏,才能以最大限度地提升效率。有時,單個數據集不能一次加載到內存中,所以即便數據集裏的數據排序也須要預先考慮。

對於LMDB,在建立數據庫以前,我一樣謹慎地提早計劃。在保存圖像以前,有幾個好問題值得問:

  • 如何保存圖像,以便讓大多數讀取都是連續的?
  • 什麼是好的鍵?
  • 如何計算適合的map_size,預測數據集中將來的潛在變化?
  • 單個事務能夠有多大,應該如何細分事務?

無論是哪一種存儲方法,在處理大型圖像數據集時,一個小的規劃就能大有幫助。

結論

你成功了!你如今對一個大問題有了高屋建瓴的瞭解。

在本文中,咱們向你介紹了用Python存儲和訪問大量圖像的三種方法,也許你有機會使用其中的一些方法。本文的全部代碼都在發佈到本文公衆號的在線《機器學習案例集》中,歡迎加入這個案例集,裏面還有其餘有關機器學習的項目。

你已經看到了各類存儲方法如何顯著地影響讀寫時間的證據,以及本文中考慮的三種方法的一些優缺點。雖然將圖像存儲爲.png文件多是最直觀的,可是HDF5或LMDB等方法也有很大的性能優點。

關注微信公衆號:老齊教室。讀深度文章,得精湛技藝,享絢麗人生。

WechatIMG6

原文連接:realpython.com/storing-ima…

相關文章
相關標籤/搜索