viernes, 12 de agosto de 2016

Nueva etapa del blog: cambio del nombre y descripción

Voy a empezar una nueva etapa en el blog, reviviéndolo y llenándolo de más y más ejemplos, tutoriales y todo tipo de recursos útiles para CREAR tecnología. Acepto colaboradores, ideas y sugerencias.

Al mismo tiempo he creado un canal de Youtube en el que subir todos los aportes, separándolo de mi canal personal y permitiendo de esta manera que aquellas personas que quieran colaborar puedan hacerlo: https://www.youtube.com/channel/UCu0keBMUoqmWjsE4TTG5GKw

Carátula del nuevo canal de Youtube - Crear

Durante un tiempo evité tocar temas de electrónica, manualidades y otros temas tangenciales a la robótica, pero en esta nueva etapa hablaremos de todo... todo lo que pueda ser de utilidad para que nuestros proyectos funcionen.

Espero que no sea un inconveniente, pero esta nueva etapa me consumirá más tiempo, recursos (tendré que comprar materiales, probablemente destruya o se estropeen y ello requiere financiación (aunque sea mínima) mediante publicidad. He agregado dos anuncios no intrusivos en el blog, soy contrario a los anuncios intrusivos (pantalla completa, superpuestos a los artículos, etc).

Sin más, os animo a comentar mis entradas, ésta incluida, comentando todo aquello que os parezca oportuno y, sobre todo, proponiéndome temas de los que hablar, poner ejemplos o hacer tutoriales.

Gracias a todos los que leen y participan en este blog. A partir de ahora pasáis a ser "CREADORES".

jueves, 10 de abril de 2014

Detector de imagen en negro usando OpenCV

Hace unos días un seguidor del blog me mandó un mensaje por hangouts preguntándome cómo podría detectar si una imagen está negra o no. Rápidamente le propuse una solución (de las muchas posibles). Animado por continuar con el blog decidí implementarlo.

Este es el resultado:

A diferencia de los primeros artículos de este blog, ahora programo mayoritariamente en C++ debido a la influencia que ha ejercido sobre mí la plataforma robótica JdeRobot y el middleware ZeroC ICE (Internet Communications Engine).

Debido a este cambio tendré que hacer un breve repaso de las bases de obtención de fotogramas en C++ que, básicamente, no dista mucho de cómo se hace en C.

Para empezar no se usa una estructura del tipo "IplImage" para almacenar una imagen. En su lugar se usa clase para tratamiento de matrices genérica llamada "Mat". La conversión entre ambos tipos de imágenes es muy sencillo y casi directo.

Otra diferencia significativa aparece a la hora de configurar el dispositivo de captura. En vez de usar la estructura "CvCapture" para almacenar el resultado de la llamada a "cvCaptureFromCAM" usaremos una clase llamada "VideoCapture" cuyo constructor permite seleccionar la cámara de manera directa (facilita el código y su lectura).

Una vez abierto el dispositivo de captura (ésto último se comprueba usando el método "isOpened") podremos obtener imágenes del dispositivo usando el operador sobrecargado ">>".

Por último las funciones C "cvNamedWindow" y "cvShowImage" son sustituidos por los métodos "namedWindow" e "imshow".

Como podéis ver no me gusta usar "namespaces" para ahorrarme usar los prefijos "cv::", "std::", etc. Esto se debe a que no me gusta olvidar qué implementa el método que estoy llamando. Usar "namespaces" complica o dificulta la lectura del código o reconocer fácilmente métodos ajenos y quién los provee.

Una vez explicadas las diferencias básicas entre la implementación C y C++ nos centraremos en el funcionamiento del detector.

Primero convertimos el fotograma obtenido a escala de grises (un único canal con datos enteros sin signo de 8 bits, ):

        // Convertimos el fotograma a escala de grises
        cv::cvtColor(fotograma, grises, CV_BGR2GRAY);

Con esto facilitaremos la interpretación de la imagen. El histograma es un recuento de cuántos puntos de una imagen tienen el mismo valor para cada uno de sus canales (las imágenes en color tienen tres o cuatro canales, uno azul, otro verde, otro rojo y un último canal opcional que suele usarse para representar la transparencia de dicho punto).

Si hiciéramos el histograma de cada uno de los canales de una imagen en color (R, G, B o bien B, G, R en el orden que prefiere OpenCV) no tendríamos información fiable de la oscuridad de la imagen ya que una imagen que no contiene tonos azules no significa que sea oscura, podría ser un rojo, un verde o una mezcla de ambos que haría fallar nuestro algoritmo. Hacer un cruce de información entre canales complicaría el código y nos haría perder tiempo de proceso que es precioso en el procesamiento de imágenes en tiempo real (25 imágenes por segundo o más).

Una solución sería convertir la imagen a matiz, saturación y brillo (HSV) y calcular el histograma sólo del canal de brillo... pero es que precisamente el canal de brillo coincide con la representación en escala de grises de una imagen, en los que cada punto está representado por su brillo y por ninguna otra característica de color.

Una vez explicado el motivo por el que se usa una imagen en escala de grises seguimos con el cálculo del histograma de la imagen:

        // Rango de los valores del canal de escala de grises (0 mínimo, 256 máximo)
        float range[] = { 0, 256 };
        // Como sólo tenemos un canal (segundo parámetro) sólo ponemos un elemento
        const float* histRange = { range };
        // Podríamos "cuantizar" a un reducido número de valores para acelerar el algoritmo
        int histSize = 256;
        // Calculamos el histograma
        cv::calcHist(&grises, 1, 0, cv::Mat(), histograma, 1, &histSize, &histRange);

El parámetro "range" (los nombres están en inglés debido a que obtuve el código con un copiar/pegar de la de OpenCV que explica el cálculo de histogramas) limita el rango máximo y mínimo de los valores que contiene el vector. Teóricamente bastaría con poner { 0, 255 } ya que cada punto en una imagen de escala de grises está representado por un número entero de 8 bits que está comprendido entre 0 y 255.

Dejando ese detalle de lado, si pusiéramos en ese rango { 0, 20 } ahorraríamos memoria y tiempo de proceso ya que posteriormente sólo usaremos los primeros 20 valores para averiguar si una imagen es negra o no, pero lo he dejado completo para poder dibujar el histograma completo sobreimpreso en el cada fotograma.

El parámetro histRange tiene un valor por cada canal de la imagen, de modo que podemos tener un rango diferente para cada canal. En nuestro caso sólo tenemos uno, por lo que el array sólo tiene una dimensión y su contenido es el rango anteriormente definido.

El parámetro histSize define el tamaño final que tendrá el histograma. Si hemos dicho que el rango va de 0 a 256 y ponemos un valor de 256 entonces prácticamente no se hará nada (¿un valor se perdería en el camino?), pero si ponemos en histSize un valor de, por ejemplo, 32 entonces el rango de valores de entrada de 0 a 256 se convertirían ("cuantizarían" si castellanizamos el término anglosajón "quantize", o "reducir" si usamos lenguaje algo más coloquial) en tan sólo 32 valores comprendidos del 0 al 31, por lo que un valor 0 de la imagen de entrada se contabilizará en la posición 0 de salida, y un valor 255 de la imagen de ésta contará como un valor en la posición 31 del array de salida, al igual que lo harían los valores 254 y, posiblemente, el valor 253. La documentación no especifica el tipo de redondeo que se usa para convertir un valor de entrada a una posición del array de salida.

(en construcción)

martes, 8 de abril de 2014

Jugando con los fotogramas entregados por la cámara de nuestro Android (paso 10)

Una vez que hemos llegado aquí es hora de jugar con nuestra librería de visión artificial. Nada mejor que un filtro Canny para mostrar el funcionamiento de OpenCV en nuestra aplicación Android.

 /* Trabajamos con el fotograma obtenido */ 
 public Mat onCameraFrame(Mat inputFrame) {
  Imgproc.Canny(inputFrame, mFotogramaGrises, 10, 80);
  return mFotogramaGrises;
 }

Por cada fotograma obtenido de la cámara se llamará al evento (método de "callback" o "listener") "onCameraFrame" con nuestro fotograma como parámetro.

En el código hemos realizado lo siguiente:

  • Usamos el filtro Canny que nos ofrece la librería de procesamiento de imágenes (Imgproc) para convertir nuestro fotograma de entrada (inputFrame) en una imagen en escala de grises que contiene la imagen tratada. Los parámetros del filtro Canny son los umbrales 10 y 80 (muy genéricos, podemos cambiarlos a gusto).
  • Devolvemos la imagen obtenida al aplicar el filtro Canny (mFotogramaGrises) en vez de la imagen que nos entregó la cámara (inputFrame). Esto provocará que nuestra vista muestre el resultado del filtro.

Realmente no hubiera sido necesario crear la imagen mFotogramaGrises, podríamos haber aplicado el filtro sobre la misma imagen, pero de esa manera no hubieramos podido mostrar las ventajas de reserva previa de memoria para imágenes de trabajo y cómo se devuelve una imagen diferente a la obtenida.

Resultado:

Filtro Canny en un dispositivo Android Sony Xperia Neo V
Captura de pantalla del resultado de filtro Canny con OpenCV en Android

Preparando nuestra actividad para recibir los eventos relacionados con la cámara (pasos 8 y 9)

Dejamos el onCreate tal y como se muestra:

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_principal);
  /* Apuntamos a la CameraView que se muestra en la vista de la actividad */
  mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.activity_main_surface_view);
  mOpenCvCameraView.setCvCameraViewListener(this);
 }

Lo que hacemos es lo siguiente:

Ahora implementaremos los eventos "onCameraViewStarted" y "onCameraViewStopped". Éstos nos permitirán crear aquellas superficies de trabajo que necesitemos una única vez cada vez que vayamos a usar la cámara.

Es mejor crear las superficies con las que vayamos a trabajar una única vez y no cada vez que obtengamos un fotograma. De esta manera nuestra aplicación hará un uso menor de la pila (heap) y, por lo tanto, se llamará con menos frecuencia al recolector de basura consiguiendo que nuestra aplicación tenga menos "tirones".

 /* Creamos la superficie (Mat) para almacenar los fotogramas de trabajo (por ahora sólo uno) */
 public void onCameraViewStarted(int width, int height) {
  mFotogramaGrises = new Mat(height, width, CvType.CV_8UC1);
 }

 /* Liberamos los recursos utilizados por las superficies de trabajo (por ahora sólo una) */
 public void onCameraViewStopped() {
  mFotogramaGrises.release();
 }

Con este pequeño código crearemos una imagen donde guardar el resultado de nuestro tratamiento de imagen (será un simple filtro Canny, una imagen en escala de grises de un único canal de 8 bits) cada vez que comencemos a usar la cámara y liberará los recursos asociados cada vez que se pare.

En la próxima entrega, por fin, jugaremos con los fotogramas que nos estregue la cámara.

martes, 8 de enero de 2013

Carga de la librería OpenCV en tiempo de ejecución y eventos onResume, onPause y onDestroy (pasos 6 y 7)

Implementar la función de callback de la carga asíncrona de la librería OpenCV

Un cambio importante introducido desde que usaba la versión 2.4.1 a la versión que estamos usando en estos ejemplos, la 2.4.3, ha sido la carga asíncrona de la librería OpenCV. Esto es debido a que ahora las librerías se encuentran concentradas y centralizadas en una aplicación de "Google Play".

La principal ventaja que tiene este nuevo sistema es que las aplicaciones que usan OpenCV ya no son tan pesadas (no contienen todas las librerías OpenCV dentro de ellas para su distribución). Antes tener dos aplicaciones OpenCV instaladas en el móvil requería tener en cada una de ellas las librerías, ocupando el doble de espacio (o el triple si fueran tres aplicaciones las que usan OpenCV).

Por otro lado tenemos la desventaja de que todas aquellas aplicaciones que hayamos desarrollado previamente con OpenCV van a necesitar ciertas modificaciones para que funcionen con las versiones actuales de OpenCV.

En el manual oficial de OpenCV disponemos del proceso de inicialización de la librería y/o instalación de todo lo necesario:

http://docs.opencv.org/android/service/doc/BaseLoaderCallback.html

Crearemos, bajo la definición de "mOpenCvCameraView", una nueva propiedad, que llamaremos "mLoaderCallback", que será la encargada de contener una nueva clase del tipo "BaseLoaderCallback" cuya función es activar la vista "mOpenCvCameraView" cuando la librería OpenCV termine su carga:

 /* Creamos el cargador de OpenCV Manager que usaremos más adelante en OpenCVLoader.initAsync */
 private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS:
                {
                    mOpenCvCameraView.enableView();
                } break;
                default:
                {
                    super.onManagerConnected(status);
                } break;
            }
        }
    };

Al teclear el código anterior se resaltará la clase "BaseLoaderCallback" en rojo indicando que ha aparecido un error. Para solucionarlo pulsamos en "Import 'BaseLoaderCallback' (org.opencv.android)":

Localización archivo activity_main.xml

Por último, en el case, habrá un error en "LoaderCallbackInterface" en rojo indicando que no reconoce la clase cuya propiedad "SUCCESS" intentamos obtener. Para solucionar este error pulsamos en "Import 'LoaderCallbackInterface' (org.opencv.android)":

Localización archivo activity_main.xml

Ahora deberemos programar en "onResume" la inicialización asíncrona de la librería OpenCV. Esto conseguirá que se inicialice no sólo durante la carga inicial de la actividad, si no también cuando la volvamos a poner en primer plano o tras apagar la pantalla del móvil:

    /* Cuando cargue la actividad deberemos realizar una inicialización asíncrona de OpenCV */
    @Override
    public void onResume()
    {
        super.onResume();
        OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_3, this, mLoaderCallback);
    }

También aprovecharemos para implementar los métodos onPause y onDestroy para que deshabiliten la vista cuando la actividad pierda el foco, apaguemos la pantalla, etc:

    /* Desactivamos la vista cada vez que la actividad se pause o se destruya */
    @Override
    public void onPause()
    {
        if (mOpenCvCameraView != null) {
            mOpenCvCameraView.disableView();
        }
        super.onPause();
    }
    
    public void onDestroy() {
        super.onDestroy();
        if (mOpenCvCameraView != null) {
            mOpenCvCameraView.disableView();
        }
    }

También aprovecharemos para implementar los métodos onPause y onDestroy para que deshabiliten la vista cuando la actividad pierda el foco, apaguemos la pantalla, etc:

sábado, 5 de enero de 2013

Preparando la actividad para recibir fotogramas de la cámara (paso 5)

Configurar nuestra Actividad para que implemente CvCameraViewListener

Para que nuestra actividad reciba eventos asíncronos cada vez que reciba un fotograma de la cámara deberemos modificar dos cosas en nuestra actividad.

La primera es agregar a nuestra actividad un elemento llamado "NativeCameraView" que será el encargado de gestionar la comunicación con la cámara Android.

Para ello abriremos el archivo "activity_main.xml" localizado dentro de la ruta "/res/layout/" de nuestro proyecto haciendo doble click sobre él:

Localización archivo activity_main.xml

Al abrir el archivo nos aseguramos que no estamos en "Graphical Layout", deberemos pulsar en la pestaña inferior donde aparece el nombre de archivo (activity_main.xml) y en la parte superior comprobamos que aparece el código XML del aspecto que tendrá la actividad:

Aspecto de activity_principal.xml

En el código XML dereremos cambiar el "TextView" (un simple campo de texto) que viene de ejemplo:

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text="@string/hello_world" />

Por este otro elemento llamado "org.opencv.android.NativeCameraView":

    <org.opencv.android.NativeCameraView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:id="@+id/activity_main_surface_view" />

Hay que notar que he identificado el elemento org.opencv.android.NativeCameraView con el identificador "activity_main_surface_view" para poder referenciarlo más adelante durante la carga de la aplicación.

El resultado una vez modificado el archivo será algo parecido a:

Ahora deberemos abrir el código de nuestra aplicación buscando en el explorador de paquetes (Package Explorer) el archivo "MainActivity.java" dentro de la ruta "/src/com.linaresdigital.blogrobotica.tutorialopencv1":

Aspecto de activity_principal.xml

En la definición de la clase (del tipo "Activity") nos encontraremos con:

Aspecto de activity_principal.xml

Deberemos agregar, tras "public class MainActivity extends Activity", el texto "implements CvCameraViewListener" para indicar que implementaremos los métodos de invocación asíncrona de obtención de fotogramas que generará nuestro "NativeCameraView" quedando la línea completa de la siguiente manera:

public class MainActivity extends Activity implements CvCameraViewListener {

Ahora Eclipse nos subrayará la palabra "CvCameraViewListener" indicando que existe un error que debe ser corregido:

Error en 'CvCameraViewListener'

Pulsaremos en "Import 'CvCameraViewListener' (org.opencv.android.CameraBridgeViewBase)" para solucionarlo (agregará una línea con el texto "import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener;" al comienzo de nuestro código fuente).

Tras hacerlo será ahora la palabra "MainActivity" la que aparecerá resaltada indicándonos un nuevo error que debe ser subsanado:

Error en 'MainActivity'

Esto es debido a que hemos indicado que implementamos la interfaz "CvCameraViewListener" pero no hemos implementado en el código ninguno de sus métodos (en el mensaje nos avisa del método "onCameraViewStarted" en particular).

Lo solucionaremos pulsando en "Add unimplemented methods" (agregar métodos sin implementar) tras lo cual Eclipse creará por nosotros las definiciones de las funciones que aún no hemos implementado listas para insertar código en ellas:

Error en 'MainActivity'

En los puntos 9 y 10 implementaremos todo lo necesario para trabajar con los fotogramas obtenidos.

Ya tenemos casi todo preparado, por ahora agregaremos una propiedad a nuestra clase para almacenar una referencia al control "org.opencv.android.NativeCameraView" que hemos agregado a nuestra actividad:

public class MainActivity extends Activity implements CvCameraViewListener {
        /* Referencia a nuestro control "org.opencv.android.NativeCameraView" */
        private CameraBridgeViewBase mOpenCvCameraView;

De nuevo nos aparecerá otro error resaltando en rojo el tipo de datos "CameraBridgeViewBase" que deberemos solucionar indicando que importe lo necesario pulsando en "Import 'CameraBridgeViewBase' (org.opencv.android)":

Error en 'CameraBridgeViewBase'

Y por último inicializamos nuestra "mOpenCvCameraView" durante el evento onCreate (creación de la actividad):

  /* Al cargar la actividad cargamos el valor de nuestra superficie de trabajo */
  mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.activity_main_surface_view);

El evento onCreate debería quedar tal y como se muestra:

 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  /* Al cargar la actividad cargamos el valor de nuestra superficie de trabajo */
  mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.activity_main_surface_view);
 }

jueves, 3 de enero de 2013

Permisos para acceder a la cámara y enlace con la librería OpenCV (pasos 3 y 4)

Cambiar los permisos del proyecto para acceder a la cámara

Para agregar los permisos necesarios para que nuestra aplicación tenga acceso a la cámara es necesario hacer doble click sobre el archivo "AndroidManifest.xml" de nuestro proyecto.

Creando nuevo proyecto Android

Una vez se haya abierto el editor pulsamos en la pestaña "Permissions" y luego pulsamos en el botón "Add..." para agregar un nuevo permiso:

Creando nuevo proyecto Android

Ahora pulsamos en "Uses permission" y pulsamos en "OK".

En la derecha aparece un cuadro llamado "Attributes For Uses Permission", en el que deberemos teclear (o seleccionar del menú desplegable) el permiso "android.permission.CAMERA".

Creando nuevo proyecto Android

Ahora tenemos preparado nuestro proyecto para hacer uso de la cámara. Si no hacemos este paso OpenCV nos avisará que la cámara no es accesible o está siendo usada por otra aplicación, por lo que no debemos olvidar este paso si no queremos evitar posteriores quebraderos de cabeza.

Configurar el proyecto para hacer uso de la librería OpenCV

Para hacer uso de la librería OpenCV en nuestro proyecto deberemos hacer referencia al proyecto importado anteriormente de la siguiente manera:

Pulsamos Alt+Enter o bien el botón derecho sobre nuestro proyecto (en el cuadro "Package Explorer") y pulsamos en "Properties".

Creando nuevo proyecto Android

Seleccionamos en la lista izquierda el apartado "Android" y en el cuadro "Library" pulsamos en el botón "Add...".

Creando nuevo proyecto Android

Seleccionamos la única librería que tenemos por ahora, OpenCV-2.4.3.2, y pulsamos en "OK".

Creando nuevo proyecto Android

Ahora aparecerá la librería "OpenCV-2.4.3.2" en el listado del cuadro "Library". Pulsamos en "OK" para guardar los cambios en el proyecto y a partir de ahora podremos hacer uso de las librerías de OpenCV en nuestro proyecto.

miércoles, 2 de enero de 2013

Creación del proyecto (pasos 1 y 2)

Importar librería OpenCV al espacio de trabajo Eclipse

Pulsamos en el menú "File > Import", seleccionamos "Existing Android Code Into Workspace" y pulsamos en siguiente:

Importando proyecto existente

Ahora pulsamos en "Browse..." y buscamos el directorio donde descomprimimos el SDK de OpenCV. Deberemos buscar dentro de él el subdirectorio "java" que hay dentro del directorio "sdk".

Localizando ruta al SDK

Nos aparecerá un proyecto llamado "java". Podemos cambiar el nombre de proyecto pulsando en él y posteriormente pulsando la tecla F2. En nuestro caso vamos a ponerle el nombre "OpenCV-2.4.3.2".

Si aparece un mensaje de error en la consola (en la ventana "Console") que muestre el texto Unable to resolve target 'android-xx' lo ignoramos. Eclipse elegirá automáticamente la versión del SDK de Android más alta que tengamos instalada para corregirlo.

Crear proyecto nuevo

Es hora de crear nuestro primer proyecto OpenCV para Android pulsando en "File > New > Android Application Project":

Creando nuevo proyecto Android

Rellenaremos la siguiente información:

  • Minimum Required SDK: Es la versión mínima en la que sabemos que funcionará la aplicación. OpenCV está soportado únicamente por el Android 2.2 (API 8, Froyo) y superiores.
  • Target SDK: La máxima versión de Android sobre la que estamos trabajando o sabemos a ciencia cierta que funciona. Yo uso Android ICS 4.0.4, por lo que marco el API 15 aún sabiendo que es muy probable que también funcione en Jelly Bean (Android 4.1).
  • Compile With: Versión de SDK sobre la que se compilará el proyecto. Yo suelo seleccionar la misma sobre la que estoy desarrollando, aunque haya una versión superior instalada en mi equipo.
  • Theme: Como será una aplicación a pantalla completa prefiero no usar ningún tema.

Pulsamos en "Next >" para configurar qué vamos a tener en el proyecto inicialmente.

Creando nuevo proyecto Android<

Quien quiera puede marcar la opción para crear un icono personalizado, agregando un paso más al proceso de creación del proyecto.

En este caso particular usaré los ajustes por defecto del icono (desmarco la casilla) pero dejaré marcada la casilla para crear una nueva actividad.

Pulsamos en "Next >" y configuramos cómo queremos que sea la actividad principal.

Creando una actividad vacía

Seleccionamos una actividad en blanco. Más adelante configuraremos la actividad para que abra a pantalla completa.

Hay que tener en cuenta que FullscreenActivity no creará una actividad a pantalla completa si no hemos configurado la versión de Android 4.1 (Jelly Bean).

Podemos finalizar el proceso de creación de proyecto en este punto pulsando "Finish", pero tenemos la posibilidad de cambiar el nombre de la actividad y de la disposición de la actividad pulsando en "Next >".

El aspecto final del gestor de proyectos será algo parecido a:

Aspecto del gestor de proyectos de Eclipse

Crear una aplicación que use la cámara en un dispositivo Android usando OpenCV (desde cero) en sólo 10 pasos

En esta guía pretendo crear un guión paso a paso sobre cómo empezar a usar OpenCV en un proyecto Android.
Los requisitos previos son:
  • Tener instalado Eclipse+ADT (yo uso el conjunto ofrecido por Google).
  • Haber descargado y descomprimido el SDK de OpenCV (en este caso estoy usando OpenCV-2.4.3.2-android-sdk).
El resumen de los pasos son:
  1. Importar librería OpenCV al espacio de trabajo Eclipse.
  2. Crear proyecto nuevo.
  3. Cambiar los permisos del proyecto para acceder a la cámara.
  4. Configurar el proyecto para hacer uso de la librería OpenCV.
  5. Configurar nuestra Actividad para que implemente CvCameraViewListener.
  6. Implementar la función de callback de la carga asíncrona de la librería OpenCV.
  7. Implementar el comportamiento de la aplicación en los eventos onResume, onPause y onDestroy.
  8. Configurar en onCreate nuestro CameraBridgeViewBase definiendo qué clase implementa CvCameraViewListener.
  9. Implementar en onCameraViewStarted y onCameraViewStopped la creación de las superficies necesarias para trabajar con cada fotograma.
  10. Implementar en onCameraFrame qué hacemos con cada fotograma obtenido.
(Edición: 26 de enero de 2015) La guía finalmente la he dividido en los siguientes artículos del blog:
Experimentalmente me he atrevido con la grabación del proceso en vídeo y la he subido a mi canal de YouTube. Hay que tener que son pruebas y que no está completo, pero puede ayudar en algunos aspectos tener parte del tutorial en vídeo:

sábado, 29 de diciembre de 2012

Sigue líneas visual con umbrales no lineales

Es obvio que una línea horizontal para estimar la dirección que sigue una línea frente a ti no es buena idea. En el siguiente post explicaré el motivo, pero en este me centraré en comunicar la modificación de uno de los umbrales para que éste no sea lineal.

El resultado es el que se muestra en el siguiente vídeo.

Más adelante explicaré en qué mejora tener uno u otro sistema.

Este artículo lo dejé como borrador hace tiempo y he decidido publicarlo tal cual lo dejé para que no se quede en el olvido. El orden lógico sería tras el artículo Sigue líneas visual con umbrales lineales con fecha 28 de abril de 2010.

miércoles, 31 de octubre de 2012

Errores de compilación en Ubuntu 12.04

Antes de ayer me informaron que el código del guardado de imágenes capturadas por la webcam no compilaba en Ubuntu 12.04.

Como en un principio el mensaje daba a entender que era problema de no tener instaladas las librerías (por el típico error "undefined reference") recomendé instalar todo lo relacionado con opencv-dev que se me vino a la cabeza que debería necesitar:

redstar@redstar-virtual-machine:~$ sudo apt-get install libcv-dev libhighgui-dev libopencv-contrib-dev

El problema persistía, por lo que para probar y arreglar el problema (a mí me funcionaba en máquinas Debian 5, 6 y Ubuntu 10.04) me instalé una máquina virtual y comprobé que realmente daba problemas el método de compilación propuesto en mi código:

redstar@redstar-virtual-machine:~$ gcc `pkg-config --libs --cflags opencv` -o webcam_jpeg webcam_jpeg.c
/tmp/cchQFB8g.o: In function `cvRound':
webcam_jpeg.c:(.text+0x19): undefined reference to `lrint'
/tmp/cchQFB8g.o: In function `cvDecRefData':
webcam_jpeg.c:(.text+0xa5a): undefined reference to `cvFree_'
webcam_jpeg.c:(.text+0xacb): undefined reference to `cvFree_'
/tmp/cchQFB8g.o: In function `cvGetRow':
webcam_jpeg.c:(.text+0xbc1): undefined reference to `cvGetRows'
/tmp/cchQFB8g.o: In function `cvGetCol':
webcam_jpeg.c:(.text+0xbec): undefined reference to `cvGetCols'
/tmp/cchQFB8g.o: In function `cvReleaseMatND':
webcam_jpeg.c:(.text+0xbff): undefined reference to `cvReleaseMat'
/tmp/cchQFB8g.o: In function `cvSubS':
webcam_jpeg.c:(.text+0xd1f): undefined reference to `cvAddS'
/tmp/cchQFB8g.o: In function `cvCloneSeq':
webcam_jpeg.c:(.text+0xd6d): undefined reference to `cvSeqSlice'
/tmp/cchQFB8g.o: In function `cvSetNew':
webcam_jpeg.c:(.text+0xdcc): undefined reference to `cvSetAdd'
/tmp/cchQFB8g.o: In function `cvGetSetElem':
webcam_jpeg.c:(.text+0xe5f): undefined reference to `cvGetSeqElem'
/tmp/cchQFB8g.o: In function `cvEllipseBox':
webcam_jpeg.c:(.text+0xf5f): undefined reference to `cvEllipse'
/tmp/cchQFB8g.o: In function `cvFont':
webcam_jpeg.c:(.text+0xfaf): undefined reference to `cvInitFont'
/tmp/cchQFB8g.o: In function `cvReadIntByName':
webcam_jpeg.c:(.text+0x103d): undefined reference to `cvGetFileNodeByName'
/tmp/cchQFB8g.o: In function `cvReadRealByName':
webcam_jpeg.c:(.text+0x10ce): undefined reference to `cvGetFileNodeByName'
/tmp/cchQFB8g.o: In function `cvReadStringByName':
webcam_jpeg.c:(.text+0x1128): undefined reference to `cvGetFileNodeByName'
/tmp/cchQFB8g.o: In function `cvReadByName':
webcam_jpeg.c:(.text+0x1158): undefined reference to `cvGetFileNodeByName'
webcam_jpeg.c:(.text+0x116e): undefined reference to `cvRead'
/tmp/cchQFB8g.o: In function `cvCreateSubdivDelaunay2D':
webcam_jpeg.c:(.text+0x11a1): undefined reference to `cvCreateSubdiv2D'
webcam_jpeg.c:(.text+0x11cb): undefined reference to `cvInitSubdivDelaunay2D'
/tmp/cchQFB8g.o: In function `cvContourPerimeter':
webcam_jpeg.c:(.text+0x12ff): undefined reference to `cvArcLength'
/tmp/cchQFB8g.o: In function `cvCalcHist':
webcam_jpeg.c:(.text+0x1327): undefined reference to `cvCalcArrHist'
/tmp/cchQFB8g.o: In function `main':
webcam_jpeg.c:(.text+0x1558): undefined reference to `cvCreateCameraCapture'
webcam_jpeg.c:(.text+0x15bf): undefined reference to `cvQueryFrame'
webcam_jpeg.c:(.text+0x1608): undefined reference to `cvSaveImage'
webcam_jpeg.c:(.text+0x1614): undefined reference to `cvWaitKey'
webcam_jpeg.c:(.text+0x163e): undefined reference to `cvDestroyWindow'
webcam_jpeg.c:(.text+0x164a): undefined reference to `cvReleaseCapture'
collect2: ld devolvió el estado de salida 1

Para arreglarlo simplemente hay que cambiar el orden de los parámetros y poner como primer parámetro el archivo del programa C que se desea compilar, acto seguido qué archivo ejecutable será generado y por último la referencia a los parámetros necesarios para usar includes y librerías de OpenCV:

redstar@redstar-virtual-machine:~$ gcc webcam_jpeg.c -o webcam_jpeg `pkg-config --libs --cflags opencv`

Así que he aprovechado y he cambiado TODAS la cabeceras de mis aplicaciones publicadas en este blog para que estén en el orden correcto.

Si alguien conoce el motivo exacto por el que ha dejado de funcionar el método que usaba anteriormente agradecería que me lo hiciera saber, siempre he sido curioso y me gusta saber el porqué de las cosas y no aceptarlas porque sí.

lunes, 29 de octubre de 2012

Volviendo atrás en OpenCV: Captura de fotogramas JPEG a disco

Una buena parte del desarrollo software de mi proyecto correrá sobre una plataforma ARM, cuando lo más normal para los programadores es hacerlo en plataformas x86 o x86_64.

El motivo de este cambio es por la necesidad de desarrollar aplicaciones embebidas en dispositivos Android o Raspberry PI que están basados en procesadores ARM.

La primera toma de contacto con esta plataforma la he tenido al proporcionar soporte a un compañero de trabajo a la hora de capturar imágenes en un archivo guardado en disco.

Para este desarrollo haremos uso de una nueva función C llamada cvSaveImage. Pasaremos los parámetros de formato y calidad de compresión mediante una estructura CvArr.

El código fuente del ejemplo es:

martes, 10 de julio de 2012

De vuelta al TFM

Después de un año de transición desde el antiguo Máster Oficial en Sistemas Telemáticos e Informáticos, impartido en el campus de Móstoles de la URJC, al nuevo Máster Universitario en Sistemas Telemáticos e Informáticos, impartido en el campus de Fuenlabrada de la Universidad Rey Juan Carlos, retomo el proyecto dándole un pequeño giro a algunas cosas.

En un principio la plataforma JDERobot sobre la que se iba a trabajar era la versión 4.3.0. Actualmente está funcionando la versión 5.0.

La principal diferencia entre una y otra versión es el uso de un middleware para la comunicación entre componentes, permitiendo implementar drivers para todo tipo de sensores, robots, etc de manera más eficiente, abierta y sobre cualquier plataforma.

El middleware usado es ICE (The Internet Communications Engine).

Puede usarse dicho middleware en lenguajes de programación como C, C++, Java, PHP, Python, etc.

Aprovecharemos la versatilidad del middleware para comunicarnos con el robot y enviar/recibir las imágenes captadas para su análisis y decidir si el flujo del movimiento puede ser estimado con fluidez suficiente en el propio robot (usando un móvil android) o bien el cálculo pesado debe seguir siendo tarea de un PC de sobremesa.

Por lo pronto a partir de ahora me volcaré más en implementar aplicaciones OpenCV basadas en Android.

Por otro lado, y de manera paralela, desarrollaré el control autónomo de un robot basado en un viejo coche teledirigido para las pruebas de campo.

El sujeto del experimento:

En la imagen se observa el coche teledirigido con los tres sensores de proximidad de largo alcance montados sobre él.

Placa de desarrollo IOIO:

En la imagen se observa únicamente uno de los sensores conectados a la placa de desarrollo.

Vídeo de pruebas de funcionamiento:

martes, 15 de febrero de 2011

Análisis del flujo de movimiento en OpenCV (y 3)

Con muy poquitas modificaciones realizo ahora el seguimiento de dos puntos centrales de la imagen para calcular la rotación (balanceo) de la cámara.

El código de ejemplo es:


Con un par de modificaciones rápidas más (hardcoded) guardo las imágenes obtenidas en un vídeo AVI con FOURCC "XVID", 30 imágenes por segundo y con nombre de archivo "salida.avi". Lo ideal sería calcular la velocidad de cuadro real y/o enviar el mismo fotograma a la salida en caso de ralentización.

Ese tema lo dejaremos para más adelante.

El código de ejemplo modificado es:


En el siguiente vídeo puede observarse el seguimiento del horizonte realizado por el algoritmo.


Hay que recordar que no se hace corrección de perspectiva, por lo que si la webcam no sólo se rota (balancea) si no que se gira o cabecea, el cálculo del balanceo comienza a acumular errores. Por eso en el vídeo reinicio el horizonte en dos ocasiones.

lunes, 14 de febrero de 2011

Análisis del flujo de movimiento en OpenCV (y 2)

He realizado una pequeña modificación para mostrar el movimiento acumulado (y representado por la flecha roja central) mediante una cruz verde.

Aparentemente se sigue el movimiento del punto de la escena, pero sólo funciona si la escena se mueve al mismo tiempo sin rotación y sin que haya un objeto en el centro que se mueva.

El código de ejemplo es:


El resultado de un movimiento satisfactorio (sin rotación ni objetos en la zona central que interfieran) puede verse en el siguiente vídeo:



Pero si realizamos rotación de la webcam o pasamos un objeto por el centro de la escena (pero no delante del objeto perseguido) entonces el algoritmo falla, ya que la información la está obteniendo del centro de la pantalla y no del objeto rastreado:



La forma de arreglar este problema es sencilla. En vez de dejar fijo el punto del fotograma anterior al que queremos calcular el flujo de movimiento lo vamos actualizando al mismo punto donde se ha calculado el desplazamiento.

El código del ejemplo mejorado es:


En el siguiente vídeo se muestra cómo ahora el algoritmo es más robusto frente a la rotación de la imagen, al movimiento de objetos en la zona central, pero no a los movimientos de objetos frente al objeto rastreado:

viernes, 11 de febrero de 2011

Análisis del flujo de movimiento en OpenCV

Vamos a dar un salto brusco en la línea de aprendizaje para llegar al análisis del flujo de movimiento usando la librería de visión artificial OpenCV.

En este ejemplo (algo más complejo que los anteriores) usaremos tres imágenes temporales, el fotograma actual en color sobre el que dibujaremos líneas que representarán el flujo de movimiento y una copia del fotograma anterior y actual en escala de grises, y dos imágenes de trabajo para las dos pirámides necesarias para la implementación piramidal desarrollada por Jean-Yves Bouguet del algoritmo iterativo de seguimiento de características de imagen de Lucas-Kanade.

La función usada es cvCalcOpticalFlowPyrLK.

Tiene como parámetros de entrada los dos fotogramas sobre los que deseamos estudiar el flujo de movimiento (en escala de grises), las dos imágenes temporales (pirámides), una serie de puntos sobre los que deseamos averiguar el flujo de movimiento, el vector de movimiento resultante y el criterio de parada.

El código de ejemplo es (le faltan unos retoques):


Se puede experimentar con diversos criterios de parada (número máximo de iteraciones y error permitido). Si se alcanza el número máximo de iteraciones sin haber llegado a un resultado que se ajuste al error deseado la característica será marcada como no calculada. Esto suele ocurrir, sobre todo, cuando se trabaja con zonas de color liso o durante movimientos bruscos en los que el emborronado de movimiento de la cámara (o webcam) suaviza los detalles de la imagen haciéndolos irreconocibles desde el fotograma anterior.

Por otro lado, os recuerdo que una gran mayoría de webcams entregan los fotogramas en formato JPEG, por lo que para evitar que los artefactos cuadriculados de la compresión JPEG estorben en el análisis de movimientos debe usarse una apertura superior a 16 pixeles.

lunes, 7 de febrero de 2011

Filtro de detección de bordes (Canny) usando OpenCV

La detección de bordes, en procesamiento digital de imágenes, nos permite averiguar los contornos de la imagen o, mejor dicho, los cambios bruscos de intensidad lumínica. Es por esto último por lo que no se recomienda usar un filtro de detección de bordes tras haber suavizado con una amplitud grande una imagen a no ser que se use una apertura superior en el filtro Canny.

La función que realiza el filtro de suavizado de imagen en OpenCV es cvCanny.

El código fuente del ejemplo es:


El resultado obtenido tras ejecutar el código de ejemplo es:
Filtro Canny usando OpenCV

martes, 4 de enero de 2011

Filtrado de imagen (suavizado) usando OpenCV

Uno de los problemas más comunes que nos encontramos a la hora de analizar imágenes obtenidas desde dispositivos de captura es el ruido de compresión, cuantización y de sensibilidad del sensor de captura.

Uno de los métodos existentes para reducir ese ruido es filtrar espacialmente la imagen (suavizado) aunque ello provoque pérdida del detalle de la imagen.

Realizaremos tres ejemplos usando un filtro lineal gausiano (CV_GAUSSIAN) con unos tamaños de 1x9 (filtro de suavizamiento vertical), 9x1 (suavizamiento horizontal) y 9x9 (suavizamiento normal).

La función que realiza el filtro de suavizado de imagen en OpenCV es cvSmooth.

El código fuente del ejemplo es:


El resultado de suavizar horizontalmente la imagen elimina la mayoría del ruido producido por la emisión en el aire de señales de televisión al tiempo que se pierde la definición de las imágenes en forma de columnas verticales.

Tras suavizar verticalmente la imagen se elimina el granulado debido a cuantización y codificación en muchas webcams (observar cómo prácticamente ha desaparecido el ruido en la imagen superior derecha) pero se pierde la definición de las imágenes en forma de filas horizontales (ver cómo prácticamente desaparecen las rayas del jersey).

El mejor resultado de obtiene al filtrar horizontal y verticalmente la imagen, pero al mismo tiempo es cuando se pierde mayor detalle de la imagen.

El resultado obtenido tras ejecutar el código de ejemplo es:
Captura de imagen con OpenCV

Otro ejemplo de región de interés (ROI) en OpenCV

Para ilustrar mejor el funcionamiento de las regiones de interés (antes de continuar con algoritmos de filtrado de imagen) veremos un último ejemplo sencillo de regiones de interés en el que sólo visualizaremos una línea horizontal de la parte inferior de la imagen (útil para implementar, por ejemplo, algoritmos de robots sigue líneas).

El código del ejemplo es el siguiente:


En este caso lo que hemos hecho ha sido marcar como región de interés (ROI) únicamente la zona inferior de una altura determinada. Al hacerlo estamos diciendo que sólo nos interesa trabajar con esa zona de la imagen, por lo que al mostrar el fotograma sólo veremos la parte inferior seleccionada.

El resultado obtenido (con una altura de 50 puntos de pantalla) es:
Captura de imagen con OpenCV

domingo, 2 de enero de 2011

Mezclando dos imágenes en una con OpenCV

No existe forma directa de mostrar dos imágenes dentro de una misma ventana usando las funciones ofrecidas por OpenCV, pero podemos hacer uso de la región de interés (ROI - Region Of Interest) para conseguirlo.

Con las regiones de interés podemos definir a qué parte de una imagen destino queremos "pegar" el contenido de una imagen fuente. De esta manera conseguimos "componer" la imagen destino deseada a partir de todas las imágenes fuentes necesarias.

En el siguiente ejemplo obtendremos un fotograma del dispositivo de captura y lo mostraremos a tamaño completo con una versión en miniatura del mismo fotograma en su interior.

Una cosa muy importante a tener cuando trabajamos con fotogramas capturados usando OpenCV es que no debemos modificar la imagen entregada por cvQueryFrame ni podremos reutilizar la imagen obtenida por sucesivos fotogramas capturados (explicaré mejor esto último en sucesivos ejemplos).

Por este motivo la composición se hará en una imagen clonada a partir del fotograma capturado usando la función cvClone. Tras ello definiremos la región de interés en dicha imagen clonada (el área de destino donde "pegaremos" de nuevo la imagen) y usaremos de nuevo la función cvResize para copiar una versión escalada del fotograma dentro de la región deseada.

Es importante recordar que la función cvClone aloja la memoria necesaria para almacenar tanto la cabecera de la imagen como su contenido, de modo que llamadas sucesivas a dicha función sin liberar los recursos previamente provocará que nuestra aplicación esté continuamente aumentando su consumo de memoria, de modo que no debemos olvidarnos de liberar los recursos reservados mediante una llamada a la función cvReleaseImage.

Código fuente del ejemplo:

Explicación en detalle paso por paso:
  1. Obtenemos un fotograma del dispositivo de captura en "fotograma".
  2. Creamos una copia del fotograma en "copia".
  3. Seleccionamos una zona de la imagen "copia" sobre la que volcaremos una versión escalada (una miniatura) del mismo fotograma.
  4. Realizamos el escalado de imagen sobre esa zona de interés.
  5. Deshacemos la selección de zona para que la función cvShowImage no nos muestre únicamente la región de interés (nos aparecería únicamente la zona recién escalada), si no la composición completa (el fotograma en grande junto con la miniatura.
  6. Tras mostrar la composición realizada en "copia" liberamos la memoria usada.

El resultado obtenido es:
Captura de imagen con OpenCV