2011/05/11

Qtはじめました

【動機】

最近、C++ で効率的に GUI を構築する手段を探している。

ちなみに僕はそれまで、C/C++ で GUI を構築する場合には Windows API を直接叩いていた。『猫でもわかる』に洗脳された身としては、このスタイルこそが最もスタンダードな選択肢だと信じていたためである。

しかしコレ、他言語(というか Java)と比較して生産性が圧倒的に悪い。

RAD ツールの出来不出来以前の問題として、そもそも API の仕様が洗練されていないのだ。長い Windows の歴史の中で建て増しを繰り返すうちにすっかりカオスになってしまったらしい。

そして残念ながら、これをがんばって身につけるメリットを、僕はあまり見いだす事ができなかった。

ロジックの本質とは今ひとつ関係のないソースコードばかりが肥大化して、保守性や拡張性が著しく低下するのはどう考えても残念だし、そもそも人様が書いた GUI 関連のコードなど読む気すらしない。

一方、C/C++ を使用する動機は、既存のソフトウェア資源が圧倒的に豊富だからという理由だけで充分である(ラッパを使う事で、他言語から C++ で書かれたライブラリを活用する事ももちろん可能だが、使用できるライブラリのバージョンに制約があったり、不整合が起きやすかったり、そもそもネイティブライブラリに加えてラッパの仕様を調べなきゃいけなくて二度手間だったりと色々面倒なのだ)。

このあたりのせめぎ合いをうまく調停できないだろうかと思っていたところ Qt なるものを見つけたので、早速試してみる事にした。

Qt は Windows / Mac / Linux で使える C++ 向け GUI ツールキットで、Wikipedia によると
QtはGTK+やMFC等、他の標準的なグラフィックツールキットに比べて、もっとも後発であることもあり、以前から存在するライブラリーのよいところを集めたアーキテクチャーとなっている。
だそうだ。なるほど。

さらに Qt SDK には Qt Creator なる統合開発環境が付属しており、グラフィカルにコンポーネントを配置できるナイスな RAD ツール(Qt Designer)も使える。

これを使って、まずは動的に変化するダイアログボックスを作ってみた。
「More」ボタンをクリックすると、ダイアログボックスがグレードアップし、さらなる設定項目が現れる。
こんな無駄に手の込んだギミックが容易に実装できるとは面白い。



【OpenCV と合体】

今日はもうひとつ、OpenCV の初歩的なサンプルでおなじみのプログラムを、Qt Creator 上でビルド & 実行するために色々やったので、覚え書き。

#include <cv.h>
#include <cxcore.h>
#include <highgui.h>

int main(int argc, char *argv[])
{
    const char *windowName = "Test Picture";
    IplImage *img = cvLoadImage("XXX.jpg");

    cvNamedWindow(windowName, CV_WINDOW_AUTOSIZE);

    cvShowImage(windowName,img);
    cvWaitKey(0);

    cvReleaseImage(&img);
    cvDestroyWindow(windowName);

    return 0;
}

これで、フロントエンド構築に Qt を使い、バックエンドで OpenCV の高度な画像処理ルーチンを走らせるといった芸当が可能になる。



まずは前提として、僕の場合 OpenCV をインストールしたフォルダの構成を示す。

もしも僕の環境と異なる場合はこのあとの設定に関する記述を適宜読み変えてほしい。
  • Cドライブ
    • OpenCV2.0
      • bin
        • cv200.dll
        • cv200d.dll
        • cxcore200.dll
        • cxcore200d.dll
        • highgui200.dll
        • highgui200d.dll
        •  
        • (以下略)

      • include
        • opencv
          • cv.h
          • cxcore.h
          • highgui.h

          • (以下略)

      • lib
        • Debug
          • cv200d.lib
          • cxcore200d.lib
          • highgui200d.lib

          • (以下略)

        • Release
          • cv200.lib
          • cxcore200.lib
          • highgui200.lib

          • (以下略)


        • (以下略)
        •  
      •  
      • (以下略)
      •  

    • (以下略)


OpenCV 2.0 をインストールした時点で、C:\OpenCV2.0\bin へのパスは通っているが、ヘッダファイルへのパスと静的ライブラリへのパスをリンカに設定しないと、OpenCV を用いたプロジェクトはビルドできない。

XXXXXXXX.pro をテキストエディタで開いて、こんな感じに書き換える。

#-------------------------------------------------
#
# Project created by QtCreator 2011-05-11T14:27:28
#
#-------------------------------------------------

QT       += core gui

TARGET = XXXXXXXX
TEMPLATE = app


SOURCES += main.cpp\
        mainwindow.cpp

HEADERS  += mainwindow.h

INCLUDEPATH += C:/OpenCV2.0/include/opencv
DEPENDPATH += C:/OpenCV2.0/include/opencv

win32:CONFIG(release, debug|release): LIBS += -LC:/OpenCV2.0/lib/release/ -lcv200
else:win32:CONFIG(debug, debug|release): LIBS += -LC:/OpenCV2.0/lib/debug/ -lcv200d

win32:CONFIG(release, debug|release): LIBS += -LC:/OpenCV2.0/lib/release/ -lcxcore200
else:win32:CONFIG(debug, debug|release): LIBS += -LC:/OpenCV2.0/lib/debug/ -lcxcore200d

win32:CONFIG(release, debug|release): LIBS += -LC:/OpenCV2.0/lib/release/ -lhighgui200
else:win32:CONFIG(debug, debug|release): LIBS += -LC:/OpenCV2.0/lib/debug/ -lhighgui200d

僕が書き足したのは、 INCLUDEPATH で始まる以降すべてである。

.pro ファイルを更新したら qmake する。

これで、プロジェクトを正常にビルドできるようになる。

結果はこんな感じ。
ここでは Qt によるウィンドウではなく、OpenCV の HighGUI を使用した。でもせっかくだから、Qt 製のウィンドウに組み込んでみたいと思うよね。



【もっと合体】

そんなわけで今度は、OpenCV の HighGUI ではなく Qt で作った GUI のウィンドウに、OpenCV の関数を使って読み込んだ画像を表示しようと奮闘。

練習のため、ちょっとしたイメージビューアを作ってみた。

こんなの。

メニューから「Open」をクリックすると、こんなファイルダイアログが現れて、

選んだファイルを表示する、というもの。

これ、一度 IplImage に読みだしたものを QImage に変換している。

あくまで画像データは IplImage 構造体で保持するため、OpenCV での加工が容易になるのだ。



まず、プロジェクトを作り、 Qt Designer フォームクラスを追加する。

使用するフォームテンプレートには Main Window を選ぶ(べつに何でもいいんだろうけど)。

ちなみに、クラス名は MainWindow、ヘッダファイル名は mainwindow.h、ソースファイル名は mainwindow.cpp とする。

次に Qt Designer で以下のように File > Open メニュー(open_action)を作り、『スロットへ移動』ダイアログを開く。

シグナルは triggered() を選択。

すると、mainwindow.cpp に以下のメソッドが追加される。

void MainWindow::on_action_Open_triggered()

ここに、 Open メニューが選択された際の挙動を記述していけばよい。



僕が書いた MainWindow.h および MainWindow.cpp は以下の通り。 IplImageQImage に変換して即座に解放しているが、画像処理を組み込む場合は private なメンバ変数として保持しておく方がよいかも知れない。

mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QImage>

#include <cv.h>

namespace Ui {
    class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void on_action_Open_triggered();

private:
    Ui::MainWindow *ui;
    QImage* qImage;

    QImage* IplImage2QImage(const IplImage *iplImg);
    void paintEvent(QPaintEvent *);
};

#endif // MAINWINDOW_H

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

#include <highgui.h>
#include <QFileDialog>
#include <QPainter>


MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    qImage = NULL;
}

MainWindow::~MainWindow()
{
    if(qImage != NULL) delete qImage;
    delete ui;
}

// ファイルから読み込むよ!
void MainWindow::on_action_Open_triggered()
{
    QString fileName =
        QFileDialog::getOpenFileName
            (this, tr("Open Image"), ".",
             tr("JPEG (*.jpg *.jpeg)\n"
                "PNG (*.png)\n"
                "BMP (*.bmp)"));

    if(!fileName.isEmpty()) {
        IplImage *img =
            cvLoadImage(fileName.toLocal8Bit());

        if(img != NULL) {
            QImage *tmp = IplImage2QImage(img);
            if(qImage != NULL) {
                delete qImage;

            }
            qImage = tmp;
            cvReleaseImage(&img);
            update();
        }
    }
}

// 画像を表示するよ!
void MainWindow::paintEvent(QPaintEvent *) {
    if(qImage != NULL) {
        QPainter widgetPainter(this);
        widgetPainter.drawImage(0, 0, *qImage);
    }
}

// IplImage から QImage に変換するよ!
QImage* MainWindow::IplImage2QImage(const IplImage *iplImg)
{
    int h = iplImg->height;
    int w = iplImg->width;
    int channels = iplImg->nChannels;
    QImage *qimg = new QImage(w, h, QImage::Format_ARGB32);
    char *data = iplImg->imageData;

    for (int y = 0; y < h; y++, data += iplImg->widthStep)
    {
        for (int x = 0; x < w; x++)
        {
            char r, g, b, a = 0;
            if (channels == 1)
            {
                r = data[x * channels];
                g = data[x * channels];
                b = data[x * channels];
            }
            else if (channels == 3 || channels == 4)
            {
                r = data[x * channels + 2];
                g = data[x * channels + 1];
                b = data[x * channels];
            }

            if (channels == 4)
            {
                a = data[x * channels + 3];
                qimg->setPixel(x, y, qRgba(r, g, b, a));
            }
            else
            {
                qimg->setPixel(x, y, qRgb(r, g, b));
            }
        }
    }
    return qimg;
}

今日は色々試したのだけど、まだまだ分からない事が多い上にうまくまとめられなかった。

どんまい……。



追伸: ちょっと旧い情報だけど、OpenCV 2.2 から、Qt がサポートされるようになったらしい。
  • highgui:
    • GSoC 2010 プロジェクトの成果として(完成させたのは Yannick Verdie),OpenCVにQtバックエンドが追加されました.(後略)
残念ながら、僕は諸々のしがらみによって OpenCV を不用意にアップデートできないため、しばらくこのままがんばるしかないのだけど。

0 件のコメント:

コメントを投稿

ひとことどうぞφ(・ω・,,)