Aug 012012
 

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

Если интересно, как сделать вот такое приложение и преодолеть возникающие при этом проблемы, то прошу под кат.

Предыстория

Однажды мне захотелось добавить в свою скриншотилку (которая, в принципе, не совсем и скриншотилка) поддержку веб-камеры. Так как в то время я использовал Qt4, то стал искать готовые решения для этой версии, но потом мне подсказал, что в Qt5 есть класс QCamera, который как раз подходил под мои задачи.
Было принято решение перейти на 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

Выбор камеры

Как можно заметить из 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 находится все еще в состоянии даже не беты, такими вещами, как веб-камера, уже можно пользоваться, правда с некоторыми оговорками и решаемыми проблемами.

Исходники приложения можно взять здесь.
Надеюсь, эта статья кому-нибудь поможет.