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です。
今回は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]
) が必要です
- 読み込むときも添字 (
- データ型は可変長文字列となります
- string_dtype関数がそのラッパとなってます
# 追加 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へのアクセスもファイルシステムに近い
- データは配列で持つのが基本なので、ユーザフレンドリなテキストや画像などは多少の扱いづらさがある