การพัฒนา Azure Functions ใช้ AI ตรวจสอบภาพโป้ที่ Blob Storage แบบอัตโนมัติ

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

  1. เข้าไปที่เวป https://portal.azure.com แล้วกด Create resource => Compute => Function App

  1. ใส่ข้อมูลดังนี้ แล้วกดปุ่ม Create
  • App name (ห้ามซ้ำ)
  • Subscription
  • Resource Group
  • OS = Windows
  • Hosting Plan = Consumption Plan
  • Location = Southeast Asia
  • Storage = Create new

  1. เข้าไปที่ Storage accounts (ถ้าหาไม่เจอให้ไปดูที่ All services) แล้วเลือก Storage Account ที่สร้างไว้ในข้อที่ 2

  1. ที่ Services เลือก Blobs

  1. กดปุ่ม + Container แล้วใส่ชื่อ ดังนี้ (ทำ 3 รอบ)
  • uploaded
  • accepted
  • rejected

ปล. Public access level ไม่ต้องเลือกนะครับ ปกติมันจะเป็น Private (no anonymous access) อยู่แล้ว ไม่ต้องกลัวโดนแฮกซ์ :smiley: (แต่ถ้าเปิด Public เอง เค้าไม่เรียกว่า แฮกซ์นะ)

2018-04-21_00-29-16

2018-04-21_01-40-56

  1. กดปุ่ม Create resource เลือก AP + Cognitive Services เลือก Computer Vision API

  1. ใส่ข้อมูลดังนี้ แล้วกด Create
    Name
    Subscription
    Location = Southeast Asia
    Pricing tier = F0 (ฟรี)
    Resource group = Use existing

  1. ก๊อปปี้ Endpoint เก็บเอาไว้ แล้วกดตรงคำว่า Show access keys …

  1. ก๊อปปี้ KEY 1 เก็บเอาไว้

2018-04-21_00-51-21

  1. เลือก Function Apps (ถ้าหาไม่เจอให้ไปที่ All services) แล้วเลือก Application settings

  1. กดปุ่ม + Add new setting เพื่อเพิ่ม SubscriptionKey กับ VisionEndpoint แล้วเอาสิ่งท่ีได้ก๊อปปี้มาจากข้อ 8 กับ 9 มาใส่ แล้วกดปุ่ม Save

  1. เปิด Visual Studio 2017 แล้ว Create New Project เลือก Cloud และ Azure Functions แล้วกด OK

  1. เลือก Azure Functions v2 Preview (.NET Core) และเลือก Empty

  1. เลือก Storage Account (AzureWebJobsStorage) เลือก Browse…

  1. เลือก Account => Subscription => Storage Account แล้วกด OK แล้วกด OK

  1. แก้ไขไฟล์ local.settings.json เพิ่มโค้ด (แก้ SubscriptionKey กับ VisionEndpoint กันเอาเองนะ)
,"SubscriptionKey": "8c3c44d70d6740e58f1e74a88c61a319",
"VisionEndpoint": "https://southeastasia.api.cognitive.microsoft.com/vision/v1.0"

  1. คลิ๊กขวามที่โปรเจ็ค Add => New Item …

  1. ค้นหาคำว่า azure แล้วเลือก Azure Function แล้วตั้งชื่อ Name ด้านล่าง แล้วกดปุ่ม Add

  1. เลือก Blob trigger แล้วใส่ข้อมูลดังนี้
  • Connection string setting = AzureWebJobsStorage
  • Path = uploaded

  1. แก้ไขไฟล์ 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; }
    }
}
  1. กดปุ่ม F5 Run ทดสอบ Adult Function

  1. กลับมาที่เวป https://portal.azure.com แล้วเข้าไปที่ Storage accounts เหมือนข้อ 3 และเข้าไปที่ Blobs เหมือนข้อ 4 กดเข้าไปที่ uploaded

2018-04-21_01-40-56

  1. กดปุ่ม Upload

  1. กดปุ่ม Browse เพื่อหารูปมาทดสอบ แล้วกดปุ่ม Upload (ถ้าหาไม่ได้สามารถดาวน์โหลดได้ที่นี่ https://a4r.blob.core.windows.net/public/functions-resources.zip)

2018-04-21_01-44-59

  1. ถ้าอยากรู้ว่าโค้ดทำงานจริงรึไม่ ลองทดสอบ Debug ดูก่อนก็ได้

หลังจาก Upload แล้วซักพัก โค้ดจะวิ่งมาเป็นสีเหลืองตรงที่เรา Break Point เอาไว้ แสดงว่าโค้ดถูกทำงานจริง และกดปุ่ม Continue เพื่อให้โค้ดทำงานต่อจนจบ

  1. กลับไปที่ Azure Portal เพื่อไปดูรูป ถ้ารูปนั้นไม่ใช่รูปโป้จะถูกกีอปปี้ไปไว้ที่ accepted container แต่ถ้าเป็นภาพโป้จะถูกกีอปปี้ไปไว้ที่ rejected container

  1. กดปุ่ม … ที่ไฟล์ เพื่อเข้าไปดู Blob properties

2018-04-21_01-55-16

  1. สังเกตุที่ Metadata ด้านล่าง จะเห็น Metadata ที่เราใส่เพิ่มเข้าไปในกระบวนการของ Adult Funtion นั่นเอง

  1. Stop Debug แล้วคลิ๊กขวาที่โปรเจ็คแล้วกดปุ่ม Publish เพิ่อนำ Adult Function ขึ้น Azure

2018-04-21_01-58-40

  1. เลือก Select Existing แล้วกดปุ่ม Publish

  1. เลือก Subscription แล้วเลือก Azure Function ที่เราได้สร้างไว้ แล้วกดปุ่ม OK รอ Publish ขึ้น Azure ซักครู่

  1. ถ้ามีข้อความมาถามว่า Azure Function ที่เรากำลังพัฒนาอยู่นี้เป็น beta อยู่นะ จะให้เปลี่ยนแปลงค่า Setting ที่ Azure เลยมั้ย ตอบ Yes แล้วรอจนกว่าจะเสร็จ

2018-04-21_02-04-44

  1. เข้าไปทดสอบอัพโหลดรูปที่ 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

https://www.lazada.co.th/products/c-net-core-i221844611-s338593893.html?spm=a2o6z.10453683.17.2.77ba30028gg3mg&mp=3

เนื่องจาก มีการอัพเดทใหม่ทำให้การ call function ‘Run’ แบบ async มีปัญหารบกวนแก้

public async static void Run.... เป็น 
public static void Run....

และ

var info = await AnalyzeImageAsync(myBlob); เป็น
var info = AnalyzeImageAsync(myBlob).Result;

แก้ปัญหาแค่ให้รันได้นะครับ ถ้าจะเอา async เพียวรอ คห. ด้านล่างเลย

2 Likes

ขอบคุณมากครับ @Arnonthawajjana