Cognitive Services | Reconocer textos con Computer Vision API

Es verdad que hasta ahora he escrito muy poco sobre los servicios cognitivos de Microsoft, aunque he estado trabajando en diferentes clientes con ellos. Por si no los conocías, Cognitive Services ofrece a los desarrolladores la capacidad de hacer uso de Inteligencia Artificial paquetizada en forma de API. Como desarrollador, basta con enviar la información en el formato que la API lo solicite (texto, imagen o voz) y obtendrás el resultado en formato JSON para poder manipularlo de manera sencilla en tus desarrollos. Dependiendo de cuál sea tu necesidad, tienes a tu disposición diferentes tipos de APIs.

Cognitive Services

Uno de los servicios más solicitados es el reconocimiento de texto en diferentes formatos, como pueden ser tarjetas, fotografías, pósters, cartas, ropa, etcétera. Para ello, tenemos Computer Vision que ofrece diferentes resultados de una imagen pasada como parámetro:

  • Clasificación de imágenes
  • Reconocimiento de actividades y escenas en imágenes
  • Reconocimiento de celebridades y puntos de referencia en imágenes
  • Reconocimiento óptico de caractéres (OCR) en imágenes
  • Reconocimiento de escritura a mano

Para este post quiero mostrarte un ejemplo que monté para reconocer texto, basado en el código que aparece en la documentación oficial sobre el reconocimiento de la escritura a mano, ya que necesitaba poder paralelizar el análisis de varias imágenes y que se le pasara como parámetro una carpeta, no un solo archivo. Además, esta versión genera un archivo JSON que contiene el resultado que devuelve Computer Vision por cada una de las imágenes.

        static void Main()
        {
            // Get the path and filename to process from the user.
            Console.WriteLine("Handwriting Recognition:");            
            Console.Write("Enter the path of the directory: ");
            string folderPath = Console.ReadLine();
            
            if (Directory.Exists(folderPath))
            {   
                Parallel.ForEach(Directory.GetFiles(folderPath), new ParallelOptions { MaxDegreeOfParallelism = 4 }, (imagePath) =>
                    {
                        // Make the REST API call.
                        Console.WriteLine($"\nAnalyzing {imagePath} \nWait a moment for the results to appear.{DateTime.Now.ToLongTimeString()}\n");
                        ReadHandwrittenText(imagePath).Wait();                        
                    });
            }
            else
            {
                Console.WriteLine("\nInvalid file path");
            }
            Console.WriteLine("\nPress Enter to exit...");
            Console.ReadLine();
        }

En el método Main, lo primero que necesito es saber dónde están las imágenes que tengo que analizar. Para ello pido a través de la consola la ruta del directorio. Una vez que compruebo que la ruta es válida inicio un for utilizando paralelismo, con el objetivo de poder lanzar más de un análisis a la vez, y llamo al método ReadHandwrittenText donde realizaré la llamada a Computer Vision con los parámetros que necesita. En este caso, para saber los parámetros y el formato de la llamada que tengo que realizar existe un portal del desarrollador donde puedo ver y probar la llamada. En este caso quiero implementar la operación Recognize Text y puedo encontrar la documentación de la API aquí.

        /// <summary>
        /// Gets the handwritten text from the specified image file by using
        /// the Computer Vision REST API.
        /// </summary>
        /// <param name="imageFilePath">The image file with handwritten text.</param>
        static async Task ReadHandwrittenText(string imageFilePath)
        {
            try
            {
                HttpClient client = new HttpClient();

                // Request headers.
                client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", ConfigurationManager.AppSettings["ComputerVisionKey"]);

                // Request parameter.
                // Note: The request parameter changed for APIv2.
                // For APIv1, it is "handwriting=true".
                string requestParameters = "mode=Handwritten";

                // Assemble the URI for the REST API Call.
                string uri = ConfigurationManager.AppSettings["UriBase"] + "?" + requestParameters;

                HttpResponseMessage response;

                // Two REST API calls are required to extract handwritten text.
                // One call to submit the image for processing, the other call
                // to retrieve the text found in the image.
                // operationLocation stores the REST API location to call to
                // retrieve the text.
                string operationLocation;

                // Request body.
                // Posts a locally stored JPEG image.
                byte[] byteData = GetImageAsByteArray(imageFilePath);

                using (ByteArrayContent content = new ByteArrayContent(byteData))
                {
                    // This example uses content type "application/octet-stream".
                    // The other content types you can use are "application/json"
                    // and "multipart/form-data".
                    content.Headers.ContentType =
                        new MediaTypeHeaderValue("application/octet-stream");

                    // The first REST call starts the async process to analyze the
                    // written text in the image.
                    response = await client.PostAsync(uri, content);
                }

                // The response contains the URI to retrieve the result of the process.
                if (response.IsSuccessStatusCode)
                    operationLocation = response.Headers.GetValues("Operation-Location").FirstOrDefault();
                else
                {
                    // Display the JSON error data.
                    string errorString = await response.Content.ReadAsStringAsync();
                    Console.WriteLine("\n\nResponse:\n{0}\n", JToken.Parse(errorString).ToString());
                    var jsonError = JToken.Parse(errorString);

                    //Hints
                    //Supported image formats: JPEG, PNG and BMP.
                    //Image file size must be less than 4MB.
                    //Image dimensions must be at least 40 x 40, at most 3200 x 3200.
                    File.WriteAllText($"{Path.GetDirectoryName(imageFilePath)}\\{Path.GetFileNameWithoutExtension(imageFilePath)}-ERROR-{jsonError["error"]["code"].ToString()}.json", errorString);
                    return;
                }

                // The second REST call retrieves the text written in the image.
                //
                // Note: The response may not be immediately available. Handwriting
                // recognition is an async operation that can take a variable amount
                // of time depending on the length of the handwritten text. You may
                // need to wait or retry this operation.
                //
                // This example checks once per second for ten seconds.
                string contentString;
                int i = 0;
                do
                {
                    System.Threading.Thread.Sleep(1000);
                    response = await client.GetAsync(operationLocation);
                    contentString = await response.Content.ReadAsStringAsync();
                    ++i;
                }
                while (i < 10 && contentString.IndexOf("\"status\":\"Succeeded\"") == -1);

                if (i == 10 && contentString.IndexOf("\"status\":\"Succeeded\"") == -1)
                {
                    Console.WriteLine("\nTimeout error.\n");
                    File.WriteAllText($"{Path.GetDirectoryName(imageFilePath)}\\{Path.GetFileNameWithoutExtension(imageFilePath)}-TIMEOUT.json", "TIMEOUT");
                    return;
                }

                // Display the JSON response.
                Console.WriteLine("\nResponse:\n\n{0}\n", JToken.Parse(contentString).ToString());
                //And save the result into a JSON
                string json = JsonConvert.SerializeObject(contentString);
                File.WriteAllText($"{Path.GetDirectoryName(imageFilePath)}\\{Path.GetFileNameWithoutExtension(imageFilePath)}.json", json);

            }
            catch (Exception e)
            {
                Console.WriteLine("\n" + e.Message);
            }
        }

Siguiendo la documentación, el método ReadHandwrittenText se utiliza para hacer una llamada HTTP a la API Computer Vision, donde necesita como cabecera la key que debes generar y la URL del servicio, que dependerá de la zona geográfica donde hayas creado el mismo. En mi caso es https://northeurope.api.cognitive.microsoft.com/vision/v2.0/recognizeText ya que lo dí de alta en North Europe. Puedes revisar cuál es la URL donde tienes acceso en el portal de Azure en el apartado Overview:

Cognitive Services – Computer Vision API – Endpoint

Y también desde el apartado Mys APIs si estás usando una key de pruebas.

En este caso estamos usando la versión 2.0 por lo que sólo deberías quedarte con la parte https://northeurope.api.cognitive.microsoft.com/vision/ y añadir v2.0/recognizeText al final para utilizar los parámetros de la última versión.

Como estamos recuperando las imágenes de local, el método ReadHandwrittenText llama a su vez a otro llamado GetImageAsByteArray que hace básicamente lo que su nombre indica para poder añadir la imagen como parte de la llamada que vamos a hacer al servicio:

        /// <summary>
        /// Returns the contents of the specified file as a byte array.
        /// </summary>
        /// <param name="imageFilePath">The image file to read.</param>
        /// <returns>The byte array of the image data.</returns>
        static byte[] GetImageAsByteArray(string imageFilePath)
        {
            using (FileStream fileStream =
                new FileStream(imageFilePath, FileMode.Open, FileAccess.Read))
            {
                BinaryReader binaryReader = new BinaryReader(fileStream);
                return binaryReader.ReadBytes((int)fileStream.Length);
            }
        }

Una vez que tenemos la key, la URL y la imagen que se va a analizar se realiza la llamada y esta devuelve un localizador en la cabecera Operation-Location que nos servirá para consultar cuándo el análisis ha finalizado, que es lo que ocurre en las siguientes líneas, a no ser que haya ocurrido algún error que también registraremos. Una vez que tengamos el resultado lo guardamos en el mismo formato que lo hemos recibido (JSON) y lo almacenamos con el mismo nombre que la imagen, para que sea más sencillo saber qué resultado corresponde a qué imagen.

Handwritting resultado

El output del aplicativo será parecido al siguiente:

AnalyzeHandwritting output

El código del ejemplo está en mi cuenta de GitHub.

¡Saludos!