Raspberry Pi でUSBカメラ & 2D描画 OpenVG を使ってみる

H264のハードウェアエンコードを試そうとしてたら、
USBカメラとOpenVGのネタが溜まったので紹介します。

いわゆるWebカメラ的な物は持っていないので、今回は下記のUSBマイクロスコープを使用します。



こんな感じで接続、設置しました。
RaspberryPiも普通に認識しているようです。



ここからが動くようになるまで結構長かった。(>_<)

まずはキャプチャーですね~
キャプチャー関係は Video for Linux two (v4l2)でいけることは知っていましたが、
一度も使ったことがないので苦戦しまくり。

最初は既存のツールのみで表示させてみます。

まずは ffmpeg をコンパイルです。
ffmpeg から分裂した avconv なら apt-get で取得できるのですが、
H264のハードウェアエンコード試してたら、ffmpeg のほうが相性いいようなので
コンパイルして使えるようにします。

以下のサイトを参考にすれば普通に出来ると思います。
RaspberryPi を1GHzにオーバークロックしても2時間くらいです...(;´д`)
mkfifo pipe
 ./ffmpeg -f video4linux2 -s 320x240 -i /dev/video0 -vcodec mpeg4 -f avi pipe:1 > pipe

別コンソールで
omxplayer pipe

これで XWindow なくても表示は出来ますね。
でも、ラグが10~15秒もあります...( ̄~ ̄;)
エンコードに時間かかってるのかな?
ffmpegのオプション追加でフレームレートを3fpsに下げてみます。
./ffmpeg -f video4linux2 -s 320x240 -i /dev/video0 -vcodec mpeg4 -f avi -r 3 pipe:1 > pipe
変化なし...
omxplayerのバッファリングが原因かもしれませんね。

もっとラグなしで表示したいな~「プログラム自作して表示してしまおう」
と思ってしまった。
これがまた苦戦しまくり、かなり時間をかけてしまいました。
最近はJavaばかりなので、C言語のブランクが5年以上あります。
これだけブランクあると、いろいろ忘れていること多すぎでダメダメですな (;´д`)

まずは v4l2 をプログラムで扱うにはどうしたらいいのかですね。
これはネット上に情報がたくさんあるので Google先生に聞けばOKっと。

2D描画の方ですが、RaspberryPi の /opt/vc/src/hello_pi 配下にサンプルプログラムが
幾つかあります。
/opt/vc/src/hello_pi/hello_dispmanx/ が2D描画のサンプルっぽいですね。
が!
これのことを調べてたら OpenVG のラッパーライブラリを見つけました。

https://github.com/ajstarks/openvg
そうそう、これこれ!
こういう簡単なのがほしかったのだ~(=´∇`=)
ってことで、これをコンパイルします。
まずは zip で持ってきて展開
sudo  apt-get install libjpeg-dev
cd 展開したところ
make
※エラー出るけど libshapes.o、oglinit.o ができてればOK
サンプルプログラムもあるのでコンパイルする場合は以下のようにする。
cd client
※ Makefile編集でgccオプションに -lEGL -lbcm_host 追加
make

最終的に v4l2 と OpenVG で作ってみたテストプログラムを動かすと以下のような感じ。
キャプチャー画像はほぼリアルタイムで更新され、XWindowなしで動作しています。



キャプチャー部分拡大 (写真撮ったタイミング違うけど)



「青い円、テキストを2行、線1本、キャプチャー画像を描く」を無限ループで繰り返してます。

もし、キャプチャー時に select timeout する場合は
以下のようにドライバーを入れ直すといいかもしれません。
sudo rmmod uvcvideo
sudo modprobe uvcvideo nodrop=1 timeout=5000
このUSBカメラとRaspberryPiだと、取り込み画像は320x240が限界のようです。
640x480だと画像が乱れまくりでした。

USBカメラの画像フォーマットは以下のようにして、知ることが出来ます。
sudo apt-get install v4l-utils
v4l2-ctl --list-formats-ext
キャプチャー部分のコーディングに役立つかと...思う。

スクリーンショットには日本語が出ていますが、デフォだと日本語は表示できません。
OpenVG自体は文字描画に対応していない?のかな。
ラッパーのほう覗いてみると、半角アルファベットの描画方法は FreeType を使って
PATHオブジェクトを生成しているようです。
じゃ~日本語もいけそうな感じがしますね。
が、完全対応はめんどくさいので、」だけ表示できるようにしてみます。
まずは準備~
Windows7 から日本語フォント dfpop.ttc を RaspberryPi にコピー
sudo apt-get install libfreetype6-dev
cd OpenVGラッパーを展開したところ
font2openvg.cpp 編集
make font2openvg
font2openvg dfpop.ttc dfpop.inc dfpop
とすると dfpop.ttf を元に dfpop.inc が生成されます。
中身はPATHオブジェクトのデータになっているようです。
これを自作プログラムで #include "dfpop.inc" して、loadfont() すればOKです。

font2openvg.cpp の編集は以下のように1カ所だけ。



dfpopフォントを使うとどんな半角を表示しようとしても、「あ」ばかり表示されます~(笑)

日本語完全対応は...私は必要ないので、他の方任せた!(グホッ

テストプログラムをコンパイルする時は
fontinfo.h
shapes.h
libshapes.o
oglinit.o
をOpenVGラッパーから持ってきて、
gcc -o z main.c libshapes.o oglinit.o -I/opt/vc/include/ -I/opt/vc/include/interface/vcos/pthreads -lbcm_host -L/opt/vc/lib/ -lGLESv2 -ljpeg -lEGL
別のフォントを使いたい場合は font2openvg で生成した inc ファイルも必要です。

ソースコードは折りたたんでます。






/*
 * main.c
 *
 * 参考ページ
 * https://github.com/ajstarks/openvg
 *
 * gcc -o z main.c libshapes.o oglinit.o -I/opt/vc/include/ -I/opt/vc/include/interface/vcos/pthreads -lbcm_host -L/opt/vc/lib/ -lGLESv2 -ljpeg -lEGL
 */

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <assert.h>
#include <linux/videodev2.h>

#include "bcm_host.h"
#include "VG/openvg.h"
#include "VG/vgu.h"
#include "fontinfo.h"
#include "shapes.h"

#include "dfpop.inc"

#include "mycap.h"

#define WIDTH   320
#define HEIGHT  240

static int CanIRecv(int fd)
{
    fd_set fdset;
    struct timeval timeout;
    FD_ZERO(&fdset);
    FD_SET(fd, &fdset);
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;
    return select(fd + 1, &fdset, NULL, NULL, &timeout);
}

static VGImage createImageFromBGR(int imageWidth, int imageHeight)
{
    int w, h;
    VGubyte *orgData = (VGubyte *) malloc(imageWidth * 4 * imageHeight);
    VGubyte *data = orgData + imageWidth * 4 * (imageHeight - 1);
    unsigned char *tmpImage = pCapImage;

    for (h = 0; h < imageHeight; h++) {
        for (w = 0; w < imageWidth; w++) {
            data[0] = tmpImage[2];
            data[1] = tmpImage[1];
            data[2] = tmpImage[0];
            data[3] = 0xFF;
            data += 4;
            tmpImage += 3;
        }
        data -= imageWidth * 4 * 2;
    }

    VGImage img = vgCreateImage(VG_sABGR_8888, imageWidth, imageHeight, VG_IMAGE_QUALITY_BETTER);
    vgImageSubData(img, orgData, imageWidth * 4, VG_sABGR_8888, 0, 0, imageWidth, imageHeight);

    free(orgData);

    return img;
}

int main()
{
    // v4l2,OpenVG共通変数
    int imageWidth = WIDTH;
    int imageHeight = HEIGHT;
    pCapImage = malloc(imageWidth * imageHeight * 3);

    // v4l2用変数
    char *dev_name = "/dev/video0";
    int fd = -1;
    int pixel_format = 0; // YUV
    struct buffer *buffers = NULL;
    int n_buffers;

    // OpenVG用変数
    int sw, sh; // スクリーンサイズ
    Fontinfo jpFont;

    // v4l2 前処理
    open_device(&fd, dev_name);
    buffers = init_device(&fd, dev_name, imageWidth, imageHeight, &n_buffers, pixel_format);
    start_capturing(&fd, &n_buffers);

    // OpenVG初期化
    init(&sw, &sh);
    jpFont = loadfont(dfpop_glyphPoints,
                    dfpop_glyphPointIndices,
                    dfpop_glyphInstructions,
                    dfpop_glyphInstructionIndices,
                    dfpop_glyphInstructionCounts,
                    dfpop_glyphAdvances, dfpop_characterMap, dfpop_glyphCount);

    // メインループ
    for (;;) {
        // v4l2画像取得
        mainloop(&fd, imageWidth, imageHeight, &n_buffers, buffers, pixel_format);

        Start(sw, sh);
        Background(0, 0, 0);

        Rotate(20); // 20度回転:VGImageは影響を受けないみたい

        // 円
        Fill(44, 77, 232, 1);
        Circle(sw / 2, 0, sw);
        // テキスト
        Fill(255, 255, 255, 1);
        TextMid(sw / 2, sh / 2, "hello, world", SerifTypeface, sw / 10);
        TextMid(800, 0, "aaaaaaaaaaaaaaa", jpFont, sw / 10);

        // 線
        Stroke(255, 0, 0, 1);
        StrokeWidth(5);
        Line(0, 0, 100, 100);

        // キャプチャー画像
        VGImage img = createImageFromBGR(imageWidth, imageHeight);
        vgSetPixels(100, 300, img, 0, 0, sw, sh);
        vgDestroyImage(img);

        End();

        // Enterキーで終了
        if (CanIRecv(0)) {
            break;
        }
    }

    // OpenVG後処理
    unloadfont(jpFont.Glyphs, jpFont.Count);
    finish();

    // v4l2 後処理
    stop_capturing(&fd);
    uninit_device(&n_buffers, buffers);
    close_device(&fd);

    return 0;
}

/*
 * mycap.h
 *
 * 引用元
 * http://alumnos.elo.utfsm.cl/~yanez/video-for-linux-2-sample-programs/
 * http://ivis-mynikki.blogspot.jp/2011/01/v4l2_31.html
 */

#ifndef MYCAP_H_
#define MYCAP_H_

static unsigned char* pCapImage;

#define CLEAR(x) memset (&(x), 0, sizeof (x))

//info needed to store one video frame in memory
struct buffer
{
    void * start;
    size_t length;
};

static void errno_exit(const char *s)
{
    fprintf(stderr, "%s error %d, %s\n", s, errno, strerror(errno));
    exit(EXIT_FAILURE);
}

//a blocking wrapper of the ioctl function
static int xioctl(int fd, int request, void *arg)
{
    int r;

    do {
        r = ioctl(fd, request, arg);
    } while (-1 == r && EINTR == errno);

    return r;
}

static void Conv_YUYV2BGR(void *src_img, int width, int height)
{
    unsigned char *yuyv = src_img;
    unsigned char *bgr = pCapImage;
    int z = 0;
    int x;
    int yline;

    for (yline = 0; yline < height; yline++) {
        for (x = 0; x < width; x++) {
            int r, g, b;
            int y, u, v;

            if (!z)
                y = yuyv[0] << 8;
            else
                y = yuyv[2] << 8;
            u = yuyv[1] - 128;
            v = yuyv[3] - 128;

            r = (y + (359 * v)) >> 8;
            g = (y - (88 * u) - (183 * v)) >> 8;
            b = (y + (454 * u)) >> 8;

            *(bgr++) = (b > 255) ? 255 : ((b < 0) ? 0 : b);
            *(bgr++) = (g > 255) ? 255 : ((g < 0) ? 0 : g);
            *(bgr++) = (r > 255) ? 255 : ((r < 0) ? 0 : r);

            if (z++) {
                z = 0;
                yuyv += 4;
            }
        }
    }
}

//read one frame from memory and throws the data to standard output
static int read_frame(int * fd, int width, int height, int * n_buffers, struct buffer * buffers, int pixel_format)
{
    struct v4l2_buffer buf; //needed for memory mapping

    CLEAR(buf);

    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;

    if (-1 == xioctl(*fd, VIDIOC_DQBUF, &buf)) {
        switch (errno) {
        case EAGAIN:
            return 0;

        case EIO: //EIO ignored

        default:
            errno_exit("VIDIOC_DQBUF");
        }
    }

    assert(buf.index < *n_buffers);

    //writing to standard output
    Conv_YUYV2BGR(buffers[buf.index].start, width, height);

    if (-1 == xioctl(*fd, VIDIOC_QBUF, &buf)) {
        errno_exit("VIDIOC_QBUF");
    }

    return 1;
}

//just the main loop of this program
static void mainloop(int * fd, int width, int height, int * n_buffers, struct buffer * buffers, int pixel_format)
{
    fd_set fds;
    struct timeval tv;
    int r;

    FD_ZERO(&fds);
    FD_SET(*fd, &fds);

    /* Select Timeout */
    tv.tv_sec = 2;
    tv.tv_usec = 0;

    //the classic select function, who allows to wait up to 2 seconds,
    //until we have captured data,
    r = select(*fd + 1, &fds, NULL, NULL, &tv);

    if (-1 == r) {
        errno_exit("select");
    }

    if (0 == r) {
        fprintf(stderr, "select timeout\n");
        exit(EXIT_FAILURE);
    }

    //read one frame from the device and put on the buffer
    read_frame(fd, width, height, n_buffers, buffers, pixel_format);
}

//free the shared memory area
static void uninit_device(int * n_buffers, struct buffer * buffers)
{
    unsigned int i;

    for (i = 0; i < *n_buffers; ++i)
        if (-1 == munmap(buffers[i].start, buffers[i].length))
            errno_exit("munmap");
    free(buffers);
}

static struct buffer *init_mmap(int * fd, char * dev_name, int * n_buffers)
{
    struct v4l2_requestbuffers req;
    //buffers is an array of n_buffers length, and every element store a frame
    struct buffer *buffers = NULL;
    CLEAR(req);

    req.count = 8;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;

    if (-1 == xioctl(*fd, VIDIOC_REQBUFS, &req)) {
        if (EINVAL == errno) {
            fprintf(stderr, "%s does not support "
                    "memory mapping\n", dev_name);
            exit(EXIT_FAILURE);
        }
        else {
            errno_exit("VIDIOC_REQBUFS");
        }
    }

    if (req.count < 2) {
        fprintf(stderr, "Insufficient buffer memory on %s\n", dev_name);
        exit(EXIT_FAILURE);
    }
    buffers = calloc(req.count, sizeof(*buffers));
    if (!buffers) {
        fprintf(stderr, "Out of memory\n");
        exit(EXIT_FAILURE);
    }
    //map every element of the array buffers to the shared memory
    for (*n_buffers = 0; *n_buffers < req.count; ++*n_buffers) {
        struct v4l2_buffer buf;

        CLEAR(buf);

        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = *n_buffers;

        if (-1 == xioctl(*fd, VIDIOC_QUERYBUF, &buf))
            errno_exit("VIDIOC_QUERYBUF");

        buffers[*n_buffers].length = buf.length;
        buffers[*n_buffers].start = mmap(NULL /* start anywhere */, buf.length, PROT_READ | PROT_WRITE /* required */,
                MAP_SHARED /* recommended */, *fd, buf.m.offset);

        if (MAP_FAILED == buffers[*n_buffers].start)
            errno_exit("mmap");
    }
    return buffers;
}

//configure and initialize the hardware device
static struct buffer *init_device(int * fd, char * dev_name, int width, int height, int * n_buffers, int pixel_format)
{
    struct v4l2_capability cap;
    struct v4l2_format fmt;
    struct buffer * buffers = NULL;

    if (-1 == xioctl(*fd, VIDIOC_QUERYCAP, &cap)) {
        if (EINVAL == errno) {
            fprintf(stderr, "%s is no V4L2 device\n", dev_name);
            exit(EXIT_FAILURE);
        }
        else {
            errno_exit("VIDIOC_QUERYCAP");
        }
    }

    if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
        fprintf(stderr, "%s is no video capture device\n", dev_name);
        exit(EXIT_FAILURE);
    }

    if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
        fprintf(stderr, "%s does not support streaming i/o\n", dev_name);
        exit(EXIT_FAILURE);
    }

    CLEAR(fmt);

    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = width;
    fmt.fmt.pix.height = height;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    fmt.fmt.pix.field = V4L2_FIELD_ANY;

    if (-1 == xioctl(*fd, VIDIOC_S_FMT, &fmt))
        errno_exit("\nError: pixel format not supported\n");

    unsigned int min = fmt.fmt.pix.width * 2;
    if (fmt.fmt.pix.bytesperline < min)
        fmt.fmt.pix.bytesperline = min;
    min = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height;
    if (fmt.fmt.pix.sizeimage < min)
        fmt.fmt.pix.sizeimage = min;

    fprintf(stderr, "Video bytespreline = %d\n", fmt.fmt.pix.bytesperline);

    buffers = init_mmap(fd, dev_name, n_buffers);

    return buffers;
}

static void close_device(int * fd)
{
    if (-1 == close(*fd))
        errno_exit("close");

    *fd = -1;
}

static void open_device(int * fd, char * dev_name)
{
    *fd = open(dev_name, O_RDWR /* required */| O_NONBLOCK, 0);
    if (-1 == *fd) {
        fprintf(stderr, "Cannot open '%s': %d, %s\n", dev_name, errno, strerror(errno));
        exit(EXIT_FAILURE);
    }
}

static void stop_capturing(int * fd)
{
    enum v4l2_buf_type type;

    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    //this call to xioctl allows to stop the stream from the capture device
    if (-1 == xioctl(*fd, VIDIOC_STREAMOFF, &type))
        errno_exit("VIDIOC_STREAMOFF");
}

static void start_capturing(int * fd, int * n_buffers)
{
    unsigned int i;
    enum v4l2_buf_type type;

    for (i = 0; i < *n_buffers; ++i) {
        struct v4l2_buffer buf;

        CLEAR(buf);

        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;

        if (-1 == xioctl(*fd, VIDIOC_QBUF, &buf))
            errno_exit("VIDIOC_QBUF");
    }

    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    //start the capture from the device
    if (-1 == xioctl(*fd, VIDIOC_STREAMON, &type))
        errno_exit("VIDIOC_STREAMON");
}

#endif /* MYCAP_H_ */

0 件のコメント:

コメントを投稿