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

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

QML から OpenCV で取得したカメラ画を表示してみた

Qt C++ OpenCV

修正(2013/09/16)

※ タイトルが内容に即していないため変更しました(旧: Qt Quick 2 で使える OpenCV 用の QML 要素を作ってみた)。
内容に即した記事は次回で書いています。

はじめに

OpenCV 始めました。C++ インターフェース使うと C++11 と併せてサクサク処理できてとても楽しいです。ただ、単体ではどうしても弱い UI 周りをどうしようかなと思い、広く使われている openFrameworks を使おうかとも考えたのですが、もっと気軽に扱いたく思い Qt Quick 2QML から使えるようにする方法を調べてみました。

概要

QQmlImageProvider を利用します。

4.x 系の時は QtDeclarativeImageProvider と呼ばれていたもの(らしい)です。これは QML から非同期スレッドでビットマップを扱うことの出来るインターフェースを提供してくれます。

Image {
	source: "image:/(任意の ImageProvider 名)/(識別子 e.g. hoge.png)"
}

つまり、QQmlImageProvider を拡張したクラスを作り、これを QML のエンジンに登録すれば、OpenCV に限らず OpenGL 等も含めて QML から任意のビットマップを扱うことが出来るようになるわけです。

詳細

まず、QQmlImageProvider を継承したクラスを作成します。継承後のクラスの宣言は以下のようになります。

#ifndef OPENCV_VIDEO_H
#define OPENCV_VIDEO_H

#include <QQuickImageProvider>
#include <QPixmap>
#include <opencv2/opencv.hpp>

class VideoImageProvider : public QQuickImageProvider
{
public:
    VideoImageProvider();
    ~VideoImageProvider();

    QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize);

private:
   CvCapture* capture_;
};

#endif // OPENCV_VIDEO_H

virtual なメンバ関数 requestPixmap はビットマップの要求時に呼ばれます。引数の id は先述した識別子、requestedSize は QML の Image 要素にて指定されたサイズ(だと思うのですが常に -1 がやってきて不明です)、size は元の画像サイズ(Image 要素でサイズ指定がないときに使用)となっています。
これを元に実装してみます。

#include "video_image.h"

VideoImageProvider::VideoImageProvider()
    : QQuickImageProvider(QQuickImageProvider::Pixmap)
{
    capture_ = cvCaptureFromCAM(0);
}

VideoImageProvider::~VideoImageProvider()
{
    cvReleaseCapture(&capture_);
}

QPixmap VideoImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize)
{
    // get camera input
    cv::Mat img = cvQueryFrame(capture_);

    // resize
    *size = QSize(img.cols, img.rows);
    int width  = requestedSize.width()  > 0 ? requestedSize.width()  : img.rows;
    int height = requestedSize.height() > 0 ? requestedSize.height() : img.cols;
    cv::Mat resized_img(width, height, img.type());
    cv::resize(img, resized_img, resized_img.size(), cv::INTER_CUBIC);

    // BGR -> ARGB
    cv::cvtColor(resized_img, resized_img, CV_BGR2BGRA);
    std::vector<cv::Mat> bgra;
    cv::split(resized_img, bgra);
    std::swap(bgra[0], bgra[3]);
    std::swap(bgra[1], bgra[2]);

    QImage video_img(resized_img.data, resized_img.cols, resized_img.rows, QImage::Format_ARGB32);
    return QPixmap::fromImage(video_img);
}

OpenCV で得られた画像データ cv::Mat を BGR から ARGB へ変換(BGR から ARGB へ直接変換できないので BGRA 変換後に並び替えしてます)、QImage を通じて QPixmap にして返しています。特に id は使わず無視していますがカメラの番号をあてるとかすると良いと思います。

そしてこれを main.cpp から読み取ります。

#include <QtGui/QGuiApplication>
#include <QQmlEngine>
#include "qtquick2applicationviewer.h"
#include "video_image.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QtQuick2ApplicationViewer viewer;
    QQmlEngine* engine = viewer.engine();
    engine->addImageProvider(QLatin1String("videoCapture"), new VideoImageProvider);
    viewer.setMainQmlFile(QStringLiteral("qml/OpenCV_QML_Integration/main.qml"));
    viewer.showExpanded();

    return app.exec();
}

ここで QQuickView から QQmlEngine を取得していることに注意して下さい。公式のサンプルコードは誤りです*1

そして、これを利用したコードを QML で書きます。

import QtQuick 2.0

Rectangle {
    width: image.width
    height: image.height
    Image {
        id: image
        source: "image://videoCapture/hoge"
    }
}

最後にリンクの設定を .pro ファイルに追加します。

LIBS += -lopencv_core -lopencv_highgui -lopencv_imgproc

さきほど、videoCapture という名前で Image Provider を定義したので、その名前を通じてアクセスします。ID は使わないので適当に hoge とかつけています。実行すると以下のようになります。

無事表示されました!

動画として表示する

例えば以下のように Timer を回せば動画っぽい表示になります。

import QtQuick 2.0

Rectangle {
    width: image.width
    height: image.height
    Image {
        id: image
        source: "image://videoCapture/hoge"
    }
    Timer {
        property int cnt: 0
        interval: 32
        running: true
        repeat: true
        onTriggered:  {
            if (image.status === Image.Ready) {
                image.source = "image://videoCapture/" + cnt;
                ++cnt;
            }
        }
    }
}

更新頻度(Timerinterval)を上げ過ぎると、白フレームが挟まってしまいました。どこかで処理落ちしているようです。。

特徴量を重畳してみる

SURF で特徴量を載っけた画像を出すとかも出来ます。QML の利点である UI の作りやすさの例も含めて、下記を追記してみました。

// Gray-scale Image
cv::Mat gray_img;
cv::cvtColor(resized_img, gray_img, CV_BGR2GRAY);

// SURF
std::vector<cv::KeyPoint> keypoints;
cv::SurfFeatureDetector detector(4500);
cv::Scalar color(100, 255, 50);
detector.detect(gray_img, keypoints);
for (const auto& point : keypoints) {
	cv::circle(resized_img, point.pt, 1, color, -1);
	cv::circle(resized_img, point.pt, point.size, color, 1, CV_AA);
}
import QtQuick 2.0

Rectangle {
    width: 640
    height: 360
    Image {
        id: image
        anchors.fill: parent
        source: "image://videoCapture/hoge"
    }
    Timer {
        property int cnt: 0
        interval: 32
        running: true
        repeat: true
        onTriggered:  {
            if (image.status === Image.Ready) {
                image.source = "image://videoCapture/" + cnt;
                ++cnt;
            }
        }
    }
    Rectangle {
        width: parent.width
        height: parent.height/5
        anchors.bottom: parent.bottom
        color: '#aa000000'

        Text {
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.verticalCenter: parent.verticalCenter
            font.pointSize: 24
            color: '#fff'
            text: "<b>SURF</b> による特徴量検出"
        }
    }
}
LIBS += -lopencv_core -lopencv_highgui -lopencv_imgproc -lopencv_features2d


おわりに

コアのアルゴリズムでは C++ で注力し、周辺部の UI 系はこういったスクリプト言語に任せてサクサク作る手法はとても大事だと思います。

参考

*1:qmlRegisterType みたいに勝手にやってくれるのかと思っていて、結構悩みました。