Pic-a-daily: Lab 1 - จัดเก็บและวิเคราะห์รูปภาพ (Java)

1. ภาพรวม

ใน Code Lab ครั้งแรก คุณจะต้องอัปโหลดภาพในที่เก็บข้อมูล การดำเนินการนี้จะสร้างเหตุการณ์การสร้างไฟล์ที่ฟังก์ชันจะจัดการ ฟังก์ชันดังกล่าวจะเรียกใช้ Vision API เพื่อทำการวิเคราะห์รูปภาพและบันทึกผลลัพธ์ลงในพื้นที่เก็บข้อมูล

d650ca5386ea71ad.png

สิ่งที่คุณจะได้เรียนรู้

  • Cloud Storage
  • Cloud Functions
  • Cloud Vision API
  • Cloud Firestore

2. การตั้งค่าและข้อกำหนด

การตั้งค่าสภาพแวดล้อมตามเวลาที่สะดวก

  1. ลงชื่อเข้าใช้ Google Cloud Console และสร้างโปรเจ็กต์ใหม่หรือใช้โปรเจ็กต์ที่มีอยู่ซ้ำ หากยังไม่มีบัญชี Gmail หรือ Google Workspace คุณต้องสร้างบัญชี

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • ชื่อโครงการคือชื่อที่แสดงของผู้เข้าร่วมโปรเจ็กต์นี้ เป็นสตริงอักขระที่ Google APIs ไม่ได้ใช้ โดยคุณจะอัปเดตได้ทุกเมื่อ
  • รหัสโปรเจ็กต์ต้องไม่ซ้ำกันในโปรเจ็กต์ Google Cloud ทั้งหมดและจะเปลี่ยนแปลงไม่ได้ (เปลี่ยนแปลงไม่ได้หลังจากตั้งค่าแล้ว) Cloud Console จะสร้างสตริงที่ไม่ซ้ำกันโดยอัตโนมัติ ปกติแล้วคุณไม่สนว่าอะไรเป็นอะไร ใน Codelab ส่วนใหญ่ คุณจะต้องอ้างอิงรหัสโปรเจ็กต์ (โดยปกติจะระบุเป็น PROJECT_ID) หากคุณไม่ชอบรหัสที่สร้างขึ้น คุณสามารถสร้างรหัสแบบสุ่มอื่นได้ หรือคุณจะลองดำเนินการเองแล้วดูว่าพร้อมให้บริการหรือไม่ และไม่สามารถเปลี่ยนแปลงได้หลังจากขั้นตอนนี้และจะยังคงอยู่ตลอดระยะเวลาของโปรเจ็กต์
  • สำหรับข้อมูลของคุณ ค่าที่ 3 คือหมายเลขโปรเจ็กต์ที่ API บางตัวใช้ ดูข้อมูลเพิ่มเติมเกี่ยวกับค่าทั้ง 3 ค่าเหล่านี้ในเอกสารประกอบ
  1. ถัดไป คุณจะต้องเปิดใช้การเรียกเก็บเงินใน Cloud Console เพื่อใช้ทรัพยากร/API ของระบบคลาวด์ การใช้งาน Codelab นี้น่าจะไม่มีค่าใช้จ่ายใดๆ หากมี หากต้องการปิดทรัพยากรเพื่อไม่ให้มีการเรียกเก็บเงินนอกเหนือจากบทแนะนำนี้ คุณสามารถลบทรัพยากรที่คุณสร้างหรือลบทั้งโปรเจ็กต์ได้ ผู้ใช้ใหม่ของ Google Cloud จะมีสิทธิ์เข้าร่วมโปรแกรมทดลองใช้ฟรี$300 USD

เริ่มต้น Cloud Shell

แม้ว่าคุณจะดำเนินการ Google Cloud จากระยะไกลได้จากแล็ปท็อป แต่คุณจะใช้ Google Cloud Shell ซึ่งเป็นสภาพแวดล้อมแบบบรรทัดคำสั่งที่ทำงานในระบบคลาวด์ใน Codelab นี้

จากคอนโซล Google Cloud ให้คลิกไอคอน Cloud Shell ในแถบเครื่องมือด้านขวาบน ดังนี้

55efc1aaa7a4d3ad.png

การจัดสรรและเชื่อมต่อกับสภาพแวดล้อมนี้ควรใช้เวลาเพียงครู่เดียว เมื่อเสร็จแล้ว คุณจะเห็นข้อมูลต่อไปนี้

7ffe5cbb04455448.png

เครื่องเสมือนนี้เต็มไปด้วยเครื่องมือการพัฒนาทั้งหมดที่คุณต้องการ โดยมีไดเรกทอรีหลักขนาด 5 GB ที่ใช้งานได้ต่อเนื่องและทำงานบน Google Cloud ซึ่งช่วยเพิ่มประสิทธิภาพของเครือข่ายและการตรวจสอบสิทธิ์ได้อย่างมาก งานทั้งหมดใน Codelab นี้ทำได้ในเบราว์เซอร์ คุณไม่จำเป็นต้องติดตั้งอะไร

3. เปิดใช้ API

สำหรับห้องทดลองนี้ คุณจะใช้ Cloud Functions และ Vision API แต่ต้องเปิดใช้ใน Cloud Console หรือด้วย gcloud ก่อน

หากต้องการเปิดใช้ Vision API ใน Cloud Console ให้ค้นหา Cloud Vision API ในแถบค้นหา

cf48b1747ba6a6fb.png

คุณจะเข้าสู่หน้า Cloud Vision API:

ba4af419e6086fbb.png

คลิกปุ่ม ENABLE

หรือคุณจะเปิดใช้ Cloud Shell โดยใช้เครื่องมือบรรทัดคำสั่ง gcloud ก็ได้

เรียกใช้คำสั่งต่อไปนี้ใน Cloud Shell

gcloud services enable vision.googleapis.com

คุณควรเห็นการดำเนินการที่เสร็จสิ้นเรียบร้อยแล้ว

Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.

เปิดใช้ Cloud Functions ด้วยโดยทำดังนี้

gcloud services enable cloudfunctions.googleapis.com

4. สร้างที่เก็บข้อมูล (คอนโซล)

สร้างที่เก็บข้อมูลของพื้นที่เก็บข้อมูลสำหรับรูปภาพ โดยทำได้จากคอนโซล Google Cloud Platform ( console.cloud.google.com) หรือด้วยเครื่องมือบรรทัดคำสั่ง gsutil จาก Cloud Shell หรือสภาพแวดล้อมการพัฒนาในเครื่อง

จาก "แฮมเบอร์เกอร์" (⋮) ให้ไปที่หน้าStorage

1930e055d138150a.png

ตั้งชื่อที่เก็บข้อมูล

คลิกปุ่ม CREATE BUCKET

34147939358517f8.png

คลิก CONTINUE

เลือกตำแหน่ง

197817f20be07678.png

สร้างที่เก็บข้อมูลหลายภูมิภาคในภูมิภาคที่คุณเลือก (ที่นี่ Europe)

คลิก CONTINUE

เลือกคลาสพื้นที่เก็บข้อมูลเริ่มต้น

53cd91441c8caf0e.png

เลือกคลาสพื้นที่เก็บข้อมูล Standard สำหรับข้อมูลของคุณ

คลิก CONTINUE

ตั้งค่าการควบคุมการเข้าถึง

8c2b3b459d934a51.png

เนื่องจากคุณจะต้องใช้งานรูปภาพที่เข้าถึงได้แบบสาธารณะ คุณต้องการให้รูปภาพทั้งหมดที่จัดเก็บในที่เก็บข้อมูลนี้มีการควบคุมการเข้าถึงแบบเดียวกัน

เลือกตัวเลือกการควบคุมการเข้าถึง Uniform

คลิก CONTINUE

ตั้งค่าการป้องกัน/การเข้ารหัส

d931c24c3e705a68.png

เก็บค่าเริ่มต้นไว้ (Google-managed key) เนื่องจากคุณจะไม่ใช้คีย์การเข้ารหัสของคุณเอง

คลิก CREATE เพื่อสร้างที่เก็บข้อมูลขั้นสุดท้าย

เพิ่ม allUsers เป็นผู้ดูพื้นที่เก็บข้อมูล

ไปที่แท็บ Permissions:

d0ecfdcff730ea51.png

เพิ่มสมาชิก allUsers ไปยังที่เก็บข้อมูล โดยมีบทบาทเป็น Storage > Storage Object Viewer ดังนี้

e9f25ec1ea0b6cc6.png

คลิก SAVE

5. สร้างที่เก็บข้อมูล (gsutil)

คุณยังใช้เครื่องมือบรรทัดคำสั่ง gsutil ใน Cloud Shell เพื่อสร้างที่เก็บข้อมูลได้ด้วย

ใน Cloud Shell ให้ตั้งตัวแปรสำหรับชื่อที่เก็บข้อมูลที่ไม่ซ้ำกัน Cloud Shell มี GOOGLE_CLOUD_PROJECT ที่ตั้งค่าเป็นรหัสโปรเจ็กต์ที่ไม่ซ้ำกันอยู่แล้ว คุณจะเพิ่มค่านั้นต่อท้ายชื่อที่เก็บข้อมูลได้

เช่น

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

สร้างโซนหลายภูมิภาคตามมาตรฐานในยุโรป

gsutil mb -l EU gs://${BUCKET_PICTURES}

ตรวจสอบว่าสิทธิ์เข้าถึงระดับที่เก็บข้อมูลแบบเดียวกัน

gsutil uniformbucketlevelaccess set on gs://${BUCKET_PICTURES}

ทำให้ที่เก็บข้อมูลเป็นแบบสาธารณะ โดยทำดังนี้

gsutil iam ch allUsers:objectViewer gs://${BUCKET_PICTURES}

หากไปที่ส่วน Cloud Storage ของคอนโซล คุณควรมีที่เก็บข้อมูล uploaded-pictures สาธารณะ

a98ed4ba17873e40.png

ทดสอบว่าคุณสามารถอัปโหลดรูปภาพไปยังที่เก็บข้อมูล และภาพที่อัปโหลดจะปรากฏต่อสาธารณะ ตามที่อธิบายไว้ในขั้นตอนก่อนหน้า

6. ทดสอบสิทธิ์เข้าถึงที่เก็บข้อมูลแบบสาธารณะ

การกลับไปยังเบราว์เซอร์พื้นที่เก็บข้อมูล คุณจะเห็นที่เก็บข้อมูลในรายการ โดยมี "สาธารณะ" (รวมถึงป้ายเตือนที่บอกว่าทุกคนมีสิทธิ์เข้าถึงเนื้อหาของที่เก็บข้อมูลนั้น)

89e7a4d2c80a0319.png

ตอนนี้ที่เก็บข้อมูลของคุณพร้อมรับรูปภาพแล้ว

หากคลิกชื่อที่เก็บข้อมูล คุณจะเห็นรายละเอียดที่เก็บข้อมูล

131387f12d3eb2d3.png

จากจุดนี้ คุณสามารถลองใช้ปุ่ม Upload files เพื่อทดสอบว่าคุณเพิ่มรูปภาพลงในที่เก็บข้อมูลได้ ป๊อปอัปตัวเลือกไฟล์จะขอให้คุณเลือกไฟล์ เมื่อเลือกแล้ว ระบบจะอัปโหลดไฟล์ดังกล่าวไปยังที่เก็บข้อมูลของคุณ และคุณจะเห็นสิทธิ์เข้าถึง public ที่ระบบระบุแหล่งที่มาไปยังไฟล์ใหม่นี้โดยอัตโนมัติอีกครั้ง

e87584471a6e9c6d.png

คุณจะเห็นไอคอนลิงก์ขนาดเล็กที่ป้ายกำกับการเข้าถึง Public ด้วย เมื่อคลิกที่รูปภาพ เบราว์เซอร์ของคุณจะนำทางไปยัง URL สาธารณะของรูปภาพนั้น ซึ่งจะอยู่ในรูปแบบ:

https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png

โดย BUCKET_NAME เป็นชื่อที่ไม่ซ้ำกันทั่วโลกซึ่งคุณได้เลือกไว้สำหรับที่เก็บข้อมูล และตามด้วยชื่อไฟล์ของรูปภาพ

เมื่อคลิกช่องทำเครื่องหมายข้างชื่อภาพ ปุ่ม DELETE จะเปิดการใช้งาน และคุณจะสามารถลบรูปภาพแรกนี้ได้

7. สร้างฟังก์ชัน

ในขั้นตอนนี้ คุณจะได้สร้างฟังก์ชันที่ตอบสนองต่อเหตุการณ์การอัปโหลดรูปภาพ

ไปที่ส่วนCloud Functionsของคอนโซล Google Cloud การเข้าชมนี้จะเปิดใช้บริการ Cloud Functions โดยอัตโนมัติ

9d29e8c026a7a53f.png

คลิกที่ Create function

เลือกชื่อ (เช่น picture-uploaded) และภูมิภาค (โปรดทราบว่าต้องสอดคล้องกับตัวเลือกภูมิภาคสำหรับที่เก็บข้อมูล):

4bb222633e6f278.png

ฟังก์ชันมี 2 ประเภท ได้แก่

  • ฟังก์ชัน HTTP ที่สามารถเรียกใช้ผ่าน URL (เช่น API ของเว็บ)
  • ฟังก์ชันเบื้องหลังที่เหตุการณ์บางอย่างทริกเกอร์ได้

คุณต้องการสร้างฟังก์ชันพื้นหลังที่ทริกเกอร์เมื่อมีการอัปโหลดไฟล์ใหม่ไปยังที่เก็บข้อมูล Cloud Storage ของเรา ดังนี้

d9a12fcf58f4813c.png

คุณสนใจประเภทเหตุการณ์ Finalize/Create ซึ่งเป็นเหตุการณ์ที่จะทริกเกอร์เมื่อมีการสร้างหรืออัปเดตไฟล์ในที่เก็บข้อมูล

b30c8859b07dc4cb.png

เลือกที่เก็บข้อมูลที่สร้างขึ้นก่อนหน้านี้เพื่อแจ้งให้ Cloud Functions รับการแจ้งเตือนเมื่อมีการสร้าง / อัปเดตไฟล์ในที่เก็บข้อมูลเฉพาะนี้

cb15a1f4c7a1ca5f.png

คลิก Select เพื่อเลือกที่เก็บข้อมูลที่คุณสร้างไว้ก่อนหน้านี้ จากนั้นคลิก Save

c1933777fac32c6a.png

ก่อนคลิก "ถัดไป" คุณจะขยายและแก้ไขค่าเริ่มต้น (หน่วยความจำ 256 MB) ได้ในส่วนการตั้งค่ารันไทม์ บิลด์ การเชื่อมต่อ และความปลอดภัย แล้วอัปเดตเป็น 1 GB

83d757e6c38e10.png

หลังจากคลิก Next คุณจะปรับแต่งรันไทม์ ซอร์สโค้ด และจุดเริ่มต้นได้

เก็บ Inline editor ไว้สำหรับฟังก์ชันนี้:

b6646ec646082b32.png

เลือกรันไทม์ของ Java เช่น Java 11

f85b8a6f951f47a7.png

ซอร์สโค้ดประกอบด้วยไฟล์ Java และไฟล์ Maven pom.xml ที่มีข้อมูลเมตาและการอ้างอิงที่หลากหลาย

เก็บข้อมูลโค้ดเริ่มต้นไว้ซึ่งจะบันทึกชื่อไฟล์ของรูปภาพที่อัปโหลด

9b7b9801b42f6ca6.png

ในตอนนี้ โปรดเก็บชื่อของฟังก์ชันที่จะเรียกใช้ Example ไว้สำหรับการทดสอบ

คลิกที่ Deploy เพื่อสร้างและทำให้ฟังก์ชันใช้งานได้ เมื่อการติดตั้งใช้งานเสร็จสมบูรณ์ คุณจะเห็นเครื่องหมายถูกวงกลมสีเขียวในรายการฟังก์ชัน

3732fdf409eefd1a.png

8. ทดสอบฟังก์ชัน

ในขั้นตอนนี้ ให้ทดสอบว่าฟังก์ชันตอบสนองต่อเหตุการณ์พื้นที่เก็บข้อมูลหรือไม่

จาก "แฮมเบอร์เกอร์" (⋮) ให้กลับไปที่หน้า Storage

คลิกที่ที่เก็บข้อมูลรูปภาพ จากนั้นคลิก Upload files เพื่ออัปโหลดรูปภาพ

21767ec3cb8b18de.png

โปรดนำทางอีกครั้งภายใน Cloud Console เพื่อไปที่หน้า Logging > Logs Explorer

ในตัวเลือก Log Fields ให้เลือก Cloud Function เพื่อดูบันทึกสำหรับฟังก์ชันของคุณโดยเฉพาะ เลื่อนลงตามฟิลด์บันทึกและคุณสามารถเลือกฟังก์ชันที่ต้องการเพื่อดูบันทึกที่เกี่ยวข้องกับฟังก์ชันอย่างละเอียด เลือกฟังก์ชัน picture-uploaded

คุณจะเห็นรายการในบันทึกที่กล่าวถึงการสร้างฟังก์ชัน เวลาเริ่มต้นและสิ้นสุดของฟังก์ชัน และข้อความบันทึกจริงของเรา:

e8ba7d39c36df36c.png

ข้อความบันทึกของเราคือ Processing file: pic-a-daily-architecture-events.png ซึ่งหมายความว่ามีการทริกเกอร์เหตุการณ์ที่เกี่ยวข้องกับการสร้างและจัดเก็บรูปภาพนี้ตามที่คาดไว้แล้ว

9. เตรียมฐานข้อมูล

คุณจะจัดเก็บข้อมูลเกี่ยวกับรูปภาพที่ได้รับจาก Vision API ไว้ในฐานข้อมูล Cloud Firestore ซึ่งเป็นฐานข้อมูลเอกสาร NoSQL ที่ดำเนินการบนระบบคลาวด์และทำงานได้อย่างรวดเร็ว มีการจัดการครบวงจร เตรียมฐานข้อมูลด้วยการไปที่ส่วน Firestore ของ Cloud Console:

9e4708d2257de058.png

โดยมี 2 ตัวเลือก ได้แก่ Native mode หรือ Datastore mode ใช้โหมดเนทีฟซึ่งมีฟีเจอร์เพิ่มเติม เช่น การรองรับการใช้งานออฟไลน์และการซิงค์ข้อมูลแบบเรียลไทม์

คลิก SELECT NATIVE MODE

9449ace8cc84de43.png

เลือกหลายภูมิภาค (ที่นี่ในยุโรป แต่อย่างน้อยควรเป็นภูมิภาคเดียวกับฟังก์ชันและที่เก็บข้อมูลของพื้นที่เก็บข้อมูล)

คลิกปุ่ม CREATE DATABASE

เมื่อสร้างฐานข้อมูลแล้ว คุณจะเห็นสิ่งต่อไปนี้

56265949a124819e.png

สร้างคอลเล็กชันใหม่โดยคลิกปุ่ม + START COLLECTION

ชื่อคอลเล็กชัน pictures

75806ee24c4e13a7.png

คุณไม่ต้องสร้างเอกสาร คุณจะเพิ่มรูปภาพเหล่านั้นโดยใช้โปรแกรมเมื่อมีการจัดเก็บรูปภาพใหม่ใน Cloud Storage และวิเคราะห์โดย Vision API

คลิก Save

Firestore สร้างเอกสารเริ่มต้นรายการแรกในคอลเล็กชันที่สร้างขึ้นใหม่ คุณลบเอกสารดังกล่าวได้อย่างปลอดภัยเนื่องจากไม่มีข้อมูลที่เป็นประโยชน์ใดๆ ดังต่อไปนี้

5c2f1e17ea47f48f.png

เอกสารที่จะสร้างขึ้นแบบเป็นโปรแกรมในคอลเล็กชันของเราประกอบด้วย 4 ฟิลด์ ดังนี้

  • name (สตริง): ชื่อไฟล์ของรูปภาพที่อัปโหลด ซึ่งเป็นคีย์ของเอกสาร
  • labels (อาร์เรย์ของสตริง): ป้ายกำกับของรายการที่รู้จักโดย Vision API
  • color (สตริง): รหัสสีเลขฐาน 16 ของสีหลัก (เช่น #ab12ef)
  • สร้าง (วันที่): การประทับเวลาที่เก็บข้อมูลเมตาของรูปภาพนี้
  • ภาพขนาดย่อ (บูลีน): ช่องที่ไม่บังคับซึ่งจะแสดงและเป็น "จริง" หากมีการสร้างภาพขนาดย่อสำหรับรูปภาพนี้

เนื่องจากเราจะค้นหาใน Firestore เพื่อค้นหารูปภาพที่มีภาพขนาดย่อ และจัดเรียงตามวันที่สร้าง เราจะต้องสร้างดัชนีการค้นหา

คุณสร้างดัชนีด้วยคำสั่งต่อไปนี้ใน Cloud Shell ได้

gcloud firestore indexes composite create \
  --collection-group=pictures \
  --field-config field-path=thumbnail,order=descending \
  --field-config field-path=created,order=descending

หรือดำเนินการจาก Cloud Console โดยคลิก Indexes ในคอลัมน์การนำทางด้านซ้าย แล้วสร้างดัชนีผสมดังที่แสดงด้านล่างก็ได้

ecb8b95e3c791272.png

คลิก Create การสร้างดัชนีอาจใช้เวลาสักครู่

10. อัปเดตฟังก์ชัน

กลับไปที่หน้า Functions เพื่ออัปเดตฟังก์ชันเพื่อเรียกใช้ Vision API เพื่อวิเคราะห์รูปภาพและจัดเก็บข้อมูลเมตาใน Firestore

จาก "แฮมเบอร์เกอร์" (⋮) ไปยังส่วน Cloud Functions จากนั้นคลิกชื่อฟังก์ชัน จากนั้นเลือกแท็บ Source แล้วคลิกปุ่ม EDIT

ก่อนอื่น ให้แก้ไขไฟล์ pom.xml ซึ่งแสดงรายการทรัพยากร Dependency ของฟังก์ชัน Java อัปเดตโค้ดเพื่อเพิ่มทรัพยากร Dependency ของ Cloud Vision API Maven ดังนี้

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>cloudfunctions</groupId>
  <artifactId>gcs-function</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.target>11</maven.compiler.target>
    <maven.compiler.source>11</maven.compiler.source>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>libraries-bom</artifactId>
        <version>26.1.1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>com.google.cloud.functions</groupId>
      <artifactId>functions-framework-api</artifactId>
      <version>1.0.4</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-firestore</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-vision</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-storage</artifactId>
    </dependency>
  </dependencies>

  <!-- Required for Java 11 functions in the inline editor -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <excludes>
            <exclude>.google/</exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

ตอนนี้ทรัพยากร Dependency เป็นปัจจุบันแล้ว คุณจะทำงานกับโค้ดของฟังก์ชันได้ โดยอัปเดตไฟล์ Example.java ด้วยโค้ดที่กำหนดเองของเรา

เลื่อนเมาส์เหนือไฟล์ Example.java แล้วคลิกดินสอ เปลี่ยนชื่อแพ็กเกจและชื่อไฟล์เป็น src/main/java/fn/ImageAnalysis.java

แทนที่รหัสใน ImageAnalysis.java ด้วยรหัสด้านล่าง ซึ่งจะอธิบายในขั้นตอนถัดไป

package fn;

import com.google.cloud.functions.*;
import com.google.cloud.vision.v1.*;
import com.google.cloud.vision.v1.Feature.Type;
import com.google.cloud.firestore.*;
import com.google.api.core.ApiFuture;

import java.io.*;
import java.util.*;
import java.util.stream.*;
import java.util.concurrent.*;
import java.util.logging.Logger;

import fn.ImageAnalysis.GCSEvent;

public class ImageAnalysis implements BackgroundFunction<GCSEvent> {
    private static final Logger logger = Logger.getLogger(ImageAnalysis.class.getName());

    @Override
    public void accept(GCSEvent event, Context context) 
            throws IOException, InterruptedException, ExecutionException {
        String fileName = event.name;
        String bucketName = event.bucket;

        logger.info("New picture uploaded " + fileName);

        try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
            List<AnnotateImageRequest> requests = new ArrayList<>();
            
            ImageSource imageSource = ImageSource.newBuilder()
                .setGcsImageUri("gs://" + bucketName + "/" + fileName)
                .build();

            Image image = Image.newBuilder()
                .setSource(imageSource)
                .build();

            Feature featureLabel = Feature.newBuilder()
                .setType(Type.LABEL_DETECTION)
                .build();
            Feature featureImageProps = Feature.newBuilder()
                .setType(Type.IMAGE_PROPERTIES)
                .build();
            Feature featureSafeSearch = Feature.newBuilder()
                .setType(Type.SAFE_SEARCH_DETECTION)
                .build();
                
            AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
                .addFeatures(featureLabel)
                .addFeatures(featureImageProps)
                .addFeatures(featureSafeSearch)
                .setImage(image)
                .build();
            
            requests.add(request);

            logger.info("Calling the Vision API...");
            BatchAnnotateImagesResponse result = vision.batchAnnotateImages(requests);
            List<AnnotateImageResponse> responses = result.getResponsesList();

            if (responses.size() == 0) {
                logger.info("No response received from Vision API.");
                return;
            }

            AnnotateImageResponse response = responses.get(0);
            if (response.hasError()) {
                logger.info("Error: " + response.getError().getMessage());
                return;
            }

            List<String> labels = response.getLabelAnnotationsList().stream()
                .map(annotation -> annotation.getDescription())
                .collect(Collectors.toList());
            logger.info("Annotations found:");
            for (String label: labels) {
                logger.info("- " + label);
            }

            String mainColor = "#FFFFFF";
            ImageProperties imgProps = response.getImagePropertiesAnnotation();
            if (imgProps.hasDominantColors()) {
                DominantColorsAnnotation colorsAnn = imgProps.getDominantColors();
                ColorInfo colorInfo = colorsAnn.getColors(0);

                mainColor = rgbHex(
                    colorInfo.getColor().getRed(), 
                    colorInfo.getColor().getGreen(), 
                    colorInfo.getColor().getBlue());

                logger.info("Color: " + mainColor);
            }

            boolean isSafe = false;
            if (response.hasSafeSearchAnnotation()) {
                SafeSearchAnnotation safeSearch = response.getSafeSearchAnnotation();

                isSafe = Stream.of(
                    safeSearch.getAdult(), safeSearch.getMedical(), safeSearch.getRacy(),
                    safeSearch.getSpoof(), safeSearch.getViolence())
                .allMatch( likelihood -> 
                    likelihood != Likelihood.LIKELY && likelihood != Likelihood.VERY_LIKELY
                );

                logger.info("Safe? " + isSafe);
            }

            // Saving result to Firestore
            if (isSafe) {
                FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
                Firestore pictureStore = firestoreOptions.getService();

                DocumentReference doc = pictureStore.collection("pictures").document(fileName);

                Map<String, Object> data = new HashMap<>();
                data.put("labels", labels);
                data.put("color", mainColor);
                data.put("created", new Date());

                ApiFuture<WriteResult> writeResult = doc.set(data, SetOptions.merge());

                logger.info("Picture metadata saved in Firestore at " + writeResult.get().getUpdateTime());
            }
        }
    }

    private static String rgbHex(float red, float green, float blue) {
        return String.format("#%02x%02x%02x", (int)red, (int)green, (int)blue);
    }

    public static class GCSEvent {
        String bucket;
        String name;
    }
}

968749236c3f01da.png

11. สำรวจฟังก์ชัน

มาดูรายละเอียดเกี่ยวกับส่วนต่างๆ ที่น่าสนใจกัน

โดยก่อนอื่น เราจะรวมทรัพยากร Dependency เฉพาะในไฟล์ Maven pom.xml ไลบรารีของไคลเอ็นต์ Google Java จะเผยแพร่ Bill-of-Materials(BOM) เพื่อกำจัดความขัดแย้งของทรัพยากร Dependency ในการใช้งาน คุณไม่จำเป็นต้องระบุเวอร์ชันใดๆ สำหรับไลบรารีของไคลเอ็นต์ Google แต่ละรายการ

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>libraries-bom</artifactId>
        <version>26.1.1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

จากนั้นเราจะเตรียมไคลเอ็นต์สำหรับ Vision API ดังนี้

...
try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
...

ตอนนี้มาถึงโครงสร้างของฟังก์ชัน เราเก็บข้อมูลจากเหตุการณ์ที่เข้ามาใหม่ในฟิลด์ที่เราสนใจ และแมปฟิลด์เหล่านั้นกับโครงสร้าง GCSEvent ที่เรากำหนดไว้ ดังนี้

...
public class ImageAnalysis implements BackgroundFunction<GCSEvent> {
    @Override
    public void accept(GCSEvent event, Context context) 
            throws IOException, InterruptedException,     
    ExecutionException {
...

    public static class GCSEvent {
        String bucket;
        String name;
    }

โปรดสังเกตลายเซ็น รวมถึงวิธีที่เราเรียกชื่อไฟล์และที่เก็บข้อมูลซึ่งเรียกใช้ Cloud Function

สำหรับการอ้างอิง เปย์โหลดเหตุการณ์มีลักษณะดังนี้

{
  "bucket":"uploaded-pictures",
  "contentType":"image/png",
  "crc32c":"efhgyA==",
  "etag":"CKqB956MmucCEAE=",
  "generation":"1579795336773802",
  "id":"uploaded-pictures/Screenshot.png/1579795336773802",
  "kind":"storage#object",
  "md5Hash":"PN8Hukfrt6C7IyhZ8d3gfQ==",
  "mediaLink":"https://www.googleapis.com/download/storage/v1/b/uploaded-pictures/o/Screenshot.png?generation=1579795336773802&alt=media",
  "metageneration":"1",
  "name":"Screenshot.png",
  "selfLink":"https://www.googleapis.com/storage/v1/b/uploaded-pictures/o/Screenshot.png",
  "size":"173557",
  "storageClass":"STANDARD",
  "timeCreated":"2020-01-23T16:02:16.773Z",
  "timeStorageClassUpdated":"2020-01-23T16:02:16.773Z",
  "updated":"2020-01-23T16:02:16.773Z"
}

เราเตรียมคำขอที่จะส่งผ่านไคลเอ็นต์ Vision ดังนี้

ImageSource imageSource = ImageSource.newBuilder()
    .setGcsImageUri("gs://" + bucketName + "/" + fileName)
    .build();

Image image = Image.newBuilder()
    .setSource(imageSource)
    .build();

Feature featureLabel = Feature.newBuilder()
    .setType(Type.LABEL_DETECTION)
    .build();
Feature featureImageProps = Feature.newBuilder()
    .setType(Type.IMAGE_PROPERTIES)
    .build();
Feature featureSafeSearch = Feature.newBuilder()
    .setType(Type.SAFE_SEARCH_DETECTION)
    .build();
    
AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
    .addFeatures(featureLabel)
    .addFeatures(featureImageProps)
    .addFeatures(featureSafeSearch)
    .setImage(image)
    .build();

เราขอแนะนำความสามารถหลัก 3 อย่างของ Vision API ดังนี้

  • การตรวจจับป้ายกำกับ: เพื่อให้ทราบสิ่งที่อยู่ในรูปภาพเหล่านั้น
  • คุณสมบัติของรูปภาพ: เพื่อแสดงแอตทริบิวต์ที่น่าสนใจของรูปภาพ (เราสนใจสีที่โดดเด่นของรูปภาพ)
  • ค้นหาปลอดภัย: เพื่อดูว่ารูปภาพนั้นปลอดภัยหรือไม่ (ไม่ควรมีเนื้อหาสำหรับผู้ใหญ่ / การแพทย์ / สำหรับผู้ใหญ่ / ความรุนแรง)

ในตอนนี้ เราสามารถเรียกใช้ Vision API ได้โดยทำดังนี้

...
logger.info("Calling the Vision API...");
BatchAnnotateImagesResponse result = 
                            vision.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = result.getResponsesList();
...

สำหรับการอ้างอิง การตอบสนองจาก Vision API จะมีลักษณะดังนี้

{
  "faceAnnotations": [],
  "landmarkAnnotations": [],
  "logoAnnotations": [],
  "labelAnnotations": [
    {
      "locations": [],
      "properties": [],
      "mid": "/m/01yrx",
      "locale": "",
      "description": "Cat",
      "score": 0.9959855675697327,
      "confidence": 0,
      "topicality": 0.9959855675697327,
      "boundingPoly": null
    },
    ✄ - - - ✄
  ],
  "textAnnotations": [],
  "localizedObjectAnnotations": [],
  "safeSearchAnnotation": {
    "adult": "VERY_UNLIKELY",
    "spoof": "UNLIKELY",
    "medical": "VERY_UNLIKELY",
    "violence": "VERY_UNLIKELY",
    "racy": "VERY_UNLIKELY",
    "adultConfidence": 0,
    "spoofConfidence": 0,
    "medicalConfidence": 0,
    "violenceConfidence": 0,
    "racyConfidence": 0,
    "nsfwConfidence": 0
  },
  "imagePropertiesAnnotation": {
    "dominantColors": {
      "colors": [
        {
          "color": {
            "red": 203,
            "green": 201,
            "blue": 201,
            "alpha": null
          },
          "score": 0.4175916016101837,
          "pixelFraction": 0.44456374645233154
        },
        ✄ - - - ✄
      ]
    }
  },
  "error": null,
  "cropHintsAnnotation": {
    "cropHints": [
      {
        "boundingPoly": {
          "vertices": [
            { "x": 0, "y": 118 },
            { "x": 1177, "y": 118 },
            { "x": 1177, "y": 783 },
            { "x": 0, "y": 783 }
          ],
          "normalizedVertices": []
        },
        "confidence": 0.41695669293403625,
        "importanceFraction": 1
      }
    ]
  },
  "fullTextAnnotation": null,
  "webDetection": null,
  "productSearchResults": null,
  "context": null
}

ถ้าไม่มีการส่งคืนข้อผิดพลาด เราสามารถดำเนินการต่อได้ ด้วยเหตุนี้เราจึงมีข้อผิดพลาดนี้หากบล็อก:

AnnotateImageResponse response = responses.get(0);
if (response.hasError()) {
     logger.info("Error: " + response.getError().getMessage());
     return;
}

เราจะทำป้ายกำกับของสิ่งต่างๆ หมวดหมู่ หรือธีมที่จำได้ในภาพ

List<String> labels = response.getLabelAnnotationsList().stream()
    .map(annotation -> annotation.getDescription())
    .collect(Collectors.toList());

logger.info("Annotations found:");
for (String label: labels) {
    logger.info("- " + label);
}

เราอยากทราบสีที่โดดเด่นของรูปภาพ

String mainColor = "#FFFFFF";
ImageProperties imgProps = response.getImagePropertiesAnnotation();
if (imgProps.hasDominantColors()) {
    DominantColorsAnnotation colorsAnn = 
                               imgProps.getDominantColors();
    ColorInfo colorInfo = colorsAnn.getColors(0);

    mainColor = rgbHex(
        colorInfo.getColor().getRed(), 
        colorInfo.getColor().getGreen(), 
        colorInfo.getColor().getBlue());

    logger.info("Color: " + mainColor);
}

นอกจากนี้เรายังใช้ฟังก์ชันยูทิลิตีเพื่อแปลงค่าสีแดง / เขียว / น้ำเงินเป็นรหัสสีเลขฐาน 16 ที่เราใช้ในสไตล์ชีต CSS ได้

ลองดูว่าภาพนั้นปลอดภัยไหม:

boolean isSafe = false;
if (response.hasSafeSearchAnnotation()) {
    SafeSearchAnnotation safeSearch = 
                      response.getSafeSearchAnnotation();

    isSafe = Stream.of(
        safeSearch.getAdult(), safeSearch.getMedical(), safeSearch.getRacy(),
        safeSearch.getSpoof(), safeSearch.getViolence())
    .allMatch( likelihood -> 
        likelihood != Likelihood.LIKELY && likelihood != Likelihood.VERY_LIKELY
    );

    logger.info("Safe? " + isSafe);
}

เรากำลังตรวจสอบแอตทริบิวต์สำหรับผู้ใหญ่ / การปลอมแปลง / การแพทย์ / ความรุนแรง / สำหรับผู้ใหญ่เพื่อดูว่ามีลักษณะมีแนวโน้มหรือมีแนวโน้มมากหรือไม่

หากผลของฟีเจอร์ค้นหาปลอดภัยถูกต้อง เราสามารถจัดเก็บข้อมูลเมตาใน Firestore ได้ ดังนี้

if (isSafe) {
    FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
    Firestore pictureStore = firestoreOptions.getService();

    DocumentReference doc = pictureStore.collection("pictures").document(fileName);

    Map<String, Object> data = new HashMap<>();
    data.put("labels", labels);
    data.put("color", mainColor);
    data.put("created", new Date());

    ApiFuture<WriteResult> writeResult = doc.set(data, SetOptions.merge());

    logger.info("Picture metadata saved in Firestore at " + writeResult.get().getUpdateTime());
}

12. ทำให้ฟังก์ชันใช้งานได้

ได้เวลาทำให้ฟังก์ชันใช้งานได้แล้ว

604f47aa11fbf8e.png

กด DEPLOY และเวอร์ชันใหม่จะถูกทำให้ใช้งานได้ โดยคุณจะเห็นความคืบหน้า:

13da63f23e4dbbdd.png

13. ทดสอบฟังก์ชันอีกครั้ง

เมื่อทำให้ฟังก์ชันใช้งานได้เรียบร้อยแล้ว คุณจะโพสต์รูปภาพไปยัง Cloud Storage ดูว่าฟังก์ชันของเราเรียกใช้หรือไม่ Vision API แสดงผลหรือไม่ และข้อมูลที่เก็บข้อมูลเมตาอยู่ใน Firestore หรือไม่

กลับไปที่ Cloud Storage แล้วคลิกที่เก็บข้อมูลที่เราสร้างไว้เมื่อเริ่มต้นห้องทดลอง

d44c1584122311c7.png

เมื่ออยู่ในหน้ารายละเอียดที่เก็บข้อมูล ให้คลิกปุ่ม Upload files เพื่ออัปโหลดรูปภาพ

26bb31d35fb6aa3d.png

จาก "แฮมเบอร์เกอร์" (⋮) เพื่อไปที่การสำรวจ Logging > Logs

ในตัวเลือก Log Fields ให้เลือก Cloud Function เพื่อดูบันทึกสำหรับฟังก์ชันของคุณโดยเฉพาะ เลื่อนลงตามฟิลด์บันทึกและคุณสามารถเลือกฟังก์ชันที่ต้องการเพื่อดูบันทึกที่เกี่ยวข้องกับฟังก์ชันอย่างละเอียด เลือกฟังก์ชัน picture-uploaded

b651dca7e25d5b11.png

อันที่จริง ในรายการบันทึก เราจะเห็นว่ามีการเรียกใช้ฟังก์ชันของเรา ดังนี้

d22a7f24954e4f63.png

บันทึกจะระบุจุดเริ่มต้นและสิ้นสุดของการดำเนินการของฟังก์ชัน และระหว่างนั้น เราจะเห็นบันทึกที่เราใส่ไว้ในฟังก์ชันโดยใช้คำสั่งconsole.log() เราพบว่า

  • รายละเอียดของเหตุการณ์ที่ทริกเกอร์ฟังก์ชันของเรา
  • ผลลัพธ์ดิบจากการเรียก Vision API
  • ป้ายกำกับที่พบในภาพที่เราอัปโหลด
  • ข้อมูลสีที่โดดเด่น
  • รูปภาพปลอดภัยที่จะแสดงหรือไม่
  • และท้ายที่สุดแล้วระบบได้จัดเก็บข้อมูลเมตาเกี่ยวกับรูปภาพไว้ใน Firestore

9ff7956a215c15da.png

อีกครั้งจาก "แฮมเบอร์เกอร์" (src) ไปที่ส่วน Firestore ในส่วนย่อย Data (แสดงโดยค่าเริ่มต้น) คุณจะเห็นคอลเล็กชัน pictures ที่มีเอกสารใหม่เพิ่มเข้ามา ซึ่งจะสอดคล้องกับภาพที่คุณเพิ่งอัปโหลด

a6137ab9687da370.png

14. ล้างข้อมูล (ไม่บังคับ)

หากไม่ต้องการใช้ห้องทดลองอื่นๆ ในชุดนี้ต่อ คุณสามารถล้างทรัพยากรเพื่อประหยัดต้นทุนและเป็นพลเมืองระบบคลาวด์ที่ดีโดยรวม คุณสามารถล้างทรัพยากรแต่ละรายการได้ดังนี้

ลบที่เก็บข้อมูล

gsutil rb gs://${BUCKET_PICTURES}

ลบฟังก์ชัน

gcloud functions delete picture-uploaded --region europe-west1 -q

ลบคอลเล็กชัน Firestore โดยเลือก "ลบคอลเล็กชัน" ออกจากคอลเล็กชัน ดังนี้

410b551c3264f70a.png

หรือจะลบทั้งโปรเจ็กต์ก็ได้ โดยทำดังนี้

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

15. ยินดีด้วย

ยินดีด้วย คุณใช้งานบริการจัดการคีย์แรกของโปรเจ็กต์เรียบร้อยแล้ว

หัวข้อที่ครอบคลุม

  • Cloud Storage
  • Cloud Functions
  • Cloud Vision API
  • Cloud Firestore

ขั้นตอนถัดไป