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

1. ภาพรวม

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

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 ของ Cloud การทำตาม Codelab นี้ไม่ควรมีค่าใช้จ่ายมากนัก หรืออาจไม่มีเลย หากต้องการปิดทรัพยากรเพื่อไม่ให้มีการเรียกเก็บเงินนอกเหนือจากบทแนะนำนี้ คุณสามารถลบทรัพยากรที่สร้างขึ้นหรือลบทั้งโปรเจ็กต์ได้ ผู้ใช้ Google Cloud รายใหม่มีสิทธิ์เข้าร่วมโปรแกรมช่วงทดลองใช้ฟรีมูลค่า$300 USD

เริ่มต้น Cloud Shell

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

จาก Google Cloud Console ให้คลิกไอคอน 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. สร้างที่เก็บข้อมูล (คอนโซล)

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

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

1930e055d138150a.png

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

คลิกปุ่ม CREATE BUCKET

34147939358517f8.png

คลิก CONTINUE

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

197817f20be07678.png

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

คลิก CONTINUE

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

53cd91441c8caf0e.png

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

คลิก CONTINUE

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

8c2b3b459d934a51.png

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

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

คลิก CONTINUE

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

d931c24c3e705a68.png

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

คลิก CREATE เพื่อสร้าง Bucket ให้เสร็จสมบูรณ์

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

ไปที่แท็บ Permissions โดยทำดังนี้

d0ecfdcff730ea51.png

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

e9f25ec1ea0b6cc6.png

คลิก SAVE

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

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

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

เช่น

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

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

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

ตรวจสอบว่าสิทธิ์เข้าถึงระดับ Bucket เหมือนกัน

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

ทำให้ Bucket เป็นแบบสาธารณะ

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

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

a98ed4ba17873e40.png

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

6. ทดสอบการเข้าถึงแบบสาธารณะใน Bucket

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

89e7a4d2c80a0319.png

ตอนนี้ Bucket พร้อมรับรูปภาพแล้ว

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

131387f12d3eb2d3.png

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

e87584471a6e9c6d.png

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

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

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

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

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

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

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

9d29e8c026a7a53f.png

คลิก Create function

เลือกชื่อ (เช่น picture-uploaded) และภูมิภาค (อย่าลืมเลือกภูมิภาคให้สอดคล้องกับตัวเลือกภูมิภาคสำหรับ Bucket)

4bb222633e6f278.png

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

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

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

d9a12fcf58f4813c.png

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

b30c8859b07dc4cb.png

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

cb15a1f4c7a1ca5f.png

คลิก Select เพื่อเลือก Bucket ที่คุณสร้างไว้ก่อนหน้านี้ แล้วคลิก Save

c1933777fac32c6a.png

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

83d757e6c38e10.png

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

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

b6646ec646082b32.png

เลือกรันไทม์ Java อย่างใดอย่างหนึ่ง เช่น Java 11

f85b8a6f951f47a7.png

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

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

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 ที่ดำเนินการบนระบบคลาวด์แบบ Serverless ที่มีการจัดการครบวงจรและรวดเร็ว เตรียมฐานข้อมูลโดยไปที่ส่วน 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 (สตริง): ชื่อไฟล์ของรูปภาพที่อัปโหลด ซึ่งเป็นคีย์ของเอกสารด้วย
  • ป้ายกำกับ (อาร์เรย์ของสตริง): ป้ายกำกับของรายการที่ Vision API รู้จัก
  • color (สตริง): รหัสสีเลขฐานสิบหกของสีหลัก (เช่น #ab12ef)
  • created (วันที่): การประทับเวลาเมื่อจัดเก็บข้อมูลเมตาของรูปภาพนี้
  • thumbnail (บูลีน): ฟิลด์ที่ไม่บังคับซึ่งจะแสดงและมีค่าเป็นจริงหากมีการสร้างภาพขนาดย่อสำหรับรูปภาพนี้

เนื่องจากเราจะค้นหาใน 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 ซึ่งแสดงรายการการอ้างอิงของฟังก์ชัน 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>

ตอนนี้เมื่ออัปเดตการขึ้นต่อกันแล้ว คุณจะทำงานกับโค้ดของฟังก์ชันได้โดยการอัปเดตไฟล์ 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. สำรวจฟังก์ชัน

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

ก่อนอื่น เราจะรวมการอ้างอิงที่เฉพาะเจาะจงในไฟล์ pom.xml ของ Maven Google Java Client Libraries เผยแพร่ 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;
    }

สังเกตลายเซ็น รวมถึงวิธีที่เราดึงชื่อไฟล์และ Bucket ที่ทริกเกอร์ 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
}

หากไม่มีข้อผิดพลาดแสดงขึ้น เราจะดำเนินการต่อได้ จึงเป็นเหตุผลที่เรามีบล็อก if นี้

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);
}

นอกจากนี้ เรายังใช้ฟังก์ชันยูทิลิตีเพื่อแปลงค่าสีแดง / เขียว / น้ำเงินเป็นรหัสสีเลขฐานสิบหกที่เราใช้ในสไตล์ชีต 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 แล้วคลิกที่ Bucket ที่เราสร้างไว้ตอนต้นของแล็บ

d44c1584122311c7.png

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

26bb31d35fb6aa3d.png

จากเมนู "แฮมเบอร์เกอร์" (☰) ให้ไปที่ Logging > Logs Explorer

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

b651dca7e25d5b11.png

และในรายการบันทึก ฉันเห็นว่าฟังก์ชันของเราถูกเรียกใช้แล้ว

d22a7f24954e4f63.png

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

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

9ff7956a215c15da.png

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

a6137ab9687da370.png

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

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

ลบ Bucket

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

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