【Перевод】Как читать и записывать большие изображения в Python

Python

Перевод: Лао Ци


Почему необходимо больше узнать о хранении изображений и доступе к ним в Python? Если в вашем бизнесе используется только небольшое количество изображений, например, классификация изображений по цвету или использование OpenCV для реализации распознавания лиц, вам вообще не нужно беспокоиться об этой проблеме. Даже с помощью PIL Python вы можете легко обрабатывать сотни фотографий, конвертировать изображения в.pngили.jpgФорма файла хранится на диске, что просто, удобно и целесообразно.

Однако это не всегда так для реальных задач, когда такие алгоритмы, как сверточные нейронные сети (CNN), могут обрабатывать наборы данных, содержащие большое количество изображений, и учиться на них. Если вы заинтересованы в этом, вы можете подать заявку на участие в онлайн-кейсе машинного обучения, предоставленном публичной учетной записью WeChat «Laoqi Classroom» этой статьи, и испытать его в реальном кейс-проекте.

Примечание. Обратите внимание на общедоступную учетную запись WeChat: класс Лао Ци, ответьте «имя + номер мобильного телефона +« дело »», подайте заявку на «Коллекцию случаев машинного обучения», код и данные этой статьи были собраны в этом случае коллекция.

WechatIMG6

ImageNet — известная общедоступная база данных изображений, которую можно использовать для обучения моделей таким задачам, как классификация и распознавание объектов, содержит более 14 миллионов изображений.

Подумайте, сколько времени потребуется, чтобы загрузить их в память партиями, сотни или тысячи раз, для обучения. Если вы читаете эти картинки обычным способом, вы должны оставить компьютер, чтобы заняться чем-то другим после того, как вы начнете читать, и оно может быть не закончено, когда вы вернетесь. Но если вы надеетесь пойти работать в Google или Nvidia, вы не можете так играть.

В этой статье вы узнаете о:

  • Сохраните изображение на диске в виде файла .png
  • Сохранение изображений в LMDB (базы данных Lightning с отображением памяти)
  • Сохраните изображение в файл в формате HDF5.

Мы также изучим следующее:

  • Почему стоит рассмотреть альтернативные методы хранения
  • Как отличается производительность этих трех методов при чтении и записи одного изображения
  • Как производительность этих трех методов отличается при чтении и записи нескольких изображений
  • Сравнение трех методов с точки зрения использования диска

Если ни один из методов хранения не кажется вам знакомым, не волнуйтесь: для этой статьи все, что вам нужно, — это базовые знания языка Python и базовое понимание изображений (которые на самом деле состоят из многомерных массивов), памяти, такой как Difference от 10 МБ до 10 ГБ.

Давайте начнем!

Установщик

В следующем проекте требуется набор данных изображений, а также некоторые пакеты Python. Публичный аккаунт WeChat этой статьи «Laoqi Classroom» предоставляет онлайн-платформу для экспериментов со всеми следующими кодами. Пожалуйста, следуйте приведенным выше инструкциям, чтобы подать заявку на использование «Коллекции примеров машинного обучения».

набор данных

Набор данных в этом случае взят из известного CIFAR-10, который состоит из 60 000 цветных изображений размером 32x32 пикселя, принадлежащих различным классам объектов, таким как собаки, кошки и самолеты. Условно говоря, CIFAR — не очень большой набор данных, но если мы используем полный набор данных TinyImages, потребуется около 400 ГБ свободного места на диске, что является роскошью для обучения.

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

Следующий код будет считывать данные изображения из файла набора данных и загружать их в массив 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Проверьте, если об ошибке не сообщается, все в порядке.

Начало работы с HDF5

HDF5 означает формат иерархических данных, и этот формат файла известен как HDF4 или HDF5. Нам не нужно беспокоиться о HDF4, так как в настоящее время поддерживается версия HDF5.

Интересно, что HDF был разработан Национальным центром суперкомпьютерных приложений (США) и представляет собой портативный компактный формат для научных данных. Если вам интересно, широко ли он используется, ознакомьтесь с этим введением в HDF5 в проекте NASA Earth Data Project.

Файлы HDF состоят из двух типов объектов:

  • набор данных
  • группа

Наборы данных — это многомерные массивы, а группы состоят из наборов данных или других групп. Многомерные массивы любого размера и типа могут храниться как наборы данных, но размеры и типы в наборе данных должны быть однородными. Каждый набор данных должен содержать однородный N-мерный массив. Тем не менее, поскольку группы и наборы данных могут быть вложенными, вы все равно можете получить необходимую неоднородность:

$ pip install h5py

Как и в случае с другими библиотеками, вы можете установить через Anaconda:

$ conda install -c conda-forge h5py

если выimport h5pyОшибок нет, значит все будет настроено правильно.

сохранить одно изображение

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

Когда я говорю о файлах, я обычно имею в виду множество файлов. Однако, поскольку некоторые методы могут быть оптимизированы для разных операций и количества файлов, важно различать их.

Чтобы облегчить эксперимент, мы можем сравнить производительность чтения разного количества файлов, увеличив количество изображений от 1 до 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модуль для синхронизации программы.

сохранить на диск

В приведенном ниже эксперименте входными данными является одно изображение.image, который в настоящее время хранится в памяти в виде массива NumPy. Сначала сделай как.pngИзображение сохраняется на диск с уникальным идентификатором изображения.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 не имеют ограничений на размер файла, поэтому все изображения, как и раньше, дополняются одним набором данных и в конечном итоге сохраняются как один файл.

Далее вам нужно подготовить набор данных для эксперимента, вырастив его.

Подготовьте набор данных

Прежде чем снова запустить эти эксперименты, давайте сначала удвоим размер набора данных, чтобы мы могли протестировать до 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 изображений будут сохранены на диск три раза в трех разных форматах, и, конечно же, вам потребуется около 2 ГБ дискового пространства.

Пришло время стать свидетелем чуда! Сколько времени займет все это хранение? Диаграмма стоит тысячи слов:

На первом графике показано непреобразованное сравнение времени хранения, полученное программой, которое показывает, что существует большая разница во времени между сохранением в файл «.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и.csvFile немного быстрее, но все три метода выполняются быстро. Эксперименты, которые мы собираемся провести дальше, более интересны.

читать несколько изображений

Продолжайте модифицировать код для реализации функции чтения нескольких изображений, конечно, важна производительность.

Измените код для чтения нескольких изображений

Измените предыдущую функцию (Примечание:Подробности смотрите в этой серииВторая часть),ты можешь использовать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.

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

На практике время записи обычно менее важно, чем время чтения. Представьте, что вы разрабатываете глубокую нейронную сеть для изображений, и только половина всего набора данных изображений может одновременно помещаться в память. Для каждого этапа обучения нейронной сети требуется весь набор данных, а для сходимости модели требуются сотни этапов. По сути, каждый этап считывает половину набора данных в память.

На этом этапе люди используют трюки, такие как обучение псевдоэпох, чтобы сделать их немного лучше.

Теперь, снова взглянув на график выше, вы увидите, что разница между 40 и 4 секундами внезапно оказывается разницей между 6 часами и 40 минутами!

Если мы посмотрим на время чтения и записи на одном и том же графике, мы получим следующий график:

когда вы сохраняете изображение как.pngПри записи файлов существует большая разница между временем записи и временем чтения. Однако для LMDB и HDF5 разница менее выражена. В общем, есть веские причины использовать LMDB или HDF5 для хранения изображений, даже если время чтения важнее, чем время записи.

Теперь, когда вы увидели преимущества производительности LMDB и HDF5, давайте рассмотрим еще один важный показатель: использование диска.

использование диска

Скорость — не единственный показатель производительности, который вас интересует. Мы уже имеем дело с очень большими наборами данных, поэтому дисковое пространство также является очень важным и актуальным вопросом.

Допустим, у вас есть набор данных изображений объемом 3 ТБ. В отличие от нашего примера CIFAR, у вас, вероятно, они уже есть где-то на диске, поэтому, используя другой метод хранения, вы фактически создаете резервные копии, и эти резервные копии также должны храниться. Это даст вам огромное преимущество в производительности при работе с изображениями, но вам нужно убедиться, что у вас достаточно места на диске.

Сколько дискового пространства используется различными методами хранения? Ниже показано дисковое пространство, используемое каждым методом для каждого количества изображений:

И HDF5, и LMDB работают быстрее, чем при использовании обычных.pngХранение изображений занимает больше места на диске. Важно отметить, что использование диска и производительность LMDB и HDF5 сильно зависят от различных факторов, включая операционную систему. Что еще более важно, размер хранимых данных.

LMDB использует кэширование для повышения эффективности, вам не нужно понимать его внутреннюю работу, но имейте в виду: для больших изображений дисковое пространство, используемое LMDB, значительно увеличится, так как изображения не подходят для хранения в качестве ответвления LMDB. будет много переполнения. Бар LMDB на графике выше вот-вот выскочит из графика.

Изображения размером 32x32x3 пикселя относительно малы по сравнению с обычными изображениями, которые вы могли бы использовать, и обеспечивают наилучшую производительность LMDB.

Хотя мы не будем проводить здесь экспериментальные исследования, исходя из моего собственного опыта работы с изображениями размером 256x256x3 или 512x512x3 пикселей, HDF5, как правило, немного более эффективен, чем LMDB, с точки зрения использования диска. Это хороший переход к заключительному разделу, где качественно обсуждаются различия между методами.

обсуждать

Есть и другие важные особенности LMDB и HDF5, о которых стоит знать, а также важно кратко обсудить некоторые комментарии по обоим подходам.

параллельный доступ

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

В большинстве случаев вам не будет интересно одновременно читать части одного и того же изображения, но вы захотите прочитать несколько изображений одновременно. При таком параллельном определении.pngХранение файлов на диске фактически обеспечивает параллелизм. Пока имена изображений разные, ничто не мешает вам одновременно читать несколько изображений из разных потоков или записывать несколько файлов одновременно.

Как насчет ЛМДБ? В среде LMDB может быть несколько одновременных программ чтения, но только один модуль записи, и модуль записи не блокирует модуль чтения. Подробнее об этом можно прочитать на техническом сайте LMDB.

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

HDF5 также обеспечивает параллельный ввод-вывод, позволяя выполнять параллельное чтение и запись. Однако в реализации, если у вас нет параллельной файловой системы, блокировки записи удерживаются, а доступ осуществляется последовательно.

Если вы имеете дело с такой системой, есть два основных варианта. В этой статье группа HDF по параллельному вводу-выводу более подробно обсудит эти два основных варианта. Это может быть довольно сложно, самый простой вариант — разумно разделить набор данных на несколько файлов HDF5, чтобы каждый процесс мог обрабатывать один файл независимо..h5документ.

Документация

если вы гуглитеlmdb, по крайней мере, в Великобритании третьим результатом поиска является IMDb (Internet Movie Database). Это не то, что вы ищете!

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

Что касается самой технологии LMDB, то на сайте технологии LMDB есть более подробная документация. Это похоже на изучение исчисления во втором классе, если только вы не начинаете с их вводной страницы.

Для HDF5 есть очень понятная документация на сайте документации h5py и полезная запись в блоге Кристофера Ловелла, которая представляет собой очень подробное введение в использование пакета h5py. Книга О'Рейли, 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, но вы можете написать классы, которые наследуют набор данных. Я лично использую пользовательский класс, предназначенный для оптимизации доступа для чтения в зависимости от того, как я структурирую свои файлы HDF5.
  • Theano не поддерживает какой-либо конкретный формат файла или базу данных, но, как упоминалось ранее, можно использовать что угодно, если оно считывается как N-мерный массив.

Хотя это далеко не все, я надеюсь, что некоторые ключевые библиотеки глубокого обучения дадут вам представление об интеграции LMDB/HDF5.

Некоторые личные идеи по хранению изображений в Python

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

Как правило, модели необходимо обучать с использованием k-кратной перекрестной проверки, которая включает в себя разделение всего набора данных на k наборов (k обычно равно 10) и обучение k моделей, каждая из которых использует свой набор k в качестве тестового набора. Это гарантирует, что модель не будет соответствовать набору данных или, другими словами, не сможет делать хорошие прогнозы на невидимых данных.

Вышеупомянутые наборы данных должны быть сохранены в отдельном наборе данных HDF5, чтобы максимизировать эффективность. Иногда один набор данных не может быть загружен в память сразу, поэтому даже порядок данных в наборе данных должен быть заранее продуман.

С LMDB я так же тщательно планирую заранее, прежде чем создавать базу данных. Перед сохранением изображения стоит задать несколько хороших вопросов:

  • Как я могу сохранить изображение, чтобы большинство чтений были последовательными?
  • Что такое хороший ключ?
  • Как рассчитать подходящийmap_size, прогнозировать возможные будущие изменения в наборе данных?
  • Насколько большой может быть одна транзакция и как должны быть разделены транзакции?

Независимо от метода хранения, небольшое планирование может иметь большое значение при работе с большими наборами данных изображений.

в заключении

Ты сделал это! Теперь у вас есть общее представление о большой проблеме.

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

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

Обратите внимание на публичный аккаунт WeChat: Lao Qi Classroom. Читайте подробные статьи, получайте превосходные навыки и наслаждайтесь блестящей жизнью.

WechatIMG6

Оригинальная ссылка:realPython.com/storing-IMA…