Pic-a-daily: Лабораторная работа 1 — Хранение и анализ изображений (Java)

1. Обзор

В первой лабораторной работе с кодом вы будете загружать изображения в корзину. Это создаст событие создания файла, которое будет обработано функцией. Функция выполнит вызов Vision API для анализа изображения и сохранения результатов в хранилище данных.

d650ca5386ea71ad.png

Что вы узнаете

  • Облачное хранилище
  • Облачные функции
  • API облачного видения
  • Облачный пожарный магазин

2. Настройка и требования

Самостоятельная настройка среды

  1. Войдите в Google Cloud Console и создайте новый проект или повторно используйте существующий. Если у вас еще нет учетной записи Gmail или Google Workspace, вам необходимо ее создать .

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • Имя проекта — это отображаемое имя для участников этого проекта. Это строка символов, не используемая API Google. Вы можете обновить его в любое время.
  • Идентификатор проекта должен быть уникальным для всех проектов Google Cloud и неизменяемым (нельзя изменить после его установки). Cloud Console автоматически генерирует уникальную строку; обычно тебя не волнует, что это такое. В большинстве лабораторий кода вам потребуется указать идентификатор проекта (обычно он обозначается как PROJECT_ID ). Если вам не нравится сгенерированный идентификатор, вы можете создать другой случайный идентификатор. Кроме того, вы можете попробовать свой собственный и посмотреть, доступен ли он. Его нельзя изменить после этого шага, и он останется в силе на протяжении всего проекта.
  • К вашему сведению, есть третье значение — номер проекта , который используют некоторые API. Подробнее обо всех трех этих значениях читайте в документации .
  1. Затем вам необходимо включить выставление счетов в Cloud Console, чтобы использовать облачные ресурсы/API. Прохождение этой лаборатории кода не должно стоить много, если вообще стоит. Чтобы отключить ресурсы и не взимать плату за пределами этого руководства, вы можете удалить созданные вами ресурсы или удалить весь проект. Новые пользователи Google Cloud имеют право на участие в программе бесплатной пробной версии стоимостью 300 долларов США .

Запустить Cloud Shell

Хотя Google Cloud можно управлять удаленно с вашего ноутбука, в этой лаборатории вы будете использовать Google Cloud Shell , среду командной строки, работающую в облаке.

В Google Cloud Console щелкните значок Cloud Shell на верхней правой панели инструментов:

55efc1aaa7a4d3ad.png

Подготовка и подключение к среде займет всего несколько минут. Когда все будет готово, вы должны увидеть что-то вроде этого:

7ffe5cbb04455448.png

Эта виртуальная машина оснащена всеми необходимыми инструментами разработки. Он предлагает постоянный домашний каталог объемом 5 ГБ и работает в Google Cloud, что значительно повышает производительность сети и аутентификацию. Всю работу в этой лаборатории кода можно выполнять в браузере. Вам не нужно ничего устанавливать.

3. Включите API

В этом практическом занятии вы будете использовать 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 , чтобы окончательно завершить создание корзины.

Добавить allUsers в качестве средства просмотра хранилища

Перейдите на вкладку Permissions :

d0ecfdcff730ea51.png

Добавьте в корзину член allUsers с ролью Storage > Storage Object Viewer , как показано ниже:

e9f25ec1ea0b6cc6.png

Нажмите SAVE .

5. Создайте корзину (gsutil).

Вы также можете использовать инструмент командной строки gsutil в Cloud Shell для создания сегментов.

В Cloud Shell задайте переменную для уникального имени сегмента. В Cloud Shell уже указан уникальный идентификатор вашего проекта GOOGLE_CLOUD_PROJECT . Вы можете добавить это к имени корзины.

Например:

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-адрес (т. е. веб-API),
  • Фоновые функции, которые могут быть вызваны каким-либо событием.

Вы хотите создать фоновую функцию, которая срабатывает при загрузке нового файла в нашу корзину 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 облачной консоли:

9e4708d2257de058.png

Предлагаются два варианта: Native mode или Datastore mode . Используйте собственный режим, который предлагает дополнительные функции, такие как автономная поддержка и синхронизация в реальном времени.

Нажмите SELECT NATIVE MODE .

9449ace8cc84de43.png

Выберите мультирегион (здесь, в Европе, но в идеале по крайней мере тот же регион, в котором находятся ваша функция и сегмент хранилища).

Нажмите кнопку CREATE DATABASE .

После создания базы данных вы должны увидеть следующее:

56265949a124819e.png

Создайте новую коллекцию , нажав кнопку + START COLLECTION .

Назовите pictures из коллекции.

75806ee24c4e13a7.png

Вам не нужно создавать документ. Вы будете добавлять их программно по мере того, как новые изображения сохраняются в облачном хранилище и анализируются Vision API.

Нажмите Save .

Firestore создает первый документ по умолчанию во вновь созданной коллекции. Вы можете безопасно удалить этот документ, поскольку он не содержит никакой полезной информации:

5c2f1e17ea47f48f.png

Документы, которые будут созданы программно в нашей коллекции, будут содержать 4 поля:

  • имя (строка): имя файла загруженного изображения, которое также является ключом документа.
  • labels (массив строк): метки элементов, распознанных Vision API.
  • цвет (строка): шестнадцатеричный код доминирующего цвета (например, #ab12ef).
  • создано (дата): временная отметка, когда метаданные этого изображения были сохранены.
  • миниатюра (логическое значение): необязательное поле, которое будет присутствовать и иметь значение true, если для этого изображения было создано миниатюрное изображение.

Поскольку мы будем искать в 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

Или вы также можете сделать это из облачной консоли, нажав 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. Изучите функцию

Давайте подробнее рассмотрим различные интересные части.

Сначала мы включаем конкретные зависимости в файл pom.xml Maven. Клиентские библиотеки Google Java публикуют Bill-of-Materials(BOM) для устранения любых конфликтов зависимостей. Используя его, вам не нужно указывать какую-либо версию для отдельных клиентских библиотек Google.

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>libraries-bom</artifactId>
        <version>26.1.1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

Затем готовим клиент для Vision API:

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

Теперь поговорим о структуре нашей функции. Мы извлекаем из входящего события интересующие нас поля и сопоставляем их со структурой GCSEvent, которую мы определяем:

...
public class ImageAnalysis implements BackgroundFunction<GCSEvent> {
    @Override
    public void accept(GCSEvent event, Context context) 
            throws IOException, InterruptedException,     
    ExecutionException {
...

    public static class GCSEvent {
        String bucket;
        String name;
    }

Обратите внимание на подпись, а также на то, как мы получаем имя файла и сегмента, которые активировали функцию облака.

Для справки, вот как выглядит полезная нагрузка события:

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

Мы просим реализовать три ключевые возможности 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 .

В селекторе Log Fields выберите Cloud Function , чтобы просмотреть журналы, посвященные вашим функциям. Прокрутите вниз поля журнала, и вы даже можете выбрать конкретную функцию, чтобы получить более детальное представление журналов, связанных с функциями. Выберите функцию picture-uploaded .

b651dca7e25d5b11.png

И действительно, в списке логов я вижу, что наша функция была вызвана:

d22a7f24954e4f63.png

В журналах указывается начало и окончание выполнения функции. А между ними мы можем видеть журналы, которые мы помещаем в нашу функцию, с помощью операторов console.log(). Мы видим:

  • Подробности события, запускающего нашу функцию,
  • Необработанные результаты вызова Vision API,
  • Этикетки, которые были найдены на загруженной нами картинке,
  • Информация о доминирующих цветах,
  • Безопасно ли показывать изображение,
  • И в конечном итоге эти метаданные об изображении были сохранены в Firestore.

9ff7956a215c15da.png

Снова из меню «гамбургер» (☰) перейдите в раздел Firestore . В подразделе Data (отображается по умолчанию) вы должны увидеть коллекцию pictures с добавленным новым документом, соответствующим только что загруженному изображению:

а6137ab9687da370.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. Поздравляем!

Поздравляем! Вы успешно реализовали первый ключевой сервис проекта!

Что мы рассмотрели

  • Облачное хранилище
  • Облачные функции
  • API облачного видения
  • Облачный пожарный магазин

Следующие шаги