Aplicación OpenCV / GtK de pantalla completa en C++ ejecutándose en Raspberry PI

Introducción

C++ , OpenCV y Gtk son un buen triplete para crear aplicaciones que se ejecutan en una Raspberry PI, toman imágenes de la cámara, las procesan, las muestran y tienen una interfaz de usuario ilimitada. En este artículo, le mostraré una ruta ingenua para mostrar capturas de cámara en una ventana de pantalla completa. En un segundo artículo, corregiré algunas de las deficiencias de este método demasiado simple. Espero que estos dos artículos puedan ayudarlo a superar los aburridos problemas que necesita resolver antes de llegar al lugar donde puede divertirse.

Supongo que sabe de codificación pero no está familiarizado con C++ , Gtk u OpenCV . Fue mi caso cuando comencé este pequeño proyecto, y pasé una gran cantidad de tiempo descubriendo los detalles de este rico lenguaje y sus dependencias. Como este no es un tutorial de C++ , solo nombraré los conceptos y proporcionaré enlaces a las explicaciones.

Aquí encontrará las fuentes del proyecto: https://github.com/cpp-tutorial/raspberry-cpp-gtk-opencv . La rama maestra muestra solo el primer paso de este tutorial. Los pasos adicionales se encuentran en las ramas Paso 1, Paso 2, etc. Para tener la versión completa, puede consultar el último paso o la última versión.

Un elemento importante es poder construir y probar su proyecto en su computadora de escritorio o portátil preferida. Raspberry Pi está destinado a ser una plataforma de sistema integrada. Es increíble, pero no tiene el teclado, el mouse, el monitor o la cantidad de memoria adecuados para ser una herramienta de desarrollo cómoda.

Estoy usando la siguiente pila técnica:

  • Raspberry Pi : el objetivo final es iniciar la aplicación en él.
  • CMake : este es el ingrediente necesario para que su código se pueda construir en la mayoría de las plataformas. También exporta su proyecto a Xcode, VisualStudio, Eclipse y una larga lista de otros, por lo que podrá elegir su IDE preferido.
  • Mac OS X , Ubuntu , Windows : donde puede escribir y probar su aplicación.
  • C++ : Python está de moda, es joven, eficiente y está bien respaldado por la gente de Raspberry. Pero resulta que me gusta más C++ . Si Python es lo tuyo, entonces deja de leer.
  • OpenCV : una biblioteca de visión por computadora (de ahí el CV) de código abierto muy utilizada (de ahí el Open).
  • Gtkmm , que es el puerto de Gtk orientado a C++ : aunque OpenCV le permite mostrar imágenes en la pantalla, es algo limitado cuando interactúa con el usuario. Al ser fácilmente compatibles, Gtk / Gtkmm son un excelente complemento de OpenCV para crear interfaces de usuario reales en torno a aplicaciones de visión por computadora.
    • Instalación de su entorno de desarrollo

      Lo primero que necesita es un entorno de desarrollo de trabajo. He probado las tres principales, Mac OS X , Windows y Linux . Las instrucciones de Linux también se aplican a Raspberry .

      Entorno de desarrollo para Mac OS

      En el mundo de Mac OS, el IDE predeterminado es Xcode . Los otros paquetes se instalan a través de Homebrew .

      C++-Xcode

      Tienes una Mac, por lo que tienes derecho a usar Xcode gratis. Sé que hay mucha gente que lo odia, y tienen sus razones. Si sabe mejor, entonces use el IDE que prefiera. Si no, entonces use Xcode :

      • Desde tu Mac, abre App Store .
      • Navega o busca Xcode .
      • Consíguelo o actualízalo.
      • En el primer lanzamiento, propondrá instalar más cosas. Aceptarlos.

      Cerveza casera

      Es un gestor de paquetes para Mac OS X, y la forma más sencilla de instalar el resto de paquetes que necesites.

      Si ya tienes instalada una versión de Homebrew , ahora es un buen momento para verificar, actualizar y arreglar todo lo necesario. El siguiente comando le dará muchas sugerencias:

$ brew update
$ brew doctor

Sigue las instrucciones hasta que las hayas arreglado todas (bueno, no te pongas demasiado paranoico en cualquier caso; arregla todo lo que puedas).

pkg-config

Este paquete ayuda a vincular su proyecto a las bibliotecas OpenCV y GtK (ver Wikipedia en pkg-config ).

Para instalarlo:

$ brew install pkg-config

Cuando finalice la instalación, puede verificar si todo está bien escribiendo:

$ pkg-config --version
> 0.29.2
$ pkg-config --list-all
> zlib                                zlib - zlib compression library
> ...                                 etc...
> ...                                 etc...
> ...                                 etc...
> harfbuzz-icu                        HarfBuzz text shaping library ICU integration

OpenCV y GTK

Instalamos OpenCV con aportes de terceros. No los necesitarás al principio, pero tampoco te molestarán. Y usamos Gtkmm3 , que es el GtK para C++:

brew install opencv3 --with-contrib
brew install gtkmm3

Para verificar que las bibliotecas están ahora en su lugar:

$ pkg-config --list-all | grep opencv
> opencv                              OpenCV - Open Source Computer Vision Library
$ pkg-config --list-all | grep gtkmm
> gtkmm-3.0                           gtkmm - C++ binding for the GTK+ toolkit

Git

Probablemente lo tenía instalado cuando ejecutó brew doctor y requería ejecutar lo siguiente:

$ xcode-select --install

Para comprobar que efectivamente lo tienes:

$ git --version
git version 2.17.2 (Apple Git-113)

Entorno de desarrollo para Windows

QUE HACER

Entorno de desarrollo para Linux (Debian)

Para escribir esta parte, mi intención inicial después de instalar una nueva distribución de Ubuntu era configurar cualquier IDE conocido y hacer clic hasta que pudiera compilar, depurar y ejecutar el proyecto. ¡Vaya, no fui ingenuo! Varios días después, descarté Clion porque es una aplicación de pago, probé sin éxito Code::Blocks , Eclipse y perdí la paciencia antes de probar Qt Builder ; sin embargo, no he podido encontrar un IDE aceptable para solo hacer las cosas básicas con una sencillez razonable. Y luego encontré esta pregunta de desbordamiento de pila «C++ IDE para Linux?» donde la respuesta aceptada y más votada establece que «UNIX es un IDE. Todo ello» .

C++: Linux es un IDE

Aceptemos que en Linux no vamos a tener un IDE elegante y contentarnos con un editor de texto. Dos opciones populares son:

La herramienta para depurar código es gdb . Parece sombrío, pero en realidad puedes hacer un buen trabajo con él. Echa un vistazo a esos videos:

En el primer paso, un poco más abajo, daré instrucciones breves y específicas para compilar con símbolos de depuración y depurar los programas con gdb .

apt-get

Este es el administrador de paquetes oficial de la mayoría de las distribuciones de Linux . Las aplicaciones más populares se distribuyen a través de él y, por lo general, está preinstalado.

pkg-config

En una distribución de Linux, existe la mayor posibilidad de que pkg-config ya esté instalado. De todos modos, puedes intentarlo; si ya está instalado, simplemente le dirá:

sudo apt-get update
sudo apt-get install pkg-config

CHacer

Lo necesitarás para compilar OpenCV , así que será mejor que lo instales ahora:

$ sudo apt-get install cmake

OpenCV

Como requisito previo, debe tener las siguientes bibliotecas de apt-get :

# required:
sudo apt-get install \
   libgtk-3-dev \
   pkg-config \
   libavcodec-dev \
   libavformat-dev \
   libswscale-dev 
# Optional:
sudo apt-get install \
   python-dev \
   python-numpy \
   libjpeg-dev \
   libpng-dev \
   libtiff-dev \
   libjasper-dev \
   libdc1394-22-dev

Una vez que haya instalado todos los requisitos previos, obtenga la última versión de las fuentes de OpenCV y descomprímala:

$ wget https://github.com/opencv/opencv/archive/3.4.1.zip
$ unzip 3.4.1.zip

Preparar y compilar:

$ cd opencv-3.4.1
$ mkdir build
$ cd build
$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local ..
$ make -j4  # Number of processors. Don't use more that you computer has.

Ir a caminar; esto toma años. Si el proceso se interrumpe, puede volver a iniciarlo simplemente volviendo a escribir:

$ make -j4

Cuando termine la compilación, complete la instalación:

$ sudo make install
$ sudo ldconfig

Para comprobar si la biblioteca está disponible como dependencia:

$ pkg-config --list-all | grep opencv
> opencv                         OpenCV - Open Source Computer Vision Library

Ahora puede eliminar la carpeta de fuentes; ya no lo necesitas:

$ cd ..
$ cd ..
$ rm -rf opencv-3.4.1
$ rm 3.4.1.zip

Gtkmm

Gtkmm es el Gtk para C++ :

$ sudo apt-get install libgtkmm-3.0-dev

Para comprobar si la biblioteca está disponible como dependencia:

$ pkg-config --list-all | grep gtkmm
gtkmm-3.0                      gtkmm - C++ binding for the GTK+ toolkit

Git

Para instalar Git :

$ sudo apt-get install git

Y para verificar que Git está presente:

$ git --version
> git version 2.7.4

La estructura de carpetas del proyecto.

Hay varios artículos excelentes que tratan sobre la mejor estructura de carpetas para un proyecto creado con CMake :

En esta primera etapa (hay más en la segunda parte del artículo), estoy proponiendo una estructura de carpetas simplificada. Los proyectos que tienen múltiples ejecutables y bibliotecas necesitan una carpeta por módulo y la estructura es mucho más compleja. En este caso, como habrá un solo ejecutable, solo tengo una carpeta raíz, llamada srcy, en ella, una carpeta para cada tipo de archivo: fuentes, encabezados y recursos.

.gitignore  <-- Ignore xcode, codeb, build folders.
/src  <----- All sources are here
   CMakeList.txt  <-- The CMake configuration file.
      /cpp  <----- Contains the C++ source files.
      /hpp  <----- Contains the C++ header files.
      /res  <----- Contains resource files.
/build  <----- Contains the temporary build files. 
                 Not under version control
/xcode  <----- Contains the XCode project.
                 Not under version control.
/codeb  <----- Contains the Code::Blocks project. 
                 Not under version control. 

Paso 1: una aplicación de ventana única

En este primer paso construimos una aplicación de ventana única muy simple usando CMake para configurar el proyecto y Gtk para mostrar ventanas. El código fuente está disponible como rama principal en: https://github.com/cpp-tutorial/raspberry-cpp-gtk-opencv . Para recuperarlo:

Configurar el proyecto con CMake

Este es el CMakeLists.txt que va en la carpeta src :

# src/CMakeLists.txt
cmake_minimum_required(VERSION 3.3 FATAL_ERROR)

# Project name and current version
project(rascam VERSION 0.1 LANGUAGES CXX)

# Enable general warnings
# See http://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")

# Use 2014 C++ standard.
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Must use GNUInstallDirs to install libraries into correct locations on all platforms:
include(GNUInstallDirs)

# Pkg_config is used to locate headers and files for dependency libraries:
find_package(PkgConfig)

# Defines variables GTKMM_INCLUDE_DIRS, GTKMM_LIBRARY_DIRS and GTKMM_LIBRARIES.
pkg_check_modules(GTKMM gtkmm-3.0)

# Adds GTKMM_INCLUDE_DIRS, GTKMM_LIBRARY_DIRS to the list of folders the
# compiler and linker will use to look for dependencies:
link_directories( ${GTKMM_LIBRARY_DIRS} )
include_directories( ${GTKMM_INCLUDE_DIRS} )

# Declares a new executable target called rascapp
# and lists the files to compile for it:
add_executable(rascapp
    cpp/main.cpp    
    cpp/main-window.cpp
)

# Adds the folder with all headers:
target_include_directories(rascapp PRIVATE hpp)

# Link files:
target_link_libraries(rascapp
   ${GTKMM_LIBRARIES}  
)

Repasemos los comandos uno por uno:

  • cmake_minimum_requiredrequiere una versión mínima de CMake . Esto le permite utilizar con confianza las funciones existentes en la versión especificada o mostrar un mensaje de error explícito si la versión instalada es demasiado antigua.
  • projectdeclara el nombre del proyecto global rascam, especifica la versión actual y enumera los lenguajes que estamos usando, a saber, C++. Históricamente, la extensión para C++ era *.c++, pero en algún contexto hay problemas con «+», por lo que cambiaron a cppo cxx. En CMake , CXXsignifica C++ .
  • set(CMAKE_CXX...)establece algunas banderas que son apropiadas para C++ moderno .
  • find_package(PkgConfig)enlaces al pkg-configcomando, para que podamos usarlo más adelante.
  • pkg_check_modules(GTKMM gtkmm-3.0)hace lo mismo que ejecutar pkg-config gtkmm-3.0en la terminal (pruébelo, si lo desea), y luego copia cada sección de la respuesta en variables con el prefijo especificado GTKMM. Por lo tanto, después de este comando tenemos tres variables llamadas GTKMM_LIBRARY_DIRS, GTKMM_INCLUDE_DIRSy GTKMM_LIBRARIES.
  • Entonces declaramos un objetivo. Un proyecto puede tener varios objetivos. En general, los objetivos comprenden ejecutables o bibliotecas que se definen llamando add_executableo add_libraryy que pueden tener muchas propiedades configuradas (consulte la distinción entre un «objetivo» y un «comando» ). Como este es un proyecto de un solo objetivo, es perfectamente conveniente definir el objetivo directamente en el CMakeLists.txtarchivo principal. Enumeramos los archivos de origen para compilar para crear el destino.
  • target_include_directoriesestablece la lista de carpetas donde buscar los archivos referidos en las fuentes como include "...".
  • target_link_librariesestablece la lista de bibliotecas que el enlazador (consulte qué hacen los enlazadores ) tiene que vincular para completar la compilación.

En caso de que quiera leer más sobre los pasos de compilación:

La puesta en marcha de GTK

Todos los ejecutables de C++main necesitan un método como punto de entrada. Para iniciar una aplicación GtK , debe especificar una ventana principal, que actuará como base para todas las demás interacciones del usuario, incluso para abrir más ventanas.

#include "main-window.hpp"
  
int main(int argc, char* argv[])
{
    auto app
        = Gtk::Application::create(argc,
                                   argv,
                                   "raspberry-cpp-gtk-opencv");
  
    MainWindow mainWindow(300, 300);
    return app->run(mainWindow);
}

Declarar una variable como autoespecifica que el tipo de la variable que se declara se deducirá automáticamente de su inicializador . Esto se llama inferencia de tipos .

La llamada a Gtk::Application::createinicializa un nuevo contexto de ejecución de GtK , pasa los argumentos del programa y establece la identificación de la aplicación (que puede ser cualquier cosa, pero la convención dice que debe contener su nombre de dominio al revés, seguido de su nombre, como en ch.agl-developpement.cpp-tutorial.raspberry-cpp-gtk-opencv)

Una vez que tenga un contexto listo, puede usarlo para abrir una ventana. La ventana que puede ser una instancia de cualquier clase que se extienda Gtk::Window. En este caso, nuestra ventana principal se llama de forma bastante explícita mainWindowy es una instancia de clase MainWindow, que definiremos más adelante.

La forma en que definimos (vea la diferencia entre definir y declarar ) la mainWindowvariable la convierte en una variable automática (no es lo mismo que el automodificador de inferencia de tipo visto antes). Las variables automáticas se inicializan y asignan en la memoria en el punto de definición, y se desinicializan y desasignan automáticamente cuando finaliza su alcance (lea más sobre el alcance ). En una variable local, esto sucede al cerrar ‘}’ de su bloque de código. No es necesario que nos preocupemos por eliminar variables automáticas (ver, en cambio, Asignación de memoria dinámica con new y delete , o también asignación de memoria dinámica aquí en geeks for geeks ).

Siempre que el tipo de variable sea una clase (a diferencia de un tipo básico como int), se llama al constructor de la clase con los argumentos de construcción y devuelve la instancia inicializada. En este caso, los argumentos de construcción son el tamaño inicial de la ventana.

La llamada a runregresará cuando el usuario cierre la ventana. Esto le da al contexto de ejecución de Gtk la oportunidad de controlar el estado de salida .

La ventana básica de GtK

En Gtk , todos los botones, etiquetas, casillas de verificación y elementos que se muestran o interactúan con el usuario se denominan Widgets . Aquí hay una galería de Widgets .

Luego, gtkmm agrega su propia salsa:

gtkmm organiza los widgets jerárquicamente, utilizando contenedores. Un widget de contenedor contiene otros widgets. La mayoría de los widgets de gtkmm son contenedores. Windows, las pestañas de Notebook y los botones son widgets de contenedor. Hay dos tipos de contenedores: contenedores de un solo hijo, que son todos descendientes de Gtk::Bin, y contenedores de varios hijos, que son descendientes de Gtk::Container. La mayoría de los widgets en gtkmm son descendientes de Gtk::Bin, incluido Gtk::Window

Consulte la documentación oficial de Gtkmm .

MainWindowdeclaración de clase: el encabezado

class MainWindow : public Gtk::Windowdeclara MainWindowcomo una clase y como descendiente o heredero de Gtk::Window. El publicmodificador hace que todos los miembros públicos del antepasado sean también miembros públicos del descendiente. La herencia pública es la forma más habitual de extender una clase (a diferencia de la herencia privada ). Estamos proporcionando MainWindowal contexto de ejecución de Gtk , que espera que todas las funcionalidades de Gtk::Windowa estén disponibles, por lo que evitamos restringir su acceso.

Como descendiente de Gtk::Window, MainWindowes un contenedor de un solo hijo que solo puede contener un Gtk::Widget. Elijo que sea un Gtk::Box, que es un Gtk::Container, por lo que, a su vez, puede contener varios widgets . En esta ocasión quiero que sean uno Gtk::Buttony dos Gtk::Label, para ilustrar el mecanismo de Empaquetado que usa GtK para apilar juntos un montón de controles. Para MainWindowresponder a los clics en el botón, declaramos un buttonClickmétodo (podría ser cualquier otro nombre, pero manténgalo significativo). Más adelante explicaré cómo conectarle la señal de ‘clic’.

#ifndef MAIN_WINDOW_H
#define MAIN_WINDOW_H
  
#include <gtkmm.h>
  
class MainWindow : public Gtk::Window {
public:
    MainWindow(int width, int height);
    virtual ~MainWindow() = default;
  
private:
    void buttonClick();
    Gtk::Button m_button;
    Gtk::Box m_box;
    Gtk::Label m_label1, m_label2;
};
  
#endif

Los dos primeros métodos son el constructor y el destructor. Como se dijo antes, se llama a un constructor cada vez que declaramos una nueva instancia de esta clase. Este constructor acepta el ancho y alto inicial de la ventana como parámetros.

Cada vez que se destruye una instancia, se llama al destructor de clases para realizar todas las tareas de limpieza necesarias. Este destructor tiene dos modificadores importantes:

  • virtualsignifica que, en caso de herencia, el método llamado será el de tipo dinámico. Lea más sobre qué es un tipo de objeto dinámico , funciones virtuales y por qué la mayoría de las veces es una buena idea tener destructores virtuales .
  • defaultsignifica que definimos este destructor para que tenga una implementación predeterminada. La implementación predeterminada de un destructor es capaz de desasignar todos los miembros definidos de la clase, que son , m_boxy ( code>buttonClick es un método, por lo tanto, no necesita asignación ni desasignación). Al especificar la implementación predeterminada, nos ahorramos la necesidad de definir el destructor.m_buttonm_label1m_label2

El modelo de memoria de C++ no tiene recolector de basura . La reserva y liberación de memoria se basa en la presencia de constructores y destructores de clase y en una estrategia llamada Adquisición de recursos es inicialización, o RAII .

MainWindowdefinición de clase: la fuente

La declaración de MainWindowrequiere dos definiciones más: el constructor de la clase y el buttonClickmétodo. Aquí está el código:

#include "main-window.hpp"
#include <iostream>
  
MainWindow::MainWindow(int witdh, int height)
    : m_button("Hello World"), m_box(Gtk::ORIENTATION_VERTICAL), m_label1("First Label"), m_label2("Second Label")
{
    // Configure this window:
    this->set_default_size(witdh, height);
  
    // Connect the 'click' signal and make the button visible:
    m_button.signal_clicked().connect(
        sigc::mem_fun(*this, &MainWindow::buttonClick));
    m_button.show();
  
    // Make the first label visible:
    m_label1.show();
  
    // Make the second label visible:
    m_label2.show();
  
    // Pack all elements in the box:
    m_box.pack_start(m_button, Gtk::PACK_SHRINK);
    m_box.pack_start(m_label1, Gtk::PACK_SHRINK);
    m_box.pack_start(m_label2, Gtk::PACK_EXPAND_WIDGET);
  
    // Add the box in this window:
    add(m_box);
  
    // Make the box visible:
    m_box.show();
}
  
void MainWindow::buttonClick()
{
    std::cout << "Hello World" << std::endl;
}

El nombre completo del constructor es MainWindow::MainWindow, lo que significa que es la función llamada MainWindow(el nombre del constructor es el mismo que el de la clase) que pertenece a la clase MainWindow. Toma widthy heightcomo parámetros, y comienza llamando a los constructores de todos los miembros declarados (lea más sobre las listas de inicializadores de miembros constructores ).

Luego establecemos el tamaño predeterminado de la ventana llamando a this->set_default_size. El thises un puntero a la instancia de clase actual . Como es un puntero, lo usamos ->para acceder a sus miembros. A medida que la clase MainWindowse extiende Gtk::Window, esta instancia contiene todos los miembros públicos o protegidos de esta clase y los de la clase principal. Esto se llama herencia . El uso del thispuntero es en realidad opcional, y el siguiente fragmento también sería válido:

.

// Configure this window (now, without the 'this'):
set_default_size(witdh, height);

A continuación, queremos reaccionar al hacer m_buttonclic. Una forma de lograr esto es conectar una función a la señal de clic de la m_buttoninstancia (para ver todas las señales disponibles de un widget, puede consultar la documentación oficial, por ejemplo, el Gtk::Buttontiene solo la señal de clic ). La conexión de funciones a las señales del widget permite procesar el evento desde cualquier parte del código, siempre que tenga acceso al widget y pueda proporcionar la dirección de la función.

m_button.signal_clicked().connect([address of the method to call]);

La dirección de la función podría ser tan simple como colocar el nombre de la función (ver sobre la dirección de una función ). Nuestro caso es más complejo porque la función que queremos especificar es miembro de una clase. Las funciones de miembro deben proporcionarse con el thispuntero, para que pueda acceder a otros miembros de la misma instancia. Para construir el puntero a un método de una instancia específica, usamos sigc::mem_fun:

sigc::mem_fun(                    // Convert member function to function pointer.
   *this,                         // The address of the instance.
   &MainWindow::buttonClick   // The address of the method.
);

De forma predeterminada, los widgets no están visibles. Necesitamos establecer su visibilidad explícitamente llamando a show(). Hacemos esto para todos los widgets, incluido el m_box.

A continuación, empaquetamos todos los elementos en la caja. Durante la inicialización, ya configuramos el cuadro en Gtk::ORIENTATION_VERTICAL, lo que significa que los widgets se colocarán uno debajo del otro, utilizando todo el espacio horizontal disponible. pack_startagrega un nuevo widget al cuadro, Gtk::PACK_SHRINKespecifica que el tamaño vertical del widget debe ser lo más pequeño posible ( Gtk::ORIENTATION_VERTICALya especifica que todos los widgets en el cuadro deben tener un ancho lo más grande posible, por lo que el resultado serán widgets muy anchos y muy planos. )

Por el contrario, Gtk::PACK_EXPAND_WIDGETespecifica que el widget debe ocupar todo el espacio disponible. Esto hace m_label2que se expanda cuando el usuario expande la ventana (puede probar esto en breve).

El último paso es hacer que el m_boxmismo sea visible.

En cuanto al buttonClickmétodo, solo mostramos un mensaje en la salida estándar .

Aplicación de lanzamiento en XCode

Comience revisando el código fuente en alguna carpeta donde desee colocar el proyecto. Ingrese a la carpeta desprotegida y cree una xcodecarpeta que contendrá todos los datos de trabajo para Xcode . Ingrese a la carpeta de trabajo, cree el proyecto Xcode :

$ cd go-to-your-working-folder
$ git clone https://github.com/cpp-tutorial/raspberry-cpp-gtk-opencv.git
$ cd raspberry-cpp-gtk-opencv
$ mkdir xcode
$ cd xcode
$ cmake -G Xcode ../src
$ make

Ahora abra Xcode , desde el menú, haga un Archivo y Abrir y navegue hasta la xcodecarpeta, y abra un archivo llamado rascam.xcodeproj(observe que rascames el nombre del proyecto especificado en Cmake) . El navegador se abrirá, mostrando una carpeta por objetivo. Algunos de los objetivos, como ALL_BUILDy ZERO_CHECKson componentes internos de Xcode . Debería haber un objetivo llamado rascapp, que es el nombre del objetivo ejecutable en Cmake. Encontrará allí sus fuentes y encabezados. Abra cualquiera que desee y coloque un punto de interrupción. Luego, seleccione larascappdestino en la lista desplegable anodino en la barra de herramientas y, finalmente, jugar. Si todo está bien, la aplicación debería ejecutarse. Es posible que la ventana no esté en la parte superior, por lo que no se ve directamente.

Aplicación de lanzamiento en VisualStudio

Aplicación de lanzamiento en Linux

Clona el proyecto de ejemplo, configúralo con símbolos de depuración y constrúyelo:

$ cd go-to-your-working-folder
$ git clone https://github.com/cpp-tutorial/raspberry-cpp-gtk-opencv.git
$ cd raspberry-cpp-gtk-opencv
$ mkdir build
$ cd build
$ cmake -DCMAKE_BUILD_TYPE=Debug ../src
$ make

Para depurar con gdb , asumiendo que todavía estás en la buildcarpeta:

$ gdb ./rascapp
[Now you're in gdb]
b main-window.cpp:10  # Place a break-point on line 10 of this file.
run                   # Run the program. It will stop at the breakpoint.
where                 # It will show the stack trace
list                  # It will show some context.
print width           # Displays the value of this variable
n                     # Step over
s                     # Step into
c                     # To continue the program
q                     # Quit gdb 

Paso 2: un poco de refinamiento

Este segundo paso añade un par de pequeñas mejoras a la aplicación. El primero es reaccionar a eventos clave. Como la aplicación estará en la Raspberry Pi , y no siempre se podrá acceder a ella fácilmente con un mouse, puede ser útil tener algunos atajos de teclado, en particular, para alternar la ventana de tamaño completo o cerrar la aplicación. La segunda mejora es registrar eventos en el registro del sistema. Las aplicaciones que se ejecutan desde la línea de comandos pueden coutenviar mensajes que son directamente visibles. Sin embargo, si inicia su aplicación de ventana desde el escritorio de un usuario, no verá la consola ni el resultado de cout.

Iniciar sesión en syslog

Como las aplicaciones de ventana generalmente se inician desde el escritorio, el contenido producido coutno es fácilmente visible. En su lugar, puede enviar mensajes de syslog (vea también un ejemplo de syslog ). Es bastante fácil de hacer y funciona en todas las plataformas.

En Linux ( Ubuntu ), puede ver los registros del sistema a través de la aplicación Registro del sistema . En Mac OS X es la aplicación Consola . En Windows puedes usar el Visor de eventos .

Reaccionar a los eventos del teclado

Otra forma de reaccionar a los eventos es anular una función específica en el Widget, que se llama cada vez que ocurre ese evento. Por ejemplo, para reaccionar a los eventos del teclado en el MainWindowtenemos que anular el on_key_press_eventmétodo. Para anular una función en C++ :

  • La función tiene que ser declarada como virtualen uno de los ancestros de nuestra clase.
  • Necesitamos declararlo nuevamente, en nuestra clase, con exactamente la misma firma y accesibilidad.
  • Podemos agregar la overridepalabra clave en la declaración de la función, para reconocer el hecho de que estamos anulando un método existente de una clase principal. El compilador muestra un error cuando usamos overrideun método que no existe o no lo es virtual.

on_key_press_eventse declara en clase Gtk::Widgetde la siguiente manera:

/// This is a default handler for the signal signal_key_press_event().
virtual bool on_key_press_event(GdkEventKey* key_event);

En consecuencia, tenemos que volver a declararlo nuestra clase.

#ifndef MAIN_WINDOW_H
#define MAIN_WINDOW_H
  
#include <gtkmm.h>
  
#include "camera-drawing-area.hpp"
  
class MainWindow : public Gtk::Window {
public:
    MainWindow(int width, int height);
    virtual ~MainWindow() = default;
  
protected:
    bool on_key_press_event(GdkEventKey* event) override;
  
private:
    void buttonClick();
    bool probablyInFullScreen;
    Gtk::Button m_button;
    Gtk::Box m_box;
    Gtk::Label m_label1;
    CameraDrawingArea cameraDrawingArea;
};
  
#endif

El widget donde se origina el evento es el primero en recibirlo en su on_xx_xxx_event()controlador de eventos. Si devuelve true, el evento se considera manejado y no se hace nada más al respecto. Por el contrario, si el controlador no puede hacer nada con respecto al evento, vuelve a falsehacer que el evento se ofrezca al mismo controlador de su widget principal y al padre principal, y así sucesivamente (ver sobre la propagación de eventos en la documentación oficial). La implementación predeterminada de controladores como on_key_press_eventes no hacer nada más que devolver false, por lo que los eventos se propagan a menos que se especifique lo contrario.

[Ctrl] + [C] sale de la aplicación, [F] o [f] cambia el modo de pantalla completa y [esc] desactiva el modo completo:

#include "main-window.hpp"
#include <syslog.h>
  
MainWindow::MainWindow(int witdh, int height)
    : probablyInFullScreen(false), m_button("Hello World"), m_box(Gtk::ORIENTATION_VERTICAL), m_label1("First Label")
{
    // Configure this window:
    this->set_default_size(witdh, height);
  
    // Connect the 'click' signal and make the button visible:
    m_button.signal_clicked().connect(
        sigc::mem_fun(*this, &MainWindow::buttonClick));
    m_button.show();
  
    // Make the first label visible:
    m_label1.show();
  
    // Make the second label visible:
    cameraDrawingArea.show();
  
    // Pack all elements in the box:
    m_box.pack_start(m_button, Gtk::PACK_SHRINK);
    m_box.pack_start(m_label1, Gtk::PACK_SHRINK);
    m_box.pack_start(cameraDrawingArea, Gtk::PACK_EXPAND_WIDGET);
  
    // Add the box in this window:
    add(m_box);
  
    // Make the box visible:
    m_box.show();
  
    // Activate Key-Press events
    add_events(Gdk::KEY_PRESS_MASK);
}
  
void MainWindow::buttonClick()
{
    syslog(LOG_NOTICE, "Hello world!");
}
  
bool MainWindow::on_key_press_event(GdkEventKey* event)
{
    switch (event->keyval) {
    // Ctrl + C: Ends the app:
    case GDK_KEY_C:
    case GDK_KEY_c:
        if ((event->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK) {
            get_application()->quit();
        }
        return true;
  
    // [F] toggles fullscreen mode:
    case GDK_KEY_F:
    case GDK_KEY_f:
        if (probablyInFullScreen) {
            unfullscreen();
            probablyInFullScreen = false;
        }
        else {
            fullscreen();
            probablyInFullScreen = true;
        }
        return true;
  
    // [esc] exits fullscreen mode:
    case GDK_KEY_Escape:
        unfullscreen();
        probablyInFullScreen = false;
        return true;
    }
  
    return false;
}

De forma predeterminada, los eventos no se capturan. add_events(Gdk::KEY_PRESS_MASK)activa la captura de eventos del teclado.

Las funciones para alternar pantalla completa son fullscreen()y unfullscreen(), ambas pertenecen a la Gtk::Windowclase (aquí estamos omitiendo el thispuntero). La documentación advierte que no debe asumir que la ventana está definitivamente a pantalla completa después, porque otras entidades (por ejemplo, el usuario o el administrador de ventanas) podrían activarla o desactivarla nuevamente, y no todos los administradores de ventanas cumplen con esas requests. Es por eso que hemos agregado un probablyInFullScreenestado, que transmite la posibilidad de que la ventana no esté en el estado esperado.

Para salir de la aplicación no estamos llamando directamente exit(), sino que usamos get_application()->quit()para decirle al contexto de ejecución de Gtkexit que llame . Eventualmente lo hará, pero primero cerrará todas las ventanas y abrirá los dispositivos para nosotros.

Paso 3 – Captura de la cámara de visualización – Forma aproximada

Para capturar imágenes estamos usando OpenCV . Si solo desea capturar una imagen de la cámara, puede usar otras bibliotecas, pero el objetivo final es aplicar algoritmos de visión por computadora a la imagen capturada. Por lo tanto, OpenCV es la mejor alternativa.

Empaquetado de la aplicación Mac OS X

Por motivos de seguridad, Mac OS X obliga a las aplicaciones a solicitar el permiso del usuario antes de utilizar la cámara. El procedimiento para ello es incluir un Info.plistarchivo con el contenido necesario:

  • Algunas indicaciones sobre el número de versión y el autor.
  • Una clave NSCameraUsageDescriptioncon la descripción de por qué su aplicación necesita la cámara. El mensaje que escribes aquí se muestra al usuario, quien tiene que aceptarlo. Si no acepta, el sistema da por terminada su solicitud.

El procedimiento es incluir un MacOSXBundleInfo.plist.inarchivo, que es una plantilla; algunas de las entradas pueden estar gobernadas por propiedades en el CMakeLists.txtarchivo, y otras las puede poner usted mismo. El archivo se coloca en src/resla carpeta y tiene el siguiente contenido:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC 
    "-//Apple Computer//DTD PLIST 1.0//EN" 
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>English</string>
    <key>CFBundleExecutable</key>
    <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
    <key>CFBundleGetInfoString</key>
    <string>${MACOSX_BUNDLE_INFO_STRING}</string>
    <key>CFBundleIconFile</key>
    <string>${MACOSX_BUNDLE_ICON_FILE}</string>
    <key>CFBundleIdentifier</key>
    <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleLongVersionString</key>
    <string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
    <key>CFBundleName</key>
    <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleVersion</key>
    <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
    <key>CSResourcesFileMapped</key>
    <true/>
    <key>NSHumanReadableCopyright</key>
    <string>${MACOSX_BUNDLE_COPYRIGHT}</string>
    <key>NSCameraUsageDescription</key>
    <string>This app requires to access your camera to retrieve images and perform the demo</string>
</dict>
</plist>

Estos son algunos de los recursos que utilicé para descubrir cómo hacer paquetes en Mac OS X :

Vinculación a OpenCV

Después de instalar OpenCV siguiendo las instrucciones anteriores, puede vincular su aplicación a él en CMake .

# src/CMakeLists.txt
cmake_minimum_required(VERSION 3.3 FATAL_ERROR)

# Project name and current version
project(rascam VERSION 0.1 LANGUAGES CXX)

# Enable general warnings
# See http://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")

# Use 2014 C++ standard.
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Must use GNUInstallDirs to install libraries into correct locations on all platforms:
include(GNUInstallDirs)

# Pkg_config is used to locate header and files for dependency libraries:
find_package(PkgConfig)

# Defines variables GTKMM_INCLUDE_DIRS, GTKMM_LIBRARY_DIRS and GTKMM_LIBRARIES.
pkg_check_modules(GTKMM gtkmm-3.0) 
link_directories( ${GTKMM_LIBRARY_DIRS} )
include_directories( ${GTKMM_INCLUDE_DIRS} )

# OpenCV can be linked in a more standard manner:
find_package( OpenCV REQUIRED )

# Compile files:
add_executable(rascapp
    cpp/main.cpp
    cpp/main-window.cpp
    cpp/camera-drawing-area.cpp
)

# Add folder with all headers:
target_include_directories(rascapp PRIVATE hpp)

# Link files:
target_link_libraries(rascapp
    ${GTKMM_LIBRARIES}
    ${OpenCV_LIBS}
)

# Apple requires a bundle to add a Info.plist file that contains the required
# permissions to access some restricted resources like the camera:
if (APPLE)
    set_target_properties(rascapp PROPERTIES
        MACOSX_BUNDLE TRUE
        MACOSX_FRAMEWORK_IDENTIFIER org.cmake.ExecutableTarget
        MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/res/MacOSXBundleInfo.plist.in
    
        # This property is required:
        MACOSX_BUNDLE_GUI_IDENTIFIER "rascapp-${PROJECT_VERSION}"
    
        # Those properties are not required:
        MACOSX_BUNDLE_INFO_STRING "rascapp ${PROJECT_VERSION}, by jmgonet@agl-developpement.ch"
        MACOSX_BUNDLE_LONG_VERSION_STRING ${PROJECT_VERSION}
        MACOSX_BUNDLE_BUNDLE_NAME "rascapp"
        MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION}
        MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    )
endif()

Un widget gráfico

Gtk::DrawingAreaes un widget que contiene un área gráfica para mostrar dibujos personalizados o mapas de bits. Definimos un CameraDrawingAreaque extiende este widget y copia la imagen capturada de la cámara en el área gráfica:

#ifndef CAMERA_DRAWING_AREA_H
#define CAMERA_DRAWING_AREA_H
  
#include <gtkmm.h>
#include <opencv2/highgui.hpp>
  
class CameraDrawingArea : public Gtk::DrawingArea {
public:
    CameraDrawingArea();
    virtual ~CameraDrawingArea();
  
protected:
    bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
    void on_size_allocate(Gtk::Allocation& allocation) override;
  
    bool everyNowAndThen();
  
private:
    sigc::connection everyNowAndThenConnection;
    cv::VideoCapture videoCapture;
    cv::Mat webcam;
    cv::Mat output;
    int width, height;
};
#endif

Vamos a extender esta clase y anular dos de sus métodos:

  • void on_size_allocate(Gtk::Allocation& allocation)– Este método se llama cada vez que cambia el tamaño del widget. Esto sucede la primera vez que se muestra, y cada vez que alguna acción del usuario o del sistema hace que el tamaño cambie.
  • bool on_draw(const Cairo::RefPtr& cr)– Este método se llama cada vez que se debe redibujar el área, o parte del área, contenida en el widget. Recibe una referencia a un contexto cairota . El método puede usarlo para representar cualquier dibujo o gráfico. En nuestro caso lo vamos a utilizar para copiar la imagen capturada de la cámara.

cv::Mates una clase que contiene una imagen OpenCV . Estamos usando dos de ellos: uno contiene la imagen capturada de la cámara y el otro contiene la imagen redimensionada al tamaño actual del Widget.

widthy heightcontienen el tamaño actual del widget .

VideoCapturees un acceso OpenCV a la cámara de vídeo.

everyNowAndThenConnectiones una forma de llamar al everyNowAndThen()método a intervalos de tiempo regulares, de manera similar a como configuramos la respuesta al clic del botón en los pasos anteriores.

Este es el

#include "opencv2/core.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
  
#include "camera-drawing-area.hpp"
  
CameraDrawingArea::CameraDrawingArea()
    : videoCapture(0)
{
    // Lets refresh drawing area very now and then.
    everyNowAndThenConnection = Glib::signal_timeout().connect(sigc::mem_fun(*this, &CameraDrawingArea::everyNowAndThen), 100);
}
  
CameraDrawingArea::~CameraDrawingArea()
{
    everyNowAndThenConnection.disconnect();
}
  
/**
 * Every now and then, we invalidate the whole Widget rectangle,
 * forcing a complete refresh.
 */
bool CameraDrawingArea::everyNowAndThen()
{
    auto win = get_window();
    if (win) {
        Gdk::Rectangle r(0, 0, width, height);
        win->invalidate_rect(r, false);
    }
  
    // Don't stop calling me:
    return true;
}
  
/**
 * Called every time the widget has its allocation changed.
 */
void CameraDrawingArea::on_size_allocate(Gtk::Allocation& allocation)
{
    // Call the parent to do whatever needs to be done:
    DrawingArea::on_size_allocate(allocation);
  
    // Remember the new allocated size for resizing operation:
    width = allocation.get_width();
    height = allocation.get_height();
}
  
/**
 * Called every time the widget needs to be redrawn.
 * This happens when the Widget got resized, or obscured by
 * another object, or every now and then.
 */
bool CameraDrawingArea::on_draw(const Cairo::RefPtr<Cairo::Context>& cr)
{
  
    // Prevent the drawing if size is 0:
    if (width == 0 || height == 0) {
        return true;
    }
  
    // Capture one image from camera:
    videoCapture.read(webcam);
  
    // Resize it to the allocated size of the Widget.
    resize(webcam, output, cv::Size(width, height), 0, 0, cv::INTER_LINEAR);
  
    // Initializes a pixbuf sharing the same data as the mat:
    Glib::RefPtr<Gdk::Pixbuf> pixbuf = Gdk::Pixbuf::create_from_data(
        (guint8*)output.data,
        Gdk::COLORSPACE_RGB,
        false,
        8,
        output.cols,
        output.rows,
        (int)output.step);
  
    // Display
    Gdk::Cairo::set_source_pixbuf(cr, pixbuf);
    cr->paint();
  
    // Don't stop calling me.
    return true;
}

En el constructor, comenzamos por inicializar videoConstructorel dispositivo predeterminado. Luego configuramos una conexión para llamar al everyNowAndThenmétodo cada 100 ms (10 x por segundo). Posteriormente llamamos a la conexión para desconectarla durante el proceso de destrucción.

El everyNowAndThen()método invalida toda el área del widget. Esta es una forma indirecta de provocar una llamada a on_draw.

on_size_allocaterealiza un seguimiento del tamaño actual del widget. En caso de que la clase principal haya implementado algo importante en su propia implementación, llamamos al método principal.

Todo lo que ponemos en su lugar antes se on_drawllama así regularmente, y podemos usar la referencia Cairo::Contextpara pegar la imagen capturada de la cámara:

  • Primero comprobamos que las corrientes widthy heightno son cero. Esto puede suceder durante la fase de inicio. Llamar resizecon una dimensión 0 terminaría instantáneamente la aplicación, por lo que es mejor evitarlo.
  • videoCapture.read(webcam)copia la imagen capturada en la proporcionada cv::Mat, en este caso webcam. Si el material proporcionado no está inicializado o no tiene el tamaño adecuado, el método realizará las tareas necesarias y reservará la memoria necesaria.
  • resizecopia la imagen desde el origen ( webcam) al destino ( output), cambiando su tamaño. Si el destino no está inicializado o no tiene el tamaño adecuado, realiza las tareas necesarias y reserva de memoria. Para mantener la variable dentro del alcance, también la declaramos como una propiedad de clase: la primera llamada inicializará la variable, las próximas llamadas pueden simplemente reutilizarla.
  • Ambos webcamy outputson propiedades de clase, por lo que se mantienen dentro del alcance mientras lo haga la instancia de clase. Deberán inicializarse solo durante la primera llamada on_draw, pero mantendrán sus valores en las llamadas posteriores. Esto ayuda notablemente al rendimiento.
  • create_from_datacrea un nuevo Gdk::Pixbufobjeto, que contiene una imagen en un formato compatible con Cairo . La larga lista de parámetros debe tomarse como una receta para convertir OpenCV ‘s Materiala Gtk ‘s Pixbuf.
  • set_source_pixbufestablece la fuente de mapa de bits que se usará para la próxima llamada apaint
  • Finalmente, painthace la pintura.

Colocar el área de dibujo

El último paso es colocar el widget que acabamos de crear en la ventana principal. Necesitamos declararlo en el MainWindowencabezado:

#ifndef MAIN_WINDOW_H
#define MAIN_WINDOW_H
  
#include <gtkmm.h>
  
#include "camera-drawing-area.hpp"
  
class MainWindow : public Gtk::Window {
public:
    MainWindow(int width, int height);
    virtual ~MainWindow() = default;
  
protected:
    bool on_key_press_event(GdkEventKey* event) override;
  
private:
    void buttonClick();
    bool probablyInFullScreen;
    Gtk::Button m_button;
    Gtk::Box m_box;
    Gtk::Label m_label1;
    CameraDrawingArea cameraDrawingArea;
};
  
#endif

Y colócalo en la caja, igual que una etiqueta o un botón:

#include <syslog.h>
#include <unistd.h>
  
#include "main-window.hpp"
  
MainWindow::MainWindow(int width, int height)
    : probablyInFullScreen(false), m_button("Hello World"), m_box(Gtk::ORIENTATION_VERTICAL), m_label1("First Label")
{
    // Configure this window:
    this->set_default_size(width, height);
  
    // Connect the 'click' signal and make the button visible:
    m_button.signal_clicked().connect(
        sigc::mem_fun(*this, &MainWindow::buttonClick));
    m_button.show();
  
    // Make the first label visible:
    m_label1.show();
  
    // Make the second label visible:
    cameraDrawingArea.show();
  
    // Pack all elements in the box:
    m_box.pack_start(m_button, Gtk::PACK_SHRINK);
    m_box.pack_start(m_label1, Gtk::PACK_SHRINK);
    m_box.pack_start(cameraDrawingArea, Gtk::PACK_EXPAND_WIDGET);
  
    // Add the box in this window:
    add(m_box);
  
    // Make the box visible:
    m_box.show();
  
    // Activate Key-Press events
    add_events(Gdk::KEY_PRESS_MASK);
}
  
void MainWindow::buttonClick()
{
    syslog(LOG_NOTICE, "User %d says 'Hello World'", getuid());
}
  
bool MainWindow::on_key_press_event(GdkEventKey* event)
{
    switch (event->keyval) {
    // Ctrl + C: Ends the app:
    case GDK_KEY_C:
    case GDK_KEY_c:
        if ((event->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK) {
            get_application()->quit();
        }
        return true;
  
    // [F] toggles fullscreen mode:
    case GDK_KEY_F:
    case GDK_KEY_f:
        if (probablyInFullScreen) {
            unfullscreen();
            probablyInFullScreen = false;
        }
        else {
            fullscreen();
            probablyInFullScreen = true;
        }
        return true;
  
    // [esc] exits fullscreen mode:
    case GDK_KEY_Escape:
        unfullscreen();
        probablyInFullScreen = false;
        return true;
    }
  
    return false;
}

Instalación en Raspberry Pi

Por último, pero no menos importante, debe instalar el proyecto en una Raspberry Pi .

Activar cámara

La forma más sencilla es hacerlo desde el escritorio:

  1. Frambuesa –> Preferencias
  2. Abra la pestaña Interfaces .
  3. Habilitar la cámara
  4. Para comprobar que la cámara funciona, escriba:

    $raspistill -f -t 0
    

    Debería poder ver las imágenes de la cámara. Salga con Ctrl+C .

    Controlador v4L2

    En Raspberry , el controlador v4L2 ofrece la interfaz estándar para la cámara que OpenCV necesita para capturar imágenes. El controlador está instalado de forma predeterminada, pero no está activo. Para activarlo:

    $sudo modprobe bcm2835-v4l2
    $ls /dev/video0
    

    Si el dispositivo /dev/vide0está presente, significa que el controlador está activo. Para activarlo de forma predeterminada cuando se inicia Raspberry, edite el archivo /etc/modulesy agregue el siguiente fragmento en la parte inferior:

    bcm2835-v4l2
    

    Instalar herramientas

    Para poder descargar y compilar el proyecto, necesita las mismas herramientas que en su plataforma de desarrollo: Git , Cmake , pkg-config y las bibliotecas Gtk y OpenCV . Para instalarlos, siga el procedimiento para el entorno Linux , en este mismo artículo.

    Ejecución automática

    Es posible que desee que Raspberry Pi inicie su aplicación al inicio. La forma más fácil que encontré para ejecutar automáticamente una aplicación GUI en Raspberry es crear un archivo de escritorio en el autostartdirectorio. En una instalación completamente nueva, este directorio no existe y debe crearlo:

    $mkdir ~/.config/autostart
    $vim ~/.config/autostart/rascapp.desktop
    

    El contenido de «rascapp.desktop» debe ser similar a:

    [Desktop Entry]
    Name=raspberry-pi-camera-display
    Exec=/home/pi/raspberry-cpp-gtk-opencv/build/rascapp
    Path=/home/pi
    Type=application
    

    La Pathclave es la carpeta raíz de la aplicación, cuando se accede a los archivos por una ruta relativa.

  • Vea más sobre la sintaxis aquí:
  • Ver explicación original aquí:

Evita que la consola quede en blanco

El espacio en blanco de la consola lo afecta si está usando ssh para ejecutar comandos. Si durante algún tiempo no escribe nada
en la consola, se cerrará.

Para evitarlo, edite el archivo /boot/cmdline.txty agregue el parámetroconsoleblank=0

Vea la explicación original aquí: https://www.raspberrypi.org/documentation/configuration/screensaver.md

Evitar la pantalla inactiva

Las aplicaciones de solo visualización normalmente no tendrán interacción con el usuario, por lo que la pantalla puede quedar inactiva y dejarlo a oscuras. Para evitar esto, el enfoque más simple es instalar un protector de pantalla y luego configurarlo para que NO se ejecute:

$ sudo apt-get install xscreensaver

Después de esto, la aplicación de salvapantallas se encuentra en Preferencias , en el menú del escritorio. Utilice las opciones apropiadas para evitar el protector de pantalla.

Habilitación de salida de video compuesto

Si está colocando la Raspberry a bordo de algún dispositivo móvil con un transmisor FPV , probablemente quiera usar la salida de video analógico. Edite el /boot/config.txtarchivo y modifique las entradas de la siguiente manera:

...
sdtv_mode=2
...
hdmi_force_hotplug=0
...

Cuando hdmi_force_hotplugse establece en 1, el sistema asume que hay un dispositivo HDMI presente, por lo
que nunca activa la salida de video compuesto.

Cuando hdmi_force_hotplugse establece en 0, el sistema utilizará video compuesto a menos que detecte un dispositivo HDMI. Entonces,
Raspberry seguirá usando el monitor HDMI si está conectado durante la secuencia de arranque.

Ver más sobre esto:

Solución de problemas

Espero que no necesites esta sección.

Gtk-ERROR **: Símbolos GTK+ 2.x detectados.

Si recibe este mensaje de error:

Gtk-ERROR **: GTK+ 2.x symbols detected. 
Using GTK+ 2.x and GTK+ 3 in the same process is not 
supported

Puede vincular accidentalmente OpenCV con Gtk2 . Como este proyecto usa Gtk3 , hay un conflicto. Debe asegurarse de que OpenCV esté vinculado con Gtk3 .

Comience por desinstalar Gtk2 y asegúrese de que Gtk3 esté presente:

$ sudo apt-get remove libgtk2.0-dev
sudo apt-get install libgtk-3-dev
sudo apt-get auto-remove
sudo apt-get install libgtkmm-3.0-dev

Construir de nuevo OpenCV :

  • Si aún no lo ha hecho, elimine el directorio de compilación de OpenCV .
  • Comience de nuevo el procedimiento de construcción como se describe arriba PERO…
  • Antes de iniciar make, verifique el cmakeregistro y verifique que la versión de Gtk esté vinculada. Busque algo como esto:
...
--   GUI: 
--     GTK+:                        YES (ver 3.22.11)
--       GThread :                  YES (ver 2.50.3)
--       GtkGlExt:                  NO
--     VTK support:                 NO
...

Conclusión

Este ejemplo es una prueba de concepto funcional sobre cómo escribir una aplicación multiplataforma basada en dos bibliotecas muy populares, OpenCV para procesar la visión artificial y Gtk para la interfaz de usuario. Sin embargo, hay una lista de deficiencias que deben abordarse antes de poder utilizarlo como base para la aplicación más compleja que pueda tener en mente:

  • El más visible es que la imagen se deforma para que coincida con el tamaño de la ventana. Esto se puede resolver fácilmente utilizando un algoritmo más sofisticado para calcular un tamaño que quepa en la ventana pero manteniendo la relación de aspecto original.
  • Una segunda, muy molesta si planea una aplicación de vista en primera persona , es el retraso perceptible entre la vida real y la transmisión de imágenes. El retraso parece variable según la cámara y las condiciones de luz. La razón es que las cámaras tienen un búfer de imagen; como estamos recuperando una imagen de vez en cuando, siempre estamos consumiendo la imagen más antigua en el búfer. Para resolver esto, debemos dejar que el proceso de captura controle la actualización de la ventana, en lugar de usar un temporizador.
  • El código tiene una falta general de desacoplamiento que hace que todo dependa mucho de todo lo demás. Este defecto no está muy presente en una aplicación tan sencilla, pero se notará en cuanto intentemos solucionar el problema del lag. Además, parte del código debe ser un proceso puro de OpenCV que podría copiar/pegar de algún otro blog, sin necesidad de adaptarlo al CameraDrawingAreaque depende de las bibliotecas OpenCV y Gtk .
  • Finalmente, debería poder realizar pruebas unitarias del procesamiento de OpenCV para aumentar el ciclo de tipo-compilación-depuración.

Mi próximo artículo cubre esos temas.

Publicación traducida automáticamente

Artículo escrito por jmgonet y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *