Qtの基礎 - 画像処理

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

Qtにおける画像処理は、主にQImageクラスを中心に行われる。
このクラスは、画像データの読み込み、保存、操作などの基本的な機能を提供している。

その他、Qtでは画像を取り扱うクラスとして、QBitmapクラス、QPixmapクラスもあり、それぞれ使い分けが必要である。

画像の読み込みと保存は、QImageクラスのloadメソッドとsaveメソッドを使用する。
これにより、JPEG、PNG、GIF等の様々な形式の画像ファイルを扱うことができる。

ピクセル単位の操作も可能であり、個々のピクセルの色情報を取得・変更することができる。
これは、画像の細かい編集や分析に役立つ。

基本的な画像変換機能も充実しており、
例えば、画像のサイズ変更、回転、反転等を簡単に行うことができる。
これらの操作は、QImageクラスのメソッドを使用するだけで実現できる。

より高度な処理が必要な場合、QPainterクラスを使用してQImageクラス上に直接描画することができる。
これにより、画像に図形やテキストを追加、あるいは、複雑な視覚効果を適用したりすることが可能である。

また、QImageReaderクラスを使用することにより、画像の読み込み時にフィルタを適用することもできる。
これは、画像の前処理や特定の効果を適用する場合に便利である。

しかし、非常に複雑な画像処理や高度な機能が必要な場合は、OpenCV等の専門的な外部ライブラリとQtを組み合わせて使用することも一般的である。
これにより、Qtの使いやすいGUI機能とOpenCVの強力な画像処理アルゴリズムを組み合わせることができる。

Qtでの画像処理は、これらの機能を組み合わせることにより、基本的な画像編集から複雑な画像分析まで幅広いタスクに対応できる。


QImageクラス / QPixmapクラス / QBitmapクラス

QImageクラス、QBitmapクラス、QPixmapクラスの3つのクラスは、Qtで画像を扱う時に使用される重要なクラスである。
それぞれのクラスは異なる用途と特徴を持つ。

QImageクラス

QImageクラスは、ピクセルデータの直接操作に適している。
主な特徴として、デバイスに依存しない画像表現を提供しており、個々のピクセルへのアクセスや操作が容易である。

画像処理、フィルタリング、色変換等の操作に最適である。
また、様々な色深度と形式 (1-bit、8-bit、32-bit等) をサポートしている。

QImageクラスは、画像ファイルの読み込み、保存、画像データの詳細な操作が必要な場合に使用する。

QPixmapクラス

QPixmapクラスは、画面表示に最適化された画像表現を提供している。
このクラスは、ハードウェアアクセラレーションを利用できるため、GUIアプリケーションでの画像表示に非常に効率的である。

QPixmapクラスは、デバイスに依存する形式で画像を保持するため、画面への描画が高速である。
ただし、個々のピクセルへのアクセスはQImageクラスほど容易ではない。

QPixmapクラスは、主にボタンのアイコンやウインドウの背景等、UIコンポーネントでの画像表示に使用する。

QBitmapクラス

QBitmapクラスは、QPixmapクラスの特殊なケースであり、1ビット深度 (モノクロ) の画像を表現している。
各ピクセルは、オン / オフの2つの状態のみを持つ。

QBitmapクラスは、主にマスク、カーソル、アイコン等の単純な画像要素の作成に使用する。

メモリ使用量が少なく、単純な形状やパターンの表現に適している。

クラスの使い分け

  • QImageクラス
    画像処理や詳細な編集が必要な場合に使用する。
  • QPixmapクラス
    GUIでの高速な画像表示が必要な場合に使用する。
  • QBitmapクラス
    モノクロの単純な画像やマスクが必要な場合に使用する。


また、これらのクラス間では相互変換が可能である。
例えば、QImageクラスで画像を処理した後、QPixmapクラスに変換して画面に表示するといった使い方ができる。

これらのクラスを使い分けることにより、効率的で柔軟な画像処理と表示が可能になる。


QImageクラスの使用例

画像の拡大

以下の例では、QImageクラスを使用して、画像を拡大している。

 #include <QImage>
 #include <QSize>
 #include <QFileInfo>
 
 int main()
 {
    // 入力ファイルの存在確認
    if (!QFileInfo::exists("input.jpg")) return -1;
 
    // 画像の読み込み
    QImage image("input.jpg");
    if (image.isNull()) return -1;
 
    // 画像の拡大
    QImage resizedImage = image.scaled(QSize(800, 600), Qt::KeepAspectRatio, Qt::SmoothTransformation);
 
    // 拡大した画像の保存
    if (!resizedImage.save("output.jpg")) return -1;
 
    return 0;
 }


画像の縮小

以下の例では、QImageクラスを使用して、指定された最大サイズ (例: 400x300ピクセル) に収まるように縮小する。
元の画像が既このサイズ以下の場合は、縮小処理は行わない。

 #include <QImage>
 #include <QSize>
 #include <QFileInfo>
 
 QImage shrinkImage(QImage &image, int maxWidth, int maxHeight)
 {
    QSize originalSize = image.size();
    QSize newSize      = originalSize;
 
    // アスペクト比を維持しながら、指定された最大サイズに収まるように縮小
    if (originalSize.width() > maxWidth || originalSize.height() > maxHeight) {
       newSize.scale(maxWidth, maxHeight, Qt::KeepAspectRatio);
    }
 
    // 元のサイズより小さい場合のみ縮小を行う
    // 縮小の必要がない場合は元の画像をそのまま
    if (newSize != originalSize) {
       return image.scaled(newSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
    }
    else {
       return image;
    }
 }
 
 int main()
 {
    // 入力ファイルの存在確認
    if (!QFileInfo::exists("input.jpg")) return -1;
 
    // 画像の読み込み
    QImage image("input.jpg");
    if (image.isNull()) return -1;
 
    // 画像の縮小
    QImage resizedImage = shrinkImage(image, 400, 300)
 
    // 縮小した画像の保存
    if (!resizedImage.save("output.jpg")) return -1;
 
    return 0;
 }


画像の回転

以下の例では、QImageクラスを使用して、画像を回転している。

 #include <QApplication>
 #include <QImage>
 #include <QTransform>
 #include <QFileDialog>
 
 int main(int argc, char *argv[])
 {
    QApplication app(argc, argv);
 
    // 画像を読み込む
    QImage image("input.jpg");
    if (image.isNull()) return -1;
 
    // 回転角度を指定 (例: 90度)
    double angle = 90.0;
 
    // QTransformクラスを使用して画像を回転
    QTransform transform;
    transform.rotate(angle);
    QImage rotatedImage = image.transformed(transform, Qt::SmoothTransformation);
    if (rotatedImage.isNull()) return -1;
 
    // 回転した画像を保存
    if (!rotatedImage.save("output.jpg")) return -1;
 
    return 0;
 }



QPixmapクラスの使用例

画像ビューアウィジェット

以下の例では、画像ビューアウィジェットのクラスを定義している。

 // ImageViewer.hファイル
 
 #include <QWidget>
 #include <QPixmap>
 #include <QLabel>
 #include <QVBoxLayout>
 #include <QPushButton>
 #include <QFileDialog>
 #include <QMessageBox>
 
 class ImageViewer : public QWidget
 {
    Q_OBJECT
 
 private:
    QLabel      *imageLabel;  // 画像表示用ラベル
    QPushButton *loadButton;  // 画像読み込みボタン
 
 public:
    // コンストラクタ: ウィジェットの初期化
    ImageViewer(QWidget *parent = nullptr) : QWidget(parent)
    {
       // 画像を表示するためのQLabel
       imageLabel = new QLabel(this);
       imageLabel->setAlignment(Qt::AlignCenter);
 
       // 画像読み込みボタン
       loadButton = new QPushButton("画像を読み込む", this);
 
       // ボタンクリック時にloadImage()スロットを呼び出す
       connect(loadButton, &QPushButton::clicked, this, &ImageViewer::loadImage);
 
       // レイアウトの設定
       QVBoxLayout *layout = new QVBoxLayout(this);
       layout->addWidget(imageLabel);
       layout->addWidget(loadButton);
 
       setLayout(layout);
    }
 
 private slots:
    // 画像読み込み処理を行うスロット
    void loadImage()
    {
       // ファイル選択ダイアログを表示
       QString fileName = QFileDialog::getOpenFileName(this, "画像を開く", "", "画像ファイル (*.png *.jpg *.bmp)");
       if (!fileName.isEmpty()) {
          // 選択されたファイルからQPixmapを作成
          QPixmap pixmap(fileName);
          if (pixmap.isNull()) {
             // 画像読み込みに失敗した場合はエラーメッセージを表示
             QMessageBox::critical(this, "エラー", "画像の読み込みに失敗");
             return;
          }
 
          // 読み込んだ画像をラベルに表示 (アスペクト比を保持してリサイズ)
          imageLabel->setPixmap(pixmap.scaled(imageLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
       }
    }
 };


アニメーションスプライトシート

以下の例では、スプライトシートを使用したアニメーションクラスを定義している。

 // AnimatedSprite.hファイル
 
 #include <QWidget>
 #include <QPixmap>
 #include <QTimer>
 #include <QPainter>
 
 class AnimatedSprite : public QWidget
 {
    Q_OBJECT
 
 private:
    QPixmap spriteSheet;  // スプライトシート
    QTimer *timer;        // アニメーション用タイマ
    int currentFrame;     // 現在のフレーム番号
    int frameWidth;       // 1フレームの幅
    int frameHeight;      // 1フレームの高さ
  
 protected:
    // ペイントイベント: 現在のフレームを描画
    void paintEvent(QPaintEvent *event) override
    {
       Q_UNUSED(event);
       QPainter painter(this);
       // スプライトシートから現在のフレームを切り出して描画
       painter.drawPixmap(0, 0, spriteSheet, currentFrame * frameWidth, 0, frameWidth, frameHeight);
    }
 
 public:
    // コンストラクタ: スプライトシートの読み込みとタイマの設定
    AnimatedSprite(QWidget *parent = nullptr) : QWidget(parent), currentFrame(0)
    {
       // スプライトシートの読み込み
       spriteSheet.load(":/sprites/character.png");  // スプライトシートのリソースパス
       if (spriteSheet.isNull()) {
          qWarning() << "スプライトシートの読み込みに失敗";
          return;
       }
 
       // スプライトシートの各フレームのサイズを計算
       frameWidth = spriteSheet.width() / 4;  // 4フレームあると仮定
       frameHeight = spriteSheet.height();
 
       // アニメーション用タイマの設定
       timer = new QTimer(this);
       connect(timer, &QTimer::timeout, this, &AnimatedSprite::nextFrame);
       timer->start(100);  // 100[mS]ごとにフレーム更新
 
       // ウィジェットのサイズを1フレームのサイズに固定
       setFixedSize(frameWidth, frameHeight);
    }
 
 private slots:
    // 次のフレームに進むスロット
    void nextFrame()
    {
       currentFrame = (currentFrame + 1) % 4;  // 4フレームを循環
       update();                               // ウィジェットの再描画をリクエスト
    }
 };


非同期で画像を読み込む

以下の例では、大きなサイズの画像を非同期で読み込むウィジェットを定義している。

 // AsyncImageLoader.hファイル
 
 #include <QWidget>
 #include <QPixmap>
 #include <QLabel>
 #include <QPushButton>
 #include <QVBoxLayout>
 #include <QFuture>
 #include <QtConcurrent>
 #include <QFileDialog>
 #include <QMessageBox>
 
 class AsyncImageLoader : public QWidget
 {
    Q_OBJECT
 
 private:
    QLabel *imageLabel;       // 画像表示用ラベル
    QPushButton *loadButton;  // 画像読み込みボタン
  
 public:
    // コンストラクタ: ウィジェットの初期化
    AsyncImageLoader(QWidget *parent = nullptr) : QWidget(parent)
    {
       // 画像表示用ラベル
       imageLabel = new QLabel(this);
       imageLabel->setAlignment(Qt::AlignCenter);
 
       // 画像読み込みボタン
       loadButton = new QPushButton("大きな画像を読み込む", this);
       connect(loadButton, &QPushButton::clicked, this, &AsyncImageLoader::loadImageAsync);
 
       // レイアウトの設定
       QVBoxLayout *layout = new QVBoxLayout(this);
       layout->addWidget(imageLabel);
       layout->addWidget(loadButton);
 
       setLayout(layout);
    }
 
 private slots:
    // 非同期で画像を読み込むスロット
    void loadImageAsync()
    {
       // ファイル選択ダイアログを表示
       QString fileName = QFileDialog::getOpenFileName(this, "大きな画像を開く", "", "画像ファイル (*.png *.jpg *.bmp)");
       if (!fileName.isEmpty()) {
          // UIの更新(ボタンを無効化し、テキストを変更)
          loadButton->setEnabled(false);
          loadButton->setText("読み込み中...");
 
          // 非同期で画像を読み込む
          QFuture<QPixmap> future = QtConcurrent::run([fileName]() {
             return QPixmap(fileName);
          });
 
          // 非同期処理の完了を監視するためのQFutureWatcherを設定
          QFutureWatcher<QPixmap> *watcher = new QFutureWatcher<QPixmap>(this);
          connect(watcher, &QFutureWatcher<QPixmap>::finished, this, [this, watcher]() {
             QPixmap pixmap = watcher->result();
             if (pixmap.isNull()) {
                // 画像読み込みに失敗した場合はエラーメッセージを表示
                QMessageBox::critical(this, "エラー", "画像の読み込みに失敗");
             }
             else {
                // 読み込んだ画像をラベルに表示 (アスペクト比を保持してリサイズ)
                imageLabel->setPixmap(pixmap.scaled(imageLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
             }
 
             // UIの更新 (ボタンを再度有効化して、元のテキストに戻す)
             loadButton->setEnabled(true);
             loadButton->setText("大きな画像を読み込む");
 
             // watcherのメモリを解放
             watcher->deleteLater();
          });
 
          // 非同期処理の監視を開始
          watcher->setFuture(future);
       }
    }
 };



QBitmapクラスの使用例

描画ツール

以下の例では、1ビット深度の描画ツールクラスを定義している。

 // SimpleDrawingTool.hファイル
 
 #include <QWidget>
 #include <QBitmap>
 #include <QPainter>
 #include <QMouseEvent>
 #include <QPushButton>
 #include <QVBoxLayout>
 #include <QLabel>
 #include <QMessageBox>
 
 class SimpleDraingTool : public QWidget
 {
    Q_OBJECT
 
 private:
    QLabel      *drawingLabel;  // 描画エリア用ラベル
    QPushButton *clearButton;   // クリアボタン
    QBitmap     bitmap;         // 描画用ビットマップ
    bool        drawing;        // 描画中フラグ
 
    // 描画用ビットマップを初期化する関数
    void initializeBitmap()
    {
       bitmap = QBitmap(300, 300);
       bitmap.fill(Qt::color0);  // 背景を黒で初期化
       updateLabel();
    }
 
    // 指定された位置に点を描画する関数
    void drawPoint(const QPoint &pos)
    {
       QPainter painter(&bitmap);
       painter.setPen(Qt::color1);  // 白で描画
       painter.drawPoint(pos);
       updateLabel();
    }
 
    // ラベルの表示を更新する関数
    void updateLabel()
    {
       drawingLabel->setPixmap(QPixmap::fromImage(bitmap.toImage()));
    }
 
 public:
    // コンストラクタ: ウィジェットの初期化
    SimpleDraingTool(QWidget *parent = nullptr) : QWidget(parent), drawing(false)
    {
       // 描画エリア用ラベル
       drawingLabel = new QLabel(this);
       drawingLabel->setAlignment(Qt::AlignCenter);
 
       // クリアボタン
       clearButton = new QPushButton("クリア", this);
       connect(clearButton, &QPushButton::clicked, this, &SimpleDraingTool::clearDrawing);
 
       // レイアウトの設定
       QVBoxLayout *layout = new QVBoxLayout(this);
       layout->addWidget(drawingLabel);
       layout->addWidget(clearButton);
       setLayout(layout);
 
       // 描画用ビットマップの初期化
       initializeBitmap();
    }
 
 protected:
    // マウスボタンが押下された時のイベントハンドラ
    void mousePressEvent(QMouseEvent *event) override
    {
       if (event->button() == Qt::LeftButton) {
          drawing = true;
          drawPoint(event->pos());
       }
    }
 
    // マウスが移動した時のイベントハンドラ
    void mouseMoveEvent(QMouseEvent *event) override
    {
       if ((event->buttons() & Qt::LeftButton) && drawing) {
          drawPoint(event->pos());
       }
    }
 
    // マウスボタンが離された時のイベントハンドラ
    void mouseReleaseEvent(QMouseEvent *event) override
    {
       if (event->button() == Qt::LeftButton && drawing) {
          drawing = false;
       }
    }
 
 private slots:
    // 描画をクリアするスロット
    void clearDrawing()
    {
       try {
          initializeBitmap();
          updateLabel();
       }
       catch (const std::exception& e) {
          QMessageBox::critical(this, "エラー", QString("描画のクリアに失敗: %1").arg(e.what()));
       }
    }
 };


カスタムカーソル作成器

以下の例では、カスタムカーソルを作成および適用するウィジェットクラスを定義している。

 // CustomCursorCreator.hファイル
 
 #include <QWidget>
 #include <QBitmap>
 #include <QPainter>
 #include <QPushButton>
 #include <QVBoxLayout>
 #include <QCursor>
 #include <QMessageBox>
 
 class CustomCursorCreator : public QWidget
 {
    Q_OBJECT
 
 private:
    QPushButton *createButton;  // カーソル作成ボタン
 
 public:
    // コンストラクタ: ウィジェットの初期化
    CustomCursorCreator(QWidget *parent = nullptr) : QWidget(parent)
    {
       // カーソル作成ボタン
       createButton = new QPushButton("カスタムカーソルを作成", this);
       connect(createButton, &QPushButton::clicked, this, &CustomCursorCreator::createAndApplyCursor);
 
       // レイアウトの設定
       QVBoxLayout *layout = new QVBoxLayout(this);
       layout->addWidget(createButton);
       setLayout(layout);
    }
 
 private slots:
    // カスタムカーソルを作成・適用するスロット
    void createAndApplyCursor()
    {
       try {
          // 32x32ピクセルのビットマップを作成
          QBitmap bitmap(32, 32);
          bitmap.fill(Qt::color0);  // 透明で初期化
 
          // ビットマップ上に描画
          QPainter painter(&bitmap);
          painter.setPen(Qt::color1);  // 不透明な色で描画
 
          // 簡単な十字型のカーソルを描画
          painter.drawLine(16, 0, 16, 31);  // 縦線
          painter.drawLine(0, 16, 31, 16);  // 横線
 
          // マスクを作成 (カーソルの形状を定義)
          QBitmap mask = bitmap;
 
          // カーソルを作成・適用
          QCursor cursor(bitmap, mask, 16, 16);  // ホットスポットを中心に設定
          this->setCursor(cursor);
 
          QMessageBox::information(this, "成功", "カスタムカーソルを作成・適用");
       }
       catch (const std::exception& e) {
          QMessageBox::critical(this, "エラー", QString("カーソルの作成に失敗: %1").arg(e.what()));
       }
    }
 };


モノクロパターン生成器

以下の例では、ランダムなモノクロパターンを生成し表示するウィジェットクラスを定義している。

 // MonochromePatternGenerator.hファイル
 
 #include <QWidget>
 #include <QBitmap>
 #include <QPainter>
 #include <QPushButton>
 #include <QVBoxLayout>
 #include <QLabel>
 #include <QRandomGenerator>
 #include <QMessageBox>
 
 class MonochromePatternGenerator : public QWidget
 {
    Q_OBJECT
 
 private:
    QLabel      *patternLabel;    // パターン表示用ラベル
    QPushButton *generateButton;  // パターン生成ボタン
 
 public:
    // コンストラクタ: ウィジェットの初期化を行います
    MonochromePatternGenerator(QWidget *parent = nullptr) : QWidget(parent)
    {
       // パターン表示用ラベル
       patternLabel = new QLabel(this);
       patternLabel->setAlignment(Qt::AlignCenter);
 
       // パターン生成ボタン
       generateButton = new QPushButton("新しいパターンを生成", this);
       connect(generateButton, &QPushButton::clicked, this, &MonochromePatternGenerator::generatePattern);
 
       // レイアウトの設定
       QVBoxLayout *layout = new QVBoxLayout(this);
       layout->addWidget(patternLabel);
       layout->addWidget(generateButton);
       setLayout(layout);
 
       // 初期パターンを生成
       generatePattern();
    }
 
 private slots:
    // ランダムなモノクロパターンを生成するスロット
    void generatePattern()
    {
       try {
          // 100x100ピクセルのビットマップを作成
          QBitmap bitmap(100, 100);
          bitmap.fill(Qt::color0);     // 黒で初期化
 
          QPainter painter(&bitmap);
          painter.setPen(Qt::color1);  // 白で描画
 
          // ランダムなパターンを生成
          for (int y = 0; y < 100; y++) {
             for (int x = 0; x < 100; x++) {
                if (QRandomGenerator::global()->generateDouble() > 0.5) {
                   painter.drawPoint(x, y);
                }
             }
          }
 
          // パターンをラベルに表示
          patternLabel->setPixmap(QPixmap::fromImage(bitmap.toImage().scaled(200, 200, Qt::KeepAspectRatio, Qt::FastTransformation)));
       }
       catch (const std::exception& e) {
          QMessageBox::critical(this, "エラー", QString("パターンの生成に失敗: %1").arg(e.what()));
       }
    }
 };



QGraphicsViewクラスの使用

画像を表示する機能を持っているウィジェットは無いため、ビュークラスを継承して派生クラスを作成する。
(元々、QGraphicsViewクラスは、QGraphicsSceneクラスを表示するためのものである)

ビュークラスを継承した派生クラスには、元の画像のデータを保持するメンバ変数を定義する。

 #pragma once
 
 #include <QtGui>
 
 class MyGraphicsView : public QGraphicsView
 {
    Q_OBJECT
 
    public:
       MyGraphicsView(QWidget *pWnd);
       ~MyGraphicsView(void);
 
       void setImg(QImage &img);
 
    private:
       void paintEvent(QPaintEvent *event);
       QImage m_img;
 };


QGraphicsViewクラスのpaintEventメソッドは、再描画時に呼び出される仮想関数である。
paintEventメソッドをオーバーライドして描画部分を記述する。

paintEventメソッドとsetImgメソッドの定義は、以下の通りである。

以下の例では、paintEventメソッドを使用してQImageクラスのインスタンス(描画用)を生成して、QPainter::drawImageメソッドで描画している。
描画用の画像は、ウィジェットのサイズに合わせてリサイズしている。

ウィジェットをウインドウやダイアログに貼り付けて、setImgメソッドを使用して画像データを送ることで描画される。

 void MyGraphicsView::paintEvent(QPaintEvent *event)
 {
    QPainter widgetpainter(viewport());
    widgetpainter.setWorldTransform(m_matrix);
 
    QImage qimg = m_img.scaled(viewport()->width(), viewport()->height(), Qt::KeepAspectRatio,Qt::FastTransformation);
    widgetpainter.drawImage(0, 0, qimg);
 }
 
 void MyGraphicsView::setImg(QImage &img)
 {
    m_img = QImage(img);
    viewport()->update();
 }


OpenCVライブラリのcv::Matメソッドを使用する場合、OpenCVで画像を処理してその画像を表示するには、以下のように記述する。
カラー画像の場合は、色配列をRGBからBGRに変換する必要がある。

 void MyGraphicsView::paintEvent(QPaintEvent *event)
 {
    QPainter widgetpainter(viewport());
    widgetpainter.setWorldTransform(m_matrix);
 
    // メンバ変数m_imgはcv::Matクラスのインスタンス
    QImage qimg = QImage(m_img.ptr(), m_img.cols, m_img.rows, m_img.step, QImage::Format_RGB888);
    qimg = qimg.scaled(viewport()->width(), viewport()->height(), Qt::KeepAspectRatio,Qt::FastTransformation);
    widgetpainter.drawImage(0, 0, qimg);
 }
 
 // 読み込み関数
 void MainWindow::onOpen()
 {
    QString strFName = QFileDialog::getOpenFileName(this, "Select image",  "C:\\", "Image File(*.*)");
    if(strFName.size() == 0)
    {
       return;
    }
 
    cv::Mat img = cv::imread(strFName.toStdString(), CV_LOAD_IMAGE_ANYCOLOR | CV_LOAD_IMAGE_ANYDEPTH);
 
    // RGBからBGRに変換する
    cv::cvtColor(img, img, CV_RGB2BGR);
 
    // graphicsViewはMyGraphicsViewクラスのメンバ変数
    graphicsView->setImg(img);
 }


画像の拡大・縮小・移動

画像を表示する機能には、拡大・縮小、移動等の制御が必要になる場合が多い。
これらの機能を実装するには、QTransformクラスを使用する。

まず、QTransformクラスのメンバ変数を2つ宣言する。
QTransformクラスのデフォルトコンストラクタを呼び出す場合、単位行列で初期化される。

 QTransform m_matrix;
 QTransform m_matrix_inv;


次に、QGraphicsViewクラスのpaintEventメソッドをオーバーライドする。
以下のように記述することで、QTransformクラスの情報が反映されるようになる。

 void MyGraphicsView::paintEvent(QPaintEvent *event)
 {
    QPainter widgetpainter(viewport());
    widgetpainter.setWorldTransform(m_matrix);
 
    QImage qimg = QImage(m_img.ptr(), m_img.cols, m_img.rows, m_img.step, QImage::Format_RGB888);
    qimg = qimg.scaled(viewport()->width(), viewport()->height(), Qt::KeepAspectRatio,Qt::FastTransformation);
    widgetpainter.drawImage(0, 0, qimg);
 }


拡大・縮小の機能は、以下の通りである。

以下の例では、拡大・縮小の限界値を設定して、変換行列に拡大率を設定する。
既に拡大・縮小が適用されている場合のために、予め、ズームの中心を移動している。

第1引数には、拡大する場合は正の値、縮小する場合は負の値を指定する。
第2引数には、拡大・縮小の中心となる点を指定する。

 void MyGraphicsView::scaleView(qreal factor, QPointF center)
 {
    factor /= 5;  // -0.1 <-> 0.1
    factor += 1;  // 0.9 <-> 1.1
 
    // limit zoom out ---
    if(m_matrix.m11() == 1 && factor < 1)
    {
       return;
    }
 
    if(m_matrix.m11() * factor < 1)
    {
       factor = 1 / m_matrix.m11();
    }
 
    // limit zoom in ---
    if(m_matrix.m11() > 100 && factor > 1)
    {
       return;
    }
 
    // inverse the transform
    int a = 0,
        b = 0;
 
    m_matrix_inv.map(center.x(), center.y(), &a, &b);
 
    m_matrix.translate(a - factor * a, b - factor * b);
    m_matrix.scale(factor, factor);
 
    controlImagePosition();
 }


controlImagePositionメソッドは、拡大する時、余白が表示されないように画像位置を調整する。
controlImagePositionメソッドの最後で、viewport()->update();を実行して画面を更新する。

 void MyGraphicsView::controlImagePosition()
 {
    qreal left, top, right, bottom;
 
    // after check top-left, bottom right corner to avoid getting "out" during zoom/panning
    m_matrix.map(0, 0, &left, &top);
 
    if(left > 0)
    {
       m_matrix.translate(-left, 0);
       left = 0;
    }
 
    if(top > 0)
    {
       m_matrix.translate(0, -top);
       top = 0;
    }
 
    QSize sizeImage = size();
    m_matrix.map(sizeImage.width(), sizeImage.height(), &right, &bottom);
 
    if(right < sizeImage.width())
    {
       m_matrix.translate(sizeImage.width() - right, 0);
       right = sizeImage.width();
    }
 
    if(bottom < sizeImage.height())
    {
       m_matrix.translate(0, sizeImage.height() - bottom);
       bottom = sizeImage.height();
    }
 
    m_matrix_inv = m_matrix.inverted();
 
    viewport()->update();
 }


以下の例では、移動はマウスの左ボタンを押してドラッグする場合に適用している。
また、拡大している場合にのみ適用している。

 void MyGraphicsView::mouseMoveEvent(QMouseEvent *event)
 {
    QPoint pnt = event->pos();
 
    if(m_matrix.m11() > 1 && event->buttons() == Qt::LeftButton)
    {
       QPointF pntf = (pnt - m_pntDownPos) / m_matrix.m11();
       m_pntDownPos = event->pos();
       m_matrix.translate(pntf.x(), pntf.y());
       controlImagePosition();
       viewport()->update();
    }
 
    viewport()->update();
 
    QWidget::mouseMoveEvent(event);
 }
 
 void MyGraphicsView::mousePressEvent(QMouseEvent *event)
 {
    m_pntDownPos = event->pos();
 
    QWidget::mousePressEvent(event);
 }


画像のクリック位置の座標を逆算する

画像上において、クリックする位置の座標を取得して、ピクセル値を取得する操作がある。

画像がウインドウの一辺に合わせて全体が表示されている場合は縦横比のみを考慮すればよいが、
拡大・縮小・移動している場合、座標を取得することは困難である。

Qtでは、拡大・縮小・移動の情報は変換行列が持っているため、それを元に逆算することで簡単に座標を取得できる。

以下の例では、画像をクリックする時に座標を渡してシグナルを発行している。
その後、ウィジェットの親ウィンドウ側でシグナルを受信して、該当ピクセルに対する処理を実行する。

 class MyGraphicsView : public QGraphicsView
 {
    protected:
       void mousePressEvent(QMouseEvent *event)
 
    // 画像の座標を送信するシグナルを定義する
    Q_SIGNALS:
       void mousePressed(QPoint p);
 
       // ...略
 }


 void MyGraphicsView::mousePressEvent(QMouseEvent *event)
 {
    // ウィンドウサイズと画像サイズの比を計算する
    double dScale = (double)viewport()->width() / (double)m_pimg->width;
 
    if(dScale > ((double)viewport()->height() / (double)m_pimg->height))
    {
       dScale = (double)viewport()->height() / (double)m_pimg->height;
    }
 
    // 画像座標を計算してシグナルを発行
    QPointF p = m_matrix_inv.map(event->pos());
 
    emit mousePressed(QPoint(p.x() / dScale, p.y() / dScale));
 }


拡大・縮小時の画像の解像度を上げる

上記のセクション群では、画像を拡大・縮小する場合、画像の解像度はビューポートのサイズのままである。
また、描画および再描画ごとに表示用の画像を作成しているため非効率である。

そこで、拡大・縮小するごとに表示用の画像の解像度を上げるようにする。

 void MyGraphicsView::scaleView( qreal factor, QPointF center )
 {
    factor /= 5;  // -0.1 <-> 0.1
    factor+=1;  // 0.9 <-> 1.1
 
    // limit zoom out ---
    if(m_matrix.m11() == 1 && factor < 1)
    {
       return;
    }
 
    if(m_matrix.m11() * factor < 1)
    {
       factor = 1 / m_matrix.m11();
    }
 
    // limit zoom in ---
    if(m_matrix.m11() > 100 && factor > 1)
    {
       return;
    }
 
    // inverse the transform
    int a = 0,
        b = 0;
 
    m_matrix_inv.map(center.x(), center.y(), &a, &b);
 
    m_matrix.translate(a-factor*a,b-factor*b);
    m_matrix.scale(factor,factor);
 
    // ここで表示用画像を作成、上記コードのOpenCVの場合
    m_qimg = QImage( m_img.ptr(), m_img.cols, m_img.rows, m_img.step, QImage::Format_RGB888 );
    if(viewport()->width() < m_qimg.width())
    {
       m_qimg = m_qimg.scaled(viewport()->width() * m_matrix.m11(), viewport()->height() * m_matrix.m11(),
                              Qt::KeepAspectRatio,Qt::FastTransformation);
    }
 
    controlImagePosition();
 }


 void MyGraphicsView::controlImagePosition()
 {
    qreal left, top, right, bottom;
    qreal limRight, limBottom;
 
    // after check top-left, bottom right corner to avoid getting "out" during zoom/panning
    m_matrix.map(0, 0, &left, &top);
 
    if(left > 0)
    {
       m_matrix.translate(-left, 0);
       left = 0;
    }
 
    if(top > 0)
    {
       m_matrix.translate(0, -top);
       top = 0;
    }
 
    QSize sizeImage = size() * (qreal)m_matrix.m11();
    m_matrix.map(sizeImage.width(), sizeImage.height(), &right, &bottom);
 
    limRight = sizeImage.width() / m_matrix.m11();
    limBottom = sizeImage.height() / m_matrix.m11()
 
    if(right < limRight)
    {
       m_matrix.translate(limRight - right, 0);
       right = limRight;
    }
 
    if(bottom < limBottom)
    {
       m_matrix.translate(0, limBottom - bottom);
       bottom = limBottom;
    }
 
    m_matrix_inv = m_matrix.inverted();
 
    viewport()->update();
 }


 void MyGraphicsView::mousePressEvent(QMouseEvent *event)
 {
    // ウィンドウサイズ・画像サイズの比を計算
    double dScale = (double)viewport()->width() / (double)m_pimg->width;
    if(dScale > ( (double)viewport()->height() / (double)m_pimg->height))
    {
       dScale = (double)viewport()->height() / (double)m_pimg->height;
    }
 
    dScale *= m_matrix.m11()
 
    // 画像座標を計算してシグナルを発行
    pnt = m_matrix_inv.map(event->pos());
    QPoint pntI = pnt / dScale;
 
    emit mousePressed(pntI);
 }