Nuestro Pequeño Asistente de Investigación. Parte II: Usando OCR

{:deps {org.clojure/clojure {:mvn/version "1.10.3"}
        ;; complient is used for autocompletion
        ;; add your libs here (and restart the runtime to pick up changes)
        compliment/compliment {:mvn/version "0.3.9"}}}
Extensible Data Notation

Pues bien, ahora que nos sentimos un poco más cómodos con esto de la programación, podemos enfocarnos a resolver nuestro problema: crear nuestro pequeño asistente de investigación.

¿Qué vamos a hacer?

Vamos crear un pequeño programa que tome imágenes de texto y las convierta en un string que podamos manipular.

¿Qué necesitamos?

La tecnología que nos permite convertir imagen a texto se llama OCR, esto es, Optical Character Recognition, así que necesitamos una librería que nos permita realizar este proceso.

Y ¿qué es una librería? Se trata simplemente de un programa que escribió otra persona y que aloja en un repositorio (la mayoría de las veces de forma pública y gratuita) desde donde lo podemos descargar. Una vez que lo tengamos donde corresponde podemos emplear las funciones que ese programa exponga para su consumo. A esto se le llama API o Application Programming Interface.

Una de las ventajas de Clojure y Clojurescript (un lenguaje hermano que corre en el navegador) consiste en que podemos tomar librerías escritas en los lenguajes más importantes, a saber, Java, Javascript (no tiene nada que ver con Java), Python e incluso R.

En este caso la librería está escrita en un lenguaje llamado C++, sin embargo, la podemos descargar en nuestro sistema operativo (en este caso, nuestra Notebook corre sobre Linux) y usarla desde la línea de comandos. Esto es posible ya que Linux está escrito en C y C++ y ejecutará cualquier librería escrita en estos lenguajes sin ningún problema.

Clojure nos permite comunicarnos con nuestro sistema operativo a través del Shell (o terminal). Para esto importamos la librería clojure.java.shell y le ponemos un apodo (sh) para poder llamarla luego sin tener que colocar el nombre completo.

(require '[clojure.java.shell :as sh])

Luego creamos un shell (sh/sh) y a continuación vamos a enviarle algunas instrucciones a nuestro sistema operativo.

En primer lugar, debemos pedirle que actualice la lista de paquetes disponibles para descargar. Para eso escribiremos el comando: apt-get update.

En segundo lugar, le pediremos que instale la librería Tesseract: apt-get install tesseract-ocr -y.

(sh/sh "apt-get" "update")
(sh/sh "apt" "install" "tesseract-ocr" "-y")
16.4s

Este último comando:

a) está invocando al administrador de paquetes de nuestro sistema operativo;

b) está invocando la función install;

c) le está pasando el argumento tesseract-ocr, que es nombre de la librería que necesitamos instalar;

y d) le pasamos una bandera (o flag) -y para contestar de una vez y afirmativamente al prompt ¿quiere usted instalar esta librería? (Y / N) que nos va a aparecer.

No se alarmen ya que no es necesario que entiendan al detalle todo esto de una vez. Sólo basta que se comprenda el propósito, es decir, lo que queremos lograr. Y luego, que independientemente del lenguaje en el que nos estamos comunicando con nuestro computador, el modo de comunicarnos es muy similar:

  1. Llamamos o ejecutamos un programa.

  2. Este programa ofrece un conjunto de funciones para realizar un conjunto determinado de tareas.

  3. Y llamamos a estas funciones junto con los argumentos requeridos para obtener el efecto deseado.

Ahora que ya tenemos nuestra librería instalada, necesitamos también algunas imágenes de prueba.

Lo primero que haremos será crear una estructura de datos con nuestras imágenes. Utilizaremos un vector

(def rutas-imagenes [Carlos_Reynoso_II.jpg IMG_20230312_143105081.jpg])
0.1s

A continuación, nos descargamos los modelos entrenados para reconocer los lenguajes español e inglés. En la configuración de la Notebook los ubicaremos en la carpeta /tessdata junto con otros detalles que dejaremos para luego.

eng.traineddata
15.40 MBDownload
spa.traineddata
13.57 MBDownload

Ya tenemos todo listo. Ahora vamos a llamar al programa tesseract. Para poder ejecutarlo se nos piden los siguientes parámetros:

a) la ruta de ubicación de la imagen;

b) en qué formato queremos el resultado (en este caso queremos el resultado por defecto así que escribiremos "-");

y c) si deseamos especificar otro lenguaje distinto al inglés, debemos pasar la bandera -l y el nombre abreviado del lenguaje (para nosotros será "spa" de spanish):

(def resultado (sh/sh "tesseract" Carlos_Reynoso_II.jpg "-" "-l" "spa"))
18.5s

La función sh nos devuelve un mapa como resultado y el output del comando lo guarda en una llave de nombre :out, así que utilicemos esta llave para obtener el string de nuestra foto.

(:out resultado)
0.1s

¡Genial! Hemos logrado transformar una foto en un string con el que podemos trabajar.

El resultado no es óptimo, ciertamente, pero esto ya tiene que ver con la calidad de la foto y otros detalles que se podrían ajustar.

Ahora debemos refinar un poco lo que hemos hecho. Así que creemos una función que nos ahorre tener que escribir todos esos comandos cada vez que los necesitemos. Y como sabemos que vamos a recibir un mapa y de ese mapa necesitamos sólo el valor alojado en la llave out, incluyamos eso también en nuestra función:

(defn procesar-ocr
  [imagen]
  (:out
   (sh/sh "tesseract" imagen "-" "-l" "spa")))
0.0s

Tenemos entonces una función llamada procesar-ocr que recibe un solo argumento llamado imagen. En el cuerpo tenemos una forma compuesta de dos expresiones simbólicas. Clojure ejecuta primero la expresión simbólica más interna, así que creará un Shell y llamará a tesseract pasándole el nombre de la imagen que especifiquemos como variable. Luego salta a la expresión simbólica siguiente (como si de capas de cebolla se tratase) y allí utilizamos la llave :out (la cual ocupa la posición de función) para obtener el resultado del comando ejecutado.

Probémosla:

(procesar-ocr (first rutas-imagenes))
18.2s

¡Muy bien!

Ahora bien, si queremos aplicar esa función a un conjunto de imágenes, sería muy tedioso llamar a la función por cada imagen de la colección. Así que podemos usar la función map y hacer que se aplique nuestra función a cada elemento de una colección de imágenes determinada; map toma como argumentos una función y una colección:

(map (fn [img] (procesar-ocr img)) rutas-imagenes)
49.0s

El tipo de función que le pasamos a map es lo que se denomina una función anónima; creamos una función que recibe un solo argumento o parámetro y se lo pasa a nuestra función procesar-ocr. Y como colección le pasamos nuestro vector de imágenes.

Vemos que obtenemos el resultado como una colección, concretamente una lista de dos ítems, donde cada cual contiene el resultado correspondiente a su respectiva imagen.

Tarda un poco en ejecutar, pero por ahora no nos preocupemos por el desempeño. Aunque ciertamente no estoy muy contento con el resultado de las imágenes.

Tengo una idea, ¿por qué no subes tú alguna imagen?

Para hacerlo, lo primero que debes hacer es clonar esta Notebook. Ve a Editor > Remix, haz click y listo.

Luego, una vez que tengas las imágenes, sólo tienes que arrastrarlas a la Notebook.

Después crea una estructura de datos, la más acorde según cuántas subas, y para referirte a la imagen subida haz click en la barra de abajo donde dice Reference file y selecciónala.

Ahora llama a la función en la siguiente celda y mira los resultados

Si has llegado hasta acá, lo has hecho muy bien ¡Felicidades!

Balance

En esta lección hemos armado un pequeño programa que toma como insumo un conjunto de fotos de textos, las convierte a archivo, las lee con la tecnología OCR y devuelve un texto que podemos manipular, bien sea aplicando una serie de transformaciones sobre el mismo ó escribiéndolos a un archivo, en fin, lo que deseemos.

En el interín hemos aprendido algunas cosas sobre programación:

  • Hemos aprendido sobre los tipos de datos (numéricos, booleanos, strings o cadenas de caracteres, caracteres símbolos y llaves).

  • Hemos aprendido a vincular un símbolo con el resultado de una expresión simbólica (lo que también suele denominarse declarar una variable).

  • Hemos aprendido que las estructuras de datos almacenan elementos de diverso tipo y que entre ellas tenemos vectores [], listas (), conjuntos #{} y mapas {}.

  • Hemos aprendido a manipular, mediante las funciones correspondientes, estas estructuras de datos.

  • Hemos aprendido que podemos combinar las estructuras de datos como deseemos.

  • Hemos aprendido que en Clojure la unidad básica es la expresión simbólica (a saber, simplemente una lista con una función ocupando el primer puesto, seguido de sus argumentos) y que un conjunto de expresiones simbólicas anidadas se denomina forma.

  • Hemos aprendido cuál es la estructura de una función, cómo llamarla y cómo declararla cuando deseemos crear una propia.

  • Hemos aprendido que cuando cometemos un error (usual aunque no exclusivamente) el intérprete nos envía una excepción y que estas excepciones pueden detener la ejecución de nuestro programa si no las capturamos con la función try-catch.

  • Y por último, pero no menos importante, hemos aprendido que un programa o software se compone de datos y funciones; que las funciones transforman esos datos para producir nueva información; y que debemos mantener la integridad de nuestros datos y la pureza de nuestras funciones para que así podamos en todo momento comprender qué está haciendo nuestro programa.

Runtimes (1)