Capturing Still Images with Qt

Prologue

Recently, I started to develop a generic front-office software for hotels. In any hotel where security is of utmost importance, data has to be captured. Some of these data are important( name, address, next of kin information ), others are purely optional( vehicle plate, departure to, occupation, passport etc ), at least in some hotels. To any C++/Qt software developer, capturing these data can be as simple as throwing a couple QLineEdits, QTextEdits, QLabel for displaying pictures and other widgets available in. The fields could then be checked to see if the data represented by these widgets are left unfilled, and further processing of the data could be done before finally saving it into the database. Checks that are performed on these widgets could make the resulting code look like so:

template<typename T>
bool IsEmpty( T const & str ){ return str.isEmpty(); }

template<typename T, typename... U>
bool IsEmpty( T const & str, U &&...other_str )
{
    return IsEmpty( str ) || IsEmpty( std::forward<U>( other_str )... );
}
// then somewhere in your code, check these
void NewGuestWindow::OnSaveButtonClicked()
{
    bool const any_is_empty = IsEmpty( ui->surname_edit->text(), ui->other_names_edit->text(),
                ui->nationality_edit->text(), ui->occupation_edit->text(),
                ui->vehicle_edit->text(), ui->id_card_edit->text(),
                ui->nok_name_edit->text() ); // next of kin(nok)
    if( any_is_empty ){
        QMessageBox::critical( this, "Error", 
            "One of the information fields is left unfilled, Please cross-check and try again.",
            QMessageBox::Ok );
        return;
    }
    //... other processing follows
}

Now, assume the task is to provide an implementation of the system that allows the hotel capture the pictures of guests lodging and the source of the image data should be one of the ( possibly many ) cameras attached to the host machine, how can this be done? Well, that is the purpose of this post, to have an overview of how to use Qt to capture a still image from cameras, regardless of whether it is built into a machine or detachable.

Using the Qt Camera feature

As mentioned earlier, in some hotels, it is strictly the policy to have a snapshot of the guest lodging into the hotel. Libraries exist in C++ that simplify ways of capturing still images, OpenCV is one of them. It has a non-static function cv::VideoCapture::open( int ) to do just that in a simple way and a cv::Matrix to store the result of the frame captured. This tutorial, on the other hand, shall make use of the classes and functions Qt provides. Qt has made capturing still/motion images fairly easy to do and although there are very few tutorials on the Internet about how to do this, still, the Qt documentation is the preferred go-to. The example given in this tutorial will describe how to use everything Qt provides. To make this example work, you should have a standard-conformat C++11 compiler and Qt5 installed. To move on, some assumptions will be made:

  • There is a QPushButton object, called capture_button, that when clicked captures an image in view.
  • A view( which will be discussed later ) exists, sort of like the view presented when a phone camera app is opened.
  • A QDialog box that its layout has been set to a QGridLayout.

This code snippet explains what is meant by the assumptions above
camera_capture.hpp

class CameraCapture: public QDialog
{
    Q_OBJECT
public:
    CameraCapture( QWidget *parent = nullptr );
    ~CameraCapture() = default;
private:    
    QCameraViewfinder   *view_finder; // the view which shows what our webcam sees
    QPushButton         *capture_button; // the button that, when pressed, captures our still image
    QGridLayout         *grid_layout; // holds our button and the view -- should not even be a data member, but OK.
};

camera_capture.cpp

CameraCapture::CameraCapture( QWidget *parent ): QDialog( parent ),
    view_finder{ new QCameraViewfinder() },
    capture_button{ new QPushButton( "Capture", this ) },
    grid_layout { new QGridLayout( this ) }
{
    view_finder->setMinimumSize( 640, 640 );

    grid_layout->addWidget( capture_button, 0, 0, 1, 3 );
    grid_layout->addWidget( view_finder, 1, 0, 10, 3 );
    this->setLayout( grid_layout );
}

In order to capture an image, you need a class that encapsulate the webcam object and provide us with convenient functions to tell its state, report errors, or( to put it simply ) to interact with it. In order to find the camera(s) available, and of course the state they are in, Qt provide a convenient static function in the QCameraInfo class, called availableCameras(). It returns a QList<QCameraInfo> with information about camera objects attached to the machine at that moment. For simplicity sake, this tutorial shall stick with the first camera object that we can use( it is possible to prompt users to select one of the many objects returned by QCameraInfo::availableCameras ) after traversing the returned list. First, we shall add a QCamera object and a member function to initialize the object or report error when it cannot.

class CameraCapture: public QDialog
{
    Q_OBJECT
public:
    CameraCapture( QWidget *parent = nullptr );
    //... the rest of the class code
    void DisplayCamera() noexcept(false);
private:
    QCamera *camera_to_use;
    //... other data members 
};

camera_capture.cpp

void CameraCapture::DisplayCamera() noexcept( false )
{
    QList<QCameraInfo> const available_cameras = QCameraInfo::availableCameras();
    int const all_cameras = available_cameras.size();

    if( all_cameras < 1 ){
        throw std::exception( "No cameras were found, please try again later" );
    }
    for( auto const &current_camera_info: available_cameras ){
        if( !current_camera_info.isNull() ){
            this->camera_to_use = new QCamera( current_camera_info, this );
            break;
        }
    }
    // if at this stage, the camera object has not been initialized, then none of the camera works
    if( !camera_to_use ){
        throw std::exception( "Unable to use any of the available cameras" );
    }
}

Next, we need to display the world to our user. Maybe not the whole wide world but at least a glimpse of what the ( web )camera sees. Qt, as you might have guessed correctly, provides yet another convenient class that takes care of this, it is called the QCameraViewfinder. Loosely speaking, QCameraViewfinder is a widget that helps the QCamera object present a good view of the world to users. What is left is to make the camera use this viewfinder widget in order to give a humanly presentable view of the world.

class CameraCapture: public QDialog
{
    Q_OBJECT
    // ... all data members and member functions
private:
    //... the rest of the class code
    QCamera             *camera_to_use;
    QCameraViewfinder   *view_finder;
};

camera_capture.cpp

void CameraCapture::DisplayCamera() noexcept( false )
{
    // ... code from previous definition
    if( !camera_to_use ){
        throw std::exception( "Unable to use any of the available cameras" );
    }
    // set the camera to use the view finder widget
    camera_to_use->setViewfinder( view_finder );
    view_finder->show();
    
    // then we connect the individual signals to appropriate callbacks
    QObject::connect( camera_to_use, &QCamera::lockFailed, [this]{
        emit this->camera_failed( "Unable to place a lock on the WebCam" );
    });
    QObject::connect( camera_to_use, 
        static_cast<void( QCamera::* )( bool )>( &QCamera::availabilityChanged ),
        [this]( bool available )->void
    {
        if( !available ){
            emit this->camera_failed( "Camera detached or not available" );
        }
    });
    QObject::connect( camera_to_use, 
        static_cast<void( QCamera::* )(QCamera::Error)>( &QCamera::error ),
        [this]( QCamera::Error error ) -> void
    {
        if( error != QCamera::NoError ){
            emit this->camera_failed( "An error occured while using the camera" );
        }
    });
}

Here is an overview of the journey so far.

  • Select a camera from a ( possible ) list of camera(s) available using QCameraInfo::availableCameras()
    • Communicate with the webcam using the QCamera class
  • Display a view for the world using the QCameraViewfinder
    • Connect the appropriate signals and slots.

We are almost done. Yes, the view has been shown, but we have not been able to capture any image. Well, in order to capture a still image ( or call it a picture ), we need to add something that allows the camera do the capturing such that when the Capture button is pressed, we get a snapshot off the webcam. We shall use the QCameraImageCapture class for that. The QCameraImageCapture class is used for recording media content and the documentation states it clearly that:

QCameraImageCapture class is a high level images recording class, it is not intended to be used alone but for accessing the media recording functions of other media objects, like QCamera.

Basically, it is an object of this class that captures the still image that is seen through the view from the camera. Let us see that in action

void CameraCapture::DisplayCamera() noexcept( false )
{
    QList<QCameraInfo> const available_cameras = QCameraInfo::availableCameras();
    // ...
    QObject::connect( camera_to_use, 
        static_cast<void( QCamera::* )( QCamera::Error )>( &QCamera::error ),
        [this]( QCamera::Error error ) -> void
    {
        if( error != QCamera::NoError ){
            emit this->camera_failed( "An error occured while using the camera" );
        }
    });

    image_capture = new QCameraImageCapture( camera_to_use );
    camera_to_use->setCaptureMode( QCamera::CaptureStillImage );
    camera_to_use->start();
    camera_to_use->searchAndLock();
    
    QObject::connect( capture_button, &QPushButton::clicked, [this]( bool ){
        image_capture->capture(); // when you press the capture button, capture the image
    });
    // when the image has been captured, show us a preview using the OnImageCaptured( int, QImage ) callback
    QObject::connect( image_capture, SIGNAL( imageCaptured( int, QImage )), this,
                      SLOT( OnImageCaptured( int,QImage ) ) );
}

void CameraCapture::OnImageCaptured( int, QImage const & image_preview )
{
    QDialog *preview_dialog = new QDialog( this );
    preview_dialog->setWindowTitle( "Preview" );
    
    // let us setup the layout accordingly
    QVBoxLayout *layout = new QVBoxLayout();
    // when we are satisfied with the captured image, we press this.
    QPushButton *use_captured_image_button = new QPushButton( "Use image" );
    
    // when we are not, we simply quit this preview and continue capturing another
    QPushButton *take_another_image_button = new QPushButton( "Take another" );
    
    // this label shows the picture captured
    QLabel      *preview = new QLabel();

    preview->clear();
    preview->setPixmap( QPixmap::fromImage( image_preview ));
    
    QGridLayout *button_grid = new QGridLayout();
    button_grid->addWidget( use_captured_image_button, 0, 0, Qt::AlignLeft );
    button_grid->addWidget( take_another_image_button, 0, 1, Qt::AlignRight );
    layout->addWidget( preview );
    layout->addLayout( button_grid );
    preview_dialog->setLayout( layout );
    
    
    QObject::connect( use_captured_image_button, &QPushButton::clicked, [=]( bool ){
        preview_dialog->accept();
        this->UnlockCamera(); // free the camera object and exit
        this->captured_image = QPixmap::fromImage( image_preview );
        this->accept();
    });
    QObject::connect( take_another_image_button, &QPushButton::clicked, [=]( bool ){
        preview_dialog->accept(); // simply exit this dialog and do nothing
    });
    preview_dialog->exec();
}

Epilogue

Sigh OK! We are done. Maybe not in its entirety. This only shows how to use the few classes mentioned, the dialog is no doubt ugly and not near production-ready.

We can do better than this!

  • Instead of using a QPushButton for capturing, we could subclass the QCameraViewfinder and provide a better widget like those found on phones camera apps.
  • Instead of crapping it all in code, the Qt Designer could be used.

If it is doable, it could form the topics in subsequent posts. Anyway, here is the class code in full or click here to see the code on Github

camera_capture.hpp

/* Copyright 2018 Joshua Ogunyinka <ogunyinkajoshua@yahoo.com> */

#ifndef CAMERACAPTURE_HPP
#define CAMERACAPTURE_HPP

#include <QDialog>
#include <QPixmap>
#include <QImage>

class QWidget;
class QCameraViewfinder;
class QCamera;
class QPushButton;
class QCameraImageCapture;
class QCloseEvent;

class CameraCapture: public QDialog
{
    Q_OBJECT
public:
    // when images are captured, you will need a filepath/filename to save it as
    CameraCapture( QString const filename, QWidget *parent = nullptr );
    ~CameraCapture();
    void DisplayCamera();
    QPixmap GetImage() const;

protected slots:
    void closeEvent(QCloseEvent *) override;
    void OnImageCaptured( int, QImage const &image_preview );
signals:
    void camera_failed( QString );
private:
    QCameraViewfinder   *view_finder;
    QCameraImageCapture *image_capture;
    QCamera             *camera_to_use;
    QPushButton         *capture_button;
    QPixmap             captured_image;

    void UnlockCamera(); // the destructor calls this to free the camera object on exit
};

#endif // CAMERACAPTURE_HPP

camera_capture.cpp

#include "camera_capture.hpp"
#include <QWidget>
#include <QCameraViewfinder>
#include <QCamera>
#include <QCloseEvent>
#include <QCameraImageCapture>
#include <QCameraInfo>
#include <QList>
#include <exception>
#include <QImage>
#include <QPushButton>
#include <QGridLayout>
#include <QLabel>

CameraCapture::CameraCapture( QString const file_name, QWidget *parent ): QDialog( parent ),
    view_finder{ new QCameraViewfinder() }, camera_to_use{ nullptr },
    capture_button{ new QPushButton( "Capture", this ) }
{
    view_finder->setMinimumSize( 640, 640 );

    QGridLayout *grid_layout { new QGridLayout( this ) };
    grid_layout->addWidget( capture_button, 0, 0, 1, 3 );
    grid_layout->addWidget( view_finder, 1, 0, 10, 3 );

    QObject::connect( capture_button, &QPushButton::clicked, [file_name, this]( bool ){
        image_capture->capture( file_name );
    });

    this->setLayout( grid_layout );
}

void CameraCapture::UnlockCamera()
{
    if( camera_to_use ){
        camera_to_use->unlock();
        camera_to_use->unload();
        camera_to_use->stop();
    }
    if( this->image_capture ){
        image_capture->cancelCapture();
    }
}

CameraCapture::~CameraCapture()
{
    UnlockCamera();
}

QPixmap CameraCapture::GetImage() const
{
    return captured_image;
}
// this may throw, so make sure calling code puts this in mind
void CameraCapture::DisplayCamera() noexcept( false )
{
    QList<QCameraInfo> const available_cameras = QCameraInfo::availableCameras();
    int const all_cameras = available_cameras.size();

    if( all_cameras < 1 ){
        throw std::exception( "No cameras were found, please try again later" );
    }
    for( auto const &current_camera_info: available_cameras ){
        if( !current_camera_info.isNull() ){
            this->camera_to_use = new QCamera( current_camera_info, this );
            break;
        }
    }
    if( !camera_to_use ){
        throw std::exception( "Unable to use any of the available cameras" );
    }

    camera_to_use->setViewfinder( view_finder );
    view_finder->show();

    QObject::connect( camera_to_use, &QCamera::lockFailed, [this]{
        emit this->camera_failed( "Unable to place a lock on the WebCam" );
    });
    QObject::connect( camera_to_use, static_cast<void(QCamera::*)( bool )>( &QCamera::availabilityChanged ),
                      [this]( bool available )->void
    {
        if( !available ) emit this->camera_failed( "Camera detached or not available" );
    });
    QObject::connect( camera_to_use, static_cast<void(QCamera::*)(QCamera::Error)>(&QCamera::error),
                      [this]( QCamera::Error error ) -> void
    {
        if( error != QCamera::NoError ) emit this->camera_failed( "An error occured while using the camera" );
    });

    image_capture = new QCameraImageCapture( camera_to_use );
    camera_to_use->setCaptureMode( QCamera::CaptureStillImage );
    camera_to_use->start();
    camera_to_use->searchAndLock();
    QObject::connect( image_capture, SIGNAL(imageCaptured(int,QImage)), this,
                      SLOT(OnImageCaptured(int,QImage)) );

}

void CameraCapture::OnImageCaptured( int, QImage const & image_preview )
{
    QDialog *preview_dialog = new QDialog( this );
    preview_dialog->setWindowTitle( "Preview" );
    QVBoxLayout *layout = new QVBoxLayout();
    QPushButton *use_captured_image_button = new QPushButton( "Use image" );
    QPushButton *take_another_image_button = new QPushButton( "Take another" );
    QLabel      *preview = new QLabel();

    preview->clear();
    preview->setPixmap( QPixmap::fromImage( image_preview ));
    QGridLayout *button_grid = new QGridLayout();
    button_grid->addWidget( use_captured_image_button, 0, 0, Qt::AlignLeft );
    button_grid->addWidget( take_another_image_button, 0, 1, Qt::AlignRight );

    layout->addWidget( preview );
    layout->addLayout( button_grid );
    preview_dialog->setLayout( layout );

    QObject::connect( use_captured_image_button, &QPushButton::clicked, [=]( bool ){
        preview_dialog->accept();
        this->UnlockCamera();
        this->captured_image = QPixmap::fromImage( image_preview );
        this->accept();
    });
    QObject::connect( take_another_image_button, &QPushButton::clicked, [=]( bool ){
        preview_dialog->accept();
    });
    preview_dialog->exec();
}

void CameraCapture::closeEvent(QCloseEvent * event)
{
    UnlockCamera();
    event->accept();
}

usage.cpp

int main(int argc, char **argv)
{
    QApplication a{ argc, argv };
    QString const picture_location { GetPictureFilename() }; // defined somewhere
    CameraCapture * const camera_capture { new CameraCapture( picture_location ) };
    QObject::connect( camera_capture, &CameraCapture::camera_failed,
                      [camera_capture]( QString const & message )
    {
        QMessageBox::critical( camera_capture, "Error", message );
        camera_capture->reject();
        std::exit( -1 );
    });

    try {
        camera_capture->DisplayCamera();
    } catch( std::exception & exception ) {
        QMessageBox::critical( this, "Error", exception.what() );
        camera_capture->reject();
        return;
    }
    if( camera_capture->exec() == QDialog::Accepted )
    {
        auto const captured_image_pixel = camera_capture->GetImage(); // do whatever you want with it
    }
    return a.exec();
}

For any suggestionns/improvements, contact me.