الصورة اليومية: التمرين المعملي 1: تخزين الصور وتحليلها (Java)

1. نظرة عامة

في تجربة الترميز الأولى، ستحمّل صورًا في حزمة. سيؤدي ذلك إلى إنشاء حدث إنشاء ملف ستتعامل معه إحدى الدوال. ستُجري الدالة طلبًا إلى Vision API لإجراء تحليل الصور وحفظ النتائج في مخزن بيانات.

d650ca5386ea71ad.png

أهداف الدورة التعليمية

  • Cloud Storage
  • وظائف السحابة الإلكترونية
  • 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 تلقائيًا سلسلة فريدة، ولا يهمّك عادةً ما هي. في معظم دروس البرمجة، عليك الرجوع إلى رقم تعريف المشروع (يتم تحديده عادةً على أنّه PROJECT_ID). إذا لم يعجبك رقم التعريف الذي تم إنشاؤه، يمكنك إنشاء رقم تعريف عشوائي آخر. يمكنك بدلاً من ذلك تجربة اسم مستخدم من اختيارك لمعرفة ما إذا كان متاحًا. لا يمكن تغيير هذا الخيار بعد هذه الخطوة وسيظل ساريًا طوال مدة المشروع.
  • للعلم، هناك قيمة ثالثة، وهي رقم المشروع الذي تستخدمه بعض واجهات برمجة التطبيقات. يمكنك الاطّلاع على مزيد من المعلومات عن كل هذه القيم الثلاث في المستندات.
  1. بعد ذلك، عليك تفعيل الفوترة في Cloud Console لاستخدام موارد/واجهات برمجة تطبيقات Cloud. لن تكلفك تجربة هذا الدرس التطبيقي حول الترميز الكثير من المال، إن لم تكلفك شيئًا على الإطلاق. لإيقاف الموارد كي لا يتم تحصيل رسوم منك بعد هذا البرنامج التعليمي، يمكنك حذف الموارد التي أنشأتها أو حذف المشروع بأكمله. يمكن لمستخدمي Google Cloud الجدد الاستفادة من برنامج الفترة التجريبية المجانية بقيمة 300 دولار أمريكي.

بدء Cloud Shell

على الرغم من إمكانية تشغيل Google Cloud عن بُعد من الكمبيوتر المحمول، ستستخدم في هذا الدرس العملي Google Cloud Shell، وهي بيئة سطر أوامر تعمل في السحابة الإلكترونية.

من Google Cloud Console، انقر على رمز Cloud Shell في شريط الأدوات العلوي على يسار الصفحة:

55efc1aaa7a4d3ad.png

لن يستغرق توفير البيئة والاتصال بها سوى بضع لحظات. عند الانتهاء، من المفترض أن يظهر لك ما يلي:

7ffe5cbb04455448.png

يتم تحميل هذه الآلة الافتراضية مزوّدة بكل أدوات التطوير التي ستحتاج إليها. توفّر هذه الخدمة دليلًا منزليًا ثابتًا بسعة 5 غيغابايت، وتعمل على Google Cloud، ما يؤدي إلى تحسين أداء الشبكة والمصادقة بشكل كبير. يمكن إكمال جميع المهام في هذا الدرس العملي ضمن المتصفّح. لست بحاجة إلى تثبيت أي تطبيق.

3- تفعيل واجهات برمجة التطبيقات

في هذا المختبر، ستستخدم 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، اضبط متغيّرًا لاسم الحزمة الفريد. يتم ضبط 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. اختبار إمكانية الوصول العلني إلى الحزمة

بالرجوع إلى متصفّح مساحة التخزين، سترى الحزمة في القائمة، مع إذن الوصول "متاح للجميع" (بما في ذلك علامة تحذير تذكّرك بأنّ بإمكان أي مستخدم الوصول إلى محتوى هذه الحزمة).

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 Console. عند الانتقال إلى هذه الصفحة، سيتم تفعيل خدمة Cloud Functions تلقائيًا.

9d29e8c026a7a53f.png

انقر على Create function.

اختَر اسمًا (مثلاً picture-uploaded) والمنطقة (تذكَّر أن تكون متسقًا مع اختيار المنطقة للحزمة):

4bb222633e6f278.png

هناك نوعان من الدوال:

  • دوال HTTP التي يمكن استدعاؤها من خلال عنوان URL (أي واجهة برمجة تطبيقات على الويب)
  • وظائف الخلفية التي يمكن أن يتم تشغيلها بواسطة حدث معيّن

تريد إنشاء دالة تعمل في الخلفية يتم تشغيلها عند تحميل ملف جديد إلى الحزمة Cloud Storage:

d9a12fcf58f4813c.png

أنت مهتم بنوع الحدث Finalize/Create، وهو الحدث الذي يتم تشغيله عند إنشاء ملف أو تعديله في الحزمة:

b30c8859b07dc4cb.png

اختَر الحزمة التي تم إنشاؤها من قبل لإعلام Cloud Functions عند إنشاء ملف أو تعديله في هذه الحزمة المحدّدة:

cb15a1f4c7a1ca5f.png

انقر على Select لاختيار الحزمة التي أنشأتها سابقًا، ثم انقر على Save.

c1933777fac32c6a.png

قبل النقر على "التالي"، يمكنك توسيع الإعدادات التلقائية (ذاكرة بسعة 256 ميغابايت) وتعديلها ضمن إعدادات بيئة التشغيل والإنشاء والاتصالات والأمان وتعديلها إلى 1 غيغابايت.

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

انتقِل مرة أخرى داخل وحدة تحكّم السحابة الإلكترونية إلى صفحة 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

يتوفّر خياران: 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 (قيمة منطقية): حقل اختياري سيظهر وتكون قيمته صحيحة إذا تم إنشاء صورة مصغّرة لهذه الصورة

بما أنّنا سنبحث في 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. عدِّل الرمز البرمجي لإضافة اعتمادية 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. استكشاف الدالة

لنلقِ نظرة عن كثب على الأجزاء المختلفة المثيرة للاهتمام.

أولاً، سنضمّن التبعيات المحدّدة في ملف Maven pom.xml. تنشر Google Java Client Libraries 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 للاطّلاع على السجلات المخصّصة لوظائفك. انتقِل للأسفل خلال "حقول السجلّ"، ويمكنك حتى اختيار دالة معيّنة للحصول على عرض أكثر تفصيلاً لسجلات الدوال ذات الصلة. اختَر الدالة 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 من خلال اختيار "حذف المجموعة" (Delete collection) من المجموعة:

410b551c3264f70a.png

بدلاً من ذلك، يمكنك حذف المشروع بأكمله باتّباع الخطوات التالية:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

15. تهانينا!

تهانينا! لقد نفّذت خدمة إدارة مفاتيح التشفير الأولى للمشروع بنجاح.

المواضيع التي تناولناها

  • Cloud Storage
  • وظائف السحابة الإلكترونية
  • Cloud Vision API
  • Cloud Firestore

الخطوات التالية