CUDA 7.5 con OpenCV 3.1 (C++) y MongoDB sobre QT 5 en Linux - Detección de peatones
Esta será uno de los tutoriales más extensos que haya realizado, en la cual abarcaré un gran número de 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 ampliamente en vehículos para evitar accidentes, sistemas de seguridad y como herramienta de mercadotecnia (para saber cuántas 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 enviará 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 artículo
Requisitos
- Tarjeta gráfica NVIDIA compatible con CUDA (Aquí la lista)
- En lo posible procesador multi-núcleo ya que se utilizará programación multi-thread por parte del procesador
- Linux (En mi caso Debian 8)
- CUDA 7.5 instalado (Guía desarrollada por mí)
- OpenCV 3.1 instalado (Guía desarrollada por mí)
- QT 5 instalado (Guía desarrollada por mí)
- Drivers de C++ para MongoDB instalados (Guía externa)
Consideraciones Generales
Si ya has programado en C++ utilizando las librerías de OpenCV, este tutorial debería ser sencillo de seguir. Sin embargo, explicaré a grandes rasgos cómo procesa imágenes OpenCV y qué 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 incorporó soporte para gráficas NVIDIA compatibles 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. Además, 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 realizar operaciones fotograma a fotograma, el tiempo que transcurra entre procesos ralentizará la reproducción del vídeo. Es por ello que se utilizará 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 además en mi caso se encuentra en un servidor remoto.
Se utilizará el Método Supresión no máxima del inglés Non-Maximum Suppression para eliminar la mayor cantidad de detecciones duplicadas (generar un recuadro en el mismo peatón varias veces). Para más detalle puede leer un artículo anterior en donde explico su utilidad: Non-Maximum Suppression en la detección de objetos.
Creación del proyecto
Seguiremos los pasos estándar para la creación de un proyecto en QT, seleccionando crear un nuevo proyecto.

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": uno configurado como checkable para el "play"/"parar" y otro para seleccionar el archivo, además de los "Radio Buttons" para identificar la entrada de vídeo. Además, se despliegan datos como el número de peatones, origen del vídeo y los fotogramas por segundo a los cuales se procesa el vídeo.
Dejaré el código fuente completo por si presentan dudas.

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.

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 código 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, contraseña 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 headers 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 tutoriales anteriores, se omitieron los namespaces para fines académicos, ya que de esta forma el lector sabrá a qué 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;
}
/**
* Método para obtener la dirección u origen del 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);
}
/**
* Método para procesar el video frame a frame si checked==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 multidimensional
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" está 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 presione "parar video"
{
Atime = cv::getTickCount(); //tiempo inicial
cap >> frame; // obtiene un nuevo frame del video o cámara
if (frame.empty()) break; // detiene el bucle si el frame está vacío
//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 redimensionar
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
}
}
}
/**
* Método 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));
}
// mantendrá 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 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