Image::MagickとFFmpegで文字を動画にする。

FFmpegの -i オプションで連番の画像ファイルを動画に変換できるらしいです。
http://opensourceaki.blogspot.com/2007/10/ffmpeg_19.html

となれば話は簡単で、先ほどのtext2imageで連番の画像ファイルを生成してFFmpegで連結してあげればいいということになります。Image::Magickでα値を徐々に変化させて、フェードイン・フェードアウトを付けることもできます。

こんな感じで、

> perl text2movie.pl
Usage: text2movie.pl -t text [option(s)] outfile

Options:
    -t, --text       text to convert to movie
    -g, --size       size              (default: 640x480)
    -b, --bgcolor    background color  (default: black)
    -c, --fgcolor    foreground color  (default: white)
        --font       font              (default: C:/Windows/Fonts/meiryo.ttc)
        --pointsize  font size         (default: 56)
    -r, --rate       frame rate [fps]  (default: 30)
        --fadein     fade-in [sec]     (default: 0.7)
        --hold       hold [sec]        (default: 0.7)
        --fadeout    fade-out [sec]    (default: 0.6)

> perl text2movie.pl -t "Image::Magickと\nFFmpegで\n文字を動画にする\nテスト" out.avi

こんな感じの、ムービー編集ソフトで作るようなタイトル動画が生成できます。あとはタイトル動画と撮影した動画をMEncoderで連結してあげればGUIに頼ることなく見栄えのよい動画クリップを作成することができます。
プラットフォームに依存する部分はたぶんないので、ImageMagickFFmpegPerlがあればどこでも動くと思います。動画をバッチ処理で作成したいちょっとマニアックな方はお試しあれ。

コードはこちら。無保証、自己責任でよろしくです。

#!/usr/bin/perl

# text2movie

use strict;
use warnings;

use Image::Magick;
use Getopt::Long;
use Jcode;

my $DEBUG = $ENV{DEBUG} || 0;

# FFmpegの場所
my $ffmpeg  = "C:\\Path\\To\\ffmpeg.exe";

# 一時ファイルの作成場所
my $tmpdir  = "tmp_text2movie_$$";

# 一時ファイルの名前
my $tmpfile = "$tmpdir\\tmp%06d.jpg";

# オプションのデフォルト値
my %default = (
    size      => '640x480',
    bgcolor   => 'black',
    fgcolor   => 'white',
    font      => 'C:/Windows/Fonts/meiryo.ttc',
    pointsize => 56,
    rate      => 30,
    fadein    => 0.7,
    hold      => 0.7,
    fadeout   => 0.6,
);

# オプション
my %opts;

# 出力ファイル名
my $outfile;

# usage
sub usage {
    print <<EOM;
Usage: $0 -t text [option(s)] outfile

Options:
    -t, --text       text to convert to movie
    -g, --size       size              (default: $default{size})
    -b, --bgcolor    background color  (default: $default{bgcolor})
    -c, --fgcolor    foreground color  (default: $default{fgcolor})
    --font           font              (default: $default{font})
    --pointsize      font size         (default: $default{pointsize})
    -r, --rate       frame rate [fps]  (default: $default{rate})
    --fadein         fade-in [sec]     (default: $default{fadein})
    --hold           hold [sec]        (default: $default{hold})
    --fadeout        fade-out [sec]    (default: $default{fadeout})
EOM
    exit 1;
}

Main: {

    # コマンドラインオプション取得
    GetOptions(
        "t|text=s"    => \$opts{text},
        "g|size=s"    => \$opts{size},
        "b|bgcolor=s" => \$opts{bgcolor},
        "c|fgcolor=s" => \$opts{fgcolor},
        "font=s"      => \$opts{font},
        "pointsize=i" => \$opts{pointsize},
        "r|rate=i"    => \$opts{rate},
        "fadein=i"    => \$opts{fadein},
        "hold=i"      => \$opts{hold},
        "fadeout=i"   => \$opts{fadeout},
    ) or usage();

    $outfile = shift;
    usage() if ! defined $outfile;
    usage() if ! defined $opts{text};

    # 文字コードをUTF-8に変換
    my $text = Jcode->new( $opts{text} )->utf8;

    # 連続した画像を生成する
    seqimage( {
        filename  => $tmpfile,
        text      => $text,
        size      => $opts{size}      || $default{size},
        bgcolor   => $opts{bgcolor}   || $default{bgcolor},
        fgcolor   => $opts{fgcolor}   || $default{fgcolor},
        font      => $opts{font}      || $default{font},
        pointsize => $opts{pointsize} || $default{pointsize},
        rate      => $opts{rate}      || $default{rate},
        fadein    => $opts{fadein}    || $default{fadein},
        hold      => $opts{hold}      || $default{hold},
        fadeout   => $opts{fadeout}   || $default{fadeout},
    } );

    # 画像を連結して動画にする
    system qq($ffmpeg -r 30 -i $tmpfile) .
           qq( -vcodec mjpeg -sameq -y $outfile);

    # 一時ファイル削除
    while ( my $f = glob "$tmpdir/*" ) {
        unlink $f
    }
    rmdir $tmpdir;

    exit 0;
}

# 連続した画像を出力する
sub seqimage {
    my $arg = shift;

    # 一時ディレクトリ作成
    mkdir $tmpdir;
    
    # 文字色を取得する
    my $image = Image::Magick->new();
    my @fgcolor = $image->QueryColor( $arg->{fgcolor} );

    # α値を変化させて画像を生成する
    my $max = $arg->{rate} * ( $arg->{fadein} + $arg->{hold} + $arg->{fadeout} );
    for ( my $i = 0; $i < $max; $i++ ) {
        my $file = sprintf $arg->{filename}, $i;

        # α値計算
        my $alpha;
        if    ( $i < $arg->{rate} * $arg->{fadein} ) {
            $alpha = 1 / ( $arg->{rate} * $arg->{fadein} ) * $i;
            print "fade-in: $file, $alpha\n" if $DEBUG;
        }
        elsif ( $i < $arg->{rate} * ( $arg->{fadein} + $arg->{hold} ) ) {
            $alpha = 1;
            print "hold:    $file, $alpha\n" if $DEBUG;
        }
        else {
            $alpha = 1 / ( $arg->{rate} * $arg->{fadeout} ) * ( $max - $i );
            print "fadeout: $file, $alpha\n" if $DEBUG;
        }

        # 文字色
        my $fgcolor = "rgba(" . join(",", @fgcolor[0, 1, 2], $alpha) . ")";

        # 画像生成
        text2image( { 
            file      => $file,
            text      => $arg->{text},
            size      => $arg->{size},
            bgcolor   => $arg->{bgcolor},
            fgcolor   => $fgcolor,
            font      => $arg->{font},
            pointsize => $arg->{pointsize},
        } );
    }
}

# 文字を画像に変換する
sub text2image {
    my $arg = shift;

    # オブジェクト生成
    my $image = Image::Magick->new( size => $arg->{size} );

    # キャンバス生成
    $image->ReadImage( 'xc:' . $arg->{bgcolor} );

    # キャンバスサイズ取得
    my ( $width, $height ) = $arg->{size} =~ /(\d+)x(\d+)/g;

    # 文字描画位置取得
    my ( $char_width, $char_height, $ascender, $descender,
         $text_width, $text_height, $max_advance, 
         $bounds_x1, $bounds_y1, $bounds_x2, $bounds_y2,
         $origin_x, $origin_y )
    = $image->QueryFontMetrics(
        encoding  => 'UTF-8',
        text      => $arg->{text},
        font      => $arg->{font},
        fill      => $arg->{fgcolor},
        pointsize => $arg->{pointsize},
        align     => 'Center',
    );

    # 文字描画位置計算(画面中央)
    my $lines = ( $arg->{text} =~ s/\\n|\n/\\n/g );
    my $x = $width / 2;
    my $y = $height / 2 - $char_height * $lines / 2;

    # 文字描画
    $image->Annotate(
        encoding  => 'UTF-8',
        text      => $arg->{text},
        font      => $arg->{font},
        fill      => $arg->{fgcolor},
        pointsize => $arg->{pointsize},
        align     => 'Center',
        x         => $x,
        y         => $y,
    );

    # ファイル出力
    $image->Write($arg->{file});
}