前回、顔検出アプリをつくる上で必要なファイルのアップロードの処理を実装することができたので、今回はAzure.CognitiveServicesをつかって顔検出アプリを完成させます。
動作確認はこちらからお願いします。
Contents
準備
まずnpmで以下をインストールします。
1 |
npm install @azure/cognitiveservices-face @azure/ms-rest-js |
そして以下をapp.jsに追加します。
1 2 |
const msRest = require("@azure/ms-rest-js"); const Face = require("@azure/cognitiveservices-face"); |
最初に前回のmain関数を示します。これによると/face-testにアクセスされたときと/face-resultにPostされたときの処理をすれば顔検出の処理ができるということになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function main(){ app.get('/', (req, res) => GetResponseIndex(req, res, obj)); app.get('/index.html', (req, res) => GetResponseIndex(req, res, obj)); app.get('/style.css', (req, res) => GetResponseStyleSeet(req, res, obj)); app.get('/form-test', (req, res) => GetResponseFormTest(req, res, obj)); app.post('/form-post', (req, res) => GetResponseFormPost(req, res, obj)); app.post('/file-upload', multer({dest: './'}).single('file'), (req, res) => { GetResponseFileUpload(req, res, obj); }); app.get('/face-test', (req, res) => GetResponseFaceTest(req, res, obj)); app.post('/face-result', multer({dest: './'}).single('file'), (req, res) => { GetResponseFaceResult(req, res, obj); }); app.use((req, res) => GetResponse404(req, res, obj)); var server = app.listen(port, function() { console.log("listening at port %s", server.address().port); }); } |
画像をアップロードするフォームを表示させる
/face-testにアクセスされたときはGetResponseFaceTest関数が呼び出されます。また/face-resultにPostされたときはGetResponseFaceResultが呼び出されます。
GetResponseFaceTest関数を示します。これは顔検出したいファイルをアップロードするためのフォームを表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function GetResponseFaceTest(req, res, obj){ obj.page_title = '顔認識の実験'; let str = ` <p>画像をアップロードしてください</p> <form action="face-result" method="POST" enctype="multipart/form-data"> <input type="file" name="file"> <input type="submit" value="Upload"> </form> `; obj.main_content = str; let content = ejs.render(index, obj); res.writeHead(200, {'Content-Type':'text/html'}); res.write(content); res.end(); } |
顔検出の結果を表示させる
次にフォームのアップロードボタンが押されたときの処理を示します。
まずアップロードされたファイルのパスを取得します。これはreq.file.pathで取得できます。もしファイルが指定されていないのであれば「ファイルが選択されていません」と表示させます。
画像ファイルのなかの顔を検出するのはDetectFaceExtract関数です。アップロードされたファイルが画像ファイルではない場合、例外が発生するので例外処理をしています。
DetectFaceExtract関数が正常に終了した場合、検出されたFaceオブジェクトが返されるので、これをGetFacesResultContent関数に渡します。これは引数で渡されたオブジェクトから顔の位置や状態(感情:喜んでいるとか怒っているなど)を取得して、表を作ります。それから検出された顔を矩形で囲んだ画像を表示させます。
これらの処理が終了したら結果をページに表示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
async function GetResponseFaceResult(req, res, obj){ obj.page_title = '顔認識の結果'; if(req.file == null){ obj.main_content = 'ファイルが選択されていません。'; let content = ejs.render(index, obj); res.writeHead(200, {'Content-Type':'text/html'}); res.write(content); res.end(); return; } let filePath = req.file.path; try { let detected_faces = await DetectFaceExtract(filePath); let result = await GetFacesResultContent(filePath, detected_faces); obj.main_content = result; } catch (e) { obj.main_content = '例外が発生しました'; } let content = ejs.render(index, obj); res.writeHead(200, {'Content-Type':'text/html'}); res.write(content); res.end(); fs.unlink(filePath, function(err){}); } |
顔検出するDetectFaceExtract関数
DetectFaceExtract関数は顔を検出したらオブジェクトを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
async function DetectFaceExtract(filePath) { const SUBSCRIPTION_KEY = "各自で取得してください"; const ENDPOINT = "各自で取得してください"; const credentials = new msRest.ApiKeyCredentials({ inHeader: { 'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY } }); const client = new Face.FaceClient(credentials, ENDPOINT); let obj = { returnFaceAttributes: [ "Accessories","Age","Blur","Emotion","Exposure","FacialHair", "Gender","Glasses","Hair","HeadPose","Makeup","Noise","Occlusion","Smile" ], detectionModel: "detection_01" }; let file = fs.readFileSync(filePath); let detected_faces = await client.face.detectWithStream(file, obj); return detected_faces; } |
dataUrlとTableタグで結果を表示
顔検出のオブジェクトが取得されたら、GetFacesResultContent関数を実行して顔オブジェクトから顔部分を矩形で囲んだ画像と顔の情報をTableタグを使って表示させるための文字列を取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
async function GetFacesResultContent(filePath, detected_faces){ // 顔を囲う矩形を配列に格納する let rectArray = []; for(let i =0; i<detected_faces.length; i++){ let left = detected_faces[i].faceRectangle.left; let top = detected_faces[i].faceRectangle.top; let width = detected_faces[i].faceRectangle.width; let height = detected_faces[i].faceRectangle.height; rectArray.push(new Rectangle(left, top, width, height)); } // 前回作成したファイルパスからdataUrlを生成する関数(ただし変更あり)を呼び出す。 let dataUrl = await GetDataUrl(filePath, rectArray); // 画像の幅が大きすぎる場合、最大でもカラムと同じ幅になるように // style="max-width: 100%;"を入れて表示サイズを調整する str = `<img src = "${dataUrl}" style="max-width: 100%;">\n`; // 顔の情報をTableタグを使って表示させるための文字列を生成する for(let i =0; i<detected_faces.length; i++){ str += `<p>${i+1} 人目 ====</p>`; str += GetTableText(detected_faces[i]); } // もし画像ファイルではあるけれども顔の検出ができなかった場合は //「顔は検出されませんでした」と表示 if(detected_faces.length == 0) str += '<p>顔は検出されませんでした。</p>'; return str; } |
顔として検出された部分に矩形を描画
前回のファイルパスからdataUrlを生成する関数を修正します。矩形の配列を第二引数に渡すとその部分に矩形を表示させます。
まず矩形の描画で必要なクラスを示します。GetFacesResultContent関数の第二引数からは矩形の左上の座標と幅と高さしか取得できませんが、ここから右の座標と下の座標は計算できます(GetRight関数とGetBottom関数)。このクラスのインスタンスから取得された情報で矩形を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Rectangle { constructor(left, top, width, height){ this.Left = left; this.Top = top; this.Width = width; this.Height = height; } GetRight(){ return this.Left + this.Width; } GetBottom(){ return this.Top + this.Height; } Left = 0; Top = 0; Width = 0; Height = 0; } |
一部修正したGetDataUrl関数を示します。第二引数がnullまたはundefinedのときは単にdataUrlを返すだけです。第二引数が存在する場合は矩形をDrawRectangles関数を呼び出して矩形を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
async function GetDataUrl(filePath, rectArray){ let dataUrl = ''; await Jimp.read(filePath).then(function (image) { if(rectArray != null && rectArray !== 'undefined') DrawRectangles(image, rectArray); image.getBase64(Jimp.MIME_PNG, (err, src) => dataUrl = src); }).catch(function (err) { console.error(err); return ''; }); return dataUrl; } |
矩形を画像のなかに描画するDrawRectangles関数を示します。矩形を描画する方法がわからなかったので、setPixelColor関数で1ピクセルずつ線の幅は3ピクセルになるように処理をしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
function DrawRectangles(image, rectArray){ let color = 0; for(let i =0; i< rectArray.length; i++) { let left = rectArray[i].Left; let top = rectArray[i].Top; let bottom = rectArray[i].GetBottom(); let right = rectArray[i].GetRight(); for(let x =0; x<100; x++){ for(let x =left; x<right; x++) { image.setPixelColor(color, x, top); image.setPixelColor(color, x, top + 1); image.setPixelColor(color, x, top - 1); } for(let x =left; x<right; x++) { image.setPixelColor(color, x, bottom); image.setPixelColor(color, x, bottom + 1); image.setPixelColor(color, x, bottom - 1); } for(let y =top; y<bottom; y++) { image.setPixelColor(color, left, y); image.setPixelColor(color, left + 1, y); image.setPixelColor(color, left - 1, y); } for(let y =top; y<bottom; y++) { image.setPixelColor(color, right, y); image.setPixelColor(color, right + 1, y); image.setPixelColor(color, right - 1, y); } } } } |
顔の属性をTableタグで表示
顔の属性を表示させるためのTableタグを生成するGetTableText関数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
function GetTableText(face){ let rect = face["faceRectangle"]; let atlib = face["faceAttributes"]; let headPose = atlib["headPose"]; let emotion = atlib["emotion"]; let facialHair = atlib["facialHair"]; let hair = atlib["hair"]; let makeup = atlib["makeup"]; let occlusion = atlib["occlusion"]; let accessories = atlib["accessories"]; let blur = atlib["blur"]; let exposure = atlib["exposure"]; let noise = atlib["noise"]; let accessoriesText = ''; let len = accessories.length; for(let i =0; i<len; i++){ let type = accessories[i]["type"]; let confidence = accessories[i]["confidence"]; accessoriesText += `${type} = ${confidence}, `; } let hairColorText = ''; len = hair["hairColor"].length; for(let i =0; i<len; i++){ let color = hair["hairColor"][i]["color"]; let confidence = hair["hairColor"][i]["confidence"]; hairColorText += `${color} = ${confidence}, `; } return ` <table border="1"> <tr><td>faceRectangle</td><td>width = ${rect["width"]} height = ${rect["height"]} left = ${rect["left"]} top = ${rect["top"]}</td></tr> <tr><td>age</td><td>${atlib["age"]}</td></tr> <tr><td>gender</td><td>${atlib["gender"]}</td></tr> <tr><td>smile</td><td>${atlib["smile"]}</td></tr> <tr><td>facialHair</td><td>moustache = ${facialHair["moustache"]} beard = ${facialHair["beard"]} sideburns = ${facialHair["sideburns"]}</td></tr> <tr><td>glasses</td><td>${atlib["glasses"]}</td></tr> <tr><td>headPose</td><td>roll = ${headPose["roll"]} yaw = ${headPose["yaw"]} pitch = ${headPose["pitch"]}</td></tr> <tr><td>emotion</td><td>anger = ${emotion["anger"]}</td></tr> <tr><td>emotion</td><td>contempt = ${emotion["contempt"]}</td></tr> <tr><td>emotion</td><td>disgust = ${emotion["disgust"]}</td></tr> <tr><td>emotion</td><td>fear = ${emotion["fear"]}</td></tr> <tr><td>emotion</td><td>happiness = ${emotion["happiness"]}</td></tr> <tr><td>emotion</td><td>neutral = ${emotion["neutral"]}</td></tr> <tr><td>emotion</td><td>sadness = ${emotion["sadness"]}</td></tr> <tr><td>emotion</td><td>surprise = ${emotion["surprise"]}</td></tr> <tr><td>hairColor</td><td>${hairColorText}</td></tr> <tr><td>makeup</td><td>eyeMakeup = ${makeup["eyeMakeup"]} lipMakeup = ${makeup["lipMakeup"]}</td></tr> <tr> <td>occlusion</td> <td>eyeOccluded = ${occlusion["eyeOccluded"]}, mouthOccluded = ${occlusion["mouthOccluded"]} foreheadOccluded = ${occlusion["foreheadOccluded"]}</td> </tr> <tr><td>accessories</td><td>${accessoriesText}</td></tr> <tr><td>blur</td><td>blurLevel = ${blur["blurLevel"]}, value = ${blur["value"]}</td></tr> <tr><td>exposure</td><td>exposureLevel = ${exposure["exposureLevel"]} value = ${exposure["value"]}</td></tr> <tr><td>noise</td><td>noiseLevel = ${noise["noiseLevel"]} value = ${noise["value"]}</td></tr> </table> ` } |