Share This Post

Azure / Öne Çıkanlar / Yapay Zeka (AI)

Kolay Yoldan Yüz Tanıma

Bundan birkaç ay önce Istanbul’daki AI Bootcamp için bir demo hazırlamam gerekiyordu. Kısa ve kolay yoldan hızlıca çalışabilecek birşeyler ararken aklıma her gün kullandığım iPhone’daki FaceLogin tadında birşey yapmak geldi. Azure Cognitive Services’da Face Recognition API’ları var. Fakat burada önemli olan aslında bizim AI’yı train etmemiz. Çünkü sadece bir fotoğraftaki yüzü bulmak değil mesele 🙂 O yüzün bizim tanıdığımız Ahmet Abi olup olmadığını da anlamamız lazım Login yapabilmek için. Tabi ki tüm bunlar Cognitive Services ile mümkün, aksi halde bu yazının anlamı kalmazdı 🙂 Şaka bir yana birazdan kullanacağımız API’ların dokümantasyonu epey ekşi 🙂 o nedenle epey bir depreşmek gerekti ayağı kaldırmak için. Yine de sonuç güzel.

Tamam da o iş öyle olmaz ki!

Kesinlike 🙂 Şimdi birincisi, yapacağımız işe Face Login demek süper yanlış. Hadi iPhone veya Windows Hello depth camera kullanıyor. Bizim örnek direk basit bir kameradan altığı 2D fotodan yola çıkarak şahsı tanıyacak. O nedenle buna “authentication” değil de “identification” desek daha doğru olur. Zaten itiraf etmek gerekirse ister parmak izi ile tanıma olsun, ister yüz, ister göz bebeği.. tüm bunların aslında birer authentication yöntemi olarak kullanılması pek de doğru değil. Neden mi? Çünkü hiçbirini değiştiremezsiniz 🙂 Şifre authentication için kullanılabilir, çalındığında değiştirirsiniz. Fakat parmak iziniz şifreniz olursa…. Sanırım derdimi anlatabildim 🙂 O nedenle biz her ne kadar bu yazı boyunca FaceLogin falan diyecek olsak da tüm bunları sadece identification için kullanmakta fayda var.

Mobil uygulamalardan kullanılmak üzere API açacağız.

Hayali projemizde tüm bu sistem mobil uygulamalar üzerinden kullanılacak. O nedenle birkaç API açmamız gerek. Bunlardan ilki basit bir şekilde herhangi bir fotoğrafın yüklenebileceği API olacak. Tüm API’ları birer Function olarak Serverless diyarında Azure Functions üzerine atacağım ben.

[FunctionName("UploadFile")]
public static async Task<HttpResponseMessage> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    ILogger log, ExecutionContext context)
{
    #region Config
    var config = new ConfigurationBuilder()
    .SetBasePath(context.FunctionAppDirectory)
    .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
    .AddEnvironmentVariables()
    .Build();

    string storageConnectionString = config["AzureWebJobsStorage"];
    #endregion

    #region Setting Up Cloud Storage
    CloudStorageAccount.TryParse(storageConnectionString, out CloudStorageAccount storageAccount);
    CloudBlobClient cloudBlobClient = storageAccount.CreateCloudBlobClient();
    var cloudBlobContainer = cloudBlobClient.GetContainerReference("tempphotos");
    await cloudBlobContainer.CreateIfNotExistsAsync();
    BlobContainerPermissions permissions = new BlobContainerPermissions
    {
        PublicAccess = BlobContainerPublicAccessType.Blob
    };
    await cloudBlobContainer.SetPermissionsAsync(permissions);
    #endregion

    #region Multipart Receive and Upload
    List<string> filesUploaded = new List<string>();
    var boundary = MultipartRequestHelper.GetBoundary(Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse(req.ContentType), int.MaxValue);
    var reader = new MultipartReader(boundary, req.Body);
    var section = await reader.ReadNextSectionAsync();
    while (section != null)
    {
        Microsoft.Net.Http.Headers.ContentDispositionHeaderValue contentDisposition;
        var hasContentDispositionHeader = Microsoft.Net.Http.Headers.ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);

        if (hasContentDispositionHeader && MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
        {
            CloudBlockBlob cloudBlockBlob = cloudBlobContainer.GetBlockBlobReference(contentDisposition.FileName.Value);
            await cloudBlockBlob.UploadFromStreamAsync(section.Body);
            filesUploaded.Add(cloudBlockBlob.Uri.ToString());
        }
        section = await reader.ReadNextSectionAsync();
    }
    #endregion

    #region Return JSON
    var jsonToReturn = JsonConvert.SerializeObject(filesUploaded.ToArray());

    return new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent(jsonToReturn, Encoding.UTF8, "application/json")
    };
    #endregion
}

Yukarıdaki kodun yazımız konusu ile pek alakası yok. Sonuçta dosya alıp Azure Storage’a kaydeden bir Function yazmış olduk. Storage Account olarak da direk Azure Functions ile beraber gelen AzureWebJobsStorage’ı kullandım. Siz böyle yapmayın 🙂 Ayrı bir account yaratın. Adı üzerinde bu bir demo 🙂 Çak-Geç kısımlar var. O kısımları parmakla göstermeye çalışacağım ki yanlış örnek alınmasın.

Bu örnekte mobil uygulamayı yazan arkadaş 🙂 illa MultiPart upload yapacağım dediği için bu şekilde bir function yazmak zorunda kaldık. Aslında konu okursanız bir helper kullandığımı da göreceksiniz. Ona da buradan ulaşabilirsiniz.

Upload işlemi bittikten sonra dosyanın Blob URL’i string olarak geri dönüyoruz. MultiPart kısmında birden çok dosya gelebilir. Kodumuz bunu da karşılayarak birden çok dosyayı Storage’a attıp geriye string array döndürebilecek durumda. Tabi Azure Functions Runtime limitlerine dikkat etmek gerek. Konu dışı olduğunu için detaylarına dalmıyorum şu an.

Eğer bu kodu alıp da yayına çıkmak isterseniz tavsiyem Storage’a giderken alabileceğiniz hataları da düşünmeniz.

Yeni yüzler ile Training

Elimizdeki resim dosyasını clouda attığımıza göre bir sonraki adım bunu Cognitive Services’a vermek. Tabi bu noktada “Training” aşamasında olduğumuzu hatırlatayım. Amacımız “Ahmet Abi budur” diyerek AI’a Ahmet Abi’nin resmini vermek.

{   
  "imageUrl":"https://i1.rgstatic.net/ii/profile.image/644263821971457-1530615873370_Q512/Cihan_Yakar.jpg",   
  "personName":"Daron"   
}  

İlk yazacağımız API yeni yüzleri sisteme ekleyecek olan API. Yukarıdaki gibi bir body ile giderek bir fotoğraf adresi ve bu fotoğrafın kime ait olduğunu göndereceğiz. AI’ın bu fotoğrafı o adresten indirip (bizim örnekte aslında bu bir Blob adresi olacak) sonrasında da verdiğimiz isimle eşleştirerek Model’imizi Train etmesini istiyoruz.

{
    "faceId": "6d3e9486-ac88-42ac-9ec8-714b37dab654",
    "personId": "2c9e53a3-bbe3-43a0-9c28-0ec349e0ea62"
}

API’ın Response’unda ise geriye bir personId ve faceId döneceğiz. Bu Id’lerden biri şahsın kimliği diğeri ise fotoğrafını gönderdiğimiz yüzün tekil belirleyicisi (unique identifier) olacak. Hadi kodu yazalım 🙂

#region Read Body
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
string imageUrl = data?.imageUrl;
string personName = data?.personName;
string personId = data?.personId;
#endregion

Gelin bu sefer kodu bölüm bölüm gezelim. Yukarıdaki değişkenler arasında size garip gelebilecek tek değişken personId olabilir. Request örneğimizde böyle bir alan yoktu. Bu alanı aslında farklı bir senaryo için kullanacağız. Bu API ile yeni bir insan ismi ve fotoğrafı verebileceğimiz gibi, daha önce sisteme girilmiş bir insana yeni fotoğraf eklemek için de kullanabilelim istiyorum. O nedenle API’ya aşağıdaki gibi bir request ile gelindiğinde yeni bir insan yaratmak yerine daha önce oluşturulmuş ve ID’si bilinen birine yeni yüz ekleyeceğiz.

{   
  "imageUrl":"https://i1.rgstatic.net/ii/profile.image/644263821971457-1530615873370_Q512/Cihan_Yakar.jpg",   
  "personId":"2c9e53a3-bbe3-43a0-9c28-0ec349e0ea62"   
} 

Aslında bu noktada yapmaya çalıştığımız şey “3 numarala adamın fotoğrafı bu” diyerek AI’a yeni bir fotoğraf daha vermek. 3 numaralı adamın Ahmet abi olduğunu biz biliyoruz çünkü zaten bir önceki turda Ahmet Abi için bir fotoğraf verdiğimizde geriye bize onu tanımlayan 3 sayısı gelmişti 🙂 Bu sayı bizim örneğimizde personId oluyor.

FaceClient faceClient = new FaceClient(new ApiKeyServiceClientCredentials(subscriptionKey), new System.Net.Http.DelegatingHandler[] { })
{
    Endpoint = faceEndpoint
};

//İlk Person Group'u ben demoyu çalıştırmak için daha önce yarattım.
//await faceClient.PersonGroup.CreateAsync(PersonGroupId, PersonGroupId); 
PersonGroup humanGroup = await faceClient.PersonGroup.GetAsync(PersonGroupId);

Yukarıdaki kod içerisinde ilk olarak Azure Cognitive Service ile konuşmamızı sağlayacak FaceClient’ı oluşturuyoruz. Bunun için öncesinde Azure hesabınızdan bir Cognitive Services Account almış olmanız şart. Söz konusu accoun’un Endpoint ve SubscriptionKey bilgilerini parametre olarak vermek gerekcek. Ben zaten Azure Function App’in local.settings.json’ına ve App Service’in de App Settings’ine koyduğum için herşey çalışacak.

İlk yapmamız gereken bir PersonGroup tanımlamacak. Cognitive Services’a bir foto verdiğimizde ve “Bu kim?” dediğimizde bir PersonGroup içerisinde arama yaptırabiliyoruz. Bu aramanın maliyetini bizim karşılayacağımızı düşünürsek dataseti PersonGroup’lara bölmek mantıklı. Örneğin, aradığınız kişinin zaten IT departmanında olduğunu biliyorsanız gidip de tüm şirkette lookup yapmaya gerek yok 🙂 IT departmanını bir PersonGroup’ta tutabilirseniz direk orada arama yapabilirsiniz. Bu arada, tüm bunlarla ilgili resmi dokümanları okumak isterseniz buyurun 🙂 Allah kolaylık versin. Şaka bir yana C# ile değil de başka bir dille yapacaksanız tüm bu işlemlerin REST API dokümanları biraz önceki linkte mevcut.

Yukarıdaki koddaki çak-geç noktalarında biri PersonGroup’un yaratıldığı yer. Ben bir defa çalıştırıp, yaratıp geçtim 🙂 O satırı commentlediğimi görebilirsiniz. Oraya bir kontrol koyup, yarattıysak al, yoksa yarat demek lazım. Bu arada PersonGroup yaratırken verdiğimiz ID de bir string ve o da kodda gözükmüyor ama final dosyada hard coded.

Person human = null;
if (string.IsNullOrEmpty(personId))
{
    human = await faceClient.PersonGroupPerson.CreateAsync(humanGroup.PersonGroupId, personName);
}
else
{
    human = await faceClient.PersonGroupPerson.GetAsync(humanGroup.PersonGroupId, new System.Guid(personId));
}

PersistedFace face = await faceClient.PersonGroupPerson.AddFaceFromUrlAsync(humanGroup.PersonGroupId, human.PersonId, imageUrl);

PersonGroup hazır olduğuna göre bir sonraki adım yeni bir insan yaratıp yüzü eklemek. PersonGroupPerson 🙂 olarak süper bir isimlendirmeye sahip entitymiz ile yeni bir insan yaratabiliyoruz. REST API’dan SDK Auto-Generate edersen böyle oluyor işte. Her neyse 🙂 Hatırlarsanız insan ismi gelirse yaratacağız, Id gelirse var olanı bulacağız demiştik. İşte koddaki IF bu yüzden orada 🙂 Son olarak AddFaceFromUrlAsync diyerek eldeki yüz fotoğrafını eldeki insana ekliyoruz.

await faceClient.PersonGroup.TrainAsync(PersonGroupId);

Veee finalde de tüm bu yüklediğimiz verilerle bir “Training” başlatıyoruz. Bu noktada da bir sürü uyarıda bulunmam gerek 🙂 Birincisi Training uzun sürebilir ve bunun için bir status check API’ı var. Onu kullanarak tekrar training request atmamakta fayda var. İkincisi Training’in de bir maliyeti var. O nedenle eğer fark yaratacak kadar data yüklediğinizi düşünmüyorsanız dakikada bir train etmenin anlamı yok. Ben etkinlikte demo yapacağım için tabi ki hızlıca train etmem gerekiyordu. O nedenle yüz ekleme kodunun sonuna yapıştırdım “Train” diye 🙂 Training’in de PersonGroup contextinde geliştiğini koddan anlamışsınızdır. O nedenle PersonGroup’ların designları aslında bir DB’deki partitionKey gibi kritik 🙂

var myObj = new { faceId = face.PersistedFaceId, personId = human.PersonId };
var jsonToReturn = JsonConvert.SerializeObject(myObj);

return new HttpResponseMessage(HttpStatusCode.OK)
{
    Content = new StringContent(jsonToReturn, Encoding.UTF8, "application/json")
};

Tüm bu işler bittikten sonra API’dan geriye faceId ve personId döndürüyoruz. Şu an için faceId’yi kullanmayacağız, fakat mobil uygulama aynı kişiye birden çok fotoğraf vermek istersen buradan aldığı personId ile gelerek yapabilir.

İşin aslına bakarsanız araya bir DB alıp bu Id’leri ve isimleri saklamak mantıklı olabilir. Her türlü lookup için sürekli Cognitive Services’a gitmekden daha düşük maliyetli olacaktır.

Validasyon Zamanı

Sıra geldi artık validasyon yapmaya. Kime ait olduğunu bilmediğimiz bir foto ile gelip “Bu kimdir?” diyeceğiz. Cognitive Services tanıyorsa bize tahminlerini iletecek. Biz de buna göre işlem yapacağız.

FaceClient faceClient = new FaceClient(new ApiKeyServiceClientCredentials(subscriptionKey), new System.Net.Http.DelegatingHandler[] { })
{
    Endpoint = faceEndpoint
};

IList<DetectedFace> foundFaces = await faceClient.Face.DetectWithUrlAsync(imageUrl, true);
var result = await faceClient.Face.IdentifyAsync(foundFaces.Select(x => x.FaceId.Value).ToList(), PersonGroupId, maxNumOfCandidatesReturned: 3);
var foundPersonId = result.FirstOrDefault()?.Candidates.FirstOrDefault()?.PersonId;
Person human = await faceClient.PersonGroupPerson.GetAsync(PersonGroupId, foundPersonId.GetValueOrDefault());

Kodun üst kısımlarını anlatmama gerek yok sanırım. Aydı FaceClient ile devam ediyoruz. İlk olarak Face.DetectWithUrlAsync diyerek eldeki fotoyu verip oradaki yüzleri buluyoruz. Sonra IdentifyAsync ile bulunan yüzlerin kime ait olduğunu soruyoruz. Burada yine benim çak-geç yaptığım bir çok nokta var 🙂 İlki gönderdiğim yüzlerden sadece ilkinin sonucunu inceliyor olmam 🙂 İkincisi ise gelen sonuçlardan (Candidates) sadece ilkine bakıyor olmam. Burada aslında birden çok kişinin olduğu bir fotoğraf verirseniz herkesin yüzleri alınarak, hepsi ile ilgili lookup yapılıyor. Geriye ise bir yüz listesi ve bu yüzlerin kimlere ait olabileceğine dair ayrı ayrı listeler geliyor. Ben etkinlikte yaptığım demoda hep tek kişilik fotoğraf vereceğimi bildiğim için çakıp geçtim 🙂 Ayrıca gelen “Candidates”‘in içindeki veriyi de pek önemsemedim. Bu noktada gelen listede “Bu kişi %50 Ahmet abi, %20 Saffet” gibi bir yorum oluyor 🙂 Bu verilerle ilgili siz farklı kurallar koyabilirsiniz. Nasıl yapacağız size kalmış. Benim ilgilendiğim sadece fotoğraftaki ilk yüzle ilgili ilk tahmin olduğu için geri kalanı salladım.

Ele personId geldikten sonra kişinin ismi de lazım olduğu için bir de PersonGroupPerson.GetAsync diyerek kişinin kendisi aldım. Bundan sonrası aşağıdaki gibi artık API’dan response dönmeye kalıyor.

var myObj = new { name = human.Name, personId = human.PersonId };
var jsonToReturn = JsonConvert.SerializeObject(myObj);

return new HttpResponseMessage(HttpStatusCode.OK)
{
    Content = new StringContent(jsonToReturn, Encoding.UTF8, "application/json")
};

İşte bu kadar 🙂 Her zaman olduğu gibi bu kodda da birçok ekleme yapmak gerek. Cognitive Services’a giden her call sonuç itibari ile dışarı giden bir call demek. Onları retry etmek ve transactional problemler çıkarmamak adına idempotency’ye saygılarınızı sunmanızda fayda var 🙂

Tüm bu kodların çalışır, full haline ulaşmak isterseniz Github’da duruyor. Hatta sevgili Yiğit’in yazdığı Xamarin istemcisi de aynı repo’da mevcut.

Başka neler yapılabilir?

Şirketin sharepointe atılan etkinlik fotoğraflarını otomatik tagleyebilirsiniz 🙂 Sharepoint’te bu özellik zaten varsa buradan saygılar. Şirket girişindeki kameradan işe kimin kaçta geldiğini kaydedebilirsiniz (İşsiz patron modeli) veya her yıl şirket genelinde çekilen o vesikalıkları AD’ye atarken hangi foto kimin sorusunun cevabını bulacak bir stajer yerine AI kullanabilirsiniz. Hayal gücünüzü yeterince zorladıysam 🙂 gerisini size bırakıyorum. Bu arada, bu saydıklarımın hiçbiri benim hayal gücümün göstergesi de sayılmasın 🙂 Hepsini sektörde isteyen firmalar duydum 🙂

Kolay gelsin.

Share This Post

XOGO CTO, Azure MVP, Microsoft RD

6 Comments

  1. Teşekkürler hocam.

  2. Eline sağlık Daron hocam.

  3. emeğine sağlık hocam.

Leave a Reply