Qtでは画像を取り扱うクラスとしてQBitmap、QPixmap、QImageの3つが用意されていて、それぞれ使い分けが必要だそうです。
ここではOpenCVのHighGUIを参考に(というかほぼパクッて)Qtウィジェット上に画像の表示をする方法を示します。
QGraphicsViewの派生クラスを作成
画像を表示する機能を持っているウィジェットクラスはとりあえず無いようなので、何かのビュークラスから自作の派生クラスを作ります。
一番近そうなのはQGraphicsView
のように思われるので、ここではこのクラスの派生クラスを作ります
(本当はQGraphicsView
はQGraphicsScene
オブジェクト表示用らしいのですが、
他に適当なクラスも無さそう)。
作成した派生クラスにはメンバ変数として画像データを保持する画像データオブジェクトをひとつ持つものとします。
このオブジェクトは元の画像データを保持するものとします。
1: #pragma once 2: 3: #include <QtGui> 4: 5: class MyGraphicsView : 6: public QGraphicsView 7: { 8: Q_OBJECT 9: 10: public: 11: MyGraphicsView( QWidget *pWnd ); 12: ~MyGraphicsView(void); 13: 14: void setImg( QImage &img ); 15: 16: private: 17: void paintEvent( QPaintEvent *event ); 18: 29: QImage m_img; 20: }; 21:
paintEvent()
関数は再描画時に呼び出される仮想関数です。
なので、この関数をオーバーライドして描画部分を記述します。
2つの関数の定義は以下のとおりです。
1: 2: void MyGraphicsView::paintEvent( QPaintEvent *event ) 3: { 4: QPainter widgetpainter( viewport() ); 5: widgetpainter.setWorldTransform( m_matrix ); 6: 7: QImage qimg = m_img.scaled( 8: viewport()->width(), 9: viewport()->height(), 10: Qt::KeepAspectRatio,Qt::FastTransformation); 11: widgetpainter.drawImage( 0, 0, qimg ); 12: } 13: 14: 15: void MyGraphicsView::setImg( QImage &img ) 16: { 17: m_img = QImage( img ); 18: viewport()->update(); 19: }
paintEvent()
で描画用QImage
オブジェクトを作成して、
QPainter::drawImage()
関数で描画しています。
この例ではウィジェットのサイズに合わせて描画用画像をリサイズしています。
メンバ変数のQImage
オブジェクトをリサイズしてしまうと元に戻せなくなるので、
これを描画用にしてはいけません。
あとはこのウィジェットをダイアログの一部やメインウィンドウに貼り付けて、
setImg()
関数で画像データを送り込めば描画されるはずです。
OpenCVのcv::Matを使う場合
OpenCVであれこれ画像をいじって、結果を表示したい場合は上記のメンバ変数をcv::Mat
にしたいところです。
この場合は以下のようにします。
1: 2: void MyGraphicsView::paintEvent( QPaintEvent *event ) 3: { 4: QPainter widgetpainter( viewport() ); 5: widgetpainter.setWorldTransform( m_matrix ); 6: 7: // ここでのm_imgはcv::Mat型オブジェクト 8: QImage qimg = QImage( m_img.ptr(), m_img.cols, m_img.rows, m_img.step, QImage::Format_RGB888 ); 9: qimg = qimg.scaled( 10: viewport()->width(), 11: viewport()->height(), 12: Qt::KeepAspectRatio,Qt::FastTransformation); 13: widgetpainter.drawImage( 0, 0, qimg ); 14: }
あと、カラー画像の場合は色配列をRGBからBGRに変換する必要があります。
1: // 読み込み関数 2: void MainWindow::onOpen() 3: { 4: QString strFName = QFileDialog::getOpenFileName( this, "Select image", "C:\", "Image File(*.*)" ); 5: if ( strFName.size() == 0 ) 6: return; 7: 8: cv::Mat img = cv::imread( strFName.toStdString(), CV_LOAD_IMAGE_ANYCOLOR | CV_LOAD_IMAGE_ANYDEPTH ); 9: 10: // RGB => BGR 11: cv::cvtColor( img, img, CV_RGB2BGR ); 12: 13: // graphicsViewはMyGraphicsViewクラスのメンバ変数 14: graphicsView->setImg( img ); 15: 16: }
画面コントロール
画像表示プログラムではズームイン/アウト、移動等の画面コントロールが必要になる場合が多いかと思われます。
これらの機能を実装するのは結構面倒ですが、Qtの場合はQTransform
クラスを使って少し簡単にできます。
以下はOpenCVのhighGUIをほぼまるパチリしています。
まず、QTransform
クラスのメンバ変数を2つ用意します。
QTransform m_matrix; QTransform m_matrix_inv;
QTransform
クラスのデフォルトコンストラクタを呼び出した場合は単位行列で初期化されるので、
特に何もする必要は今のところありません。
次に、paintEvent()
関数に以下のコードを追加します。
1: 2: void MyGraphicsView::paintEvent( QPaintEvent *event ) 3: { 4: QPainter widgetpainter( viewport() ); 5: widgetpainter.setWorldTransform( m_matrix ); 6: 7: QImage qimg = QImage( m_img.ptr(), m_img.cols, m_img.rows, m_img.step, QImage::Format_RGB888 ); 8: qimg = qimg.scaled( 9: viewport()->width(), 10: viewport()->height(), 11: Qt::KeepAspectRatio,Qt::FastTransformation); 12: widgetpainter.drawImage( 0, 0, qimg ); 13: }
こうすることで、QTransform
クラスの情報が反映されるようになるそうです。
あとは、ズームイン・アウト、移動についてこの行列を操作すればよいということになります。
まず、ズームイン/アウトは以下のようにします。
1: 2: void MyGraphicsView::scaleView( qreal factor, QPointF center ) 3: { 4: factor/=5;//-0.1 <-> 0.1 5: factor+=1;//0.9 <-> 1.1 6: 7: //limit zoom out --- 8: if (m_matrix.m11()==1 && factor < 1) 9: return; 10: 11: if (m_matrix.m11()*factor<1) 12: factor = 1/m_matrix.m11(); 13: 14: 15: //limit zoom in --- 16: if (m_matrix.m11()>100 && factor > 1) 17: return; 18: 19: //inverse the transform 20: int a, b; 21: m_matrix_inv.map(center.x(),center.y(),&a,&b); 22: 23: m_matrix.translate(a-factor*a,b-factor*b); 24: m_matrix.scale(factor,factor); 25: 26: controlImagePosition(); 27: }
この関数ではズームイン/アウトのリミットを設定後、変換行列に拡大率を設定しています。
すでに以前のズームが適用された後の場合のために、あらかじめズームの中心を移動するようにしています。
最後のcontrolImagePosition()
関数は以下のとおりです。
1: 2: void MyGraphicsView::controlImagePosition() 3: { 4: qreal left, top, right, bottom; 5: 6: //after check top-left, bottom right corner to avoid getting "out" during zoom/panning 7: m_matrix.map(0,0,&left,&top); 8: 9: if (left > 0) 10: { 11: m_matrix.translate(-left,0); 12: left = 0; 13: } 14: if (top > 0) 15: { 16: m_matrix.translate(0,-top); 17: top = 0; 18: } 19: 20: QSize sizeImage = size(); 21: m_matrix.map(sizeImage.width(),sizeImage.height(),&right,&bottom); 22: if (right < sizeImage.width()) 23: { 24: m_matrix.translate(sizeImage.width()-right,0); 25: right = sizeImage.width(); 26: } 27: if (bottom < sizeImage.height()) 28: { 29: m_matrix.translate(0,sizeImage.height()-bottom); 30: bottom = sizeImage.height(); 31: } 32: 33: m_matrix_inv = m_matrix.inverted(); 34: 35: viewport()->update(); 36: }
この関数は、ズームアウトのときに余白が表示されないように画像位置を調整するものです。
最後にviewport()->update();
で画面を更新しています。
ズームイン/アウトをするには、scaleView()
関数の第一引数にズームインの場合は正の値、
ズームアウトをする場合は負の値を指定し、第2引数にズームイン/アウトの中心となる点を指定します。
移動は、ここではマウスの左ボタンを押してドラッグする場合を示します。
1: 2: void MyGraphicsView::mouseMoveEvent( QMouseEvent *event ) 3: { 4: QPoint pnt = event->pos(); 5: 6: if ( m_matrix.m11() > 1 && event->buttons() == Qt::LeftButton ) 7: { 8: QPointF pntf = ( pnt - m_pntDownPos ) / m_matrix.m11(); 9: m_pntDownPos = event->pos(); 10: m_matrix.translate( pntf.x(), pntf.y() ); 11: controlImagePosition(); 12: viewport()->update(); 13: } 14: 15: viewport()->update(); 16: 17: QWidget::mouseMoveEvent( event ); 18: } 19: 20: 21: void MyGraphicsView::mousePressEvent( QMouseEvent *event ) 22: { 23: m_pntDownPos = event->pos(); 24: 25: QWidget::mousePressEvent( event ); 26: }
移動はが初期表示よりズームインしている場合にのみ適用されるようにしています。
クリック位置の画像座標を逆算するには
画像系のプログラムでよくあるのが、クリックした位置の画像座標を取得してピクセル値を取得したりする操作です。
ウィンドウ座標から画像座標を取得するのは実際はかなり大変です。
画像がウィンドウのどちらか一辺に合わせて全体が表示されている場合は縦横比だけを考慮すればいいですが、
ズームインしていたり、さらにそこから移動していたりした場合はもうわけがわからなくなってしまいます。
Qtで上記のように実装した場合は、ズームや移動の情報は変換行列が持っているので、
それを元に逆算することで大変楽に画像座標を逆算することができます。
ここではクリック時に画像座標を持たせたシグナルを発行するようにしています。
class MyGraphicsView : public QGraphicsView { // 画像座標を送るシグナルを定義しておく Q_SIGNALS: void mousePressed( QPoint p ); //////////////////////////////////////// 1: 2: void MyGraphicsView::mousePressEvent( QMouseEvent *event ) 3: { 4: // ウィンドウサイズ・画像サイズの比を計算 5: double dScale = (double)viewport()->width() / (double)m_pimg->width; 6: if ( dScale > ( (double)viewport()->height() / (double)m_pimg->height ) ) 7: dScale = (double)viewport()->height() / (double)m_pimg->height; 8: 9: // 画像座標を計算してシグナルを発行 10: QPointF p = m_matrix_inv.map( event->pos() ); 11: emit mousePressed( QPoint( p.x() / dScale, p.y() / dScale ) ); 12: 13: }
あとはウィジェットの親ウィンドウ側でシグナルを捕まえて、当該ピクセルに対する何らかの操作をすればよいでしょう。
改良
上記のコードでは拡大しても画像の解像度はビューポートのサイズのままです。
また、描画イベントのたびに表示用画像を作成しているので効率も悪いです。
そこで、拡大するごとに表示用画像も解像度を上げるようにしてみましょう。
まず、paintEvent()
関数内にある表示用画像をメンバ変数(ここではm_qimg
)にして、
拡大・縮小のところで作成するようにします。
拡大・縮小の箇所も少し変えます。
1: 2: void MyGraphicsView::scaleView( qreal factor, QPointF center ) 3: { 4: factor/=5;//-0.1 <-> 0.1 5: factor+=1;//0.9 <-> 1.1 6: 7: //limit zoom out --- 8: if (m_matrix.m11()==1 && factor < 1) 9: return; 10: 11: if (m_matrix.m11()*factor<1) 12: factor = 1/m_matrix.m11(); 13: 14: 15: //limit zoom in --- 16: if (m_matrix.m11()>100 && factor > 1) 17: return; 18: 19: //inverse the transform 20: int a, b; 21: m_matrix_inv.map(center.x(),center.y(),&a,&b); 22: 23: m_matrix.translate(a-factor*a,b-factor*b); 24: m_matrix.scale(factor,factor); 25: 26: // ここで表示用画像を作成、上記コードのOpenCVの場合 27: m_qimg = QImage( m_img.ptr(), m_img.cols, m_img.rows, m_img.step, QImage::Format_RGB888 ); 28: if ( viewport()->width() < m_qimg.width() ) 29: m_qimg = m_qimg.scaled( 30: viewport()->width()*m_matrix.m11(), 31: viewport()->height()*m_matrix.m11(), 32: Qt::KeepAspectRatio,Qt::FastTransformation); 33: 34: controlImagePosition(); 35:
これにともなって、controlImagePosition()
関数も書き換えます。
1: void MyGraphicsView::controlImagePosition() 2: { 3: qreal left, top, right, bottom; 4: qreal limRight, limBottom; 5: 6: //after check top-left, bottom right corner to avoid getting "out" during zoom/panning 7: m_matrix.map(0,0,&left,&top); 8: 9: if (left > 0) 10: { 11: m_matrix.translate(-left,0); 12: left = 0; 13: } 14: if (top > 0) 15: { 16: m_matrix.translate(0,-top); 17: top = 0; 18: } 19: //------- 20: 21: QSize sizeImage = size() * (qreal)m_matrix.m11(); 22: m_matrix.map( sizeImage.width(), sizeImage.height(), &right, &bottom ); 23: 24: limRight = sizeImage.width()/m_matrix.m11(); 25: limBottom = sizeImage.height()/m_matrix.m11() 26: 27: if ( right < limRight ) 28: { 29: m_matrix.translate( limRight-right, 0 ); 30: right = limRight; 31: } 32: if ( bottom < limBottom ) 33: { 34: m_matrix.translate( 0, limBottom-bottom ); 35: bottom = limBottom; 36: } 37: 38: m_matrix_inv = m_matrix.inverted(); 39: 40: viewport()->update(); 41: }
また、画像座標の逆算部分も書き換える必要があります。
1: 2: void MyGraphicsView::mousePressEvent( QMouseEvent *event ) 3: { 4: // ウィンドウサイズ・画像サイズの比を計算 5: double dScale = (double)viewport()->width() / (double)m_pimg->width; 6: if ( dScale > ( (double)viewport()->height() / (double)m_pimg->height ) ) 7: dScale = (double)viewport()->height() / (double)m_pimg->height; 8: 9: dScale *= m_matrix.m11() 10: 11: // 画像座標を計算してシグナルを発行 12: pnt = m_matrix_inv.map( event->pos() ); 13: QPoint pntI = pnt/dScale; 14: 15: emit mousePressed( pntI ); 16: 17: }