PythonでOpenEXRファイルの圧縮方法を変換する

# 自己紹介
株式会社digitalbigmoのパイプラインエンジニアの小池@plinecomです。

# これは何？
仕事で、何万枚もの大量の非圧縮OpenEXRファイルを貰ってしまった。OpenEXRには可逆圧縮のオプションがあるので、この大量の非圧縮OpenEXRファイルを可逆圧縮なZIPのOpenEXRに変換したい。
Pythonを使って、一括処理する方法を記録する。

# とりあえず、コード
```python:main.py
import OpenEXR
import Imath
import glob
import os

if __name__ == '__main__':

    for path in glob.glob('/foo/bar/*.exr'):
        print(path)
        dir_name = os.path.dirname(path)
        base_name = os.path.basename(path)

        ret = OpenEXR.InputFile(path)  # Read EXR file
        header = ret.header()  # Get OpenEXR header info
        print(header)
        # Change Compression Type
        header['compression'] = Imath.Compression(Imath.Compression.ZIP_COMPRESSION)
        print(header)

        pix = {}
        for ch in header['channels']:   # Get channel name from header
            pix[ch] = ret.channel(ch)   # Get channel image data
        ret.close()  # Close Read File handler

        # Open write file and Set header info
        exr = OpenEXR.OutputFile(dir_name + '/' + base_name, header)
        exr.writePixels(pix)    # Write Pixel Data
        exr.close()  # Close Write File handler
```

# 必要なPython外部モジュール
* OpenEXR
```terminal:terminal
pip install OpenEXR
```
pipを使ってPyPIからダウンロードしてくる。Imathモジュールも一緒にインストールされる。
ただし、インストール時のビルドの際Python自身のheaderファイルが必要になるので、Linuxではpython-develなパッケージを管理ソフトで突っ込むこと。用意できない環境の場合は、いっそのことPythonからビルドした方が楽。
一応Dockerfileも用意してみた。一部rpmfusionからダウンロードする形で気持ち悪いけど許してほしい。良い方法があれば教えてください。
```Dockerfile:Dockerfile
FROM rockylinux:8
RUN dnf install -y which python3 python3-devel gcc gcc-c++ epel-release zlib-devel
RUN dnf config-manager --set-enabled powertools
RUN dnf localinstall -y --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm
RUN dnf install -y OpenEXR-devel
RUN pip3 install OpenEXR
```
```terminal:terminal
docker pull plinecom/py_exr_zip
```


# コードの説明
## EXRファイルの読み込み
```python:
        ret = OpenEXR.InputFile(path)  # Read EXR file
```
## 圧縮方法の指定
```python:
        header = ret.header()
        # Change Compression Type
        header['compression'] = Imath.Compression(Imath.Compression.ZIP_COMPRESSION)
```
直接テキストで圧縮方法を書いて代入したら失敗する。Imath.CompressionオブジェクトをImath.Compression.*列挙子で初期化したものを渡さないといけない。今回はZIP圧縮したいので、Imath.Compression.ZIP_COMPRESSIONを渡している。圧縮方法以外は変更しないので、それ以外のヘッダ情報は元のファイルのヘッダ情報を流用する。


## ファイルの書き出し
```python:
        # Open write file and Set header info
        exr = OpenEXR.OutputFile(dir_name + '/' + base_name, header)
        exr.writePixels(pix)    # Write Pixel Data
        exr.close()  # Close Write File handler
```
流用したヘッダを元に、書き出し先を指定してファイルを作成し、画素データを書き込んでファイルを閉じる。

# OpenEXRの圧縮オプションについての解説
## 可逆（ロスレス）圧縮オプション
### RLE
ランレングス圧縮。単純な圧縮処理だが１パスで終わる上に計算量も少ないので非常に高速。非圧縮にするのと大差ない速度で書き出せる。これだけでも概ね９０％のサイズになるので、非圧縮を選ぶ理由がない。~~ので、非圧縮OpenEXRファイルなんか送ってくるな。~~
### ZIP
みなさんご存知ZIP圧縮。画像を１６ライン毎に区切って圧縮してくれる。これを使えば非圧縮の半分くらいのサイズにできる。
### ZIPS
ZIPでは16ライン毎に圧縮するが、ZIPSでは１ライン毎にZIP圧縮する。SはスキャンラインのS。複数形のSではない。Nukeなどのコンポジットソフトでは処理が速くなるという噂を聞いたが、体感できるほどの差を感じたことはない。ZIPよりは圧縮効率が落ちる。
### PIZ
ウエーブレット変換というフーリエ変換の上位互換的なものを使って、周波数領域に画像データを持っていって圧縮する。最高の圧縮率が出るものの、計算量も非常に大きい。ZIP圧縮よりさらに数％くらい小さくなる割には計算量の犠牲が大きすぎると思う。

## 可逆圧縮になったり非可逆圧縮になったりするもの
### PXR24
24bitに量子化を下げて圧縮する。24bit以下の16bit半精度浮動小数点データや16bit整数データならロスレスな可逆圧縮だが、32bit単精度浮動小数点データ等ではロッシーな非可逆圧縮となる。この点、取扱注意なので、よくわからないときは選ばない方がいい。

## 非可逆圧縮オプション
他に非可逆圧縮オプションとして以下のものがある。
* B44
* B44A
* DWAA
* DWAB

ただ、高品位を狙ってOpenEXR利用することがほとんどなのと、納品先で解凍結果が異なって事故る可能性のあるロッシーな非可逆圧縮は、あんまり使わないかなと思う。自社でマスターまで出す大規模スタジオならストレージ効率の面から選択肢に入るかもしれない。

# 宣伝
株式会社digitalbigmoでは美肌プラグインの販売や映像VFXの制作業務を行なっています。ご興味のある方は、webページを見にきてください。一緒にお仕事しましょう。

https://digitalbigmo.com

# 参考文献
[PythonのOpenEXRモジュールについて（英語）](https://excamera.com/sphinx/articles-openexr.html)
[OpenEXR本家（英語）](https://www.openexr.com/)
[WikipediaのOpenEXRのページ（英語）](https://en.wikipedia.org/wiki/OpenEXR)
[WikipediaのOpenEXRのページ（日本語）](https://ja.wikipedia.org/wiki/OpenEXR)

http://yamagishi-2bit.blogspot.com/2020/05/vfxexr.html