文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

Python Pillow-SIMDで並列画像変換するDockerイメージ用意したよ〜

ちょっと微妙かもしれない・・・


f:id:treeapps:20170501215041p:plain

最近画像変換周りを触っています。

実は既にImageMagickで並列画像変換するDockerイメージを用意してたりするのですが、ImageMagickをそろそろ卒業しようと思いまして、今回PythonのPillow-SIMDで全部書き直す事にしました。

Simple fast image converter

いいからリポジトリ見せろよ

github.com

https://hub.docker.com/r/treetips/simple-fast-image-converter/

こちらです。

何ができるの?

環境変数で画像パスを指定してあげて、docker-compose upすると、再帰的に画像フォルダを検索して、拡張子毎に異なる圧縮品質で、並列に画像変換(圧縮)を行う事ができます。

可逆圧縮はそもそも圧縮がほとんど効かずコスパが悪いので、jpg,jpeg辺りの非可逆圧縮のみを対象とすると、非常に高速に圧縮できます。

ちなみにリサイズは行わず、あくまで圧縮のみを行うものです。リサイズしてサムネイルも作成したい場合は、リポジトリ内の scripts/converter.py のimage.save周辺で作成しちゃって下さい。

技術要素

  • Docker v17
  • docker-compose v1.16
  • Alpine Linux v3.6
  • Python v3.6.3
  • Pillow-SIMD v4.3.0

Alpine linuxベースのpythonイメージに、頑張ってPillow-SIMDをインストールしてイメージを生成してます。依存ライブラリが多いので、228MBというデブデブなイメージになってしまいました。。。

並列処理についてはPython側でmultiprocessingのProcessで、マルチプロセスで行っています。Dockerに割り当てられているCPU個数を取得して、コア数分自動で並列処理されます。

もしdocker for macやdocker for windowsをご利用の方は、DockerへのCPU割当数を増やして実行すると、CPUをフルに使い切って高速に変換可能になります。

どんな時に使うの?

主にCIサーバで以下のフローで使う事を想定しています。

  1. Jenkinsでgit clone。
  2. cloneしたファイル群に対して、画像変換をかけて圧縮。
  3. ビルド。
  4. デプロイ。
  5. やったね。

Jenkinsの場合はWORKSPACEという環境変数に自動でパスが設定されますよね。本dockerイメージもWORKSPACEという環境変数を設定するようにしたので、Jenkinsフレンドリーだと思います。(多分)

もし対象の画像パスを変えたい場合は、以下のように任意のパスに書き換える事ができます。

export WORKSPACE=/tmp/images

拡張子毎に圧縮品質を調整したい場合は、settings.txtを編集して下さい。

SUPPORT_EXTENSIONS=.jpg,.jpeg
DEFAULT_IMAGE_QUALITY=80
JPEG=70
GIF=70

↑のJPEG・GIFといった部分が、フォーマット毎の圧縮品質値で、これはPillow-SIMDに完全に依存しています。詳細は http://pillow.readthedocs.io/en/3.4.x/handbook/image-file-formats.html をご覧下さい。

ちなみに画像変換されたファイルは上書きされますのでご注意下さい。元々CIで使う事を想定しているので、上書きにしちゃってます。

ログ

↓こんな感じに標準出力されます。システム情報と、対象ファイルの「拡張子」「(本当の)画像フォーマット」「圧縮品質」「圧縮時間」「画像パス」を出力します。

8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,378 INFO  === SYSTEM INFO =========================================
8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,385 INFO  System          : Linux
8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,386 INFO  Release         : 4.9.49-moby
8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,386 INFO  Version         : #1 SMP Wed Sep 27 23:17:17 UTC 2017
8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,386 INFO  Machine         : x86_64
8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,386 INFO  Processor       :
8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,386 INFO  Python version  : 3.6.3
8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,386 INFO  Compiler        : GCC 6.3.0
8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,387 INFO  Docker cpu core : 4
8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,387 INFO  =========================================================
8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,463 INFO  ext=.jpg, format=JPEG, quality=70, time=0.06s, path=/images/1124191045763.jpg
8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,493 INFO  ext=.jpg, format=JPEG, quality=70, time=0.03s, path=/images/background.jpg
8c40001ff7da_simple-fast-image-converter | 2017-11-05 07:19:10,499 INFO  elapsed_time = 0.11s

Pillowとの速度比較

jpg, png混在で、21362ファイル、合計268.2MByte の画像が存在するフォルダに対して、画像変換をかけてみました。

さてさて、PillowとPillow-SIMDはどう違いが出るでしょうか。

PillowとPillow-SIMDは、Dockerイメージでpip installする際にpillowとするか、pillow-simdとするか、で切り分けています。

ということで、同じ条件でそれぞれ5回づつ変換してかかった時間を計測してみました。

Pillow

1回目 25.66s
2回目 29.70s
3回目 27.16s
4回目 28.62s
5回目 28.30s

Pillow-SIMD

1回目 27.79s
2回目 27.79s
3回目 27.70s
4回目 27.85s
5回目 30.54s

んんん?対して変わらない???AVX2に対応していないCPUだからですかね。うーむ。。。

TIPS

OSError: [Errno 24] No file descriptors available

手持ちのiMacのDocker for macで画像変換を試しても、全くエラーが起きなかったのですが、Linuxサーバ上のDockerで試すと、以下のエラーが発生しました。

Traceback (most recent call last):
  File "/tmp/scripts/converter.py", line 118, in <module>
    convert_parallel(src_file_path_units)
  File "/tmp/scripts/converter.py", line 106, in convert_parallel
    job.start()
  File "/usr/local/lib/python3.6/multiprocessing/process.py", line 105, in start
    self._popen = self._Popen(self)
  File "/usr/local/lib/python3.6/multiprocessing/context.py", line 223, in _Popen
    return _default_context.get_context().Process._Popen(process_obj)
  File "/usr/local/lib/python3.6/multiprocessing/context.py", line 277, in _Popen
    return Popen(process_obj)
  File "/usr/local/lib/python3.6/multiprocessing/popen_fork.py", line 20, in __init__
    self._launch(process_obj)
  File "/usr/local/lib/python3.6/multiprocessing/popen_fork.py", line 66, in _launch
    parent_r, child_w = os.pipe()
OSError: [Errno 24] No file descriptors available

なんかファイルディスクリプタが足りないとの事です。ホストOSはAmazonLinuxで、ファイルディスクリプタは65536にしてあります。

となると足りていないのはDockerのコンテナ側ですね。という事で以下のように設定すると、コンテナ内のファイルディスクリプタを増やす事ができ、無事、ファイルディスクリプタ不足が起きなくなりました。

    ulimits:
      nproc: 65535
      nofile:
        soft: 20000
        hard: 40000

docs.docker.com

気づいた事

この画像変換イメージを作り、自分で使っていて気づいたのですが・・・

拡張子偽装が結構見つかる

という点です。

例えば、ファイル名が「xxx.png」だったとします。しかし、Pillowで画像フォーマットを確認してみると「JPEG」と表示されます。拡張子というものはいくらでも偽装して嘘を付けるのですが、以外なほどポロポロ見つかります。

一番困るのがPNGが.png以外の拡張子に偽装されているケースです。pngは可逆圧縮ですが、圧縮をかけてもほとんどファイルサイズは変わりません。ほとんど圧縮できないのに、圧縮にはJPEGよりも数倍時間を要します。可逆圧縮はコストパフォーマンスは最悪なので、可能な限り可逆圧縮の圧縮は避けるべきです。

可逆圧縮フォーマットを、非可逆圧縮の拡張子に偽装しているケースは非常にいやらしいですね。今の実装だと偽装拡張子を無視する形にしていますが、pngの圧縮が混入してしまうと、圧縮速度が段違いに遅くなってしまうので、偽装されている場合は処理をスキップしようかな〜?なんて考えてます。

なんせjpgは0.1〜0.3秒程度で変換できるのに、pngだと1〜10秒かかる事もあるので、少しの混入で相当遅くなってしまうのです。会社の業務プロジェクトのアセット内ですら偽装拡張子がポロポロでてくるので、ネットから拾った壁紙のような画像群に対して変換を書けてしまうと、偽装pngのせいで処理速度が大幅劣化しそうです。

雑感

私はPythonはfabricで少し書いた程度の知識しか無いのですが、今のPython v3.6.3は、型アノテーションというものがあるのですね。知りませんでした。微妙にkotlinと記法が似てて、v2系の頃とは大分様変わりしていて、ちょっとビックリしました。

昔は型が無い事を売りにしていたと思うのですが、今となっては真逆ですね。どの言語も後付で型を導入したり、最初から型有りきで登場したり。後付で型を導入するケースの場合、やはりIDEとの連携が弱い(最初から型があるものと比較して)ので、強力な連携をしてくれるIDEを使わないと、逆に書きにくくなってしまうジレンマがあるような、無いような。

今回Pythonをちょっと触ったわけですが、競合である?Rubyは私はさっぱりです。Ruby系のツール、例えばCapistrano・Chef等は全部敬遠しており、唯一使っているのがVagrantでVagrantfileをいじる時くらいでしょうか。

完全に好き嫌いの話ですが、私はrubyって好きではないし、好きになれないです。私は型や文法がカッチリしている方が好きなので、「色々な書き方ができる!」「ワンライナーでこれだけ書ける!」といったものは避けたく、一定のルールで一定の記述ができる方を好みます。

かといってよく触るJavaだと堅苦しいのは間違い無いので、最近は専らkotlinかtypescript辺りを好んで使っています。この辺は型がありつつ推論で緩く記述する事もできて、ちょうどいい感じです。

IntelliJ IDEAハンズオン――基本操作からプロジェクト管理までマスター

IntelliJ IDEAハンズオン――基本操作からプロジェクト管理までマスター

Kotlinイン・アクション

Kotlinイン・アクション

  • 作者: Dmitry Jemerov,Svetlana Isakova,長澤太郎,藤原聖,山本純平,yy_yank
  • 出版社/メーカー: マイナビ出版
  • 発売日: 2017/10/31
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る
Kotlinスタートブック -新しいAndroidプログラミング

Kotlinスタートブック -新しいAndroidプログラミング

Kotlin Webアプリケーション 新しいサーバサイドプログラミング

Kotlin Webアプリケーション 新しいサーバサイドプログラミング