Python: HDF5ファイルの作成と読み込み (h5py)

お仕事で触れる機会が増えてきたHDF5について調べて整理します。また、h5pyを使ってWFLWデータセットをHDF5へ変換してみました。

HDF5

HDF (Hierarchical Data Format) は大量のデータを格納するファイルフォーマットで、The HDF Groupによって開発・メンテナンスされています。
5はバージョン番号で、現在の主流となっています1

HDF5の特徴はおおまかに4点です。こちらにまとまっています。

  • 多数・大容量のデータを1ファイルにまとめることができる
    • HDF5ファイルそのもののサイズやHDF5内のオブジェクト (DatasetとGroup) 数に制限がない
  • DatasetとGroupによってファイルシステムライクな階層構造を表現できる
    • Dataset で、numpy.ndarrayのような多次元配列を保持 (= ファイル)
    • Group で、複数のDatasetやGroupを保持 (= ディレクトリ)
    • DatasetやGroupへの属性付与 (Attribute) やリンクもできます
  • プラットフォームに依存しないフォーマットなので、可搬性が高い
  • C, C++, Java, FortranのAPIを公式サポート
    • 他言語でもサードパーティ製のライブラリによって使用できます

h5pyを使ったHDF5ファイルの入出力

PythonでHDF5を入出力するためのライブラリとしてよく使われるのがh5pyです。

github.com

今回はWFLW2の画像とアノテーションから1つのHDF5を生成しそれを読み込む処理を、h5pyを使って実装していきます。

$ python --version    
Python 3.7.4

$ pip install h5py numpy pandas pillow

$ pip list | grep h5py
h5py            2.10.0 

あらかじめこちらからWFLW_images.tar.gzとWFLW_annotations.tar.gzをダウンロード・展開しておきます。

  • WFLW_annotaions配下のlist_98pt_*.txtがアノテーションデータとなっており、1行1画像で各行に画像パスとランドマーク座標がスペース (" ") 区切りで格納されています
  • WFLW_images配下にはシチュエーション (0--Paradeなど) ごとにディレクトリが切られ、各ディレクトリにjpg画像が格納されています

HDF5ファイルの構成は以下のレイアウトにします。

WFLW.h5
├── annotations (Group)
│   ├── README (Dataset)
│   └── list_98pt_rect_attr_test (Dataset)
└── images (Group)
    ├── 0--Parade (Group)
    │   ├ ── 0_Parade_marchingband_1_116.jpg (Dataset)
    │   ...
    ├── ...
    ...

最初に実装全体を示します。WFLW_annotations、WFLW_imagesと同じ階層にPythonファイルを作成しています。

import h5py
import io
import glob
import os
import pandas as pd
import numpy as np
from PIL import Image

h5_path = "./WFLW.h5"

# HDF5ファイルの作成
with h5py.File(h5_path, "w") as h5:
    # annotations Group
    annotations_group = h5.create_group("annotations")

    with open("./WFLW_annotations/list_98pt_rect_attr_train_test/README", "r") as readme:
        readme_dataset = annotations_group.create_dataset(
            name="README", shape=(1,), dtype=h5py.string_dtype()
        )
        readme_dataset[0] = readme.read()

    with open("./WFLW_annotations/list_98pt_rect_attr_train_test/list_98pt_rect_attr_test.txt", "r") as test:
        test_dataset = annotations_group.create_dataset(
            name="list_98pt_rect_attr_test.txt", shape=(1,), dtype=h5py.string_dtype()
        )
        test_dataset[0] = test.read()

    # images Group
    images_group = h5.create_group("images")

    # WFLW_images内のディレクトリを列挙
    for d in sorted(glob.glob("WFLW_images/*")):
        # images/subset group
        subset_group = images_group.create_group(os.path.basename(d))

        # jpgファイルを列挙
        for p in sorted(glob.glob(os.path.join(d, "*.jpg"))):
            # numpyで読み込んでDatasetで追加
            image = np.array(Image.open(p)).astype(np.uint8)
            image_dataset = subset_group.create_dataset(
                name=os.path.basename(p), data=image, compression="gzip"
            )

# HEF5の読み込み
with h5py.File(h5_path, "r") as h5:
    readme = h5["annotations/README"][0]
    print(readme)
    # # Look at Boundary: A Boundary-Aware Face Alignment Algorithm.
    # ...

    test = pd.read_csv(
        io.StringIO(h5["annotations/list_98pt_rect_attr_test.txt"][0]), delimiter=" ", header=None
    )
    print(test.head())
    #           0           1    ...  205                                         206
    # 0  182.212006  268.895996  ...    0  37--Soccer/37_Soccer_soccer_ball_37_45.jpg
    # [1 rows x 207 columns]

    image = h5["images/0--Parade/0_Parade_marchingband_1_930.jpg"]
    print(type(image), image.shape)  # <class 'h5py._hl.dataset.Dataset'> (575, 1024, 3)

    image = image[()]  # or image = image[:, :, :]
    print(type(image), image.shape)  # <class 'numpy.ndarray'> (575, 1024, 3)

    image = Image.fromarray(image, "RGB")
    image.save("./output.jpg")

細かいところを解説していきます。

書き込み・読み込み

h5py.Fileオブジェクトを生成します。通常のファイルのopenと同様に、第2引数が"w"なら書き込み、"r"なら読み込みとして開きます。

以降のGroupやDatasetはこのFileオブジェクトを用いて更新・参照します。

# HDF5ファイルの書き込み
with h5py.File(h5_path, "w") as h5:
    ...


# HDF5ファイルの読み込み
with h5py.File(h5_path, "r") as h5:
    ...

Groupの作成

Groupの作成はcreate_groupメソッドで作成します。ルート直下に作成する場合はFileオブジェクトで呼び出します。

# images Group
images_group = h5.create_group("images")

前述の通りGroupはGroupを内包できますので、作成したGroupオブジェクトでcreate_groupをコールすると配下にGroupが作られます。

# images/subset group
subset_group = images_group.create_group(os.path.basename(d))

テキストDatasetの追加と読み込み

FileまたはGroupオブジェクトのcreate_datasetメソッドを用いて追加します。

  • Fileの場合はルート、Groupの場合はそのGroup配下に、それぞれ作成されます
  • 引数nameがDataset名で、ファイルシステムっぽくannotations/READMEでアクセスできます
  • Datasetは内部的には多次元配列で表現されますので、文字列の場合はshape=(1,)で1要素の扱いになります
    • 読み込むときも添字 ([0]) が必要です
  • データ型は可変長文字列となります
# 追加
with open("./WFLW_annotations/list_98pt_rect_attr_train_test/README", "r") as readme:
    readme_dataset = annotations_group.create_dataset(
        name="README", shape=(1,), dtype=h5py.string_dtype()
    )
    readme_dataset[0] = readme.read()

# 読み込み
readme = h5["annotations/README"][0]
print(readme)

画像Datasetの追加と読み込み

create_datasetには引数dataにnumpy配列を直接渡すことができます (この場合shapeは不要です) 。そのため画像はpillowのImageでロードしてnumpy配列にして渡してます。

テキスト同様にパス指定で読み込みますが、そのままではDatasetオブジェクトなので[()]とすることで配列全体をロードできます。

  • numpy同様のインデックス指定 ([0, :, :]など) で一部だけロードすることもできます
# numpyで読み込んでDatasetで追加
image = np.array(Image.open(p)).astype(np.uint8)
image_dataset = subset_group.create_dataset(
    name=os.path.basename(p), data=image
)

# 画像Datasetの読み込み
image = h5["images/0--Parade/0_Parade_marchingband_1_930.jpg"]
print(type(image), image.shape)  # <class 'h5py._hl.dataset.Dataset'> (575, 1024, 3)

image = image[()]  # or image = image[:, :, :]
print(type(image), image.shape)  # <class 'numpy.ndarray'> (575, 1024, 3)

ちなみに画像を配列で持つため、トータルのファイルサイズはぐっと増えます。jpg時には800MB以下だったものが20GB近くまで膨れます。そうした巨大なファイルでも、メモリに全てをロードすることなく1ファイル (Dataset) ずつ読み込むことができます。

  • create_datasetの引数にてcompression="gzip"などで圧縮形式を指定することもできます

まとめ

HDF5ファイルをh5pyでアクセスする方法について整理しました。

  • ディレクトリ構成をGroupで実現することで、データのユーザにわかりやすい直感的な構造を維持できる
  • 各Fileへのアクセスもファイルシステムに近い
  • データは配列で持つのが基本なので、ユーザフレンドリなテキストや画像などは多少の扱いづらさがある

  1. 旧バージョンとしてHDF4もありますが、制約が強く、あまり使われていないようです。

  2. Wu, Wayne and Qian, Chen and Yang, Shuo and Wang, Quan and Cai, Yici and Zhou, Qiang. Look at Boundary: A Boundary-Aware Face Alignment Algorithm. CVPR 2018.