Привет, хабр!
В этой статье я расскажу о работе с веб-камерой из Qt5 под Windows (но пример также должен работать под Linux и Mac OS X с установленным плагином gstreamer).

Если интересно, как сделать вот такое приложение и преодолеть возникающие при этом проблемы, то прошу под кат.
Однажды мне захотелось добавить в свою скриншотилку (которая, в принципе, не совсем и скриншотилка) поддержку веб-камеры. Так как в то время я использовал Qt4, то стал искать готовые решения для этой версии, но потом EuroElessar мне подсказал, что в Qt5 есть класс QCamera, который как раз подходил под мои задачи.
Было принято решение перейти на Qt5, которая была все еще в состоянии альфы (да и сейчас только предбета).
Первые проблемы начались еще на этапе компиляции. Из-за кривых скриптов/гайдов у меня никак не хотел компилироваться qtwebkit, из-за чего я потерял один вечер, но зато потом весь фреймворк был скомпилирован в виде debug и release версии.
Дальше — интереснее.
Зайдя в примеры для QtMultimedia и найдя там директорию camera, я решил запустить и посмотреть как оно работает.
Тут меня ждала вторая проблема:

Очевидно, что кутям не хватает какого-то плагина. Чтобы его найти, я полез в QtMultimedia\src\plugins. Там мой взгляд первым делом пал на gstreamer, но довольно быстро я понял, что под винду его не откомпилить.
Затем я там же нашел недописанный directshow.
Откомпилировав этот плагин и положив его в QtBase\plugins\mediaservice, я успешно запустил пример из QtMultimedia, который показал список камер и даже пытался вывести изображение, но у него это получалось плохо и полосато:

Плюнув на это, я стал писать свой код, надеясь, что у меня этой проблемы не будет. И ее действительно не оказалось, зато была другая: разрешение изображений было всегда 320x240. Полистав немного код directshow плагина, я решил пойти спать, чтобы разобраться с этим завтра. Следующий день опять не принес никаких результатов с directshow, зато я полностью дописал код в своем приложении. Поэтому оставалось только одно — добить этот directshow.
На следующий день я нашел решение, которое, как обычно бывает в таких ситуациях, оказалось довольно простым и очевидным. В коде нигде не вызывалась функция updateProperties(), которая получала информацию о поддерживаемых разрешениях, а также в самом конструкторе класса были жестко прописаны размеры 320x240. Исправив эту функцию и добавив ее вызов, я стал получать изображение максимально возможного разрешения.
Теперь переходим непосредственно к коду.
Так как пример небольшой и служит лишь для демонстрации, то все слоты я описал в конструкторе.
Для начала набросаем в дизайнере две небольшие формы.
webcam.ui — собственно, главное окошко:

webcamselect.ui — служит для выбора веб-камеры, если их установлено несколько:

Здесь я просто приведу код заголовочного файла, потому что комментировать тут нечего.
Как можно заметить из webcam.h, у нас в классе присутствует статический член с именем m_defaultDevice, который мы и определим до конструктора:
В самом конструкторе функцией QCamera::availableDevices() получим список камер, а затем проверим есть ли в этом списке наша m_defaultDevice. В зависимости от этого у нас будет два дальнейших пути:
1) Если устройство оказалось в списке, то просто пропускаем этот шаг.
2) Если его там не оказалось, то необходимо вывести диалог с выбором:

Однако, если веб-камер нет, то надо просто выйти с ошибкой, а если она всего одна, то выбрать ее.
Но если веб-ка��ер несколько, то в цикле создадим кнопочки для каждой веб-камеры и покажем диалог:
Здесь очень удобно использовать новый синтаксис сигнал-слотов, чтобы не размазывать код по всему классу, что я и сделал.
После выбора пользователя программа либо выйдет (он нажал на крестик), либо в m_defaultDevice будет id нашего устройства.
При создании этих объектов никаких проблем возникнуть не должно, поэтому мы просто передаем в конструктор QCamera id камеры и соединяем ее со слотами ошибки и смены состояния:
QCameraViewfinder — это такой объект, который позволяет выводить изображение с веб-камеры сразу на виджет (мы ведь хотим, чтобы пользователь не вслепую себя фотографировал?).
Создаем, устанавливаем минимальный размер (иначе наш виджет невозможно будет уменьшить) и соединяем с объектом камеры:
(Параметр QCamera::CaptureStillImage необходим для того, чтобы можно было захватывать изображения.)
Создадим новую метку, которая будет рисоваться поверх изображения и вести отсчет, и переменную шаблона для нее:
и наложим ее на viewfinder:
Дальше объявим таймер, который будет запускаться при отсчете и его слот:
Как видно из кода, если время еще есть, то просто уменьшаем его на секунду, а если оно кончилось, то фотографируем, отключая таймер и скрывая счетчик.
Так как код у всех них достаточно простой, то приводить его здесь я не буду, но скажу пару слов про QClipboard:
В текущей версии Qt он работает довольно странно: может не записать изображение в буфер (случается редко), либо, пока будет доставать его оттуда, испортить его. Надеюсь, к релизу это поправят.
Включаем камеру и создаем объект QCameraImageCapture, который должен поддерживать запись в буфер (QCameraImageCapture::CaptureToBuffer), но пишет все равно в файл.
Слот imageSaved() почти дублирует imageCaptured(), поэтому в статье я опишу только его.
Открываем файл, в который камера поместила изображение, и считываем из него картинку, которую затем отзеркаливаем и помещаем в m_pixmap, а затем, растягивая или сжимая по размеру, в QLabel picture. Удаляем файл, чтобы не мусорить.
Функция захвата, как и все остальные функции, не отличается большей сложностью и состоит из 3-х значимых строк:
Во-первых, фокусируем и блокируем камеру. Блокировку необходимо делать для того, чтобы другое приложение не стало изменять настроенные нами параметры для выполнения снимка.
Во-вторых, делаем снимок в файл, переданный в качестве параметра.
В-третьих, разблокируем камеру.
Остальные функции интереса не представляют и, я думаю, комментировать их смысла нет.
Несмотря на то, что Qt5 находится все еще в состоянии даже не беты, такими вещами, как веб-камера, уже можно пользоваться, правда с некоторыми оговорками и решаемыми проблемами.
Исходники приложения можно взять здесь.
Надеюсь, эта статья кому-нибудь поможет.
(Так как это моя первая статья, то обо всех опечатках и ошибках оформления прошу сообщать в личку.)
В этой статье я расскажу о работе с веб-камерой из Qt5 под Windows (но пример также должен работать под Linux и Mac OS X с установленным плагином gstreamer).

Если интересно, как сделать вот такое приложение и преодолеть возникающие при этом проблемы, то прошу под кат.
Предыстория
Однажды мне захотелось добавить в свою скриншотилку (которая, в принципе, не совсем и скриншотилка) поддержку веб-камеры. Так как в то время я использовал Qt4, то стал искать готовые решения для этой версии, но потом EuroElessar мне подсказал, что в Qt5 есть класс QCamera, который как раз подходил под мои задачи.
Было принято решение перейти на Qt5, которая была все еще в состоянии альфы (да и сейчас только предбета).
Первые проблемы
Первые проблемы начались еще на этапе компиляции. Из-за кривых скриптов/гайдов у меня никак не хотел компилироваться qtwebkit, из-за чего я потерял один вечер, но зато потом весь фреймворк был скомпилирован в виде debug и release версии.
Дальше — интереснее.
Зайдя в примеры для QtMultimedia и найдя там директорию camera, я решил запустить и посмотреть как оно работает.
Тут меня ждала вторая проблема:

Очевидно, что кутям не хватает какого-то плагина. Чтобы его найти, я полез в QtMultimedia\src\plugins. Там мой взгляд первым делом пал на gstreamer, но довольно быстро я понял, что под винду его не откомпилить.
Затем я там же нашел недописанный directshow.
Direct Show
Откомпилировав этот плагин и положив его в QtBase\plugins\mediaservice, я успешно запустил пример из QtMultimedia, который показал список камер и даже пытался вывести изображение, но у него это получалось плохо и полосато:

Плюнув на это, я стал писать свой код, надеясь, что у меня этой проблемы не будет. И ее действительно не оказалось, зато была другая: разрешение изображений было всегда 320x240. Полистав немного код directshow плагина, я решил пойти спать, чтобы разобраться с этим завтра. Следующий день опять не принес никаких результатов с directshow, зато я полностью дописал код в своем приложении. Поэтому оставалось только одно — добить этот directshow.
На следующий день я нашел решение, которое, как обычно бывает в таких ситуациях, оказалось довольно простым и очевидным. В коде нигде не вызывалась функция updateProperties(), которая получала информацию о поддерживаемых разрешениях, а также в самом конструкторе класса были жестко прописаны размеры 320x240. Исправив эту функцию и добавив ее вызов, я стал получать изображение максимально возможного разрешения.
Исправление
1) Открываем файл QtMultimedia\src\plugins\directshow\camera\dscamerasession.cpp.
2) В функции DSCameraSession::setDevice(… ) в самом конце в конец блока if добавляем updateProperties();.
3) Функцию updateProperties() заменяем на эту:
2) В функции DSCameraSession::setDevice(… ) в самом конце в конец блока if добавляем 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
Так как пример небольшой и служит лишь для демонстрации, то все слоты я описал в конструкторе.
Рисуем формочки
Для начала набросаем в дизайнере две небольшие формы.
webcam.ui — собственно, главное окошко:

webcamselect.ui — служит для выбора веб-камеры, если их установлено несколько:

Заголовочный файл
Здесь я просто приведу код заголовочного файла, потому что комментировать тут нечего.
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. В зависимости от этого у нас будет два дальнейших пути:
1) Если устройство оказалось в списке, то просто пропускаем этот шаг.
2) Если его там не оказалось, то необходимо вывести диалог с выбором:

Однако, если веб-камер нет, то надо просто выйти с ошибкой, а если она всего одна, то выбрать ее.
Но если веб-ка��ер несколько, то в цикле создадим кнопочки для каждой веб-камеры и покажем диалог:
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
При создании этих объектов никаких проблем возникнуть не должно, поэтому мы просто передаем в конструктор QCamera id камеры и соединяем ее со слотами ошибки и смены состояния:
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 — это такой объект, который позволяет выводить изображение с веб-камеры сразу на виджет (мы ведь хотим, чтобы пользователь не вслепую себя фотографировал?).
Создаем, устанавливаем минимальный размер (иначе наш виджет невозможно будет уменьшить) и соединяем с объектом камеры:
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>";
и наложим ее на viewfinder:
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(); } } );
Как видно из кода, если время еще есть, то просто уменьшаем его на секунду, а если оно кончилось, то фотографируем, отключая таймер и скрывая счетчик.
Слоты кнопок управления
Так как код у всех них достаточно простой, то приводить его здесь я не буду, но скажу пару слов про 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 picture. Удаляем файл, чтобы не мусорить.
Функция захвата
Функция захвата, как и все остальные функции, не отличается большей сложностью и состоит из 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 находится все еще в состоянии даже не беты, такими вещами, как веб-камера, уже можно пользоваться, правда с некоторыми оговорками и решаемыми проблемами.
Исходники приложения можно взять здесь.
Надеюсь, эта статья кому-нибудь поможет.
(Так как это моя первая статья, то обо всех опечатках и ошибках оформления прошу сообщать в личку.)
