SharePoint y Azure: Reconocimiento Óptico de Caracteres (OCR)

Escrito por Gustavo Velez - 15/08/2018

Que es OCR

Optical Character Recognition (OCR, Reconocimiento Óptico de Caracteres) es la operación de conversión de imágenes que contienen texto impreso en texto codificado para computadores. OCR se utiliza extensamente en todo tipo de procesos de IT, variando desde el reconocimiento de texto impreso en papel y escaneado electrónicamente, hasta lectura de documentos de identificación, de tal forma que puedan ser almacenados en sistemas de intercambio de información tales como SharePoint, indexados para facilitar su búsqueda, y clasificados por medio de metadatos.

OCR ha sido históricamente una de las primeras tareas que sistemas de computación ha tenido que realizar, desde los tiempos en que se utilizaban telégrafos mecánicos. Desde la aparición de los teléfonos inteligentes, OCR ha tomado un impulso mayor debido a todo tipo de software que reconoce texto desde las imágenes que los aparatos móviles son capaces de generar.

OCR en Azure

Microsoft ha ofrecido durante muchos años diferentes tipos de software para OCR, integrándolo inclusive en algunos de sus programas, tales como Word y PowerPoint, especialmente para archivos de PDF. Azure ofrece OCR a través del servicio "Computer Vision API" en 25 lenguajes (en el momento de escribir este artículo): Árabe, Chino Simplificado, Chino Tradicional, Checo, Danés, Holandés, Ingles, Fines, Frances, Alemán, Griego, Húngaro, Italiano, Japonés, Coreano, Noruego, Polaco, Portugués, Rumano, Ruso, Serbio (Cirílico y Latino), Eslovaco, Español, Sueco y Turco.

Las imágenes tienen que tener una resolución de entre 40x40 y 3200x3200 pixeles y no ser mayores de 10 megapíxeles. El texto dentro de las imágenes puede estar girado en cualquier ángulo, y el API se encarga de rotarlo para que pueda ser leído horizontalmente. No todos los tipos de letras pueden ser reconocidos con exactitud, especialmente los estilos "artísticos", con imágenes de fondo complejas, con letras muy pequeñas o muy grandes, o con textos tachados, pero el índice de reconocimiento es muy alto, y, por ser un sistema dinámico, Azure "aprende" a reconocer cada vez más texto con el tiempo de utilización.

Azure tiene otros dos servicios con funcionalidad parecida a OCR, pero ambos están en preview en el momento: el "Text Recognition" del Computer Vision API que hace un trabajo muy similar al OCR API, pero reconociendo texto escrito sobre superficies complejas, y el "Azure Media OCR" que puede extraer texto desde videos y películas.

Azure OCR y SharePoint

El servicio de OCR del Azure Computer Vision API se puede utilizar en SharePoint para enriquecer automáticamente la información en el sistema. Cuando se suben imágenes a SharePoint, en realidad el sistema no puede indexar su contenido, solamente los metadatos que posee. Por medio de OCR es posible extraer información directamente desde la imagen, y agregarla al sistema como metadatos, permitiendo clasificarla más exactamente, y obtener resultados de búsqueda más precisos.

En el ejemplo que se va a desarrollar enseguida se utiliza el OCR del Computer Vision API para extraer información de una imagen y agregarla a un campo en la Librería a donde se sube. La Biblioteca de SharePoint dispone para esto de un campo de texto extra para insertar el texto encontrado en la imagen y otro campo de texto para el identificador del lenguaje.

Configuración del Azure Computer Vision API

Para utilizar el Computer Vision API es necesario crear primero el servicio en Azure, aunque también es posible utilizar una cuenta temporal de prueba que se puede crear desde la página de Microsoft https://azure.microsoft.com/en-us/try/cognitive-services/.  

Para crear un servicio (de pago) en Azure:

1.       Entre al portal de manejo de Azure (https://portal.azure.com) utilizando sus credenciales.
2.       Vaya a la sección de "Resource Groups" y cree un nuevo Grupo de Recursos (también es posible reutilizar un grupo ya existente).
3.       Cree un servicio de "Computer Vision API":

a. En el Resource Group, utilice el botón de "+Add" para crear un recurso, busque por "Computer Vision" en la casilla de búsqueda y seleccione "Computer Vision" en los resultados.

b. Asígnele un nombre al servicio y utilice el Grupo de Recursos deseado. En la casilla de "Pricing tier" seleccione un nivel dependiendo de la cantidad de consultas a esperar por segundo, lo que determina el precio del servicio (por bloques de mil consultas).

Imagen 1.- Creación del servicio de Computer Vision API. 

4.       Una vez creado el servicio, haga clic sobre su nombre en la lista de recursos del Resource Group, vaya a "Keys" y copie el valor de "Key 1"

Utilizando el Azure OCR con SharePoint

En el siguiente ejemplo, como se indicó anteriormente, se va a utilizar una Biblioteca de SharePoint con un campo de texto extra llamado "OCRText" y otro para el identificador del lenguaje ("Language"). Cuando se sube una imagen a la Biblioteca, un WebHook hace que una Función de Azure comience a funcionar, extrae el texto y lenguaje en la imagen utilizando el OCR del Computer Vision API y modifica los campos adicionales del documento agregándoles el texto e idioma encontrados.

Nota: la creación y configuración de una Función de Azure se puede encontrar el en artículo "SharePoint y Azure – Azure Functions" (http://www.compartimoss.com/revistas/numero-30/sharepoint-y-azure-azure-functions). La configuración y utilización de WebHooks de SharePoint se puede encontrar en el artículo "Eventos sobre SharePoint Online con Webhooks" (http://www.compartimoss.com/revistas/numero-32/eventos-sobre-sharepoint-online-con-webhooks).

5.        Cree una cuenta de Funciones básica en el Grupo de Recursos, asignándole un nombre, Plan de Servicios y cuenta de Azure Storage.
6.       Utilizando Visual Studio 2017 (o Visual Studio 2015 con el AddIn para programar Funciones de Azure), cree una nueva solución del tipo "Azure Function". Una vez creada la solución, agréguele una Función del tipo "Http Trigger" con derechos de acceso anónimos.
7.       Agréguele a la solución los paquetes NuGet "AppForSharePointOnlineWebToolkit" y "Microsoft.Azure.CognitiveServices.Language" (chequee la casilla de "Include prerelease").

8.       Reemplace toda la rutina "Run" con el siguiente código:

[FunctionName("FunctionOCR")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]HttpRequestMessage req, TraceWriter log)
{
    log.Info("FunctionOCR trigger function processed a request.");
 
    // Registration
    string validationToken = GetValidationToken(req);
    if (validationToken != null)
    {
        log.Info($"---- Processing Registration");
        var myResponse = req.CreateResponse(HttpStatusCode.OK);
        myResponse.Content = new StringContent(validationToken);
        return myResponse;
    }
 
    // Changes
    var myContent = await req.Content.ReadAsStringAsync();
    var allNotifications = JsonConvert.DeserializeObject<ResponseModel<NotificationModel>>(myContent).Value;
 
    if (allNotifications.Count > 0)
    {
        log.Info($"---- Processing Notifications");
        string siteUrl = ConfigurationManager.AppSettings["whSiteListUrl"];
        foreach (var oneNotification in allNotifications)
        {
            // Login in SharePoint
            ClientContext SPClientContext = HelpFunctions.LoginSharePoint(siteUrl);
 
            // Get the Changes
            GetChanges(SPClientContext, oneNotification.Resource, log);
        }
    }
 
    return new HttpResponseMessage(HttpStatusCode.OK);
}​

 

Esta rutina primero se encarga de hacer el registro del WebHook (si la consulta contiene un parámetro "validationtoken" en el Query String) utilizando la rutina "GetValidationToken":

public static string GetValidationToken(HttpRequestMessage req)
{
    string strReturn = string.Empty;
 
    strReturn = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "validationtoken", true) == 0)
        .Value;
 
    return strReturn;
}​

Después de registrado el WebHook, cada consulta es procesada para extraer las notificaciones que contiene. En cada notificación de la colección de notificaciones se hace un logeo en SharePoint para obtener los cambios detectados en la Biblioteca (por medio de la rutina "GetChanges"). En la variable "whSiteListUrl" del App Settings de la función se encuentra el URL del sitio en donde está la Lista a examinar ("https://[Dominio].sharepoint.com/sites/[NombreSitio")

9.       La rutina "GetChanges" recibe el contexto de SharePoint y el identificador de la Lista, y tiene la forma:

static void GetChanges(ClientContext SPClientContext, string ListId, TraceWriter log)
{
    // Get the List
    Web spWeb = SPClientContext.Web;
    List myList = spWeb.Lists.GetByTitle(ConfigurationManager.AppSettings["whListName"]);
    SPClientContext.Load(myList);
    SPClientContext.ExecuteQuery();
 
    // Create the ChangeToken and Change Query
    ChangeQuery myChangeQuery = GetChangeQueryNew(ListId);
 
    // Get all the Changes
    var allChanges = myList.GetChanges(myChangeQuery);
    SPClientContext.Load(allChanges);
    SPClientContext.ExecuteQuery();
 
    foreach (Change oneChange in allChanges)
    {
        if (oneChange is ChangeItem)
        {
            int myItemId = (oneChange as ChangeItem).ItemId;
 
            // Get what is changed
            log.Info($"---- Changed ItemId : " + myItemId);
            ListItem myItem = myList.GetItemById(myItemId);
            Microsoft.SharePoint.Client.File myFile = myItem.File;
            ClientResult<System.IO.Stream> myFileStream = myFile.OpenBinaryStream();
            SPClientContext.Load(myFile);
            SPClientContext.ExecuteQuery();
 
            // The picture as Byte Array
            byte[] myFileBytes = ConvertStreamToByteArray(myFileStream);
 
            // Analyze the text
            TextAnalyzeOCRResult myResult = GetAzureTextAnalyzeOCR(myFileBytes).Result;
            log.Info($"---- Text Analyze OCR Result : " + JsonConvert.SerializeObject(myResult));
 
            // Insert the values back in the List
            myItem["Language"] = myResult.language;
            string myText = string.Empty;
            for (int oneLine = 0; oneLine < myResult.regions[0].lines.Count(); oneLine++)
            {
                for (int oneWord = 0; oneWord < myResult.regions[0].lines[oneLine].words.Count(); oneWord++)
                {
                    myText += myResult.regions[0].lines[oneLine].words[oneWord].text + " ";
                }
            }
            myItem["OCRText"] = myText;
            myItem.Update();
            SPClientContext.ExecuteQuery();
            log.Info($"---- Text Analyze OCR added to SharePoint Item");
        }
    }
}​

Primero se crea un objeto que contienen la Lista a utilizar en SharePoint. Luego se crea una consulta de cambio (variable "myChangeQuery") que especifica que se requieren los cambios ocurridos en el ultimo minuto, que ocurren en elementos de la Lista y que sean del tipo "Add", es decir, elementos nuevos:

public static ChangeQuery GetChangeQueryNew(string ListId)
{
    ChangeToken lastChangeToken = new ChangeToken();
    lastChangeToken.StringValue = string.Format("1;3;{0};{1};-1", ListId, DateTime.Now.AddMinutes(-1).ToUniversalTime().Ticks.ToString());
    ChangeToken newChangeToken = new ChangeToken();
    newChangeToken.StringValue = string.Format("1;3;{0};{1};-1", ListId, DateTime.Now.ToUniversalTime().Ticks.ToString());
    ChangeQuery myChangeQuery = new ChangeQuery(false, false);
    myChangeQuery.Item = true;  // Get only Item changes
    myChangeQuery.Add = true;   // Get only the new Items
    myChangeQuery.ChangeTokenStart = lastChangeToken;
    myChangeQuery.ChangeTokenEnd = newChangeToken;
 
    return myChangeQuery;
}​

Luego de ejecutar la consulta, se examina cada uno de los cambios y se obtiene un objeto con la imagen agregada, que se convierte en un array de bytes por medio de la rutina "ConvertStreamToByteArray":

public static Byte[] ConvertStreamToByteArray(ClientResult<System.IO.Stream> myFileStream)
{
    Byte[] bytReturn = null;
 
    using (System.IO.MemoryStream myFileMemoryStream = new System.IO.MemoryStream())
    {
        if (myFileStream != null)
        {
            myFileStream.Value.CopyTo(myFileMemoryStream);
            bytReturn = myFileMemoryStream.ToArray();
        }
    }
 
    return bytReturn;
}​

En la misma rutina se llama a "GetAzureTextAnalyzeOCR", la que se encarga de hacer la consulta en Azure, utilizando como parámetro de entrada el array de bytes de la imagen. Esta rutina entrega de regreso un objeto del tipo "TextAnalyzeOCRResult" que contiene los resultados de la consulta y que tiene la forma:

public class TextAnalyzeOCRResult
{
    public string language { get; set; }
    public float textAngle { get; set; }
    public string orientation { get; set; }
    public Region[] regions { get; set; }
}
 
public class Region
{
    public string boundingBox { get; set; }
    public Line[] lines { get; set; }
}
 
public class Line
{
    public string boundingBox { get; set; }
    public Word[] words { get; set; }
}
 
public class Word
{
    public string boundingBox { get; set; }
    public string text { get; set; }
}​

Como Azure retorna un resultado con varios elementos, uno por cada línea de texto encontrada, por medio de un loop se encadenan los valores. Finalmente se utilizan los valores de "language" y "myText" para insertarlos en los campos de "Language" y "OCRText" del Documento de SharePoint.

10.  La rutina "GetAzureTextAnalyzeOCR" recibe como parámetros de entrada el array de bytes de la imagen y retorna un objeto con los valores encontrados por Azure:

public static async Task<TextAnalyzeOCRResult> GetAzureTextAnalyzeOCR(byte[] myFileBytes)
{
    TextAnalyzeOCRResult resultReturn = new TextAnalyzeOCRResult();
 
    HttpClient client = new HttpClient();
 
    // Request headers
    client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", ConfigurationManager.AppSettings["azVisionApiServiceKey"]);
 
    // Request parameters.
    string requestParameters = "language=unk&detectOrientation=true";
 
    // Assemble the URI for the REST API Call.
    string uri = ConfigurationManager.AppSettings["azVisionApiOcrEndpoint"] + "?" + requestParameters;
    string contentString = string.Empty;
 
    HttpResponseMessage response;
 
    using (ByteArrayContent content = new ByteArrayContent(myFileBytes))
    {
        content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
 
        // Execute the REST API call
        response = await client.PostAsync(uri, content);
 
        // Get the JSON response
        contentString = await response.Content.ReadAsStringAsync();
 
        resultReturn = JsonConvert.DeserializeObject<TextAnalyzeOCRResult>(contentString);
        return resultReturn;
    }
}​

Cada consulta se realiza por medio de una llamada REST a un URL pre-especificado del servicio de búsqueda (dado en el valor de la App Settings "azVisionApiOcrEndpoint" y que es "https://westeurope.api.cognitive.microsoft.com/vision/v1.0/ocr"), utilizando como parámetros en el QueryString el "language" ("unk", para forzar un resultado de retorno) y si es necesario detectar la orientación de la imagen ("detectOrientation"). En la App Settings "azVisionApiServiceKey" se mantiene el valor de la llave mencionada en el punto 4.

11. Otras tres clases definen objetos utilizados por el WebHook:
public class ResponseModel<T>
{
    [JsonProperty(PropertyName = "value")]
    public List<T> Value { get; set; }
}
 
public class NotificationModel
{
    [JsonProperty(PropertyName = "subscriptionId")]
    public string SubscriptionId { get; set; }
 
    [JsonProperty(PropertyName = "clientState")]
    public string ClientState { get; set; }
 
    [JsonProperty(PropertyName = "expirationDateTime")]
    public DateTime ExpirationDateTime { get; set; }
 
    [JsonProperty(PropertyName = "resource")]
    public string Resource { get; set; }
 
    [JsonProperty(PropertyName = "tenantId")]
    public string TenantId { get; set; }
 
    [JsonProperty(PropertyName = "siteUrl")]
    public string SiteUrl { get; set; }
 
    [JsonProperty(PropertyName = "webId")]
    public string WebId { get; set; }
}
 
public class SubscriptionModel
{
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string Id { get; set; }
 
    [JsonProperty(PropertyName = "clientState", NullValueHandling = NullValueHandling.Ignore)]
    public string ClientState { get; set; }
 
    [JsonProperty(PropertyName = "expirationDateTime")]
    public DateTime ExpirationDateTime { get; set; }
 
    [JsonProperty(PropertyName = "notificationUrl")]
    public string NotificationUrl { get; set; }
 
    [JsonProperty(PropertyName = "resource", NullValueHandling = NullValueHandling.Ignore)]
    public string Resource { get; set; }
}​

12.       Registre el WebHook en la Biblioteca de SharePoint y suba una imagen que contenga texto. El WebHook hará que la Función realice su trabajo, entregue los resultados encontrados por Azure y muestre el idioma y el texto en la imagen:

Imagen 2.- Imágenes con texto recobrado por el servicio de OCR. 


 

Conclusiones

El servicio de OCR del Azure Computer Vision API permite enriquecer la información que los usuarios guardan en SharePoint extrayendo el texto que se encuentra en imágenes. El servicio de OCR es fácil de utilizar desde cualquiera lenguaje de programación, y produce resultados confiables rápida y seguramente. El API utiliza algoritmos de Inteligencia Artificial que se mejoran con el uso, por lo no es necesario crear ni entrenar algoritmos propios.

 

Gustavo Velez
MVP Office Apps and Services
gustavo@gavd.net
http://www.gavd.net
***