Capturando secuencias de vídeo con Qt

Capturando secuencias de vídeo con Qt

Qt incluye el módulo Qt Multimedia para facilitar la manipulación de contenidos multimedia. Entre otras cosas permite reproducir audio y vídeo y capturar desde dispositivos de adquisición soportados por el sistema operativo.

Como por defecto no viene activado, es necesario abrir el archivo .pro del proyecto y añadir la línea

QT += multimedia multimediawidgets

Primeros pasos con la webcam

Capturar de una webcam es tan sencillo como crear un objeto QCamera e iniciar la captura mediante el método QCamera::start():

QCamera* camera = new QCamera;
camera->start();

Por lo general suele ser interesante incorporar un visor en el que mostrar al usuario lo que la cámara está capturando. Para simplificar esta tarea, Qt nos ofrece el control QCameraViewfinder que hereda del más genérico QVideoWidget:

QCamera* camera = new QCamera;
QCameraViewfinder* viewfinder = new QCameraViewfinder;
camera->setViewfinder(viewfinder);

Como QCameraViewfinder —al igual que otros controles de Qt Multimedia— no está disponible en Qt Designer, tendremos que colocarlo en nuestra ventana desde el propio código. Por ejemplo como el control central de la misma:

viewfinder->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
setCentralWidget(viewfinder);

Hecho esto, finalmente, podemos iniciar la cámara:

// camera->setCaptureMode(QCamera::CaptureVideo);
camera->setCaptureMode(QCamera::CaptureViewfinder);
camera->start();

Acceder a un dispositivo específico

En un mismo sistema pueden haber varios dispositivos de captura, de tal forma que puede ser necesario crear un objeto QCamera para un dispositivo concreto. En ese caso simplemente tenemos que indicarlo en el constructor:

QCamera* camera = new QCamera("/dev/video0");

Obviamente para hacerlo necesitamos conocer la lista de los dispositivos disponibles en el sistema. Esto se puede hacer a través del método QCamera::availableDevices(). Al igual que se puede obtener para cada uno de ellos un texto más descriptivo, principalmente de cara a los usuarios, usando QCamera::deviceDescription():

QList devices = QCamera::availableDevices();
qDebug() << "Capturando de... "
         << QCamera::deviceDescription(devices[0]);
QCamera* camera = new QCamera(devices[0]);

Accediendo a los frames individualmente

En el módulo Qt Multimedia cada objeto de la clase QVideoFrame representa un frame de vídeo.

La clase QVideoFrame

El motivo por el que no se usa para esto la clase QImage es porque QVideoFrame no siempre contiene los datos de los píxeles del frame, como sí ocurre con la clase QImage.

Cada sistema operativo tiene su propio API multimedia que tiene una manera particular de gestionar los buffers de memoria —por ejemplo, como una textura en OpenGL, como un buffer de memoria compartida en XVideo, como una CImage en MacOS X, etc.— . Estos buffers suelen ser internos al API, así que las aplicaciones los identifican a través de manejadores o handlers. Copiar los datos de los píxeles desde los buffers internos a la memoria del programa suele tener cierto coste, por lo que QVideoFrame evita hacerlo, a menos que el programador lo solicite. Esto tiene una serie de implicaciones para nuestros fines:

  • Por lo general QVideoFrame no contiene los datos de los píxeles sino un manejador al buffer interno del API donde están almacenados. Dicho manejador se puede obtener a través del método QVideoFrame::handle().

  • Para conocer el tipo de manejador —algo que depende del API nativo que esté usando el módulo Qt Multimedia— se puede usar el método QVideoFrame::handleType().

  • Para acceder al contenido de los píxeles se puede usar el método QVideoFrame::bits() pero primero hay que asegurarse de que dichos datos son copiados desde el buffer interno a la memoria del programa. Eso se hace invocando previamente el método QVideoFrame::map(). Cuando el acceso a estos datos ya no es necesario se debe usar el método QVideoFrame::unmap() para liberar la memoria.

  • El contenido de los objetos QVideoFrame se comparte explícitamente, por lo que cualquier cambio en un frame será visible en todas las copias.

Ganar acceso a los frames

Si estamos interesados en procesar los frames capturados, la forma más sencilla —aunque no la única— es crear nuestro propio visor para la cámara. El método QCamera::setViewfinder() admite un puntero a objetos de la clase QAbstractVideoSurface, que define la interfaz genérica para las superficies que saben como mostrar vídeo. Por lo que será esa la clase de la que heredaremos la nuestra:

class CaptureBuffer : public QAbstractVideoSurface
{
    Q_OBJECT

public:

    QList<QVideoFrame::PixelFormat> supportedPixelFormats(
            QAbstractVideoBuffer::HandleType handleType =
            QAbstractVideoBuffer::NoHandle) const
    {
        // A través de este método nos preguntan que formatos de
        // vídeo soportamos. Como vamos a guardar los frames en
        // objetos QImage nos sirve cualquiera de los formatos
        // sorportados por dicha clase: http://kcy.me/z6pa
        QList<QVideoFrame::PixelFormat> formats;
        formats << QVideoFrame::Format_ARGB32;
        formats << QVideoFrame::Format_ARGB32_Premultiplied;
        formats << QVideoFrame::Format_RGB32;
        formats << QVideoFrame::Format_RGB24;
        formats << QVideoFrame::Format_RGB565;
        formats << QVideoFrame::Format_RGB555;
        return formats;
    }

    bool present(const QVideoFrame &frame)
    {
        // A través de este método nos darán el frame para que
        // lo mostremos.
        return true;
    }
};

De tal forma que sólo tenemos que instanciarla y configurarla como visor de nuestra cámara.

CaptureBuffer* captureBuffer = new CaptureBuffer;
camera->setViewfinder(captureBuffer);

Convertir objetos QVideoFrame en QImage

A través del método present() de nuestra clase CaptureBuffer obtenemos los frames capturados. Sin embargo estos son de poca utilidad si no podemos acceder al contenido de los píxeles. Así que vamos a convertir el objeto QVideoFrame en un objeto QImage.

Como ya sabemos, QImage tiene diversos constructores. Entre ellos el siguiente:

QImage (const uchar *buffer, int width, int height,
        int bytesPerLine, QImage::Format format)

que permite crear un objeto QImage si tenemos:

  • El ancho —width— y el alto —height— del frame. Esto lo podemos obtener a través de los métodos QVideoFrame::width() y QVideoFrame::width().

  • El número de bytes por línea —bytesPerLine— . Para lo que tenemos el método QVideoFrame::bytesPerLine().

  • El formato de los píxeles —format—. En este caso el método QVideoFrame::pixelFormat() proporciona dicho formato para el frame y podemos convertirlo en uno equivalente de QImage usando QVideoFrame::imageFormatFromPixelFormat().

  • Un puntero al buffer en memoria que contiene los datos de los píxeles. Como comentamos anteriormente, QVideoFrame::bits() nos proporciona esa información pero primero tenemos que invocar al método QVideoFrame::map() para que dichos datos sean copiados desde el buffer interno a la memoria del programa.

frame.map(QAbstractVideoBuffer::ReadOnly);
QImage frameAsImage = QImage(frame.bits(), frame.width(),
    frame.height(), frame.bytesPerLine(),
    QVideoFrame::imageFormatFromPixelFormat(frame.pixelFormat()));

// Aquí el código que manipula frameAsImage...

frame.unmap();

Hay que tener en cuenta que cuando se crea un objeto QImage de esta manera, no se hace una copia del contenido de los píxeles, por lo que es importante asegurarse de que el puntero devuelto por QVideoFrame::bits() es válido mientras se esté haciendo uso del objeto. Por eso se invoca al método QVideoFrame::unmap() después de manipularlo y no antes.

Si se quiere conservar la imagen del objeto QImage después de invocar a QVideoFrame::unmap() es necesario hacer una copia usando, por ejemplo, QImage::copy().

Referencias