使用 Azure Functions (serverless) 搭配 Microsoft Face API 解決你臉盲症的問題

我身旁有一些自稱患有「臉盲症」的朋友,就是他們經常活躍於許多社群,有些朋友可能結識於 A 社群,結果在 B 社群對方來打招呼時卻忘記了面前這位眼熟的仁兄在哪認識、名字是什麼等等.... 曾經有一個產品叫「Evernote Hello」想幫助臉盲症的朋友解決這個問題,不過這個產品似乎已經不存在了,所以我一時興起,在想做一些 Microsoft Cognitive Services 的展示範例時,就試著用其中的 Face API 來玩玩看!

設計想法

在這個 Face API 中,它可以讓你「永久性地」(persistent) 建立人物臉譜資料庫,只要你對同一個人物 (Person) 傳入幾張臉譜照片後,Face API 會自己學習這個人物的臉,爾後再傳入新的臉譜照片就能辨別是哪個人物的臉。

所以我打算設計的系統,主要就會分成兩個部份:

  1. 可以一直上傳新的照片讓 Face API 的系統學習認識某個人。(目前 API 的限制是一個人物最多可以傳 248 張臉譜)
  2. 可以傳入新的照片來回答我可能是哪一個人。(辨識臉譜可能會因為照片的解析度或光線等因素而影響結果,所以應該把結果當然是輔助參考而已)

因為還沒有決定前端、後台該用什麼方式呈現,所以我先打算使用 Azure Functions (serverless) 搭配 Azure EventHubs (queue) 及 Azure Blob / Table Storage 來做一個通用的核心。

實作流程

實作分為兩個部份:學習臉譜及辨識人臉。

學習臉譜

要能辨識人臉,至少要先學習認人吧!

這個系統核心很簡單,就是把要學習的人物及對應的臉蛋整理成如 json 格式的資料,再把這個 json 內容丟進 queue 裡驅動 Azure Functions 裡的其中一個「function」來完成學習的流程,最後把學習結果存在 Table Storage 裡查。

架構圖如下所示:

學習臉譜的系統流程

於是在 Azure Functions 中建立一個由 Event Hubs 驅動的 function,然後 output 是 Table Storage

(function.json)
{
"bindings": [
{
"type": "eventHubTrigger",
"name": "person",
"direction": "in",
"path": "learningq",
"connection": "skfaceblind_RootManageSharedAccessKey_EVENTHUB",
"cardinality": "one",
"consumerGroup": "$Default"
},
{
"type": "table",
"name": "personTable",
"tableName": "persons",
"connection": "skfaceblind_STORAGE",
"direction": "out"
},
{
"type": "table",
"name": "personEntity",
"tableName": "persons",
"connection": "skfaceblind_STORAGE",
"filter": "PartitionKey eq 'LearnedFace'",
"direction": "in"
}
],
"disabled": false
}

上述的 function.json 是描述這個 function 會由 Event Hubs 的新 item 驅動執行,學習完的結果會儲存在 table storage,但同時也會對同一張 table 查詢一些資料,所以設定了一個 table input 以及一個 table output,都是對應到 persons 這張 table。

而放進 event hubs (queue) 中的資訊大概會長成這樣:

(event hubs message)
{
"name": "Eric SK",
"faceImages": [
"https://......../path/to/ericsk1.jpg",
"https://......../path/to/ericsk2.jpg",
"https://......../path/to/ericsk3.jpg"
],
"data": {
"nick": "擊敗人",
"phone": "0978978978"
}
}

格式很簡單,這個資料放的就是待學習的人物臉譜,內容包含名稱 (name)、對應的臉譜照片 (faceImages) 以及這個人物的其它相關註解。

每一次要丟新的學習資料時,就產生這樣一個 JSON 格式的資料丟進 event hubs (queue) 裡。

再來就是當 function 被驅動後如何學習臉譜:

(index.js)
let req = require('request');
const FACE_API_KEY = '';
const PERSON_GROUP_ENDPOINT = '';
const PERSON_GROUP_ID = '';
const PERSON_GROUP_NAME = '';
module.exports = function (context, person) {
let personTable = context.bindings.personTable = [];
    let pgId, pId;
    // check if the person group has been created.
ensurePersonGroup(context)
.then((personGroupId)=>{
pgId = personGroupId;
return ensurePerson(context, personGroupId, person);
})
.then((personId) => {
pId = personId;
            let addingFaces = [];
person.faceImages.forEach(faceImage => {
addingFaces.push(addingPersonFace(context, pgId, pId, faceImage));
});
            return Promise.all(addingFaces);
})
.then(() => {
context.log('[Final] Training person group...');
req({
url: `${PERSON_GROUP_ENDPOINT}/${pgId}/train`,
method: 'POST',
headers: {
'Ocp-Apim-Subscription-Key': FACE_API_KEY
}
}, (e, r, b) => {
context.log('Done.')
context.done();
});
});
};
function ensurePersonGroup(context) {
context.log('[PersonGroup] Check if the person group is created...');
    return new Promise((resolve, reject) => {
req({
url: `${PERSON_GROUP_ENDPOINT}/${PERSON_GROUP_ID}`,
method: 'GET',
headers: {
'Ocp-Apim-Subscription-Key': FACE_API_KEY
},
json: true
}, (err, response, body) => {
if (body.error) {
context.log('[PersonGroup] Person group does not exist.');
req({
url: `${PERSON_GROUP_ENDPOINT}/${PERSON_GROUP_ID}`,
method: 'PUT',
body: {
"name": PERSON_GROUP_NAME
},
headers: {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': FACE_API_KEY
},
json: true
}, () => {
resolve(PERSON_GROUP_ID);
})
} else {
context.log('[PersonGroup] Person group existed.');
                resolve(PERSON_GROUP_ID);
}
});
});
}
function ensurePerson(context, personGroupId, person) {
return new Promise((resolve, reject) => {
context.log('[Person] Check if the person existed...');
        let result = context.bindings.personEntity.find((element) => element.RowKey == person.name);
if (result !== undefined) {
context.log(`[Person] Person ${result.PersonId} existed`);
resolve(result.PersonId);
} else {
context.log(`[Person] Creating new person...`);
req({
url: `${PERSON_GROUP_ENDPOINT}/${personGroupId}/persons`,
method: 'POST',
json: true,
headers: {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': FACE_API_KEY
},
body: {
'name': person.name,
'userData': JSON.stringify(person.data)
}
}, (e, r, b) => {
// write back to table storage
context.log(`[Person] New person ${b.personId} has been created...`);
context.bindings.personTable.push({
'PartitionKey': 'LearnedFace',
'RowKey': person.name,
'PersonId': b.personId
});
resolve(b.personId);
});
}
});
}
function addingPersonFace(context, personGroupId, personId, faceImageUrl) {
return new Promise((resolve, reject) => {
context.log('[AddingPersonFace] Adding person face...');
req({
url: `${PERSON_GROUP_ENDPOINT}/${personGroupId}/persons/${personId}/persistedFaces`,
method: 'POST',
json: true,
headers: {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': FACE_API_KEY
},
body: {
'url': faceImageUrl
}
}, (e, r, b) => {
context.log(`[AddingPersonFace] Added face ${b.persistedFaceId}...`);
resolve(b.persistedFaceId);
});
});
}

以下是這個 function 的工作流程:

  1. 先確定是否有建立 Person Group,可以自己設定 group id 跟 name,參考的是 Get a Person Group 以及 Create Person Group 兩個 API 來實作。
  2. 有了 Person Group(有 personGroupId)之後,也是先檢查是否有針對某個人(以他的名字為 key),否則就使用 Create a Person 的 API 來建立 Person 資料結構。建立完成的資料就存在 Table storage 裡做為未來檢查使用。
  3. 有了 group id 以及 person id 之後,就可以把照片加到指定的 Person 上做學習,這裡參考的是 Add a Person Face 這個 API 實作。
  4. 臉譜都加入後,要再呼叫一次 Train Person Group 這個 API 要它學習新的臉譜。

所以只要有新的臉孔照片,組合好對應的資訊,後面這些學習與記錄的工作就完成了。

辨識人物

有了臉譜的認知核心後,若碰到了面熟卻想不起來的人,就可以把新照片拿到另一個系統來進行辨識,這個系統架構大致如下所示:

辨識照片對應的臉譜系統架構

這裡我們用另一個 Azure Functions 的 function 來實作,建立一個由 Blob storage 驅動的 function,一有新的照片進來就拿去 Face API 針對訓練好的 person group 來進行辨識。

(function.json)
{
"bindings": [
{
"name": "image",
"type": "blobTrigger",
"direction": "in",
"path": "faces/{img}",
"connection": "skfaceblind_STORAGE",
"dataType": "binary"
},
{
"type": "table",
"name": "outputTable",
"tableName": "idlog",
"connection": "skfaceblind_STORAGE",
"direction": "out"
}
],
"disabled": false
}

上述的 function.json 是描述這個 function 會由 blob storage 的新檔案所驅動,路徑是 faces/{img},所以可以把新拍下來的照片放在這裡,驅動 function 起來做辨識的工作。

所以 function 的操作就很簡單,直接拿照片去比對先前建立好的 person group 來進行:

const req = require('request');
const FACE_API_KEY = '';
const FACE_API_ENDPOINT = '';
const FACE_PERSON_GROUP_ID = '';
module.exports = function (context, image) {
getFaceId(context, image)
.then((faceId) => {
return identifyFace(context, faceId);
})
.then((personId) => {
return getPerson(context, personId);
})
.then((name, data) => {
context.bindings.outputTable.push({
'PartitionKey': 'IdentifiedFace',
'RowKey': name,
'UserData': data
});
context.log("Done.");
context.done();
});
};
function getFaceId(context, image) {
return new Promise((resolve, reject) => {
context.log("[GETFACEID] Getting face ID...");
req({
url: `${FACE_API_ENDPOINT}/detect?returnFaceId=true&returnFaceLandmarks=false`,
method: 'POST',
body: image,
headers: {
'Content-Type': 'application/octet-stream',
'Ocp-Apim-Subscription-Key': FACE_API_KEY
}
}, (err, res, body) => {
let faces = JSON.parse(body);
context.log(`[GETFACEID] The Face ID is ${faces[0].faceId}`);
resolve(faces[0].faceId);
});
});
}
function identifyFace(context, faceId) {
return new Promise((resolve, reject) => {
context.log("[IDENTIFY] Identifying face....");
req({
url: `${FACE_API_ENDPOINT}/identify`,
method: 'POST',
json: true,
headers: {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': FACE_API_KEY
},
body: {
'personGroupId': FACE_PERSON_GROUP_ID,
'faceIds':[
faceId
],
"maxNumOfCandidatesReturned":1,
"confidenceThreshold": 0.5
}
}, (e, r, b) => {
context.log(`[IDENTIFY] Identified person: ${b[0].candidtes[0].personId}`);
resolve(b[0].candidtes[0].personId);
});
});
}
function getPerson(context, personId) {
return new Promise((resolve, reject) => {
req({
url: `${FACE_API_ENDPOINT}/persongroups/${FACE_PERSON_GROUP_ID}/persons/${personId}`,
method: 'GET',
json: true,
headers: {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': FACE_API_KEY
}
}, (e, r, b) => {
context.log(`[GET PERSON] Got person ${b.name}`);
resolve(b.name, b.userData);
});
});
}

簡單描述一下這個 function 的辨識流程:

  1. 先拿照片去 Face Detect API 來取得 face id,這個 id 的有效時間是 24 小時。
  2. 再拿 face id 去比對 person group,使用 Face Identify 這個 API 來實作,傳回對應的 Person id。
  3. 有了 person id 之後,再呼叫 Get a Person 這個 API 取得曾經儲存過的名稱及相關資訊。

結論與參考資料

有了這樣的核心系統,雖然不是非常專業完美,但也算是一個有趣的輔助系統,若善用 Microsoft Cognitive Services 內的這些智慧型認知服務,你我都能輕鬆開發出有趣又聰明的應用程式。而透過 Azure Functions (serverless) 來實作又不必維護伺服器或虛擬主機,相當方便!

本文所提及的程式碼可在 GitHub 上瀏覽,也歡迎追蹤「艾瑞克趣寫軟體」的 Facebook 專頁。

參考資料: