В этой статье я расскажу о работе с веб-камерой из Qt5 под Windows (но пример также должен работать под Linux и Mac OS X с установленным плагином gstreamer).
Если интересно, как сделать вот такое приложение и преодолеть возникающие при этом проблемы, то прошу под кат.
Предыстория
Однажды мне захотелось добавить в свою скриншотилку (которая, в принципе, не совсем и скриншотилка) поддержку веб-камеры. Так как в то время я использовал Qt4, то стал искать готовые решения для этой версии, но потом
Было принято решение перейти на Qt5, которая была все еще в состоянии альфы (да и сейчас только предбета).
Первые проблемы
Первые проблемы начались еще на этапе компиляции. Из-за кривых скриптов/гайдов у меня никак не хотел компилироваться qtwebkit, из-за чего я потерял один вечер, но зато потом весь фреймворк был скомпилирован в виде debug и release версии.
Дальше — интереснее.
Зайдя в примеры для QtMultimedia и найдя там директорию camera, я решил запустить и посмотреть как оно работает.
Тут меня ждала вторая проблема:
Очевидно, что кутям не хватает какого-то плагина. Чтобы его найти, я полез в QtMultimedia\src\plugins. Там мой взгляд первым делом пал на gstreamer, но довольно быстро я понял, что под винду его не откомпилить.
Затем я там же нашел недописанный directshow.
Direct Show
Откомпилировав этот плагин и положив его в QtBase\plugins\mediaservice, я успешно запустил пример из QtMultimedia, который показал список камер и даже пытался вывести изображение, но у него это получалось плохо и полосато:
Full size
Плюнув на это, я стал писать свой код, надеясь, что у меня этой проблемы не будет. И ее действительно не оказалось, зато была другая: разрешение изображений было всегда 320×240. Полистав немного код directshow плагина, я решил пойти спать, чтобы разобраться с этим завтра. Следующий день опять не принес никаких результатов с directshow, зато я полностью дописал код в своем приложении. Поэтому оставалось только одно — добить этот directshow.
На следующий день я нашел решение, которое, как обычно бывает в таких ситуациях, оказалось довольно простым и очевидным. В коде нигде не вызывалась функция updateProperties(), которая получала информацию о поддерживаемых разрешениях, а также в самом конструкторе класса были жестко прописаны размеры 320×240. Исправив эту функцию и добавив ее вызов, я стал получать изображение максимально возможного разрешения.
Теперь переходим непосредственно к коду.
Работа с веб-камерой в Qt5
Так как пример небольшой и служит лишь для демонстрации, то все слоты я описал в конструкторе.
Рисуем формочки
Для начала набросаем в дизайнере две небольшие формы.
webcam.ui — собственно, главное окошко:
Full size
webcamselect.ui — служит для выбора веб-камеры, если их установлено несколько:
Заголовочный файл
Здесь я просто приведу код заголовочного файла, потому что комментировать тут нечего.
Выбор камеры
Как можно заметить из 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 находится все еще в состоянии даже не беты, такими вещами, как веб-камера, уже можно пользоваться, правда с некоторыми оговорками и решаемыми проблемами.
Исходники приложения можно взять здесь.
Надеюсь, эта статья кому-нибудь поможет.