תמונה יומית: שיעור 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

  • Project name הוא השם המוצג של המשתתפים בפרויקט. זו מחרוזת תווים שלא משמשת את Google APIs. אפשר לעדכן אותו בכל שלב.
  • Project ID חייב להיות ייחודי בכל הפרויקטים ב-Google Cloud ואי אפשר לשנות אותו (אי אפשר לשנות אותו אחרי שמגדירים אותו). מסוף Cloud יוצר מחרוזת ייחודית באופן אוטומטי; בדרך כלל לא מעניין אותך מה זה. ברוב ה-Codelabs תצטרכו להפנות אל מזהה הפרויקט (בדרך כלל הוא מזוהה כ-PROJECT_ID). אם המזהה שנוצר לא מוצא חן בעיניך, יש לך אפשרות ליצור מזהה אקראי אחר. לחלופין, אפשר לנסות תבנית משלך ולבדוק אם היא זמינה. לא ניתן לשנות אותו אחרי השלב הזה, והוא יישאר למשך הפרויקט.
  • לידיעתך, יש ערך שלישי – Project Number (מספר פרויקט), שחלק מממשקי ה-API משתמשים בו. מידע נוסף על כל שלושת הערכים האלה זמין במסמכי התיעוד.
  1. בשלב הבא צריך להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבים או בממשקי API של Cloud. מעבר ב-Codelab הזה לא אמור לעלות הרבה, אם בכלל. כדי להשבית את המשאבים ולא לצבור חיובים מעבר למדריך הזה, אתם יכולים למחוק את המשאבים שיצרתם או למחוק את הפרויקט כולו. משתמשים חדשים ב-Google Cloud זכאים להצטרף לתוכנית תקופת ניסיון בחינם בשווי 1,200 ש"ח.

הפעלת Cloud Shell

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

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

55efc1aaa7a4d3ad.png

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

7ffe5cbb04455448.png

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

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

בשיעור ה-Lab הזה תשתמשו ב-Cloud Functions וב-Vision API, אבל קודם צריך להפעיל אותם ב-Cloud Console או באמצעות gcloud.

כדי להפעיל את Vision API במסוף Cloud, מחפשים את Cloud Vision API בסרגל החיפוש:

cf48b1747ba6a6fb.png

בשלב הזה מגיעים לדף Cloud Vision API:

ba4af419e6086fbb.png

לוחצים על הלחצן ENABLE.

לחלופין, אפשר גם להפעיל אותו באמצעות Cloud Shell באמצעות כלי שורת הפקודה של Google Cloud.

בתוך 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 או מסביבת הפיתוח המקומית.

מתוך "המבורגר" (\t) בתפריט, צריך לעבור לדף Storage.

1930e055d138150a.png

מתן שם לקטגוריה

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

34147939358517f8.png

לוחצים על CONTINUE.

בחירת מיקום

197817f20be07678.png

יוצרים קטגוריה מרובת אזורים באזור הרצוי (כאן Europe).

לוחצים על CONTINUE.

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

53cd91441c8caf0e.png

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

לוחצים על CONTINUE.

הגדרה של בקרת גישה

8c2b3b459d934a51.png

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

צריך לבחור באפשרות Uniform של בקרת הגישה.

לוחצים על CONTINUE.

הגדרת הגנה/הצפנה

d931c24c3e705a68.png

אני רוצה להשאיר את ברירת המחדל (Google-managed key), כי לא ייעשה שימוש במפתחות ההצפנה שלכם).

לוחצים על CREATE כדי לסיים את יצירת הקטגוריה.

הוספת כל המשתמשים כמציג של נפח אחסון

מעבר לכרטיסייה Permissions:

d0ecfdcff730ea51.png

מוסיפים לקטגוריה חבר allUsers עם התפקיד Storage > Storage Object Viewer:

e9f25ec1ea0b6cc6.png

לוחצים על SAVE.

5. יצירת הקטגוריה (gsutil)

אפשר גם להשתמש בכלי שורת הפקודה gsutil ב-Cloud Shell כדי ליצור קטגוריות.

ב-Cloud Shell, מגדירים משתנה לשם הייחודי של הקטגוריה. השדה GOOGLE_CLOUD_PROJECT כבר מוגדר ב-Cloud Shell למזהה הפרויקט הייחודי שלכם. אפשר להוסיף אותו לשם הקטגוריה.

לדוגמה:

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. בדיקת הגישה הציבורית לקטגוריה

בחזרה לדפדפן האחסון, הקטגוריה שלך תופיע ברשימה עם הכיתוב 'Public' (ציבורי) גישה (כולל סימן אזהרה שמזכיר שלכל אחד יש גישה לתוכן של הקטגוריה).

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 (כלומר, Web API),
  • פונקציות ברקע שאירוע מסוים יכול להפעיל.

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

d9a12fcf58f4813c.png

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

b30c8859b07dc4cb.png

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

cb15a1f4c7a1ca5f.png

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

c1933777fac32c6a.png

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

83d757e6c38e10.png

אחרי לחיצה על Next, אפשר לכוונן את זמן הריצה, קוד המקור ונקודת הכניסה.

צריך לשמור את Inline editor לפונקציה הזו:

b6646ec646082b32.png

בוחרים באחד מסביבות זמני הריצה של Java, לדוגמה Java 11:

f85b8a6f951f47a7.png

קוד המקור מורכב מקובץ Java ומקובץ Maven ל-pom.xml שמספק מטא-נתונים ויחסי תלות שונים.

משאירים את קטע הקוד שמוגדר כברירת מחדל: הוא מתעד את שם הקובץ של התמונה שהועלתה:

9b7b9801b42f6ca6.png

בשלב הזה, כדאי לשמור את שם הפונקציה להפעלה ב-Example, למטרות בדיקה.

לוחצים על Deploy כדי ליצור את הפונקציה ולפרוס אותה. אחרי שהפריסה הצליחה, אמור להופיע סימן וי מוקף בעיגול ירוק ברשימת הפונקציות:

3732fdf409eefd1a.png

8. בדיקת הפונקציה

בשלב הזה, בודקים שהפונקציה מגיבה לאירועי אחסון.

מתוך "המבורגר" (\t) בתפריט, צריך לנווט חזרה לדף Storage.

לוחצים על קטגוריית התמונות, ואז על Upload files כדי להעלות תמונה.

21767ec3cb8b18de.png

כדי לעבור לדף Logging > Logs Explorer, צריך לעבור שוב במסוף Cloud.

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

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

e8ba7d39c36df36c.png

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

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

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

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

ecb8b95e3c791272.png

לוחצים על Create. יצירת האינדקס עשויה להימשך מספר דקות.

10. עדכון הפונקציה

צריך לחזור לדף Functions כדי לעדכן את הפונקציה כדי להפעיל את Vision API לצורך ניתוח התמונות שלנו ואחסון המטא-נתונים ב-Firestore.

מתוך "המבורגר" (\t) בתפריט, עוברים לקטע 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 Functions.

לידיעתך, כך נראה המטען הייעודי (payload) של האירוע:

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

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

d44c1584122311c7.png

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

26bb31d35fb6aa3d.png

מתוך "המבורגר" (\t) בתפריט, מנווטים לחוקר של Logging > Logs.

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

b651dca7e25d5b11.png

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

d22a7f24954e4f63.png

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

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

9ff7956a215c15da.png

שוב מהמבורגר (\t) בתפריט, עוברים לקטע 'Firestore'. בקטע המשנה Data (מוצג כברירת מחדל), אמור להופיע האוסף pictures עם מסמך חדש שנוסף לתמונה שהעלית:

a6137ab9687da370.png

14. הסרת המשאבים (אופציונלי)

אם אתם לא מתכוונים להמשיך עם שיעורי ה-Lab האחרים בסדרה, תוכלו לפנות משאבים כדי לחסוך בעלויות ולהיות אזרחי הענן באופן כללי. אפשר למחוק משאבים בנפרד באופן הבא.

מוחקים את הקטגוריה:

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

השלבים הבאים