Привет!
В этой статье я расскажу о работе с веб-камерой из Qt5 под Windows (но пример также должен работать под Linux и Mac OS X с установленным плагином gstreamer).
Если интересно, как сделать вот такое приложение и преодолеть возникающие при этом проблемы, то прошу под кат.
Предыстория
Однажды мне захотелось добавить в свою скриншотилку (которая, в принципе, не совсем и скриншотилка) поддержку веб-камеры. Так как в то время я использовал Qt4, то стал искать готовые решения для этой версии, но потом EuroElessar мне подсказал, что в Qt5 есть класс QCamera, который как раз подходил под мои задачи.
Было принято решение перейти на Qt5, которая была все еще в состоянии альфы (да и сейчас только предбета).
Первые проблемы
Первые проблемы начались еще на этапе компиляции. Из-за кривых скриптов/гайдов у меня никак не хотел компилироваться qtwebkit, из-за чего я потерял один вечер, но зато потом весь фреймворк был скомпилирован в виде debug и release версии.
Дальше — интереснее.
Зайдя в примеры для QtMultimedia и найдя там директорию camera, я решил запустить и посмотреть как оно работает.
Тут меня ждала вторая проблема:
Очевидно, что кутям не хватает какого-то плагина. Чтобы его найти, я полез в QtMultimediasrcplugins. Там мой взгляд первым делом пал на gstreamer, но довольно быстро я понял, что под винду его не откомпилить.
Затем я там же нашел недописанный directshow.
Direct Show
Откомпилировав этот плагин и положив его в QtBasepluginsmediaservice, я успешно запустил пример из QtMultimedia, который показал список камер и даже пытался вывести изображение, но у него это получалось плохо и полосато:
Плюнув на это, я стал писать свой код, надеясь, что у меня этой проблемы не будет. И ее действительно не оказалось, зато была другая: разрешение изображений было всегда 320x240. Полистав немного код directshow плагина, я решил пойти спать, чтобы разобраться с этим завтра. Следующий день опять не принес никаких результатов с directshow, зато я полностью дописал код в своем приложении. Поэтому оставалось только одно — добить этот directshow.
На следующий день я нашел решение, которое, как обычно бывает в таких ситуациях, оказалось довольно простым и очевидным. В коде нигде не вызывалась функция updateProperties(), которая получала информацию о поддерживаемых разрешениях, а также в самом конструкторе класса были жестко прописаны размеры 320x240. Исправив эту функцию и добавив ее вызов, я стал получать изображение максимально возможного разрешения.
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 — служит для выбора веб-камеры, если их установлено несколько:
Заголовочный файл
Здесь я просто приведу код заголовочного файла, потому что комментировать тут нечего.
#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 находится все еще в состоянии даже не беты, такими вещами, как веб-камера, уже можно пользоваться, правда с некоторыми оговорками и решаемыми проблемами.
Исходники приложения можно взять здесь.
Надеюсь, эта статья кому-нибудь поможет.
(Так как это моя первая статья, то обо всех опечатках и ошибках оформления прошу сообщать в личку.)
Автор: zodiac