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.
- 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.
- Ve a visitar https://brew.sh/
- Seguir instrucciones
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 :
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:
- Texto sublime
- Los verdaderos geeks incondicionales pueden configurar Vim para que sea un IDE .
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:
- Programación en C en Linux Tutorial #056 – Depurador GDB (1/2)
- Introducción rápida a gdb
- CppCon 2015: Greg Law «Dame 15 minutos y cambiaré tu visión de GDB»
- guía de autoestopistas a la gdb
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 :
- https://arne-mertz.de/2018/06/cmake-project-structure/ : Me gusta este porque analiza cómo integrar esas bibliotecas de solo encabezado y usa Catch como ejemplo.
- https://rix0r.nl/blog/2015/08/13/cmake-guide/ : Me gusta este porque muestra en detalle cómo configurar CMake y por qué, y también reconoce la diferencia entre la biblioteca (que es fácilmente probada por unidad ) y la aplicación (que es la interfaz de usuario y no es muy fácil de probar).
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 src
y, 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_required
requiere 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.project
declara el nombre del proyecto globalrascam
, 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 acpp
ocxx
. En CMake ,CXX
significa C++ .set(CMAKE_CXX...)
establece algunas banderas que son apropiadas para C++ moderno .find_package(PkgConfig)
enlaces alpkg-config
comando, para que podamos usarlo más adelante.pkg_check_modules(GTKMM gtkmm-3.0)
hace lo mismo que ejecutarpkg-config gtkmm-3.0
en la terminal (pruébelo, si lo desea), y luego copia cada sección de la respuesta en variables con el prefijo especificadoGTKMM
. Por lo tanto, después de este comando tenemos tres variables llamadasGTKMM_LIBRARY_DIRS
,GTKMM_INCLUDE_DIRS
yGTKMM_LIBRARIES
.- Entonces declaramos un objetivo. Un proyecto puede tener varios objetivos. En general, los objetivos comprenden ejecutables o bibliotecas que se definen llamando
add_executable
oadd_library
y 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 elCMakeLists.txt
archivo principal. Enumeramos los archivos de origen para compilar para crear el destino. target_include_directories
establece la lista de carpetas donde buscar los archivos referidos en las fuentes comoinclude "..."
.target_link_libraries
establece 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:
- Cómo funciona C++: comprensión de la compilación
- Stack Overflow: ¿cómo funciona el proceso de compilación/vinculació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 auto
especifica 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::create
inicializa 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 mainWindow
y es una instancia de clase MainWindow
, que definiremos más adelante.
La forma en que definimos (vea la diferencia entre definir y declarar ) la mainWindow
variable la convierte en una variable automática (no es lo mismo que el auto
modificador 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 run
regresará 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 .
MainWindow
declaración de clase: el encabezado
class MainWindow : public Gtk::Window
declara MainWindow
como una clase y como descendiente o heredero de Gtk::Window
. El public
modificador 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 MainWindow
al contexto de ejecución de Gtk , que espera que todas las funcionalidades de Gtk::Window
a estén disponibles, por lo que evitamos restringir su acceso.
Como descendiente de Gtk::Window
, MainWindow
es 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::Button
y dos Gtk::Label
, para ilustrar el mecanismo de Empaquetado que usa GtK para apilar juntos un montón de controles. Para MainWindow
responder a los clics en el botón, declaramos un buttonClick
mé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:
virtual
significa 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 .default
significa 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_box
y ( 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_button
m_label1
m_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 .
MainWindow
definición de clase: la fuente
La declaración de MainWindow
requiere dos definiciones más: el constructor de la clase y el buttonClick
mé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 width
y height
como 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 this
es un puntero a la instancia de clase actual . Como es un puntero, lo usamos ->
para acceder a sus miembros. A medida que la clase MainWindow
se 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 this
puntero 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_button
clic. Una forma de lograr esto es conectar una función a la señal de clic de la m_button
instancia (para ver todas las señales disponibles de un widget, puede consultar la documentación oficial, por ejemplo, el Gtk::Button
tiene 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 this
puntero, 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_start
agrega un nuevo widget al cuadro, Gtk::PACK_SHRINK
especifica que el tamaño vertical del widget debe ser lo más pequeño posible ( Gtk::ORIENTATION_VERTICAL
ya 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_WIDGET
especifica que el widget debe ocupar todo el espacio disponible. Esto hace m_label2
que se expanda cuando el usuario expande la ventana (puede probar esto en breve).
El último paso es hacer que el m_box
mismo sea visible.
En cuanto al buttonClick
mé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 xcode
carpeta 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 xcode
carpeta, y abra un archivo llamado rascam.xcodeproj
(observe que rascam
es el nombre del proyecto especificado en Cmake) . El navegador se abrirá, mostrando una carpeta por objetivo. Algunos de los objetivos, como ALL_BUILD
y ZERO_CHECK
son 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 larascapp
destino 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 build
carpeta:
$ 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 cout
enviar 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 cout
no 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 MainWindow
tenemos que anular el on_key_press_event
método. Para anular una función en C++ :
- La función tiene que ser declarada como
virtual
en uno de los ancestros de nuestra clase. - Necesitamos declararlo nuevamente, en nuestra clase, con exactamente la misma firma y accesibilidad.
- Podemos agregar la
override
palabra 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 usamosoverride
un método que no existe o no lo esvirtual
.
on_key_press_event
se declara en clase Gtk::Widget
de 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 false
hacer 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_event
es 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::Window
clase (aquí estamos omitiendo el this
puntero). 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 probablyInFullScreen
estado, 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.plist
archivo con el contenido necesario:
- Algunas indicaciones sobre el número de versión y el autor.
- Una clave
NSCameraUsageDescription
con 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.in
archivo, que es una plantilla; algunas de las entradas pueden estar gobernadas por propiedades en el CMakeLists.txt
archivo, y otras las puede poner usted mismo. El archivo se coloca en src/res
la 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 :
- Una documentación oficial, bastante antigua, sobre Bundles-And-Frameworks
- La plantilla Info.plist original .
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::DrawingArea
es un widget que contiene un área gráfica para mostrar dibujos personalizados o mapas de bits. Definimos un CameraDrawingArea
que 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::Mat
es 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.
width
y height
contienen el tamaño actual del widget .
VideoCapture
es un acceso OpenCV a la cámara de vídeo.
everyNowAndThenConnection
es 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 videoConstructor
el dispositivo predeterminado. Luego configuramos una conexión para llamar al everyNowAndThen
mé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_allocate
realiza 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_draw
llama así regularmente, y podemos usar la referencia Cairo::Context
para pegar la imagen capturada de la cámara:
- Primero comprobamos que las corrientes
width
yheight
no son cero. Esto puede suceder durante la fase de inicio. Llamarresize
con 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 proporcionadacv::Mat
, en este casowebcam
. 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.resize
copia 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
webcam
youtput
son 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 llamadaon_draw
, pero mantendrán sus valores en las llamadas posteriores. Esto ayuda notablemente al rendimiento. create_from_data
crea un nuevoGdk::Pixbuf
objeto, que contiene una imagen en un formato compatible con Cairo . La larga lista de parámetros debe tomarse como una receta para convertir OpenCV ‘sMaterial
a Gtk ‘sPixbuf
.set_source_pixbuf
establece la fuente de mapa de bits que se usará para la próxima llamada apaint
- Finalmente,
paint
hace 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 MainWindow
encabezado:
#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:
- Frambuesa –> Preferencias
- Abra la pestaña Interfaces .
- Habilitar la cámara
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/vide0
está presente, significa que el controlador está activo. Para activarlo de forma predeterminada cuando se inicia Raspberry, edite el archivo /etc/modules
y 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 autostart
directorio. 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 Path
clave 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.txt
y 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.txt
archivo y modifique las entradas de la siguiente manera:
... sdtv_mode=2 ... hdmi_force_hotplug=0 ...
Cuando hdmi_force_hotplug
se 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_hotplug
se 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:
- Artículo original: Forzar la salida de Raspberry Pi a video compuesto en lugar de HDMI
- Documento oficial: Configurar video compuesto
- Más oficial: Configuración de la Raspberry Pi
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 elcmake
registro 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
CameraDrawingArea
que 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.