个性化阅读
专注于IT技术分析

用Python存储和访问大量图像的三种方法

本文概述

你为什么想更多地了解Python中存储和访问图像的不同方式?如果你要按颜色分割少数图像或使用OpenCV一张一张地检测人脸, 则无需担心。即使你正在使用Python Imaging Library(PIL)绘制几百张照片, 也仍然不需要。将图像以.png或.jpg文件形式存储在磁盘上既合适又合适。

但是, 给定任务所需的图像数量越来越多。卷积神经网络之类的算法(也称为卷积神经网络或CNN)可以处理庞大的图像数据集, 甚至可以从中学习。如果你有兴趣, 可以阅读更多有关卷积网如何用于自拍排名或情感分析的信息。

ImageNet是一个著名的公共图像数据库, 用于训练对象分类, 检测和分割等任务的模型, 由超过1400万幅图像组成。

考虑将它们全部加载到内存中进行分批(可能数百次或数千次)培训需要多长时间。继续阅读, 你会坚信这将花费相当长的时间-至少要花很长时间才能离开计算机并做其他很多事情, 而你希望在Google或NVIDIA工作。

在本教程中, 你将了解:

  • 将图像作为.png文件存储在磁盘上
  • 将图像存储在闪电存储映射数据库(LMDB)中
  • Storing images in hierarchical data format (HDF5)

你还将探索以下内容:

  • 为什么需要考虑其他存储方式
  • 在读取和写入单个图像时, 性能上的区别是什么
  • 在读取和写入许多图像时, 性能差异是什么?
  • 三种方法在磁盘使用率方面的比较

如果没有任何一种存储方法, 请不用担心:对于本文而言, 你所需要的只是在Python中有一个相当扎实的基础, 并且对图像有基本的了解(它们实际上是由数字的多维数组组成), 并且相对内存, 例如10MB和10GB之间的差异。

让我们开始吧!

免费红利:单击此处以获得Python人脸检测和OpenCV示例迷你指南, 该指南向你展示了现实世界中Python计算机视觉技术的实用代码示例。

移除广告

设定

你将需要一个图像数据集进行试验, 以及一些Python软件包。

可玩的数据集

我们将使用加拿大高级研究学院的图像数据集, 即众所周知的CIFAR-10, 该数据集由属于不同对象类别(例如狗, 猫和飞机)的60, 000个32×32像素彩色图像组成。相对而言, CIFAR并不是一个很大的数据集, 但是如果我们要使用完整的TinyImages数据集, 那么你将需要大约400GB的可用磁盘空间, 这可能是一个限制因素。

如本技术报告第3章所述, 该数据集归功于Alex Krizhevsky, Vinod Nair和Geoffrey Hinton。

如果你想按照本文中的代码示例进行操作, 可以在此处下载CIFAR-10, 选择Python版本。你将牺牲163MB的磁盘空间:

cifar-10-数据集

图片:A。Krizhevsky

下载并解压缩该文件夹后, 你会发现这些文件不是人类可读的图像文件。它们实际上已经被序列化, 并使用cPickle批量保存。

尽管在本文中我们将不考虑pickle或cPickle, 除了提取CIFAR数据集外, 值得一提的是, Python pickle模块的主要优势在于无需任何额外的代码或转换就可以序列化任何Python对象。在处理大量数据时, 还存在潜在的严重缺陷, 即带来安全风险并且不能很好地应对。

以下代码解开五个批处理文件中的每个批处理文件, 并将所有图像加载到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变量的RAM中, 并且其相应的元数据在标签中, 并可供你操作。接下来, 你可以安装将用于这三种方法的Python软件包。

注意:最后一个代码块使用了f字符串。你可以在Python 3的f字符串:改进的字符串格式语法(指南)中了解有关它们的更多信息。

在磁盘上存储映像的设置

你需要为环境设置默认的从磁盘保存和访问这些图像的方法。本文将假定你在系统上安装了Python 3.x, 并将使用Pillow进行图像处理:

$ pip install Pillow

另外, 如果你愿意, 可以使用Anaconda安装它:

$ conda install -c conda-forge pillow

注意:PIL是Python Imaging Library的原始版本, 不再维护且与Python 3.x不兼容。如果你以前安装过PIL, 请确保在安装Pillow之前先将其卸载, 因为它们不能同时存在。

现在, 你可以从磁盘存储和读取图像了。

移除广告

LMDB入门

LMDB(有时称为“ Lightning数据库”)代表Lightning内存映射数据库, 因为它速度快且使用内存映射文件。它是键值存储, 而不是关系数据库。

在实现方面, LMDB是B +树, 这基本上意味着它是存储在内存中的树状图结构, 其中每个键值元素都是一个节点, 并且节点可以有许多子级。同一级别上的节点相互链接以快速遍历。

至关重要的是, 将B +树的关键组件设置为与主机操作系统的页面大小相对应, 从而在访问数据库中的任何键值对时都可以最大程度地提高效率。由于LMDB的高性能很大程度上依赖于这一点, 因此已证明LMDB的效率取决于基础文件系统及其实现。

LMDB效率的另一个关键原因是它是内存映射的。这意味着它直接返回指向键和值的内存地址的指针, 而无需像大多数其他数据库一样复制内存中的任何内容。

那些想深入了解B +树的内部实现细节的人可以在B +树上查看本文, 然后尝试使用这种节点插入的可视化方法。

如果B +树对你不感兴趣, 请不要担心。你不需要了解它们的内部实现就可以使用LMDB。我们将对LMDB C库使用Python绑定, 该库可通过pip安装:

$ pip install lmdb

你还可以选择通过Anaconda安装:

$ conda install -c conda-forge python-lmdb

检查是否可以从Python Shell导入lmdb, 并且一切顺利。

HDF5入门

HDF5代表分层数据格式, 一种称为HDF4或HDF5的文件格式。我们不必担心HDF4, 因为HDF5是当前维护的版本。

有趣的是, HDF起源于国家超级计算应用中心, 它是一种便携式, 紧凑的科学数据格式。如果你想知道它是否被广泛使用, 请从其地球数据项目中查看NASA关于HDF5的介绍。

HDF文件包含两种类型的对象:

  1. 数据集
  2. 团体

数据集是多维数组, 并且组由数据集或其他组组成。任何大小和类型的多维数组都可以存储为数据集, 但是维度和类型在数据集中必须一致。每个数据集必须包含齐次N维数组。就是说, 由于组和数据集可能是嵌套的, 因此你仍然可以获得所需的异质性:

$ pip install h5py

与其他库一样, 你可以通过Anaconda替代安装:

$ conda install -c conda-forge h5py

如果你可以从Python Shell导入h5py, 则说明一切都已正确设置。

移除广告

存储单个图像

既然你已大致了解了这些方法, 那么让我们直接看一看, 对我们关心的基本任务进行定量比较:读取和写入文件需要多长时间, 以及将使用多少磁盘内存。这还将作为方法的基本介绍, 并提供有关如何使用方法的代码示例。

当我提到“文件”时, 通常指的是很多文件。但是, 重要的是要有所区别, 因为某些方法可能针对不同的操作和文件数量进行了优化。

出于实验目的, 我们可以比较不同数量文件之间的性能, 从单个图像到100, 000个图像, 其系数是10。由于我们的五批CIFAR-10总共可添加50, 000张图像, 因此我们可以使用每个图像两次以获取100, 000张图像。

为了准备实验, 你需要为每种方法创建一个文件夹, 其中将包含所有数据库文件或图像, 并将这些目录的路径保存在变量中:

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模块来帮助安排实验时间。

尽管本文的主要目的不是学习不同Python软件包的API, 但了解如何实现它们将很有帮助。我们将遵循通用原则以及用于进行存储实验的所有代码。

存储到磁盘

我们为此实验输入的是单个图像图像, 当前以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。

最后, 在事务中执行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()

注意:最好计算每个键值对占用的确切字节数。

对于大小可变的图像数据集, 这将是一个近似值, 但是你可以使用sys.getsizeof()来获得一个合理的近似值。请记住, sys.getsizeof(CIFAR_Image)将仅返回类定义的大小(即1056), 而不返回实例化对象的大小。

该函数也将无法完全计算嵌套项目, 列表或包含对其他对象的引用的对象。

另外, 你可以使用pympler通过确定对象的确切大小来节省一些计算。

现在你可以将映像保存到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位整数。你可以在此处查看HDF的预定义数据类型的完整列表。

注意:数据类型的选择会严重影响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:达到环境mapsize限制错误。请务必注意, 即使LMDB具有相同的密钥, 也不会覆盖它们。

这有助于缩短写入时间, 但是这也意味着, 如果在同一LMDB文件中多次存储图像, 则会用完地图大小。如果你运行存储功能, 请确保首先删除任何先前存在的LMDB文件。

请记住, 我们对运行时感兴趣(以秒为单位在此处显示), 以及内存使用情况:

方法 保存单张图片+元 记忆
Disk 1.915毫秒 8 K
LMDB 1.203毫秒 32 K
HDF5 8.243毫秒 8 K

这里有两个要点:

  1. 所有这些方法都非常快捷。
  2. 在磁盘使用方面, LMDB使用更多磁盘。

显然, 尽管LMDB在性能上略有领先, 但我们还没有说服任何人为什么不仅仅将图像存储在磁盘上。毕竟, 这是一种人类可读的格式, 你可以在任何文件系统浏览器中打开并查看它们!好了, 该看更多图片了……

存储许多图像

你已经看到了使用各种存储方法保存单个图像的代码, 因此现在我们需要调整代码以保存许多图像, 然后运行定时实验。

调整许多图像的代码

将多个图像另存为.png文件就像多次调用store_single_method()一样简单。但这不适用于LMDB或HDF5, 因为你不希望每个图像都有不同的数据库文件。相反, 你想将所有图像放入一个或多个文件中。

你将需要稍微更改代码并创建三个可以接受多个图像的新函数store_many_disk(), store_many_lmdb()和store_many_hdf5:

 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文件对文件大小没有任何限制, 因此像以前一样, 所有图像都被填充到单个数据集中。

接下来, 你需要通过增加数据集的大小来准备实验数据集。

移除广告

准备数据集

在再次运行实验之前, 我们首先将数据集大小增加一倍, 以便可以测试多达100, 000张图像:

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张图像以三种不同格式分别存储到磁盘上3次。你还需要告别大约2 GB的磁盘空间。

现在是关键时刻!所有这些存储需要多长时间?一张图片胜过千言万语:

储存大量图像
多次存储日志

第一张图显示了正常的, 未经调整的存储时间, 突出显示了存储到.png文件和LMDB或HDF5之间的巨大差异。

第二张图显示了时序的对数, 突出显示HDF5的启动速度比LMDB慢, 但是在图像数量较大的情况下, HDF5的输出略微领先。

尽管确切结果可能会因你的计算机而异, 但这就是为什么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中读取相同的映像和元:

 1 def read_single_lmdb(image_id):
 2     """ Stores a single image to LMDB.
 3         Parameters:
 4         ---------------
 5         image_id    integer unique ID for image
 6 
 7         Returns:
 8         ----------
 9         image       image array, (32, 32, 3) to be stored
10         label       associated meta data, int label
11     """
12     # Open the LMDB environment
13     env = lmdb.open(str(lmdb_dir / f"single_lmdb"), readonly=True)
14 
15     # Start a new read transaction
16     with env.begin() as txn:
17         # Encode the key the same way as we stored it
18         data = txn.get(f"{image_id:08}".encode("ascii"))
19         # Remember it's a CIFAR_Image object that is loaded
20         cifar_image = pickle.loads(data)
21         # Retrieve the relevant bits
22         image = cifar_image.get_image()
23         label = cifar_image.label
24     env.close()
25 
26     return image, label

以下是与上述代码段无关的几点:

  • 第13行:readonly = True标志指定在事务完成之前, 不允许在LMDB文件上进行任何写操作。在数据库术语中, 这相当于获得读锁定。
  • 第20行:要检索CIFAR_Image对象, 你需要颠倒我们在编写它时对其进行酸洗的步骤。这是对象的get_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}")

以下是读取单个图像的实验结果:

方法 阅读单张图片+元
Disk 1.61970毫秒
LMDB 4.52063毫秒
HDF5 1.98036毫秒

直接从磁盘读取.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读取之间的巨大差异。

相反, 底部的图形显示了时序的对数, 突出显示了较少图像的相对差异。即, 我们可以看到HDF5是如何从后面开始的, 但是随着图像的增加, 它始终比LMDB快一点。

绘制显示/隐藏读取时序图

使用与写入时序相同的绘图功能, 我们具有以下功能:

disk_x_r = read_many_timings["disk"]
lmdb_x_r = read_many_timings["lmdb"]
hdf5_x_r = read_many_timings["hdf5"]

plot_with_legend(
    cutoffs, [disk_x_r, lmdb_x_r, hdf5_x_r], ["PNG files", "LMDB", "HDF5"], "Number of images", "Seconds to read", "Read time", log=False, )

plot_with_legend(
    cutoffs, [disk_x_r, lmdb_x_r, hdf5_x_r], ["PNG files", "LMDB", "HDF5"], "Number of images", "Seconds to read", "Log read time", log=True, )

实际上, 写时间通常不如读时间重要。想象一下, 你正在针对图像训练一个深度神经网络, 而整个图像数据集中只有一半可以一次放入RAM。训练网络的每个时期都需要整个数据集, 并且模型需要数百个时期才能收敛。你实际上将在每个时期将数据集的一半读取到内存中。

人们可以采用多种技巧, 例如训练伪纪元以使其稍好一些, 但是你可以理解。

现在, 再次查看上面的阅读图。 40秒和4秒的读取时间之间的时间差突然就是模型训练等待6个小时或40分钟!

如果我们在同一张图表上查看读写时间, 则将具有以下内容:

读写

绘制显示/隐藏读写时序图

你可以使用相同的绘图功能在一个图形上绘制所有读写时序:

plot_with_legend(
    cutoffs, [disk_x_r, lmdb_x_r, hdf5_x_r, disk_x, lmdb_x, hdf5_x], [
        "Read PNG", "Read LMDB", "Read HDF5", "Write PNG", "Write LMDB", "Write HDF5", ], "Number of images", "Seconds", "Log Store and Read Times", log=False, )

当你将图像存储为.png文件时, 读写时间之间会有很大的差异。但是, 使用LMDB和HDF5, 区别不明显。总体而言, 即使读取时间比写入时间更为关键, 也存在使用LMDB或HDF5存储图像的强烈理由。

既然你已经了解了LMDB和HDF5的性能优势, 那么让我们看一下另一个关键指标:磁盘使用率。

移除广告

考虑磁盘使用情况

速度不是你可能感兴趣的唯一性能指标。我们已经在处理非常大的数据集, 因此磁盘空间也是一个非常有效且相关的问题。

假设你有一个3TB的图像数据集。大概你已经将它们存储在磁盘上的某个地方, 这与我们的CIFAR示例不同, 因此, 通过使用备用存储方法, 你实际上是在复制它们, 也必须将它们存储。这样做会在使用图像时为你带来巨大的性能优势, 但是你需要确保有足够的磁盘空间。

各种存储方法使用多少磁盘空间?这是每种方法用于每种图像数量的磁盘空间:

存储映像

生成显示/隐藏磁盘空间使用情况的条形图

我使用Linux du -h -c folder_name / *命令来计算系统上的磁盘使用情况。由于四舍五入, 此方法固有一些近似值, 但这是一般的比较:

# Memory used in KB
disk_mem = [24, 204, 2004, 20032, 200296]
lmdb_mem = [60, 420, 4000, 39000, 393000]
hdf5_mem = [36, 304, 2900, 29000, 293000]

X = [disk_mem, lmdb_mem, hdf5_mem]

ind = np.arange(3)
width = 0.35

plt.subplots(figsize=(8, 10))
plots = [plt.bar(ind, [row[0] for row in X], width)]
for i in range(1, len(cutoffs)):
    plots.append(
        plt.bar(
            ind, [row[i] for row in X], width, bottom=[row[i - 1] for row in X]
        )
    )

plt.ylabel("Memory in KB")
plt.title("Disk memory used by method")
plt.xticks(ind, ("PNG", "LMDB", "HDF5"))
plt.yticks(np.arange(0, 400000, 100000))

plt.legend(
    [plot[0] for plot in plots], ("10", "100", "1, 000", "10, 000", "100, 000")
)
plt.show()

与使用普通的.png图像存储相比, HDF5和LMDB都占用更多的磁盘空间。请务必注意, LMDB和HDF5磁盘的使用情况和性能在很大程度上取决于各种因素, 包括操作系统, 更关键的是, 你存储的数据大小。

LMDB通过缓存和利用OS页面大小来提高效率。你无需了解其内部工作原理, 但请注意, 使用较大的映像时, 由于LMDB的叶子页, 树中的常规存储位置以及相反, 你将有很多溢出页面。上方图表中的LMDB条形图将跳出图表。

与你可以使用的平均图像相比, 我们的32x32x3像素图像相对较小, 并且它们可实现最佳的LMDB性能。

虽然我们不会在这里进行实验性探索, 但以我自己对256x256x3或512x512x3像素的图像的经验, HDF5在磁盘使用方面通常比LMDB效率更高。这是进入最后部分的良好过渡, 这是对方法之间差异的定性讨论。

讨论区

LMDB和HDF5还有其他与众不同的功能, 这些都是值得了解的, 简短讨论这两种方法的一些批评也很重要。如果你想了解更多信息, 请在讨论中包括几个链接。

并行访问

我们在上述实验中未测试的关键比较是并发读写。通常, 对于如此大的数据集, 你可能希望通过并行化来加快操作速度。

在大多数情况下, 你不会对同时读取同一张图像的部分感兴趣, 但是你希望一次读取多张图像。使用并发定义, 实际上将.png文件存储到磁盘上可以实现完全的并发。只要图像名称不同, 什么都不会阻止你一次从不同线程读取多个图像或一次写入多个文件。

LMDB呢?一次在LMDB环境中可以有多个读取器, 但是只有一个写入器, 并且写入器不会阻止读取器。你可以在LMDB技术网站上了解有关此内容的更多信息。

多个应用程序可以同时访问同一LMDB数据库, 并且来自同一进程的多个线程也可以同时访问LMDB进行读取。这样可以缩短读取时间:如果将所有CIFAR分为十组, 则可以在一组中为每个读取设置十个进程, 并且将加载时间除以十。

HDF5还提供并行I / O, 允许并发读写。但是, 在实现中, 除非拥有并行文件系统, 否则将保持写锁定, 并且访问是顺序的。

如果你正在使用这样的系统, 则有两个主要选项, HDF组关于并行IO的内容将在本文中进行更深入的讨论。这可能会变得非常复杂, 最简单的选择是将数据集智能地拆分为多个HDF5文件, 以便每个进程可以独立处理一个.h5文件。

文献资料

如果你至少在英国使用Google lmdb, 则第三个搜索结果是Internet电影数据库IMDb。那不是你要找的东西!

实际上, LMDB的Python绑定有一个主要的文档来源, 该文档宿主在“读取文档LMDB”上。尽管Python软件包甚至还没有达到0.94以上的版本, 但它已经被广泛使用并且被认为是稳定的。

至于LMDB技术本身, LMDB技术网站上有更多详细的文档, 除非你从其入门页面开始, 否则它的感觉有点像在学习二年级的微积分。

对于HDF5, h5py docs网站上有非常清晰的文档, 以及Christopher Lovell的有用的博客文章, 该博客很好地概述了如何使用h5py软件包。 O’Reilly的书, 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数组一样访问数据集对象, 并带有索引数据集[i], 范围, 数据集[i:j]和其他拼接数据集[ i:j:interval]。

由于系统的优化方式以及操作系统的不同, 访问项目的顺序可能会影响性能。

以我的经验, 通常来说, 对于LMDB, 按键顺序访问项(键-值对按键按字母数字顺序存储在内存中)时, 可能会获得更好的性能;对于HDF5, 访问大范围比读取要好数据集的每个元素都使用以下命令一个接一个地:

# Slightly slower
for i in range(len(dataset)):
    # Read the ith value in the dataset, one at a time
    do_something_with(dataset[i])

# This is better
data = dataset[:]
for d in data:
    do_something_with(d)

如果你正在考虑选择一种文件存储格式来编写软件, 那么更不用说Cyrille Rossant离开HDF5了HDF5的陷阱以及Konrad Hinsen对HDF5的回应以及数据管理的未来, 展示了如何在他自己的用例中使用许多较小的数据集而不是几个巨大的数据集来避免某些陷阱。请注意, 相对较小的数据集大小仍为数GB。

与其他图书馆的整合

如果你要处理非常大的数据集, 则很有可能会对它们做一些重要的事情。值得考虑的是深度学习库以及与LMDB和HDF5的集成方式。

首先, 所有库都支持以.png文件的形式从磁盘读取图像, 只要将它们转换为预期格式的NumPy数组即可。所有方法都适用, 并且我们已经在上面看到, 将图像读取为数组相对容易。

以下是几个最受欢迎的深度学习库及其LMDB和HDF5集成:

  • Caffe具有稳定且得到良好支持的LMDB集成, 并且可以透明地处理读取步骤。 LMDB层也可以轻松替换为HDF5数据库。
  • Keras使用HDF5格式保存和还原模型。这意味着TensorFlow也可以。
  • TensorFlow具有内置的LMDBDataset类, 该类提供了用于从LMDB文件中读取输入数据的接口, 并且可以批量生成迭代器和张量。 TensorFlow没有用于HDF5的内置类, 但是可以编写一个继承自Dataset类的类。我个人完全使用一个自定义类, 该类旨在根据我构建HDF5文件的方式实现最佳读取访问。
  • Theano本身不支持任何特定的文件格式或数据库, 但如前所述, 只要可以将其读取为N维数组, 就可以使用任何东西。

尽管还不全面, 但是希望通过一些关键的深度学习库使你对LMDB / HDF5集成有所了解。

关于在Python中存储图像的一些个人见解

在我自己的日常工作中, 分析TB级的医学图像时, 我同时使用了LMDB和HDF5, 并且了解到, 对于任何存储方法, 预见都是至关重要的。

通常, 需要使用k倍交叉验证来训练模型, 这涉及将整个数据集拆分为k个集合(k通常为10), 并且要训练k个模型, 每个模型都使用不同的k集作为测试集。这可以确保模型不会过度拟合数据集, 或者换句话说, 无法对看不见的数据做出良好的预测。

制作k集的标准方法是对每个k集的数据集中表示的每种数据类型进行均等表示。因此, 将每个k集保存到单独的HDF5数据集中可最大程度地提高效率。有时, 单个k集无法立即加载到内存中, 因此, 即使数据集中的数据顺序也需要一定的考虑。

使用LMDB, 我同样会在创建数据库之前仔细计划。在保存图像之前, 有几个好问题值得问:

  • 如何保存图像, 以便大多数读取是连续的?
  • 什么是好钥匙?
  • 如何计算一个好的map_size, 以预测数据集中将来可能发生的变化?
  • 单个交易可以有多大, 交易应如何细分?

无论采用哪种存储方式, 当你处理大型图像数据集时, 进行一点规划都会大有帮助。

移除广告

结论

你已经做到了!现在, 你可以鸟瞰一个大话题。

在本文中, 向你介绍了使用Python存储和访问大量图像的三种方法, 也许有机会尝试其中的一些方法。本文的所有代码都在此处的Jupyter笔记本中或此处的Python脚本中。运行风险自负, 因为小小的正方形的汽车, 船只等图像将占用你几GB的磁盘空间。

你已经看到了各种存储方法如何极大地影响读写时间的证据, 以及本文考虑的三种方法的一些优缺点。虽然将图像存储为.png文件可能是最直观的, 但是考虑使用诸如HDF5或LMDB之类的方法具有很大的性能优势。

请随时在评论部分中讨论本文未介绍的出色存储方法, 例如LevelDB, Feather, TileDB, Badger, BoltDB或其他任何方法。没有完美的存储方法, 最好的方法取决于你的特定数据集和用例。

进一步阅读

这里是与本文涵盖的三种方法相关的一些参考:

  • LMDB的Python绑定
  • LMDB文档:入门
  • Python binding for HDF5 (h5py)
  • HDF5组
  • O’Reilly的“ Python和HDF5”
  • 枕头

你可能还会喜欢Lim, Young和Patton撰写的“分析用于深度神经网络的可扩展训练的图像存储系统”。该论文涵盖了与本文类似的实验, 但考虑到冷, 热缓存以及其他因素, 其规模更大。

赞(0)
未经允许不得转载:srcmini » 用Python存储和访问大量图像的三种方法

评论 抢沙发

评论前必须登录!