Ruby Freaks Lounge

第27回RMagickを用いた画像処理:アニメGIF

はじめに

前回RMagickを用いて画像のリサイズの仕方を紹介しました。今回は、RMagickでのアニメGIFの取り扱いについて説明していきます。

なお、紹介するすべてのサンプルコードは、事前に以下のコードを実行している前提で記述しています。また、破壊メソッドと非破壊メソッドの両方があるメソッドについては、サンプルコードでは特段の理由がない限り破壊メソッドを利用しています。

require 'rubygems'
require 'rmagick'

height = 160
width = 160
scale = 0.5
動作確認環境ruby 1.8.6
ImageMagick 6.5.6-8
rmagick 2.12.0

サンプル画像は以下を用います。

図1 original.gif
※この画像はネムリクスリで配布されているものを使用させていただきました。

アニメGIFファイルの読み込み

RMagickでアニメGIFファイルの読み込みを行う場合は、以下のようにImageList.newメソッドを用いて行います。

なお、ImageListクラスは、大雑把な理解としてはアニメGIFを扱うためのクラスと考えていても間違いないと思います(もちろん他の画像形式で使うこともできますし、使うと便利なケースもありますが⁠⁠。

ilist = Magick::ImageList.new('original.gif')

アニメGIFの構成

アニメGIF、特に画像が少しずつ変化していくアニメーションの場合には、各コマごとの画像を別々に持つのではなく、コマ間の差分をデータとして持つことがよくあります。

たとえば、サンプルの画像では次のような構成になっています。

図2 original-0.png~original-5.png
図2-0 original-0.png 図2-1 original-1.png 図2-2 original-2.png 図2-3 original-3.png 図2-4 original-4.png 図2-5 original-5.png

ちなみに、コマ別の画像を得るためには、ImageListを使ってファイルを開き別ファイル形式で保存するのが簡単です。指定ファイル名にナンバリングしたファイルを自動的に作ってくれます。

ilist = Magick::ImageList.new('original.gif')
ilist.write('original.png')

アニメGIFのリサイズ

アニメGIFをリサイズする場合に気をつけなければいけない点がいくつかあります。サンプルとする画像によっては問題にならないケースもありますが、実際に、どんな問題が起こるのか、何をすべきかを紹介していきます。

普通にリサイズしてみる

前回紹介したresizeメソッドを使ってそのままリサイズしてみます。

ilist = Magick::ImageList.new('original.gif')
ilist.resize!(scale)
ilist.write('just_resize.gif')

すると、最後のコマだけがリサイズされてしまい残念な結果になってしまいました。これではいくらなんでも使いものにならないので、きちんとフレーム(コマ)毎にリサイズする必要があります。

図3 just_resize.gif

フレーム毎にリサイズする

次に、eachを回してフレーム毎にresize処理を呼び出してみます。

ilist = Magick::ImageList.new('original.gif')
ilist.each{|frame| frame.resize!(0.5)}
ilist.write('frame_resize.gif')

これで全フレームをリサイズすることはできましたが、できあがった結果を見るとやっぱり残念な感じになってしまっています。これは「アニメGIFの構成」のところで紹介したように、フレーム間を差分で持っているためです。これを補正するには一度差分から展開する必要があります。

図4 frame_resize.gif

画像を展開してからリサイズする

そこで、coalesceメソッドを用いて画像を差分から展開してみます。

ilist = Magick::ImageList.new('original.gif')
ilist = ilist.coalesce
ilist.each{|frame| frame.resize!(0.5)}
ilist.write('coal_resize.gif')

これで綺麗なリサイズ画像を得ることはできました。しかしファイルサイズを見ると、リサイズ前が約21KBなのに対しリサイズ後が約32KBとなっていて、リサイズしたのにファイルサイズが大きくなってしまっています。これは画像が展開されたままになってしまっているせいです。

図5 coal_resize.gif

リサイズ後最適化する

そのため、最後に再度差分に戻すなどの最適化を施します。optimize_layersメソッドを使って最適化を行ってみましょう。

引き数は適用する仕組みの指定なのですが、OptimizeLayerという指定は大雑把に説明すると、すべての最適化を可能な限り適用するという理解でよいかと思います。

ilist = Magick::ImageList.new('original.gif')
ilist = ilist.coalesce
ilist.each{|frame| frame.resize!(scale)}
ilist = ilist.optimize_layers(Magick::OptimizeLayer)
ilist.write('opt_resize.gif')
図6 opt_resize.gif

この最適化により、ファイルサイズも無事約23KBになりました。まだ元画像より大きいですが、リサイズするとデータの冗長性としては下がるのでやむを得ないところでしょうか。

ただ、OptimizeLayers指定の「すべて」というのが結構曲者で、一部の携帯端末や一部の画像ビューアーを利用したときに最大限最適化した画像をきちんと表示できないことがあります。その辺りの懸念を払拭したい場合には、明示的に適用する最適化を指定した以下のような形をお勧めします。

ilist = Magick::ImageList.new('original.gif')
ilist = ilist.coalesce
ilist.each{|frame| frame.resize!(scale)}
ilist = ilist.optimize_layers(Magick::OptimizeTransLayer)
ilist = ilist.deconstruct
ilist.write('opt_resize2.gif')

ここでは、OptimizeTransLayer指定することで、前のフレームとの差分をとる形にするcoalesceの逆のようなことをする最適化と、deconstructメソッドで、差分領域が狭い場合にフレームの画像領域を小さくする最適化を行っています。

図7 opt_resize2.gif

指定サイズにリサイズ

最後に前回紹介した手法を交えて、画像を変質させずに、指定サイズぴったりにリサイズさせてみます。

ポイントしては、まずchange_geometryメソッドをeachの外側で呼んでいます。もちろん内側で呼んでも目的は達することができますが、毎回計算を行うのは無駄なので外で呼ぶようにしました。ただ、ImageListにはchange_geometryメソッドがないため、firstでフレームを取り出して無理やり呼んでいます。次にフレームをリサイズした後拡張するためのextentメソッドが破壊メソッドがないため、新しいImageListを用意して作り直しています。最後は忘れずに最適化を行っています。

ilist = Magick::ImageList.new('original.gif')
ilist = ilist.first.change_geometry("#{height}x#{height}") do |cols,rows,img|
  new_il = Magick::ImageList.new
  ilist.each{|frame|
    new_frame = frame.resize(cols,rows)
    new_frame = new_frame.extent(width,height,-(width-cols)/2,-(height-rows)/2)
    new_il << new_frame
  }
  new_il
end
ilist = ilist.optimize_layers(Magick::OptimizeLayer)
ilist.write('resize_extent.gif')
図8 resize_extent.gif

まとめ

今回はRMagickでのアニメGIFの取扱いについて紹介しました。アニメGIFをリサイズ(加工)する際に気をつけないといけないところをまとめると、以下の事柄になるかと思います。

  • フレーム毎にリサイズする
  • 画像を差分から展開してからリサイズする
  • リサイズ後に最適化を施す

これはRMagickを利用しない場合でも同じです。

おすすめ記事

記事・ニュース一覧