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.