ImageMagick + xargs で大量の画像をリサイズする
この記事は、シリーズ 機械学習エンジニアのためのLinux の第10回です。
概要
前回の記事で xargs
コマンドについて、第8回の記事で ImageMagick の convert
コマンドについて説明した。
今回は、これら2つのコマンドを組み合わせて、大量の画像をリサイズする方法を見ていこう。
例題
次のようなケースを考えてみよう(第8回の記事で取り上げた例題と同じものだ)。
❓ 課題
下記のようなディレクトリ構造をもつ画像データセットがあるとしよう。
dataset |-thumbnails/ |-train/ | |-00000000.JPG | |-00000001.JPG | |-00000002.JPG | | ... | `-00009999.JPG `-val/ |-00010000.JPG |-00010001.JPG |-00010002.JPG | ... `-00010999.JPG
dataset/train
とdataset/val
ディレクトリの下にあるすべての画像ファイルを縮小して、dataset/thumbnails
ディレクトリに保存したい。縮小した画像は幅・高さとも 256px とし、次のように
thumbnails/
の直下に保存したいとしよう。dataset |-thumbnails/ | |-thumb_00000000.JPG | |-thumb_00000001.JPG | | ... | `-thumb_00010999.JPG
どうすればいいだろうか?
ディレクトリを作成する
まず、サムネイル画像の保存先となる thumbnails/
ディレクトリを作成しておこう。
ディレクトリを作成するには、mkdir
というコマンドを使う。
$ cd /path/to/dataset
$ mkdir thumbnails
1枚の画像を変換するケースを考える
続いて、本題である画像のサムネイル作成に移るのだが、 すべての画像を処理するパイプラインを構築する前に、まずは1枚の画像を変換するケースから始めよう。
train/00000000.JPG
をリサイズする場合を考えてみよう。
第8回の記事で見たように、画像のリサイズには ImageMagick の convert
コマンドが使える。
オペレータに -resize '256x256!'
を指定すれば、縦横256pxにリサイズできる。
$ # train/00000000.JPG を 256px 四方にリサイズして、 thumbnails/ に保存する
$ convert train/00000000.JPG -resize '256x256!' thumbnails/thumb_00000000.JPG
xargs
でバッチ処理する
1枚の画像をリサイズするコマンドが作成できたら、次に xargs
を使ったバッチ化に取りかかろう。
xargs
を使うときに大切なのは、「ファイルごとに変化する部分」と「ファイルによらず変化しない部分」を見分けることだ。
今回の例題では、先ほどの convert
コマンドを train/
と val/
ディレクトリ内のすべての画像に対して実行する。
このとき、引数のなかでファイルごとに変化するのは次の2箇所だ。
(1) 変換元ファイルのパスの一部(convert
の最初の引数)
(2) 保存先ファイル名の一部(thumb_
以降の部分)
変化する箇所を下線で示すと、次のようになる。
$ convert train/00000000.JPG -resize '256x256!' thumbnails/thumb_00000000.JPG
(1) ^^^^^^^^^^^^^^^^^^ (2) ^^^^^^^^^^^^
この「ファイルごとに変化する箇所」にピンポイントで文字列を埋め込む場合、xargs -I
が使える。
前回の記事でみたように、コマンド引数のうち変化する部分を {}
で示して、
xargs
に -I{}
オプションを指定すると、
xargs
はその記号を標準入力から受け取った値に置き換えてくれる。
$ # xargs -I{} を指定すると...
$ find ... | xargs -I{} convert {} ... {}
$ # xargs が受け取った値は {} の部分に埋め込まれて、次のように実行される
$ convert /path/to/file1 ... /path/to/file1
$ convert /path/to/file2 ... /path/to/file2
$ convert /path/to/file3 ... /path/to/file3
...
ここで問題になるのは、xargs -I{}
では複数の {}
に異なる値を埋め込むことができない、という制約だ。
今回の例題の場合、次のようなコマンドにはできない。
$ # ❌ {} に異なる値をセットできないので、このようにはできない
$ xargs -I{} convert {} ... thumbnails/thumb_{}
なぜなら、xargs
は1つめと2つめの {}
に別の値をセットすることができない からだ。
仮に、この xargs
の標準入力に train/00000000.JPG
を渡したとすると、すべての {}
が train/00000000.JPG
に置き換えられて、次のようなコマンドが実行されてしまう。
(2) の出力先ファイルパスがおかしくなっているのが分かると思う。
$ convert train/00000000.JPG ... thumbnails/thumb_train/00000000.JPG
(1) ^^^^^^^^^^^^^^^^^^ (2) ^^^^^^^^^^^^^^^^^^
だからといって、xargs
にファイル名 (00000000.JPG
など) だけを渡すと、今度は入力元のファイルパス (1) からサブディレクトリ名(train/
と val/
)が抜けてしまう。
$ convert 00000000.JPG ... thumbnails/thumb_00000000.JPG
(1) ^^^^^^^^^^^^ (2) ^^^^^^^^^^^^
このような場合の解決策として、複数の箇所に異なる値を埋め込めるようにする方法もあるのだが、複雑な作業が必要になる。
そこで、今回はシンプルに train/
ディレクトリと val/
ディレクトリで別々のコマンドを用意することにしよう。
コマンドを分ければ、(1) に含まれるディレクトリ名(train/
と val/
) の部分は考えなくてよい。
次のように(1)、(2) の両方に同じ値(ファイル名)を指定すればよいので、xargs
での扱いが楽になる、というわけだ
$ # ✅ {} に同じ値をセットできるように、
$ # train/ と val/ でコマンドを分けてしまえばよい
$ # train/ ディレクトリ内の画像サムネイルを作成するコマンド
$ xargs -I{} convert train/{} -resize '256x256!' thumbnails/thumb_{}
(1) ^^ (2) ^^
$ # val/ ディレクトリ内の画像サムネイルを作成するコマンド
$ xargs -I{} convert val/{} -resize '256x256!' thumbnails/thumb_{}
(1) ^^ (2) ^^
xargs
への入力パイプラインを構築する
xargs
で実行するコマンドができたら、必要なデータ(この場合は、ファイル名)をxargs
の標準入力に流し込むパイプラインを構築する。
まずは train/
ディレクトリ内の画像名を取得する部分を構築しよう。
特定のディレクトリからファイルパスを取得する場合、第5回で学んだ find
コマンドが使える。
$ cd /path/to/dataset
$ find train -name '*.JPG'
train/00000000.JPG
train/00000001.JPG
train/00000002.JPG
...
出力のうち train/
の部分は不要なので、cut
コマンドで削除しよう。
$ find train -name '*.JPG' | cut -d/ -f2
00000000.JPG
00000001.JPG
00000002.JPG
...
val/
ディレクトリ内の画像名を取得する場合も同様だ。
これで、xargs
の {}
にセットする値の一覧ができた。
あとは、このパイプラインを xargs
に接続すればいい。
$ # サムネイルを作成する処理
$ # train/ ディレクトリ
$ find train -name '*.JPG' \
| cut -d/ -f2 \
| xargs -I{} \
convert train/{} -resize '256x256!' thumbnails/thumb_{}
$ # val/ ディレクトリ
$ find val -name '*.JPG' \
| cut -d/ -f2 \
| xargs -I{} \
convert val/{} -resize '256x256!' thumbnails/thumb_{}
これこそが、例題で示した「train/
と val/
ディレクトリにある画像を検索して、
それぞれの画像のサムネイルを thumbnails/
に保存する」処理をするコマンドだ。
上記のコマンドを実行すると、次のように画像1枚ごとの convert
コマンドが自動的に実行される。
$ convert train/00000000.JPG -resize '256x256!' thumbnails/thumb_00000000.JPG
$ convert train/00000001.JPG -resize '256x256!' thumbnails/thumb_00000001.JPG
$ convert train/00000002.JPG -resize '256x256!' thumbnails/thumb_00000002.JPG
...
$ convert val/00010000.JPG -resize '256x256!' thumbnails/thumb_00010000.JPG
$ convert val/00010001.JPG -resize '256x256!' thumbnails/thumb_00010001.JPG
$ convert val/00010002.JPG -resize '256x256!' thumbnails/thumb_00010002.JPG
...
📔 ノート
「サムネイルを作成する処理」として示したコマンドの途中にある
\
は 「改行を無視して、1行のコマンドと見なす」という意味をもつ。 長いコマンドを見やすくするために改行しているだけなので、実際に入力するときには1行のコマンドとして記述して問題ない。このシリーズではまだ紹介していない機能だが、シェル(bash)の
for
ループを使えば ディレクトリ名を書き直す作業を省略できる。$ # "train" と "val" を train_val 変数に代入してループ $ for train_val in train val; do find ${train_val} -name '*.JPG' \ | cut -d/ -f2 \ | xargs -I{} \ convert dataset/${train_val}/{} -resize '256x256!' \ dataset/thumbnails/thumb_{} done
ループ処理をはじめとするシェルのプログラミング機能については、別の記事で詳しく紹介する予定だ。
並列化
先ほどのコマンドでは convert
が画像1枚ごとに実行されるが、
前回紹介した xargs
の -P
オプションを使えば、並列処理によって変換時間を短くできる。
例えば xargs -P8
を指定すれば、最大8つの convert
プロセスが同時に実行される。
処理をもっとも効率よく進められる -P
の値は、一概には決められない。
CPUの論理コア数と同じ値にする(理論上はCPUを100%活用できる)というのが目安の1つになるが、処理の内容やマシンの性能、負荷状況などの要因で、これが最適な値とならないこともある。
論理コア数を出発点としつつ、システムの負荷状況(どの処理がボトルネックになっているか)をみて値を増減させる、というのが一般的だ。
$ # サムネイル作成処理(並列実行)
$ # -P8 はコア数などに応じて適宜調整
$ # train/ の処理
$ find train -name '*.JPG' \
| cut -d/ -f2 \
| xargs -I{} -P8 \
convert train/{} -resize '256x256!' thumbnails/thumb_{}
$ # val/ の処理も同様
ファイル名にスペースや改行が含まれる場合
この例題ではファイル名が 00000000.JPG
のようなフォーマットで、
スペースや改行などが名前に含まれないと想定した。
機械学習の現場で使うデータセットには、ファイル名を連番にするなどの命名規則がある場合が多く、
こうした仮定ができるケースが大半だ。
一方、こうした命名規則がない場合、これまで見たコマンドは正しく処理を完了できない可能性がある。
ファイル名に空白などの文字が含まれていると、xargs
がその文字を入力値の切れ目(デリミタ)と見なしてしまうからだ。
前回の記事で見たように、ヌル文字をファイル名の区切りとすれば、どんなファイル名でも処理できるようになる。
$ # サムネイルを作成する処理
$ # ファイル名にスペースや改行などが含まれる場合は、
$ # ヌル文字を区切りとする (find -print0, cut -z, xargs -0)
$ # train/ の処理
$ find train -name '*.JPG' -print0 \
| cut -z -d/ -f2 \
| xargs -0 -P8 -I{} \
convert train/{} -resize '256x256!' thumbnails/thumb_{}
$ # val/ の処理も同様
これで、あらゆるファイル名を正しく処理でき、しかも並列処理で画像サムネイルを生成するコマンドができた。
まとめ
ここまでの数回の記事では、「Linuxコマンドラインのコンセプト」と題してLinuxコマンドラインの特徴を紹介した。
いくつかのコマンドをパイプや xargs
で組み合わせれば、本記事の例題で示したような複雑な作業も可能になる。
こうしたコマンドの組み合わせを支えるのが「テキスト指向性」、つまり コマンドの入出力はテキスト形式(テキストストリーム)であるという原則だ。 Linux(と、その祖先であるUNIX)にはこのような設計上の原則がいくつかあり、 これらはまとめてUNIX哲学と呼ばれることもある。
次回以降の記事では、ファイルとディレクトリの操作を行うコマンドを紹介していこう。 UNIX哲学には「すべてのものはファイルである (Everything is a file)」という原則もあり、 Linux においてはファイルが重要な意味をもっている。 ファイル操作に習熟すると、Linux 上でいろいろなタスクを実行できるようになる。
それでは、次回の記事もお楽しみに!