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

1. نظرة عامة

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

d650ca5386ea71ad.png

المُعطيات

  • تخزين في السحابة الإلكترونية
  • وظائف السحابة الإلكترونية
  • واجهة برمجة تطبيقات Cloud Vision
  • 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.

تفعيل وظائف السحابة الإلكترونية أيضًا:

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 لإكمال عملية إنشاء الحزمة.

إضافة كل المستخدمين كعارض لمساحة التخزين

الانتقال إلى علامة التبويب "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. من خلال الانتقال إليه، سيتم تفعيل خدمة 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

يمكنك الانتقال مرة أخرى ضمن Cloud Console للانتقال إلى صفحة "Logging > Logs Explorer".

في أداة اختيار Log Fields، اختَر Cloud Function للاطّلاع على السجلات المخصَّصة لدوالك. قم بالتمرير لأسفل عبر حقول السجل ويمكنك حتى تحديد دالة معينة للحصول على عرض أكثر دقة للسجلات ذات الصلة بالدوال. اختَر الدالة picture-uploaded.

يُفترض أن ترى عناصر السجل التي تشير إلى إنشاء الدالة، ووقت بدء وانتهاء الدالة، وبيان السجل الفعلي:

e8ba7d39c36df36c.png

نص بيان السجل لدينا هو: Processing file: pic-a-daily-architecture-events.png، مما يعني أن الحدث المرتبط بإنشاء هذه الصورة وتخزينها قد شغّلها بالفعل كما هو متوقع.

9. إعداد قاعدة البيانات

ستخزن معلومات حول الصورة المقدّمة من Vision API في قاعدة بيانات Cloud Firestore، وهي قاعدة بيانات مستندات NoSQL سريعة ومُدارة بالكامل وبدون خادم. يمكنك إعداد قاعدة البيانات بالانتقال إلى القسم Firestore في Cloud Console:

9e4708d2257de058.png

يتوفّر خياران: Native mode أو Datastore mode. استخدم الوضع الأصلي، الذي يوفر ميزات إضافية مثل إمكانية الاستخدام بلا اتصال بالإنترنت والمزامنة في الوقت الفعلي.

انقر على SELECT NATIVE MODE.

9449ace8cc84de43.png

اختَر منطقة متعددة (هنا في أوروبا، ولكن يُفضَّل أن تكون المنطقة نفسها على الأقل تكون الوظيفة وحزمة التخزين).

انقر على الزر CREATE DATABASE.

بعد إنشاء قاعدة البيانات، من المفترض أن يظهر لك ما يلي:

56265949a124819e.png

يمكنك إنشاء مجموعة جديدة بالنقر على الزر + START COLLECTION.

تسمية مجموعة "pictures".

75806ee24c4e13a7.png

لست بحاجة إلى إنشاء مستند. ستضيفها آليًا حيث يتم تخزين الصور الجديدة في Cloud Storage وتحليلها من خلال واجهة برمجة تطبيقات Vision.

انقر على Save.

ينشئ Firestore أول مستند افتراضي في المجموعة التي تم إنشاؤها حديثًا، ويمكنك حذف هذا المستند بأمان لأنه لا يحتوي على أي معلومات مفيدة:

5c2f1e17ea47f48f.png

ستحتوي المستندات التي سيتم إنشاؤها آليًا في مجموعتنا على 4 حقول:

  • name (سلسلة): اسم ملف الصورة التي تم تحميلها، وهو أيضًا مفتاح المستند
  • labels (مصفوفة من السلاسل): تصنيفات العناصر المعروفة بواسطة Vision API
  • color (سلسلة): رمز اللون السداسي العشري للون السائد (مثل #ab12ef)
  • CREATE (التاريخ): الطابع الزمني لوقت تخزين البيانات الوصفية لهذه الصورة
  • الصورة المصغّرة (قيمة منطقية): حقل اختياري يتم عرضه ويكون صحيحًا إذا تم إنشاء صورة مصغّرة لهذه الصورة

بينما سنبحث في 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. استكشاف الدالة

لنلقِ نظرة فاحصة على مختلف الأجزاء الشيقة.

أولاً، نعمل على تضمين الاعتماديات المحدّدة في ملف 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.

في ما يلي حمولة بيانات الحدث كمرجع لك:

{
  "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:

...
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 وانقر على الحزمة التي أنشأناها في بداية التمرين المعملي:

d44c1584122311c7.png

بعد الانتقال إلى صفحة تفاصيل الحزمة، انقر على الزر Upload files لتحميل صورة.

26bb31d35fb6aa3d.png

من "الهامبرغر" (⇧)، انتقِل إلى مستكشف Logging > Logs.

في أداة اختيار Log Fields، اختَر Cloud Function للاطّلاع على السجلات المخصَّصة لدوالك. قم بالتمرير لأسفل عبر حقول السجل ويمكنك حتى تحديد دالة معينة للحصول على عرض أكثر دقة للسجلات ذات الصلة بالدوال. اختَر الدالة picture-uploaded.

b651dca7e25d5b11.png

وفي قائمة السجلّات، أرى أنّه تم استدعاء الدالة:

d22a7f24954e4f63.png

تشير السجلات إلى بداية تنفيذ الدالة ونهايتها. وبين ما بينهما، يمكننا رؤية السجلات التي وضعناها في الدالة باستخدام عبارات console.log(). يتضح لنا ما يلي:

  • تفاصيل الحدث الذي يؤدي إلى تشغيل الدالة،
  • النتائج الأولية من طلب بيانات من واجهة Vision API
  • الملصقات التي تم العثور عليها في الصورة التي قمنا بتحميلها،
  • معلومات الألوان السائدة،
  • ما إذا كانت الصورة آمنة للعرض
  • وفي النهاية، تم تخزين تلك البيانات الوصفية حول الصورة في Firestore.

9ff7956a215c15da.png

مرة أخرى من "الهامبرغر" (⇧)، وانتقِل إلى قسم "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 Vision
  • Cloud Firestore

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