תמונה יומית: שיעור Lab 1 – אחסון וניתוח של תמונות (Java)

1. סקירה כללית

בשיעור ה-Lab הראשון, תעלו תמונות לקטגוריה. הפעולה הזו תיצור אירוע של יצירת קובץ, שיטופל על ידי פונקציה. הפונקציה תבצע קריאה ל-Vision API כדי לבצע ניתוח תמונות ולשמור את התוצאות במאגר נתונים.

d650ca5386ea71ad.png

מה תלמדו

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

‫2. הגדרה ודרישות

הגדרת סביבה בקצב אישי

  1. נכנסים ל-מסוף Google Cloud ויוצרים פרויקט חדש או משתמשים בפרויקט קיים. אם עדיין אין לכם חשבון Gmail או Google Workspace, אתם צריכים ליצור חשבון.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • שם הפרויקט הוא השם המוצג של הפרויקט הזה למשתתפים. זו מחרוזת תווים שלא נמצאת בשימוש ב-Google APIs. אפשר לעדכן את המיקום הזה בכל שלב.
  • מזהה הפרויקט חייב להיות ייחודי בכל הפרויקטים ב-Google Cloud, והוא קבוע (אי אפשר לשנות אותו אחרי שמגדירים אותו). מסוף Cloud יוצר באופן אוטומטי מחרוזת ייחודית, ובדרך כלל לא צריך לדעת מה היא. ברוב ה-Codelabs, תצטרכו להפנות למזהה הפרויקט (בדרך כלל הוא מסומן כ-PROJECT_ID). אם אתם לא אוהבים את המזהה שנוצר, אתם יכולים ליצור מזהה אקראי אחר. אפשר גם לנסות שם משתמש משלכם ולבדוק אם הוא זמין. אי אפשר לשנות את ההגדרה הזו אחרי השלב הזה, והיא תישאר כזו למשך הפרויקט.
  • לידיעתכם, יש ערך שלישי, מספר פרויקט, שחלק מממשקי ה-API משתמשים בו. במאמרי העזרה מפורט מידע נוסף על שלושת הערכים האלה.
  1. בשלב הבא, תצטרכו להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבי Cloud או בממשקי API של Cloud. העלות של התרגול הזה לא אמורה להיות גבוהה, ואולי אפילו לא תצטרכו לשלם בכלל. כדי להשבית את המשאבים ולא לחייב אתכם מעבר למדריך הזה, אתם יכולים למחוק את המשאבים שיצרתם או למחוק את כל הפרויקט. משתמשים חדשים ב-Google Cloud זכאים לתוכנית תקופת ניסיון בחינם בשווי 300$.

מפעילים את Cloud Shell

אפשר להפעיל את Google Cloud מרחוק מהמחשב הנייד, אבל ב-codelab הזה תשתמשו ב-Google Cloud Shell, סביבת שורת פקודה שפועלת בענן.

ב-מסוף Google Cloud, לוחצים על סמל Cloud Shell בסרגל הכלים שבפינה הימנית העליונה:

55efc1aaa7a4d3ad.png

יחלפו כמה רגעים עד שההקצאה והחיבור לסביבת העבודה יושלמו. בסיום התהליך, אמור להופיע משהו כזה:

7ffe5cbb04455448.png

המכונה הווירטואלית הזו כוללת את כל הכלים שדרושים למפתחים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר מאוד את הביצועים והאימות ברשת. אפשר לבצע את כל העבודה ב-codelab הזה בדפדפן. לא צריך להתקין שום דבר.

‫3. הפעלת ממשקי ה-API

בשיעור ה-Lab הזה תשתמשו ב-Cloud Functions וב-Vision API, אבל קודם צריך להפעיל אותם במסוף Cloud או באמצעות 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

Name your bucket (שם הקטגוריה)

לוחצים על הלחצן CREATE BUCKET.

34147939358517f8.png

לוחצים על CONTINUE.

בחירת מיקום

197817f20be07678.png

יוצרים קטגוריה במספר אזורים באזור הרצוי (בדוגמה הזו Europe).

לוחצים על CONTINUE.

בחירת סוג האחסון (storage class) שמוגדר כברירת מחדל

53cd91441c8caf0e.png

בוחרים את Standard סוג האחסון (storage class) של הנתונים.

לוחצים על 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

יש שני סוגים של פונקציות:

  • פונקציות HTTP שאפשר להפעיל באמצעות כתובת URL (כלומר, API לאינטרנט),
  • פונקציות ברקע שאפשר להפעיל באמצעות אירוע מסוים.

רוצים ליצור פונקציית רקע שמופעלת כשמעלים קובץ חדש לקטגוריית Cloud Storage:

d9a12fcf58f4813c.png

אתם מתעניינים בסוג האירוע Finalize/Create, שהוא האירוע שמופעל כשקובץ נוצר או מתעדכן בדלי:

b30c8859b07dc4cb.png

בוחרים את הקטגוריה שנוצרה קודם, כדי להגדיר ש-Cloud Functions יקבל התראה כשקובץ נוצר או מתעדכן בקטגוריה הזו:

cb15a1f4c7a1ca5f.png

לוחצים על Select כדי לבחור את הקטגוריה שיצרתם קודם, ואז על Save.

c1933777fac32c6a.png

לפני שלוחצים על Next, אפשר להרחיב ולשנות את ברירות המחדל (256MB זיכרון) בקטע Runtime, build, connections and security settings ולעדכן ל-1GB.

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 לדף Logging > Logs Explorer.

בבורר Log Fields, בוחרים באפשרות Cloud Function כדי לראות את היומנים שמוקדשים לפונקציות שלכם. גוללים למטה דרך Log Fields (שדות יומן) ואפשר אפילו לבחור פונקציה ספציפית כדי לקבל תצוגה מפורטת יותר של היומנים שקשורים לפונקציות. בוחרים בפונקציה picture-uploaded.

אמורים להופיע ביומן פריטים שקשורים ליצירת הפונקציה, לזמני ההתחלה והסיום של הפונקציה ולהצהרת היומן בפועל:

e8ba7d39c36df36c.png

ההצהרה ביומן היא: Processing file: pic-a-daily-architecture-events.png, כלומר האירוע שקשור ליצירה ולאחסון של התמונה הזו אכן הופעל כמצופה.

9. הכנת מסד הנתונים

תאחסנו מידע על התמונה שמתקבל מ-Vision API במסד הנתונים Cloud Firestore, שהוא מסד נתונים מהיר, מנוהל, מבוסס-ענן, בלי שרת (serverless) ולא יחסי (NoSQL). כדי להכין את מסד הנתונים, עוברים לקטע Firestore ב-Cloud Console:

9e4708d2257de058.png

יש שתי אפשרויות: 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 (מחרוזת): קוד הצבע ההקסדצימלי של הצבע הדומיננטי (למשל #ab12ef)
  • created (תאריך): חותמת הזמן של מועד שמירת המטא-נתונים של התמונה
  • thumbnail (בוליאני): שדה אופציונלי שיופיע ויקבל את הערך true אם נוצרה תמונה ממוזערת לתמונה הזו

כדי שנוכל לחפש ב-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. מעדכנים את הקוד כדי להוסיף את התלות במאגר Maven של Cloud Vision API:

<?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 מפרסמות Bill-of-Materials(BOM), כדי למנוע התנגשויות בין תלויות. כשמשתמשים בו, לא צריך לציין גרסה לספריות הלקוח הנפרדות של 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
}

אם לא מוחזרת שגיאה, אפשר להמשיך, ולכן יש לנו את בלוק ה-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 ולוחצים על הדלי שיצרנו בתחילת המעבדה:

d44c1584122311c7.png

בדף הפרטים של הקטגוריה, לוחצים על הלחצן Upload files כדי להעלות תמונה.

26bb31d35fb6aa3d.png

בתפריט ההמבורגר (☰), עוברים אל Logging > Logs Explorer.

בבורר Log Fields, בוחרים באפשרות Cloud Function כדי לראות את היומנים שמוקדשים לפונקציות שלכם. גוללים למטה דרך Log Fields (שדות יומן) ואפשר אפילו לבחור פונקציה ספציפית כדי לקבל תצוגה מפורטת יותר של היומנים שקשורים לפונקציות. בוחרים בפונקציה picture-uploaded.

b651dca7e25d5b11.png

ואכן, ברשימת היומנים אפשר לראות שהפונקציה שלנו הופעלה:

d22a7f24954e4f63.png

ביומנים מצוינים ההתחלה והסיום של הפעלת הפונקציה. בין לבין, אפשר לראות את היומנים שהוספנו לפונקציה באמצעות ההצהרות console.log(). הנתונים שמוצגים הם:

  • פרטי האירוע שמפעיל את הפונקציה,
  • התוצאות הגולמיות מקריאה ל-Vision API,
  • התוויות שנמצאו בתמונה שהעלינו,
  • מידע על הצבעים הדומיננטיים,
  • אם התמונה בטוחה לצפייה,
  • בסופו של דבר, המטא-נתונים האלה לגבי התמונה מאוחסנים ב-Firestore.

9ff7956a215c15da.png

שוב, בתפריט ההמבורגר (☰), עוברים לקטע 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

השלבים הבאים