Azure Functions คือ รูปแบบการทำระบบ Back-end ในรูปแบบใหม่โดยที่ท่านไม่จำเป็นต้องเปิดเครื่องทิ้งไว้เพื่อรอ Request อีกต่อไป หมายถึงเราไม่ต้องเสียเงินค่าเช่าเครื่อง server ตามระยะเวลา standby นั่นเอง แต่การคิดค่าใช้จ่ายจะเกิดขึ้นตอนเกิด event นั้นๆ หรือเกิด Request นั้นๆ รูปแบบนี้เราเรียกันว่า Serverless Architecture ดูโพสเต็มๆได้ที่ Azure Functions คืออะไร
ก่อนทำตามบทความนี้ให้ไปเตรียมเครื่องคอมฯให้พร้อมก่อน ไปอ่านที่นี่นะครับ การพัฒนา Azure Functions ด้วย Visual Studio 2017
บทความนี้ผมจะมาสอนการพัฒนา Azure Functions โดยใช้ Computer Vision AI จาก Azure Cognitive Services ตรวจสอบภาพโป้ที่ Azure Blob Storage แบบอัตโนมัติด้วย Blog Trigger
- เข้าไปที่เวป https://portal.azure.com แล้วกด Create resource => Compute => Function App
- ใส่ข้อมูลดังนี้ แล้วกดปุ่ม Create
- App name (ห้ามซ้ำ)
- Subscription
- Resource Group
- OS = Windows
- Hosting Plan = Consumption Plan
- Location = Southeast Asia
- Storage = Create new
- เข้าไปที่ Storage accounts (ถ้าหาไม่เจอให้ไปดูที่ All services) แล้วเลือก Storage Account ที่สร้างไว้ในข้อที่ 2
- ที่ Services เลือก Blobs
- กดปุ่ม + Container แล้วใส่ชื่อ ดังนี้ (ทำ 3 รอบ)
- uploaded
- accepted
- rejected
ปล. Public access level ไม่ต้องเลือกนะครับ ปกติมันจะเป็น Private (no anonymous access) อยู่แล้ว ไม่ต้องกลัวโดนแฮกซ์ (แต่ถ้าเปิด Public เอง เค้าไม่เรียกว่า แฮกซ์นะ)
- กดปุ่ม Create resource เลือก AP + Cognitive Services เลือก Computer Vision API
- ใส่ข้อมูลดังนี้ แล้วกด Create
Name
Subscription
Location = Southeast Asia
Pricing tier = F0 (ฟรี)
Resource group = Use existing
- ก๊อปปี้ Endpoint เก็บเอาไว้ แล้วกดตรงคำว่า Show access keys …
- ก๊อปปี้ KEY 1 เก็บเอาไว้
- เลือก Function Apps (ถ้าหาไม่เจอให้ไปที่ All services) แล้วเลือก Application settings
- กดปุ่ม + Add new setting เพื่อเพิ่ม SubscriptionKey กับ VisionEndpoint แล้วเอาสิ่งท่ีได้ก๊อปปี้มาจากข้อ 8 กับ 9 มาใส่ แล้วกดปุ่ม Save
- เปิด Visual Studio 2017 แล้ว Create New Project เลือก Cloud และ Azure Functions แล้วกด OK
- เลือก Azure Functions v2 Preview (.NET Core) และเลือก Empty
- เลือก Storage Account (AzureWebJobsStorage) เลือก Browse…
- เลือก Account => Subscription => Storage Account แล้วกด OK แล้วกด OK
- แก้ไขไฟล์ local.settings.json เพิ่มโค้ด (แก้ SubscriptionKey กับ VisionEndpoint กันเอาเองนะ)
,"SubscriptionKey": "8c3c44d70d6740e58f1e74a88c61a319",
"VisionEndpoint": "https://southeastasia.api.cognitive.microsoft.com/vision/v1.0"
- คลิ๊กขวามที่โปรเจ็ค Add => New Item …
- ค้นหาคำว่า azure แล้วเลือก Azure Function แล้วตั้งชื่อ Name ด้านล่าง แล้วกดปุ่ม Add
- เลือก Blob trigger แล้วใส่ข้อมูลดังนี้
- Connection string setting = AzureWebJobsStorage
- Path = uploaded
- แก้ไขไฟล์ AdultFunction.cs
เพิ่ม Using Namespace
using System;
using Microsoft.WindowsAzure.Storage;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Net.Http;
using System.Net;
เพิ่ม Model 2 คลาส ใช้สำหรับเป็นแปลง Result Json จาก Vision API มาเป็น Object
public class ImageAnalysisInfo
{
public Adult adult { get; set; }
public string requestId { get; set; }
}
public class Adult
{
public bool isAdultContent { get; set; }
public bool isRacyContent { get; set; }
public float adultScore { get; set; }
public float racyScore { get; set; }
}
เพิ่ม ToByteArrayAsync Method ที่ AdultFunction Class ใช้สำหรับแปลงจาก Stream ให้กลายเป็น Byte Array
// Converts a stream to a byte array
private async static Task<byte[]> ToByteArrayAsync(Stream stream)
{
var length = stream.Length > Int32.MaxValue ? Int32.MaxValue : Convert.ToInt32(stream.Length);
var buffer = new Byte[length];
await stream.ReadAsync(buffer, 0, length);
stream.Position = 0;
return buffer;
}
เพิ่ม AnalyzeImageAsync Method ที่ AdultFunction Class ใช้สำหรับเรียก Vision API เพื่อวิเคราะห์รูปภาพ
private async static Task<ImageAnalysisInfo> AnalyzeImageAsync(Stream myBlob)
{
var client = new HttpClient();
var key = Environment.GetEnvironmentVariable("SubscriptionKey");
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", key);
var blobArray = await ToByteArrayAsync(myBlob);
var content = new ByteArrayContent(blobArray);
content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/octet-stream");
var endpoint = Environment.GetEnvironmentVariable("VisionEndpoint");
var response = await client.PostAsync(endpoint + "/analyze?visualFeatures=Adult", content);
if (response.StatusCode == HttpStatusCode.OK)
{
return await response.Content.ReadAsAsync<ImageAnalysisInfo>();
}
else return null;
}
เพิ่ม StoreBlobWithMetadata Method ที่ AdultFunction Class ใช้สำหรับก๊อปปี้รูปไปที่ Azure Blob Storage Container แต่ละที่
// Writes a blob to a specified container and stores metadata with it
private async static void StoreBlobWithMetadata(Stream image, string containerName, string blobName, ImageAnalysisInfo info, TraceWriter log)
{
log.Info($"Writing blob and metadata to {containerName} container...");
var connection = Environment.GetEnvironmentVariable("AzureWebJobsStorage");
var account = CloudStorageAccount.Parse(connection);
var client = account.CreateCloudBlobClient();
var container = client.GetContainerReference(containerName);
try
{
var blob = container.GetBlockBlobReference(blobName);
if (blob != null)
{
// Upload the blob
await blob.UploadFromStreamAsync(image);
// Get the blob attributes
await blob.FetchAttributesAsync();
// Write the blob metadata
blob.Metadata["isAdultContent"] = info.adult.isAdultContent.ToString();
blob.Metadata["adultScore"] = info.adult.adultScore.ToString("P0").Replace(" ", "");
blob.Metadata["isRacyContent"] = info.adult.isRacyContent.ToString();
blob.Metadata["racyScore"] = info.adult.racyScore.ToString("P0").Replace(" ", "");
// Save the blob metadata
await blob.SetMetadataAsync();
}
}
catch (Exception ex)
{
log.Info(ex.Message);
}
}
แก้ไข Run Method ให้เป็นดังนี้
[FunctionName("AdultFunction")]
public async static void Run([BlobTrigger("uploaded/{name}", Connection = "AzureWebJobsStorage")]Stream myBlob, string name, TraceWriter log)
{
log.Info($"Analyzing uploaded image {name} for adult content...");
var info = await AnalyzeImageAsync(myBlob);
if (info == null)
{
log.Info("Cannot connect Azure Vision AI");
return;
}
log.Info("Is Adult: " + info.adult.isAdultContent.ToString());
log.Info("Adult Score: " + info.adult.adultScore.ToString());
log.Info("Is Racy: " + info.adult.isRacyContent.ToString());
log.Info("Racy Score: " + info.adult.racyScore.ToString());
if (info.adult.isAdultContent || info.adult.isRacyContent)
{
// Copy blob to the "rejected" container
StoreBlobWithMetadata(myBlob, "rejected", name, info, log);
}
else
{
// Copy blob to the "accepted" container
StoreBlobWithMetadata(myBlob, "accepted", name, info, log);
}
}
Run Method จะถูกเรียกใช้งานเมื่อ Blob Storage ที่ชื่อว่า uploaded มีไฟล์รูปอัพโหลดเข้าไป แล้วทำการเรียก Vision API เพื่อวิเคราะห์ภาพ เสร็จแล้วมาเช็คว่าถ้าเป็น isAdultContent หรือ isRacyContent จะถูกก๊อปปี้ไปที่ rejected container แต่ถ้าไม่ใช่จะ accepted container โดยแต่ละรูปภาพจะติด Meta Data ทั้ง 4 ที่ได้มาจาก Vision API เข้าไปด้วยนั่นเอง
- isAdultContent
- adultScore
- isRacyContent
- racyScore
สรุปไฟล์ AdultFunction.cs ทั้งหมดเป็นดังนี้
using System.IO;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using System;
using Microsoft.WindowsAzure.Storage;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Net.Http;
using System.Net;
namespace CBFunction
{
public static class AdultFunction
{
[FunctionName("AdultFunction")]
public async static void Run([BlobTrigger("uploaded/{name}", Connection = "AzureWebJobsStorage")]Stream myBlob, string name, TraceWriter log)
{
log.Info($"Analyzing uploaded image {name} for adult content...");
var info = await AnalyzeImageAsync(myBlob);
if (info == null)
{
log.Info("Cannot connect Azure Vision AI");
return;
}
log.Info("Is Adult: " + info.adult.isAdultContent.ToString());
log.Info("Adult Score: " + info.adult.adultScore.ToString());
log.Info("Is Racy: " + info.adult.isRacyContent.ToString());
log.Info("Racy Score: " + info.adult.racyScore.ToString());
if (info.adult.isAdultContent || info.adult.isRacyContent)
{
// Copy blob to the "rejected" container
StoreBlobWithMetadata(myBlob, "rejected", name, info, log);
}
else
{
// Copy blob to the "accepted" container
StoreBlobWithMetadata(myBlob, "accepted", name, info, log);
}
}
private async static Task<ImageAnalysisInfo> AnalyzeImageAsync(Stream myBlob)
{
var client = new HttpClient();
var key = Environment.GetEnvironmentVariable("SubscriptionKey");
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", key);
var blobArray = await ToByteArrayAsync(myBlob);
var content = new ByteArrayContent(blobArray);
content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/octet-stream");
var endpoint = Environment.GetEnvironmentVariable("VisionEndpoint");
var response = await client.PostAsync(endpoint + "/analyze?visualFeatures=Adult", content);
if (response.StatusCode == HttpStatusCode.OK)
{
return await response.Content.ReadAsAsync<ImageAnalysisInfo>();
}
else return null;
}
// Writes a blob to a specified container and stores metadata with it
private async static void StoreBlobWithMetadata(Stream image, string containerName, string blobName, ImageAnalysisInfo info, TraceWriter log)
{
log.Info($"Writing blob and metadata to {containerName} container...");
var connection = Environment.GetEnvironmentVariable("AzureWebJobsStorage");
var account = CloudStorageAccount.Parse(connection);
var client = account.CreateCloudBlobClient();
var container = client.GetContainerReference(containerName);
try
{
var blob = container.GetBlockBlobReference(blobName);
if (blob != null)
{
// Upload the blob
await blob.UploadFromStreamAsync(image);
// Get the blob attributes
await blob.FetchAttributesAsync();
// Write the blob metadata
blob.Metadata["isAdultContent"] = info.adult.isAdultContent.ToString();
blob.Metadata["adultScore"] = info.adult.adultScore.ToString("P0").Replace(" ", "");
blob.Metadata["isRacyContent"] = info.adult.isRacyContent.ToString();
blob.Metadata["racyScore"] = info.adult.racyScore.ToString("P0").Replace(" ", "");
// Save the blob metadata
await blob.SetMetadataAsync();
}
}
catch (Exception ex)
{
log.Info(ex.Message);
}
}
// Converts a stream to a byte array
private async static Task<byte[]> ToByteArrayAsync(Stream stream)
{
var length = stream.Length > Int32.MaxValue ? Int32.MaxValue : Convert.ToInt32(stream.Length);
var buffer = new Byte[length];
await stream.ReadAsync(buffer, 0, length);
stream.Position = 0;
return buffer;
}
}
public class ImageAnalysisInfo
{
public Adult adult { get; set; }
public string requestId { get; set; }
}
public class Adult
{
public bool isAdultContent { get; set; }
public bool isRacyContent { get; set; }
public float adultScore { get; set; }
public float racyScore { get; set; }
}
}
- กดปุ่ม F5 Run ทดสอบ Adult Function
- กลับมาที่เวป https://portal.azure.com แล้วเข้าไปที่ Storage accounts เหมือนข้อ 3 และเข้าไปที่ Blobs เหมือนข้อ 4 กดเข้าไปที่ uploaded
- กดปุ่ม Upload
- กดปุ่ม Browse เพื่อหารูปมาทดสอบ แล้วกดปุ่ม Upload (ถ้าหาไม่ได้สามารถดาวน์โหลดได้ที่นี่ https://a4r.blob.core.windows.net/public/functions-resources.zip)
- ถ้าอยากรู้ว่าโค้ดทำงานจริงรึไม่ ลองทดสอบ Debug ดูก่อนก็ได้
หลังจาก Upload แล้วซักพัก โค้ดจะวิ่งมาเป็นสีเหลืองตรงที่เรา Break Point เอาไว้ แสดงว่าโค้ดถูกทำงานจริง และกดปุ่ม Continue เพื่อให้โค้ดทำงานต่อจนจบ
- กลับไปที่ Azure Portal เพื่อไปดูรูป ถ้ารูปนั้นไม่ใช่รูปโป้จะถูกกีอปปี้ไปไว้ที่ accepted container แต่ถ้าเป็นภาพโป้จะถูกกีอปปี้ไปไว้ที่ rejected container
- กดปุ่ม … ที่ไฟล์ เพื่อเข้าไปดู Blob properties
- สังเกตุที่ Metadata ด้านล่าง จะเห็น Metadata ที่เราใส่เพิ่มเข้าไปในกระบวนการของ Adult Funtion นั่นเอง
- Stop Debug แล้วคลิ๊กขวาที่โปรเจ็คแล้วกดปุ่ม Publish เพิ่อนำ Adult Function ขึ้น Azure
- เลือก Select Existing แล้วกดปุ่ม Publish
- เลือก Subscription แล้วเลือก Azure Function ที่เราได้สร้างไว้ แล้วกดปุ่ม OK รอ Publish ขึ้น Azure ซักครู่
- ถ้ามีข้อความมาถามว่า Azure Function ที่เรากำลังพัฒนาอยู่นี้เป็น beta อยู่นะ จะให้เปลี่ยนแปลงค่า Setting ที่ Azure เลยมั้ย ตอบ Yes แล้วรอจนกว่าจะเสร็จ
- เข้าไปทดสอบอัพโหลดรูปที่ Azure Storage Account ที่ uploaded container อีกครั้ง แล้วดูผลจาก accepted container กับ rejected container
ที่มา: https://github.com/Microsoft/computerscience
ที่มา: https://github.com/Microsoft/computerscience/tree/master/Labs/Azure%20Services/Azure%20Functions
Source Code: https://github.com/CodeBangkok/CBFunction
บริจาค (Donate) ให้กับ CodeBangkok ได้ที่
BTC = 3GDxhb84ho2jmAV9seAgAFJ7dy1XR3GCyc
ETH = 0x119fa8A618A0283D1834853325A8FF4fe1101230
LTC = ME2abSdDeQYuTmzZSAnHL7LGGeF836d1ut
ZEC = t1Y4NkK3Dx3yBbwCVdpXzKYrqUSJSHgaFXa
https://www.lazada.co.th/products/codebangkok-i219874509-s334754949.html