Zdjęcia codziennie: moduł 1 – przechowywanie i analizowanie zdjęć (Java)

1. Przegląd

W pierwszym module prześlesz zdjęcia do zasobnika. Spowoduje to wygenerowanie zdarzenia utworzenia pliku, które zostanie obsłużone przez funkcję. Funkcja wywoła interfejs Vision API, aby przeprowadzić analizę obrazu i zapisać wyniki w magazynie danych.

d650ca5386ea71ad.png

Czego się nauczysz

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

2. Konfiguracja i wymagania

Samodzielne konfigurowanie środowiska

  1. Zaloguj się w konsoli Google Cloud i utwórz nowy projekt lub użyj istniejącego. Jeśli nie masz jeszcze konta Gmail ani Google Workspace, musisz je utworzyć.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • Nazwa projektu to wyświetlana nazwa uczestników tego projektu. Jest to ciąg znaków, który nie jest używany przez interfejsy API Google. Możesz ją zaktualizować w dowolnym momencie.
  • Identyfikator projektu musi być unikalny we wszystkich projektach Google Cloud i jest niezmienny (nie można go zmienić po ustawieniu). Konsola Cloud automatycznie generuje unikalny ciąg znaków. Zwykle nie musisz się nim przejmować. W większości ćwiczeń z programowania musisz odwoływać się do identyfikatora projektu (zwykle jest on oznaczony jako PROJECT_ID). Jeśli wygenerowany identyfikator Ci się nie podoba, możesz wygenerować inny losowy identyfikator. Możesz też spróbować własnej nazwy i sprawdzić, czy jest dostępna. Po tym kroku nie można go zmienić i będzie obowiązywać przez cały czas trwania projektu.
  • Warto wiedzieć, że istnieje też trzecia wartość, czyli numer projektu, z której korzystają niektóre interfejsy API. Więcej informacji o tych 3 wartościach znajdziesz w dokumentacji.
  1. Następnie musisz włączyć płatności w konsoli Cloud, aby korzystać z zasobów i interfejsów API Google Cloud. Ukończenie tego laboratorium nie powinno wiązać się z dużymi kosztami, a nawet z żadnymi. Aby wyłączyć zasoby i uniknąć naliczania opłat po zakończeniu tego samouczka, możesz usunąć utworzone zasoby lub cały projekt. Nowi użytkownicy Google Cloud mogą skorzystać z bezpłatnego okresu próbnego, w którym mają do dyspozycji środki w wysokości 300 USD.

Uruchamianie Cloud Shell

Z Google Cloud można korzystać zdalnie na laptopie, ale w tym module praktycznym będziesz używać Google Cloud Shell, czyli środowiska wiersza poleceń działającego w chmurze.

W konsoli Google Cloud kliknij ikonę Cloud Shell na pasku narzędzi w prawym górnym rogu:

55efc1aaa7a4d3ad.png

Uzyskanie dostępu do środowiska i połączenie się z nim powinno zająć tylko kilka chwil. Po zakończeniu powinno wyświetlić się coś takiego:

7ffe5cbb04455448.png

Ta maszyna wirtualna zawiera wszystkie potrzebne narzędzia dla programistów. Zawiera również stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i usprawnia proces uwierzytelniania. Wszystkie zadania w tym laboratorium możesz wykonać w przeglądarce. Nie musisz niczego instalować.

3. Włącz interfejsy API

W tym module będziesz korzystać z Cloud Functions i interfejsu Vision API, ale najpierw musisz je włączyć w konsoli Google Cloud lub za pomocą gcloud.

Aby włączyć interfejs Vision API w konsoli Cloud, na pasku wyszukiwania wpisz Cloud Vision API:

cf48b1747ba6a6fb.png

Wyświetli się strona Cloud Vision API:

ba4af419e6086fbb.png

Kliknij przycisk ENABLE.

Możesz też włączyć go w Cloud Shell za pomocą narzędzia wiersza poleceń gcloud.

W Cloud Shell uruchom to polecenie:

gcloud services enable vision.googleapis.com

Operacja powinna zakończyć się powodzeniem:

Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.

Włącz też Cloud Functions:

gcloud services enable cloudfunctions.googleapis.com

4. Tworzenie zasobnika (konsola)

Utwórz zasobnik na zdjęcia. Możesz to zrobić w konsoli Google Cloud Platform ( console.cloud.google.com) lub za pomocą narzędzia wiersza poleceń gsutilCloud Shell lub lokalnym środowisku programistycznym.

W menu „hamburger” (☰) otwórz stronę Storage.

1930e055d138150a.png

Nazwij zasobnik

Kliknij przycisk CREATE BUCKET.

34147939358517f8.png

Kliknij CONTINUE.

Wybierz lokalizację

197817f20be07678.png

Utwórz zasobnik z wieloma regionami w wybranym regionie (w tym przypadku Europe).

Kliknij CONTINUE.

Wybierz domyślną klasę pamięci masowej

53cd91441c8caf0e.png

Wybierz klasę pamięci masowej Standard dla swoich danych.

Kliknij CONTINUE.

Ustawianie kontroli dostępu

8c2b3b459d934a51.png

Będziesz pracować z publicznie dostępnymi obrazami, więc chcesz, aby wszystkie zdjęcia przechowywane w tym zasobniku miały takie same, jednolite ustawienia kontroli dostępu.

Wybierz opcję kontroli dostępu Uniform.

Kliknij CONTINUE.

Ustawianie ochrony/szyfrowania

d931c24c3e705a68.png

Zachowaj ustawienie domyślne (Google-managed key)), ponieważ nie będziesz używać własnych kluczy szyfrowania.

Kliknij CREATE, aby zakończyć tworzenie zasobnika.

Dodawanie allUsers jako przeglądającego miejsce na dane

Otwórz kartę Permissions:

d0ecfdcff730ea51.png

Dodaj do zasobnika użytkownika allUsers z rolą Storage > Storage Object Viewer w ten sposób:

e9f25ec1ea0b6cc6.png

Kliknij SAVE.

5. Tworzenie zasobnika (gsutil)

Do tworzenia zasobników możesz też używać narzędzia wiersza poleceń gsutil w Cloud Shell.

W Cloud Shell ustaw zmienną dla unikalnej nazwy zasobnika. W Cloud Shell zmienna GOOGLE_CLOUD_PROJECT jest już ustawiona na Twój unikalny identyfikator projektu. Możesz dodać go do nazwy zasobnika.

Na przykład:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

Utwórz standardową strefę wieloregionową w Europie:

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

Sprawdź, czy jest włączony jednolity dostęp na poziomie zasobnika:

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

Ustaw zasobnik jako publiczny:

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

Jeśli przejdziesz do sekcji Cloud Storage w konsoli, powinien być tam publiczny uploaded-pictures:

a98ed4ba17873e40.png

Sprawdź, czy możesz przesyłać zdjęcia do zasobnika i czy przesłane zdjęcia są dostępne publicznie, jak opisano w poprzednim kroku.

6. Testowanie dostępu publicznego do zasobnika

Wracając do przeglądarki pamięci, zobaczysz na liście swój zasobnik z dostępem „Publiczny” (wraz z ostrzeżeniem, że każdy ma dostęp do zawartości tego zasobnika).

89e7a4d2c80a0319.png

Zasobnik jest gotowy do odbierania zdjęć.

Jeśli klikniesz nazwę zasobnika, zobaczysz jego szczegóły.

131387f12d3eb2d3.png

Możesz tam kliknąć przycisk Upload files, aby sprawdzić, czy możesz dodać obraz do zasobnika. Pojawi się wyskakujące okienko z prośbą o wybranie pliku. Po wybraniu plik zostanie przesłany do zasobnika i ponownie zobaczysz public dostęp, który został automatycznie przypisany do tego nowego pliku.

e87584471a6e9c6d.png

Obok etykiety dostępu Public zobaczysz też małą ikonę linku. Po kliknięciu przeglądarka przejdzie do publicznego adresu URL tego obrazu, który będzie miał postać:

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

gdzie BUCKET_NAME to niepowtarzalna globalnie nazwa zasobnika, a następnie nazwa pliku obrazu.

Kliknięcie pola wyboru obok nazwy obrazu spowoduje włączenie przycisku DELETE, dzięki czemu możesz usunąć pierwszy obraz.

7. Tworzenie funkcji

W tym kroku utworzysz funkcję, która reaguje na zdarzenia przesyłania obrazów.

Otwórz sekcję Cloud Functions w konsoli Google Cloud. Gdy ją otworzysz, usługa Cloud Functions zostanie automatycznie włączona.

9d29e8c026a7a53f.png

Kliknij Create function.

Wybierz nazwę (np. picture-uploaded) i region (pamiętaj, aby był zgodny z regionem wybranym dla zasobnika):

4bb222633e6f278.png

Istnieją 2 rodzaje funkcji:

  • funkcje HTTP, które można wywoływać za pomocą adresu URL (np. interfejs API sieciowy);
  • Funkcje działające w tle, które mogą być wywoływane przez określone zdarzenie.

Chcesz utworzyć funkcję w tle, która będzie wywoływana, gdy do zasobnika Cloud Storage zostanie przesłany nowy plik:

d9a12fcf58f4813c.png

Interesuje Cię typ zdarzenia Finalize/Create, czyli zdarzenie wywoływane, gdy plik jest tworzony lub aktualizowany w zasobniku:

b30c8859b07dc4cb.png

Wybierz utworzony wcześniej zasobnik, aby poinformować Cloud Functions o otrzymywaniu powiadomień o utworzeniu lub zaktualizowaniu pliku w tym zasobniku:

cb15a1f4c7a1ca5f.png

Kliknij Select, aby wybrać utworzony wcześniej zasobnik, a następnie Save.

c1933777fac32c6a.png

Zanim klikniesz Dalej, możesz rozwinąć i zmodyfikować ustawienia domyślne (256 MB pamięci) w sekcji Ustawienia środowiska wykonawczego, kompilacji, połączeń i zabezpieczeń i zmienić je na 1 GB.

83d757e6c38e10.png

Po kliknięciu Next możesz dostosować środowisko wykonawcze, kod źródłowy i punkt wejścia.

Zachowaj Inline editor dla tej funkcji:

b6646ec646082b32.png

Wybierz jedno ze środowisk wykonawczych Javy, np. Java 11:

f85b8a6f951f47a7.png

Kod źródłowy składa się z pliku Java i pliku pom.xml Maven, który zawiera różne metadane i zależności.

Pozostaw domyślny fragment kodu: rejestruje on nazwę pliku przesłanego zdjęcia:

9b7b9801b42f6ca6.png

Na potrzeby testowania pozostaw nazwę funkcji do wykonania ustawioną na Example.

Aby utworzyć i wdrożyć funkcję, kliknij Deploy. Po pomyślnym wdrożeniu na liście funkcji powinien pojawić się zielony znacznik wyboru w kółku:

3732fdf409eefd1a.png

8. Testowanie funkcji

W tym kroku sprawdzisz, czy funkcja reaguje na zdarzenia związane z pamięcią.

W menu (☰) wróć na stronę Storage.

Kliknij zasobnik obrazów, a potem Upload files, aby przesłać obraz.

21767ec3cb8b18de.png

W konsoli w chmurze ponownie otwórz stronę Logging > Logs Explorer.

W selektorze Log Fields wybierz Cloud Function, aby wyświetlić logi dotyczące Twoich funkcji. Przewiń w dół sekcję Log Fields (Pola logów). Możesz nawet wybrać konkretną funkcję, aby uzyskać bardziej szczegółowy widok logów powiązanych z funkcjami. Wybierz funkcję picture-uploaded.

Powinny być widoczne wpisy w logu dotyczące utworzenia funkcji, czasu jej rozpoczęcia i zakończenia oraz nasza rzeczywista instrukcja logowania:

e8ba7d39c36df36c.png

W naszym dzienniku widnieje wpis: Processing file: pic-a-daily-architecture-events.png, co oznacza, że zdarzenie związane z utworzeniem i zapisaniem tego zdjęcia zostało wywołane zgodnie z oczekiwaniami.

9. Przygotowywanie bazy danych

Informacje o zdjęciu podane przez interfejs Vision API zapiszesz w bazie danych Cloud Firestore, czyli szybkiej, w pełni zarządzanej, bezserwerowej, chmurowej bazie danych dokumentów NoSQL. Przygotuj bazę danych, przechodząc do sekcji Firestore w konsoli Cloud:

9e4708d2257de058.png

Dostępne są 2 opcje: Native mode lub Datastore mode. Używaj trybu natywnego, który oferuje dodatkowe funkcje, takie jak obsługa offline i synchronizacja w czasie rzeczywistym.

Kliknij SELECT NATIVE MODE.

9449ace8cc84de43.png

Wybierz region obejmujący wiele lokalizacji (w tym przypadku w Europie, ale najlepiej co najmniej ten sam region, w którym znajdują się Funkcje i zasobnik pamięci).

Kliknij przycisk CREATE DATABASE.

Po utworzeniu bazy danych zobaczysz te informacje:

56265949a124819e.png

Utwórz nową kolekcję, klikając przycisk + START COLLECTION.

Nazwij kolekcję pictures.

75806ee24c4e13a7.png

Nie musisz tworzyć dokumentu. Dodasz je programowo, gdy nowe zdjęcia będą przechowywane w Cloud Storage i analizowane przez interfejs Vision API.

Kliknij Save.

Firestore tworzy pierwszy domyślny dokument w nowo utworzonej kolekcji. Możesz go bezpiecznie usunąć, ponieważ nie zawiera żadnych przydatnych informacji:

5c2f1e17ea47f48f.png

Dokumenty, które zostaną utworzone programowo w naszej kolekcji, będą zawierać 4 pola:

  • name (string): nazwa pliku przesłanego zdjęcia, która jest też kluczem dokumentu.
  • labels (tablica ciągów znaków): etykiety rozpoznanych elementów przez Vision API.
  • color (string): szesnastkowy kod koloru dominującego (np. #ab12ef)
  • created (data): sygnatura czasowa wskazująca, kiedy metadane tego obrazu zostały zapisane.
  • thumbnail (wartość logiczna): pole opcjonalne, które będzie obecne i będzie miało wartość „true”, jeśli dla tego zdjęcia została wygenerowana miniatura.

Będziemy wyszukiwać w Firestore zdjęcia, dla których dostępne są miniatury, i sortować je według daty utworzenia, więc musimy utworzyć indeks wyszukiwania.

Możesz utworzyć indeks za pomocą tego polecenia w Cloud Shell:

gcloud firestore indexes composite create \
  --collection-group=pictures \
  --field-config field-path=thumbnail,order=descending \
  --field-config field-path=created,order=descending

Możesz też to zrobić w konsoli Google Cloud, klikając Indexes w kolumnie nawigacyjnej po lewej stronie, a następnie tworząc indeks złożony w sposób pokazany poniżej:

ecb8b95e3c791272.png

Kliknij Create. Tworzenie indeksu może potrwać kilka minut.

10. Aktualizowanie funkcji

Wróć na stronę Functions, aby zaktualizować funkcję, która będzie wywoływać interfejs Vision API w celu analizowania zdjęć i przechowywania metadanych w Firestore.

W menu (☰) przejdź do sekcji Cloud Functions, kliknij nazwę funkcji, wybierz kartę Source, a następnie kliknij przycisk EDIT.

Najpierw zmień plik pom.xml, który zawiera listę zależności naszej funkcji w Javie. Zaktualizuj kod, aby dodać zależność Maven interfejsu 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>

Po zaktualizowaniu zależności możesz pracować nad kodem funkcji, aktualizując plik Example.java za pomocą niestandardowego kodu.

Najedź kursorem myszy na plik Example.java i kliknij ołówek. Zastąp nazwę pakietu i nazwę pliku ciągiem src/main/java/fn/ImageAnalysis.java.

Zastąp kod w pliku ImageAnalysis.java kodem poniżej. Wyjaśnimy to w następnym kroku.

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. Poznaj funkcję

Przyjrzyjmy się bliżej różnym interesującym częściom.

Po pierwsze, uwzględniamy konkretne zależności w pliku pom.xml Maven. Biblioteki klienta Google Java publikują Bill-of-Materials(BOM), aby wyeliminować konflikty zależności. Dzięki temu nie musisz określać wersji poszczególnych bibliotek klienta 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>

Następnie przygotowujemy klienta do korzystania z interfejsu Vision API:

...
try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
...

Teraz przejdźmy do struktury naszej funkcji. Z przychodzącego zdarzenia pobieramy interesujące nas pola i mapujemy je na zdefiniowaną przez nas strukturę 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;
    }

Zwróć uwagę na sygnaturę, ale też na to, jak pobieramy nazwę pliku i zasobnika, które wywołały funkcję w Cloud Functions.

Oto przykładowy ładunek zdarzenia:

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

Przygotowujemy żądanie do wysłania za pomocą klienta 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();

Prosimy o 3 kluczowe funkcje interfejsu Vision API:

  • Wykrywanie etykiet: aby zrozumieć, co znajduje się na zdjęciach.
  • Właściwości obrazu: aby podać interesujące atrybuty obrazu (interesuje nas dominujący kolor obrazu).
  • Bezpieczne wyszukiwanie: aby sprawdzić, czy obraz jest bezpieczny do wyświetlenia (nie powinien zawierać treści dla dorosłych, medycznych, o charakterze seksualnym ani treści przedstawiających przemoc).

W tym momencie możemy wywołać interfejs Vision API:

...
logger.info("Calling the Vision API...");
BatchAnnotateImagesResponse result = 
                            vision.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = result.getResponsesList();
...

Oto przykład odpowiedzi interfejsu 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
}

Jeśli nie zostanie zwrócony żaden błąd, możemy przejść dalej. Dlatego mamy ten blok if:

AnnotateImageResponse response = responses.get(0);
if (response.hasError()) {
     logger.info("Error: " + response.getError().getMessage());
     return;
}

Pobierzemy etykiety rzeczy, kategorii lub motywów rozpoznanych na zdjęciu:

List<String> labels = response.getLabelAnnotationsList().stream()
    .map(annotation -> annotation.getDescription())
    .collect(Collectors.toList());

logger.info("Annotations found:");
for (String label: labels) {
    logger.info("- " + label);
}

Chcemy poznać dominujący kolor zdjęcia:

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

Używamy też funkcji użytkowej do przekształcania wartości czerwonej, zielonej i niebieskiej w szesnastkowy kod koloru, którego możemy używać w arkuszach stylów CSS.

Sprawdźmy, czy zdjęcie jest bezpieczne:

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

Sprawdzamy atrybuty dotyczące treści dla dorosłych, parodii, treści medycznych, przemocy i treści o charakterze erotycznym, aby określić, czy są one prawdopodobne lub bardzo prawdopodobne.

Jeśli wynik bezpiecznego wyszukiwania jest prawidłowy, możemy zapisać metadane w 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. Wdrażanie funkcji

Czas wdrożyć funkcję.

604f47aa11fbf8e.png

Kliknij przycisk DEPLOY, a nowa wersja zostanie wdrożona. Możesz śledzić postęp:

13da63f23e4dbbdd.png

13. Ponowne testowanie funkcji

Po pomyślnym wdrożeniu funkcji opublikujesz obraz w Cloud Storage, sprawdzisz, czy funkcja została wywołana, co zwraca interfejs Vision API i czy metadane są przechowywane w Firestore.

Wróć do Cloud Storage i kliknij zasobnik utworzony na początku modułu:

d44c1584122311c7.png

Na stronie z informacjami o zasobniku kliknij przycisk Upload files, aby przesłać zdjęcie.

26bb31d35fb6aa3d.png

W menu (☰) przejdź do Logging > Logs Eksploratora.

W selektorze Log Fields wybierz Cloud Function, aby wyświetlić logi dotyczące Twoich funkcji. Przewiń w dół sekcję Log Fields (Pola logów). Możesz nawet wybrać konkretną funkcję, aby uzyskać bardziej szczegółowy widok logów powiązanych z funkcjami. Wybierz funkcję picture-uploaded.

b651dca7e25d5b11.png

Na liście logów widać, że nasza funkcja została wywołana:

d22a7f24954e4f63.png

Logi wskazują początek i koniec wykonania funkcji. Pomiędzy nimi widzimy logi, które umieściliśmy w funkcji za pomocą instrukcji console.log(). Widzimy:

  • szczegóły zdarzenia, które wywołało naszą funkcję,
  • Nieprzetworzone wyniki wywołania interfejsu Vision API.
  • etykiety znalezione na przesłanym przez nas zdjęciu;
  • informacje o kolorach dominujących,
  • czy obraz jest bezpieczny do wyświetlenia,
  • Metadane obrazu zostały ostatecznie zapisane w Firestore.

9ff7956a215c15da.png

Ponownie w menu „hamburger” (☰) otwórz sekcję Firestore. W podsekcji Data (wyświetlanej domyślnie) powinna pojawić się kolekcja pictures z dodanym nowym dokumentem odpowiadającym właśnie przesłanemu zdjęciu:

a6137ab9687da370.png

14. Zwalnianie miejsca (opcjonalnie)

Jeśli nie zamierzasz kontynuować pracy z innymi ćwiczeniami z tej serii, możesz usunąć zasoby, aby zaoszczędzić pieniądze i być dobrym użytkownikiem chmury. Możesz zwolnić miejsce, czyszcząc poszczególne zasoby w ten sposób:

Usuń zasobnik:

gsutil rb gs://${BUCKET_PICTURES}

Usuń funkcję:

gcloud functions delete picture-uploaded --region europe-west1 -q

Usuń kolekcję Firestore, wybierając opcję Usuń kolekcję:

410b551c3264f70a.png

Możesz też usunąć cały projekt:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

15. Gratulacje!

Gratulacje! Udało Ci się wdrożyć pierwszą kluczową usługę projektu.

Omówione zagadnienia

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

Następne kroki