画像内から検出した顔領域をImageMagickで固定サイズに切り出す

TensorFlowでのDeep Learningによるアイドルの顔識別 のためのデータ作成 - すぎゃーんメモ の記事で書いているけれど、学習用データとして使うために収集した画像から「顔の領域」だけを切り出して「固定サイズ」(112x112など)に切り出す必要があって。

以前にも書いたけど、自撮り画像はけっこう顔が傾いた状態で写っているものが多いので、それも検出できるようにしたりしている。

で、せっかく傾きの角度も含めて検出できるならそのぶんを補正して回転加工して切り出すようにしていて。

…というのを RMagick のRVGを使ってcanvasっぽい感じでどやこや書いていたのだけど、どうも使っているImageMagickのバージョンなどの影響もあるのかもしれないけど

  • #destroy!とか明示的に呼んでるはずなのにメモリ使用量がどんどん増え続けてしまう
  • 特定の画像を読み込ませて加工しようとすると必ずSegmention faultになってclockworkプロセスごと死んでしまう

といった問題が起きていて、ちょっと原因追うのも面倒 というかわざわざこれくらいの加工にRMagickで頑張りすぎることもないんじゃないか、と思って捨てることにした。

要はImageMagickCLIを使いこなせればそれくらいのことが出来るはず、ということで調べたら

  • 指定倍率で拡大縮小させて
  • 中心を指定して回転させて
  • 任意の場所に並行移動する

というのにピッタリな、「Scale-Rotate-Translate (SRT) Distortion」というのがあることを知った。

Angle -> centered rotate
Scale Angle -> centered scale and rotate
X,Y Angle -> rotate about given coordinate
X,Y Scale Angle -> scale and rotate about coordinate
X,Y ScaleX,ScaleY Angle -> ditto
X,Y Scale Angle NewX,NewY -> scale, rotate and translate coord
X,Y ScaleX,ScaleY Angle NewX,NewY -> ditto

という具合に、引数で「回転角」「倍率」「回転中心座標」「中心の移動先座標」をそれぞれ指定することで一発で変換ができるらしい。

ImageMagickCLI wrapper的な MiniMagick を使ってそれぞれ実験してみる。

顔の検出は Google Cloud Vision API でのFACE_DETECTIONのレスポンスを使うとする。


1. まずは回転角度だけを指定する場合

MiniMagick.logger.level = Logger::DEBUG
detected['responses'].first['faceAnnotations'].each do |annotation|
  srt = [
    - annotation['rollAngle']
  ].join(' ')
  MiniMagick::Image.open('./CgVXVF5UEAAFrM_.jpg').mogrify do |convert|
    convert.distort(:SRT, srt)
  end
end
DEBUG -- : [0.39s] mogrify -distort SRT -47.189156 /var/folders/j7/n0xq_2fs1bz3llgcm9snw7cc0000gn/T/mini_magick20160429-90415-1tuqqvg.jpg
DEBUG -- : [0.36s] mogrify -distort SRT -38.082096 /var/folders/j7/n0xq_2fs1bz3llgcm9snw7cc0000gn/T/mini_magick20160429-90415-kfub15.jpg


2. ちょっと背景を…

回転によって空く領域はvirtual-pixelで指定できるらしい。デフォルトは白のようなので黒にする。

detected['responses'].first['faceAnnotations'].each do |annotation|
  srt = [
    - annotation['rollAngle']
  ].join(' ')
  MiniMagick::Image.open('./CgVXVF5UEAAFrM_.jpg').mogrify do |convert|
    convert.background('black')
    convert.virtual_pixel('background')
    convert.distort(:SRT, srt)
  end
end
DEBUG -- : [0.22s] mogrify -background black -virtual-pixel background -distort SRT -47.189156 /var/folders/j7/n0xq_2fs1bz3llgcm9snw7cc0000gn/T/mini_magick20160429-90304-1e0od53.jpg
DEBUG -- : [0.22s] mogrify -background black -virtual-pixel background -distort SRT -38.082096 /var/folders/j7/n0xq_2fs1bz3llgcm9snw7cc0000gn/T/mini_magick20160429-90304-1yp6bof.jpg


それぞれの顔が真っ直になるよう微妙に回転角が調整されているのが確認できる


3. 回転中心座標を指定

何も指定していないと画像中央を中心として回転していたけど、顔の中心座標を指定すると

detected['responses'].first['faceAnnotations'].each do |annotation|
  xs = annotation['fdBoundingPoly']['vertices'].map { |v| v['x'] }
  ys = annotation['fdBoundingPoly']['vertices'].map { |v| v['y'] }
  srt = [
    "#{(xs.min + xs.max) * 0.5},#{(ys.min + ys.max) * 0.5}",
    - annotation['rollAngle']
  ].join(' ')
  MiniMagick::Image.open('./CgVXVF5UEAAFrM_.jpg').mogrify do |convert|
    convert.background('black')
    convert.virtual_pixel('background')
    convert.distort(:SRT, srt)
  end
end
DEBUG -- : [0.22s] mogrify -background black -virtual-pixel background -distort SRT 457.0,402.0 -47.189156 /var/folders/j7/n0xq_2fs1bz3llgcm9snw7cc0000gn/T/mini_magick20160429-91029-ctowe2.jpg
DEBUG -- : [0.20s] mogrify -background black -virtual-pixel background -distort SRT 210.5,290.5 -38.082096 /var/folders/j7/n0xq_2fs1bz3llgcm9snw7cc0000gn/T/mini_magick20160429-91029-irskuw.jpg


顔の中心位置は動かずにそこを中心に回転した形、になる


4. スケールを指定する

顔のサイズと、切り出したいサイズの比 から倍率を求めて指定

size = 96
detected['responses'].first['faceAnnotations'].each do |annotation|
  xs = annotation['fdBoundingPoly']['vertices'].map { |v| v['x'] }
  ys = annotation['fdBoundingPoly']['vertices'].map { |v| v['y'] }
  srt = [
    "#{(xs.min + xs.max) * 0.5},#{(ys.min + ys.max) * 0.5}",
    size.to_f / [xs.max - xs.min, ys.max - ys.min].max,
    - annotation['rollAngle']
  ].join(' ')
  MiniMagick::Image.open('./CgVXVF5UEAAFrM_.jpg').mogrify do |convert|
    convert.background('black')
    convert.virtual_pixel('background')
    convert.distort(:SRT, srt)
  end
end
DEBUG -- : [0.31s] mogrify -background black -virtual-pixel background -distort SRT 457.0,402.0 0.43636363636363634 -47.189156 /var/folders/j7/n0xq_2fs1bz3llgcm9snw7cc0000gn/T/mini_magick20160429-90917-l2v5ta.jpg
DEBUG -- : [0.29s] mogrify -background black -virtual-pixel background -distort SRT 210.5,290.5 0.4085106382978723 -38.082096 /var/folders/j7/n0xq_2fs1bz3llgcm9snw7cc0000gn/T/mini_magick20160429-90917-1gf4b9q.jpg


倍率の差はあまり分からないけど、それぞれ回転したものが縮小されているのは間違いない


5. 移動先座標を指定

最終的に左上から指定サイズでcropするために、変換後のものを左上に寄せる。顔の中心を既に指定しているので、これが指定サイズ領域の中心になるようになれば良い。

detected['responses'].first['faceAnnotations'].each do |annotation|
  xs = annotation['fdBoundingPoly']['vertices'].map { |v| v['x'] }
  ys = annotation['fdBoundingPoly']['vertices'].map { |v| v['y'] }
  srt = [
    "#{(xs.min + xs.max) * 0.5},#{(ys.min + ys.max) * 0.5}",
    size / [xs.max - xs.min, ys.max - ys.min].max,
    - annotation['rollAngle'],
    "#{size * 0.5},#{size * 0.5}"
  ].join(' ')
  MiniMagick::Image.open('./CgVXVF5UEAAFrM_.jpg').mogrify do |convert|
    convert.background('black')
    convert.virtual_pixel('background')
    convert.distort(:SRT, srt)
  end
end
DEBUG -- : [0.28s] mogrify -background black -virtual-pixel background -distort SRT 457.0,402.0 0.43636363636363634 -47.189156 48.0,48.0 /var/folders/j7/n0xq_2fs1bz3llgcm9snw7cc0000gn/T/mini_magick20160429-91316-icip1v.jpg
DEBUG -- : [0.30s] mogrify -background black -virtual-pixel background -distort SRT 210.5,290.5 0.4085106382978723 -38.082096 48.0,48.0 /var/folders/j7/n0xq_2fs1bz3llgcm9snw7cc0000gn/T/mini_magick20160429-91316-13jij7r.jpg


6. 指定サイズでcrop

既に左上に寄せてあるので、offsetなしで切り取れば良いだけ、となる。

detected['responses'].first['faceAnnotations'].each do |annotation|
  xs = annotation['fdBoundingPoly']['vertices'].map { |v| v['x'] }
  ys = annotation['fdBoundingPoly']['vertices'].map { |v| v['y'] }
  srt = [
    "#{(xs.min + xs.max) * 0.5},#{(ys.min + ys.max) * 0.5}",
    size / [xs.max - xs.min, ys.max - ys.min].max,
    - annotation['rollAngle'],
    "#{size * 0.5},#{size * 0.5}"
  ].join(' ')
  MiniMagick::Image.open('./CgVXVF5UEAAFrM_.jpg').mogrify do |convert|
    convert.background('black')
    convert.virtual_pixel('background')
    convert.distort(:SRT, srt)
    convert.crop("#{size}x#{size}+0+0")
  end
end
DEBUG -- : [0.28s] mogrify -background black -virtual-pixel background -distort SRT 457.0,402.0 0.43636363636363634 -47.189156 48.0,48.0 -crop 96x96+0+0 /var/folders/j7/n0xq_2fs1bz3llgcm9snw7cc0000gn/T/mini_magick20160429-91967-1wh75bk.jpg
DEBUG -- : [0.27s] mogrify -background black -virtual-pixel background -distort SRT 210.5,290.5 0.4085106382978723 -38.082096 48.0,48.0 -crop 96x96+0+0 /var/folders/j7/n0xq_2fs1bz3llgcm9snw7cc0000gn/T/mini_magick20160429-91967-un47j8.jpg


できあがり。


…ということで、

$ convert <元画像> -background black -virtual-pixel background -distort SRT '<顔の中心座標> <拡大縮小倍率> <回転角度> <中心移動先座標>' -crop <切り出しサイズ>+0+0 <出力画像>

のような形でconvertmogrifyを使えば一発で検出された顔の領域を指定サイズで得ることができることが分かった。


のでRMagickの使用を止めてこの方法で顔画像領域を取得するよう変更した。今のところは問題なく動いているっぽい。

結論

音咲セリナちゃんも宇佐美幸乃ちゃんも可愛い。