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

1. Omówienie

W pierwszym module dotyczącym programowania umieścisz zdjęcia w zasobniku. Spowoduje to wygenerowanie zdarzenia utworzenia pliku, które będzie obsługiwane przez funkcję. Ta 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 wykorzystaj już istniejący. Jeśli nie masz jeszcze konta Gmail ani Google Workspace, musisz je utworzyć.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • Nazwa projektu jest wyświetlaną nazwą uczestników tego projektu. To ciąg znaków, który nie jest używany przez interfejsy API Google. W każdej chwili możesz ją zmienić.
  • Identyfikator projektu musi być unikalny we wszystkich projektach Google Cloud i nie można go zmienić (nie można go zmienić po ustawieniu). Cloud Console automatycznie wygeneruje unikalny ciąg znaków. zwykle nieważne, co ona jest. W większości ćwiczeń z programowania konieczne jest odwołanie się do identyfikatora projektu (zwykle nazywa się on PROJECT_ID). Jeśli nie podoba Ci się wygenerowany identyfikator, możesz wygenerować kolejny losowy. Możesz też spróbować własnych sił i sprawdzić, czy jest dostępna. Potem nie będzie można go zmienić. Pozostanie ono przez czas trwania projektu.
  • Dostępna jest trzecia wartość, numer projektu, z którego korzystają niektóre interfejsy API. Więcej informacji o wszystkich 3 wartościach znajdziesz w dokumentacji.
  1. Następnie musisz włączyć płatności w Cloud Console, aby korzystać z zasobów Cloud/interfejsów API. Ukończenie tego ćwiczenia z programowania nie powinno kosztować zbyt wiele. Aby wyłączyć zasoby, aby nie naliczać opłat po zakończeniu tego samouczka, możesz usunąć utworzone przez siebie zasoby lub cały projekt. Nowi użytkownicy Google Cloud mogą skorzystać z programu bezpłatnego okresu próbnego o wartości 300 USD.

Uruchamianie Cloud Shell

Google Cloud można obsługiwać zdalnie z laptopa, ale w ramach tego ćwiczenia z programowania wykorzystasz Google Cloud Shell – środowisko wiersza poleceń działające w Cloud.

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

55efc1aaa7a4d3ad.png

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

7ffe5cbb04455448.png

Ta maszyna wirtualna ma wszystkie potrzebne narzędzia dla programistów. Zawiera stały katalog domowy o pojemności 5 GB i działa w Google Cloud, znacząco zwiększając wydajność sieci i uwierzytelnianie. Wszystkie zadania w ramach tego ćwiczenia z programowania można wykonywać w przeglądarce. Nie musisz niczego instalować.

3. Włącz interfejsy API

W tym module będziesz używać Cloud Functions i Vision API, ale najpierw musisz włączyć te funkcje w konsoli Cloud lub za pomocą usługi gcloud.

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

cf48b1747ba6a6fb.png

Wyświetli się strona Cloud Vision API:

ba4af419e6086fbb.png

Kliknij przycisk ENABLE.

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

W Cloud Shell uruchom to polecenie:

gcloud services enable vision.googleapis.com

Powinna pojawić się operacja dokończenia:

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ń gsutil z Cloud Shell albo lokalnego środowiska programistycznego.

Z „hamburgera” (<!--) przejdź na 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 (tutaj Europe).

Kliknij CONTINUE.

Wybierz domyślną klasę pamięci

53cd91441c8caf0e.png

Wybierz klasę pamięci Standard dla swoich danych.

Kliknij CONTINUE.

Ustaw kontrolę dostępu

8c2b3b459d934a51.png

Ponieważ pracujesz z obrazami dostępnymi publicznie, chcesz, aby wszystkie zdjęcia przechowywane w tym zasobniku miały taką samą jednolitą kontrolę dostępu.

Wybierz opcję kontroli dostępu w usłudze Uniform.

Kliknij CONTINUE.

Ustaw zabezpieczenia/szyfrowanie

d931c24c3e705a68.png

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

Kliknij CREATE, aby zakończyć tworzenie zasobnika.

Dodawanie użytkownika allUsers jako wyświetlającego miejsce na dane

Otwórz kartę Permissions:

d0ecfdcff730ea51.png

Dodaj do zasobnika użytkownika allUsers z rolą Storage > Storage Object Viewer w następujący sposób:

e9f25ec1ea0b6cc6.png

Kliknij SAVE.

5. Tworzenie zasobnika (gsutil)

Zasobniki możesz też tworzyć za pomocą narzędzia wiersza poleceń gsutil w Cloud Shell.

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

Na przykład:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

Tworzenie standardowej strefy obejmującej wiele regionów w Europie:

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

Sprawdź, czy 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 otworzysz sekcję Cloud Storage w konsoli, powinien być już publiczny zasobnik uploaded-pictures:

a98ed4ba17873e40.png

Sprawdź, czy możesz przesyłać zdjęcia do zasobnika i czy są one publicznie dostępne, jak wyjaśniliśmy w poprzednim kroku.

6. Testowanie dostępu publicznego do zasobnika

Gdy wrócisz do przeglądarki, zobaczysz zasobnik na liście z oznaczeniem „Publiczny”. dostępu (w tym znak ostrzegawczy przypominający o tym, że każdy ma dostęp do zawartości danego zasobnika).

89e7a4d2c80a0319.png

Twój zasobnik jest teraz gotowy na otrzymywanie zdjęć.

Po kliknięciu nazwy zasobnika zobaczysz jego szczegóły.

131387f12d3eb2d3.png

Tam możesz wypróbować przycisk Upload files, aby sprawdzić, czy możesz dodać zdjęcie do zasobnika. Pojawi się wyskakujące okienko wyboru plików z prośbą o wybranie pliku. Po wybraniu plik zostanie przesłany do zasobnika i ponownie zobaczysz uprawnienia dostępu na poziomie public, które zostały automatycznie przypisane do tego nowego pliku.

e87584471a6e9c6d.png

Na etykiecie dostępu Public zobaczysz też małą ikonę linku. Po kliknięciu obrazu przeglądarka otworzy jego publiczny adres URL, który będzie miał postać:

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

BUCKET_NAME to globalnie unikalna nazwa nadana przez Ciebie dla zasobnika oraz nazwa pliku zdjęcia.

Gdy klikniesz pole wyboru obok nazwy zdjęcia, przycisk DELETE stanie się aktywny i będzie można 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 w Cloud Functions zostanie automatycznie włączona.

9d29e8c026a7a53f.png

Kliknij Create function.

Wybierz nazwę (np. picture-uploaded) i region (pamiętaj, aby zachować zgodność z wyborem regionu dla zasobnika):

4bb222633e6f278.png

Są 2 rodzaje funkcji:

  • funkcji HTTP, które można wywoływać za pomocą adresu URL (np. internetowego interfejsu API);
  • Funkcje działające w tle, które mogą być aktywowane przez niektóre zdarzenia.

Chcesz utworzyć funkcję działającą w tle, która będzie aktywowana po przesłaniu nowego pliku do naszego zasobnika Cloud Storage:

d9a12fcf58f4813c.png

Interesuje Cię typ zdarzenia Finalize/Create, który jest wywoływany, gdy plik jest tworzony lub aktualizowany w zasobniku:

b30c8859b07dc4cb.png

Wybierz utworzony wcześniej zasobnik, aby wskazać usłudze Cloud Functions, że ma otrzymywać powiadomienia o utworzeniu lub zaktualizowaniu pliku w tym konkretnym zasobniku:

cb15a1f4c7a1ca5f.png

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

c1933777fac32c6a.png

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

83d757e6c38e10.png

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

Zachowaj Inline editor dla tej funkcji:

b6646ec646082b32.png

Wybierz jedno ze środowisk wykonawczych Java, na przykład Java 11:

f85b8a6f951f47a7.png

Kod źródłowy składa się z pliku Java i pliku Maven pom.xml z różnymi metadanymi i zależnościami.

Pozostaw domyślny fragment kodu – spowoduje to zarejestrowanie nazwy pliku przesłanego zdjęcia:

9b7b9801b42f6ca6.png

Na razie zachowaj nazwę funkcji do wykonania w polu Example na potrzeby testowania.

Kliknij Deploy, aby utworzyć i wdrożyć funkcję. Po pomyślnym wdrożeniu powinien pojawić się zielony znacznik wyboru na liście funkcji:

3732fdf409eefd1a.png

8. Testowanie funkcji

W tym kroku sprawdź, czy funkcja odpowiada na zdarzenia związane z przechowywaniem danych.

Z „hamburgera” (<!--) wróć na stronę Storage.

Kliknij zasobnik obrazów, a następnie Upload files, aby przesłać obraz.

21767ec3cb8b18de.png

W Cloud Console otwórz stronę Logging > Logs Explorer.

W selektorze Log Fields wybierz Cloud Function, aby wyświetlić logi przeznaczone dla Twoich funkcji. Przewiń stronę Pola logów w dół, aby wybrać konkretną funkcję, aby zobaczyć bardziej szczegółowe logi związane z funkcjami. Wybierz funkcję picture-uploaded.

Powinny się wyświetlić pozycje dziennika wspominające o utworzeniu funkcji, czasy rozpoczęcia i zakończenia funkcji oraz nasza instrukcja logu:

e8ba7d39c36df36c.png

Komunikat dziennika o treści: Processing file: pic-a-daily-architecture-events.png, co oznacza, że zdarzenie związane z tworzeniem i przechowywaniem tego zdjęcia zostało rzeczywiście wywołane zgodnie z oczekiwaniami.

9. Przygotowywanie bazy danych

Informacje o zdjęciu wyświetlanym przez interfejs Vision API będziesz przechowywać w bazie danych Cloud Firestore – szybkiej, w pełni zarządzanej, bezserwerowej, chmurowej bazie dokumentów NoSQL. Przygotuj bazę danych w sekcji Firestore w konsoli Cloud:

9e4708d2257de058.png

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

Kliknij SELECT NATIVE MODE.

9449ace8cc84de43.png

Wybierz wiele regionów (w Europie, ale najlepiej co najmniej taki sam region, w jakim są Twoje funkcja i zasobnik na dane).

Kliknij przycisk CREATE DATABASE.

Po utworzeniu bazy danych powinien wyświetlić się ten komunikat:

56265949a124819e.png

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

Nazwij kolekcję pictures.

75806ee24c4e13a7.png

Nie musisz tworzyć dokumentu. Będziesz dodawać je automatycznie w miarę zapisywania nowych zdjęć w Cloud Storage i analizowania ich przez interfejs Vision API.

Kliknij Save.

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

5c2f1e17ea47f48f.png

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

  • name (ciąg znaków): nazwa pliku przesłanego zdjęcia, który jest również kluczem dokumentu.
  • labels (tablica ciągów znaków): etykiety rozpoznawane przez interfejs Vision API
  • color (ciąg znaków): szesnastkowy kod koloru dominującego (np. #ab12ef)
  • created (data): sygnatura czasowa przechowywania metadanych obrazu.
  • thumbnail (wartość logiczna): opcjonalne pole, które będzie obecne i będzie mieć wartość true (prawda), jeśli dla danego zdjęcia wygenerowano obraz miniatury

Będziemy szukać w Firestore zdjęć, które mają dostępne miniatury, i sortujemy według daty utworzenia, więc konieczne będzie utworzenie indeksu wyszukiwania.

Indeks możesz utworzyć 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 to też zrobić w konsoli Cloud. Aby to zrobić, w kolumnie nawigacji po lewej stronie kliknij Indexes, a potem utwórz indeks złożony w ten sposób:

ecb8b95e3c791272.png

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

10. Zaktualizuj funkcję

Wróć na stronę Functions, aby zaktualizować funkcję wywoływania interfejsu Vision API w celu analizy obrazów oraz przechowywać metadane w Firestore.

Z „hamburgera” (<!--) przejdź do sekcji Cloud Functions, kliknij nazwę funkcji, wybierz kartę Source, a następnie kliknij przycisk EDIT.

Najpierw edytuj 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>

Teraz gdy zależności są już aktualne, będziesz pracować nad kodem naszej funkcji, aktualizując plik Example.java za pomocą kodu niestandardowego.

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

Zastąp kod w polu ImageAnalysis.java poniższym kodem. 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 elementom.

Najpierw uwzględniamy konkretne zależności w pliku Maven pom.xml. Biblioteki klienta w języku Java firmy Google publikują plik Bill-of-Materials(BOM), aby wyeliminować wszelkie konflikty zależności. Dzięki niemu nie musisz określać żadnej wersji dla 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 zaczyna się struktura naszej funkcji. Przechwytujemy ze zdarzenia przychodzącego te pola, które nas interesują, 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 podpis oraz to, jak pobieramy nazwę pliku i zasobnika, które aktywowały funkcję w Cloud Functions.

Poniżej znajdziesz 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: sprawdzanie, co znajduje się na zdjęciach.
  • Właściwości obrazu: w celu nadania nam interesujących atrybutów zdjęcia (chcemy zwrócić uwagę na dominujący kolor zdjęcia).
  • Bezpieczne wyszukiwanie: pozwala określić, czy obraz jest bezpieczny do wyświetlenia (nie może zawierać treści dla dorosłych, medycyny, treści dla dorosłych ani przemocy).

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, jak wygląda odpowiedź z 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, z tego powodu w przypadku blokady:

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

Poznamy etykiety elementów, kategorii i tematów rozpoznawane na ilustracji:

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

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

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

Korzystamy również z funkcji użytkowej, która przekształca wartości koloru czerwonego, zielonego i niebieskiego w szesnastkowy kod koloru, którego można użyć w arkuszach stylów CSS.

Sprawdźmy, czy zdjęcie można bezpiecznie wyświetlać:

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, czy treści dla dorosłych / parodie / medycyna / przemoc / treści dla dorosłych i czy są one prawdopodobne lub bardzo prawdopodobne.

Jeśli wynik bezpiecznego wyszukiwania jest prawidłowy, możemy przechowywać 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

Naciśnij przycisk DEPLOY, aby wdrożyć nową wersję. Możesz sprawdzić postęp:

13da63f23e4dbbdd.png

13. Ponownie przetestuj funkcję

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 szczegółów zasobnika kliknij przycisk Upload files, aby przesłać zdjęcie.

26bb31d35fb6aa3d.png

Z „hamburgera” (<!--) otwórz Eksploratora Logging > Logs.

W selektorze Log Fields wybierz Cloud Function, aby wyświetlić logi przeznaczone dla Twoich funkcji. Przewiń stronę Pola logów w dół, aby wybrać konkretną funkcję, aby zobaczyć bardziej szczegółowe logi związane 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 widać logi umieszczone w funkcji za pomocą instrukcji Console.log(). Widzimy:

  • szczegóły zdarzenia aktywującego naszą funkcję,
  • Nieprzetworzone wyniki z wywołania interfejsu Vision API
  • Etykiety znalezione na przesłanym zdjęciu,
  • informacje o kolorach dominujących,
  • Whether the picture is safe to show,
  • Metadane obrazu zostały zapisane w Firestore.

9ff7956a215c15da.png

Ponownie od „hamburgera” (<!--) przejdź do sekcji Firestore. W podsekcji Data (wyświetlanej domyślnie) powinna być widoczna kolekcja pictures z nowym dokumentem odpowiadającym przesłanym właśnie zdjęciu:

a6137ab9687da370.png

14. Czyszczenie (opcjonalnie)

Jeśli nie chcesz przechodzić do innych modułów z tej serii, możesz zwolnić zasoby, aby zmniejszyć koszty i zachować dobre praktyki związane z chmurą. Zasoby możesz wyczyścić pojedynczo w ten sposób.

Usuń zasobnik:

gsutil rb gs://${BUCKET_PICTURES}

Usuń tę funkcję:

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

Usuń kolekcję Firestore, wybierając z niej 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ą usługę kluczy w projekcie.

Omówione zagadnienia

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

Następne kroki