しばらく勉強会だのイベントレポートだの電子工作だのばかりの記事で、ちっともwebプログラミングのエントリをしていなかった。これじゃいかんという一念発起したの(と、諸事情から必要になったの)で、そっち寄りの内容を書いてみることにした。

…と言っても表題どおり、ImageMagick / PECL::Imagickの使い方がメイン。ドキュメントを読めばわかることから、知らないとハマる落とし穴まで、知ってることは大放出で書くことにする。

テーマ

  • ImageMagickとPECL::Imagickを使い、PHPでアニメーションGIFの合成を行う。
  • サーバサイドでいわゆる「アバター」の画像を合成する、という使い方を想定している。
  • メジャー3キャリアの3G以降の端末に対応する。

環境

ソフトウェアのバージョンがちょっと古いのはご容赦願いたい。ImageMagickはバージョンによって結構挙動が変わるので、新バージョンを試すときは必ず別の環境を用意し、すべての動作をチェックしてからにしよう。

  • x86 Linux(RHEL 3)
$ uname --all
Linux ********* 2.4.21-47.EL #1 SMP Wed Jul 5 20:30:36 EDT 2006 x86_64 x86_64 x86_64 GNU/Linux
  • ImageMagick 6.4.7
  • PECL::Imagick 2.2.1

インストール

ImageMagick

地道にソースからインストール。

$ pwd
/usr/local/src
$ wget ftp://ftp.imagemagick.org/pub/ImageMagick/ImageMagick.tar.gz
$ tar -zxvf ImageMagick.tar.gz
$ cd ImageMagick-6.4.7-3
$ ./configure --disable-openmp --with-quantum-depth=8
$ make
# make install
$ cd PerlMagick/
$ perl Makefile.PL
$ make
# make install
PECL::Imagick

pecl installはダメだったので、こちらもソースから。

アーカイブを入手
$ pwd
/usr/local/src
$ pecl download imagick
downloading imagick-2.2.1.tgz ...
Starting to download imagick-2.2.1.tgz (75,873 bytes)
.................done: 75,873 bytes
File /usr/local/src/imagick-2.2.1.tgz downloaded

パッケージのページからwgetでアーカイブを取得してもok。

展開してインストール
$ tar -zxvf imagick-2.2.1.tgz
$ cd imagick-2.2.1
$ phpize
$ ./configure
$ make
$ su -
# make install
Installing shared extensions:     /usr/local/lib/php/extensions/no-debug-non-zts-20060613/
モジュールのsymlinkを作成
# cd /usr/local/lib/php/extensions/
# ln -s no-debug-non-zts-20060613/imagick.so
php.iniを修正
[imagick]
extension=imagick.so

すべての作業が完了したらapacheをrestart。

基本的な合成

いくつかの画像を合成して1枚に仕上げる場合を開設する。前提は…

  • 素材はすべて同一サイズ
  • 透明部分は「透明」として保存されている
  • すべてGIF画像
素材の準備

上記の条件で素材を用意するだけ。

合成

やってることは簡単なのでソースコードを。要点は…

  • コンストラクタで受け取った画像ファイルの配列を読み込む

    (プライオリティ低→高でソートしておくこと)

  • 結果を乗せるために「出力サイズで透明の画像」を読み込んでおく

  • 透明のパレットを持つ画像を読み込んでおかないと合成パーツの透明が抜けないことがある

  • Imagickをごにょごにょして動的に生成するよりは持っちゃったほうが簡単

  • APCなどを使っているなら定数で持っちゃうという手もある

  • パスを指定して保存

GifCompositeSingle.php
<?php
class GifCompositeSingle
{
    // imagick objects
    private $imgs ;             // 合成対象(array)
    private $result ;           // 合成結果

    // 合成のベースとなる透明画像
    const TRANS_IMG = "img/blank.gif" ;

    /**
     *  コンストラクタ
     *      @param      array       &$img_files     画像ファイルのパス配列
     */

    public function __construct(&$img_files)
    {
        foreach ($img_files as $img_file)
        {
            $this->imgs[] = new Imagick($img_file) ;
        }
        $this->result = new Imagick(self::TRANS_IMG) ;
        $this->anim_delay = $anim_delay ;
    }

    /**
     *  合成
     */

    public function composite()
    {
        // 透明画像に全部かぶせて終了
        foreach ($this->imgs as $key => &$img)
        {
            $this->result->compositeImage($img, imagick::COMPOSITE_OVER, , ) ;
        }
        return true ;
    }

    /**
     *  合成結果を保存
     *      @param      string      $save_name      保存先のパス
     */

    public function save($save_name)
    {
        return $this->result->writeImage($save_name) ;
    }
}

//******************************************************************************
//  エントリポイント
//******************************************************************************
// 合成画像の配列
$img_files = array(
    "img/sara_yamaguchi.gif",
    "img/devlish_heart.gif"
) ;

$gifComposite = new GifCompositeSingle($img_files) ;
$gifComposite->composite() ;
$gifComposite->save("result_single.gif") ;
?>

アニメーションGIFの合成

アニメーションGIFを合成するにはいろいろとハマりポイント / バッドノウハウがある。いろいろ苦労したのを全部放出しちゃうよ。

素材の準備

合成対象の素材を用意するときは、画像の最適化オプションをオフにして保存する。こうすることで、すべてのフレームがすべての画素情報を持つファイルができあがる。ImageMagickUsageページ「Compression Optimization」に詳細な説明(英語)がある。合成結果を保存するところでも出てくるので、画像だけでも眺めておくこと。

念のため解説しておくと、アニメーションGIFの最適化には2つの方法がある。

  • バウンディングボックス

    →前のフレームから変更がある部分を矩形で切り出し、変更がない部分は省略する。

  • 透過

    →前のフレームと同一のピクセルは透明にする。

元画像
Devlish_heart
最適化なし
Devlish_heart_optimize_none_flatten
バウンディングボックスで最適化
Devlish_heart_optimize_bbox_flatten
バウンディングボックス + 透過で最適化
Devlish_heart_optimize_all_flatten

ImageReady / FireWorksなどのツールでは、保存時のオプションに最適化の項目があるので適宜変更する。Fireworks CS3の場合を例にすると、ファイル→名前をつけて保存→オプション→アニメーションと辿り、以下のオプションを外す。

Save_option

Imagick::getImagePageを使うとジオメトリ(そのフレームの幅 / 高さ / オフセット)が取得できるので、前のフレームを保存しておいてCompositeすることもできる…が、プログラムが煩雑になるので元データで用意してもらったほうが手っ取り早いだろう。パフォーマンスの測定はしていないが、利用メモリが増大するであろうことは容易に想像がつく。ファイルサイズの増大とのトレードオフなので、きちんと判断するならベンチマークを取るべきである。私の現場では「素材で対応」で決定してしまったが。

合成

読み込んだ各画像のフレーム数を取得し、最大のものを結果ファイルのフレーム数とする。フレーム数ループを回し、アニメーションアイテムの場合は順次フレームを進めながら合成していく。

最大フレーム数が、各画像のフレーム数の最小公倍数になっていないとループが崩れるのは見たとおり。12フレームと5フレームの画像を合成すると60フレームのGIFアニメが出来上がる、というのは非現実的だろう。

最適化

素材を用意するときは「最適化するな」と説明したが、合成結果画像は2つの理由から最適化する必要がある。

  • サイズの問題

    →携帯で見ること前提なので、ファイルサイズを縮小する必要がある。ページ全体でのサイズ制限があるため、画像サイズが70KBあたり(経験値)を超えると表示できない端末が出てくる。

  • パレットの問題

    →au端末はグローバルパレットになっている必要があるらしく、2フレーム目以降のパレット情報は無視される。

そして困ったことに、この2つの機能をImagickからは使えない(っぽい)。Imagick::optimizeImageLayersも、Imagick::deconstructImagesも、透過の最適化は行ってくれなかった。

なお、これらの関数はboolじゃなくて成功するとImagickオブジェクトを返す、というのはまめちしき。

しかたないのでコマンドラインを通す。最適化前の画像を[save_name].MIFFと保存しておいて…

$ convert [save_name].MIFF -layers OptimizeTransparency +map [save_name]

これでパレットの共通化と透過の最適化を行った画像が得られる。.MIFFファイルはテンポラリなので適宜削除すること。

ということでソースコード。さっきのもそうだが、エラー処理など一切省いているので注意されたい。

GifComposite.php
<?php
class GifComposite
{
    // imagick objects
    private $imgs ;             // 合成対象(array)
    private $result ;           // 合成結果
    private $anim_delay ;       // アニメーションディレイ

    // 合成のベースとなる透明画像
    const TRANS_IMG = "img/blank.gif" ;

    /**
     *  コンストラクタ
     *      @param      array       &$img_files     画像ファイルのパス配列
     *      @param      integer     $anim_delay     1コマの時間(msec)
     */

    public function __construct(&$img_files, $anim_delay = 20)
    {
        foreach ($img_files as $img_file)
        {
            $this->imgs[] = new Imagick($img_file) ;
        }
        $this->result = new Imagick(self::TRANS_IMG) ;
        $this->anim_delay = $anim_delay ;
    }

    /**
     *  合成
     */

    public function composite()
    {
        //----------------------------------------------------------------------
        // 最大フレーム数を算出
        //----------------------------------------------------------------------
        $max_frames = ;
        foreach ($this->imgs as &$img)
        {
            // 枚数をチェックして最大数を控える
            $num = $img->getNumberImages() ;
            if ($num > $max_frames) $max_frames = $num ;
            unset($img) ;
        }
        if ($max_frames <= ) return false ;

        //----------------------------------------------------------------------
        // アニメーションアイテムを読み込んでいなければ単純に合成して終了
        //----------------------------------------------------------------------
        if ($max_frames == 1)
        {
            // 透明画像に全部かぶせて終了
            foreach ($this->imgs as $key => &$img)
            {
                $this->result->compositeImage($img, imagick::COMPOSITE_OVER, , ) ;
            }

            $this->result->writeImages("result.gif", true) ;
            return true ;
        }

        //----------------------------------------------------------------------
        // アニメーションアイテムの合成
        //----------------------------------------------------------------------
        $comp_tmp = $this->result->clone() ; // 透明画像の控えを取っておく
        // アニメーションのコマ数ループ
        for ($i = ; $i < $max_frames ; $i ++)
        {
            // 2コマ目以降:積み込み先画像は透明画像のコピー
            if ($i > )
            {
                $target = $comp_tmp->clone() ;
            }
            // 1コマ目:積み込み先画像は結果として使う予定の透明画像
            else
            {
                $target = &$this->result ; // 使用後かならずunsetしないと悲惨
            }
            // 読み込み済みファイルをループ
           ;  foreach ($this->imgs as $key => &$img)
            {
                $count = $img->getNumberImages() ;  // 画像のコマ数
                // アニメーションアイテムならコマを指定
                if ($count > 1)
                {
                    $index = $i % $max_frames ;
                }
                // 通常アイテムは1コマ目
                else
                {
                    $index = ;
                }
                // コマ数を指定して$this->resultにかぶせる
                $img->setImageIndex($index) ;
                $target->compositeImage($img, imagick::COMPOSITE_OVER, $offset_x, $offset_y) ;
            }
            $target->setImageDelay($this->anim_delay) ; // アニメーションディレイの設定
            // 最初のコマ
            if ($i == )
            {
                $target->setImageIterations($loop_count) ; // アニメーションループの設定
            }
            // それ以降
            else
            {
                $this->result->addImage($target) ; // 結果画像に積む
                $target->destroy() ; // 後片付け
            }
            unset($target) ;    // 変数の解放(参照入れてるので重要)
        }
        return true ;

    }

    /**
     *  合成結果を保存
     *      @param      string      $save_name      保存先のパス
     */

    public function save($save_name)
    {
        $frames = $this->result->getNumberImages() ;

        // アニメーションしなければ普通に書いて終了
        if ($frames == 1)
        {
            return $this->result->writeImage($save_name) ;
        }

        // アニメーションするなら最適化
        // 結果画像を書き出す
        $this->result->writeImages("{$save_name}.MIFF", true) ;
        // Quantizeが使えないので保存ファイルをconvert +mapに通す
        exec("convert {$save_name}.MIFF -layers OptimizeTransparency +map {$save_name}", $ret, $result) ;
        // エラーチェックは省略
        return true ;
    }
}

//******************************************************************************
//  エントリポイント
//******************************************************************************
// 合成画像の配列
$img_files = array(
    "img/sara_yamaguchi.gif",
    "img/devlish_heart.gif",
) ;

$gifComposite = new GifComposite($img_files) ;
$gifComposite->composite() ;
$gifComposite->save("result.gif") ;
?>
結果

さっきのハートの画像と女性を合成するとこんな感じになる。透明のヌキが甘いのはご容赦いただきたい。

Result

まとめ

以上でアニメーションGIFを合成することができるようになる。ただし残念なことに、ImageMagickというツールは高機能だがパフォーマンスはあまりよくない。高負荷サイトの中の人は、自前で合成ツールを書いてしまう場合も多いようだ。

とはいえ、負荷をそれほど気にしなくていい場合、ImageMagickは有用なツールである。拡大 / 縮小 / トリミングができるのでサムネイルが作れるし、アニメーションGIFの特定のコマを抜き出して保存することもできる。画像の差分を二値化してマスクを作るとか、応用範囲は広い。上手に活用して、楽しいコンテンツを増やそう。

ImageMagick: Convert, Edit, and Compose Images

http://www.imagemagick.org/

ImageMagick v6 Examples

http://www.imagemagick.org/Usage/

Animation Optimization — IM v6 Examples

http://www.imagemagick.org/Usage/anim_opt/

PECL :: Package :: imagick

http://pecl.php.net/package/imagick

PECL :: imagick Changelog

http://pecl.php.net/package-changelog.php?package=imagick

PHP: ImageMagick – Manual

http://jp2.php.net/imagick

ウノウラボ Unoh Labs: ImageMagickでGIFアニメをリサイズ

http://labs.unoh.net/2008/12/imagemagickgif.html

写真素材 足成

http://www.ashinari.com/

AvatarsDB – Free avatars

http://www.avatarsdb.com/

おまけ

アニメーションGIFファイルの最適化状況を調べるツールを作った。ちょっとしたチェックが簡単にできるので、ついでに晒しておく。アップロードされたファイルの名前をそのままexecしているので、外から見えるところにボーっと放置しないように注意されたい。

checkanim.php
<?php
define("WORK_PATH", "/path/to/save/") ;
define("WORK_DIR" , "/path/to/tmp/") ;
exec("rm -f " . WORK_PATH . WORK_DIR . "*") ;
$montages = array() ;
$chache   = md5(mktime()) ;

// アップロードされたファイルをチェック
foreach ($_FILES as $file)
{
    // アップロードをチェック
    if (!is_uploaded_file($file['tmp_name']) || $file['error'] != UPLOAD_ERR_OK)
    {
        $ng ++ ;
        continue ;
    }
    // テンポラリから移動
    if (!move_uploaded_file($file['tmp_name'], WORK_PATH . WORK_DIR . $file['name']))
    {
        $ng ++ ;
        continue ;
    }
    // montage
    exec("montage " .
                      WORK_PATH . WORK_DIR .           $file['name'] . " " .
                      "-background ". $_POST['bgcolor'] . " " .
                      WORK_PATH . WORK_DIR . $chache . $file['name']) ;

    // 結果配列に名前を積む
    array_push($montages, $file['name']) ;
    $ok ++ ;
}
?>

<html>
<head>
    <meta http-equiv="Pragma" content="no-cache">
</head>
<body>
<form action="checkanim.php" method="POST" enctype="multipart/form-data">
    bgcolor:<br />
    <select name="bgcolor">
        <option value="white">white</option>
        <option value="black">black</option>
        <option value="red">red</option>
        <option value="green">green</option>
        <option value="magenta">magenta</option>
        <option value="cyan">cyan</option>
        <option value="yellow">yellow</option>
    </select>
    <br />
    files:<br />
    <input type="file"   name="img0" /><br />
    <input type="file"   name="img1" /><br />
    <input type="file"   name="img2" /><br />
    <input type="file"   name="img3" /><br />
    <input type="file"   name="img4" /><br />
    <input type="file"   name="img5" /><br />
    <input type="file"   name="img6" /><br />
    <input type="file"   name="img7" /><br />
    <input type="submit" value="check" />
</form>
<hr />
<?php foreach ($montages as $mont) { ?>
<?php echo $mont ; ?><br />
<img src="<?php echo WORK_DIR . $chache . $mont ; ?>"" style="border:1px solid black">
<hr>
<?php } ?>
</body>
</html>

ソースコード

全文掲載してしまったが、一応アーカイブしたものも添付しておく。

「GifComposite.zip」をダウンロード