[Universal App] Usando Microsoft OCR

Introducción

Existen aplicaciones sumamente útiles en la Tienda. Algunas de ellas
impactantes. Personalmente me impresionaron alguna aplicación que
directamente de una foto o video podía capturar el texto y traducirnoslo
directamente a nuestron idioma, o aplicaciones que de una captura nos
obtienen el texto de la misma permitiendonos guardarlo o incluso
compartirlo. Son aplicaciones muy útiles en categorías diferentes que
tienen un denominador común, son capaces de obtener el texto de una imagen.

¿Cómo lo hacen?

OCR

OCR son las siglas de Optical character recognition,
o en español reconocimiento óptico de carácteres. Es un proceso
dirigido a la digitalización de textos, los cuales identifican
automáticamente a partir de una imagen símbolos o caracteres. Las
posibilidades obtenidas son muchas y variadas:

  • Obtener el texto de una imagen.
  • Traducir el texto de una imagen.
  • Verificar si una palabra o valor se encuentra en el texto de la imagen.
  • Etc.

Librería Microsoft OCR para Windows Runtime

Recientemente Microsoft ha liberado en NuGet la librería Microsoft OCR
para Windows Runtime. Esta librería nos permite en aplicaciones
universales digitalizar textos desde imagenes. La librería funciona
completamente en local tratando imágenes desde la cámara, local o desde
la red soportando hasta 21 idiomas.

Utilizando la librería

Comenzamos creando un nuevo proyecto:

Añadimos las carpetas Views, ViewModels y Services además de las clases base necesarias para implementar el patrón MVVM de la misma forma que vimos en este artículo.

Nuestro objetivo sera muy sencillo. Nuestra aplicación de ejemplo obtendrá una imagen local gracias al uso del FileOpenPicker para sintetizar la imagen obtenida con la librería Microsoft OCR.

La interfaz de usuario

Comenzamos creando la interfaz de nuestro ejemplo. En la carpeta Views tenekos disponible la vista MainView. Añadimos:

<ScrollViewer>
     <StackPanel>
          <Image Source="{Binding Bitmap}" Stretch="Uniform" MaxHeight="300" Margin="10" />
          <TextBlock Text="{Binding Text}" TextWrapping="Wrap" FontSize="24"/>
     </StackPanel>
</ScrollViewer>

Hemos añadido dos controles básicos para nuestro ejemplo. Por un lado
la imagen bindeada a una propiedad Bitmap que mostrará la imagen
obtenida con el FileOpenPicker, por otro lado añadimos un sencillo TextBlock que mostrará el resultado de obtener el texto correspondiente a la imagen cargadada.

En la vista principal trendremos además dos botones. Un botón para
obtener una imagen local, y otro para utilizar la imagen obtenida para
extraer el texto:

<Page.BottomAppBar>
     <CommandBar>
          <AppBarButton Label="get picture" Icon="Pictures" Command="{Binding FileOpenPickerCommand}" />
          <AppBarButton Label="get text" Icon="Forward" Command="{Binding GetTextCommand}" />
     </CommandBar>
</Page.BottomAppBar>

Los añadimos en la CommandBar de la página. En la viewmodel
de la vista tendremos disponibles tanto la imagen como el texto
bindeados asi como los dos comandos necesarios para ejecutar las
acciones requeridas:

//Variables
private WriteableBitmap _bitmap;
private string _text;
 
//Commands
private ICommand _fileOpenPickerCommand;
private ICommand _getTextCommand;
 
public WriteableBitmap Bitmap
{
     get { return _bitmap; }
     set
     {
          _bitmap = value;
          RaisePropertyChanged("Bitmap");
     }
}
 
public string Text
{
     get { return _text; }
     set
     {
          _text = value;
          RaisePropertyChanged("Text");
     }
         
public ICommand FileOpenPickerCommand
{
     get { return _fileOpenPickerCommand = _fileOpenPickerCommand ?? new DelegateCommandAsync(FileOpenPickerCommandDelegate); }
}
 
public ICommand GetTextCommand
{
     get { return _getTextCommand = _getTextCommand ?? new DelegateCommandAsync(GetTextCommandDelegate); }
}
public async Task FileOpenPickerCommandDelegate()
{
 
}
 
public async Task GetTextCommandDelegate()
{
 
}

Añadiendo la librería

Continuamos añadiendo la librería.

La librería la tenemos disponible en NuGet por lo que podemos instalarlo usando Nuget Package Manager. En las referencias del proyecto hacemos clic derecho y seleccionamos la opción Manage NuGet Packages …

En la ventana modal que nos aparece, en la parte superior derecha donde podemos realizar una búsqueda, buscamos por “Microsoft OCR”:

Seleccionamos el elemento “Microsoft OCR Library for Windows Runtime” y pulsamos el botón Install.
Tras un breve periodo donde se procede a descargar e incluir las
librerías en las referencias del proyecto, tendremos lo necesario para
comenzar a trabajar con OCR.

FileOpenPicker

Representa un elemento de interfaz que permite al usuario elegir y
abrir ficheros. Es una API asíncrona disponible dentro del namespace Windows.Storage.Pickers. La API esta disponible tanto en Windows como en Windows Phone pero con diferencias entre ambas plataformas.

En aplicaciones Windows Store se realiza una llamada asíncrona a la
API esperando el resultado directamente. Mientras que en Windows Phone
el proceso es algo más complejo. En Windows Phone podemos contar con
dispositivos de baja memoria. Por ese motivo, en se utilizan métodos “AndContinue”.
Esto quiere decir que al realizar la llamada a la API la aplicación es
suspendida e incluso podría llegar a ser terminada. Continuará el
proceso al reactivar la aplicación.

Debido a las diferencias entre ambas plataformas, en nuestro ejemplo, vamos a crear una interfaz comun de un servicio FileOpenPicker en el proyecto Shared:

public interface IFilePickerService
{
     void Initialise();
     Task<StorageFile> ShowPickerAsync(FileOpenPicker picker);
}

Realizaremos la implementación adecuada de cada plataforma en el proyecto correspondiente.

Windows Store

Conenzamos por la implementacion en la aplicación Windows Store que es el caso más sencillo. El FileOpenPicker  es invocado llamando al método PickSingleFileAsync que devolverá (asíncronamente) un objeto de tipo StorageFile que representa el fichero seleccionado por el usuario:

public async Task<StorageFile> ShowPickerAsync(FileOpenPicker picker)
{
     StorageFile file = await picker.PickSingleFileAsync();
     return (file);
}

Sencillo y brutalmente potente.

Windows Phone

Continuamos con Windows Phone. En este caso, el FileOpenPicker es invocado mediante el evento PickSingleFileAndContinue. La aplicación pasa a estar suspendida.

public async Task<StorageFile> ShowPickerAsync(FileOpenPicker picker)
{
     _completionSource = new TaskCompletionSource<StorageFile>();
 
     picker.PickSingleFileAndContinue();
 
     StorageFile file = await _completionSource.Task;
 
     return (file);
}

Una vez resumida se obtiene el objeto de tipo StorageFile vía método Activated de la aplicación. La clase FileOpenPickerContinuationEventArgs nos proporciona información acerca del evento Activated cuando se produce por una operación de FileOpenPicker.

private void OnApplicationActivated(CoreApplicationView sender,
            IActivatedEventArgs args)
{
     var continueArgs =
                args as FileOpenPickerContinuationEventArgs;
 
     if (continueArgs != null)
     {
          _selectedFile = continueArgs.Files[0];
 
         if (_completionSource != null)
         {
              _completionSource.SetResult(_selectedFile);
              _completionSource = null;
         }
     }
}

Con esto ya tenemos la capacidad en nuestra aplicación, tanto en
Windows como en Windows Phone, de permitir seleccionar una imagen al
usuario.

Microsoft OCR

Llegamos al eje central del ejemplo. La librería Microsoft OCR nos
permite extraer el texto de la imagen obtenida previamente con el FileOpenPicker.
La librería Microsoft OCR para Windows Runtime nos permite extraer el
texto de una imagen además de poder encontrar patrones como correos o
números de teléfono, ideal para pooder ejecutar acciones.

Vamos a crear un servicio (al igual que hicimos con el
FileOpenPicker) para inyectarlo posteriormente con inyección de
dependencias. La interfaz del servicio es la siguiente:

public interface IOcrService
{
     Task<string> GetText(WriteableBitmap bitmap);
}

Muy muy simple. Tendremos un único método asíncrono que obtendra la
imagen y devolverá el texto. Trabajamos con la librería OCR en el
namespace WindowsPreview.Media.Ocr. La implementación del servicio para ambas plataformas es la siguiente:

public class OcrService : IOcrService
{
     private OcrEngine _ocrEngine;
 
     public async Task<string> GetText(WriteableBitmap bitmap)
     {
         string result = string.Empty;
 
         if (_ocrEngine == null)
             _ocrEngine = new OcrEngine(OcrLanguage.English);
 
         // Sintetizamos la imagen para extraer el texto (RecognizeAsync)
         var
ocrResult = await _ocrEngine.RecognizeAsync((uint)bitmap.PixelHeight,
(uint)bitmap.PixelWidth, bitmap.PixelBuffer.ToArray());
 
         // Si el resultado no contiene líneas no hacemos nada
         if (ocrResult.Lines != null)
             // Si hay líneas, las vamos añadiendo al resultado final
             result
= ocrResult.Lines.Aggregate(result, (current1, line) =>
line.Words.Aggregate(current1, (current, word) => current + word.Text
+
" "));
 
         return result;
     }
}

Analicemos con calma el código anterior. Lo primero que hacemos es crear una instancia de OcrEngine.
Esta clase es la encargada de proporcionar la capacidad de realizar OCR
a nuestra aplicación. Al instanciar el OcrEngine podemos hacerlo sin
parámetros o indicando el idioma a utilizar para
detectar el texto en la imagen. Hay 21 lenguajes soportados. Dependiendo
de la calidad de detección, el rendimiento y otros parámetros podemos
establecer la siguiente divisón en grupos:

  • Excelente: Checo, Danés, Holandés, Inglés, Finlandés, Francés, Alemán, Húngaro, Italiano, Noruego, Polaco, Portugués, Español y Sueco.
  • Muy bueno: Chino, Griego, Japones, Ruso y Turco.
  • Bueno: Chino tradicional y Creano.

NOTA: Al incluir la librería Microsoft OCR
además de la propia librería se nos añade un archivo de recursos OCR.
Estos recursos son utilizados para un correcto reconocimiento del texto
correspondiente en el idioma deseado. El archivo de recursos OCR añadido
por defecto es en ingles. Si deseamos utilizar otros idiomas a los
recursos OCR debemos utilizar la herramienta OCR Resources Generator tool. En el siguiente apartado de este mismo artículo utilizaremos la herramienta.

Continuamos analizando el código de nuestro servicio OCR. Tras instanciar el OcrEngine, utilizamos el método RecognizeAsync. A este método le pasamos la imagen junto con sus dimensiones para extraer el texto. Se devuelve un objeto de tipo OcrResult. Este objeto contiene el texto extraido asi como sus posiciones y tamaños.

El objeto OcrResult cuenta con una colección de objetos de tipo OcrLine que son cada una de las líneas de texto extraidas que a su vez contienen una colección de objetos de tipo OcrWord, que contienen la información de cada una de las palabras extraidas.

Iremos recorriendo las líneas añadiendo cada palabra en una cadena que sera el resultado devuelto por nuestro servicio.

El uso en nuestra viewmodel sera simple:

Text = await _ocrService.GetText(_bitmap);

Donde _bitmap representa la imagen local obtenida. Guardamos
en Text el resultado obtenido. En nuestro ejemplo, para simplificar
solo obtenemos el texto y nuestro objetivo finaliza aqui. Sin embargo,
desde este punto podéis utilidad OCR para una enorme cantidad de
situaciones como traducciones, extraer ciertos campos, verificar si la
iamgen contiene o no cierta palabra, compartir el contenido, etc.

Continuamos el ejemplo. La librería OCR tiene límites.
La dimensión de la imagen utilizada no puede ser inferior de 40 x 40px o
superior a 2600 x 2600px. Debemos considerar esto en nuestras
aplicaciones. Definimos los límites en constantes:

public const int MinWidth = 40;
public const int MaxWidth = 2600;
public const int MinHeight = 40;
public const int MaxHeight = 2600;

De modo que antes de utilizar nuestro servicio OCR para extraer el texto realicemos la verificación correspondiente:

public async Task GetTextCommandDelegate()
{
     // Verificamos si la imagen cumple con las características necesarias para ser procesada.
     // Las dimensiones soportadas son desde 40 a 2600 pixels.
     if (_bitmap.PixelHeight < MinHeight ||
         _bitmap.PixelHeight > MaxHeight ||
         _bitmap.PixelWidth < MinWidth ||
         _bitmap.PixelWidth > MaxHeight)   
         await _dialogService.ShowAsync("Imagen inválida", "La imagen no esta dentro del tamaño soportado.");
     else    
         Text = await _ocrService.GetText(_bitmap);
}

Todo listo!. Si ejecutamos el ejemplo y probamos:

Como generar recursos OCR

Hasta este punto tenemos lo necesario para saber como utilizar la librería Microsoft OCR
y extraer texto de una imagen. Sin embargo, en el proceso de extracción
como ya mencionamos previamente se utilizan unos
recursos (MsOcrRes.orp) localizados por idioma que facilitan la calidad
del proceso.

Hasta ahora hemos utilizado el ingles (idioma por defecto), pero… ¿que ocurre si intentamos extraer texto en japones?

Bueno, para lograr ese objetivo con l mayor calidad posible tenemos a nuestra disposición la herramienta OCR Resources Generator tool. Si nos paramos a anlizar la estructura de nuestro proyecto, tendremos algo similar a:

<solution_name>
   <project_name>
      OcrResources
         MsOcrRes.orp
   packages
      Microsoft.Windows.Ocr.1.0.0
         build
         lib
         content
            OcrResources
               MsOcrRes.orp
         OcrResourcesGenerator
            OcrResourcesGenerator.exe

Tenemos disponible la herramienta en <solution_name>packagesMicrosoft.Windows.Ocr.1.0.0OcrResourcesGeneratorOcrResourcesGenerator.exe. Tras ejecutarla veremos una pantalla como la siguiente:

Nos permite elegir entre los 21 idiomas soportados.Podemos añadir o
quitar idiomas y una vez esta a nuestro antojo pulsaremos el botón Generate Resources.

Nos aparecerá una ventana que nos permite guardar el nuevo archivo de
recursos OCR. Una vez guardado bastara con reemplazar el archivo
añadido por defecto en OcrResourcesMsOcrRes.orp.

Sencillo, ¿verdad?

Podéis descargar el ejemplo realizado a continuación:

Más información

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *