miércoles, 9 de septiembre de 2015

Contar Olivos con Ortofoto: Parte II (Clustering con K-Means)


Introducción


El siguiente post, por su propio titulo lo indica, es la segunda parte del post 'Contar Olivos con Ortofoto: Parte I'.

En esta segunda parte, se trata de ver que métodos o de que forma se puede discriminar, que son olivos y que no son, para de esta forma tener el número de olivos de una ortofoto con el menor margen de error (+- 4 olivos creo que es un margen razonable¿?):

Nota: para quién quiera ver los olivos que tiene cada recinto, una herramienta que puede utilizar es el SIGPAC (Sistema de Información Geográfica de Identificación de Parcelas Agrícolas).



Redes Neuronales

Utilice redes neuronales con una capa oculta, pero no era capaz de aprender con un margen de error mínimo.

Como un olivo puede estar representado por diferentes tamaños en pixeles. Por ejemplo, 20x20, 15x15, 4x4,..etc..lo que se hacía, era pasar todas las imagenes a un tamaño fijo, 7x7. Con este tamaño, las muestras de olivos, y las muestras de no olivos, para que aprendiese la red neuronal, daba un error grande, y no aprendía bien. Era por que la diferencia entre las imagenes de los olivos y los que no eran olivos, era muy pequeña.

Es posible, con ortofotos de alta calidad, donde un olivo puede ser representado por tamaños de 50x50, se pueda usar una red neuronal.¿?. Ya que la diferencia ahora, entre una imagen de lo que es un olivo y lo que no, se diferencian más.

Entonces, ¿qué usar?


Clustering k-means

Para una mejor explicación, ver Clustering k-means.  Y a mi modo de ver, este algoritmo lo que hace es que dado un número determinado a priori de clusters, el algoritmo, va mirando si los valores de un elemento, un objeto, está más cerca de un cluster o de otro. Y encuentra un cluster que este más cerca, asigna ese objeto a ese cluster. Creo que no está muy bien explicado. En el caso concreto que nos atañe.

Se han identificado por ejemplo 100 posibles olivos de diferentes tamaños (5x5, 20x20,..etc). Ahora, el algoritmo, inicializa aleatoriamente, los clusters a los que pertenece cada objeto. Es decir, va diciendo..."objeto 1 tu perteneces aleatoriamente al cluster 0, tu el objeto 2, al cluster 3,...hasta 6 clusters".

Luego se calcula el centroide de cada cluster. El centroido sera el tamaño medio de los objetos que lo componen. 
Y luego ya por ultimo, entramos en el 'while'. Que será, mientras haya cambio de un objeto de un cluster a otro, se hace: se va por cada objeto, y se mide la distancia (distancia de Euler) de ese objeto a todos los cluster, si se encuentra que el cluster más cercano encontrado, es distinto al que pertenece el objeto, eureka!, el objeto no pertenece a ese cluster y se pasa al otro.

Al final del algoritmo, en este ejemplo, tenemos 6 clusters, y cada uno con objetos similares entre sí.

Aquí, se supone que como los olivos serán casi todos del mismo tamaño, estarán casi todos en un mismo cluster, y los objetos que no son olivos, en otro clusters distinto.

Veamos un pantallazo.



Ene el pantallazo se ven rectangulos semitransparentes. Esto son cluster que se han determinado que no entran a formar parte del computo de la suma de los olivos. Y como se van son rectangulos que no tienen nada que ver con un olivo.

Pero, aquí está el problema que veo, que hay objetos en un cluster, que son olivos y otros que no son. Por ejemplo, el Cluster 5, son olivos, pero luego hay etiquetas 5 en el tejado de una casa.

¿como solucionar esto?

Un solución rápida, sería que el usuario, definiera una región de la ortofoto, donde se le diga, donde debe contar, y los objetos que estén fuera de esta región (la marcada por el trazado rojo), no entrar en el conteo de los olivos. 

El trazado rojo, está dibujado con el Paint, y no ha sido implementado en el programa, pero no sería un ejercicio complicado el crear una región y aplicar clipping.

Conclusión

No ha sido muy elegante la solución, por el hecho de tener que dibujar una región, pero si por lo menos para una primera versión, si contaría los olivos correctamente. Ahorrando los costes de llevar personal para que los cuente (tiempo, viajes, dietas,....).

El programa en release está aquí.

Félix Romo
felix.romo.sanchezseco@gmail.com





martes, 1 de septiembre de 2015

Un producto software, varios clientes. O como customizar.

Introducción


Hace un tiempo, unos años, trabaje en una empresa donde tenían un producto para varios clientes, Y cada cliente, requería un nuevo requisito, o quitar alguno, esto generaba código del estilo:

#if __CLIENT_A__

// habilitar caracteristica del cliente A


#endif


Este tipo de código a lo largo, de miles de lineas de código, mezclado con las demás funcionalidades de los demás clientes, el código es más difícil de seguir que una demostración matemática (al menos para mí...)

  Hace un tiempo, me tope con este problema, pero no quería extender la complejidad de los #if a lo largo de todas las clases del programa. Debía de encapsular esta complejidad en una clase.

Pero antes, veamos un ejemplo práctico.

Ejemplo práctico


Imaginemos, que tenemos un software que tiene las siguientes funcionalidades:

- Exportar sus datos a un fichero Txt (__TXT__EXPORT__ )
- Exportar sus datos a un fichero Excel (__EXCEL_EXPORT__)
- Visualización 2d (__2D_VIEW__ )
- Visualización 3d (__3D_VIEW__ )
- Exportación del modelo 3d generado a un fichero (__EXPORT_3D_MODEL__ ). Esta funcionalidad solo se activo si está activa la visualicación 2d y 3d.

La función drawOptionsInstallOldWay dibuja en pantalla las opciones instaladas de la forma #if. Y la función drawOptionsInstallNewWay las dibuja de otra forma que ya veremos.

El aspecto de la aplicación sería con las siguientes opciones activadas:
__TXT__EXPORT__, __2D_VIEW__,__3D_VIEW__





La parte izquierda se dibuja con el método drawOptionsInstallOldWay y la parte derecha (azul) con el método drawOptionsInstallNewWay .

Ahora, veamos cual de los dos métodos es más difícil de leer y de mantener a la larga.

 private void drawOptionsInstallOldWay(Graphics g)
        {
            
            float y = 40.0f;
#if __TXT__EXPORT__ 
                g.DrawString("Txt Export", SystemFonts.DefaultFont, Brushes.Black, 10.0f, y);
                y += SystemFonts.DefaultFont.Height + 2.0f;
#endif

#if __EXCEL_EXPORT__
                g.DrawString("Excel Export", SystemFonts.DefaultFont, Brushes.Black, 10.0f, y);
                y += SystemFonts.DefaultFont.Height + 2.0f;
#endif

#if __2D_VIEW__
                g.DrawString("2d View", SystemFonts.DefaultFont, Brushes.Black, 10.0f, y);
                y += SystemFonts.DefaultFont.Height + 2.0f;
#endif

#if __3D_VIEW__
                g.DrawString("3d View", SystemFonts.DefaultFont, Brushes.Black, 10.0f, y);
                y += SystemFonts.DefaultFont.Height + 2.0f;
#endif

#if __3D_VIEW__ && __2D_VIEW__ // __EXPORT_3D_MODEL__
                g.DrawString("Export 3d Model", SystemFonts.DefaultFont, Brushes.Black, 10.0f, y);
                y += SystemFonts.DefaultFont.Height + 2.0f;
#endif


        }


 private void drawOptionsInstallNewWay(Graphics g)
        {
            float y = 40.0f;

if(RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.EXPORT_TXT__REQUERIMENT))
            {
                g.DrawString("Txt Export", SystemFonts.DefaultFont, Brushes.Blue, 200.0f, y);
                y += SystemFonts.DefaultFont.Height + 2.0f;
            }
if(RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.EXPORT_EXCEL__REQUERIMENT))
            {
                g.DrawString("Excel Export", SystemFonts.DefaultFont, Brushes.Blue, 200.0f, y);
                y += SystemFonts.DefaultFont.Height + 2.0f;
            }

            if(RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.GRAPHICS_2D_REQUERIMENT))
            {
                g.DrawString("2d View", SystemFonts.DefaultFont, Brushes.Blue, 200.0f, y);
                y += SystemFonts.DefaultFont.Height + 2.0f;
            }

            if (RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.GRAPHICS_3D_REQUERIMENT))
            {
                g.DrawString("3d View", SystemFonts.DefaultFont, Brushes.Blue, 200.0f, y);
                y += SystemFonts.DefaultFont.Height + 2.0f;
            }

            if (RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.GENERATE_3D_MODEL))
            {
                g.DrawString("Export 3d Model", SystemFonts.DefaultFont, Brushes.Blue, 200.0f, y);
                y += SystemFonts.DefaultFont.Height + 2.0f;
            }
        }

Vemos que el método drawOptionsInstallNewWay no hace uso de #if, sino de llamadas a un objeto de tipo RequerimentsActivator, el cual, le responde si una funcionalidad o requisito está activo o no.
Con esto se consigue que la complejidad de los #if, está centralidad, encapsulada en la clase RequerimentActivator.

La clase RequerimentActivator


Esta clase sigue el patrón de diseño singleton. De esta forma, se podrá acceder desde cualquier parte del programa, pero, yo creo que las partes donde se debería de acceder, es desde el interfaz, pero no llamarla desde las clases del problema de la aplicación.
Es decir, hacer uso de esta clase, RequerimentActivator, solo desde el MainForm.cs o clases que deriven de la clase Form. Así, las clases del dominio del problema no estarán acopladas a esta clase.


Esta clase tiene un enumerado llamado OptionalRequeriments donde se enumeraran todas las opciones que se pueden activar y desactivar.

public enum OptionalRequeriments { EXPORT_EXCEL__REQUERIMENT = 0, EXPORT_TXT__REQUERIMENT = 1, GRAPHICS_2D_REQUERIMENT = 2,GRAPHICS_3D_REQUERIMENT = 3, GENERATE_3D_MODEL=4 };
            int _MaxOptionalRequeriments = 5; //numero de opciones


Y el método iniRequeriments inicializa el array booleano _requeriments a true o a false dependiendo si un requisito se activo o no. Pero, ¿cómo?

Haciendo uso, ahora sí, de los #if.


#if __EXCEL_EXPORT__
                _requeriments[(int)OptionalRequeriments.EXPORT_EXCEL__REQUERIMENT] = true;
#endif
#if __TXT__EXPORT__
                _requeriments[(int)OptionalRequeriments.EXPORT_TXT__REQUERIMENT] = true;
#endif

Sí por ejemplo el simbolo __EXCEL_EXPORT__ no está definido, entonces 
 _requeriments[(int)OptionalRequeriments.EXPORT_EXCEL__REQUERIMENT] estaría a falso.

La función completa sería:

   private void iniRequeriments()
            {
                _requeriments = new bool[_MaxOptionalRequeriments];
            
                for (int i = 0; i < _MaxOptionalRequeriments; i++)
                {
                    _requeriments[i] = false;

                }

#if __EXCEL_EXPORT__
                _requeriments[(int)OptionalRequeriments.EXPORT_EXCEL__REQUERIMENT] = true;
#endif
#if __TXT__EXPORT__
                _requeriments[(int)OptionalRequeriments.EXPORT_TXT__REQUERIMENT] = true;
#endif
#if __2D_VIEW__
                _requeriments[(int)OptionalRequeriments.GRAPHICS_2D_REQUERIMENT] = true;
#endif

#if __3D_VIEW__
                _requeriments[(int)OptionalRequeriments.GRAPHICS_3D_REQUERIMENT] = true;
#endif

#if __2D_VIEW__ && __3D_VIEW__
                _requeriments[(int)OptionalRequeriments.GENERATE_3D_MODEL] = true;
#endif

            }

En negrita he puesto el requisito 'generar modelo 3d a fichero'. Como este requisito depende de dos requisitos, no se define un simbolo de tipo __GENERATE_MODEL_3D__.

La gracia de esto, es que se puede establecer una jerarquía de requisitos. Requisitos que se activen o no, dependiendo de otros requisitos!!

Además, de luego, la facilidad para habilitar los menús, dependiendo de las funcionalidades activas. Como se ve abajo.

private void updateMenuFromRequerimentsNewWay()
        {
            txtToolStripMenuItem.Enabled = RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.EXPORT_TXT__REQUERIMENT);

            excelToolStripMenuItem.Enabled = RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.EXPORT_EXCEL__REQUERIMENT);

            View2dToolStripMenuItem.Enabled = RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.GRAPHICS_2D_REQUERIMENT);

            View3dToolStripMenuItem1.Enabled = RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.GRAPHICS_3D_REQUERIMENT);

            generateModel3DToolStripMenuItem.Enabled = RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.GENERATE_3D_MODEL);
            
        }

Conclusión

Creo que está forma de activar o desactivar los requisitos de un software, hace más mantenible a la larga el código fuente, ya que, encapsula la complejidad de los #if . Además, esto permite, aumentar la complejidad, pudiendo crear jerarquías de requisitos (activar un requisito cuando varios requisitos están activos, o pueden obedecer a una lógica booleana).


El código fuente se puede encontrar aquí.

Félix Romo
felix.romo.sanchezseco@gmail.com