Как стать автором
Обновить

Qt и мобильная камера. Часть 1, Symbian

Разработка мобильных приложений *
Из песочницы
Доброго времени суток, Хабр!

За время моей работы в области разработки мобильных приложений, в частности для Symbian, было создано несколько решений для работы с камерой телефона. Со временем решения эти эволюционировали, о чем я и хочу рассказать в двух следующих статьях.
В первой речь пойдет о нетривиальном, но гибком получении изображения с Symbian-телефона средствами QtMobility 1.1.3, во второй — о проблемах и их решениях при переносе кода на платформу Meego 1.2 Harmattan под управлением которой в данный момент работают Nokia N900, N950, N9.
Данный материал может быть полезен начинающим Qt-разработчикам мобильных приложений.

Итак, Qt для работы с Symbian-камерой.

Задача, решением которой я занимался, оставалась одной и той же: необходимо было получить изображение с камеры, вывести его в собственный видоискатель, отрисовать некоторый виджет поверх выводимого изображения и передать несколько видео фреймов на удаленный сервер. Важным аспектом задачи является то, что выполнение всех действий должно происходить без участия пользователя — от него требуется только запуск приложения и наведение камеры.

В первый раз решение рождалось в диких муках, обусловленных некоторыми особенностями QtMobility. Если использовать простейший прямой метод вывода изображения на видоискатель (QVideoWidget или его наследник QCameraViewfinder) мы сталкиваемся со следующими проблемами:
  • QVideoWidget не позволяет отрисовывать полупрозрачные виджеты поверх себя (за прозрачностью получаются лишь черные прямоугольники)
  • Получить изображение как объект можно только в виде фотографии с обязательным ее сохранением в файловой системе (с помощью QCameraImageCapture)
  • Из вышеперечисленного следует, что управлять изображением «внутри» видоискателя невозможно, то есть на экране мы увидим только то, что видит сама камера

Кроме того, если попытаться получить изображение уже после его вывода на видоискатель через QWidget::render(), то опять же мы увидим лишь черный прямоугольник, так как сам QVideoWidget не перерисовывается на каждом фрейме. По той же причине бесполезно переопределять для видоискателя метод QWidget::paintEvent().

Когда задача решалась впервые, мы в конце концов пришли к использованию нативного Symbian кода. В итоге получился QObject, высылающий сингал с QPixmap на каждом новом фрейме. Этот pixmap и обрабатывался в приложении — рисовался, высылался и т.д.
И хотя данный вариант был полностью работоспособным, с ним пропала возможно легкого и быстрого портирования на другие платформы, точнее на другую — Meego.

Далее я предлагаю вариант решения задачи средствами только Qt и QtMobility, который, к слову, является более простым (особенно для тех, кто не знаком с Symbian.C++).
Приведенный ниже код частично основан на официальном примере, в котором для нас есть много лишнего, к делу не относящегося.

Итак, получаем видео фреймы с камеры на телефоне Symbian^3 или Symbian 5th edition средствами Qt в виде отдельных объектов. Для этого напишем свой собственный видоискатель, отрисовываемый заново на каждом новом фрейме.
Схема операций следующая:
  1. Создаем потомок абстрактного класса QAbstractVideoSurface и определяем в нем два чистых виртуальных метода QAbstractVideoSurface::supportedPixelFormats() и QAbstractVideoSurface::present(). Это будет обработчик фреймов, в который будут передаваться данные с камеры
  2. В методе present() производим конвертацию видео фрейма в объект QImage и передачу его в callback
  3. Подключаем камеру к нашему приложению и устанавливаем для нее новый «surface» (обработчик фреймов) — объект нашего класса из первого пункта
  4. При получении объекта QImage в методе callback'а обрабатываем его по своему усмотрению, в данном случае будем отрисовывать его в paintEvent() нашего видоискателя

Для начала создадим callback, в метод которого будет передаваться изображение (фрейм). В принципе, его можно заменить парой сигнал-слот, но я буду использовать именно этот способ. Этот класс должен наследоваться нашим конечным видоискателем.
P.S. Подключение библиотек указывать не буду — все необходимые Qt-классы можно подключить как обычно — по имени, например
#include <QAbstractVideoSurface>

>videosurfaceimageobserver.h

class VideoSurfaceImageObserver
{
public:
    virtual void newImage(QImage) = 0;
};

Далее — создание класса, наследованного от QAbstractVideoSurface:
>myvideosurface.h

class myVideoSurface : public QAbstractVideoSurface
{
    Q_OBJECT
public:
    myVideoSurface(VideoSurfaceImageObserver *mObserver, QObject *parent = 0);
    bool present(const QVideoFrame &frame);
    QList<QVideoFrame::PixelFormat> supportedPixelFormats(QAbstractVideoBuffer::HandleType type=QAbstractVideoBuffer::NoHandle ) const;

private:
      QVideoFrame m_frame;
      QImage::Format m_imageFormat;
      QVideoSurfaceFormat m_videoFormat;
      VideoSurfaceImageObserver *observer;
};

>myvideosurface.cpp

myVideoSurface::myVideoSurface(VideoSurfaceImageObserver *mObserver, QObject *parent) :
    QAbstractVideoSurface(parent)
{
    //определим ссылку на колбэк, в метод newImage() которого
    //будем передавать новые фреймы
    observer = mObserver;

    m_imageFormat = QImage::Format_Invalid;
}

/**
*Данный метод определяет, какие форматы видео фреймов
*поддерживаются нашим video surface'ом
*для Symbian данного списка форматов будет достаточно
*/
QList<QVideoFrame::PixelFormat> myVideoSurface::supportedPixelFormats(
            QAbstractVideoBuffer::HandleType handleType) const
{
    if (handleType == QAbstractVideoBuffer::NoHandle) {
        return QList<QVideoFrame::PixelFormat>()
                << QVideoFrame::Format_RGB32
                << QVideoFrame::Format_ARGB32
                << QVideoFrame::Format_ARGB32_Premultiplied
                << QVideoFrame::Format_RGB565
                << QVideoFrame::Format_RGB555;
    } else {
        return QList<QVideoFrame::PixelFormat>();
    }
}

/**
*Наш самый важный метод.
*Здесь, получая фрейм с камеры в объекте QVideoFrame,
*мы должны создать из него QImage и передать в колбэк
*/
bool myVideoSurface::present(const QVideoFrame &frame){
    m_frame = frame;
    //Проверяем "на вшивость" - соответствует ли текущий формат входящего фрейма
    //формату, установленному для текущего surface'а.
    //В нашем случае текущий формат устанавливает сама камера,
    //так что по идее будет соответствовать всегда
    if(surfaceFormat().pixelFormat() != m_frame.pixelFormat() ||
        surfaceFormat().frameSize() != m_frame.size()) {
        stop();
        return false;
    } else {
        //Создаем из фрейма QImage...
        if (m_frame.map(QAbstractVideoBuffer::ReadOnly)) {
         QImage image(
                 m_frame.bits(),
                 m_frame.width(),
                 m_frame.height(),
                 m_frame.bytesPerLine(),
                 m_imageFormat);

         //... и передаем его в колбэк
         observer->newImage(image);
        }
        return true;
    }
}

Наконец, создаем класс нашего видоискателя.
>myviewfinder.h

class myViewFinder: public QWidget, public VideoSurfaceImageObserver
{
    Q_OBJECT
public:
    explicit myViewFinder(QWidget *parent = 0);
    virtual ~myViewFinder();

    //не забываем про виртуальный метод
    void newImage(QImage);

private:
    void paintEvent(QPaintEvent *);

private:
    QCamera* camera_;
    myVideoSurface *surface;
    QImage *pix;
};

>myviewfinder.сpp

myViewFinder::myViewFinder(QWidget *parent)
    : QWidget(parent), camera_(0), viewfinder_(0), pix(0)
{
    //Собственно, инициализируем камеру - пригодится
    camera_ = new QCamera;

    //Создаем наш video surface и передаем в качестве 
    //колбэка наш видоскателя
    surface = new myVideoSurface(this, this);

    //Инициализируем текущий фрейм пустым изображением
    pix = new QImage();

    //---Данный код подключает в качестве обработчика 
    //---фреймов наш video surface
    QVideoRendererControl *control = qobject_cast<QVideoRendererControl *>(
                camera_->service()->requestControl("com.nokia.Qt.QVideoRendererControl/1.0"));
    if(control){
        control->setSurface(surface);
    }
    //---

    //Включаем камеру, фреймы автоматически начнут передаваться
    //в myVideoSurface после инициализации устройства
    camera_->start();
}

/**
*Именно сюда приходит каждый фрейм с камеры, 
*преобразованный в QImage
*/
void myViewFinder::newImage(QImage img){
    //Сохраняем изображение...
    &pix = img;

    //... и перерисовываем видоискатель.
    //Результатом выполнения update()
    //будет асинхронный вызов paintEvent()
    //текущего виджета
    update();
}

void myViewFinder::paintEvent(QPaintEvent *event){
    QPainter painter(this);

   //Заливаем видоискатель черным.
   //Зачем - опишу ниже.
   painter.fillRect(this->geometry(), Qt::black);

    if(!pix) return;

    //Проверяем размеры текущего виджета и
    //изображения с камеры. В случае ошибки
    //при обработке изображение в video surface
    //изображение может быть пустым
    if(!pix->isNull() && this->geometry().width() != 0 && this->geometry().height() != 0
                    && pix->width() != 0 && pix->height() != 0)
    {
            //Определяем область рисования и...
            QRect pixR(pix->rect());
            pixR.moveCenter(this->geometry().center());

            //... рисуем!
            painter.save();
            painter.drawImage(pixR, *pix);
            painter.restore();
    }
}

myViewFinder::~myViewFinder()
{
    camera_->unload();
    delete viewfinder_;
    delete camera_;
    delete pix; 
}

Теперь о том, зачем заливать видоискатель черным перед отрисовкой изображения. Размер экрана на телефонах Symbian^3 и Symbian 5th edition 640x360, а разрешение камеры — как минимум 640x480. Так что если у вас, например, в полноэкранном приложении нет ничего кроме видоискателя, соотношение сторон не позволит заполнить изображением с камеры весь экран. Для этого заливаем полотно сначала черным (или любым другим цветом или картинкой), чтобы вокруг видоискателя не просвечивал рабочий стол или другие виджеты.

Чтобы заполнить видоискателем весь виджет без черных полос по краям, но с обрезкой краев изображения, painEvent() можно модифицировать следующим образом:

void myViewFinder::paintEvent(QPaintEvent *event){
    QPainter painter(this);

    if(!pix) return;

    if(!pix->isNull() && this->geometry().width() != 0 && this->geometry().height() != 0
                    && pix->width() != 0 && pix->height() != 0)
    {
            //Растянем изображение так, чтобы оно покрывало весь виджет
            QPixmap newPix = pix->scaled(this->size(), Qt::KeepAspectRatioByExpanding);
            QRect pixR(newPix.rect());
            pixR.moveCenter(this->geometry().center());

            //... рисуем!
            painter.save();
            painter.drawImage(pixR, newPix );
            painter.restore();
    }
}

Таким образом мы получили полноценный видоискатель, который можно в дальнейшем использовать как обычный виджет. При этом после поступления фрейма в newImage() делать с ним можно все что угодно, что дает преимущество по сравнению с QVideoWidget.

В следующей статье я расскажу о портировании данного кода на Meego 1.2 Harmattan и его модификации, связанной с аппаратными особенностями камеры в устройствах под управлением последней.

Использованные источники:
  1. Тот самый пример
  2. Остальные официальные доки по Qt и QtMobility

Спасибо за внимание.
Теги:
Хабы:
Всего голосов 20: ↑19 и ↓1 +18
Просмотры 4.3K
Комментарии 18
Комментарии Комментарии 18

Публикации

Истории

Работа