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

1. Обзор

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

d650ca5386ea71ad.png

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

  • Облачное хранилище
  • Облачные функции
  • API Cloud Vision
  • Облачный Firestore

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

Настройка среды для самостоятельного обучения

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

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

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

Запустить Cloud Shell

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

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

55efc1aaa7a4d3ad.png

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

7ffe5cbb04455448.png

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

3. Включите API.

Для этой лабораторной работы вы будете использовать Cloud Functions и Vision API, но сначала их необходимо включить либо в Cloud Console, либо с помощью gcloud .

Чтобы включить Vision API в Cloud Console, введите Cloud Vision API в строку поиска:

cf48b1747ba6a6fb.png

Вы попадете на страницу API Cloud Vision:

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)

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

В 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 файла и Maven-файла pom.xml , который предоставляет различные метаданные и зависимости.

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

9b7b9801b42f6ca6.png

Пока что, для целей тестирования, оставьте имя выполняемой функции как Example .

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

3732fdf409eefd1a.png

8. Проверьте функцию.

На этом этапе проверьте, реагирует ли функция на события, связанные с хранилищем данных.

Из меню-гамбургера (☰) вернитесь на страницу « Storage .

Нажмите на раздел изображений, а затем на Upload files , чтобы загрузить изображение.

21767ec3cb8b18de.png

В консоли облака снова перейдите на страницу Logging > Logs Explorer .

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

В журнале должны отобразиться записи, содержащие информацию о создании функции, времени её начала и окончания, а также сам текст сообщения:

e8ba7d39c36df36c.png

В нашем журнале событий указано: Processing file: pic-a-daily-architecture-events.png , что означает, что событие, связанное с созданием и сохранением этого изображения, действительно было запущено, как и ожидалось.

9. Подготовьте базу данных.

Вы будете сохранять информацию об изображении, полученном с помощью Vision API, в базу данных Cloud Firestore — быструю, полностью управляемую, бессерверную, облачную документоориентированную базу данных NoSQL. Подготовьте свою базу данных, перейдя в раздел Firestore в консоли Cloud Console:

9e4708d2257de058.png

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

Нажмите кнопку SELECT NATIVE MODE .

9449ace8cc84de43.png

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

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

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

56265949a124819e.png

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

pictures из коллекции имен.

75806ee24c4e13a7.png

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

Нажмите « Save ».

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

5c2f1e17ea47f48f.png

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

  • имя (строка): имя файла загруженного изображения, которое также является ключом документа.
  • метки (массив строк): метки распознанных элементов в 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

Или же вы можете сделать это из консоли Cloud Console, щелкнув Indexes в левой навигационной колонке, а затем создав составной индекс, как показано ниже:

ecb8b95e3c791272.png

Нажмите Create . Создание индекса может занять несколько минут.

10. Обновите функцию

Вернитесь на страницу Functions , чтобы обновить функцию, которая будет вызывать API Vision для анализа наших изображений и сохранять метаданные в 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 Client Libraries публикуют спецификацию Bill-of-Materials(BOM) , чтобы исключить любые конфликты зависимостей. Используя её, вам не нужно указывать версию для отдельных библиотек Google Client Libraries.

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

Мы запрашиваем 3 ключевые возможности Vision API:

  • Распознавание меток : чтобы понять, что изображено на этих картинках.
  • Свойства изображения : для придания изображению интересных характеристик (нас интересует преобладающий цвет).
  • Безопасный поиск : чтобы узнать, безопасно ли показывать изображение (оно не должно содержать материалы для взрослых / медицинского характера / откровенные / жестокие).

На этом этапе мы можем обратиться к API Vision:

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

Для справки, вот как выглядит ответ от Vision API:

{
  "faceAnnotations": [],
  "landmarkAnnotations": [],
  "logoAnnotations": [],
  "labelAnnotations": [
    {
      "locations": [],
      "properties": [],
      "mid": "/m/01yrx",
      "locale": "",
      "description": "Cat",
      "score": 0.9959855675697327,
      "confidence": 0,
      "topicality": 0.9959855675697327,
      "boundingPoly": null
    },
     - - - 
  ],
  "textAnnotations": [],
  "localizedObjectAnnotations": [],
  "safeSearchAnnotation": {
    "adult": "VERY_UNLIKELY",
    "spoof": "UNLIKELY",
    "medical": "VERY_UNLIKELY",
    "violence": "VERY_UNLIKELY",
    "racy": "VERY_UNLIKELY",
    "adultConfidence": 0,
    "spoofConfidence": 0,
    "medicalConfidence": 0,
    "violenceConfidence": 0,
    "racyConfidence": 0,
    "nsfwConfidence": 0
  },
  "imagePropertiesAnnotation": {
    "dominantColors": {
      "colors": [
        {
          "color": {
            "red": 203,
            "green": 201,
            "blue": 201,
            "alpha": null
          },
          "score": 0.4175916016101837,
          "pixelFraction": 0.44456374645233154
        },
         - - - 
      ]
    }
  },
  "error": null,
  "cropHintsAnnotation": {
    "cropHints": [
      {
        "boundingPoly": {
          "vertices": [
            { "x": 0, "y": 118 },
            { "x": 1177, "y": 118 },
            { "x": 1177, "y": 783 },
            { "x": 0, "y": 783 }
          ],
          "normalizedVertices": []
        },
        "confidence": 0.41695669293403625,
        "importanceFraction": 1
      }
    ]
  },
  "fullTextAnnotation": null,
  "webDetection": null,
  "productSearchResults": null,
  "context": null
}

Если ошибка не возвращается, мы можем продолжить, поэтому у нас и есть этот блок 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(). Мы видим:

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

9ff7956a215c15da.png

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

a6137ab9687da370.png

14. Уборка (необязательно)

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

Удалите корзину:

gsutil rb gs://${BUCKET_PICTURES}

Удалите функцию:

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

Удалите коллекцию Firestore, выбрав пункт «Удалить коллекцию» в списке коллекций:

410b551c3264f70a.png

В качестве альтернативы вы можете удалить весь проект:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

15. Поздравляем!

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

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

  • Облачное хранилище
  • Облачные функции
  • API Cloud Vision
  • Облачный Firestore

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