detecciÓn de lÍneas de texto mediante la transformada de ... · trabajo de análisis de la...
TRANSCRIPT
DETECCIÓN DE LÍNEAS DE TEXTO
MEDIANTE LA TRANSFORMADA DE
HOUGH
Informe Detallado
Trabajo de análisis de la asignatura “Procesamiento de Imágenes Digitales” de 5º curso de Ingeniería Informática.
Escuela Técnica Superior de Ingeniería Informática Tutor: María José Jiménez Rodríguez
REALIZADO POR:
Elena Hernández Salmerón [email protected]
Elena Lozano Rosch [email protected]
Mercedes Jiménez Romero [email protected]
2
INDICE
1. Introducción ……………………………………………………………………………………… 4
2. Problema Teórico ……………………………………………………………………………… 6
2.1. Planteamiento del problema ………………………………………………………. 6
2.2. Consideraciones Previas ……………………………………………………………… 6
2.3. Metodología ……………………………………………………………………………….. 6
2.3.1. Pre-Procesado …………………………………………………………………… 7
2.3.1.1. Binarización ………………………………………………………….. 7
2.3.1.2. Extracción de componentes conexas ……………………. 7
2.3.1.3. Calculo de la altura media de las componentes …….. 8
2.3.1.4. Partición en subconjuntos …………………………………….. 8
2.3.2. Transformada de Hough ……………………………………………………. 10
2.3.3. Post-Procesado …………………………………………………………………. 12
3. Problema Práctico: La aplicación informática……………………………………… 13
3.1. Descripción de la Aplicación ……………………………………………………….. 13
3.1.1. Lenguaje de Programación ……………………………………………….. 13
3.1.2. Estructura de Paquetes y Clases ……………………………………….. 13
3.2. Manual de Usuario ……………………………………………………………………… 15
3.2.1. Descripción de la Interfaz Gráfica de Usuario ……………………. 15
3.2.2. Instrucciones para una Ejecución Básica …………………………… 19
3.3. Implementación de Algoritmos: La Clase MiImagen …………………… 20
3.3.1. Binarización ……………………………………………………………………… 22
3.3.2. Cálculo de Subdominios …………………………………………………… 23
3.3.3. Transformada de Hough …………………………………………………… 30
3.3.4. Imagen Final …………………………………………………………………….. 34
3.3.5. Post-Procesado ………………………………………………………………… 36
3.4. Experimentación ………………………………………………………………………… 37
4. Conclusiones ……………………………………………………………………………………… 39
5. Bibliografía ………………………………………………………………………………………… 40
6. Carga de Trabajo ………………………………………………………………………………… 41
3
LISTA DE FIGURAS
Figura 1: Líneas de texto inclinadas …………………………………………………………………….. 4
Figura 2: Distintas inclinaciones en un mismo texto ……………………………………………. 4
Figura 3: Altura media de los caracteres de un texto …………………………………………… 8
Figura 4: Componentes pertenecientes al Subdominio 1 …………………………………….. 9
Figura 5: Componentes pertenecientes al Subdominio 2 …………………………………….. 9
Figura 6: Componentes pertenecientes al Subdominio 3 …………………………………….. 10
Figura 7: Subdivisión en bloques de las componentes conexas del Subdominio 1 .. 11
Figura 8: Correspondencia entre espacio Cartesiano y coordenada Polar ……………. 11
Figura 9: Estructura del proyecto …………………………………………………………………………. 14
Figura 10: Ventana Principal de la Aplicación ………………………………………………………. 15
Figura 11: División de la ventana en Lienzos ………………………………………………………… 16
Figura 12: Imagen mostrada con barras de desplazamiento ………………………………… 16
Figura 13: Barra de Menú ……………………………………………………………………………………. 16
Figura 14: Vista del Menú Archivo con todas sus opciones ………………………………….. 17
Figura 15: Vista del Menú Operaciones ……………………………………………………………….. 17
Figura 16: Barra Principal …………………………………………………………………………………….. 18
Figura 17: Ventana que muestra la imagen resultante …………………………………………. 19
Figura 18: Ejemplo de barra de estado mostrando información de la imagen ………. 19
Figura 19: Representación de componentes conexas …………………………………………… 26
Figura 20: Representación del recuadro de una componente conexa …………………… 30
Figura 21: Componente conexa “grande” dividida en dos mediante el proceso de
filtrado (Caso A) ……………………………………………………………………………………………………. 37
Figura 22: Componente conexa “grande” procesada como si fuera del subdominio1
(Caso B) …………………………………………………………………………………………………………………. 37
Fig.23: Posible mejora de la aplicación utilizando las componentes del subconjunto de
las imágenes pequeñas. …………………………………………………………………………………………. 39
4
1. INTRODUCCIÓN
El reconocimiento óptico de caracteres es un procedimiento mediante el cual se
extraen de una imagen los caracteres que forman un texto para que puedan ser
almacenados en un formato que permita interactuar con programas de edición de
texto. En una imagen, cada carácter queda determinado por los píxeles que lo
conforman mientras que en un documento de texto sólo es necesario un número para
describirlos. Una vez hecha esta transformación, el texto empieza a ser considerado
como texto y puede ser manipulado utilizando las características comunes de los
editores de texto. En el caso de un documento impreso, todo este proceso es
prácticamente directo. Sin embargo, si hablamos de un texto escrito a mano existen
diversos factores que complican su transformación.
En general, la eficiencia de un sistema de reconocimiento de caracteres escritos a
mano depende del proceso de detección de líneas de texto. Si este proceso no da
buenos resultados, la precisión del reconocimiento de palabras se verá afectada y, por
tanto, también afectará al reconocimiento de caracteres individuales.
Algunas de las dificultades con las que nos podemos encontrar a la hora de tratar un
texto escrito a mano son inclinación que podemos encontrar en distintas líneas de un
mismo texto (Fig. 1), la variabilidad de estas inclinaciones (Fig. 2), la presencia de
palabras solapadas, el uso de acentos que aparecen en muchos idiomas como el
español, etc.
Fig.1: Líneas de texto inclinadas Fig.2: Distintas inclinaciones en un mismo texto
Son muchos los estudios que se han llevado a cabo para intentar solventar en la
medida de lo posible este problema. Sin embargo, la amplia cantidad de factores a
tener en cuenta para la detección de una línea dificulta considerablemente la
obtención de un método que proporcione resultados óptimos.
La aplicación de Hough resuelve el problema de las líneas inclinadas. Sin embargo, las
5
distintas orientaciones de las inclinaciones dentro del mismo texto son difíciles de
manejar. En nuestro caso, vamos a intentar mejorar las técnicas básicas para poder
tratar todos estos inconvenientes. En concreto, el método consiste en aplicar la
transformada de Hough sobre una serie de bloques en los que ha sido dividido el texto
previamente y realizar una votación entre ellos que determinará que bloques
pertenecen a una misma línea. Para ello, será necesario dividir la imagen del texto en
tres subdominios, cada uno de los cuales será tratado de una manera concreta para
que las líneas puedan ser obtenidas de la manera más aproximada posible. Y, por
último, es posible realizar una mejora que permita la distinción entre caracteres
conectados verticalmente.
6
2. PROBLEMA TEÓRICO
2.1. PLANTEAMIENTO DEL PROBLEMA
Dada una imagen que representa un texto escrito a mano, se desean obtener las rectas
que unen todas las palabras que pertenecen a una misma línea del texto. Dichas líneas
del texto pueden presentar diferentes inclinaciones, acentos, letras sobrepuestas
verticalmente, etc.
2.2. CONSIDERACIONES PREVIAS
Como se ha mencionado previamente, la mayoría de las técnicas que se utilizan para la
detección de líneas de texto, no tienen en cuenta todos los factores que pueden influir
en el problema o simplemente suponen que dichos factores no aparecen en el texto
considerado.
En el caso de métodos basados en la Transformada de Hough, sólo se tiene en cuenta
un punto por cada componente conexa a la hora de llevar a cabo la votación. Esto
puede dar lugar a error pues una componente conexa puede ser una palabra completa
pero también una marca de acentuación y en ambos casos se considera un punto de
ellas para la votación. En este sentido, no deberían tener la misma importancia pues
lógicamente la palabra debería tener un peso mayor en el proceso.
En el método que se va a utilizar, se intenta solventar este problema típico de Hough
junto que todos los fallos comunes de los métodos de detección de líneas.
2.3. METODOLOGÍA
El método propuesto para la detección de líneas de texto pretende tratar los
siguientes problemas:
- Cada línea de texto que aparece en el documento puede presentar
un ángulo de inclinación arbitrario y dicho ángulo puede ser distinto
para cada línea del texto.
- Los acentos pueden estar por encima o por debajo de la línea de
texto (no es el caso del español en el que todas las tildes se
encuentran por encima pero sí de otros idiomas).
- Las líneas de texto pueden encontrarse solapadas.
El proceso consiste en tres pasos principales:
7
- Pre-Procesado: Binarización de la imagen, extracción de
componentes conexas, estimación de altura media de caracteres y
particionado en tres subconjuntos.
- Transformada de Hough: Detecta las líneas potenciales del texto.
- Post-Procesado: Corrige posibles errores en la detección.
2.3.1. PRE-PROCESADO
2.3.1.1. Binariazación:
La binarización es un proceso que permite transformar una imagen en escala de
grises con varios niveles de gris o una imagen RGB en otra imagen en escala de
grises pero sólo con dos colores: blanco y negro (0 y 1 respectivamente).
Este proceso será necesario para poder aplicar nuestro método no sólo a
imágenes en blanco y negro sino también a imágenes en color o para ajustar los
niveles de la imagen de manera que el resultado obtenido sea el más óptimo
posible.
2.3.1.2. Extracción de componentes conexas:
Es necesario realizar el cálculo de las componentes conexas de la imagen para
posteriormente poder realizar su la división en subdominios. Para llevar a cabo
este proceso se utiliza el Algoritmo de cálculo de componentes conexas usando
la 8-adyacencia en negro.
Este algoritmo consiste en recorrer la imagen de izquierda a derecha y de arriba
abajo siguiendo los siguientes pasos:
1. Durante el primer rastreo, para cada punto P(x,y) que tenga valor 1,
examinamos a los vecinos superiores A(x-1,y-1), B(x-1,y), C(x-1,y+1) y
D(x,y-1); nótese que si existen, acaban de ser visitados por el rastreo, así
que si son pixeles negros, ya han sido etiquetados.
• Si todos son 0’s, damos a P una nueva etiqueta;
• si tan sólo uno es 1 le damos a P la etiqueta del otro;
• Y si hay más de uno que no es 0’s, le damos a P la etiqueta de
uno de ellos, y si sus etiquetas son diferentes, registramos el
hecho de que son equivalentes, i.e., pertenecen a la misma
componente.
8
2. Cuando se completa el primer rastreo, cada pixel negro tiene una
etiqueta, pero puede que se asignen muchas etiquetas diferentes a
puntos en el mismo componente.
Ahora ordenamos las parejas equivalentes en clases de equivalencia, y
escogemos una etiqueta para representar cada clase.
Finalmente, realizamos un segundo rastreo de la imagen y sustituimos
cada etiqueta por el representante de cada clase; cada componente ha
sido ahora etiquetada de forma única.
3. Una vez que hayamos etiquetado las componentes conexas,
sabremos cuántas componentes tiene, ya que es justo el número de
etiquetas finales usadas.
2.3.1.3. Estimación de altura media de los caracteres:
Una vez realizado el paso anterior, tendremos componentes conexas que
describan un carácter, varios caracteres, una palabra completa, acentos,
caracteres superpuestos, etc. Para poder realizar el paso siguiente, será
necesario calcular la altura media (AH) de los caracteres del texto completo
(Fig. 3).
Fig. 3: Altura media de los caracteres de un texto
2.3.1.4. Partición en subconjuntos:
Como se ha mencionado en el punto anterior, cada componente conexa puede
ser un elemento del texto distinto. Esta variabilidad hace necesaria la
subdivisión de las componentes en tres subdominios para evitar los problemas
mencionados previamente a la hora de realizar la votación en la Transformada
de Hough. Los subdominios serán los siguientes:
- Subdominio 1: Contiene a las componentes correspondientes a la
mayoría de los caracteres cuyo tamaño satisface la siguiente
9
restricción:
donde H y W representan la altura y la anchura de la componente
respectivamente y AH y AW la altura y la anchura medias del texto
completo.
Este subconjunto servirá para excluir los acentos y las componentes
altas que pertenezcan a más de una línea de texto. (Fig.4)
Fig.4: Componentes pertenecientes al Subdominio 1
- Subdominio 2: Contiene todas las componentes conexas grandes.
Estas componentes serán o bien letras mayúsculas o caracteres de
líneas de texto adyacentes que lleguen a tocarse. Estas
componentes serán aquellas que cumplan la siguiente ecuación:
Este subconjunto englobará a aquellas componentes que existen
debido a líneas de texto que se tocan. Asumiremos que la altura
correspondiente será tres veces mayor que la media.(Fig.5)
Fig.5: Componentes pertenecientes al Subdominio 2
- Subdominio 3: Contendrá caracteres tales como acentos, signos de
puntuación y pequeños caracteres. La ecuación que describe este
subconjunto es:
Es decir, contendrá aquellos símbolos cuya anchura es menos de la
10
mitad de la media o aquellos cuya altura sea menos de la mitad de la
media de la altura.(Fig.6)
Fig.6: Componentes pertenecientes al Subdominio 3.
2.3.2. TRANSFORMADA DE HOUGH
La Transformada de Hough es un algoritmo que permite encontrar ciertas
formas dentro de una imagen, como líneas, círculos, etc. En nuestro caso nos
interesan las líneas. Opera principalmente de forma estadística y consiste en
que para cada punto que se desea averiguar si es parte de una línea se aplica
una operación dentro de cierto rango, con lo que se averiguan las posibles
líneas de las que puede ser parte el punto. Esto se continúa para todas los
puntos en la imagen, al final se determina qué líneas fueron las que más puntos
posibles tuvieron y esas son las líneas en la imagen.
La Transformada de Hough sólo tendrá en cuenta aquellas componentes que
pertenezcan al subdominio 1 por las siguientes razones:
- Se garantiza que las componentes que aparezcan en más de una
línea de texto no tomarán parte de la votación.
- Desecha componentes de tamaño pequeño como pueden ser los
acentos. Esto evita detección de líneas que en realidad no lo son.
En lugar de considerar un solo punto por cada componente conexa como
ocurre en otras aplicaciones de esta transformada para la detección de líneas
de texto, se realizará otra partición de cada una de las componentes conexas
pertenecientes al subdominio 1 para tener más puntos representativos a la
hora de realizar la votación en el dominio de Hough. En concreto, cada
componente conexa considerada del subdominio1 será particionada en bloques
del mismo tamaño. El número de bloques en el que quedará dividida cada
componente conexa viene dado por la siguiente ecuación:
11
Donde representa la anchura de la componente conexa y AW la media de la
anchura de todas las componentes conexas de la imagen (Fig.7). Una vez hecho
esto, se calculan los centros de gravedad de las componentes conexas
contenidas en cada bloque. Éstos serán los que se usen posteriormente para
realizar la votación.
Fig.7: Subdivisión en bloques de las componentes conexas del Subdominio 1
La Transformada de Hough transforma líneas del espacio Cartesiano en puntos
del espacio de las coordenadas Polares. Una línea en el espacio de las
coordenadas cartesianas se define como:
Cada línea del espacio Cartesiano se representa por un punto en el espacio de
las coordenadas polares cuyas coordenadas son y , donde es la longitud
desde el origen hasta la recta y es el ángulo que forma con el eje x (Fig.8)
Fig.8:Correspondencia entre espacio Cartesiano y coordenada Polar
Se tendrá una matriz acumuladora que representará la discretización del
espacio - . Cada punto transformado del espacio Cartesiano a coordenadas
Polares corresponde a un conjunto de celdas de la matriz acumuladora. En
función del número de divisiones de divisiones de los ejes y , obtendremos
mayor o menor precisión. En este caso concreto, la construcción del espacio de
Hough se llevará a cabo dejando que tome valores en el rango de 85 -95 y
será 0.2*AH.
Una vez obtenido el espacio de Hough mediante la computación de la matriz
acumuladora, se procede a realizar la votación. En primer lugar se busca la
12
celda que tenga la máxima contribución y asignamos a la línea de texto
todos los puntos que votan en el área . Para
decidir si una componente conexa pertenece o no a una línea de texto, al
menos la mitad de los puntos que representan el bloque correspondiente
deben estar asignados a ese área.
Una vez realizada la asignación de una componente conexa a una línea de
texto, se elimina de la matriz acumuladora todos los votos correspondientes a
esta componente conexa considerada. Este procedimiento se repite hasta que
la celda que tenga la máxima contribución contiene menos de votos
para evitar detecciones falsas.
2.3.3. POST-PROCESADO
El post-procesado consiste en analizar las componentes conexas grandes
(subdominio dos) y añadir sus centros de gravedad a los que utilizará la
Transformada de Hough para detectar las líneas del texto.
Estas componentes se dividen a su vez en dos casos diferenciados que hay que tratar aparte: A) Cuando la componente conexa es mayor que la media debido a que dos
palabras de líneas distintas se pisan.
B) Cuando la componente conexa es ligeramente mayor que la media del texto, sin pisar con componentes de líneas adyacentes.
Para el primero de los casos (A) dividimos las componentes en dos, añadiendo los centros de gravedad obtenidos a los que ya teníamos del subdominio 1. Para el segundo caso (B), tratamos la componente como si de una normal se tratara, añadiendo sus centros de gravedad a los obtenidos anteriormente.
13
3. PROBLEMA PRÁCTICO: LA APLICACIÓN INFORMÁTICA
3.1. DESCRIPCIÓN DE LA APLICACIÓN
En este apartado se llevará a cabo una descripción de las características de la
aplicación realizada así como detalles sobre la implementación de la misma tales como
lenguaje de programación y software utilizados. Los algoritmos principales utilizados
para la detección de líneas de texto se encuentran implementados en la clase
MiImagen.java, por ello, será esta la que se explique con mayor detenimiento.
La aplicación está implementada para trabajar con imágenes en formato JPG, por lo
que se recomienda que se utilice este formato a la hora de probar el programa. Hemos
adjuntado una carpeta, imágenes_prueba, donde están las imágenes con las que
hemos estado trabajando y han sido utilizadas para hacer la experimentación.
3.1.1. LENGUAJE DE PROGRAMACIÓN
La aplicación ha sido desarrollada íntegramente en Java utilizando la versión
más reciente de su entorno de ejecución, es decir, Java Runtime Environment
Version 1.6.0 y se ha utilizado el entorno de programación Eclipse en sus
versiones SDK y Ganymede.
Para el entorno gráfico se ha utilizado la librería Java Swing.
El empleo de este lenguaje de programación hace posible que la aplicación sea
portable, es decir, que pueda ser ejecutada en cualquier sistema operativo que
tenga instalado el correspondiente paquete Java. Puesto que no se han
utilizado características exclusivas de la versión 1.6, la aplicación debería
funcionar correctamente en cualquier entorno que disponga de una versión
anterior hasta la 1.4.
3.1.2. ESTRUCTURA DE PAQUETES Y CLASES
Para mayor claridad a la hora de trabajar con el código, se han organizado las
distintas clases del proyecto en cuatro paquetes (Fig.9).
14
Fig.9: Estructura del proyecto
El contenido de los paquetes es el que se explica a continuación:
Entorno: Contiene las clases relacionadas con la creación y el manejo de la
interfaz gráfica. Cada uno de los elementos que conforman dicha interfaz serán
descritos con mayor detenimiento más adelante.
- BarraPrincipal.java: Implementa el menú principal de la aplicación.
- Lienzo.java: Esta clase modela los espacios de la ventana que
servirán para alojar las imágenes.
- Menu.java: Implementa la barra de menú.
- VentanaPrincipal.java: Agrupa todos los elementos en lo que será la
ventana principal de nuestra aplicación con sus respectivas
funcionalidades.
Imagen: Contiene a la clase más importante de nuestro proyecto, la clase
MiImagen.java. En ella se encuentran implementados todos los algoritmos
necesarios para poder detectar las líneas del texto. Cada uno de estos
algoritmos serán explicados detalladamente en el apartado 3.3.
Oyentes: Contiene las clases que se encargan de recoger las acciones que se
realizan en la aplicación, es decir, el manejo de menús, botones, etc. Estas
clases son las encargadas de llamar a los métodos de la clase MiImagen para
aplicarle a la imagen que se esté considerando en cada momento la
transformación elegida.
- BotonOyentes.java: Asigna a cada botón de la barra principal su
función correspondiente para que puedan funcionar al hacer click
sobre ellos.
15
- MenuOyentes.java: Asigna a cada opción de la barra de menú su
función asociada.
- SliderOyentes.java: Hace que funcione la barra desplazable que
permite variar el umbral de binarización.
Principal: Contiene a la clase Principal.java a partir de la cual se ejecuta la
aplicación.
3.2. MANUAL DE USUARIO
Para el correcto funcionamiento de la aplicación, se recomienda disponer de la última
versión de Java. Esta puede obtenerse de manera gratuita en www.java.com/es . Una
vez instalada, basta con hacer doble click sobre el archivo “nombredelarchivo”.jar para
arrancar la aplicación (Fig.10).
3.2.1. DESCRIPCIÓN DE LA INTERFAZ GRÁFICA DE USUARIO
La ventana principal que se muestra al arrancar está dividida en cuatro partes:
Barra de Menú, Imágenes, Barra Principal y Barra de estado. Cada una de ellas
será explicada con detalle a continuación.
Figura 10: Ventana Principal de la Aplicación
16
La zona denominada Imágenes se encuentra dividida en cuatro lienzos (Fig.11),
cada uno de los cuales mostrará una imagen dependiendo de la fase en la que
nos encontremos. El primero de los lienzos mostrará la imagen original, el
segundo la imagen binarizada, el tercero la imagen mostrando los subconjuntos
en los que ha quedado dividida después de realizar el pre-procesado y por
último el resultado de la transformada de Hough. Existe un lienzo adicional que
se despliega como una ventana independiente para mostrar el resultado final
del proceso completo.
Fig.11: División de la ventana en Lienzos
El funcionamiento de cada uno de estos lienzos es bastante simple y se limita a
mostrar el resultado de cada una de las etapas una vez obtenido, añadiendo
una barra de desplazamiento en caso de que la imagen sea demasiado grande
para ser mostrada en el espacio destinado para ello (Fig.12).
Fig.12: Imagen mostrada con barras de desplazamiento
En la parte superior de la ventana disponemos de un menú con todas las
opciones disponibles (Fig.13). Este menú se encuentra organizado en tres
submenús, cada uno de ellos con sus correspondientes funcionalidades que se
detallan a continuación.
Fig.13: Barra de Menú
17
Archivo: Funciones básicas de apertura, cierre y guardado de la imagen.
Permite además salir de la aplicación. La opción de guardar da la posibilidad de
salvar cualquiera de las imágenes obtenidas en las distintas fases (Fig.14).
Fig.14: Vista del Menú Archivo con todas sus opciones
Operaciones: Este menú tiene la misma funcionalidad que los botones de la
Barra Principal, que será explicada más adelante, y permite básicamente aplicar
los distintos algoritmos sobre la imagen (Fig.15).
Figura 15: Vista del Menú Operaciones
Ayuda: Dentro de este submenú se encuentra la opción Acerca de… que
muestra información sobre la aplicación y las autoras.
En la parte derecha de la ventana se encuentra la Barra Principal (Fig.16). Es en
dicha barra donde se encuentra toda la funcionalidad de la aplicación. Ésta está
pensada para ejecutarse de manera didáctica y por pasos. Es por ello que no se
puede aplicar una transformación sin haber aplicado la anterior previamente.
Los botones aparecen, por tanto, desactivados y no se pueden utilizar hasta
que no se llevado a cabo la fase anterior.
18
Figura 16: Barra Principal
Los botones de los que se dispone son los siguientes:
Binarizar: Efectúa la binarización sobre la imagen original. Para que este
proceso pueda llevarse a cabo, es necesario haber abierto previamente una
imagen. Se dispone además de una barra desplazable sobre el botón que
permite ajustar de manera dinámica el umbral utilizado para la binarización.
Subdominios: Este botón permite, a partir de la imagen binarizada, obtener una
representación de los tres subdominios en los que queda dividida. Debajo del
mismo aparece también una leyenda que indica el color que toma cada uno de
los subdominios.
Trans.Hough: Permite aplicar la transformada de Hough a la imagen pre-
procesada y muestra el resultado en el dominio de Hough. Sobre ella aparece
otra barra de desplazamiento en la que podemos elegir el número de puntos
mínimos (en tanto por ciento) que se tendrán en cuenta a la hora de realizar la
votación.
Imagen Final: Este botón despliega una nueva ventana con el resultado del
proceso completo, es decir, muestra la imagen original con las líneas que se han
detectado sobre ella después de llevar a cabo todos los pasos anteriores
(Fig.17).
Imagen Final + Mejoras: Mediante este botón, se le aplica un post-procesado a
la imagen final, obteniendo así una versión mejorada de la misma. El resultado
se muestra en una ventana similar a la anterior.(Fig.17)
19
Fig.17: Ventanas que muestran la imagen resultante con y sin mejora.
Por último, en la parte inferior de la ventana se encuentra la Barra de Estado
(Fig.18). Esta barra muestra información variada sobre la ejecución del
programa o las características de la imagen dependiendo de la fase en la que
nos encontremos y además, tiene un botón que al pulsarlo, muestra
información adicional sobre los pasos realizados.
Fig.18: Ejemplo de barra de estado mostrando información de la imagen
3.2.2. INSTRUCCIONES PARA UNA EJECUCIÓN BÁSICA
Una vez familiarizados con el entorno, se van a describir los pasos básicos
necesarios para manejar la aplicación.
En primer lugar, es necesario abrir la imagen con la que se quiere trabajar. Para
ello usamos el menú Archivo->Abrir Imagen. Elegimos la imagen deseada y ésta
aparecerá en el lienzo Imagen Original.
Una vez abierta la imagen, se activa el botón Binarizar. Elegimos el umbral
deseado y pulsamos en el mismo para obtener la imagen binarizada. Si no
hemos quedado satisfechos con el resultado obtenido, podemos modificarlo
dinámicamente desplazando con el ratón la barra deslizable que marca el
umbral.
20
Al obtener la imagen binarizada, se activa automáticamente el botón
Subdominios y al pulsar en el mismo obtenemos la imagen dividida en
subdominios en el apartado reservado para ello.
A continuación podemos hacer click en el botón Trans.Hough para obtener la
correspondiente transformada en el último de los lienzos. De nuevo podemos
modificar dinámicamente el resultado de este proceso haciendo uso de la barra
de desplazamiento que aparece sobre ella.
Todas estas funciones pueden aplicarse también a partir de sus equivalentes en
el menú Operaciones y podemos salvar cualquiera de los resultados de cada
fase utilizando Archivo->Guardar y eligiendo la imagen que queramos guardar.
Por último, sólo faltaría obtener el resultado final haciendo click en Imagen
Final. Este mostrará una primera aproximación que puede ser mejorada
utilizando el botón siguiente, Imagen Final + Mejoras.
Si en algún momento de este proceso cerramos la imagen utilizando Archivo-
>Cerrar o simplemente abrimos una nueva imagen, el contenido de los lienzos
se resetea para poder aplicar las transformaciones desde cero a la nueva
imagen elegida.
Finalmente, para salir de la aplicación basta con pulsar en el botón de la barra
de título destinado para ello como en cualquier aplicación o elegir la opción
Salir del menú Archivo.
3.3. IMPLEMENTACIÓN DE ALGORITMOS: LA CLASE MIIMAGEN
Dentro de esta clase se encuentra implementada la mayor parte de la funcionalidad de la aplicación. Contiene los algoritmos para el tratamiento de la imagen (que serán explicados detalladamente a continuación) así como los siguientes atributos:
BufferedImage img;
Almacena la imagen en sí. El tipo BufferedImage facilita su tratamiento en memoria.
int anchura;
Este atributo servirá para almacenar la anchura de la imagen que se esté tratando en cada momento.
int altura;
Este atributo servirá para almacenar la altura de la imagen que se esté tratando en cada momento.
int AH;
21
Servirá para guardar la altura media de los caracteres del texto una vez calculado para que dicha altura pueda ser utilizada con facilidad por los métodos que lo necesiten.
int AW;
Servirá para guardar la anchura media de los caracteres del texto una vez calculado para que dicha anchura pueda ser utilizada con facilidad por los métodos que lo necesiten.
Hashtable<Integer, int[]> sub1 ;
Hashtable<Integer, int[]> sub2 ;
Hashtable<Integer, int[]> sub3 ;
Estos Hashtable servirán para almacenar respectivamente los subdominios 1, 2 y 3 una vez calculados. El tipo Hashtable permite representar una colección de pares clave-valor organizados según el valor de la clave.
LinkedList<int[]> puntosSub1 ;
LinkedList<int[]> puntosSub2 ;
LinkedList<int[]> puntosSub3 ;
Esta lista almacena los centros de gravedad (x,y) del subconjunto al que corresponden. Hashtable<Integer, List<int[]>> lineasConCentrosDeGravedad;
Este Hashtable asocia cada línea (un identificador de la misma) con la lista de centros de gravedad que se le han asignado.
List<int[]> centrosg;
Lista de centros de gravedad que permite mantenerlos almacenados de una transformación a otra.
List<int[]> lineas= new LinkedList<int[]>();
Lista de líneas pintadas en la imagen final, identificadas por las coordenadas de el punto origen y las de el punto final de la misma.
Hashtable<int[],List<int[]>> tabla;
Hashtable que almacena los valores (p,tetha) con la lista de centros de gravedad que cumplen al transformarlos a coordenadas polares.
private boolean mejora;
Atributo lógico que permite activar el procesado de los filtros o no según la opción que estemos activando desde la interfaz gráfica. private LinkedList<int[]> centrosMejorados;
Lista de centros de gravedad -coordenadas (x, y)- que queremos añadir una vez realizada la mejora y el filtrado de la imagen resultante.
22
Todos estos atributos se inicializan haciendo uso de los constructores
correspondientes de la clase y serán utilizados, junto con las variables locales
necesarias, en los algoritmos explicados a continuación.
Hay que destacar también el uso de atributos de tipo WritableRaster. Éste proporciona
una manera sencilla de realizar modificaciones en la imagen actuando sobre su raster,
es decir, sobre la malla que la representa.
3.3.1. BINARIZACIÓN Para realizar la binarización de la imagen se ha implementado el siguiente método: public MiImagen binarizar() {
BufferedImage bufferBin = new BufferedImage(anchura, altura,
BufferedImage.TYPE_BYTE_BINARY);
BufferedImage bufferGris;
bufferGris = escalaGris(this);
// Algoritmo de Binarización mediante un umbral
WritableRaster rasterBin = bufferBin.getRaster();
WritableRaster rasterGris = bufferGris.getRaster();
int[] pixelGRIS = new int[1]; // va almacenando los valores GRAY de
// cada uno de los pixeles de la imagen
// en gris
int[] blanco = { 255 }; // color blanco
int[] negro = { 0 }; // color negro
for (int i = 0; i < anchura; i++)
for (int j = 0; j < altura; j++) {
rasterGris.getPixel(i, j, pixelGRIS);
if (pixelGRIS[0] > Principal.umbral)
rasterBin.setPixel(i, j, blanco);
else
rasterBin.setPixel(i, j, negro);
}
return (new MiImagen(bufferBin));
}
Este método hace uso de:
private BufferedImage escalaGris(MiImagen mi) {
BufferedImage bi = new BufferedImage(mi.anchura, mi.altura,
BufferedImage.TYPE_BYTE_GRAY);
WritableRaster rasterRGB = mi.img.getRaster();
WritableRaster rasterGRIS = bi.getRaster();
int[] pixelRGB = new int[4]; //va almacenando los valores RGBde
// cada uno de los pixeles de la
// imagen original
double[] pixelGRIS = new double[1]; // va almacenando los
// valores GRAY de cada uno
// de los pixeles de la
// imagen en gris
// Algoritmo para pasar a escala de gris
for (int i = 0; i < mi.anchura; i++)
23
for (int j = 0; j < mi.altura; j++) {
rasterRGB.getPixel(i, j, pixelRGB);
pixelGRIS[0] = (double) (pixelRGB[0] * 0.299 +
pixelRGB[1] * 0.587 + pixelRGB[2] * 0.114);
rasterGRIS.setPixel(i, j, pixelGRIS);
}
return bi;
}
El funcionamiento es el siguiente: En primer lugar, se convierte la imagen original a escala de grises utilizando el método escalaGris. Este método aplica la transformación siguiente a cada uno de los píxeles para obtener su equivalente en escala de gris:
pixelGris = R * 0.0299 + G * 0.587 + B * 0.114
Donde R, G y B representan respectivamente los niveles de rojo, verde y azul de un pixel determinado de la imagen.
Una vez realizado este proceso, volvemos a recorrer la imagen en el método binarizar() para convertir cada uno de sus pixeles en blanco o negro dependiendo de un determinado valor umbral que se elige mediante la barra de desplazamiento destinada para ello en la barra principal.
Si el valor del pixel considerado en cada momento es mayor que el umbral, se convierte a blanco. Si por el contrario es menor, pasará a ser negro. Con esto obtenemos la primera imagen de nuestra aplicación.
3.3.2. CÁLCULO DE SUBDOMINIOS
Para poder obtener los distintos subdominios, primero se deben hallar las
componentes conexas de la imagen. Para ello, en primer lugar se utiliza el siguiente
método:
public MiImagen subsets() {
WritableRaster rasterCon = img.getRaster();
// Imagen que iremos pintando con las componentes conexas
int[][] matriz = new int[anchura][altura];
// matriz para guardar las componentes conexas
int[] pixel = new int[1];
// guarda el pixel que se está tratando en cada momento
int[] pixel_arriba = new int[1];
int[] pixel_diag_sup_izqda = new int[1];
int[] pixel_izqda = new int[1];
int[] pixel_diag_inf_izqda = new int[1];
int cont_et = 1;
// Hallar componentes conexas con la 8-adyacencia
for (int i = 1; i < anchura - 1; i++) {
for (int j = 1; j < altura - 1; j++) {
rasterCon.getPixel(i, j, pixel);
rasterCon.getPixel(i - 1, j - 1,
24
pixel_diag_sup_izqda);
rasterCon.getPixel(i, j - 1, pixel_arriba);
rasterCon.getPixel(i - 1, j, pixel_izqda);
rasterCon.getPixel(i - 1, j + 1,
pixel_diag_inf_izqda);
// Si el pixel actual es negro:
if (pixel[0] == 0) {
SortedSet<Integer> temp = new
TreeSet<Integer>();
// Si diagonal superior izquierda es negro;
// (es decir, tiene etiqueta)la etiqueta de
// nuestro pixel sera la del diagonal sup izqda
if (pixel_diag_sup_izqda[0] == 0) {
matriz[i][j] = matriz[i - 1][j - 1];
temp.add(new Integer(matriz[i][j]));
}
// Si el pixel de la izqda es negro ...
if (pixel_izqda[0] == 0) {
matriz[i][j] = matriz[i - 1][j];
temp.add(new Integer(matriz[i][j]));
}
// Si el pixel diag inf izqda es negro ...
if (pixel_diag_inf_izqda[0] == 0) {
matriz[i][j] = matriz[i - 1][j + 1];
temp.add(new Integer(matriz[i][j]));
}
// Si el pixel de arriba es negro ...
if (pixel_arriba[0] == 0) {
matriz[i][j] = matriz[i][j - 1];
temp.add(new Integer(matriz[i][j]));
}
// En caso de que no tenga ningun vecino en negro
if ((pixel_diag_inf_izqda[0] != 0)
&& (pixel_diag_sup_izqda[0] != 0)
&& (pixel_arriba[0] != 0) && (pixel_izqda[0] != 0)) {
// creamos una nueva etiqueta.
matriz[i][j] = cont_et;
cont_et++;
} else {
if (temp.size() > 1) {
for (int k = 0; k < (i + 1); k++) {
int fin = altura;
if (k == i) {
fin = (j + 1);
}
for (int k2 = 0; k2 < fin; k2++) {
if (temp.contains(new Integer(matriz[k][k2]))){
matriz[k][k2] = temp.first().intValue();
}
}
}
}
}
}
}
}
// Para almacenar las componentes conexas en un hashtable
Hashtable<Integer,int[]> componentes
=almacenarComponentesConexas(matriz);
25
// Calcular altura/anchura media
this.AH = calcularAlturaMedia(componentes);
this.AW = calcularAnchuraMedia(componentes);
// Calcular subconjunto
MiImagen rf = calcularSubconjuntos(componentes, AH, AW);
rf.setSub1(this.getSub1());
rf.setSub2(this.getSub2());
rf.setSub3(this.getSub3());
rf.setAH(this.getAH());
rf.setAltura(this.getAltura());
rf.setAnchura(this.getAnchura());
rf.setAW(this.getAW());
rf.setPuntosSub1(this.getPuntosSub1());
rf.tabla=this.tabla;
return rf;
}
Este método no es más que una implementación del algoritmo de cálculo de
componentes conexas con la 8-adyacencia en negro descrito en la parte teórica de
este documento.
Una vez calculada la matriz con el etiquetado de las componentes conexas, es
necesario almacenarlas de forma que nos sea sencillo poder procesar la información
de las mismas. Para ello utilizamos la función almacenarComponentesConexas.
private Hashtable<Integer, int[]> almacenarComponentesConexas(int[][] matriz)
{
Hashtable<Integer, int[]> hashAux = new Hashtable<Integer, int[]>();
for (int i = 0; i < anchura; i++) {
for (int j = 0; j < altura; j++) {
if (matriz[i][j] != 0) {
int[] puntosAux = new int[4];
int[] puntos = hashAux.get(new
Integer(matriz[i][j]));
if (puntos == null) {
puntosAux[0] = i;// + izqdo necesita el i
puntosAux[1] = i;// + dcho necesita el i
puntosAux[2] = j;// +arriba necesita el j
puntosAux[3] = j;// +abajo necesita el j
} else {
if (i < puntos[0]) {// Si i esta mas a la
izqda de p0
puntosAux[0] = i;
} else {
puntosAux[0] = puntos[0];
}
if (i > puntos[1]) {// Si i esta mas a la
dcha de p1
puntosAux[1] = i;
} else {
puntosAux[1] = puntos[1];
}
if (j < puntos[2]) {//Si j esta + arriba d p2
puntosAux[2] = j;
} else {
puntosAux[2] = puntos[2];
}
if (j > puntos[3]) {//Si j esta + abajo de p3
puntosAux[3] = j;
} else {
puntosAux[3] = puntos[3];
}
26
}
hashAux.put(new Integer(matriz[i][j]), puntosAux);
}
}
}
return hashAux;
}
Esta función almacena las componentes conexas de forma que cada una de ellas es una entrada en un LinkedList, cuya llave o identificador es el número de la etiqueta de la componente conexa que apunta a un array con cuatro elementos: el pixel más superior de la componente, el más inferior, el más a la derecha y el más a la izquierda. Esta descripción la podemos ver en la siguiente imagen:
Fig.19: Representación de componentes conexas
Para poder seleccionar a qué subdomino pertenece cada componente conexa es necesario obtener la altura y anchura medias. Para ello se utilizan las siguientes funciones:
// ########## ANCHURA MEDIA #############
private int calcularAnchuraMedia(Hashtable<Integer, int[]>
componentes) {
int contador_etiquetas = 0;
int sumas_anchura = 0;
int[] array = new int[4];
//Habrá q recorrer cada etiqueta y calcular su derecho-izquierdo
// e ir sumando para cada elemento
Enumeration<Integer> enumer = componentes.keys();
while (enumer.hasMoreElements()) {
Integer llave = (Integer) enumer.nextElement();
array = componentes.get(llave);
sumas_anchura = sumas_anchura + (array[1] - array[0]);
contador_etiquetas++;
}
return sumas_anchura / contador_etiquetas;
27
}
// ########## ALTURA MEDIA #############
private int calcularAlturaMedia(Hashtable<Integer, int[]> componentes)
{
int contador_etiquetas = 0;
int sumas_altura = 0;
int[] array = new int[4];
//Habrá q recorrer cada etiqueta y calcular su derecho-izquierdo
// e ir sumando para cada elemento
Enumeration<Integer> enumer = componentes.keys();
while (enumer.hasMoreElements()) {
Integer llave = (Integer) enumer.nextElement();
array = componentes.get(llave);
sumas_altura = sumas_altura + (array[3] - array[2]);
contador_etiquetas++;
}
return sumas_altura / contador_etiquetas;
}
En ambos algoritmos se sigue un procedimiento similar: Gracias a la organización obtenida con la función almacenarComponentesConexas , calculamos la media de la altura de todas las componentes conexas, teniendo en cuenta que el pixel inferior menos pixel superior (B – A en la imagen superior) será la altura. Calculamos análogamente la anchura de cada componente conexa, teniendo en cuenta que ésta será el pixel más a la derecha menos el más a la izquierda (D – A en la imagen superior).
Después de asignar los valores calculados a los atributos AH y AW de la clase se procede a asinar cada componente conexa a su subdominio correspondiente utilizando el siguiente método:
private MiImagen calcularSubconjuntos(
Hashtable<Integer, int[]> componentes, int ah2, int aw2) {
int[] array = new int[4];
int h, w = 0;
Enumeration<Integer> enumer = componentes.keys();
while (enumer.hasMoreElements()) {
Integer llave = (Integer) enumer.nextElement();
array = componentes.get(llave);
h = array[3] - array[2];
w = array[1] - array[0];
if ((h < (3 * ah2)) && (h >= (ah2 * 0.5)) && (w >= (0.5 *
aw2))) {
sub1.put(new Integer(llave), array);
}
if (h >= (3 * ah2)) {
sub2.put(new Integer(llave), array);
}
if (((h < 3 * ah2) && (w < (0.5 * aw2))) || ((h < (0.5 *
ah2)) && (w > (0.5 * aw2)))) {
sub3.put(new Integer(llave), array);
}
}
MiImagen rf = pintaSubconjunto();
return rf;
}
28
Esta función recorre las componentes conexas y las va almacenando en el subdominio
que le corresponde.
En el subdominio uno, las que cumplan las restricciones:
Altura de la componente < 3· Altura media
Altura de la componente >= 0.5· Altura media
Anchura de la componente >= 0.5 · Anchura media
En el subdominio dos, las que cumplan la restricción:
Altura de la componente >= 3· Altura media
En el subdominio tres, las que cumplan las restricciones:
Altura de la componente < 3· Altura media y Anchura de la componente <
0.5 · Anchura media
O
Altura de la componente < 0.5· Altura media y Anchura de la componente >
0.5 · Anchura media
Tras esta clasificación, en el subconjunto primero tendremos las componentes conexas
de tamaño medio, en el segundo las de mayor tamaño y en el tercero las de tamaño
menor (puntos, comas, tildes, etc.)
Finalmente, se llama al método pintaSubconjunto que será el encargado de
representar sobre la imagen los distintos subconjuntos que hemos obtenido, cada uno
de un color diferente, para así poder diferenciarlos más fácilmente.
private MiImagen pintaSubconjunto() {
BufferedImage bufferFinal = new
BufferedImage(Principal.imagenOriginal.getAnchura(),
Principal.imagenOriginal.getAltura(),
BufferedImage.TYPE_INT_RGB);
WritableRaster rasterE =
Principal.imagenBinarizada.img.getRaster();
// Colores necesarios para pintar la imagen subsets
int[] blanco = { 255, 255, 255 }; // blanco
int[] negro = { 0, 0, 0 }; // negro
int[] pixelRojo = { 255, 0, 0 }; // ROJO
int[] pixelVerde = { 0, 255, 0 }; // verde
int[] pixelAzul = { 0, 0, 255 }; // azul
int[] array = new int[4];
int[] xaras = new int[3];
MiImagen Final = new MiImagen(bufferFinal);
WritableRaster rasterFinal = Final.img.getRaster();
// Copiamos en el bufferFinal la imagen binarizada
for (int i = 0; i < anchura; i++) {
for (int j = 0; j < altura; j++) {
rasterE.getPixel(i, j, xaras);
29
if (xaras[0] == 1)
rasterFinal.setPixel(i, j, blanco);
else
rasterFinal.setPixel(i, j, negro);
}
}
// Pintamos las lineas de los 3 subconjuntos
Enumeration<Integer> enumer = sub1.keys();
LinkedList<int[]> puntos=new LinkedList<int[]>();
while (enumer.hasMoreElements()) {
Integer llave = (Integer) enumer.nextElement();
array = sub1.get(llave); // los puntos
rasterFinal = pintaLinea(rasterFinal, array, pixelRojo);
puntos.addAll(generaPuntos(array));
}
this.setPuntosSub1(puntos);
enumer = sub2.keys();
while (enumer.hasMoreElements()) {
Integer llave = (Integer) enumer.nextElement();
array = sub2.get(llave); // los puntos
rasterFinal = pintaLinea(rasterFinal, array, pixelVerde);
}
enumer = sub3.keys();
while (enumer.hasMoreElements()) {
Integer llave = (Integer) enumer.nextElement();
array = sub3.get(llave); // los puntos
rasterFinal = pintaLinea(rasterFinal, array, pixelAzul);
}
return Final;
}
Dentro de esta función llamamos al método auxiliar pinta línea, que es el encargado de
dibujar las líneas de las componentes conexas sobre la imagen:
private WritableRaster pintaLinea(WritableRaster rasterFinal, int[]
array, int[] data) {
for (int i = array[0]; i <= array[1]; i++) {
rasterFinal.setPixel(i, array[2], data);
rasterFinal.setPixel(i, array[3], data);
}
for (int j = array[2]; j <= array[3]; j++) {
rasterFinal.setPixel(array[0], j, data);
rasterFinal.setPixel(array[1], j, data);
}
return rasterFinal;
}
Esta función se basa en la forma de describir las componentes conexas para pintar el
recuadro de la misma; calculando los puntos entre los que tiene que pintar cada recta
de la forma que podemos ver en la siguiente ilustración:
30
Fig.20: Representación del recuadro de una componente conexa
Se utiliza además el siguiente método auxiliar para generar los puntos de la forma
descrita arriba; donde recorremos el array con la información de los píxeles superior,
inferior, más a la derecha y más a la izquierda de la componente:
private LinkedList<int[]> generaPuntos(int[] array){
LinkedList<int[]> aux=new LinkedList<int[]>();
int[] a=new int[2];
for(int i=array[0];i<=array[1];i++)
{
for(int j=array[2];j<=array[3];j++)
{
a[0]=i;
a[1]=j;
aux.add(a);
}
}
return aux;
}
Por último, se tienen también los siguientes dos métodos que sirven respectivamente para obtener y almacenar los puntos del subdominio 1 para que puedan ser utilizados posteriormente:
public LinkedList<int[]> getPuntosSub1() {
return puntosSub1;
}
public void setPuntosSub1(LinkedList<int[]> puntosSub1) {
this.puntosSub1 = puntosSub1;
}
3.3.3. TRANSFORMADA DE HOUGH
Cuando llega el momento de realizar la transformada de Hough, se realiza el algoritmo
devolviendo una imagen denominada imgHough.
########## TRANSFORMADA DE HOUGH #############
31
public MiImagen hough(){
MiImagen imgHough = algoritmoHough();
return imgHough;
}
La función que realiza el algoritmo de Hough es la siguiente. En ella se siguen los siguientes pasos explicados anteriormente:
1. Para todas las componentes del subconjunto 1 obtenemos sus centros de gravedad (se podrían utilizar todos los píxeles negros, pero con esta reducción obtenemos un mejor tiempo de computación).
2. Para cada centro de la lista: a) Calculamos la coordenada polar (p,theta) del centro a través de su
coordenada cartesiana dada por el punto (x,y). b) Aumentamos el valor de la acumuladora para esa coordenada (p,theta).
3. Una vez obtenida la acumuladora, vamos obteniendo (mientras que tenga un valor mayor a n1) la coordenada (p,theta) que mayor valor haya tenido, aceptándola para una componente dada, si más de la mitad de los centros de gravedad de la componente está en el área (p+5,theta), (p-5,theta).
4. Cuando consigamos la lista de centros en tabla, ya tendremos las líneas para dibujarlas.
public MiImagen algoritmoHough() {
//el número de divisiones del eje Theta, anchura ---> 0∫ a 180∫ (theta)
int ejeTheta = 95;
//el n˙mero de divisiones del eje Rho, altura --->de 0 a la longitud de la
diagonal de la imagen (maxima distancia de un pixel al origen)
int ejeRho = (int)(Math.sqrt( (anchura*anchura) + (altura*altura) ));
//creamos una matriz denominada la matriz acumuladora, que no es más que la
discretización del espacio p-theta.
int[][] acumuladora= new int[ejeTheta][ejeRho];
//va a recorrer la imagen binarizada
WritableRaster rasterBin = Principal.imagenBinarizada.getImg().getRaster();
//va almacenando los valores BINARY de cada uno de los pixeles de la imagen
en binarizada)
int[] pixelBIN = new int[1];
double theta,rho;
int[] punto=new int[2];
double max2=0;
int[] ptheta;
int key;
int[] array;
tabla=new Hashtable<int[],List<int[]>>();
List<int[]> centraux=new LinkedList<int[]>();
//Calculamos todos los centros de gravedad de los componentes del
subconjunto 1 en centraux
Enumeration<Integer> iteradorComponentes = sub1.keys();
while(iteradorComponentes.hasMoreElements()){
key = iteradorComponentes.nextElement();
array = sub1.get(key);
centrosg = obtenerCentrosGravedad(array);
centraux.addAll(centrosg);
}
//Caso en el que estemos en mejora
if(this.mejora){
filtro();
32
centraux.addAll(this.centrosMejorados);
}
Iterator<int[]> it=centraux.iterator();
while (it.hasNext()){
punto = it.next();
rasterBin.getPixel(punto[0],punto[1],pixelBIN);
for (int k=85 ; k < ejeTheta; k++)
{
theta = k * 2 * Math.PI / 360;
rho = Math.abs(punto[0] * Math.cos(theta) + punto[1] *
Math.sin(theta));
acumuladora[k][Math.round(((float)(rho)))]++;
}
}
//Calculamos la celda con mayor valor (ro,tetha)
int[][] acuaux=acumuladora.clone();
ptheta=valorptheta(acuaux,ejeTheta,ejeRho);
max2=valormaxacumuladora(acuaux,ejeTheta,ejeRho);
//Mientras este valor sea mayor a n1=minPuntos calculamos la votacion
Hashtable<Integer,int[]> aux=(Hashtable<Integer,int[]>)sub1.clone();
Enumeration<Integer> iterador2;
while(max2>((5*Principal.minPuntos)/100)){
iterador2 = aux.keys();
while(iterador2.hasMoreElements()){
key = iterador2.nextElement();
array = sub1.get(key);
centrosg = obtenerCentrosGravedad(array);
int media=(int)centrosg.size()/2;
if(masmitadcentros(centrosg, media, ptheta)){
aux.remove(key);
}
}
tabla.put(ptheta,centraux);
acuaux[ptheta[1]][ptheta[0]]=0;
max2=valormaxacumuladora(acuaux,ejeTheta,ejeRho);
ptheta=valorptheta(acuaux,ejeTheta,ejeRho);
}
//Convertimos la matriz acumuladora obtenida a una imagen en escala de
grises
BufferedImage bufferHough = new BufferedImage
(ejeTheta,ejeRho,BufferedImage.TYPE_BYTE_GRAY);
WritableRaster rasterHough = bufferHough.getRaster();
int max=valormaxacumuladora(acumuladora,ejeTheta,ejeRho);
double razon = 255.0/max;
for (int x2=0; x2 < ejeTheta; x2++)
for (int y2=0;y2 < ejeRho; y2++)
{
pixelBIN[0] = (int)(acumuladora[x2][y2] * razon);
rasterHough.setPixel(x2,y2,pixelBIN);
}
MiImagen rf = new MiImagen(bufferHough);
rf.setSub1(this.getSub1());
rf.setSub2(this.getSub2());
rf.setSub3(this.getSub3());
rf.setAH(this.getAH());
rf.setAltura(this.getAltura());
rf.setAnchura(this.getAnchura());
rf.setAW(this.getAW());
rf.setLineasConCentrosDeGravedad(this.getLineasConCentrosDeGravedad());
rf.lineas=this.lineas;
rf.tabla=this.tabla;
rf.centrosg=this.centrosg;
rf.lineas=this.lineas;
rf.tabla=this.tabla;
rf.mejora=this.mejora;
33
rf.centrosMejorados=this.centrosMejorados;
return rf;
}
Las funciones auxiliares utilizadas son:
1. Dado el array de las posiciones de la componente, calcula los centros de gravedad:
private List<int[]> obtenerCentrosGravedad(int[] array) {
List<int[]> centros_gravedad = new LinkedList<int[]>();
int[] centro = new int[2];
int ancho = AW; //Ancho del conjunto
int numBlq = 1;
int anchoSub = AW; // Ancho del subconjunto
ancho = array[1]-array[0];
if (AW!=0){
numBlq = (int) Math.ceil(ancho / AW);
}else{
System.out.println("Error: ancho medio nulo");
}
int auxDerecho = array[1];
int auxIzquierdo = array[0];
int auxArriba = array[2];
int auxAbajo = array[3];
if(numBlq==0){
numBlq=1;
}
if(numBlq==1){ // Caso en que no tengamos q dividir en bloques
centro[0]= auxIzquierdo +(int)Math.floor((auxDerecho -
auxIzquierdo)/2); //La i;
centro[1]= auxArriba +(int)Math.floor((auxAbajo-auxArriba)/2);
//La j;
centros_gravedad.add(centro);
}else{
anchoSub = (int) Math.floor(ancho / numBlq);
for(int s=0;s<numBlq;s++)
{
anchoSub = (int) Math.floor(ancho / numBlq);
centro=new int[2];
centro[0]=auxIzquierdo+anchoSub/2+(s*anchoSub);
centro[1]=auxArriba+ (int)Math.floor((auxAbajo-
auxArriba)/2);
centros_gravedad.add(s,centro);
}
}
return centros_gravedad;
}
2. Calcula si más de la mitad de los centros de la componente dada están en el
rango de (p+5,teta...p-5,teta): private boolean masmitadcentros(List<int[]> centrosg, int media,
int[] ptheta) {
int cont=0;
int[] aux=new int[2];
int p=ptheta[0];
int pcentro;
double thetacentro;
for(int i=0;i<centrosg.size();i++)
{
aux=centrosg.get(i);
//Pasando a coordenadas polares
thetacentro=Math.atan2((double)aux[1], (double)aux[0]);
pcentro=(int)Math.abs(aux[0] * Math.cos(thetacentro) + aux[1] *
Math.sin(thetacentro));
//Comparar la p para ver si esta en rango p-5, p+5
34
if((p+5>=pcentro)&&(p-
5<=pcentro)&&(((ptheta[1]*Math.PI/180)==thetacentro)||((ptheta[1]*Math.PI/180)
+(2*Math.PI/180)>=thetacentro)))
{
cont++;
}
}
return (cont>=media);
}
3. Calcula el valor de (p,theta) que tiene mayor valor en la acumuladora.
private int[] valorptheta(int[][] acumuladora, int ejeTheta, int ejeRho) {
int[] aux=new int[2];
int max=0;
for (int i=0; i <ejeTheta ; i++){
for (int j=0;j < ejeRho; j++){
if(acumuladora[i][j]>max){
max=acumuladora[i][j];
aux[0]=j;
aux[1]=i;
}
}
}
return aux;
}
4. Calcula el valor máximo de la acumuladora.
private int valormaxacumuladora(int[][] acumuladora, int ejeTheta, int ejeRho) {
int max=0;
for (int i=0; i <ejeTheta ; i++){
for (int j=0;j < ejeRho; j++){
if(acumuladora[i][j]>max){
max=acumuladora[i][j];
}
}
}
return max;
}
3.3.4. IMAGEN FINAL
Para mostrar la imagen resultante tras los procesos efectuados utilizamos la función superponer(), la cual superpone la imagen binarizada con las rectas obtenidas tras la Transformada de Hough. Primero pinta la imagen en blanco y negro y luego superpone en color rojo las líneas halladas con la función drawLine(). public MiImagen superponer(boolean mejora2) {
BufferedImage bufferFinal = new
BufferedImage(Principal.imagenOriginal.getAnchura(),
Principal.imagenOriginal.getAltura(),
BufferedImage.TYPE_INT_RGB);
WritableRaster rasterE =
Principal.imagenBinarizada.img.getRaster();
int[] blanco = { 255, 255, 255 }; // blanco
int[] negro = { 0, 0, 0 }; // negro
int[] pixelRojo = { 255, 0, 0 }; // ROJO
int[] array = new int[4];
int[] xaras = new int[3];
int[] pixelInv = {0};
MiImagen Final = new MiImagen(bufferFinal);
WritableRaster rasterFinal = Final.img.getRaster();
35
for (int i = 0; i < anchura; i++) {
for (int j = 0; j < altura; j++) {
rasterE.getPixel(i, j, xaras);
if (xaras[0] == 1)
rasterFinal.setPixel(i, j, blanco);
else
rasterFinal.setPixel(i, j, negro);
}
}
Graphics2D g = Final.img.createGraphics();
g.setStroke(new BasicStroke(1.5f));
g.setPaint(Color.red);
List<int[]> listalineas = new LinkedList<int[]>();
Enumeration<int[]> iteradorComponentes
=Principal.imagenHough.tabla.keys();
int[] aux=new int[2];
int[] coordsLineas=new int[2];
int y1,y2;
double rad;
while(iteradorComponentes.hasMoreElements()){
aux = iteradorComponentes.nextElement();
rad=2*Math.PI/360*aux[1];
y1=(int)((aux[0]-(0 * Math.sin(rad)))/ Math.cos(rad));
y2=(int)((aux[0]-(anchura*Math.sin(rad)))/ Math.cos(rad));
int m = (y2 -y1)/(anchura);
if(mejora2){
if (m < -20){
g.drawLine(y1,0,y2,anchura);
coordsLineas[0]=y1;
coordsLineas[1]=y2;
listalineas.add(coordsLineas);
}
}else{
g.drawLine(y1,0,y2,anchura);
coordsLineas[0]=y1;
coordsLineas[1]=y2;
listalineas.add(coordsLineas);
}
}
Final.mejora=this.mejora;
Final.centrosMejorados=this.centrosMejorados;
Final.setSub1(this.getSub1());
Final.setSub2(this.getSub2());
Final.setSub3(this.getSub3());
Final.setAH(this.getAH());
Final.setAltura(this.getAltura());
Final.setAnchura(this.getAnchura());
Final.setAW(this.getAW());
Final.setPuntosSub1(this.getPuntosSub1());
Final.setPuntosSub2(this.getPuntosSub2());
Final.setPuntosSub3(this.getPuntosSub3());
Final.tabla=this.tabla;
return Final;
}
36
3.3.5. POST-PROCESADO
El post-procesado que hemos implementado se basa en añadir las componentes conexas grandes al conjunto de componentes que se procesan en la Transformada de Hough, hasta este momento sólo formado por las componentes de tamaño medio (subdominio 1).
private void filtro(){
int[] array = new int[4];
int[] centro = new int[2];
LinkedList<int[]> nuevoscentros = new LinkedList<int[]>();
Enumeration<Integer> it = this.getSub3().keys();
while (it.hasMoreElements()) {
Integer llave = it.nextElement();
array = this.getSub3().get(llave);
int auxDerecho = array[1];
int auxIzquierdo = array[0];
int auxArriba = array[2];
int auxAbajo = array[3];
double h = (auxAbajo - auxArriba)/2;
if(h >= (2* this.AH)){
centro[0] = auxIzquierdo
+(int)Math.floor((auxDerecho - auxIzquierdo)/2) ;
centro[1] = auxArriba +(int)Math.floor(h/2);
nuevoscentros.add(centro);
centro[1] = auxArriba
+ (int)h/2 + (int)Math.floor(h/2);
nuevoscentros.add(centro);
}else if(h < 2* this.AH){
centro[0]= auxIzquierdo
+(int)Math.floor((auxDerecho - auxIzquierdo)/2);
centro[1]= auxArriba +(int)Math.floor(h);
nuevoscentros.add(centro);
}
}
this.setCentrosMejorados(nuevoscentros);
}
La función utilizada para este cometido es filtro(), la cual trata las componentes del subconjunto 2 (las de tamaño mayor a la media).
Éstas componentes se dividen a su vez en dos casos diferenciados que hay que tratar aparte:
C) Cuando la componente conexa es mayor que la media debido a que dos
palabras de líneas distintas se pisan.
D) Cuando la componente conexa es ligeramente mayor que la media del texto, sin pisar con componentes de líneas adyacentes.
Para el primero de los casos (A) dividimos las componentes en dos, añadiendo los centros de gravedad obtenidos a los que ya teníamos del subdominio 1. Véase la figura siguiente:
37
Fig.21: Componente conexa “grande” dividida en dos mediante el proceso de filtrado. (Caso A)
Para el segundo caso (B), tratamos la componente como si de una normal se tratara, añadiendo sus centros de gravedad a los obtenidos anteriormente. Una ilustración de esto puede verse en la siguiente imagen:
Fig.22: Componente conexa “grande” procesada como si fuera del subdominio 1 (Caso B)
3.4 EXPERIMENTACIÓN
Una vez terminado el programa, hemos realizado diferentes pruebas sobre distintos
escritos escaneados cambiando los valores de umbral para la binarización (que supone
una variación importante en los subconjuntos conexos obtenidos posteriormente), y
en el umbral de la acumuladora (que permite obtener mayor cantidad de líneas de
detección al disminuir éste), realizando una métrica de verificación del algoritmo
obtenida por los mismos autores del método y definida por:
TLDM=
Siendo Det el número de líneas reales en el texto, y Rec el número de líneas
detectadas por el algoritmo. Los resultados conseguidos son los siguientes:
Imagen N1 Umbral TLDM %
Imagen1 53 63
86.6
“ 70 63 = 13.36 78.57
38
Imagen2 70 150
96.55
Imagen3 70 196
86.9
“ 55 196
96
Imagen4 70 196
93.3
“ 52 196
100
Imagen5 70 213
91.6
Imagen6 70 213
50
“ 50 213
87.5
39
4. CONCLUSIONES
Tras haber realizado el trabajo, los resultados nos han parecido bastante buenos. Los
textos probados obtenían la detección de más de un 85% de sus líneas salvo escasas
excepciones, por lo que el método funciona y funciona bien.
Sin embargo, hemos observado que se podrían realizar varias mejoras posteriores, a
parte de las ya realizadas, que nos gustaría sugerir como posible trabajo futuro y que
por falta de tiempo y/o no ser lo especificado en el artículo científico en el cual nos
hemos basado no hemos podido realizar:
1. Obtención de una sola línea por línea real de texto. Al basarse el algoritmo en
un primer cálculo de las componentes conexas, el texto se divide en áreas a las
que hay que calcular su dirección, y rectas que pasen por combinación de dos o
más áreas hay muchas, por lo que en el resultado se observan las que se han
asignado a más componentes conexas, consiguiéndose en la mayoría de los
casos más de una línea. Por lo tanto, se podría tener un contador con el
número de líneas obtenidas en áreas cercanas y quedarnos solo con una de
ellas.
2. Utilizar las componentes del subconjunto 3. Se podrían utilizar estas
componentes para modificar la pendiente de las líneas obtenidas, en base a la
cercanía de las componentes conexas a las rectas e incluso utilizándolas con un
menor peso que las del subconjunto 1.
Fig.23: Posible mejora de la aplicación utilizando las componentes del subconjunto de las imágenes pequeñas.
3. Mejorar la aplicación para que pueda funcionar correctamente con otros
formatos de imagen y no sólo con el formato JPG.
40
5. BIBLIOGRAFÍA
- Text line detection in handwritten documents. G. Louloudis, B. Gatos, I. Pratikakis, C.
Halatsis.
- L.A. Fletcher, R. Kasturi, A robust algorithm for text string separation from mixed text/graphics images, IEEE Trans. Pattern Anal. Mach. Intell. 10 (6) (1988) 910–918. - P. Hough, Methods and means for recognizing complex patterns, US Patent 3,069,654, 1962. - http://alojamientos.us.es/gtocoma/pid/ - http://java.sun.com/reference/api/ - http://www.programacion.com/java/ - http://www.wikipedia.com
41
6. CARGA DE TRABAJO
Fecha
reunión
Hora
inicio
Hora fin Interrupciones Tiempo
total
Nº miembros Actividad Comentarios
28/11/2008 12:00 13:30 0 1,30 horas 3 Estudio del TDA
anterior.
01/12/2008 10.30 12.30 0 2 horas 3 Estudio del problema y
organización del trabajo
02/12/2008 15.30 17:30 0 2 horas 1 Estudio e
implementación del
entorno gráfico
04/12/2008 8:30 10:30 0 2 horas 2 Implementación de
binarización y
componentes conexas
07/12/2008 15:00 17:00 0 2 1 Implementación de
cálculo de componentes
conexas
09/12/2008 15:30 17:30 0 2 horas 2 Implementación de
cálculo de componentes
conexas y subdominios
11/12/2008 8:30 10:30 0 2 horas 2 Implementación y
representación de
componentes conexas y
subdominios
11/12/2008 10:30 12:30 0 2 horas 1 Implementación y
representación de
componentes conexas y
subdominios
11/12/2008 15:20 17:30 0 2,10 horas 1 Implementación y
representación de
componentes conexas y
subdominios
11/12/2008 22:00 00:15 0 2,15 horas 1 Implementación y
representación de
componentes conexas y
subdominios
12/12/2008 9:15 13:30 0 4,15 horas 2 Implementación de
subdominios; anchura y
altura medias.
13/12/2008 15:45 20:45 1h 4 horas 1 Implementación de
centros de gravedad,
subconjuntos y primera
42
aproximación a
T.Hough.
15/12/2008 10:30 12:30 0 2 horas 3 Implementación de T.
Hough.
16/12/2008 15:30 19:30 0 5 horas 2 Implementación de T.
Hough.
17/12/2008 18:30 21:30 0 3 horas 1 Implementación de
T.Hough
26/12/2008 11:00 19:30 30 minutos 8 horas 1 Documentación
27/12/2008 11:00 14:00 0 3 horas 1 Documentación y
modificación del
entorno gráfico
29/12/2008 15:00 16:00 0 1 hora 1 Entorno Gráfico
31/12/2008 11:00 13:00 0 2 horas 1 Implementación de
T.Hough
01/01/2009 10:30 13:00 0 2,5 horas 1 Entorno Gráfico y
Documentación
01/01/2009 16:00 20:00 0 4 horas 1 Implementación de
T.Hough
02/01/2009 16:00 21:00 0 5 horas 1 Implementación de
T.Hough
03/01/2009 11:00 20:00 3 horas 6 horas 1 Implementación de
T.Hough
Buscando error
en coordenadas
polares.
04/01/2009 10:30 14:30 0 4 horas 1 Documentación
04/01/2009 11:45 13:30 0 1,45 horas 1 Realización de la
Presentación.
04/01/2009 16:25 21:15 0 4,10 horas 1 Realización de la
Presentación.
05/01/2009 12:00 14:00 0 2 horas 1 Realización de la
Presentación y
Documentación.
05/01/2009 15:50 20:00 1 hora 3,10 horas 1 Realización de la
Presentación y
Documentación.
06/01/2009 12:35 13:45 0 45
minutos
1 Implementación Filtros
y mejoras.
06/01/2009 15:50 21:20 30 min 6 horas 1 Implementación Filtros
43
y mejoras.
06/01/2009 10:40 14:00 10 min 3,10 horas 1 Documentación y
presentación
correspondiente a
Filtros y mejoras.
06/01/2009 17:20 23:00 1h 4,40 horas 1 Documentación y
presentación
correspondiente a
Filtros y mejoras.
Mejora de la Interfaz.
07/01/2009 11:00 13:00 0 2 horas 1 Documentación de
experimentación y
conclusiones.
07/01/2009 17:20 19:00 0 1,40 horas 1 Documentación y
Presentación. Hacer
Ejecutable.
08/01/2009 10:30 11:30 0 1 horas 1 Documentación y
Presentación. Hacer
Ejecutable.
Carga total de cada miembro del grupo:
Elena Hernández Salmerón - 30 horas
Elena Lozano Rosch - 55 horas 25 min.
Mercedes Jiménez Romero – 30’5 horas
Distribución del trabajo:
Elena Hernández Salmerón – Documentación, implementación de subdominios,
componentes conexas y transformada de Hough.
Elena Lozano Rosch – Presentación, documentación, implementación de subdominios,
componentes conexas, filtros de mejora y superposición de la imagen final.
Mercedes Jiménez Romero – Documentación, implementación de binarización y
entorno gráfico y superposición de la imagen final.
44