TSM - Utilizarea Azure Function pentru a obţine coordonatele GPS ale unei imagini

Radu Vunvulea - Solution Architect

Să analizăm Azure Functions din perspectiva programatorului. În acest articol vom scrie o funcţie Azure (Azure Function) care adaugă coordonatele GPS pentru o imagine sub formă de watermark. Dacă vă interesează doar partea de cod, fără explicaţii, vă invit să vizitaţi GitHub

Ce sunt Azure Functions?

Cea mai bună explicaţie pe care o putem da este că Azure Functions sunt AWS Lambda din lumea Azure. Funcţiile ne permit să executăm codul fără server, fără a ne gândi unde este găzduit codul. Pur şi simplu scrieţi codul şi rulaţi-l ca Azure Function.
Caracterizând Azure Functions, reies următoarele trăsături:

Misiunea noastră

Misiunea noastră este de a scrie o funcţie Azure care:

Pasul unu  - Creaţi o funcţie Azure

Primul pas este crearea unei funcţii Azure. Acest lucru se poate realiza de pe Azure Portal, iar o documentaţie explicită în acest sens se poate găsi aici. Aici putem crea iniţial o funcţie 'GenericWebhookCSharp', apoi vom elimina părţile de care nu avem nevoie.

După ce facem acest lucru, vom accesa tabul Integrate şi ne vom specifica triggerul. Ştergeţi toate triggerele şi outputurile care sunt deja definite. Le vom defini din nou.

Triggere (declanşatori) şi bindings (relaţiile de legare)

În cazul de faţă va trebui să folosim External File, iar în câmpul connection vom crea unul nou care va face referire la OneDrive. Este important de ştiut că, deşi credenţialele de acces sunt nişte interogări, acestea nu sunt stocate în Azure Functions. Acestea sunt stocate drept API Connections, iar aceeaşi conexiune poate fi folosită în funcţii multiple dacă este necesar. Acest trigger va fi apelat când un fişier nou va fi copiat pe calea dată. Drept output, vom folosi acelaşi External File. Acum, putem refolosi aceeaşi conexiune pe care am creat-o pentru trigger - onedrive_ONEDRIVE. 

Acum, să analizăm legăturile (bindings). 

{
  "bindings": [
    {
      "type": "apiHubFileTrigger",
      "name": "inputFile",
      "path": "Imagini/Peliculă/{filename}",
      "connection": "onedrive_ONEDRIVE",
      "direction": "in"
    },
    {
      "type": "apiHubFile",
      "name": "outputFile",
      "path": "Imagini/Peliculă/pictureswithgpswatermark/{rand-guid}.jpg",
      "connection": "onedrive_ONEDRIVE",
      "direction": "out"
    }
  ],
  "disabled": false
}

Primul binding specifică triggerul. După cum se poate observa, direction este "in", iar connection este racordat la OneDrive. Câmpul path este relativ la OneDrive-ul nostru. În cazul nostru, fişierul monitorizat este 'Imagini/Peliculă'. {filename} este parametrul de fişier. În Azure Function, ne vom referi la input file utilizând atributul 'name' - inputFile.

În mod similar, avem outputul care este înregistrat în fişierul "Imagini/Peliculă/pictureswithgpswatermark". '{rand-guid}' este folosit pentru a se genera un nume aleatoriu pentru fiecare imagine. 

Scrierea unei funcţii de log

După cum putem observa, inputFile şi outputFile sunt parametrii metodei Run. Această metodă este punctul de intrare de fiecare dată când rulează triggere. Dacă doriţi să scrieţi ceva în loguri, puteţi folosi cu succes TraceWriter, care trebui specificat drept parametru.

public static void Run(Stream inputFile, 
   Stream outputFile, TraceWriter log)
{
     log.Info("Image Process Starts"); 

     log.Info("Image Process Ends");
}

Putem să ne definim propriile clase, librării bazate pe referinţe sau pachete Nuget. Pentru a putea lucra cu acest tip de fişiere şi bindings, trebuie să adăugăm o referinţă în ApiHub. Altfel, rezultatul va fi o eroare criptată:

Exception while executing function: Functions.SaasFileTriggerCSharp1. Microsoft.Azure.WebJobs.Host: One or more errors occurred. Exception binding parameter 'input'. Microsoft.Azure.ApiHub.Sdk

Referinţa este adăugată, dar specifică Azure Functions pentru a încărca ansamblul (the assembly) din propriul shared repository.

#r "Microsoft.Azure.WebJobs.Extensions.ApiHub" //

Salvare şi Rulare

Înainte de a apăsa butonul Save, asiguraţi-vă că Logs window este vizibilă. Acest lucru este util, deoarece, de fiecare dată când apăsaţi butonul Save, funcţia este compilată. Orice eroare din timpul buildului este afişată în Logs window.

De acum înainte, de fiecare dată când copiaţi/încărcaţi un fişier nou în fişierul OneDrive, funcţia voastră va fi apelată automat. În loguri veţi putea vedea logurile de output.

Citirea localizării GPS

Pentru a putea citi localizarea GPS din imagini, vom folosi ExifLib. Acest pachet Nuget ne permite să citim informaţia GPS cu uşurinţă. Pentru a face o operaţiune push pentru Nuget package, vom deschide project.json şi vom adăuga o dependinţă pachetului Nuget. Mai jos găsiţi cum ar trebuie să arate JSON. Am mai adăugat pachetul Nuget care va fi utilizat ulterior pentru a scrie coordonatele pe imagine.

{
  "frameworks": {
    "net46":{
      "dependencies": {
        "ExifLib": "1.7.0.0",
        "System.Drawing.Primitives": "4.3.0"
      }
    }
   }
}

Când apăsaţi butonul Save, veţi observa că funcţia este compilată, că pachetul Nuget şi toate dependinţele pachetelor sunt descărcate. Când rulăm codul care extrage locaţia GPS vom lua în considerare cazurile în care imaginea nu are această informaţie.

private static string GetCoordinate(Stream image, TraceWriter log)
{
    log.Info("Extract location information");
  ExifReader exifReader = new ExifReader(image);
  double[] latitudeComponents;
  exifReader.GetTagValue(ExifTags.GPSLatitude,
    out latitudeComponents);
  double[] longitudeComponents;
  exifReader.GetTagValue(ExifTags.GPSLongitude, 
     out longitudeComponents);

  log.Info("Prepare string content");
  string location = string.Empty;
  if (latitudeComponents == null ||
    longitudeComponents == null)
  {
    location = "No GPS location";
  }
  else
  {
    double latitude = 0;
    double longitude = 0;
    latitude = latitudeComponents[0] + 
      latitudeComponents[1] / 60 + 
      latitudeComponents[2] / 3600;

    longitude = longitudeComponents[0] + 
      longitudeComponents[1] / 60 + 
      longitudeComponents[2] / 3600;

    location = $"Latitude: '{latitude}' | 
      Longitude: '{longitude}'";
    }

    return location;
}

Următorul pas este apelarea metodei noastre din metoda Run (string locationText = GetCoordinate(inputFile, log). După ce salvăm, localizarea GPS pentru fiecare imagine se poate găsi în log window.

string locationText = GetCoordinate(inputFile, log);
log.Info($"Text to be written: '{locationText}'");
---- log window ----
2016-12-06T00:06:35.680 Image Process Starts
2016-12-06T00:06:35.680 Extract location information
2016-12-06T00:06:35.680 Prepare string content
2016-12-06T00:06:35.680 Text to be written: 'Latitude: '46.7636219722222' | Longitude: '23.5550620833333''

Scrierea coordonatelor watermark

Ultimul pas este scrierea textului pe imagine şi copierea streamului de imagine în output. Codul este acelaşi precum cel de care e nevoie de aplicaţia consolă pentru acelaşi task.

private static void WriteWatermark(string watermarkContent, Stream originalImage, Stream newImage, TraceWriter log)
{
  log.Info("Write text to picture");
  using (Image inputImage = Image
    .FromStream(originalImage, true))
  {
  using (Graphics graphic = Graphics
   .FromImage(inputImage))
   {
   graphic.SmoothingMode = SmoothingMode.HighQuality;
   graphic.InterpolationMode = InterpolationMode
    .HighQualityBicubic;
   graphic.PixelOffsetMode = PixelOffsetMode
    .HighQuality;
   graphic.DrawString(watermarkContent, 
     new Font("Tahoma", 100, FontStyle.Bold), 
     Brushes.Red, 200, 200);
   graphic.Flush();

   log.Info("Write to the output stream");
   inputImage.Save(newImage, ImageFormat.Jpeg);
    }
  }
}

Nu uitaţi să resetaţi poziţia cursorului streamului inputFile înainte de a apela WriteWatermark. Acest lucru este necesar, deoarece citirea coordonatelor va muta cursorul din poziţia 0. La final, metoda Run trebuie să arate astfel:

public static void Run(Stream inputFile, 
  Stream outputFile, TraceWriter log)
{
 log.Info("Image Process Starts"); 
 string locationText = GetCoordinate(inputFile, log);
 log.Info($"Text to be written: '{locationText}'");

 // Reset position. After Exif operations the cursor  
 // location is not on position 0 anymore;

 inputFile.Position = 0;
 WriteWatermark(locationText, inputFile, outputFile,
   log);

  log.Info("Image Process Ends");
}

Rezultatul final

Rezultatul final al funcţiei noastre va fi o imagine cu coordonatele scrise în ROŞU. A se vedea în figura alăturată.

Concluzie

În acest articol am analizat cum putem scrie o funcţie Azure care adaugă, sub forma unui watermark, coordonatele GPS ale locului unde a fost făcută poza.

Azure Functions este un tool puternic ce ne permite să ne concentrăm doar pe cod fără a ne gândi la infrastructură sau la alte aspecte.

Codul complet se găseşte pe GitHub.