QuickTime Cubic VR mov から 6 枚の JPEG 画像をとりだす

以前作った Cubic VR ファイルの元であった 6 枚の画像を捨ててしまっていたものがあったので、 mov ファイルから画像を取り出すスクリプトを作りました。 QuickTime の定義部分を読み出すスクリプトPerl で 10 年以上前に書いたものがあったので、 それを流用しています。

extract six faces from a QuickTime Cubic VR mov file

QuickTime のファイルは複数のトラックをファイルにまとめてあり、 Cubic VR の場合は、 トラック 1 が qtvr、 トラック 2 が pano に割り当ててあります。 この 2 つのトラックは画像を持たない、 パノラマのふるまいデータをトラックにしたものです。 画像はトラック pano が参照しているサンプリング・トラックに入っています。 このトラックの定義データに 6 枚の画像へのオフセットとサイズが記入してあるので、 それを使って画像を取り出します。 なお、 厳密にはトラック 2 の本体ふるまいデータを読み出して、 それの panoType メンバが cube になっていることをチェックするべきかもしれません。 ですが、 6 枚の画像になっているのは Cubic VR のはずなので、 枚数をチェックするだけにしています。

sub fetch_stbl {
    my($moov) = @_;
    my $trak = $moov->{'moov'}{'trak'};
    my $face = $trak->{'2'}{'tref'}[0]{'ref'}[0];
    my $nstco = @{$trak->{$face}{'mdia'}{'minf'}{'stbl'}{'stco'}};
    my $nstsz = @{$trak->{$face}{'mdia'}{'minf'}{'stbl'}{'stsz'}};
    $nstco == 6 or croak "not a cubic qtvr";
    $nstsz == 6 or croak "not a cubic qtvr";

    return $trak->{$face}{'mdia'}{'minf'}{'stbl'};
}

JPEG 画像の本体であるバイナリ列は、 オフセットへシークしてサイズ分読み出せば手に入ります。 それらに連番をふってファイルへ書き出します。

sub extract_jpeg {
    my($fd, $stbl, $basename) = @_;
    -e $basename or mkdir $basename;
    my $blob;
    for my $i (0 .. 5) {
        my $offset = $stbl->{'stco'}[$i];
        my $size = $stbl->{'stsz'}[$i];
        seek $fd, $offset, 0;
        $size == (read $fd, $blob, $size) or croak "read error";

        open my $jpg, '>', "$basename/$i.jpg" or croak "open: $!";
        binmode $jpg, ':raw';
        print $jpg $blob;
        close $jpg;
    }
}

QuickTime のトラック定義は mov ファイルの先頭から atom と呼ばれる入れ子のバイナリ列になっています。 それらをひたすら読んでいきます。 画像や音声本体ではなく定義データは moov の名称の atom の中に入っているので、 そこをハッシュ・リファレンスの $moov へ読み出します。 atom は先頭 4 バイトがビッグ・エンディアンのサイズで、 次の 4 バイトが atom の名称になっています。 サイズは先頭のサイズと名称も含んでおり、 既に読んだ 8 バイトを除いて、 get_atom へ渡します。

sub get_moov {
    my($fd, $moov) = @_;
    while (! eof($fd)) {
        my($len, $type) = unpack 'Na4', get_bytes($fd, 8);
        last if $len <= 0;
        if ($type eq 'moov') {
            get_atom($fd, $moov, $len - 8, $type);
            last;
        }
        else {
            seek $fd, $len - 8, 1;
        }
    }
}

get_atom はハッシュ・リファレンスの入れ子を深くして、 コンテナのときは再帰呼び出しをします。 トラック atom に対してはトラック識別子のハッシュにする追加処理をおこないます。 基本 atom は、 pack でバイナリをメンバに分解してハッシュ・リファレンスや配列リファレンスを作るのですが、 記述が煩雑なので省略します。

sub get_atom {
    my($fd, $atom, $skip, $parent) = @_;
    my $node = {};
    if ($parent ne 'trak') {
        $node = $atom->{$parent} = {};
    }
    while ($skip > 0) {
        my($len, $type) = unpack 'Na4', get_bytes($fd, 8);
        last if $len <= 0;
        $skip -= $len;
        if (exists $QT_CONTAINER_ATOM{$type}) {
            get_atom($fd, $node, $len - 8, $type);
        }
        elsif (exists $QT_SIMPLE_ATOM{$type}) {
            $QT_SIMPLE_ATOM{$type}->($fd, $node, $len - 8);
        }
        else {
            seek $fd, $len - 8, 1;
        }
    }
    if ($parent eq 'trak') {
        my $id = $node->{'tkhd'}{'track_id'};
        if (! exists $atom->{'trak'}) {
            $atom->{'trak'} = {};
        }
        $atom->{'trak'}{$id} = $node;
    }
}