2010/04/23

V4L2のサンプルコードで遊んでみる

手元にWebカメラがあってbeagleboardに接続して使えないかなと思って、V4L2 APIを使ってみました。 V4L2 APIリファレンスのAppendix B. "Video Capture Example"にあるコードを使うと、static void process_image(void *p)関数の中身を作成するぐらいで動画や静止画を書き出す事ができます。

ただ「〜するぐらいで」とはいっても、使うUSBカメラのスペックなども関連して、 そんなに簡単ではなったので試行錯誤した結果をまとめておきます。

今回はbeagleboardに入れたdebianに、ライブラリなどを入れましたが、特に問題なく試す事ができると思います。

環境

今回はprocess_image関数を実装するために、motion(GPLv2)のコードを参考にしました。 参考にしたものも合わせて、環境をまとめると次のようになります。

  • HW: BeagleBoard Rev. C3
  • USB Camera: BUFFALO BSW13K05H
  • OS: Debian Lenny 5.0.4
  • Kernel(uname -r): 2.6.32.6-x6.2
  • gcc --version: gcc (Debian 4.3.2-1.1) 4.3.2
  • Packages: libjpeg62 libjpeg62-dev
  • References: motion-3.2.9 (derived by: apt-get source motion)

サンプルコードをコンパイルする

まずは APIリファレンスのAppendix B.にあるコードをコピーして、コンパイルできるかどうか確認するのが良さそうです。 とりあえず"v4l2_example.c"という名前でコピーしてきました。

$ gcc v4l2_example.c

特別なライブラリをリンクすることもなくコンパイルできるはずです。 これはそのまま実行できます。

$ ./a.out -d /dev/video0

正常に実行できた場合

....................................................................................................

/dev/video0が存在しない場合

Cannot identify '/dev/video0': 2, No such file or directory

v4l2_format構造体に指定するデバイスの初期設定値が間違っていた場合

VIDIOC_S_FMT error 22, Invalid argument

コード中では /dev/video が指定されていますが、debian/ubuntuではudevはデバイス名の最後に番号を割り振ってくれます。 "-d"オプションを使う必要がないように"/dev/video"を"/dev/video0"に書き換えて良いのかもしれません。

fmt.pixelformatに設定できるデバイスの画像形式を確認する

最後の「v4l2_format構造体に指定するデバイスの初期設定値が間違っていた場合」では、試した範囲では接続したUSBカメラに対応したpixelformatを選択しない場合に、エラーになるようです。

サンプルコードではカバーされていませんが、ioctlでVIDIOC_ENUM_FMTを要求するとデバイスからpixelformatに設定できるフォーマットを得る事が可能です。

例えば次のような関数をmain()の中のopen_device();の直後に呼ぶと、出力例のようにデバイスのサポートする形式が出力されます。

motionを参考にしたVIDIOC_ENUM_FMTを利用したコード例

static void list_formats()
{
    int i;
    struct v4l2_fmtdesc fmt;
    fmt.index = i = 0;
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    while(-1 != xioctl(fd, VIDIOC_ENUM_FMT, &fmt)) {
        printf("%i: %c%c%c%c (%s)\n", fmt.index,
                           fmt.pixelformat >> 0, fmt.pixelformat >> 8,
                           fmt.pixelformat >> 16, fmt.pixelformat >> 24, fmt.description);
        memset(&fmt, 0, sizeof(struct v4l2_fmtdesc));
        fmt.index = ++i;
        fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    }
}

実行時のlist_formats()の出力例

0: YUYV (YUV 4:2:2 (YUYV))
...

このコードはmotionを参考にしました。

検索でみつかるV4L2 APIについてのプログラミング例は、だいたいV4L2 APIのサンプルをベースにしているようです。 そういうサイトの説明ではpixelformatに、V4L2_PIX_FMT_RGB24やV4L2_PIX_FMT_MJPEGを指定しているものがありますが、これは接続するUSBカメラに合ったものでなければなりません。

問題となるコード例

...
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB24;
...

今回の場合は、"YUYV"と表示されているのでV4L2_PIX_FMT_YUYVだけが唯一pixelformatに指定できる形式です。

"VIDIOC_S_FMT error 22"をキーにして検索するとMLに質問を投げて無視されているようなものもみつかりますが、まずは使うUSBカメラがサポートする形式を調べるところから始める必要がありそうです。

外部コマンドによるデバイス出力形式の確認

自前でコードを書かなくとも、デバイスの出力形式を調べることは可能です。

例えば、luvcviewコマンドの'-L'オプションを使う方法があります。

$ luvcview -L

luvcviewコマンドの出力例

luvcview 0.2.4

SDL information:
  Video driver: x11
  A window manager is available
Device information:
  Device path:  /dev/video0
/dev/video0 does not support read i/o
{ pixelformat = 'YUYV', description = 'YUV 4:2:2 (YUYV)' }
...

ブログなどをみると比較的高めなUSBカメラで、MJPEGにも対応しているものがあるようです。 デバイス毎の出力形式が一覧になっていると良いんですけどね。

YUYV(YUV422)形式からJPEGファイルへの出力の作成

画像処理ではtiff形式をよく使いましたし、汎用性が高そうなYUV422からRGBへの変換などは他にまとめている方が大勢います。 ここら辺は専門分野ではないので、motionのコードを流用する事にしました。

今回のUSBカメラとV4L2 APIを使った背景目的は、動画ファイルの作成ではなくて、監視カメラとして1,2秒間隔で静止画を得る事だったのでJPEG形式に出力させてみました。 motionをそのまま使ったほうが良いんじゃないかって感じですが、swfファイルとか必要ないですし、V4L2の勉強を兼ねてコンパクトなアプリを作ることにしました。

motionはデバイスからのRGB24やらYUYVやらの出力形式を受けて、YUV420形式に統一して扱っています。 JPEGファイルに出力する場合は、さらにそのYUV420形式を入力にします。

motionからのコードコピー

関数の内容は次のプロトタイプ宣言に対応するコードをmotionのvideo_common.cとpicture.cからコピーしておきます。

motionからコピーしてきた関数のプロトタイプ宣言

void conv_yuv422to420p(unsigned char *map, unsigned char *cap_map, int width, int height);
void put_jpeg_yuv420p_file(FILE *fp, unsigned char *image, int width, int height, int quality);

put_jpeg_yuv420p_fileはlibjpegライブラリを使うため、libjpeg.hファイルをincludeする事が必要です。

サンプルコードに追加するinclude文

#include <jpeglib.h>
#include <jerror.h>
process_image関数の実装

オリジナルのサンプルコードは、単純に'.'(ピリオド)を表示するだけのコードですが、(void *)型のp変数から準備した関数を使い画像形式を変換していきます。

process_image関数の概要

int ya_count = 0;
#define IMG_WIDTH 640
#define IMG_HEIGHT 480
#define FILENAME_LEN 10
...
static void process_image (const void *p) {
    FILE *fp;
    unsigned char *dst;
    char filename[FILENAME_LEN];

    snprintf(filename, FILENAME_LEN, "p_%02d.jpeg", ya_count++);
    fp = fopen(filename, "w");
    dst = (unsigned char *)malloc(sizeof(unsigned char) * (IMG_WIDTH*IMG_HEIGHT) * 2);
    conv_yuv422to420p(dst, (unsigned char *) p, IMG_WIDTH, IMG_HEIGHT);
    put_jpeg_yuv420p_file(fp, dst, IMG_WIDTH, IMG_HEIGHT, 99);
    free(dst);
    fclose(fp);
}

毎回mallocしなくても、一回確保してmemsetで初期化した方が良さそうにも見えますね…。

$ gcc v4l2_example.c -ljpeg

beagleboardなのでSDカードに220KB程度のファイルを書き出していますが、とりあえずI/Oはボトルネックになっていない模様。

とりあえず動くコードはできたので、V4L2のAPIリファレンスを読み進めていこうと思います。

この記事で取り上げた品々

0 件のコメント: