Захват изображений с вебкамеры через QCamera

в 13:55, , рубрики: c++, directshow, qcamera, Qt Software, qt5, баги, обработка изображений, метки: , , , ,

Привет!

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

Захват изображений с вебкамеры через QCamera

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

Предыстория

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

Первые проблемы

Первые проблемы начались еще на этапе компиляции. Из-за кривых скриптов/гайдов у меня никак не хотел компилироваться qtwebkit, из-за чего я потерял один вечер, но зато потом весь фреймворк был скомпилирован в виде debug и release версии.

Дальше — интереснее.
Зайдя в примеры для QtMultimedia и найдя там директорию camera, я решил запустить и посмотреть как оно работает.
Тут меня ждала вторая проблема:
Захват изображений с вебкамеры через QCamera

Очевидно, что кутям не хватает какого-то плагина. Чтобы его найти, я полез в QtMultimediasrcplugins. Там мой взгляд первым делом пал на gstreamer, но довольно быстро я понял, что под винду его не откомпилить.

Затем я там же нашел недописанный directshow.

Direct Show

Откомпилировав этот плагин и положив его в QtBasepluginsmediaservice, я успешно запустил пример из QtMultimedia, который показал список камер и даже пытался вывести изображение, но у него это получалось плохо и полосато:
Захват изображений с вебкамеры через QCamera

Плюнув на это, я стал писать свой код, надеясь, что у меня этой проблемы не будет. И ее действительно не оказалось, зато была другая: разрешение изображений было всегда 320x240. Полистав немного код directshow плагина, я решил пойти спать, чтобы разобраться с этим завтра. Следующий день опять не принес никаких результатов с directshow, зато я полностью дописал код в своем приложении. Поэтому оставалось только одно — добить этот directshow.

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

Исправление

1) Открываем файл QtMultimediasrcpluginsdirectshowcameradscamerasession.cpp.
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 — собственно, главное окошко:
Захват изображений с вебкамеры через QCamera

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

Заголовочный файл

Здесь я просто приведу код заголовочного файла, потому что комментировать тут нечего.

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) Если его там не оказалось, то необходимо вывести диалог с выбором:
Захват изображений с вебкамеры через QCamera

Однако, если веб-камер нет, то надо просто выйти с ошибкой, а если она всего одна, то выбрать ее.

Но если веб-камер несколько, то в цикле создадим кнопочки для каждой веб-камеры и покажем диалог:

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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js