Esta será una de las tutorias más extensas que allá realizado, en la cual abarcare un gran numero temas enfocados en el análisis y procesamiento de datos de imágenes o vídeo en tiempo real, este proyecto se divide en dos grandes etapas la de adquisición y almacenaje de los datos en una BD identificando la cantidad de peatones y la segunda en la visualización de los datos mediante gráficas.

Descripción

Los sistemas de detección de personas o peatones se han comenzado a utilizar amplia-mente en vehículos para evitar accidentes, sistemas de seguridad y como herramienta de mercadotecnia (para saber cuantas personas están en caja, mirando un producto etc.) es por ello, su amplia utilidad que decidí realizar un sistema como prueba de concepto que permitiera contar a las personas y desplegar la información de una manera sencilla.

En esta primera etapa detectaremos y contaremos a los peatones en un vídeo obtenido remotamente o previamente grabado, esta información se enviara a un servidor con MongoDB (un tipo de base de datos NoSQL) para almacenar la información y poder representarla gráficamente posteriormente.

Demostración (ver en 1080p)

Código fuente disponible al final del articulo

Requisitos
Consideraciones Generales

Si ya has programado en C++ utilizando las librerías de OpenCV esta tutoria debería ser sencilla de seguir, sin embargo explicare a grandes rasgos como procesa imágenes OpenCV y que consideraciones debemos tener:

El procesamiento de la imagen de un vídeo se desarrolla fotograma a fotograma, transformando cada uno en una matriz sobre la cual se desarrollan operaciones matemáticas, pues bien comúnmente este procedimiento se desarrollaba en serie (de forma continua o lineal) sin embargo en el año 2012 OpenCV incorporo soporte para gráfica NVIDIA compatible con CUDA, lo que permitió desarrollar estas operaciones matemáticas en paralelo (es decir varios cálculos al mismo tiempo).

No obstante se debe tener en cuenta que no todo el código se puede paralelizar, ademas los CUDA cores (individualmente) pueden realizar micro cálculos, no operaciones muy complejas lo que a su vez lo hace perfecto para cálculos matriciales.

Otro aspecto a tener en consideración es que al realizare operaciones fotograma a fotograma, el tiempo que transcurra entre proceso ralentizará la reproducción del vídeo es por ello que se utilizara el paradigma de programación multi-thread por parte del procesador.

El principal retardo se produciría al insertar los datos en la BBDD externa con MongoDB, que ademas en mi caso se encuentra en un servidor remoto.

Se utilizara el Método Supresión no máxima del ingles Non-Maximum Suppression para eliminar la mayor cantidad de detecciones duplicadas (generar un recuadro en el mismo peatón varias veces), para mas detalle puede leer un ártico anterior en donde explico su utilidad. Non-Maximum Suppression en la detección de objetos

Creación del proyecto

Seguiremos los pasos standard para la creación de un proyecto en QT, Seleccionando crear un nuevo proyecto.

Creación de un nuevo proyecto QT5

Configuramos el archivo .pro agregando las librerías necesarias para el correcto funcionamiento del proyecto, estas librerías son las correspondientes a OpenCV, CUDA 7.5 y MongoDB (mongo-cxx-driver)

INCLUDEPATH += /usr/local/include/opencv-3.1.0/

INCLUDEPATH += /usr/local/include/bsoncxx/v_noabi/
INCLUDEPATH += /usr/local/include/mongocxx/v_noabi/

LIBS += `pkg-config opencv libmongocxx --cflags` -L/usr/local/lib

En mi caso el archivo en cuestión Detectar-Peatones-y-Base-De-Datos.pro queda de esta forma:

QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = Detectar-Peatones-y-Base-De-Datos
TEMPLATE = app

INCLUDEPATH += /usr/local/include/opencv-3.1.0/

INCLUDEPATH += /usr/local/include/bsoncxx/v_noabi/
INCLUDEPATH += /usr/local/include/mongocxx/v_noabi/

LIBS += `pkg-config opencv libmongocxx --cflags` -L/usr/local/lib

SOURCES +=  main.cpp\
            mainwindow.cpp \
            mythread.cpp

HEADERS  += mainwindow.h \
            mythread.h

FORMS    += mainwindow.ui
Interfaz de usuario (UI)

El diseño se basa en los anteriores diseños, en todo caso lo básico que debe tener la aplicación son dos "botones" con uno configurado como checkable para el "play", "parar" y otro para seleccionar el archivo, ademas de los "Radio Buttons" para identificar la entrada de vídeo. Ademas se despliegan datos como el numero de peatones, origen del vídeo y los fotogramas por segundo a los cuales se procesa el vídeo.
Dejare el código fuente completo por si presentan dudas.

UI CUDA OpenCV MongoDB
Generación de una nueva clase (Qthread)

Para administrar un nuevo hilo en el procesador encargado de enviar los datos al servidor con la Base de Datos.

nueva clase

Header de la nueva clase

En el header incluiremos lo necesario para enviar los datos al servidor con MongoDB.

#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>
#include <QDateTime>

//incluimos lo necesario para enviar los datos al servidor con MongoDB
#include <bsoncxx/builder/stream/document.hpp>
#include <bsoncxx/types.hpp>
#include <mongocxx/client.hpp>
#include <mongocxx/instance.hpp>
#include <mongocxx/uri.hpp>

class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = 0);
    void setMessage(int &msg);
    void stop();

signals:

public slots:

protected:
    void run();

private:
    volatile bool m_Stopped;
    int m_Msg;
};

#endif // MYTHREAD_H

En el siguiente cdigo es necesario cambiar los datos correspondientes a la uri que entrega mlab al crearse una cuenta de 500mb gratis, también pueden agregar la uri de su servidor local o privado.

#include "mythread.h"

mongocxx::instance inst{};
mongocxx::client 
//se requiere cambiar el usuario, contrasena y la base de datos.
conn{mongocxx::uri{"mongodb://usuario:clave@mlab.com:17432/basededatos"}};

MyThread::MyThread(QObject *parent) :
    QThread(parent),
    m_Stopped(false)
{
}

void MyThread::setMessage(int& msg)
{
    m_Msg = msg;
}

void MyThread::stop()
{
    m_Stopped = true;
}

void MyThread::run()
{
    if (m_Stopped == false) {

       bsoncxx::builder::stream::document document{};
       auto collection = conn["riclabdb"]["datos"];

       document  << "peatones" << m_Msg
                 << "fecha" << bsoncxx::types::b_date{QDateTime::currentMSecsSinceEpoch()};

       collection.insert_one(document.view());

    }
    m_Stopped = false;
}

Headers de la clase principal

Agregamos los header de OpenCV (incluyendo las librerías de CUDA para OpenCV) y QFileDialog (para poder obtener el seleccionador de archivos) a nuestro proyecto en el archivo "mainwindow.h".


#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QFileDialog>
#include <QTime>

#include <opencv2/objdetect/objdetect.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#include <opencv2/cudaobjdetect.hpp>
#include <opencv2/cudaimgproc.hpp>
#include <opencv2/cudawarping.hpp>

//el header de la clase creada para administrar los hilos
#include "mythread.h"

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
    void SeleccionarVideo();
    void ProcesarVideo(bool checked);
    void nms(const std::vector<cv::Rect> &srcRects, std::vector<cv::Rect> &resRects, float thresh);
    unsigned int pea=0;

private slots:
    void on_toolButton_clicked();
    void on_actionAbrir_Video_triggered();
    void on_play_toggled(bool checked);

private:
    Ui::MainWindow *ui;
    cv::VideoCapture cap;
    MyThread thread1;
};
#endif // MAINWINDOW_H
Código de las funciones principales

Tal como en tutorias anteriores se omitieron los namespaces para fines académicos, ya que de esta forma el lector sabrá a que clase corresponde cada método.

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget * parent):
  QMainWindow(parent),
  ui(new Ui::MainWindow) {

    ui - > setupUi(this);
    int ini = 0;
    thread1.setMessage(ini);

  }

MainWindow::~MainWindow() {
  thread1.stop();
  thread1.wait();
  delete ui;
}

/**
 * Metodo para obtener la direccion u origen    1del video
 * @brief MainWindow::SeleccionarVideo
 */
void MainWindow::SeleccionarVideo() {
  // Declara la variable con la ruta del archivo
  QString archivo = QFileDialog::getOpenFileName(this, tr("Abrir Video"),
    "",
    tr("Videos (*.avi *.mp4 *.mov *.m4v)"));
  //Agrega la ruta del archivo
  ui - > labelVideo - > setText(archivo);
  ui - > radioVideo - > setChecked(true);
}

/**
 * Metodo para procesar el video frame a frame si ckecked==true
 * @brief MainWindow::ProcesarVideo
 * @param checked
 *
 */
void MainWindow::ProcesarVideo(bool checked) {

  cv::destroyAllWindows(); // Para cerrar todas las ventanas

  unsigned long Atime;
  int fps;

  cv::Ptr < cv::cuda::HOG > d_hog = cv::cuda::HOG::create(cv::Size(48, 96)); //Size(64, 128));//
  d_hog - > setSVMDetector(d_hog - > getDefaultPeopleDetector());

  cv::Mat frame; // Frame como array multidimencional

  QTime time;

  if (!checked) { // Si !checked detiene el video si no lo procesa
    ui - > play - > setText("Iniciar video");
    cap.release();
  } else {
    ui - > play - > setText("Parar video");

    if (ui - > radioVideo - > isChecked()) { // si el "radio button" esta seleccionado ejecuta el video si no la webcam
      cap.open(ui - > labelVideo - > text().toStdString().c_str());
      ui - > label_muestra_origen - > setText("Video Pre-Grabado");
    } else {
      cap.open(0);
      ui - > label_muestra_origen - > setText("Video en Vivo");

    }

    cap >> frame;

    //cambiar el tamaño
    double scale = float(800) / frame.cols;
    cv::cuda::GpuMat GpuImg, rGpuImg;
    GpuImg.upload(frame);
    cv::cuda::resize(GpuImg, rGpuImg, cv::Size(GpuImg.cols * scale, GpuImg.rows * scale));
    cv::Mat rInimg;
    rGpuImg.download(rInimg);

    int peatones;

    time.start();

    while (1) // bucle hasta que se precione "parar video"
    {
      Atime = cv::getTickCount(); //tiempo inicial

      cap >> frame; // obtiene un nuevo frame del video o camara

      if (frame.empty()) break; // detiene el bucle si elframe esta vacio

      //cambiar el tamaño
      GpuImg.upload(frame);
      cv::cuda::resize(GpuImg, rGpuImg, cv::Size(GpuImg.cols * scale, GpuImg.rows * scale));
      rGpuImg.download(rInimg);
      cv::cuda::cvtColor(rGpuImg, rGpuImg, CV_BGR2GRAY);

      std::vector < cv::Point > found_locations;
      d_hog - > detect(rGpuImg, found_locations);

      std::vector < cv::Rect > found_locations_rect;
      d_hog - > detectMultiScale(rGpuImg, found_locations_rect);

      std::vector < cv::Rect > resRects;
      nms(found_locations_rect, resRects, 0.1 f);

      for (unsigned int i = 0; i < resRects.size(); ++i) {
        cv::Rect peaton_i = resRects[i];
        cv::rectangle(rInimg, peaton_i, CV_RGB(0, 255, 0), 2);

        int pos_x = std::max(peaton_i.tl().x - 10, 0);
        int pos_y = std::max(peaton_i.tl().y - 10, 0);
        cv::putText(rInimg, "Peaton", cv::Point(pos_x, pos_y), CV_FONT_HERSHEY_DUPLEX, 0.8, CV_RGB(0, 255, 0), 1.5);

      }

      if (time.elapsed() > 2000) {

        peatones = resRects.size();
        thread1.setMessage(peatones);

        if (thread1.isRunning()) {
          thread1.stop();
        } else {
          thread1.start();
        }

        time.restart();
      }

      cv::namedWindow("Reproductor", cv::WINDOW_KEEPRATIO); // creamos una ventana la cual permita redimencionar
      cv::imshow("Reproductor", rInimg); // se muestran los frames

      fps = cv::getTickFrequency() / (cv::getTickCount() - Atime);

      ui - > label_muestra_fps - > setText(QString::number(fps));
      ui - > label_muestra_nro - > setText(QString::number(resRects.size()));

      char key = (char) cv::waitKey(20); //espera 20ms por la tecla ESC
      if (key == 27) break; //detiene el bucle
    }

  }

}

/**
 * Metodo para aplicar la Non-Maximum Suppression, borrando detecciones duplicadas
 * @brief nms
 * @param srcRects
 * @param resRects
 * @param thresh
 */
void MainWindow::nms(const std::vector < cv::Rect > & srcRects, std::vector < cv::Rect > & resRects, float thresh) {

  resRects.clear();
  const size_t size = srcRects.size();
  if (!size) {
    return;
  }

  // Ordena los cuadros de límite desde la parte inferior (eje y derecho)
  std::multimap < int, size_t > idxs;
  for (size_t i = 0; i < size; ++i) {
    idxs.insert(std::pair < int, size_t > (srcRects[i].br().y, i));
  }

  // mantendra el bucle mientras los índices siguen en la lista
  while (idxs.size() > 0) {
    // obtiene el último rectángulo
    auto lastElem = --std::end(idxs);
    const cv::Rect & rect1 = srcRects[lastElem - > second];
    resRects.push_back(rect1);
    idxs.erase(lastElem);

    for (auto pos = std::begin(idxs); pos != std::end(idxs);) {
      // obtiene el rectángulo actual
      const cv::Rect & rect2 = srcRects[pos - > second];
      float intArea = (rect1 & rect2).area();
      float unionArea = rect1.area() + rect2.area() - intArea;
      float overlap = intArea / unionArea;

      // si hay superposición, suprime el cuadro de actual
      if (overlap > thresh) {
        pos = idxs.erase(pos);
      } else {
        ++pos;
      }
    }
  }
}

void MainWindow::on_toolButton_clicked() {
  SeleccionarVideo();
}

void MainWindow::on_actionAbrir_Video_triggered() {
  SeleccionarVideo();
}

void MainWindow::on_play_toggled(bool checked) {
  ProcesarVideo(checked);
}

Si notas algún error de redacción o un error ortográfico por favor házmelo notar así puedo corregirlo y seguir mejorando.

Código fuente

Descargar código en GitHub - CUDA QT OpenCV MongoDB Detectar Peatones y Base De Datos