1. 개요
첫 번째 Codelab에서는 버킷에 사진을 업로드합니다. 이렇게 하면 함수에서 처리할 파일 생성 이벤트가 생성됩니다. 이 함수는 Vision API를 호출하여 이미지 분석을 수행하고 결과를 데이터 스토어에 저장합니다.
학습할 내용
- Cloud Storage
- Cloud Functions
- Cloud Vision API
- Cloud Firestore
2. 설정 및 요구사항
자습형 환경 설정
- Google Cloud Console에 로그인하여 새 프로젝트를 만들거나 기존 프로젝트를 재사용합니다. 아직 Gmail이나 Google Workspace 계정이 없는 경우 계정을 만들어야 합니다.
- 프로젝트 이름은 이 프로젝트 참가자의 표시 이름입니다. 이는 Google API에서 사용하지 않는 문자열이며 언제든지 업데이트할 수 있습니다.
- 프로젝트 ID는 모든 Google Cloud 프로젝트에서 고유해야 하며, 변경할 수 없습니다(설정된 후에는 변경할 수 없음). Cloud 콘솔이 고유한 문자열을 자동으로 생성합니다. 보통은 그게 뭔지 상관하지 않습니다. 대부분의 Codelab에서는 프로젝트 ID (일반적으로
PROJECT_ID
로 식별됨)를 참조해야 합니다. 생성된 ID가 마음에 들지 않으면 무작위로 다른 ID를 생성할 수 있습니다. 또는 직접 시도해 보고 사용 가능한지 확인할 수도 있습니다. 이 단계 이후에는 변경할 수 없으며 프로젝트 기간 동안 유지됩니다. - 참고로 세 번째 값은 일부 API에서 사용하는 프로젝트 번호입니다. 이 세 가지 값에 대한 자세한 내용은 문서를 참고하세요.
- 다음으로 Cloud 리소스/API를 사용하려면 Cloud 콘솔에서 결제를 사용 설정해야 합니다. 이 Codelab 실행에는 많은 비용이 들지 않습니다. 이 튜토리얼이 끝난 후에 요금이 청구되지 않도록 리소스를 종료하려면 만든 리소스를 삭제하거나 전체 프로젝트를 삭제하면 됩니다. Google Cloud 새 사용자에게는 미화 $300 상당의 무료 체험판 프로그램에 참여할 수 있는 자격이 부여됩니다.
Cloud Shell 시작
Google Cloud를 노트북에서 원격으로 실행할 수 있지만, 이 Codelab에서는 Cloud에서 실행되는 명령줄 환경인 Google Cloud Shell을 사용합니다.
Google Cloud Console의 오른쪽 상단 툴바에 있는 Cloud Shell 아이콘을 클릭합니다.
환경을 프로비저닝하고 연결하는 데 몇 분 정도 소요됩니다. 완료되면 다음과 같이 표시됩니다.
가상 머신에는 필요한 개발 도구가 모두 들어있습니다. 영구적인 5GB 홈 디렉터리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 이 Codelab의 모든 작업은 브라우저 내에서 수행할 수 있습니다. 아무것도 설치할 필요가 없습니다.
3. API 사용 설정
이 실습에서는 Cloud Functions 및 Vision API를 사용하지만 먼저 Cloud 콘솔 또는 gcloud
에서 사용 설정해야 합니다.
Cloud 콘솔에서 Vision API를 사용 설정하려면 검색창에서 Cloud Vision API
를 검색합니다.
Cloud Vision API 페이지로 이동합니다.
ENABLE
버튼을 클릭합니다.
또는 gcloud 명령줄 도구를 사용하여 Cloud Shell에서 사용 설정할 수도 있습니다.
Cloud Shell 내에서 다음 명령어를 실행합니다.
gcloud services enable vision.googleapis.com
작업이 성공적으로 완료됩니다.
Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.
Cloud Functions도 사용 설정합니다.
gcloud services enable cloudfunctions.googleapis.com
4. 버킷 만들기 (콘솔)
사진을 저장할 스토리지 버킷을 만듭니다. Google Cloud Platform 콘솔(console.cloud.google.com)이나 로컬 개발 환경(Cloud Shell)의 gsutil 명령줄 도구를 사용하여 지정할 수 있습니다.
Storage로 이동
'햄버거'에서 (❯) 메뉴에서 Storage
페이지로 이동합니다.
버킷 이름 지정
CREATE BUCKET
버튼을 클릭합니다.
CONTINUE
아이콘을 클릭합니다.
위치 선택
원하는 리전 (여기 Europe
)에 멀티 리전 버킷을 만듭니다.
CONTINUE
아이콘을 클릭합니다.
기본 스토리지 클래스 선택
데이터의 Standard
스토리지 클래스를 선택합니다.
CONTINUE
아이콘을 클릭합니다.
액세스 제어 설정
공개적으로 액세스할 수 있는 이미지로 작업하므로 이 버킷에 저장된 모든 사진에 동일하게 균일한 액세스 제어를 적용하고자 합니다.
Uniform
액세스 제어 옵션을 선택합니다.
CONTINUE
아이콘을 클릭합니다.
보호/암호화 설정
자체 암호화 키를 사용하지 않으므로 기본값 (Google-managed key)
)을 유지합니다.
CREATE
를 클릭하여 버킷 만들기를 완료합니다.
allUsers를 스토리지 뷰어로 추가하기
Permissions
탭으로 이동합니다.
다음과 같이 Storage > Storage Object Viewer
역할의 allUsers
구성원을 버킷에 추가합니다.
SAVE
아이콘을 클릭합니다.
5. 버킷 만들기 (gsutil)
Cloud Shell의 gsutil
명령줄 도구를 사용하여 버킷을 만들 수도 있습니다.
Cloud Shell에서 고유한 버킷 이름의 변수를 설정합니다. Cloud Shell에 이미 고유한 프로젝트 ID로 설정된 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
버킷이 있습니다.
이전 단계에서 설명한 대로 버킷에 사진을 업로드할 수 있고 업로드된 사진이 공개적으로 사용 가능한지 테스트합니다.
6. 버킷에 대한 공개 액세스 테스트
스토리지 브라우저로 돌아가면 목록에 버킷이 '공개'로 되어 있는 것을 확인할 수 있습니다. (누구나 해당 버킷의 콘텐츠에 액세스할 수 있음을 알려주는 경고 표시 포함)
이제 버킷에서 사진을 수신할 준비가 되었습니다.
버킷 이름을 클릭하면 버킷 세부정보가 표시됩니다.
여기서 Upload files
버튼을 사용하여 버킷에 사진을 추가할 수 있는지 테스트할 수 있습니다. 파일을 선택하라는 파일 선택기 팝업이 표시됩니다. 선택하면 버킷에 업로드되고 이 새 파일에 자동으로 부여된 public
액세스 권한이 다시 표시됩니다.
Public
액세스 라벨 옆에 작은 링크 아이콘도 표시됩니다. 이미지를 클릭하면 브라우저가 이미지의 공개 URL로 이동합니다. URL 형식은 다음과 같습니다.
https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png
BUCKET_NAME
은 버킷에 대해 선택한 전역적으로 고유한 이름이고 사진의 파일 이름입니다.
사진 이름의 체크박스를 클릭하면 DELETE
버튼이 사용 설정되고 이 첫 번째 이미지를 삭제할 수 있습니다.
7. 함수 만들기
이 단계에서는 사진 업로드 이벤트에 반응하는 함수를 만듭니다.
Google Cloud 콘솔의 Cloud Functions
섹션으로 이동합니다. 이 페이지로 이동하면 Cloud Functions 서비스가 자동으로 사용 설정됩니다.
Create function
를 클릭합니다.
이름을 선택합니다 (예: picture-uploaded
) 및 리전 (버킷에 대한 리전 선택과 일치해야 함)을 지정할 수 있습니다.
함수에는 다음과 같은 두 가지 종류가 있습니다.
- URL (예: 웹 API)을 통해 호출할 수 있는 HTTP 함수
- 일부 이벤트에 의해 트리거될 수 있는 백그라운드 함수.
새 파일이 Cloud Storage
버킷에 업로드될 때 트리거되는 백그라운드 함수를 만들려고 합니다.
버킷에서 파일이 생성되거나 업데이트될 때 트리거되는 이벤트인 Finalize/Create
이벤트 유형에 관심이 있습니다.
이전에 만든 버킷을 선택하여 이 특정 버킷에서 파일이 생성 / 업데이트될 때 알림을 받도록 Cloud Functions에 알립니다.
Select
를 클릭하여 앞서 만든 버킷을 선택한 다음 Save
를 클릭합니다.
'다음'을 클릭하기 전에 런타임, 빌드, 연결, 보안 설정에서 기본값 (메모리 256MB)을 확장 및 수정하고 1GB로 업데이트할 수 있습니다.
Next
를 클릭하면 런타임, 소스 코드, 진입점을 조정할 수 있습니다.
이 함수의 Inline editor
를 유지합니다.
Java 런타임 중 하나(예: Java 11)를 선택합니다.
소스 코드는 Java
파일과 다양한 메타데이터와 종속 항목을 제공하는 pom.xml
Maven 파일로 구성됩니다.
기본 코드 스니펫은 그대로 둡니다. 업로드된 사진의 파일 이름을 기록합니다.
지금은 테스트 목적으로 Example
에 실행할 함수의 이름을 유지합니다.
Deploy
를 클릭하여 함수를 만들고 배포합니다. 배포에 성공하면 함수 목록에 녹색 원으로 표시된 체크표시가 나타납니다.
8. 함수 테스트
이 단계에서는 함수가 스토리지 이벤트에 응답하는지 테스트합니다.
'햄버거'에서 (❯) 메뉴에서 Storage
페이지로 다시 이동합니다.
이미지 버킷을 클릭한 다음 Upload files
를 클릭하여 이미지를 업로드합니다.
Cloud 콘솔 내에서 다시 탐색하여 Logging > Logs Explorer
페이지로 이동합니다.
Log Fields
선택기에서 Cloud Function
를 선택하여 함수와 관련된 로그를 확인합니다. 로그 필드까지 아래로 스크롤하면 특정 함수를 선택해 함수 관련 로그를 자세히 살펴볼 수도 있습니다. picture-uploaded
함수를 선택합니다.
함수 생성, 함수의 시작 및 종료 시간, 실제 로그 구문이 언급된 로그 항목이 표시됩니다.
로그 구문이 Processing file: pic-a-daily-architecture-events.png
입니다. 이 사진의 생성 및 저장과 관련된 이벤트가 예상대로 트리거되었음을 의미합니다.
9. 데이터베이스 준비
Vision API에서 제공한 사진에 대한 정보를 클라우드 기반의 빠르고 완전 관리형 서버리스 NoSQL 문서 데이터베이스인 Cloud Firestore 데이터베이스에 저장합니다. Cloud 콘솔의 Firestore
섹션으로 이동하여 데이터베이스를 준비합니다.
Native mode
또는 Datastore mode
, 두 가지 옵션이 제공됩니다. 오프라인 지원 및 실시간 동기화와 같은 추가 기능을 제공하는 네이티브 모드를 사용하세요.
SELECT NATIVE MODE
아이콘을 클릭합니다.
멀티 리전을 선택합니다 (여기서는 유럽이지만 최소한 함수 및 스토리지 버킷과 동일한 리전인 것이 좋습니다).
CREATE DATABASE
버튼을 클릭합니다.
데이터베이스가 생성되면 다음과 같이 표시됩니다.
+ START COLLECTION
버튼을 클릭하여 새 컬렉션을 만듭니다.
컬렉션 이름을 pictures
으로 지정합니다.
문서를 만들지 않아도 됩니다. 새 사진이 Cloud Storage에 저장되고 Vision API에서 분석되므로 이를 프로그래매틱 방식으로 추가합니다.
Save
아이콘을 클릭합니다.
Firestore는 새로 생성된 컬렉션에 첫 번째 기본 문서를 만듭니다. 이 문서는 유용한 정보를 포함하지 않으므로 안전하게 삭제할 수 있습니다.
컬렉션에서 프로그래매틱 방식으로 만들어지는 문서에는 4개의 필드가 포함됩니다.
- name (문자열): 업로드된 사진의 파일 이름으로, 문서의 키이기도 합니다.
- labels (문자열 배열): Vision API에서 인식된 항목의 라벨
- color (문자열): 주요 색상의 16진수 색상 코드입니다 (예: #ab12ef)는
- created (날짜): 이 이미지의 메타데이터가 저장된 시점의 타임스탬프
- thumbnail (불리언): 표시되고 이 사진의 미리보기 이미지가 생성된 경우 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 콘솔에서 왼쪽 탐색 열의 Indexes
를 클릭한 다음 아래와 같이 복합 색인을 만들어 생성할 수도 있습니다.
Create
아이콘을 클릭합니다. 색인을 만드는 데 몇 분 정도 걸릴 수 있습니다.
10. 함수 업데이트
Functions
페이지로 돌아가서 Vision API를 호출하여 사진을 분석하고 Firestore에 메타데이터를 저장하도록 함수를 업데이트합니다.
'햄버거'에서 (🏠) 메뉴에서 Cloud Functions
섹션으로 이동하여 함수 이름을 클릭하고 Source
탭을 선택한 다음 EDIT
버튼을 클릭합니다.
먼저 Java 함수의 종속 항목을 나열하는 pom.xml
파일을 수정합니다. 코드를 업데이트하여 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;
}
}
11. 함수 살펴보기
다양하고 흥미로운 부분을 자세히 살펴보겠습니다.
먼저 Maven pom.xml
파일에 특정 종속 항목을 포함합니다. Google 자바 클라이언트 라이브러리는 Bill-of-Materials(BOM)
를 게시하여 종속 항목 충돌을 제거합니다. 이 방법을 사용하면 개별 Google 클라이언트 라이브러리의 버전을 지정하지 않아도 됩니다.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>26.1.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
그런 다음 Vision API를 사용할 클라이언트를 준비합니다.
...
try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
...
이제 함수의 구조입니다. 수신 이벤트에서 관심 있는 필드를 캡처하여 정의한 GCSEvent 구조에 매핑합니다.
...
public class ImageAnalysis implements BackgroundFunction<GCSEvent> {
@Override
public void accept(GCSEvent event, Context context)
throws IOException, InterruptedException,
ExecutionException {
...
public static class GCSEvent {
String bucket;
String name;
}
서명을 확인하고 Cloud 함수를 트리거한 파일 및 버킷의 이름을 검색하는 방법도 확인합니다.
참고로 이벤트 페이로드는 다음과 같습니다.
{
"bucket":"uploaded-pictures",
"contentType":"image/png",
"crc32c":"efhgyA==",
"etag":"CKqB956MmucCEAE=",
"generation":"1579795336773802",
"id":"uploaded-pictures/Screenshot.png/1579795336773802",
"kind":"storage#object",
"md5Hash":"PN8Hukfrt6C7IyhZ8d3gfQ==",
"mediaLink":"https://www.googleapis.com/download/storage/v1/b/uploaded-pictures/o/Screenshot.png?generation=1579795336773802&alt=media",
"metageneration":"1",
"name":"Screenshot.png",
"selfLink":"https://www.googleapis.com/storage/v1/b/uploaded-pictures/o/Screenshot.png",
"size":"173557",
"storageClass":"STANDARD",
"timeCreated":"2020-01-23T16:02:16.773Z",
"timeStorageClassUpdated":"2020-01-23T16:02:16.773Z",
"updated":"2020-01-23T16:02:16.773Z"
}
Vision 클라이언트를 통해 전송할 요청을 준비합니다.
ImageSource imageSource = ImageSource.newBuilder()
.setGcsImageUri("gs://" + bucketName + "/" + fileName)
.build();
Image image = Image.newBuilder()
.setSource(imageSource)
.build();
Feature featureLabel = Feature.newBuilder()
.setType(Type.LABEL_DETECTION)
.build();
Feature featureImageProps = Feature.newBuilder()
.setType(Type.IMAGE_PROPERTIES)
.build();
Feature featureSafeSearch = Feature.newBuilder()
.setType(Type.SAFE_SEARCH_DETECTION)
.build();
AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
.addFeatures(featureLabel)
.addFeatures(featureImageProps)
.addFeatures(featureSafeSearch)
.setImage(image)
.build();
Vision API에는 3가지 주요 기능이 필요합니다.
- 라벨 인식: 사진의 내용 파악
- 이미지 속성: 사진에 흥미로운 속성을 제공하기 위해 (Google은 사진의 주요 색상에 관심이 있음)
- 세이프서치: 이미지가 표시해도 안전한지 확인 (성인 / 의료적 / 선정적인 / 폭력적인 콘텐츠가 포함되지 않아야 함)
이 시점에서 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 block이 있는 이유는 다음과 같습니다.
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 스타일시트에서 사용할 수 있는 16진수 색상 코드로 변환하는 유틸리티 함수를 사용하고 있습니다.
사진을 표시해도 안전한지 확인해 보겠습니다.
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. 함수 배포하기
함수를 배포할 시간입니다.
DEPLOY
버튼을 누르면 새 버전이 배포되고 진행 상황을 확인할 수 있습니다.
13. 함수 다시 테스트
함수가 성공적으로 배포되면 Cloud Storage에 사진을 게시하고, 함수가 호출되는지, Vision API가 반환하는지, 메타데이터가 Firestore에 저장되었는지 확인합니다.
Cloud Storage
로 돌아가서 실습 시작 부분에서 생성한 버킷을 클릭합니다.
버킷 세부정보 페이지에서 Upload files
버튼을 클릭하여 사진을 업로드합니다.
'햄버거'에서 (🏠) 메뉴에서 Logging > Logs
탐색기로 이동합니다.
Log Fields
선택기에서 Cloud Function
를 선택하여 함수와 관련된 로그를 확인합니다. 로그 필드까지 아래로 스크롤하면 특정 함수를 선택해 함수 관련 로그를 자세히 살펴볼 수도 있습니다. picture-uploaded
함수를 선택합니다.
그리고 로그 목록에서 함수가 호출된 것을 확인할 수 있습니다.
로그는 함수 실행의 시작과 끝을 나타냅니다. 그 중간에 console.log() 문을 사용하여 함수에 넣은 로그를 볼 수 있습니다. 다음과 같은 내용을 확인했습니다.
- 함수를 트리거하는 이벤트의 세부정보,
- Vision API 호출의 원시 결과
- 우리가 업로드한 사진에 있는 라벨은
- 주요 색상 정보는
- 사진이 표시해도 안전한지
- 그리고 사진에 대한 메타데이터는 결국 Firestore에 저장되었습니다.
다시 '햄버거'에서 (❯) 메뉴에서 Firestore
섹션으로 이동합니다. Data
하위 섹션 (기본적으로 표시됨)에 방금 업로드한 사진에 해당하는 새 문서가 추가된 pictures
컬렉션이 표시됩니다.
14. 정리(선택 사항)
이 시리즈의 다른 실습을 계속하지 않으려는 경우 리소스를 삭제하여 비용을 절감하고 전반적으로 클라우드를 잘 활용할 수 있습니다. 다음과 같이 리소스를 개별적으로 삭제할 수 있습니다.
버킷을 삭제합니다.
gsutil rb gs://${BUCKET_PICTURES}
함수를 삭제합니다.
gcloud functions delete picture-uploaded --region europe-west1 -q
컬렉션에서 컬렉션 삭제를 선택하여 Firestore 컬렉션을 삭제합니다.
또는 전체 프로젝트를 삭제할 수 있습니다.
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
15. 축하합니다.
축하합니다. 프로젝트의 첫 번째 키 관리 서비스를 구현했습니다.
학습한 내용
- Cloud Storage
- Cloud Functions
- Cloud Vision API
- Cloud Firestore