読者です 読者をやめる 読者になる 読者になる

/dev/null/onishy

どーも。

VLC弄ってみた#3 - フィルタを追加する

第3回目の今回は、VLCのフィルタ機能について解説します。

VLCには色々な機能がありますが、その中でも1、2位を争うレベルで知られていない機能と言っても過言ではないのが、フィルタ機能です。

フィルタ機能とは?

普通に再生した動画は以下。

f:id:onishy:20151104004758p:plain

試しに、次のコマンドをつけてVLCを起動してみましょう。

vlc --video-filter rotate

このウィンドウで適当な動画を再生すると、次のような実行結果になります。

f:id:onishy:20151104004802p:plain

その名の通り、動画を45度回転させて再生する機能のようです。

一体どういう時に使うのか、サッパリ見当が付きません…

次に、このようなコマンドをつけて起動してみます。

vlc --video-filter ball

先ほど同様に動画を再生すると…?

f:id:onishy:20151104004807p:plain

!?!?!?

どうやら、動画から輪郭を抽出して、それを壁にボールが跳ねる仕様っぽい…??

こんなものがVLCの正式版に入っているんですから奇妙です。

フィルタ機能の実装

され、これらの謎のフィルタですが、どこに実装されているかというと、modules/video_filterの中です。これらのファイル一つ一つ、その数58個!!!そしてその大半が実用性の見いだせないおもちゃフィルタです。暇人かよ。

psychedelicとかも割とマジキチで面白いです。結果は試してみてください。

さてフィルタですが、既存のフィルタを踏襲すれば、比較的簡単に実装することができます。

ここでは、ballフィルタをパクって、ボールの代わりに画像を表示できるようにします。

モジュールの登録

ball.cの122行目、vlc_module_begin()から始まる部分が、vlcのライブラリにフィルタをモジュールとして登録しています。

vlc_module_begin ()
    set_description( N_("Ball video filter") )
    set_shortname( N_( "Ball" ))
    set_help(BALL_HELP)
    set_capability( "video filter2", 0 )
    set_category( CAT_VIDEO )
    set_subcategory( SUBCAT_VIDEO_VFILTER )

    add_string( FILTER_PREFIX "color", "red",
                BALL_COLOR_TEXT, BALL_COLOR_TEXT, false )
    change_string_list( mode_list, mode_list_text )

    add_integer_with_range( FILTER_PREFIX "speed", 4, 1, 15,
                            BALL_SPEED_TEXT, BALL_SPEED_LONGTEXT, false )

    add_integer_with_range( FILTER_PREFIX "size", 10, 5, 30,
                            BALL_SIZE_TEXT, BALL_SIZE_LONGTEXT, false )

    add_integer_with_range( FILTER_PREFIX "gradient-threshold", 40, 1, 200,
                            GRAD_THRESH_TEXT, GRAD_THRESH_LONGTEXT, false )

    add_bool( FILTER_PREFIX "edge-visible", true,
              EDGE_VISIBLE_TEXT, EDGE_VISIBLE_LONGTEXT, true )

    add_shortcut( "ball" )
    set_callbacks( Create, Destroy )
vlc_module_end ()

ballフィルタに関する情報がずらずらと並べられています。まずはこれを適当に変更します。

vlc_module_begin ()
    set_description( N_("Hemi video filter") )
    set_shortname( N_( "Hemi" ))
    set_help(BALL_HELP)
    set_capability( "video filter2", 0 )
    set_category( CAT_VIDEO )
    set_subcategory( SUBCAT_VIDEO_VFILTER )

    add_string( FILTER_PREFIX "color", "red",
                BALL_COLOR_TEXT, BALL_COLOR_TEXT, false )
    change_string_list( mode_list, mode_list_text )

    add_integer_with_range( FILTER_PREFIX "speed", 4, 1, 15,
                            BALL_SPEED_TEXT, BALL_SPEED_LONGTEXT, false )

    add_integer_with_range( FILTER_PREFIX "size", 10, 5, 30,
                            BALL_SIZE_TEXT, BALL_SIZE_LONGTEXT, false )

    add_integer_with_range( FILTER_PREFIX "gradient-threshold", 40, 1, 200,
                            GRAD_THRESH_TEXT, GRAD_THRESH_LONGTEXT, false )

    add_bool( FILTER_PREFIX "edge-visible", true,
              EDGE_VISIBLE_TEXT, EDGE_VISIBLE_LONGTEXT, true )

    add_shortcut( "hemi" )
    set_callbacks( Create, Destroy )
vlc_module_end ()

hemiというのは、僕の中でhogeと同義語の単語です。あまり気にしないでください。

BMP画像の読み込み

画像を表示するので、まずはbmpを読み込む関数を作らなければいけません。VLCには画像系の関数は色々充実していそうなものですが、bmpに出力する関数はあっても、読み込む関数はないようです。

とりあえず動けばいいので、今回はアルファチャンネルなどは考慮しません。GIMPで40x40に縮小した正方形の画像をbmpに出力した、どーもくんのアイコンを用います。 f:id:onishy:20151105015953j:plain

なのでここは、検索すると出てくる中でも一番シンプルな実装をされている、物理のかぎしっぽさんのソースコードを拝借させて頂きます。

ただ、こちらのソースコードには少し読み込みのバグがあり、そのまま使うと数ピクセル分黒いピクセルが現れた後、RGBが入れ替わった画像が不思議な周期で現れるというおかしな挙動を示してしまいます。

少しデバッグをすると分かるのですが、これはヘッダを読み込んだ際にバッファを先送りしていないためなので、56行目あたりに2行だけソースコードを追加します。

//68bytes捨てる
char null_buf[128];
fread(null_buf, sizeof(unsigned char), 68, fp); //Throw away

これで読み込み関数はOKです。

描画と基底変換

次に、ボールの代わりにこの画像を描画します。

読み込まれた画像は2次元配列になっているので、大して難しくはありません。元々円が描画されていた部分(drawBall)を、次のように変更するだけです。

static void drawHemi( filter_sys_t *p_sys, picture_t *p_outpic )
{
    Image *img = Read_Bmp(hemi_filename);

    int x = p_sys->i_hemi_x;
    int y = p_sys->i_hemi_y;
    int size = p_sys->i_hemiSize;

    const int i_width = p_outpic->p[0].i_visible_pitch;
    const int i_height = p_outpic->p[0].i_visible_lines;

    for( int j = y - img->height/2; j < y + img->height/2; j++ )
    {
        bool b_skip = ( x - size ) % 2;
        int m_y = j - (y-img->height/2);
        for( int i = x - img->width/2; i < x + img->width/2; i++ )
        {
            /* Draw the pixel if it is inside the disk
               and check we don't write out the frame. */
            {
                int m_x = i - (x-img->width/2);
                ( *p_sys->drawingPixelFunction )( p_sys, p_outpic,
                                    get_color_func(img->data[m_y * img->width + m_x]).comp1,
                                    get_color_func(img->data[m_y * img->width + m_x]).comp2,
                                    get_color_func(img->data[m_y * img->width + m_x]).comp3,
                                    i, j, b_skip );
                b_skip = !b_skip;
            }
        }
    }   
}

少しややこしいのは、VLCが必ずしもRGBで画像を描画するわけではないところです。

ball.cの冒頭、48行目付近を見てみます。

#define COLORS_RGB \
    p_filter->p_sys->colorList[RED].comp1 = 255; p_filter->p_sys->colorList[RED].comp2 = 0;        \
                                p_filter->p_sys->colorList[RED].comp3 = 0;        \
    p_filter->p_sys->colorList[GREEN].comp1 = 0; p_filter->p_sys->colorList[GREEN].comp2 = 255;    \
                               p_filter->p_sys->colorList[GREEN].comp3 = 0;       \
    p_filter->p_sys->colorList[BLUE].comp1 = 0; p_filter->p_sys->colorList[BLUE].comp2 = 0;        \
                               p_filter->p_sys->colorList[BLUE].comp3 = 255;      \
    p_filter->p_sys->colorList[WHITE].comp1 = 255; p_filter->p_sys->colorList[WHITE].comp2 = 255;  \
                                  p_filter->p_sys->colorList[WHITE].comp3 = 255;

#define COLORS_YUV \
    p_filter->p_sys->colorList[RED].comp1 = 82; p_filter->p_sys->colorList[RED].comp2 = 240;        \
                                p_filter->p_sys->colorList[RED].comp3 = 90;        \
    p_filter->p_sys->colorList[GREEN].comp1 = 145; p_filter->p_sys->colorList[GREEN].comp2 = 34;    \
                               p_filter->p_sys->colorList[GREEN].comp3 = 54 ;      \
    p_filter->p_sys->colorList[BLUE].comp1 = 41; p_filter->p_sys->colorList[BLUE].comp2 = 146;      \
                               p_filter->p_sys->colorList[BLUE].comp3 = 240;       \
    p_filter->p_sys->colorList[WHITE].comp1 = 255; p_filter->p_sys->colorList[WHITE].comp2 = 128;   \
                                  p_filter->p_sys->colorList[WHITE].comp3 = 128;

ここでYUVという文字列が出てきました。

YUV(Wikipedia)というのは、輝度信号Yと色差信号2つを用いて色を表現する方法です。

これを、231行目からのswitch文で次のように呼び出しています。

switch( p_filter->fmt_in.video.i_chroma )
{
    case VLC_CODEC_I420:
    case VLC_CODEC_J420:
        p_filter->p_sys->drawingPixelFunction = drawPixelI420;
        COLORS_YUV
        break;
    CASE_PACKED_YUV_422
        p_filter->p_sys->drawingPixelFunction = drawPixelPacked;
        COLORS_YUV
        GetPackedYuvOffsets( p_filter->fmt_in.video.i_chroma,
                             &p_filter->p_sys->i_y_offset,
                             &p_filter->p_sys->i_u_offset,
                             &p_filter->p_sys->i_v_offset );
        break;
    case VLC_CODEC_RGB24:
        p_filter->p_sys->drawingPixelFunction = drawPixelRGB24;
        COLORS_RGB
        break;
    default:
        msg_Err( p_filter, "Unsupported input chroma (%4.4s)",
                 (char*)&(p_filter->fmt_in.video.i_chroma) );
        return VLC_EGENERIC;
}

コーデックによって、RGBを用いるか、YUVを用いるかを決めています。

そして、この処理結果を395行目で次のように利用しています。

( *p_sys->drawingPixelFunction )( p_sys, p_outpic,
                    p_sys->colorList[ p_sys->ballColor ].comp1,
                    p_sys->colorList[ p_sys->ballColor ].comp2,
                    p_sys->colorList[ p_sys->ballColor ].comp3,
                    i, j, b_skip );

見ての通り、特定の色のみをサポートしていて(ボールの色は単色で良い)、かつ単色のためよほど変な色でない限り問題ありません。

BMPから取得した色はRGBであるため、これをYUV空間の値に変換する必要があります。この辺によると、次の変換公式を使うことができます。

Y = 0.299R + 0.587G + 0.114B
U = -0.169R - 0.331G + 0.500B
V = 0.500R - 0.419G - 0.081B

Y = (0.257 * R) + (0.504 * G) + (0.098 * B) + 16
Cr = V = (0.439 * R) - (0.368 * G) - (0.071 * B) + 128
Cb = U = -(0.148 * R) - (0.291 * G) + (0.439 * B) + 128

これを基に、先ほどのマクロがあった部分を次のように書き換えます。

typedef struct{
    unsigned char b;
    unsigned char g;
    unsigned char r;
}Rgb;

typedef struct{
    char comp1;
    char comp2;
    char comp3;
}color_t;

color_t (*get_color_func)(Rgb color);

color_t get_rgb_color(Rgb color) {
    color_t c = {color.b, color.g, color.r};
    return c;
}

// Y =  0.299R + 0.587G + 0.114B
// U = -0.169R - 0.331G + 0.500B
// V =  0.500R - 0.419G - 0.081B

// Y  =      (0.257 * R) + (0.504 * G) + (0.098 * B) + 16
// Cr = V =  (0.439 * R) - (0.368 * G) - (0.071 * B) + 128
// Cb = U = -(0.148 * R) - (0.291 * G) + (0.439 * B) + 128

color_t get_yuv_color(Rgb color) {
    color_t c = {
        (int)( 0.257 * color.r + 0.504 * color.g + 0.098 * color.b)+16,
        (int)( 0.439 * color.r - 0.368 * color.g - 0.071 * color.b)+128,
        (int)(-0.148 * color.r - 0.291 * color.g + 0.439 * color.b)+128};

    return c;
}

#define COLORS_RGB \
    p_filter->p_sys->colorList[RED].comp1 = 255; p_filter->p_sys->colorList[RED].comp2 = 0;        \
                                p_filter->p_sys->colorList[RED].comp3 = 0;        \
    p_filter->p_sys->colorList[GREEN].comp1 = 0; p_filter->p_sys->colorList[GREEN].comp2 = 255;    \
                               p_filter->p_sys->colorList[GREEN].comp3 = 0;       \
    p_filter->p_sys->colorList[BLUE].comp1 = 0; p_filter->p_sys->colorList[BLUE].comp2 = 0;        \
                               p_filter->p_sys->colorList[BLUE].comp3 = 255;      \
    p_filter->p_sys->colorList[WHITE].comp1 = 255; p_filter->p_sys->colorList[WHITE].comp2 = 255;  \
                                  p_filter->p_sys->colorList[WHITE].comp3 = 255;   \
    get_color_func = get_rgb_color; printf("using rgb mode\n");

#define COLORS_YUV \
    p_filter->p_sys->colorList[RED].comp1 = 82; p_filter->p_sys->colorList[RED].comp2 = 240;        \
                                p_filter->p_sys->colorList[RED].comp3 = 90;        \
    p_filter->p_sys->colorList[GREEN].comp1 = 145; p_filter->p_sys->colorList[GREEN].comp2 = 34;    \
                               p_filter->p_sys->colorList[GREEN].comp3 = 54 ;      \
    p_filter->p_sys->colorList[BLUE].comp1 = 41; p_filter->p_sys->colorList[BLUE].comp2 = 146;      \
                               p_filter->p_sys->colorList[BLUE].comp3 = 240;       \
    p_filter->p_sys->colorList[WHITE].comp1 = 255; p_filter->p_sys->colorList[WHITE].comp2 = 128;   \
                                  p_filter->p_sys->colorList[WHITE].comp3 = 128;   \
    get_color_func = get_yuv_color; printf("using yuv mode\n");

こうすることで、get_color_funcが、色情報によって正しい色を返してくれるため、画像をYUVに変換することができました。

コード全体像:

そして最後に、Makefileをいじっておきます。modules/video_filter/Makefile.amの中に、次の段落を追加します。

libhemi_plugin_la_SOURCES = $(SOURCES_hemi)
libhemi_plugin_la_CPPFLAGS = $(AM_CPPFLAGS) $(CPPFLAGS_hemi)    -DMODULE_NAME_IS_hemi
libhemi_plugin_la_CFLAGS = $(AM_CFLAGS) $(CFLAGS_hemi)
libhemi_plugin_la_CXXFLAGS = $(AM_CXXFLAGS) $(CXXFLAGS_hemi)
libhemi_plugin_la_OBJCFLAGS = $(AM_OBJCFLAGS) $(OBJCFLAGS_hemi)
libhemi_plugin_la_LIBADD = $(LIBS_hemi)
libhemi_plugin_la_LDFLAGS = $(AM_LDFLAGS) -rpath '$(video_filterdir)' $(LDFLAGS_hemi)

プロジェクトのディレクトリでconfigureとmakeを実行して完成です。

実行結果:

f:id:onishy:20151105015733p:plain

おまけ: VLC+OpenCV

さて、フィルタと言えばOpenCVですが、以前も少しだけ触れたとおり、VLCOpenCVを使うのはちょっとテクニックが必要です。

というのも、VLCのpicture_tには色空間がYUVの場合が存在するわけですが、このために少し変換がややこしいわけです。

実は、VLCのフィルタの中にopencv_example.cppというのがあります。OpenCVを使うからには、IplImageなりcv::Matなりにpicture_tを変換する必要があるわけですが、何と、こいつによると次のように変換できるらしいんですね。

158行目

//(hack) cast the picture_t to array of IplImage*
p_img = (IplImage**) p_pic;

これはさすがにまさか嘘だろ、と思いますが、真っ赤な嘘です。

実際こいつでコンパイルするとセグフォを吐きやがります。

少しググると、StackOverflowでこのような記事を見つけることができます。こちらの記事によれば、正しいコードは以下の通りです。

//picture_t to IplImage without segmentation fault
    p_img = cvCreateImageHeader( cvSize( p_pic->p[0].i_pitch, p_pic->p[0].i_visible_lines ),
        IPL_DEPTH_8U, 1 );
    cvSetData( p_img, p_pic->p[0].p_pixels, p_pic->p[0].i_pitch );

随分まともな雰囲気が漂っていますね。IplImageを(コンストラクタのない時代なので)初期化関数によって初期化し、次にpicture_tの色情報を書き込んでいますが、いずれもp[0]のみを利用しています。

p[0]というのは、picture_tのYUVのうちYのことで、これには輝度情報が含まれています。つまり、この変換によって、p_imgにはpicture_tのもつ画像情報のうち、モノクロのデータのみがコピーされます。他の色情報(色差)は完全に欠落してしまいます。実際にimShowとかで表示させてみると分かるでしょう。

ただ、このopencv_exampleフィルタは、どうやら顔認識を行うだけのフィルタなようなので、これだけでも問題がないわけです。(僕の手持ちの動画では残念ながら本当に顔認識をしているのかは分かりませんでしたが…)

OpenCVの色認識などのフィルターを使いたければ、YUVの3レイヤー全ての情報を使わなければいけません。ただ、残念ながらIplImageは古いので、RGBしかサポートしません。cv::MatならYUVでもサポートしてくれます。正直できそうなもんですが、今回は学生実験であまり時間がないので、またの機会にすることにします。