QCameraを介したWebカメラ画像のキャプチャ

こんにちは、Habr!



この記事では、WindowsでQt5のWebカメラを操作する方法について説明します(ただし、この例は、gstreamerプラグインがインストールされているLinuxおよびMac OS Xでも動作するはずです)。







そのようなアプリケーションを作成し、同時に発生する問題を克服する方法に興味がある場合は、猫をお願いします。



背景



スクリーンショットにWebカメラのサポートを追加したかった(これは、原則として、スクリーンショットではありません)。 当時Qt4を使用していたので、このバージョンの既製のソリューションを探し始めましたが、 EuroElessarはQt5に自分のタスクに合ったQCameraクラスがあると言っていました。

Qt5に切り替えることが決定されましたが、Qt5はまだアルファ状態でした(現在はプレベータのみ)。



最初の問題



最初の問題はコンパイル段階で始まりました。 スクリプト/ガイドが曲がっていたため、qtwebkitはコンパイルしたくなかったので、ある夜、失われましたが、フレームワーク全体がデバッグおよびリリースバージョンとしてコンパイルされました。



もっと面白い。

QtMultimediaの例に進み、そこでカメラのディレクトリを見つけた後、私は起動して、それがどのように機能するかを見ることにしました。

次に、2番目の問題が待っていました。





明らかに、ある種のプラグインが欠落しています。 それを見つけるために、 QtMultimedia \ src \ pluginsに登りました。 そこで最初にgstreamerに目を向けましたが、Windowsでコンパイルできないことにすぐに気付きました。



それから、未完成のダイレクトショーを見つけました。



ダイレクトショー



このプラグインをコンパイルしてQtBase \ plugins \ mediaserviceに入れたQtMultimediaからサンプルを実行し、カメラのリストを表示し、画像を表示しようとしましたが、ひどく縞模様になってしまいました。





これにつまずいて、私はこの問題が起こらないことを期待して、コードを書き始めました。 実際には存在しませんでしたが、違いがありました。画像の解像度は常に320x240でした。 プラグインのdirectshowコードを少し読んだ後、明日それを理解するためにスリープ状態に入ることにしました。 翌日もdirectshowで結果が得られませんでしたが、アプリケーションのコードは完全に完成しました。 そのため、このダイレクトショーを終了するということだけが残っていました。



翌日、私はそのような状況で通常そうであるように、非常にシンプルで明白であることが判明した解決策を見つけました。 コードでは、 updateProperties()関数が呼び出されることはなく、サポートされるアクセス許可に関する情報を受け取り、サイズ320x240もクラスコンストラクター自体にハードコーディングされていました。 この関数を修正して呼び出しを追加すると、可能な限り最高の解像度の画像を受け取り始めました。



訂正
1)ファイルQtMultimedia \ src \ plugins \ directshow \ camera \ dscamerasession.cppを開きます。

2) 最後の関数DSCameraSession :: setDevice(...)で、 updateProperties()を追加します。

3) updateProperties( )関数は次のように置き換えられます。

void DSCameraSession::updateProperties() { HRESULT hr; AM_MEDIA_TYPE *pmt = NULL; VIDEOINFOHEADER *pvi = NULL; VIDEO_STREAM_CONFIG_CAPS scc; IAMStreamConfig* pConfig = 0; hr = pBuild->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video,pCap, IID_IAMStreamConfig, (void**)&pConfig); if (FAILED(hr)) { qWarning()<<"failed to get config on capture device"; return; } int iCount; int iSize; hr = pConfig->GetNumberOfCapabilities(&iCount, &iSize); if (FAILED(hr)) { qWarning()<<"failed to get capabilities"; return; } QList<QSize> sizes; QVideoFrame::PixelFormat f = QVideoFrame::Format_Invalid; types.clear(); resolutions.clear(); for (int iIndex = 0; iIndex < iCount; iIndex++) { hr = pConfig->GetStreamCaps(iIndex, &pmt, reinterpret_cast<BYTE*>(&scc)); if (hr == S_OK) { pvi = (VIDEOINFOHEADER*)pmt->pbFormat; if ((pmt->majortype == MEDIATYPE_Video) && (pmt->formattype == FORMAT_VideoInfo)) { // Add types if (pmt->subtype == MEDIASUBTYPE_RGB24) { if (!types.contains(QVideoFrame::Format_RGB24)) { types.append(QVideoFrame::Format_RGB24); f = QVideoFrame::Format_RGB24; } } else if (pmt->subtype == MEDIASUBTYPE_RGB32) { if (!types.contains(QVideoFrame::Format_RGB32)) { types.append(QVideoFrame::Format_RGB32); f = QVideoFrame::Format_RGB32; } } else if (pmt->subtype == MEDIASUBTYPE_YUY2) { if (!types.contains(QVideoFrame::Format_YUYV)) { types.append(QVideoFrame::Format_YUYV); f = QVideoFrame::Format_YUYV; } } else if (pmt->subtype == MEDIASUBTYPE_MJPG) { } else if (pmt->subtype == MEDIASUBTYPE_I420) { if (!types.contains(QVideoFrame::Format_YUV420P)) { types.append(QVideoFrame::Format_YUV420P); f = QVideoFrame::Format_YUV420P; } } else if (pmt->subtype == MEDIASUBTYPE_RGB555) { if (!types.contains(QVideoFrame::Format_RGB555)) { types.append(QVideoFrame::Format_RGB555); f = QVideoFrame::Format_RGB555; } } else if (pmt->subtype == MEDIASUBTYPE_YVU9) { } else if (pmt->subtype == MEDIASUBTYPE_UYVY) { if (!types.contains(QVideoFrame::Format_UYVY)) { types.append(QVideoFrame::Format_UYVY); f = QVideoFrame::Format_UYVY; } } else { qWarning() << "UNKNOWN FORMAT: " << pmt->subtype.Data1; } // Add resolutions QSize res(pvi->bmiHeader.biWidth, pvi->bmiHeader.biHeight); if (!resolutions.contains(f)) { sizes.clear(); resolutions.insert(f,sizes); } resolutions[f].append(res); if ( m_windowSize.width() < res.width() && m_windowSize.height() < res.height() ) m_windowSize = res; } } } pConfig->Release(); pixelF = QVideoFrame::Format_RGB24; actualFormat = QVideoSurfaceFormat(m_windowSize,pixelF); }
      
      





次に、コードに直接渡します。



Qt5でWebカメラを操作する



この例は小さく、デモンストレーションのみを目的としているため、コンストラクターですべてのスロットについて説明しました。



金型を描く



はじめに、デザイナーで2つの小さなフォームをスケッチしましょう。



webcam.ui-実際には、メインウィンドウ:





webcamselect.ui-複数のWebカメラがある場合、Webカメラを選択します。





ヘッダーファイル



ここでは、コメントするものがないため、ヘッダーファイルのコードを示します。



webcam.h
 #ifndef WEBCAM_H #define WEBCAM_H #include <QtGui/QClipboard> #include <QtMultimedia/QtMultimedia> #include <QtMultimediaWidgets/QtMultimediaWidgets> #include <QtWidgets/QtWidgets> #include "ui_webcam.h" #include "ui_webcamselect.h" class webCam : public QWidget { Q_OBJECT public: webCam(); ~webCam(); bool nativeEvent( QByteArray ba, void *message, long *result ); public slots: void cameraError( QCamera::Error value ); void cameraStateChanged( QCamera::State state ); void capture( bool checked = false ); protected: void mouseMoveEvent( QMouseEvent* event ); void mousePressEvent( QMouseEvent* event ); void paintEvent( QPaintEvent *event ); void resizeEvent( QResizeEvent *event ); private: Ui::webCamClass ui; Ui::webCamSelectClass select_ui; QPoint m_drag_pos; static QByteArray m_defaultDevice; QDialog *m_selectDialog; QPointer< QCamera > m_camera; QPointer< QCameraImageCapture > m_imageCapture; QPixmap m_pixmap; QTimer *m_timer; int m_timerPaintState; };
      
      







カメラの選択



webcam.hからわかるように、コンストラクターの前に定義するm_defaultDeviceというクラスに静的メンバーがあります。

 QByteArray webCam::m_defaultDevice = QByteArray();
      
      





コンストラクター自体で、QCamera :: availableDevices()関数を使用して 、カメラのリストを取得し、m_defaultDeviceがこのリストにあるかどうかを確認します。 これに応じて、さらに2つの方法があります。

1)デバイスがリストされている場合は、このステップをスキップしてください。

2)存在しなかった場合、選択肢を含むダイアログを表示する必要があります。





ただし、Webカメラがない場合はエラーで終了する必要があり、1つしかない場合はそれを選択します。



ただし、複数のWebカメラがある場合は、サイクルでWebカメラごとにボタンを作成し、ダイアログを表示します。

 foreach( QByteArray webCam, cams ) { auto commandLinkButton = new QCommandLinkButton( QCamera::deviceDescription( webCam ) ); commandLinkButton->setProperty( "webCam", webCam ); connect( commandLinkButton, &QCommandLinkButton::clicked, [=]( bool ) { m_defaultDevice = commandLinkButton->property( "webCam" ).toByteArray(); m_selectDialog->accept(); } ); select_ui.verticalLayout->addWidget( commandLinkButton ); } if ( m_selectDialog->exec() == QDialog::Rejected ) { deleteLater(); return; }
      
      





ここで、 新しいシグナルスロット構文を使用すると、クラス全体でコードが塗りつぶされないように非常に便利です。



ユーザーを選択した後、プログラムは終了する(彼は十字をクリックした)か、m_defaultDeviceでデバイスのIDになります。



QCameraおよびQCameraViewfinderオブジェクトを作成する



これらのオブジェクトを作成するとき、問題は発生しないはずなので、カメラIDをQCameraコンストラクターに渡し、それをエラーおよびステータス変更スロットに接続します。

 m_camera = new QCamera( m_defaultDevice ); connect( m_camera, SIGNAL( error( QCamera::Error ) ), this, SLOT( cameraError( QCamera::Error ) ) ); connect( m_camera, SIGNAL( stateChanged( QCamera::State ) ), this, SLOT ( cameraStateChanged( QCamera::State ) ) );
      
      







QCameraViewfinderは、Webカメラの画像をウィジェットに直接表示できるようにするオブジェクトです(ユーザーが自分で盲目的に写真を撮らないようにしたいですか?)。



作成し、最小サイズを設定します(そうしないと、ウィジェットを縮小できません)、カメラオブジェクトに接続します。

 auto viewfinder = new QCameraViewfinder; viewfinder->setMinimumSize( 50, 50 ); m_camera->setViewfinder( viewfinder ); m_camera->setCaptureMode( QCamera::CaptureStillImage );
      
      





(画像をキャプチャするにはQCamera :: CaptureStillImageパラメータが必要です。)



UIおよびタイマーボタンの設定



画像の上に描画してカウントダウンする新しいラベルと、そのためのテンプレート変数を作成します。

 auto timerLabel = new QLabel; QString timerLabelTpl = "<p align=\"center\"><span style=\"font-size:50pt; font-weight:600; color:#FF0000;\">%1</span></p>";
      
      







それをビューファインダーに置きます:

 ui.gridLayout_3->addWidget( viewfinder, 0, 0 ); ui.gridLayout_3->addWidget( timerLabel, 0, 0 );
      
      





次に、カウントダウンとそのスロットのときに開始するタイマーを宣言します。

 m_timerPaintState = 0; m_timer = new QTimer( this ); m_timer->setInterval( 1000 ); connect( m_timer, &QTimer::timeout, [=]() { m_timerPaintState--; if ( m_timerPaintState ) { timerLabel->setText( timerLabelTpl.arg( QString::number( m_timerPaintState ) ) ); } else { m_timer->stop(); timerLabel->hide(); capture(); } } );
      
      





コードからわかるように、まだ時間があれば、1秒だけ単純に減らし、時間が経過したら写真を撮り、タイマーをオフにしてカウンターを非表示にします。



コントロールボタンスロット



それらすべてのコードは非常に単純なので、ここでは説明しませんが、QClipboardについて少し説明します。

 connect( ui.copyButton, &QPushButton::clicked, [=]( bool ) { QApplication::clipboard()->setImage( m_pixmap.toImage() ); } );
      
      





Qtの現在のバージョンでは、非常に奇妙に動作します。画像をバッファに書き込まない場合があります(まれにしか発生しません)。 このリリースで修正されることを願っています。



画像キャプチャ



 m_camera->start(); m_imageCapture = new QCameraImageCapture( m_camera ); //m_imageCapture->setCaptureDestination( QCameraImageCapture::CaptureToBuffer ); m_imageCapture->setCaptureDestination( QCameraImageCapture::CaptureToFile );
      
      





カメラの電源を入れ、 QCameraImageCaptureオブジェクトを作成します。このオブジェクトは、バッファーへの書き込みをサポートする必要があります( QCameraImageCapture :: CaptureToBuffer )、とにかくファイルに書き込みます。



imageSaved()スロットはimageCaptured()とほぼ重複しているため、記事ではそれについてのみ説明します。



 connect( m_imageCapture, &QCameraImageCapture::imageSaved, [=]( int id, const QString &fileName ) { QFile imageFile( fileName ); if ( imageFile.exists() ) { m_pixmap = QPixmap::fromImage( QImage( fileName ).mirrored( true, false ) ); ui.picture->setPixmap( m_pixmap.scaled( ui.picture->width(), ui.picture->height(), Qt::KeepAspectRatio ) ); imageFile.remove(); } else { QMessageBox::critical( this, "Error", "Image file are not found!" ); deleteLater(); return; } } );
      
      





カメラが画像を配置したファイルを開き、そこから画像を読み取り、それをミラー化してm_pixmapに配置し、サイズを拡大または圧縮してQLabel画像にします。 ごみを残さないようにファイルを削除します。



キャプチャー機能



他のすべての関数と同様に、キャプチャ関数はそれほど複雑ではなく、3つの重要な行で構成されています。



 void webCam::capture( bool ) { m_camera->searchAndLock(); m_imageCapture->capture( QCoreApplication::applicationDirPath() + "/image.jpg" ); m_camera->unlock(); ui.captureButton->setEnabled( true ); ui.timerButton->setEnabled( true ); }
      
      







まず、カメラの焦点を合わせてロックします。 別のアプリケーションが写真を撮るために構成した設定を変更し始めないように、ロックを行う必要があります。

次に、パラメーターとして渡されたファイルのスナップショットを取得します。

第三に、カメラのロックを解除します。



残りの関数は興味の対象ではなく、コメントする意味はないと思います。



おわりに



Qt5はまだベータ版ではないという事実にもかかわらず、いくつかの予約や解決すべき問題はありますが、ウェブカメラのようなものは既に使用できます。



ここでアプリケーションのソースを取得できます

この記事が誰かの役に立つことを願っています。



(これが私の最初の記事なので、すべてのタイプミスと設計エラーについてPMでお知らせください。)



All Articles