QtのGraphics FrameworkでKinectのカラーフレームを表示してみた

はじめに

昔、凹みさんの記事を見て、自分の子供向けに同じようなものを作ってみたいなあと漠然と思っていました。 また、ここ最近はWeb関連のタスクが多く、ちょっと違うこともやってみたいと思い、Kinectの取り扱いやOpenCVを使った画像処理の勉強も兼ねてチャレンジしてみることにしました。

まずはとにかくKinectで取得したフレームデータを表示することからやってみようということで、.NET系の技術を使おうかと思いましたがとにかく新しい刺激を求めているといこともあり、ほとんど使ったことのないQt5とC++で実装してみることにしました。

Kinectからカラーフレームを取得する

Kincetを取り扱うクラスは1つに閉じ込めるようにしています。

ヘッダ

#ifndef KINECT_FRAME_READER_H
#define KINECT_FRAME_READER_H

#include <QObject>
#include <QDebug>
#include <QElapsedTimer>
#include <mutex>
#include <atlbase.h>
#include <Kinect.h>
#include <opencv2/opencv.hpp>

class KinectFrameReader : public QObject
{
    Q_OBJECT
public:
    explicit KinectFrameReader(QObject *parent = nullptr);
    ~KinectFrameReader();
    void initialize();
    void stop();

    const cv::Mat& getImage();

private:
    bool hasError(HRESULT result);

private:
    CComPtr<IKinectSensor> kinect = nullptr; 
    CComPtr<IColorFrameSource> colorFrameSource = nullptr;
    CComPtr<IColorFrameReader> colorFrameReader = nullptr;

    int width, height;
    unsigned int colorBytesPerPixel;

    std::vector<BYTE> colorBuffer;
    cv::Mat image;
    cv::Mat sendImage;

    mutable std::mutex mutex;

    bool isInitialized = false;
    bool isRunning = false;

signals:
    void newFrameArrived() const;

public slots:
    void start();
};

#endif // KINECT_FRAME_READER_H

取得したフレームはOpenCVで画像処理する想定なので、Mat型を介して他クラスに渡せるようにしています。
また、フレームが取得できたことを通知するため、Qtのシグナル、スロットの仕組みを使っています。

ソース

#include "kinect_frame_reader.h"

KinectFrameReader::KinectFrameReader(QObject *parent) : QObject(parent)
{

}

KinectFrameReader::~KinectFrameReader()
{
}


void KinectFrameReader::initialize()
{
    if(hasError(GetDefaultKinectSensor(&kinect))) return;
    if(hasError(kinect->Open())) return;
    if(hasError(kinect->get_ColorFrameSource(&colorFrameSource))) return;
    if(hasError(colorFrameSource->OpenReader(&colorFrameReader))) return;

    CComPtr<IFrameDescription> frameDescription;
    if(hasError(colorFrameSource->CreateFrameDescription(ColorImageFormat_Rgba, &frameDescription))) return;
    if(hasError(frameDescription->get_Width(&width))) return;
    if(hasError(frameDescription->get_Height(&height))) return;
    if(hasError(frameDescription->get_BytesPerPixel(&colorBytesPerPixel))) return;

    colorBuffer.resize(width * height * colorBytesPerPixel);
    
    isInitialized = true;
}

void KinectFrameReader::stop()
{
    isRunning = false;
    kinect->Close();
}

void KinectFrameReader::start()
{
    if(!isInitialized)
    {
        return;
    }

    WAITABLE_HANDLE handle;
    if(hasError(colorFrameReader->SubscribeFrameArrived(&handle)))
    {
        return;
    };

    isRunning = true;

    while(isRunning)
    {
        if(handle)
        {
            switch(WaitForSingleObject(reinterpret_cast<HANDLE>(handle), 100)) {
            case WAIT_OBJECT_0:
                IColorFrameArrivedEventArgs *pArgs = nullptr;

                auto hr = colorFrameReader->GetFrameArrivedEventData(handle, &pArgs);

                if(SUCCEEDED(hr)) {
                    pArgs->Release();
                    CComPtr<IColorFrame> colorFrame;
                    auto ret = colorFrameReader->AcquireLatestFrame(&colorFrame);

                    if(FAILED(ret))
                    {
                        continue;
                    }

                    if(hasError(colorFrame->CopyConvertedFrameDataToArray(colorBuffer.size(), &colorBuffer[0], ColorImageFormat_Rgba))) return;

                    cv::Mat colorImage(height, width, CV_8UC4, &colorBuffer[0]);
                    {
                        std::lock_guard<std::mutex> lock(mutex);
                        image = colorImage;
                        emit newFrameArrived();
                    }
                } else {
                    continue;
                }
            }
        }
    }
}

const cv::Mat& KinectFrameReader::getImage()
{
    std::lock_guard<std::mutex> lock(mutex);
    image.copyTo(sendImage);
    return sendImage;
}

bool KinectFrameReader::hasError(HRESULT result)
{
    if(result != S_OK)
    {
        qDebug() << "failed: " << std::hex << result;
        return true;
    }
    return false;
}

初期化処理、終了処理は定石どおりの実装としました。フレームの取得についてはポーリングモデルではなく、イベントモデルで行うようにしています。SDKのリファレンスに具体的な記述がないのでイマイチこれでよいのか自信が持てないのですが、WAITABLE_HANDLEをIColorFrameReaderのSubscribeFrameArrivedメソッドで紐づけてやると、新しいフレームが取得出来たタイミングでWAITABLE_HANDLEがシグナル状態になるようです。実際タイマーを使って計測してみたところ32msec(30FPS)ごとに、シグナル状態になっていたのでちゃんと動作しているようです。

取得したフレームは今後OpenCVで処理することを想定してMat型で保持するようにしています。詳解OpenCV3によると、Mat型はスマートポインタのような動作をするようで、Mat型の変数に対して=演算子を使うとフレームバッファのコピーではなく参照が渡されるようです。

クラス外からカラーフレームを取得するときはgetImageメソッド経由で取得するようにしています。その際にはフレーム取得部分と排他制御を行いつつ、保持しているMatをディープコピーして渡すようにしました。

QtのGraphics Frameworkで取得したフレームを描画する

QtのGraphics Framework自体はこの本あたりを参考にしました。今回はGraphicsSceneとGraphicsViewはQtで提供されているものをそのまま使い、Kinectで取得したカラーフレームはQGraphicsItemを継承して実装しました。

ヘッダ

#ifndef COLOR_FRAME_GRAPHIC_ITEM_H
#define COLOR_FRAME_GRAPHIC_ITEM_H

#include <QObject>
#include <QGraphicsItem>
#include <QThread>
#include <QPainter>
#include <QDebug>
#include <mutex>
#include <opencv2/opencv.hpp>
#include "kinect_frame_reader.h"

class ColorFrameGraphicItem: public QObject, public QGraphicsItem
{
    Q_OBJECT
    Q_INTERFACES(QGraphicsItem)
public:
    ColorFrameGraphicItem();
    ~ColorFrameGraphicItem();
    QRectF boundingRect() const override;

    void initialize();
    void start();
    void stop();

protected:
    void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override;

private:
    KinectFrameReader frameReader;
    QThread workerThread;

    cv::Mat image;

public slots:
    void fetch();

signals:
    void workerStart();
    void workerStop();
};

#endif // COLOR_FRAME_GRAPHIC_ITEM_H

先ほど説明したKinect部分はループ内でフレームを取得しているので、UIの処理とぶつからないようにQtのQThreadを使い別スレッドで動作させることにしました。シグナル・スロットを使ってスレッド間のやりとりを行いますが、Kinect処理部とのメッセージングはこのクラスだけに閉じ込めようと思ったので、ポインタは必要ないと思い、実体で保持するようにしています。

ソース

#include "color_frame_graphic_item.h"

ColorFrameGraphicItem::ColorFrameGraphicItem(): QObject()
{

}

ColorFrameGraphicItem::~ColorFrameGraphicItem()
{
}


void ColorFrameGraphicItem::initialize()
{
    frameReader.initialize();
}

void ColorFrameGraphicItem::stop()
{
    frameReader.stop();
    workerThread.quit();
    workerThread.wait(3000);
}


void ColorFrameGraphicItem::start()
{
    frameReader.moveToThread(&workerThread);

    QObject::connect(this, SIGNAL(workerStart()), &frameReader, SLOT(start()));
    QObject::connect(&frameReader, SIGNAL(newFrameArrived()), this, SLOT(fetch()));

    workerThread.start();
    emit workerStart();
}

void ColorFrameGraphicItem::fetch()
{
    auto frameImage = frameReader.getImage();
    if(frameImage.empty())
    {
        return;
    }
    {
        frameImage.copyTo(image);
        update();
    }
}

QRectF ColorFrameGraphicItem::boundingRect() const
{
    return QRectF(0, 0, 1960, 1080);
}


void ColorFrameGraphicItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
{
    if(image.empty())
    {
        return;
    }

    QImage img(image.data, image.cols, image.rows, QImage::Format_RGBA8888);
    QRectF rect = boundingRect();

    painter->drawImage(rect, img);
}

initializeメソッドでKinect処理部の初期化が完了した後、startメソッドにて別スレッドのセットアップを行います。ここでシグナル・スロットを使いスレッド間でのメッセージングをセットアップしており、Kinect処理部にて新しいフレームが取得できたときに発せられるシグナル(newFrameArrivedシグナル)をfetchメソッドでハンドルしています。fetchメソッドではKinect処理部が保持しているカラーフレームをディープコピーしてメンバで保持するようにしています。

QGraphicsItemでは、Win32でいうところのWM_PAINTメッセージが送信されたタイミングでpaintメソッドが動作するようです。そこでpaintメソッド内で保持しているカラーフレームを矩形領域に描画するようにしています。ただこれだけだと、ウィンドウのサイズを変更したときなどでしかウィンドウへの描画が行われず連続したカラーフレームの表示にはなりません。そこでKinectからのカラーフレームを取得するfetchメソッド内で、QGraphicsItemのupdateメソッドをコールしています。updateメソッドはイベントループのメッセージキューにWM_PAINT(に相当するQtのメッセージ?)のようなものをキューイングしてくれるものなので、これでフレームが取得できたタイミングと(ほぼ)同時にpaintメソッドが動作するので連続したフレームの描画を行うことができます。

メインウィンドウ

特筆するようなことは何もしていませんが、各クラスのインスタンス化やセットアップはここに集中させた方がいいのかなと思ってます。

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    scene = new QGraphicsScene(this);
    ui->graphicsView->setScene(scene);

    colorItem = new ColorFrameGraphicItem();
    colorItem->initialize();
    scene->addItem(colorItem);
    colorItem->start();
}

MainWindow::~MainWindow()
{
    colorItem->stop();
    delete colorItem;
    delete ui;
}
#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

終わりに

今回はKinect V2を使いましたが、会社からAzure Kinectを借りることができたので、そちらでも試してみようと思います。
また、普段はC#やTypeScriptといった色々な機能をマネージしてくれる処理系を使っているので、C++を使った実装はなかなか新鮮でした。スマートポインタの存在は知っていたのでメモリ管理はだいぶ楽させてもらえそうですが、そもそもポインタを使うのか実体を使うのかなど、これまであまり考える必要がなかった設計上のポイントがあるので刺激的です。

参考

Maker Faire Tokyo 2015 にレゴ x ハードウェア x プロジェクションなシューティングゲーム LITTAI を出展してきた - 凹みTips
KINECT for Windows SDKプログラミング Kinect for Windows v2センサー対応版
Computer Vision with OpenCV 3 and Qt5

Profile
d_yama
元Microsoft MVP for Windows Development(2018-2020)
Sub-category : Windows Mixed Reality
Search