pythonでzipを解凍せずに画像ファイルを読み込み、numpyの配列に格納する方法


タイトルがすべてです。
データサイエンスの画像分類コンペでは「train_image.zip」のような形式で訓練(あるいはテスト)対象となる画像ファイルがzipに圧縮されていることが多いです。
もちろん解凍して個々のファイルを読み込んでも良いのですが、zipのまま一括でアクセスできればスマートだと思い、少し方法を調べてみました。

コード

以下のコードでOKです。

import numpy as np
import io
from PIL import Image
import zipfile

# zipファイルのパス
zip_path = '.\data.zip'

# 配列格納用のList
train_X = []

# zipの読み込み
with zipfile.ZipFile(zip_path, 'r') as zip_file:
    # zipファイル内の各ファイルについてループ
    for info in zip_file.infolist():
        # 「zipファイル名/」については処理をしない
        if (info.filename != 'data/'):
            # 対象の画像ファイルを開く
            with zip_file.open(info.filename) as img_file:
                # 画像のバイナリデータを読み込む
                img_bin = io.BytesIO(img_file.read())
                # バイナリデータをpillowから開く
                img = Image.open(img_bin)
                # 画像データを配列化
                img_array = np.array(img)
                # 格納用のListに追加
                train_X.append(img_array)

# 処理が完了後、np.arrayに変換
train_X = np.array(train_X)

コンペでは画像データを配列化したものをそのまま特徴量としてCNNとかにぶん投げることが多いので、格納用の配列は最初から「train_X」としています。
np.empty()等で生成してもよいのですが、一度Listにappendしてからnp.arrayに変換する方式なら画像サイズ等をコードに記述しなくて済むのでラクでした。

画像データによりますが、例えば128×128ピクセルのRGBデータが1000枚ある場合、shapeは以下のようになります。

print(train_X.shape)
(1000, 128, 128, 3)

詳細

もう少し丁寧に、各記述の詳細についても説明します。

zipの読み込み

with zipfile.ZipFile(zip_path, 'r') as zip_file:

with構文を使いZipFile()で指定されたzipファイルを変数zip_fileに読み込んでいます。

各ファイルについてループ

zip_file.infolist()

infolist()で読み込んだzip_file内のすべてのファイル名を取得することができます。
for文で各ファイル名にアクセスします。
が、環境によるのかもしれませんがinfolist()で取得するlistに「zipファイル名/」という文字列が含まれることがあります(今回は「data.zip」なので文字列は「data.zip」)。
以降の処理で画像ファイルを読み込みますが、その際にこの文字列を読み込むとエラーになってしまうので条件分岐を設定しています。

# zipファイル内の各ファイルについてループ
for info in zip_file.infolist():
    # 「zipファイル名/」については処理をしない
    if (info.filename != 'data/'):
    # 以降の処理

なんて横着しましたが、zipファイル内に画像データ以外が含まれる可能性がある場合は例外処理を実装したほうが賢明ですね……。

画像ファイルの読み込み

# 対象の画像ファイルを開く
with zip_file.open(info.filename) as img_file:
    # 画像のバイナリデータを読み込む
    img_bin = io.BytesIO(img_file.read())
    # バイナリデータをpillowから開く
    img = Image.open(img_bin)

zip_file.open()に対象のファイル名を指定し、img_fileに読み込んでいます。
画像はpillowモジュールから開くのですが、このimg_fileのままImage.open()をしようとしてもエラーが出て開けません。
一度ioモジュールのBytesIO()からバイナリデータとして扱えば、Image.open()で開くことができます。
(けっこうハマったポイントでした)

配列への変換

# 画像データを配列化
img_array = np.array(img)

読み込んだ画像ファイルをnp.array()引き渡すことで配列化することができます。

まとめ

以上です。
厳密な処理速度は拾っていないのですが、画像1000枚くらいなら解凍前後で実行時間に明確な差は出ませんでした。
いちいち解凍とかしたくないときなんかにどうぞ。