├── .flake8 ├── src ├── gcf │ ├── requirements-dev.txt │ ├── requirements.txt │ └── tests │ │ ├── test_storage_methods.py │ │ ├── test_ui_methods.py │ │ └── test_vision_methods.py └── frontend │ ├── .env.production │ ├── public │ ├── favicon.ico │ └── vite.svg │ ├── tsconfig.node.json │ ├── .gitignore │ ├── nginx.conf │ ├── src │ ├── vite-env.d.ts │ ├── index.css │ ├── main.tsx │ ├── AppConstants.ts │ ├── App.css │ ├── tests │ │ └── components │ │ │ ├── SafeSearchResultView.test.tsx │ │ │ ├── BoundingBox.test.tsx │ │ │ ├── LabelDetectionResultView.test.tsx │ │ │ ├── ConfidenceLabelRow.test.tsx │ │ │ ├── ImageSourceToggleSelection.test.tsx │ │ │ ├── FeatureToggleSelection.test.tsx │ │ │ ├── ImagePropertiesResultView.test.tsx │ │ │ ├── App.test.tsx │ │ │ ├── ObjectDetectionResultView.test.tsx │ │ │ ├── ImageWithBoundingBoxes.test.tsx │ │ │ ├── UnifiedImageSelector.test.tsx │ │ │ ├── ResultsContainer.test.tsx │ │ │ ├── FaceAnnotationsResultView.test.tsx │ │ │ └── StickyHeadTable.test.tsx │ ├── components │ │ ├── results │ │ │ ├── ConfidenceLabelRow.tsx │ │ │ ├── LabelDetectionResultView.tsx │ │ │ ├── FaceAnnotationsResultView.tsx │ │ │ ├── ImagePropertiesResultView.tsx │ │ │ ├── SafeSearchResultView.tsx │ │ │ └── ObjectDetectionResultView.tsx │ │ ├── selection │ │ │ ├── ImageSourceToggleSelection.tsx │ │ │ ├── FeatureToggleSelection.tsx │ │ │ ├── CloudImageSelector.tsx │ │ │ └── UnifiedImageSelector.tsx │ │ ├── Alert.tsx │ │ ├── ImageWithBoundingBoxes.tsx │ │ └── ResultsContainer.tsx │ ├── assets │ │ └── react.svg │ └── queries.ts │ ├── tsconfig.json │ ├── postcss.config.js │ ├── vite.config.ts │ ├── tailwind.config.js │ ├── index.html │ ├── .eslintrc.cjs │ ├── Dockerfile │ ├── vitest.config.ts │ ├── README.md │ └── package.json ├── assets └── architecture.png ├── infra ├── test │ ├── integration │ │ ├── simple_example │ │ │ └── testfile │ │ │ │ └── TestImage.jpg │ │ ├── discover_test.go │ │ └── go.mod │ └── setup │ │ ├── outputs.tf │ │ ├── versions.tf │ │ ├── variables.tf │ │ ├── main.tf │ │ └── iam.tf ├── modules │ ├── storage │ │ ├── README.md │ │ ├── versions.tf │ │ ├── variables.tf │ │ ├── output.tf │ │ ├── metadata.display.yaml │ │ ├── main.tf │ │ └── metadata.yaml │ └── cloudfunctions │ │ ├── versions.tf │ │ ├── README.md │ │ ├── output.tf │ │ ├── variables.tf │ │ ├── metadata.display.yaml │ │ └── metadata.yaml ├── examples │ └── simple_example │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── README.md │ │ └── outputs.tf ├── templates │ └── startup_script.tftpl ├── provider.tf ├── output.tf ├── metadata.display.yaml ├── README.md ├── variables.tf ├── Makefile ├── main.tf ├── metadata.yaml └── CONTRIBUTING.md ├── .github ├── renovate.json ├── release-please.yml ├── trusted-contribution.yml └── workflows │ ├── stale.yml │ ├── frontend.yml │ ├── backend.yml │ ├── lint.yaml │ └── periodic-reporter.yaml ├── SECURITY.md ├── .gitignore ├── CODEOWNERS ├── CONTRIBUTING.md ├── README.md └── CHANGELOG.md /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | -------------------------------------------------------------------------------- /src/gcf/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-mock 3 | -------------------------------------------------------------------------------- /src/frontend/.env.production: -------------------------------------------------------------------------------- 1 | VITE_API_SERVER= 2 | -------------------------------------------------------------------------------- /assets/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/HEAD/assets/architecture.png -------------------------------------------------------------------------------- /src/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/HEAD/src/frontend/public/favicon.ico -------------------------------------------------------------------------------- /infra/test/integration/simple_example/testfile/TestImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/HEAD/infra/test/integration/simple_example/testfile/TestImage.jpg -------------------------------------------------------------------------------- /src/gcf/requirements.txt: -------------------------------------------------------------------------------- 1 | functions-framework==3.* 2 | google-cloud-vision==3.7.* 3 | google-cloud-storage==2.16.* 4 | google-cloud-logging 5 | opentelemetry-api 6 | opentelemetry-sdk 7 | opentelemetry-exporter-gcp-trace 8 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>GoogleCloudPlatform/cloud-foundation-toolkit//infra/terraform/test-org/github/resources/renovate"] 4 | } 5 | -------------------------------------------------------------------------------- /src/frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). 2 | We use g.co/vulnz for our intake, and do coordination and disclosure here on 3 | GitHub (including using GitHub Security Advisory). The Google Security Team will 4 | respond within 5 working days of your report on g.co/vulnz. 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .project 3 | .idea 4 | *.DS_Store 5 | 6 | # Other local files 7 | *.pyc 8 | *.zip 9 | out/** 10 | temp/** 11 | # Terraform files 12 | **/.terraform/* 13 | terraform/**/*.log 14 | terraform/**/*manifest.json 15 | terraform/entire-tf-output 16 | **/entire-tf-output 17 | *.tfstate 18 | *.tfstate.* 19 | .terraform.lock.hcl 20 | env/ 21 | -------------------------------------------------------------------------------- /src/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen $PORT; 3 | server_name localhost; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html index.htm; 8 | try_files $uri /index.html; 9 | } 10 | 11 | gzip on; 12 | gzip_vary on; 13 | gzip_min_length 10240; 14 | gzip_proxied expired no-cache no-store private auth; 15 | gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; 16 | gzip_disable "MSIE [1-6]\."; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /infra/modules/storage/README.md: -------------------------------------------------------------------------------- 1 | # storage module 2 | 3 | 4 | ## Inputs 5 | 6 | | Name | Description | Type | Default | Required | 7 | |------|-------------|------|---------|:--------:| 8 | | gcf\_location | GCS deployment region. | `string` | n/a | yes | 9 | | labels | A map of key/value label pairs to assign to the resources. | `map(string)` | n/a | yes | 10 | 11 | ## Outputs 12 | 13 | | Name | Description | 14 | |------|-------------| 15 | | gcs\_annotations | Output GCS bucket name. | 16 | | gcs\_input | Input GCS bucket name. | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /// 18 | -------------------------------------------------------------------------------- /src/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @tailwind base; 18 | @tailwind components; 19 | @tailwind utilities; 20 | -------------------------------------------------------------------------------- /src/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "paths": { 5 | "@/*": ["*"] 6 | }, 7 | "target": "ESNext", 8 | "useDefineForClassFields": true, 9 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": false, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /.github/release-please.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | releaseType: terraform-module 16 | handleGHRelease: true 17 | primaryBranch: main 18 | bumpMinorPreMajor: true 19 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # NOTE: This file is automatically generated from values at: 2 | # https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/blob/main/infra/terraform/test-org/org/locals.tf 3 | 4 | * @GoogleCloudPlatform/blueprint-solutions @balajismaniam @donmccasland @ivanmkc @xsxm @GoogleCloudPlatform/dee-data-ai @GoogleCloudPlatform/jump-start-solutions-admins 5 | 6 | # NOTE: GitHub CODEOWNERS locations: 7 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-and-branch-protection 8 | 9 | CODEOWNERS @GoogleCloudPlatform/blueprint-solutions 10 | .github/CODEOWNERS @GoogleCloudPlatform/blueprint-solutions 11 | docs/CODEOWNERS @GoogleCloudPlatform/blueprint-solutions 12 | 13 | -------------------------------------------------------------------------------- /infra/examples/simple_example/main.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module "simple" { 18 | source = "../../" 19 | project_id = var.project_id 20 | } 21 | -------------------------------------------------------------------------------- /src/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export default { 18 | plugins: { 19 | tailwindcss: {}, 20 | autoprefixer: {}, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /infra/examples/simple_example/variables.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | variable "project_id" { 18 | description = "GCP project for provisioning cloud resources." 19 | } 20 | -------------------------------------------------------------------------------- /infra/modules/storage/versions.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | terraform { 18 | required_version = ">= 1.5" 19 | required_providers { 20 | google = { 21 | source = "hashicorp/google" 22 | version = "~> 4.52" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /infra/test/integration/discover_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package test 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" 21 | ) 22 | 23 | func TestAll(t *testing.T) { 24 | tft.AutoDiscoverAndTest(t) 25 | } 26 | -------------------------------------------------------------------------------- /infra/templates/startup_script.tftpl: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | sed -i "s/$(echo JGROUP_BUCKET | sed -e 's/\([[\/.*]\|\]\)/\\&/g')/$(echo ${jgroup_bucket_name} | sed -e 's/[\/&]/\\&/g')/g" /usr/lib/xwiki/WEB-INF/observation/remote/jgroups/tcp.xml 4 | sed -i "s/$(echo ACCESS_KEY | sed -e 's/\([[\/.*]\|\]\)/\\&/g')/$(echo ${jgroup_bucket_access_key} | sed -e 's/[\/&]/\\&/g')/g" /usr/lib/xwiki/WEB-INF/observation/remote/jgroups/tcp.xml 5 | sed -i "s/$(echo SECRET_KEY | sed -e 's/\([[\/.*]\|\]\)/\\&/g')/$(echo ${jgroup_bucket_secret_key} | sed -e 's/[\/&]/\\&/g')/g" /usr/lib/xwiki/WEB-INF/observation/remote/jgroups/tcp.xml 6 | 7 | DB_PASS="$(gcloud secrets versions access --secret ${xwiki_db_password_secret} latest --project ${gcp_project})" 8 | 9 | bash /home/xwiki_startup.sh "${db_ip}" "${xwiki_db_username}" "$${DB_PASS}" "${file_store_ip}" 10 | bash /home/xwiki_deploy_flavor.sh "${db_ip}" "${xwiki_db_username}" "$${DB_PASS}" "${file_store_ip}" 11 | -------------------------------------------------------------------------------- /infra/modules/storage/variables.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | variable "gcf_location" { 18 | description = "GCS deployment region." 19 | type = string 20 | } 21 | 22 | variable "labels" { 23 | description = "A map of key/value label pairs to assign to the resources." 24 | type = map(string) 25 | } 26 | -------------------------------------------------------------------------------- /src/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { defineConfig } from "vite"; 18 | import react from "@vitejs/plugin-react"; 19 | import tsconfigPaths from "vite-tsconfig-paths"; 20 | 21 | // https://vitejs.dev/config/ 22 | export default defineConfig({ 23 | plugins: [react(), tsconfigPaths()], 24 | }); 25 | -------------------------------------------------------------------------------- /infra/test/setup/outputs.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | output "project_id" { 18 | value = module.project.project_id 19 | } 20 | 21 | output "sa_key" { 22 | value = google_service_account_key.int_test.private_key 23 | sensitive = true 24 | } 25 | 26 | output "delete_contents_on_destroy" { 27 | value = true 28 | } 29 | -------------------------------------------------------------------------------- /src/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** @type {import('tailwindcss').Config} */ 18 | export default { 19 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 20 | theme: { 21 | extend: {}, 22 | }, 23 | plugins: [require("daisyui")], 24 | daisyui: { 25 | themes: ["emerald"], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from "react"; 18 | import ReactDOM from "react-dom/client"; 19 | import App from "./App"; 20 | import "./index.css"; 21 | 22 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 23 | 24 | 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /src/frontend/src/AppConstants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const OBJECT_LOCALIZATION = "OBJECT_LOCALIZATION"; 18 | export const LABEL_DETECTION = "LABEL_DETECTION"; 19 | export const IMAGE_PROPERTIES = "IMAGE_PROPERTIES"; 20 | export const SAFE_SEARCH_DETECTION = "SAFE_SEARCH_DETECTION"; 21 | export const FACE_DETECTION = "FACE_DETECTION"; 22 | -------------------------------------------------------------------------------- /infra/modules/storage/output.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | output "gcs_input" { 19 | description = "Input GCS bucket name." 20 | value = google_storage_bucket.vision-input.name 21 | } 22 | 23 | output "gcs_annotations" { 24 | description = "Output GCS bucket name." 25 | value = google_storage_bucket.vision-annotations.name 26 | } 27 | -------------------------------------------------------------------------------- /infra/test/setup/versions.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | terraform { 18 | required_version = ">= 1.5" 19 | required_providers { 20 | google = { 21 | source = "hashicorp/google" 22 | version = "~> 5.29" 23 | } 24 | google-beta = { 25 | source = "hashicorp/google-beta" 26 | version = "~> 5.29" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /infra/modules/cloudfunctions/versions.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | terraform { 18 | required_version = ">= 1.5" 19 | required_providers { 20 | google = { 21 | source = "hashicorp/google" 22 | version = "~> 4.52" 23 | } 24 | archive = { 25 | source = "hashicorp/archive" 26 | version = "~> 2.3" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /infra/test/setup/variables.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | variable "org_id" { 18 | description = "The numeric organization id" 19 | } 20 | 21 | variable "folder_id" { 22 | description = "The folder to deploy in" 23 | } 24 | 25 | variable "billing_account" { 26 | description = "The billing account id associated with the project, e.g. XXXXXX-YYYYYY-ZZZZZZ" 27 | } 28 | -------------------------------------------------------------------------------- /infra/examples/simple_example/README.md: -------------------------------------------------------------------------------- 1 | # Simple Example 2 | 3 | 4 | ## Inputs 5 | 6 | | Name | Description | Type | Default | Required | 7 | |------|-------------|------|---------|:--------:| 8 | | project\_id | GCP project for provisioning cloud resources. | `any` | n/a | yes | 9 | 10 | ## Outputs 11 | 12 | | Name | Description | 13 | |------|-------------| 14 | | annotate\_gcs\_function\_name | The name of the cloud function that annotates an image triggered by a GCS event. | 15 | | annotate\_http\_function\_name | The name of the cloud function that annotates an image triggered by an HTTP request. | 16 | | source\_code\_url | The URL of the source code for Cloud Functions. | 17 | | vision\_annotations\_gcs | Cloud Storage of the vision annotations | 18 | | vision\_input\_gcs | Cloud Storage of the vision input | 19 | | vision\_prediction\_url | The URL for requesting online prediction with HTTP request. | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/trusted-contribution.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2023-2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # NOTE: This file is automatically generated from: 16 | # https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/blob/main/infra/terraform/test-org/github 17 | 18 | annotations: 19 | - type: comment 20 | text: "/gcbrun" 21 | trustedContributors: 22 | - release-please[bot] 23 | - renovate[bot] 24 | - renovate-bot 25 | - forking-renovate[bot] 26 | - dependabot[bot] 27 | -------------------------------------------------------------------------------- /src/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Image Processing on GCP Demo 24 | 25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | env: { 19 | browser: true, 20 | es2021: true 21 | }, 22 | extends: [ 23 | 'plugin:react/recommended', 24 | 'standard-with-typescript', 25 | 'prettier' 26 | ], 27 | overrides: [ 28 | ], 29 | parserOptions: { 30 | ecmaVersion: 'latest', 31 | sourceType: 'module' 32 | }, 33 | plugins: [ 34 | 'react' 35 | ], 36 | rules: { 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM node:20-alpine as react-build 16 | WORKDIR /app 17 | COPY . ./ 18 | RUN npm install 19 | RUN npm run build:production 20 | 21 | # server environment 22 | FROM nginx:alpine 23 | COPY nginx.conf /etc/nginx/conf.d/configfile.template 24 | ENV PORT 8080 25 | ENV HOST 0.0.0.0 26 | RUN sh -c "envsubst '\$PORT' < /etc/nginx/conf.d/configfile.template > /etc/nginx/conf.d/default.conf" 27 | COPY --from=react-build /app/dist /usr/share/nginx/html 28 | EXPOSE 8080 29 | CMD ["nginx", "-g", "daemon off;"] 30 | -------------------------------------------------------------------------------- /infra/modules/storage/metadata.display.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: blueprints.cloud.google.com/v1alpha1 16 | kind: BlueprintMetadata 17 | metadata: 18 | name: terraform-ml-image-annotation-gcf-storage-display 19 | spec: 20 | info: 21 | title: storage module 22 | source: 23 | repo: https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf.git 24 | sourceType: git 25 | dir: /infra/modules/storage 26 | ui: 27 | input: 28 | variables: 29 | gcf_location: 30 | name: gcf_location 31 | title: Gcf Location 32 | labels: 33 | name: labels 34 | title: Labels 35 | -------------------------------------------------------------------------------- /infra/test/setup/main.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module "project" { 18 | source = "terraform-google-modules/project-factory/google" 19 | version = "~> 15.0" 20 | 21 | name = "ci-annotate-img-gcf" 22 | random_project_id = "true" 23 | org_id = var.org_id 24 | folder_id = var.folder_id 25 | billing_account = var.billing_account 26 | default_service_account = "keep" 27 | 28 | activate_apis = [ 29 | "cloudresourcemanager.googleapis.com", 30 | "iam.googleapis.com", 31 | "storage.googleapis.com", 32 | "serviceusage.googleapis.com", 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. 4 | 5 | ## Before you begin 6 | 7 | ### Sign our Contributor License Agreement 8 | 9 | Contributions to this project must be accompanied by a 10 | [Contributor License Agreement](https://cla.developers.google.com/about) (CLA). 11 | You (or your employer) retain the copyright to your contribution; this simply 12 | gives us permission to use and redistribute your contributions as part of the 13 | project. 14 | 15 | If you or your current employer have already signed the Google CLA (even if it 16 | was for a different project), you probably don't need to do it again. 17 | 18 | Visit to see your current agreements or to 19 | sign a new one. 20 | 21 | ### Review our Community Guidelines 22 | 23 | This project follows 24 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 25 | 26 | ## Contribution process 27 | 28 | ### Code Reviews 29 | 30 | All submissions, including submissions by project members, require review. We 31 | use GitHub pull requests for this purpose. Consult 32 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 33 | information on using pull requests. 34 | -------------------------------------------------------------------------------- /src/frontend/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import react from '@vitejs/plugin-react' 18 | import tsconfigPaths from "vite-tsconfig-paths"; 19 | import type { UserConfig as VitestUserConfigInterface } from 'vitest/config'; 20 | 21 | const config: VitestUserConfigInterface = { 22 | plugins: [react(), tsconfigPaths()], 23 | test: { 24 | globals: true, 25 | environment: 'jsdom', 26 | coverage: { 27 | include: ['src/**/*'], 28 | exclude: ['src/main.tsx', 'src/vite-env.d.ts', 'src/tests'], 29 | all: true, 30 | } 31 | }, 32 | } 33 | 34 | export default config 35 | 36 | -------------------------------------------------------------------------------- /infra/provider.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | terraform { 18 | required_version = ">= 1.5" 19 | required_providers { 20 | google = { 21 | source = "hashicorp/google" 22 | version = ">= 4.66, != 4.75.0" 23 | } 24 | google-beta = { 25 | source = "hashicorp/google-beta" 26 | version = ">= 4.66, != 4.75.0" 27 | } 28 | null = { 29 | source = "hashicorp/null" 30 | version = "~> 3.2" 31 | } 32 | random = { 33 | source = "hashicorp/random" 34 | version = "~> 3.1" 35 | } 36 | time = { 37 | source = "hashicorp/time" 38 | version = "~> 0.12.0" 39 | } 40 | } 41 | } 42 | 43 | provider "google" { 44 | project = var.project_id 45 | } 46 | -------------------------------------------------------------------------------- /src/gcf/tests/test_storage_methods.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from unittest.mock import patch 16 | 17 | with patch("google.cloud.logging.Client"): 18 | with patch("opentelemetry.exporter.cloud_trace.CloudTraceSpanExporter"): 19 | from main import image_filename_for_json, json_filename_for_image 20 | 21 | 22 | def test_json_filename_for_image(): 23 | # Given 24 | file_name = "image.png" 25 | 26 | # When 27 | result = json_filename_for_image(file_name) 28 | 29 | # Then 30 | assert result == "image.png.json" 31 | 32 | 33 | def test_image_filename_for_json(): 34 | # Given 35 | file_name = "annotation.json" 36 | 37 | # When 38 | result = image_filename_for_json(file_name) 39 | 40 | # Then 41 | assert result == "annotation" 42 | -------------------------------------------------------------------------------- /src/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #root { 18 | max-width: 1280px; 19 | margin: 0 auto; 20 | padding: 2rem; 21 | text-align: center; 22 | } 23 | 24 | .logo { 25 | height: 6em; 26 | padding: 1.5em; 27 | will-change: filter; 28 | transition: filter 300ms; 29 | } 30 | .logo:hover { 31 | filter: drop-shadow(0 0 2em #646cffaa); 32 | } 33 | .logo.react:hover { 34 | filter: drop-shadow(0 0 2em #61dafbaa); 35 | } 36 | 37 | @keyframes logo-spin { 38 | from { 39 | transform: rotate(0deg); 40 | } 41 | to { 42 | transform: rotate(360deg); 43 | } 44 | } 45 | 46 | @media (prefers-reduced-motion: no-preference) { 47 | a:nth-of-type(2) .logo { 48 | animation: logo-spin infinite 20s linear; 49 | } 50 | } 51 | 52 | .card { 53 | padding: 2em; 54 | } 55 | 56 | .read-the-docs { 57 | color: #888; 58 | } 59 | -------------------------------------------------------------------------------- /infra/modules/storage/main.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | data "google_project" "project" {} 18 | 19 | # input bucket for images 20 | resource "google_storage_bucket" "vision-input" { 21 | name = "vision-input-${data.google_project.project.number}" 22 | location = var.gcf_location 23 | uniform_bucket_level_access = true 24 | force_destroy = true 25 | labels = var.labels 26 | } 27 | 28 | # output bucket for prediction JSON files 29 | resource "google_storage_bucket" "vision-annotations" { 30 | name = "vision-annotations-${data.google_project.project.number}" 31 | location = var.gcf_location 32 | uniform_bucket_level_access = true 33 | force_destroy = true 34 | labels = var.labels 35 | } 36 | -------------------------------------------------------------------------------- /src/frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /infra/test/setup/iam.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | locals { 18 | int_required_roles = [ 19 | "roles/resourcemanager.projectIamAdmin", 20 | "roles/serviceusage.serviceUsageAdmin", 21 | "roles/iam.serviceAccountAdmin", 22 | "roles/storage.admin", 23 | "roles/run.admin", 24 | "roles/editor", 25 | ] 26 | } 27 | 28 | resource "google_service_account" "int_test" { 29 | project = module.project.project_id 30 | account_id = "ci-jss" 31 | display_name = "ci-jss" 32 | } 33 | 34 | resource "google_project_iam_member" "int_test" { 35 | for_each = toset(local.int_required_roles) 36 | 37 | project = module.project.project_id 38 | role = each.value 39 | member = "serviceAccount:${google_service_account.int_test.email}" 40 | } 41 | 42 | resource "google_service_account_key" "int_test" { 43 | service_account_id = google_service_account.int_test.id 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image Processing on Google Cloud 2 | 3 | This solution includes frontend and backend code to demonstrate image processing use cases on Google Cloud. 4 | 5 | Image processing is one of the most tangible ways to incorporate AI to improve customer operations. Examples include defect detection in manufacturing, unsafe image detection for user-generated content, and disease diagnosis in healthcare, among others. However, doing this in a scalable way requires the integration of several GCP products. 6 | 7 | The presented solution uses the following technologies: 8 | 9 | - [Cloud Functions](https://cloud.google.com/functions/docs) 10 | - [Vision AI](https://cloud.google.com/vision) 11 | 12 | ### Files 13 | 14 | Some important files and folders. 15 | 16 | | File/Folder | Description | 17 | | ------------------ | ------------------------------------------------------------------------------------ | 18 | | /src/frontend | React web app: A frontend to upload images/urls to the backend for image processing. | 19 | | /src/gcf | Backend Cloud Functions code that calls the Vision API for image processing. | 20 | | /infra | Terraform infrastructure for deploying the solution. | 21 | | /.github/workflows | Github workflows for CI and testing | 22 | | /build | Cloud Build configuration files for CFT testing. | 23 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/SafeSearchResultView.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { render } from "@testing-library/react"; 18 | import "@testing-library/jest-dom"; 19 | import SafeSearchResultView from "components/results/SafeSearchResultView"; 20 | 21 | const mockAnnotation = { 22 | adult: 2, 23 | spoof: 1, 24 | medical: 4, 25 | violence: 3, 26 | racy: 5, 27 | }; 28 | 29 | describe("SafeSearchResultView", () => { 30 | it("renders all label rows with correct annotation data", () => { 31 | const { getByText } = render( 32 | 33 | ); 34 | 35 | expect(getByText("UNLIKELY")).toBeInTheDocument(); 36 | expect(getByText("VERY UNLIKELY")).toBeInTheDocument(); 37 | expect(getByText("LIKELY")).toBeInTheDocument(); 38 | expect(getByText("POSSIBLE")).toBeInTheDocument(); 39 | expect(getByText("VERY LIKELY")).toBeInTheDocument(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/gcf/tests/test_ui_methods.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from unittest.mock import patch 16 | from google.cloud import storage 17 | 18 | with patch("google.cloud.logging.Client"): 19 | with patch("opentelemetry.exporter.cloud_trace.CloudTraceSpanExporter"): 20 | from main import list_bucket 21 | 22 | 23 | def test_list_bucket(mocker): 24 | mocker.patch("google.cloud.storage.Client") 25 | client_mock = storage.Client.return_value 26 | 27 | # Test when the list_blobs function returns a valid list of blobs 28 | expected_result = [storage.Blob("blob1", None), storage.Blob("blob2", None)] 29 | client_mock.list_blobs.return_value = expected_result 30 | 31 | bucket_name = "test-bucket" 32 | result = list_bucket("bucket_name") 33 | assert len(expected_result) == 2 34 | 35 | # Test when the list_blobs function raises an exception 36 | client_mock.list_blobs.side_effect = Exception("An error occurred") 37 | result = list_bucket(bucket_name) 38 | assert result is None 39 | -------------------------------------------------------------------------------- /src/frontend/src/components/results/ConfidenceLabelRow.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export default ({ 18 | index, 19 | label, 20 | confidence, 21 | }: { 22 | index: number; 23 | label: string; 24 | confidence: number; 25 | }) => { 26 | const confidencePercent = confidence * 100; 27 | 28 | return ( 29 |
30 |
31 | {label} 32 | 33 | {index === 0 ? "Confidence = " : null} 34 | {confidence.toFixed(2)} 35 | 36 |
37 |
38 |
43 |
44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/BoundingBox.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from "react"; 18 | import { render, screen } from "@testing-library/react"; 19 | import "@testing-library/jest-dom"; 20 | import { BoundingBox } from "components/ImageWithBoundingBoxes"; 21 | 22 | describe("BoundingBox", () => { 23 | test("renders a bounding box with the correct styles", () => { 24 | // Prepare test data 25 | const box = { 26 | vertices: [ 27 | { x: 10, y: 20 }, 28 | { x: 30, y: 20 }, 29 | { x: 30, y: 60 }, 30 | { x: 10, y: 60 }, 31 | ], 32 | normalizedVertices: [], 33 | }; 34 | 35 | const props = { 36 | index: 0, 37 | box, 38 | imageWidth: 100, 39 | imageHeight: 100, 40 | selectedIndex: 0, 41 | }; 42 | 43 | render(); 44 | 45 | const boundingBox = screen.getByTestId("bounding-box"); 46 | expect(boundingBox).toHaveStyle({ 47 | top: "20%", 48 | left: "10%", 49 | width: "20%", 50 | height: "40%", 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # NOTE: This file is automatically generated from: 16 | # https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/blob/main/infra/terraform/test-org/github 17 | 18 | name: "Close stale issues" 19 | on: 20 | schedule: 21 | - cron: "0 23 * * *" 22 | 23 | permissions: 24 | contents: read 25 | issues: write 26 | pull-requests: write 27 | actions: write 28 | 29 | jobs: 30 | stale: 31 | if: github.repository_owner == 'GoogleCloudPlatform' || github.repository_owner == 'terraform-google-modules' 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/stale@v10 35 | with: 36 | repo-token: ${{ secrets.GITHUB_TOKEN }} 37 | stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days' 38 | stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days' 39 | exempt-issue-labels: 'triaged' 40 | exempt-pr-labels: 'dependencies,autorelease: pending' 41 | operations-per-run: 100 42 | -------------------------------------------------------------------------------- /infra/examples/simple_example/outputs.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | output "vision_annotations_gcs" { 18 | description = "Cloud Storage of the vision annotations" 19 | value = module.simple.vision_annotations_gcs 20 | } 21 | 22 | output "vision_input_gcs" { 23 | description = "Cloud Storage of the vision input" 24 | value = module.simple.vision_input_gcs 25 | } 26 | 27 | 28 | output "vision_prediction_url" { 29 | description = "The URL for requesting online prediction with HTTP request." 30 | value = module.simple.vision_prediction_url 31 | } 32 | 33 | output "annotate_gcs_function_name" { 34 | description = "The name of the cloud function that annotates an image triggered by a GCS event." 35 | value = module.simple.annotate_gcs_function_name 36 | } 37 | 38 | output "annotate_http_function_name" { 39 | description = "The name of the cloud function that annotates an image triggered by an HTTP request." 40 | value = module.simple.annotate_http_function_name 41 | } 42 | 43 | output "source_code_url" { 44 | description = "The URL of the source code for Cloud Functions." 45 | value = module.simple.source_code_url 46 | } 47 | -------------------------------------------------------------------------------- /src/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Image Processing: Frontend 2 | 3 | This is the frontend code for the "Image Processing" solution. 4 | 5 | This project was created with [Vite](https://vitejs.dev/). 6 | 7 | It uses the following technologies: 8 | 9 | - [React](https://reactjs.org) 10 | - [DaisyUI](https://daisyui.com) 11 | - [@tanstack/react-query](https://github.com/tanstack/query) 12 | 13 | ## Available Commands 14 | 15 | In the project directory, you can run: 16 | 17 | ### Deployment 18 | 19 | #### Pre-requisite: Environment variables 20 | 21 | 1. Deploy the desired backend by following the instructions in the relevant backend folder. Note the URI that the backend is deployed to. 22 | 2. Create a file in this folder (i.e. frontend) called `.env.production`. 23 | 3. In this file, add the following with the appropriate value for ``. 24 | 25 | ``` 26 | VITE_API_SERVER= 27 | ``` 28 | 29 | #### Build and deploy to Google Cloud Run in one step 30 | 31 | ``` 32 | gcloud run deploy image-processing-frontend --source . --region=us-central1 --cpu=2 --memory=8G --timeout=3600 --allow-unauthenticated 33 | ``` 34 | 35 | This packages the frontend into an image using the Dockerfile and saves it in the Artifact Registry. It then deploys the image right away. 36 | 37 | See [Deploy from source code](https://cloud.google.com/run/docs/deploying-source-code) and [Deploy a Python service to Cloud Run](https://cloud.google.com/run/docs/quickstarts/build-and-deploy/deploy-python-service) for more information. 38 | 39 | ### Development 40 | 41 | #### `npm run dev` 42 | 43 | Runs the app in the development mode.\ 44 | Open [http://localhost:5173](http://localhost:5173) (or whatever link Vite tells you to) to view it in the browser. 45 | 46 | The page will reload if you make edits.\ 47 | You will also see any lint errors in the console. 48 | -------------------------------------------------------------------------------- /.github/workflows/frontend.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Frontend tests 16 | 17 | on: 18 | push: 19 | branches: 20 | - main 21 | paths: 22 | - 'src/frontend/**' 23 | - '.github/workflows/frontend.yml' 24 | pull_request: 25 | branches: 26 | - main 27 | paths: 28 | - 'src/frontend/**' 29 | - '.github/workflows/frontend.yml' 30 | 31 | jobs: 32 | frontend-unit-tests: 33 | runs-on: ubuntu-latest 34 | 35 | strategy: 36 | matrix: 37 | node-version: [16.x] 38 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | - name: Set up Node.js ${{ matrix.node-version }} 45 | uses: actions/setup-node@v4 46 | with: 47 | node-version: ${{ matrix.node-version }} 48 | 49 | - name: Cache dependencies 50 | uses: actions/cache@v4 51 | with: 52 | path: src/frontend/node_modules 53 | key: ${{ runner.os }}-node-modules-${{ hashFiles('src/frontend/package-lock.json') }} 54 | 55 | - name: Install dependencies 56 | run: npm ci 57 | working-directory: src/frontend 58 | 59 | - name: Execute Unit tests with coverage 60 | run: npm run coverage 61 | working-directory: src/frontend 62 | -------------------------------------------------------------------------------- /infra/output.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | output "vision_prediction_url" { 18 | description = "The URL for requesting online prediction with HTTP request." 19 | value = module.cloudfunctions.function_uri 20 | } 21 | 22 | output "vision_input_gcs" { 23 | description = "Input GCS bucket name." 24 | value = "gs://${module.storage.gcs_input}" 25 | } 26 | 27 | output "vision_annotations_gcs" { 28 | description = "Output GCS bucket name." 29 | value = "gs://${module.storage.gcs_annotations}" 30 | } 31 | 32 | output "annotate_gcs_function_name" { 33 | description = "The name of the cloud function that annotates an image triggered by a GCS event." 34 | value = module.cloudfunctions.annotate_gcs_function_name 35 | } 36 | 37 | output "annotate_http_function_name" { 38 | description = "The name of the cloud function that annotates an image triggered by an HTTP request." 39 | value = module.cloudfunctions.annotate_http_function_name 40 | } 41 | 42 | output "source_code_url" { 43 | description = "The URL of the source code for Cloud Functions." 44 | value = "gs://${module.cloudfunctions.code_bucket}/${module.cloudfunctions.source_code_filename}" 45 | } 46 | 47 | output "neos_walkthrough_url" { 48 | description = "Neos Tutorial URL" 49 | value = "https://console.cloud.google.com/products/solutions/deployments?walkthrough_id=panels--sic--image-processing-gcf_toc" 50 | } 51 | -------------------------------------------------------------------------------- /infra/modules/storage/metadata.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: blueprints.cloud.google.com/v1alpha1 16 | kind: BlueprintMetadata 17 | metadata: 18 | name: terraform-ml-image-annotation-gcf-storage 19 | annotations: 20 | config.kubernetes.io/local-config: "true" 21 | spec: 22 | info: 23 | title: storage module 24 | source: 25 | repo: https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf.git 26 | sourceType: git 27 | dir: storage 28 | actuationTool: 29 | flavor: Terraform 30 | version: '>= 0.13' 31 | description: {} 32 | content: 33 | examples: 34 | - name: simple_example 35 | location: examples/simple_example 36 | interfaces: 37 | variables: 38 | - name: gcf_location 39 | description: GCS deployment region. 40 | varType: string 41 | required: true 42 | - name: labels 43 | description: A map of key/value label pairs to assign to the resources. 44 | varType: map(string) 45 | required: true 46 | outputs: 47 | - name: gcs_annotations 48 | description: Output GCS bucket name. 49 | - name: gcs_input 50 | description: Input GCS bucket name. 51 | requirements: 52 | roles: 53 | - level: Project 54 | roles: 55 | - roles/owner 56 | services: 57 | - cloudresourcemanager.googleapis.com 58 | - iam.googleapis.com 59 | - storage.googleapis.com 60 | - serviceusage.googleapis.com 61 | -------------------------------------------------------------------------------- /src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-processing", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build:production": "tsc && vite build --mode production", 9 | "preview": "vite preview", 10 | "lint": "npx eslint src test", 11 | "lint:fix": "npm run lint -- --fix", 12 | "prettier": "npx prettier src test --check", 13 | "prettier:fix": "npm run prettier -- --write", 14 | "format": "npm run prettier:fix && npm run lint:fix", 15 | "test": "vitest", 16 | "coverage": "vitest run --coverage" 17 | }, 18 | "dependencies": { 19 | "@emotion/react": "^11.11.4", 20 | "@emotion/styled": "^11.11.5", 21 | "@heroicons/react": "^2.1.3", 22 | "axios": "^1.6.8", 23 | "clsx": "^2.1.1", 24 | "daisyui": "^4.10.2", 25 | "eslint-config-prettier": "^9.1.0", 26 | "react": "^18.3.1", 27 | "react-dom": "^18.3.1", 28 | "react-query": "^3.39.3" 29 | }, 30 | "devDependencies": { 31 | "@testing-library/dom": "^10.0.0", 32 | "@testing-library/jest-dom": "^6.4.2", 33 | "@testing-library/react": "^15.0.5", 34 | "@testing-library/user-event": "^14.5.2", 35 | "@types/react": "^18.3.1", 36 | "@types/react-dom": "^18.3.0", 37 | "@typescript-eslint/eslint-plugin": "^7.9.0", 38 | "@typescript-eslint/parser": "^7.9.0", 39 | "@vitejs/plugin-react": "^4.2.1", 40 | "@vitest/coverage-v8": "^1.5.2", 41 | "autoprefixer": "^10.4.19", 42 | "eslint": "^8.57.0", 43 | "eslint-config-love": "^49.0.0", 44 | "eslint-config-react-app": "^7.0.1", 45 | "eslint-plugin-import": "^2.29.1", 46 | "eslint-plugin-n": "^17.0.0", 47 | "eslint-plugin-promise": "^6.1.1", 48 | "eslint-plugin-react": "^7.34.1", 49 | "jsdom": "^24.0.0", 50 | "postcss": "^8.4.38", 51 | "prettier": "3.3.3", 52 | "tailwindcss": "^3.4.3", 53 | "tslint-config-prettier": "^1.18.0", 54 | "typescript": "^5.4.5", 55 | "vite": "^5.2.10", 56 | "vite-plugin-eslint": "^1.8.1", 57 | "vite-tsconfig-paths": "^4.3.2", 58 | "vitest": "^1.5.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/backend.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Backend tests 16 | 17 | on: 18 | push: 19 | branches: 20 | - main 21 | paths: 22 | - 'src/gcf/**' 23 | - '.github/workflows/backend.yml' 24 | pull_request: 25 | branches: 26 | - main 27 | paths: 28 | - 'src/gcf/**' 29 | - '.github/workflows/backend.yml' 30 | 31 | jobs: 32 | pytest: 33 | runs-on: ubuntu-latest 34 | 35 | strategy: 36 | matrix: 37 | python-version: ["3.8", "3.9", "3.10"] 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | - name: Set up Python ${{ matrix.python-version }} 44 | uses: actions/setup-python@v5 45 | with: 46 | python-version: ${{ matrix.python-version }} 47 | 48 | - name: Cache dependencies 49 | uses: actions/cache@v4 50 | with: 51 | path: src/gcf/env 52 | key: ${{ runner.os }}-venv-${{ hashFiles('src/gcf/requirements.txt') }} 53 | 54 | - name: Install dependencies 55 | run: | 56 | python -m venv venv 57 | source venv/bin/activate 58 | pip install -r requirements.txt 59 | pip install -r requirements-dev.txt 60 | working-directory: src/gcf 61 | 62 | - name: Run Pytest 63 | run: | 64 | source venv/bin/activate 65 | pytest 66 | working-directory: src/gcf 67 | env: 68 | PYTHONPATH: ${{ github.workspace }}/src/gcf 69 | -------------------------------------------------------------------------------- /infra/modules/cloudfunctions/README.md: -------------------------------------------------------------------------------- 1 | # cloudfunctions module 2 | 3 | 4 | ## Inputs 5 | 6 | | Name | Description | Type | Default | Required | 7 | |------|-------------|------|---------|:--------:| 8 | | annotations-bucket | Annotations bucket name | `string` | n/a | yes | 9 | | gcf\_annotation\_features | Requested annotation features. | `string` | n/a | yes | 10 | | gcf\_http\_ingress\_type\_index | Ingres type index. | `number` | n/a | yes | 11 | | gcf\_http\_ingress\_types\_list | Ingres type values | `list(any)` |
[
"ALLOW_ALL",
"ALLOW_INTERNAL_ONLY",
"ALLOW_INTERNAL_AND_GCLB"
]
| no | 12 | | gcf\_location | GCF deployment region | `string` | n/a | yes | 13 | | gcf\_log\_level | Set logging level for cloud functions. | `string` | n/a | yes | 14 | | gcf\_max\_instance\_count | MAX number of GCF instances | `number` | n/a | yes | 15 | | gcf\_require\_http\_authentication | Create HTTP API with public, unauthorized access. | `bool` | n/a | yes | 16 | | gcf\_timeout\_seconds | GCF execution timeout | `number` | n/a | yes | 17 | | gcr\_invoker\_members | IAM members. | `list(string)` |
[
"allUsers"
]
| no | 18 | | gcr\_role\_invoker | IAM role GCR invoker. | `string` | `"roles/run.invoker"` | no | 19 | | input-bucket | Input bucket name | `string` | n/a | yes | 20 | | labels | A map of key/value label pairs to assign to the resources. | `map(string)` | n/a | yes | 21 | 22 | ## Outputs 23 | 24 | | Name | Description | 25 | |------|-------------| 26 | | annotate\_gcs\_function\_name | The name of the Cloud Function that annotates an image triggered by a GCS event. | 27 | | annotate\_http\_function\_name | The name of the Cloud Function that annotates an image triggered by an HTTP request. | 28 | | code\_bucket | The name of the bucket where the Cloud Function code is stored. | 29 | | function\_uri | Cloud Function URI and ingress parameters. | 30 | | gcf\_sa | Cloud Functions SA. | 31 | | gcs\_account | Cloud StorageS SA. | 32 | | source\_code\_filename | The name of the file containing the Cloud Function code. | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/LabelDetectionResultView.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { test } from "vitest"; 18 | import { render, within } from "@testing-library/react"; 19 | import "@testing-library/jest-dom"; 20 | import { Annotation } from "queries"; 21 | import AnnotationsTable from "components/results/LabelDetectionResultView"; 22 | 23 | const sampleAnnotations: Annotation[] = [ 24 | { 25 | mid: "1", 26 | description: "dog", 27 | score: 0.9, 28 | locale: "en", 29 | confidence: 0.9, 30 | topicality: 0.9, 31 | properties: [], 32 | }, 33 | { 34 | mid: "2", 35 | description: "cat", 36 | score: 0.8, 37 | locale: "en", 38 | confidence: 0.8, 39 | topicality: 0.8, 40 | properties: [], 41 | }, 42 | ]; 43 | 44 | test("renders info when no annotations are present", async () => { 45 | const { getByText } = render(); 46 | expect(getByText("No labels detected.")).toBeInTheDocument(); 47 | }); 48 | 49 | test("renders a sorted table with given annotations", async () => { 50 | const { container, getByText } = render( 51 | 52 | ); 53 | const table = container.querySelector("table"); 54 | expect(table).toBeInTheDocument(); 55 | expect(within(table!).getByText("dog")).toBeInTheDocument(); 56 | expect(within(table!).getByText("cat")).toBeInTheDocument(); 57 | expect(within(table!).getByText("Confidence = 0.90")).toBeInTheDocument(); 58 | expect(within(table!).getByText("0.80")).toBeInTheDocument(); 59 | }); 60 | -------------------------------------------------------------------------------- /src/frontend/src/components/selection/ImageSourceToggleSelection.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React, { useState } from "react"; 18 | import clsx from "clsx"; 19 | import { ImageSource } from "components/selection/UnifiedImageSelector"; 20 | 21 | interface Props { 22 | onChange: (imageSource: ImageSource) => void; 23 | } 24 | 25 | const OPTION_TO_LABEL_MAP = { 26 | "File upload": ImageSource.Upload, 27 | "Image URL": ImageSource.URL, 28 | "Cloud storage": ImageSource.CloudStorage, 29 | }; 30 | 31 | export default ({ onChange }: Props) => { 32 | const [selectedOptions, setSelectedFeature] = useState( 33 | Object.values(OPTION_TO_LABEL_MAP)[0] 34 | ); 35 | 36 | const handleSelection = (newSelection: ImageSource) => { 37 | if (newSelection !== null) { 38 | setSelectedFeature(newSelection); 39 | onChange(newSelection); 40 | } 41 | }; 42 | 43 | return ( 44 |
45 | {Object.entries(OPTION_TO_LABEL_MAP).map(([label, value]) => ( 46 | 60 | ))} 61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/ConfidenceLabelRow.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { render, screen } from "@testing-library/react"; 18 | import "@testing-library/jest-dom"; 19 | import ConfidenceLabelRow from "components/results/ConfidenceLabelRow"; 20 | 21 | describe("ConfidenceComponent", () => { 22 | test("renders label, confidence, and progress bar", () => { 23 | const props = { 24 | index: 0, 25 | label: "Example", 26 | confidence: 0.85, 27 | }; 28 | 29 | const { getByText, getByTestId } = render( 30 | 31 | ); 32 | 33 | const labelElement = getByText(props.label); 34 | const confidenceElement = getByText( 35 | `Confidence = ${props.confidence.toFixed(2)}` 36 | ); 37 | const progressBarElement = getByTestId("confidence-bar"); 38 | 39 | expect(labelElement).toBeInTheDocument(); 40 | expect(confidenceElement).toBeInTheDocument(); 41 | expect(progressBarElement).toBeInTheDocument(); 42 | expect(progressBarElement).toHaveStyle(`width: ${props.confidence * 100}%`); 43 | }); 44 | 45 | test('does not render "Confidence = " for non-zero index', () => { 46 | const props = { 47 | index: 1, 48 | label: "Example", 49 | confidence: 0.85, 50 | }; 51 | 52 | const { getByText } = render(); 53 | 54 | const confidenceElement = getByText(props.confidence.toFixed(2)); 55 | expect(confidenceElement).toBeInTheDocument(); 56 | expect(confidenceElement).not.toHaveTextContent("Confidence = "); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/ImageSourceToggleSelection.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { render, fireEvent } from "@testing-library/react"; 18 | import "@testing-library/jest-dom/vitest"; 19 | import { ImageSource } from "components/selection/UnifiedImageSelector"; 20 | import { vi } from "vitest"; 21 | import ImageSourceToggleSelection from "components/selection/ImageSourceToggleSelection"; 22 | 23 | describe("ImageSelector", () => { 24 | test("renders and selects correct options", () => { 25 | const onChange = vi.fn().mockImplementation(() => {}); 26 | const { getAllByRole } = render( 27 | 28 | ); 29 | const buttons = getAllByRole("button"); 30 | fireEvent.click(buttons[0]); 31 | expect(onChange).toHaveBeenLastCalledWith(ImageSource.Upload); 32 | expect(buttons[0]).toHaveClass("btn-primary"); 33 | expect(buttons[1]).not.toHaveClass("btn-primary"); 34 | expect(buttons[2]).not.toHaveClass("btn-primary"); 35 | 36 | fireEvent.click(buttons[1]); 37 | expect(onChange).toHaveBeenLastCalledWith(ImageSource.URL); 38 | expect(buttons[0]).not.toHaveClass("btn-primary"); 39 | expect(buttons[1]).toHaveClass("btn-primary"); 40 | expect(buttons[2]).not.toHaveClass("btn-primary"); 41 | 42 | fireEvent.click(buttons[2]); 43 | expect(onChange).toHaveBeenLastCalledWith(ImageSource.CloudStorage); 44 | expect(buttons[0]).not.toHaveClass("btn-primary"); 45 | expect(buttons[1]).not.toHaveClass("btn-primary"); 46 | expect(buttons[2]).toHaveClass("btn-primary"); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/frontend/src/components/Alert.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | InformationCircleIcon, 19 | CheckCircleIcon, 20 | XCircleIcon, 21 | ClockIcon, 22 | } from "@heroicons/react/24/outline"; 23 | import clsx from "clsx"; 24 | 25 | type AlertProps = { 26 | mode: "info" | "success" | "error" | "loading"; 27 | text: string; 28 | className?: string; 29 | }; 30 | 31 | export default ({ mode, text, className }: AlertProps) => { 32 | let icon; 33 | 34 | switch (mode) { 35 | case "info": 36 | icon = ; 37 | break; 38 | case "loading": 39 | icon = ; 40 | break; 41 | case "success": 42 | icon = ; 43 | break; 44 | case "error": 45 | icon = ; 46 | break; 47 | default: 48 | icon = null; 49 | } 50 | 51 | return ( 52 |
68 | {icon} 69 | {text} 70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/FeatureToggleSelection.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { fireEvent, render } from "@testing-library/react"; 18 | import FeatureSelector, { 19 | FEATURES, 20 | } from "components/selection/FeatureToggleSelection"; 21 | import "@testing-library/jest-dom"; 22 | 23 | import { 24 | OBJECT_LOCALIZATION, 25 | LABEL_DETECTION, 26 | IMAGE_PROPERTIES, 27 | SAFE_SEARCH_DETECTION, 28 | FACE_DETECTION, 29 | } from "AppConstants"; 30 | 31 | import { vi } from "vitest"; 32 | 33 | test("should render all features", () => { 34 | const { getByText } = render( {}} />); 35 | 36 | FEATURES.forEach(({ label }) => { 37 | expect(getByText(label)).toBeInTheDocument(); 38 | }); 39 | }); 40 | 41 | test("should call onChange with the correct features on button click", () => { 42 | const onChangeMock = vi.fn().mockImplementation(() => {}); 43 | 44 | const { getByText } = render(); 45 | 46 | // All selected 47 | expect(onChangeMock).toHaveBeenCalledWith([ 48 | OBJECT_LOCALIZATION, 49 | LABEL_DETECTION, 50 | IMAGE_PROPERTIES, 51 | SAFE_SEARCH_DETECTION, 52 | FACE_DETECTION, 53 | ]); 54 | 55 | // Deselect LABEL_DETECTION 56 | const labelDetectionFeature = FEATURES.find( 57 | (feature) => feature.value === LABEL_DETECTION 58 | ); 59 | fireEvent.click(getByText(labelDetectionFeature!.label)); 60 | expect(onChangeMock).toHaveBeenCalledWith([ 61 | OBJECT_LOCALIZATION, 62 | IMAGE_PROPERTIES, 63 | SAFE_SEARCH_DETECTION, 64 | FACE_DETECTION, 65 | ]); 66 | }); 67 | -------------------------------------------------------------------------------- /infra/modules/cloudfunctions/output.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | output "function_uri" { 18 | description = "Cloud Function URI and ingress parameters." 19 | value = [ 20 | google_cloudfunctions2_function.annotate_http.service_config[0].uri, 21 | "ingressIndex:${var.gcf_http_ingress_type_index}", 22 | "ingressValue:${var.gcf_http_ingress_types_list[var.gcf_http_ingress_type_index]}", 23 | "isAuthenticated:${var.gcf_require_http_authentication} ", 24 | ] 25 | } 26 | 27 | output "annotate_gcs_function_name" { 28 | description = "The name of the Cloud Function that annotates an image triggered by a GCS event." 29 | value = google_cloudfunctions2_function.annotate_gcs.name 30 | } 31 | 32 | output "annotate_http_function_name" { 33 | description = "The name of the Cloud Function that annotates an image triggered by an HTTP request." 34 | value = google_cloudfunctions2_function.annotate_http.name 35 | } 36 | 37 | output "code_bucket" { 38 | description = "The name of the bucket where the Cloud Function code is stored." 39 | value = google_storage_bucket.code_bucket.name 40 | } 41 | 42 | output "source_code_filename" { 43 | description = "The name of the file containing the Cloud Function code." 44 | value = google_storage_bucket_object.gcf_code.name 45 | } 46 | 47 | output "gcf_sa" { 48 | description = "Cloud Functions SA." 49 | value = "GCF SA=${google_service_account.gcf_sa.email}" 50 | } 51 | 52 | output "gcs_account" { 53 | description = "Cloud StorageS SA." 54 | value = "GCF SA=${data.google_storage_project_service_account.gcs_account.email_address}" 55 | } 56 | -------------------------------------------------------------------------------- /infra/metadata.display.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: blueprints.cloud.google.com/v1alpha1 16 | kind: BlueprintMetadata 17 | metadata: 18 | name: terraform-ml-image-annotation-gcf-display 19 | spec: 20 | info: 21 | title: Infrastructure for image annotation with ML and GCF 22 | source: 23 | repo: https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf.git 24 | sourceType: git 25 | dir: /infra 26 | ui: 27 | input: 28 | variables: 29 | gcf_annotation_features: 30 | name: gcf_annotation_features 31 | title: Gcf Annotation Features 32 | gcf_http_ingress_type_index: 33 | name: gcf_http_ingress_type_index 34 | title: Gcf Http Ingress Type Index 35 | gcf_location: 36 | name: gcf_location 37 | title: Gcf Location 38 | gcf_log_level: 39 | name: gcf_log_level 40 | title: Gcf Log Level 41 | gcf_max_instance_count: 42 | name: gcf_max_instance_count 43 | title: Gcf Max Instance Count 44 | gcf_require_http_authentication: 45 | name: gcf_require_http_authentication 46 | title: Gcf Require Http Authentication 47 | gcf_timeout_seconds: 48 | name: gcf_timeout_seconds 49 | title: Gcf Timeout Seconds 50 | labels: 51 | name: labels 52 | title: Labels 53 | project_id: 54 | name: project_id 55 | title: Project Id 56 | region: 57 | name: region 58 | title: Region 59 | time_to_enable_apis: 60 | name: time_to_enable_apis 61 | title: Time To Enable Apis 62 | -------------------------------------------------------------------------------- /src/frontend/src/components/results/LabelDetectionResultView.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Annotation } from "queries"; 18 | import ConfidenceLabelRow from "components/results/ConfidenceLabelRow"; 19 | import clsx from "clsx"; 20 | 21 | interface AnnotationsProps { 22 | annotations: Annotation[]; 23 | } 24 | 25 | const AlertInfo = () => ( 26 |
27 |

No labels detected.

28 |
29 | ); 30 | 31 | export default function AnnotationsTable({ annotations }: AnnotationsProps) { 32 | if (annotations.length === 0) { 33 | return ; 34 | } 35 | 36 | // Sort rows by confidence 37 | const rows = annotations.sort((a, b) => b.score - a.score); 38 | 39 | return ( 40 |
41 |

42 | The Vision API can detect and extract information about entities in an 43 | image, across a broad group of categories. Labels can identify general 44 | objects, locations, activities, animal species, products, and more. 45 |

46 |
47 | 48 | 49 | {rows.map(({ description, score }, index) => ( 50 | 51 | 58 | 59 | ))} 60 | 61 |
52 | 57 |
62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /infra/README.md: -------------------------------------------------------------------------------- 1 | # Infrastructure for image annotation with ML and GCF 2 | 3 | Sample infrastructure. 4 | 5 | ### Tagline 6 | 7 | Sample infrastructure tagline. 8 | 9 | ### Detailed 10 | 11 | Sample infrastructure detailed description. 12 | 13 | ### Architecture 14 | 15 | 1. Cloud Functions 16 | 2. Vision API 17 | 3. Cloud Storage 18 | 19 | ## Documentation 20 | 21 | - [Architecture Diagram](https://cloud.google.com/architecture/ai-ml/image-processing-cloud-functions#architecture) 22 | 23 | 24 | ## Inputs 25 | 26 | | Name | Description | Type | Default | Required | 27 | |------|-------------|------|---------|:--------:| 28 | | enable\_apis | Whether or not to enable underlying apis in this solution. | `string` | `true` | no | 29 | | gcf\_annotation\_features | Requested annotation features. | `string` | `"FACE_DETECTION,PRODUCT_SEARCH,SAFE_SEARCH_DETECTION"` | no | 30 | | gcf\_http\_ingress\_type\_index | Ingres type index. | `number` | `0` | no | 31 | | gcf\_log\_level | Set logging level for cloud functions. | `string` | `""` | no | 32 | | gcf\_max\_instance\_count | MAX number of GCF instances | `number` | `10` | no | 33 | | gcf\_require\_http\_authentication | Require authentication. Manage authorized users with Cloud IAM. | `bool` | `false` | no | 34 | | gcf\_timeout\_seconds | GCF execution timeout | `number` | `120` | no | 35 | | labels | A map of key/value label pairs to assign to the resources. | `map(string)` |
{
"app": "terraform-ml-image-annotation-gcf"
}
| no | 36 | | project\_id | GCP project ID. | `string` | n/a | yes | 37 | | region | GCF deployment location/region. | `string` | `"us-west4"` | no | 38 | | time\_to\_enable\_apis | Time to enable APIs, approximate estimate is 5 minutes, can be more. | `string` | `"30s"` | no | 39 | 40 | ## Outputs 41 | 42 | | Name | Description | 43 | |------|-------------| 44 | | annotate\_gcs\_function\_name | The name of the cloud function that annotates an image triggered by a GCS event. | 45 | | annotate\_http\_function\_name | The name of the cloud function that annotates an image triggered by an HTTP request. | 46 | | neos\_walkthrough\_url | Neos Tutorial URL | 47 | | source\_code\_url | The URL of the source code for Cloud Functions. | 48 | | vision\_annotations\_gcs | Output GCS bucket name. | 49 | | vision\_input\_gcs | Input GCS bucket name. | 50 | | vision\_prediction\_url | The URL for requesting online prediction with HTTP request. | 51 | 52 | 53 | -------------------------------------------------------------------------------- /infra/modules/cloudfunctions/variables.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | variable "gcf_location" { 19 | description = "GCF deployment region" 20 | type = string 21 | } 22 | 23 | variable "input-bucket" { 24 | description = "Input bucket name" 25 | type = string 26 | } 27 | 28 | variable "annotations-bucket" { 29 | description = "Annotations bucket name" 30 | type = string 31 | } 32 | 33 | variable "gcr_invoker_members" { 34 | type = list(string) 35 | description = "IAM members." 36 | default = ["allUsers"] 37 | } 38 | 39 | variable "gcr_role_invoker" { 40 | type = string 41 | description = "IAM role GCR invoker." 42 | default = "roles/run.invoker" 43 | } 44 | 45 | variable "gcf_max_instance_count" { 46 | type = number 47 | description = "MAX number of GCF instances" 48 | } 49 | 50 | variable "gcf_timeout_seconds" { 51 | type = number 52 | description = "GCF execution timeout" 53 | } 54 | 55 | variable "gcf_http_ingress_type_index" { 56 | type = number 57 | description = "Ingres type index." 58 | } 59 | 60 | variable "gcf_http_ingress_types_list" { 61 | type = list(any) 62 | description = "Ingres type values" 63 | default = ["ALLOW_ALL", 64 | "ALLOW_INTERNAL_ONLY", 65 | "ALLOW_INTERNAL_AND_GCLB"] 66 | } 67 | 68 | 69 | variable "gcf_require_http_authentication" { 70 | type = bool 71 | description = "Create HTTP API with public, unauthorized access." 72 | } 73 | 74 | variable "gcf_annotation_features" { 75 | type = string 76 | description = "Requested annotation features." 77 | } 78 | 79 | variable "gcf_log_level" { 80 | type = string 81 | description = "Set logging level for cloud functions." 82 | } 83 | 84 | variable "labels" { 85 | description = "A map of key/value label pairs to assign to the resources." 86 | type = map(string) 87 | } 88 | -------------------------------------------------------------------------------- /infra/modules/cloudfunctions/metadata.display.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: blueprints.cloud.google.com/v1alpha1 16 | kind: BlueprintMetadata 17 | metadata: 18 | name: terraform-ml-image-annotation-gcf-cloudfunctions-display 19 | spec: 20 | info: 21 | title: cloudfunctions module 22 | source: 23 | repo: https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf.git 24 | sourceType: git 25 | dir: /infra/modules/cloudfunctions 26 | ui: 27 | input: 28 | variables: 29 | annotations-bucket: 30 | name: annotations-bucket 31 | title: Annotations-Bucket 32 | gcf_annotation_features: 33 | name: gcf_annotation_features 34 | title: Gcf Annotation Features 35 | gcf_http_ingress_type_index: 36 | name: gcf_http_ingress_type_index 37 | title: Gcf Http Ingress Type Index 38 | gcf_http_ingress_types_list: 39 | name: gcf_http_ingress_types_list 40 | title: Gcf Http Ingress Types List 41 | gcf_location: 42 | name: gcf_location 43 | title: Gcf Location 44 | gcf_log_level: 45 | name: gcf_log_level 46 | title: Gcf Log Level 47 | gcf_max_instance_count: 48 | name: gcf_max_instance_count 49 | title: Gcf Max Instance Count 50 | gcf_require_http_authentication: 51 | name: gcf_require_http_authentication 52 | title: Gcf Require Http Authentication 53 | gcf_timeout_seconds: 54 | name: gcf_timeout_seconds 55 | title: Gcf Timeout Seconds 56 | gcr_invoker_members: 57 | name: gcr_invoker_members 58 | title: Gcr Invoker Members 59 | gcr_role_invoker: 60 | name: gcr_role_invoker 61 | title: Gcr Role Invoker 62 | input-bucket: 63 | name: input-bucket 64 | title: Input-Bucket 65 | labels: 66 | name: labels 67 | title: Labels 68 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/ImagePropertiesResultView.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { render, within } from "@testing-library/react"; 18 | import "@testing-library/jest-dom"; 19 | import { Color, ImagePropertiesAnnotation } from "queries"; 20 | import ImagePropertiesTable, { 21 | ColorRow, 22 | } from "components/results/ImagePropertiesResultView"; 23 | 24 | describe("ColorRow", () => { 25 | test("renders correctly", () => { 26 | const testColor: Color = { 27 | red: 255, 28 | green: 0, 29 | blue: 0, 30 | }; 31 | const pixelFraction = 0.2; 32 | const index = 0; 33 | 34 | const { getByText } = render( 35 | 36 | ); 37 | 38 | expect(getByText("RGB = (255, 0, 0)")).toBeInTheDocument(); 39 | expect(getByText("Pixel fraction = 20%")).toBeInTheDocument(); 40 | }); 41 | }); 42 | 43 | describe("ImagePropertiesTable", () => { 44 | test("renders table with correct colors", () => { 45 | const testAnnotation: ImagePropertiesAnnotation = { 46 | dominantColors: { 47 | colors: [ 48 | { 49 | color: { red: 255, green: 0, blue: 0 }, 50 | score: 0.9, 51 | pixelFraction: 0.6, 52 | }, 53 | { 54 | color: { red: 0, green: 255, blue: 0 }, 55 | score: 0.5, 56 | pixelFraction: 0.3, 57 | }, 58 | ], 59 | }, 60 | }; 61 | 62 | const { getAllByRole } = render( 63 | 64 | ); 65 | const rows = getAllByRole("row"); 66 | expect(rows.length).toEqual(2); 67 | 68 | const firstRow = within(rows[0]); 69 | expect(firstRow.getByText("RGB = (255, 0, 0)")).toBeInTheDocument(); 70 | expect(firstRow.getByText("Pixel fraction = 60%")).toBeInTheDocument(); 71 | 72 | const secondRow = within(rows[1]); 73 | expect(secondRow.getByText("RGB = (0, 255, 0)")).toBeInTheDocument(); 74 | expect(secondRow.getByText("30%")).toBeInTheDocument(); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/gcf/tests/test_vision_methods.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict, List 16 | from unittest.mock import patch 17 | from google.cloud import vision 18 | 19 | with patch("google.cloud.logging.Client"): 20 | with patch("opentelemetry.exporter.cloud_trace.CloudTraceSpanExporter"): 21 | from main import ( 22 | build_features_list, 23 | get_all_vision_features, 24 | get_feature_by_name, 25 | read_vision_image_from_gcs, 26 | ) 27 | 28 | 29 | def test_read_vision_image_from_gcs(mocker): 30 | # Given 31 | mocker.patch("google.cloud.storage.Client") 32 | mocker.patch("google.cloud.vision.Image") 33 | 34 | bucket_name = "test-bucket" 35 | file_name = "test-image.jpg" 36 | 37 | # When 38 | image = read_vision_image_from_gcs(bucket_name, file_name) 39 | 40 | # Then 41 | assert image is not None 42 | 43 | 44 | def test_get_all_vision_features(): 45 | # When 46 | features = get_all_vision_features() 47 | 48 | # Then 49 | assert isinstance(features, List) 50 | assert all(isinstance(feature, Dict) for feature in features) 51 | assert all( 52 | isinstance(feature["type_"], vision.Feature.Type) for feature in features 53 | ) 54 | 55 | 56 | def test_get_feature_by_name(): 57 | # Given 58 | feature_name = "LABEL_DETECTION" 59 | 60 | # When 61 | feature = get_feature_by_name(feature_name) 62 | 63 | # Then 64 | assert isinstance(feature, vision.Feature.Type) 65 | 66 | # Given 67 | non_existent_feature_name = "NON_EXISTENT" 68 | 69 | # When 70 | non_existent_feature = get_feature_by_name(non_existent_feature_name) 71 | 72 | # Then 73 | assert non_existent_feature is None 74 | 75 | 76 | def test_build_features_list(): 77 | # Given 78 | feature_names = "LABEL_DETECTION, FACE_DETECTION" 79 | 80 | # When 81 | features_list = build_features_list(feature_names) 82 | 83 | # Then 84 | assert features_list == [ 85 | {"type_": vision.Feature.Type.LABEL_DETECTION}, 86 | {"type_": vision.Feature.Type.FACE_DETECTION}, 87 | ] 88 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/App.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { render, waitFor } from "@testing-library/react"; 18 | import "@testing-library/jest-dom"; 19 | import userEvent from "@testing-library/user-event"; 20 | 21 | import App from "App"; 22 | import { ImageSource } from "components/selection/UnifiedImageSelector"; 23 | 24 | describe("App", () => { 25 | test("renders title", async () => { 26 | const { findByText } = render(); 27 | expect(await findByText("Annotate Image")).toBeInTheDocument(); 28 | }); 29 | 30 | test("renders image source options", async () => { 31 | const { findByTestId } = render(); 32 | 33 | // Check if image source options are available 34 | expect( 35 | await findByTestId(`button-${ImageSource.Upload}`) 36 | ).toBeInTheDocument(); 37 | expect( 38 | await findByTestId(`button-${ImageSource.CloudStorage}`) 39 | ).toBeInTheDocument(); 40 | expect(await findByTestId(`button-${ImageSource.URL}`)).toBeInTheDocument(); 41 | }); 42 | 43 | test("renders feature selection", async () => { 44 | const { getByTestId, queryByTestId } = render(); 45 | // Find the ImageSourceToggleSelection buttons 46 | const uploadButton = getByTestId(`button-${ImageSource.Upload}`); 47 | const urlButton = getByTestId(`button-${ImageSource.URL}`); 48 | const cloudStorageButton = getByTestId( 49 | `button-${ImageSource.CloudStorage}` 50 | ); 51 | 52 | // Click on the "URL" button and check if the featureSelection appears 53 | userEvent.click(urlButton); 54 | await waitFor(() => { 55 | const featureSelection = getByTestId("image-feature-selection"); 56 | expect(featureSelection).toBeInTheDocument(); 57 | }); 58 | 59 | // Click on the "Upload" button and check if the featureSelection does not appear 60 | userEvent.click(cloudStorageButton); 61 | await waitFor(() => { 62 | const featureSelection = queryByTestId("image-feature-selection"); 63 | expect(featureSelection).not.toBeInTheDocument(); 64 | }); 65 | 66 | // Click on the "Upload" button and check if the featureSelection appears 67 | userEvent.click(uploadButton); 68 | await waitFor(() => { 69 | const featureSelection = getByTestId("image-feature-selection"); 70 | expect(featureSelection).toBeInTheDocument(); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023-2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # NOTE: This file is automatically generated from values at: 16 | # https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/blob/main/infra/terraform/test-org/org/locals.tf 17 | 18 | name: 'lint' 19 | 20 | on: 21 | workflow_dispatch: 22 | pull_request: 23 | types: [opened, edited, reopened, synchronize] 24 | branches: [main] 25 | 26 | permissions: 27 | contents: read 28 | 29 | concurrency: 30 | group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}' 31 | cancel-in-progress: true 32 | 33 | jobs: 34 | lint: 35 | name: 'lint' 36 | runs-on: 'ubuntu-latest' 37 | steps: 38 | - uses: 'actions/checkout@v6' 39 | - id: variables 40 | run: | 41 | MAKEFILE=$(find . -name Makefile -print -quit) 42 | if [ -z "$MAKEFILE" ]; then 43 | echo dev-tools=gcr.io/cloud-foundation-cicd/cft/developer-tools:1 >> "$GITHUB_OUTPUT" 44 | else 45 | VERSION=$(grep "DOCKER_TAG_VERSION_DEVELOPER_TOOLS := " $MAKEFILE | cut -d\ -f3) 46 | IMAGE=$(grep "DOCKER_IMAGE_DEVELOPER_TOOLS := " $MAKEFILE | cut -d\ -f3) 47 | REGISTRY=$(grep "REGISTRY_URL := " $MAKEFILE | cut -d\ -f3) 48 | echo dev-tools=${REGISTRY}/${IMAGE}:${VERSION} >> "$GITHUB_OUTPUT" 49 | fi 50 | - run: docker run --rm -v ${{ github.workspace }}:/workspace ${{ steps.variables.outputs.dev-tools }} module-swapper 51 | - run: docker run --rm -v ${{ github.workspace }}:/workspace ${{ steps.variables.outputs.dev-tools }} /usr/local/bin/test_lint.sh 52 | commitlint: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v6 56 | with: 57 | fetch-depth: 0 58 | - name: Setup node 59 | uses: actions/setup-node@v6 60 | with: 61 | node-version: lts/* 62 | - name: Install commitlint 63 | run: | 64 | npm install -D @commitlint/cli@20.2.0 @commitlint/config-conventional@20.2.0 65 | echo "module.exports = { extends: ['@commitlint/config-conventional'], rules: {'subject-case': [0], 'header-max-length': [0]} };" > commitlint.config.js 66 | npx commitlint --version 67 | - name: Validate PR commits with commitlint 68 | if: github.event_name == 'pull_request' 69 | env: 70 | TITLE: ${{ github.event.pull_request.title }} 71 | run: 'echo "$TITLE" | npx commitlint --verbose' 72 | -------------------------------------------------------------------------------- /src/frontend/src/components/results/FaceAnnotationsResultView.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { useState } from "react"; 18 | import clsx from "clsx"; 19 | import ConfidenceLabelRow from "components/results/ConfidenceLabelRow"; 20 | import { FaceAnnotation } from "queries"; 21 | import Alert from "components/Alert"; 22 | 23 | export default ({ 24 | annotations, 25 | onIndexSelected, 26 | }: { 27 | annotations: FaceAnnotation[]; 28 | onIndexSelected?: (index?: number) => void; 29 | }) => { 30 | const [hoveredIndex, setHoveredIndex] = useState(null); 31 | 32 | // Sort rows by confidence 33 | const faces = annotations.sort( 34 | (a, b) => b.detectionConfidence - a.detectionConfidence 35 | ); 36 | 37 | return ( 38 |
39 | 40 | Face Detection detects multiple faces within an image along with the 41 | associated key facial attributes such as emotional state or wearing 42 | headwear. 43 | 44 | 45 | {annotations.length === 0 ? ( 46 | 47 | ) : ( 48 | 49 | 50 | {faces.map(({ detectionConfidence }, index) => ( 51 | { 57 | setHoveredIndex(index); 58 | if (onIndexSelected) { 59 | onIndexSelected(index); 60 | } 61 | }} 62 | onMouseLeave={() => { 63 | setHoveredIndex(null); 64 | if (onIndexSelected) { 65 | onIndexSelected(undefined); 66 | } 67 | }} 68 | > 69 | 76 | 77 | ))} 78 | 79 |
70 | 75 |
80 | )} 81 |
82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /infra/variables.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | variable "project_id" { 18 | description = "GCP project ID." 19 | type = string 20 | validation { 21 | condition = var.project_id != "" 22 | error_message = "Error: project_id is required" 23 | } 24 | } 25 | 26 | variable "enable_apis" { 27 | type = string 28 | description = "Whether or not to enable underlying apis in this solution." 29 | default = true 30 | } 31 | 32 | variable "time_to_enable_apis" { 33 | description = "Time to enable APIs, approximate estimate is 5 minutes, can be more." 34 | type = string 35 | default = "30s" 36 | } 37 | 38 | variable "region" { 39 | description = "GCF deployment location/region." 40 | type = string 41 | default = "us-west4" 42 | } 43 | 44 | variable "gcf_max_instance_count" { 45 | type = number 46 | description = "MAX number of GCF instances" 47 | default = 10 48 | } 49 | 50 | variable "gcf_timeout_seconds" { 51 | type = number 52 | description = "GCF execution timeout" 53 | default = 120 54 | } 55 | 56 | variable "gcf_http_ingress_type_index" { 57 | type = number 58 | description = "Ingres type index." 59 | default = 0 # should be 1 or 2 in production environments 60 | # Index values map into:[ALLOW_ALL ALLOW_INTERNAL_ONLY ALLOW_INTERNAL_AND_GCLB] 61 | } 62 | 63 | variable "gcf_require_http_authentication" { 64 | type = bool 65 | description = "Require authentication. Manage authorized users with Cloud IAM." 66 | default = false # should be true in production environments 67 | } 68 | 69 | variable "gcf_annotation_features" { 70 | type = string 71 | description = "Requested annotation features." 72 | default = "FACE_DETECTION,PRODUCT_SEARCH,SAFE_SEARCH_DETECTION" 73 | # options: CROP_HINTS,DOCUMENT_TEXT_DETECTION,FACE_DETECTION,IMAGE_PROPERTIES,LABEL_DETECTION, 74 | # LANDMARK_DETECTION,LOGO_DETECTION,OBJECT_LOCALIZATION,PRODUCT_SEARCH,SAFE_SEARCH_DETECTION, 75 | # TEXT_DETECTION,WEB_DETECTION 76 | } 77 | 78 | variable "gcf_log_level" { 79 | type = string 80 | description = "Set logging level for cloud functions." 81 | default = "" 82 | # options are empty string or python logging level: NOTSET, DEBUG,INFO, WARNING, ERROR, CRITICAL 83 | } 84 | 85 | 86 | variable "labels" { 87 | description = "A map of key/value label pairs to assign to the resources." 88 | type = map(string) 89 | 90 | default = { 91 | app = "terraform-ml-image-annotation-gcf" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/frontend/src/components/selection/FeatureToggleSelection.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { useState, useEffect } from "react"; 18 | import clsx from "clsx"; 19 | 20 | import { 21 | OBJECT_LOCALIZATION, 22 | LABEL_DETECTION, 23 | IMAGE_PROPERTIES, 24 | SAFE_SEARCH_DETECTION, 25 | FACE_DETECTION, 26 | } from "AppConstants"; 27 | 28 | interface Props { 29 | onChange: (features: string[]) => void; 30 | } 31 | 32 | export const FEATURES = [ 33 | { 34 | value: OBJECT_LOCALIZATION, 35 | label: "Object localization", 36 | }, 37 | { 38 | value: LABEL_DETECTION, 39 | label: "Label detection", 40 | }, 41 | { value: IMAGE_PROPERTIES, label: "Image properties" }, 42 | { 43 | value: SAFE_SEARCH_DETECTION, 44 | label: "Safe-search detection", 45 | }, 46 | { value: FACE_DETECTION, label: "Face detection" }, 47 | ]; 48 | 49 | const FeatureSelector = ({ onChange }: Props) => { 50 | const [selectedFeatures, setSelectedFeatures] = useState([ 51 | OBJECT_LOCALIZATION, 52 | LABEL_DETECTION, 53 | IMAGE_PROPERTIES, 54 | SAFE_SEARCH_DETECTION, 55 | FACE_DETECTION, 56 | ]); 57 | 58 | useEffect(() => { 59 | onChange(selectedFeatures); 60 | }, [selectedFeatures]); 61 | 62 | const handleFeatureSelection = ( 63 | event: React.MouseEvent, 64 | newSelection: string[] 65 | ) => { 66 | setSelectedFeatures(newSelection); 67 | }; 68 | 69 | return ( 70 |
71 | {FEATURES.map(({ value, label }) => ( 72 | 94 | ))} 95 |
96 | ); 97 | }; 98 | 99 | export default FeatureSelector; 100 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/ObjectDetectionResultView.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { fireEvent, render } from "@testing-library/react"; 18 | import "@testing-library/jest-dom"; 19 | import ObjectDetectionResultView from "components/results/ObjectDetectionResultView"; 20 | import { vi } from "vitest"; 21 | import { LocalizedObjectAnnotation, Poly } from "queries"; 22 | 23 | describe("ObjectDetectionResultView", () => { 24 | const samplePoly: Poly = { 25 | vertices: [{ x: 0, y: 0 }], 26 | normalizedVertices: [{ x: 0, y: 0 }], 27 | }; 28 | 29 | const annotations: LocalizedObjectAnnotation[] = [ 30 | { 31 | name: "Dog", 32 | mid: "mid1", 33 | score: 0.9, 34 | boundingPoly: samplePoly, 35 | languageCode: "en", 36 | }, 37 | { 38 | name: "Cat", 39 | mid: "mid2", 40 | score: 0.8, 41 | boundingPoly: samplePoly, 42 | languageCode: "en", 43 | }, 44 | { 45 | name: "Bird", 46 | mid: "mid3", 47 | score: 0.7, 48 | boundingPoly: samplePoly, 49 | languageCode: "en", 50 | }, 51 | ]; 52 | 53 | it("displays no objects detected message", () => { 54 | const { getByText } = render( 55 | 56 | ); 57 | expect(getByText("No objects detected.")).toBeInTheDocument(); 58 | }); 59 | 60 | it("displays highest confidence result as top result", () => { 61 | const { getByText } = render( 62 | 66 | ); 67 | expect( 68 | getByText("Image is classified as 'Dog' with 90% confidence.") 69 | ).toBeInTheDocument(); 70 | }); 71 | 72 | it("renders object detections in a table", () => { 73 | const { getAllByRole } = render( 74 | 78 | ); 79 | expect(getAllByRole("row").length == annotations.length).toBeTruthy; 80 | }); 81 | 82 | it("triggers onIndexSelected when row is hovered", () => { 83 | const handleIndexSelected = vi.fn().mockImplementation(() => {}); 84 | 85 | const { getAllByRole } = render( 86 | 91 | ); 92 | const rows = getAllByRole("row"); 93 | fireEvent.mouseEnter(rows[0]); 94 | expect(handleIndexSelected).toHaveBeenCalledWith(0); 95 | fireEvent.mouseLeave(rows[0]); 96 | expect(handleIndexSelected).toHaveBeenCalledWith(undefined); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/frontend/src/components/results/ImagePropertiesResultView.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { ImagePropertiesAnnotation, Color } from "queries"; 18 | 19 | interface ColorRowProps { 20 | index: number; 21 | color: Color; 22 | pixelFraction: number; 23 | } 24 | 25 | export const ColorRow: React.FC = ({ 26 | index, 27 | color, 28 | pixelFraction, 29 | }) => { 30 | const colorString = `${color.red},${color.green},${color.blue}`; 31 | const brightness = 32 | (color.red * 299 + color.green * 587 + color.blue * 114) / 1000; 33 | const maxBrightness = 200; // set max brightness for border color 34 | const borderColorString = 35 | brightness > maxBrightness ? "#BBBBBB" : `rgba(${colorString}, 0.8)`; 36 | 37 | return ( 38 |
39 |
40 | 41 | {`RGB = (${color.red}, ${color.green}, ${color.blue})`} 42 | 43 | 44 | {index === 0 ? "Pixel fraction = " : null} 45 | {(pixelFraction * 100).toFixed(0)}% 46 | 47 |
48 |
52 |
59 |
60 |
61 | ); 62 | }; 63 | 64 | interface ImagePropertiesTableProps { 65 | annotation: ImagePropertiesAnnotation; 66 | } 67 | 68 | const ImagePropertiesTable: React.FC = ({ 69 | annotation, 70 | }) => { 71 | return ( 72 |
73 | 74 | The Image Properties feature detects general attributes of the image, 75 | such as dominant color. 76 | 77 | 78 | 79 | {annotation.dominantColors.colors 80 | .sort((a, b) => b.pixelFraction - a.pixelFraction) 81 | .map(({ color, score, pixelFraction }, index) => ( 82 | 83 | 90 | 91 | ))} 92 | 93 |
84 | 89 |
94 |
95 | ); 96 | }; 97 | 98 | export default ImagePropertiesTable; 99 | -------------------------------------------------------------------------------- /src/frontend/src/components/results/SafeSearchResultView.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import clsx from "clsx"; 18 | import { SafeSearchAnnotation } from "queries"; 19 | 20 | const CONFIDENCE_LEVELS_MAP: { 21 | [key: string]: { label: string; confidencePercent: number }; 22 | } = { 23 | "0": { label: "UNKNOWN", confidencePercent: 0 }, 24 | "1": { label: "VERY UNLIKELY", confidencePercent: 0 }, 25 | "2": { label: "UNLIKELY", confidencePercent: (1 / 4) * 100 }, 26 | "3": { label: "POSSIBLE", confidencePercent: (2 / 4) * 100 }, 27 | "4": { label: "LIKELY", confidencePercent: (3 / 4) * 100 }, 28 | "5": { label: "VERY LIKELY", confidencePercent: (4 / 4) * 100 }, 29 | }; 30 | 31 | const ConfidenceBar = ({ 32 | value, 33 | className, 34 | }: { 35 | value: number; 36 | className?: string; 37 | }) => ( 38 |
39 |
44 |
45 | ); 46 | 47 | const LabelRow = ({ 48 | label, 49 | confidence, 50 | }: { 51 | label: string; 52 | confidence: number; 53 | }) => { 54 | const confidenceLabel: string = 55 | CONFIDENCE_LEVELS_MAP[confidence.toString()]["label"]; 56 | const confidencePercent: number = 57 | CONFIDENCE_LEVELS_MAP[confidence.toString()]["confidencePercent"]; 58 | 59 | return ( 60 | 61 | 62 |
63 | {label} 64 | {confidenceLabel} 65 |
66 | 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default ({ annotation }: { annotation: SafeSearchAnnotation }) => { 73 | return ( 74 |
75 | 76 | SafeSearch Detection detects explicit content such as adult content or 77 | violent content within an image. This feature uses five categories 78 | (adult, spoof, medical, violence, and racy) and returns the likelihood 79 | that each is present in a given image. 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
90 |
91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /infra/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Please note that this file was generated from [terraform-google-module-template](https://github.com/terraform-google-modules/terraform-google-module-template). 16 | # Please make sure to contribute relevant changes upstream! 17 | 18 | # Make will use bash instead of sh 19 | SHELL := /usr/bin/env bash 20 | 21 | DOCKER_TAG_VERSION_DEVELOPER_TOOLS := 1.21 22 | DOCKER_IMAGE_DEVELOPER_TOOLS := cft/developer-tools 23 | REGISTRY_URL := gcr.io/cloud-foundation-cicd 24 | ENABLE_BPMETADATA := 1 25 | export ENABLE_BPMETADATA 26 | 27 | # Enter docker container for local development 28 | .PHONY: docker_run 29 | docker_run: 30 | docker run --rm -it \ 31 | -e SERVICE_ACCOUNT_JSON \ 32 | -v "$(CURDIR)":/workspace \ 33 | $(REGISTRY_URL)/${DOCKER_IMAGE_DEVELOPER_TOOLS}:${DOCKER_TAG_VERSION_DEVELOPER_TOOLS} \ 34 | /bin/bash 35 | 36 | # Execute prepare tests within the docker container 37 | .PHONY: docker_test_prepare 38 | docker_test_prepare: 39 | docker run --rm -it \ 40 | -e SERVICE_ACCOUNT_JSON \ 41 | -e TF_VAR_org_id \ 42 | -e TF_VAR_folder_id \ 43 | -e TF_VAR_billing_account \ 44 | -v "$(CURDIR)":/workspace \ 45 | $(REGISTRY_URL)/${DOCKER_IMAGE_DEVELOPER_TOOLS}:${DOCKER_TAG_VERSION_DEVELOPER_TOOLS} \ 46 | /usr/local/bin/execute_with_credentials.sh prepare_environment 47 | 48 | # Clean up test environment within the docker container 49 | .PHONY: docker_test_cleanup 50 | docker_test_cleanup: 51 | docker run --rm -it \ 52 | -e SERVICE_ACCOUNT_JSON \ 53 | -e TF_VAR_org_id \ 54 | -e TF_VAR_folder_id \ 55 | -e TF_VAR_billing_account \ 56 | -v "$(CURDIR)":/workspace \ 57 | $(REGISTRY_URL)/${DOCKER_IMAGE_DEVELOPER_TOOLS}:${DOCKER_TAG_VERSION_DEVELOPER_TOOLS} \ 58 | /usr/local/bin/execute_with_credentials.sh cleanup_environment 59 | 60 | # Execute lint tests within the docker container 61 | .PHONY: docker_test_lint 62 | docker_test_lint: 63 | docker run --rm -it \ 64 | -e ENABLE_BPMETADATA \ 65 | -e EXCLUDE_LINT_DIRS \ 66 | -v "$(CURDIR)":/workspace \ 67 | $(REGISTRY_URL)/${DOCKER_IMAGE_DEVELOPER_TOOLS}:${DOCKER_TAG_VERSION_DEVELOPER_TOOLS} \ 68 | /usr/local/bin/test_lint.sh 69 | 70 | # Execute lint tests non tty within the docker container 71 | .PHONY: docker_test_lint_gha 72 | docker_test_lint_gha: 73 | docker run --rm \ 74 | -e EXCLUDE_LINT_DIRS \ 75 | -v "$(CURDIR)":/workspace \ 76 | $(REGISTRY_URL)/${DOCKER_IMAGE_DEVELOPER_TOOLS}:${DOCKER_TAG_VERSION_DEVELOPER_TOOLS} \ 77 | /usr/local/bin/test_lint.sh 78 | 79 | # Generate documentation 80 | .PHONY: docker_generate_docs 81 | docker_generate_docs: 82 | docker run --rm -it \ 83 | -e ENABLE_BPMETADATA \ 84 | -v "$(dir ${CURDIR})":/workspace \ 85 | $(REGISTRY_URL)/${DOCKER_IMAGE_DEVELOPER_TOOLS}:${DOCKER_TAG_VERSION_DEVELOPER_TOOLS} \ 86 | /bin/bash -c 'source /usr/local/bin/task_helper_functions.sh && generate_docs "-d -p infra"' 87 | 88 | # Alias for backwards compatibility 89 | .PHONY: generate_docs 90 | generate_docs: docker_generate_docs 91 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/ImageWithBoundingBoxes.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from "react"; 18 | import { render } from "@testing-library/react"; 19 | import "@testing-library/jest-dom"; 20 | import ImageWithBoundingBoxes from "components/ImageWithBoundingBoxes"; 21 | import { FaceAnnotation } from "queries"; 22 | 23 | const imageUrl = "http://example.com/image.jpg"; 24 | 25 | describe("ImageWithBoundingBoxes", () => { 26 | test("renders bounding boxes for objectAnnotations", () => { 27 | // Prepare test data 28 | const objectAnnotations = [ 29 | { 30 | mid: "/m/0jbk", 31 | name: "object", 32 | score: 0.9, 33 | languageCode: "en", 34 | boundingPoly: { 35 | vertices: [ 36 | { x: 10, y: 20 }, 37 | { x: 30, y: 20 }, 38 | { x: 30, y: 60 }, 39 | { x: 10, y: 60 }, 40 | ], 41 | normalizedVertices: [], 42 | }, 43 | }, 44 | ]; 45 | 46 | const { getAllByTestId } = render( 47 | 51 | ); 52 | 53 | // Check if the bounding box is rendered 54 | const boundingBoxes = getAllByTestId("bounding-box"); 55 | expect(boundingBoxes.length).toBe(1); 56 | }); 57 | 58 | test("renders bounding boxes for faceAnnotations", () => { 59 | // Prepare test data 60 | const faceAnnotations: FaceAnnotation[] = [ 61 | { 62 | boundingPoly: { 63 | vertices: [], 64 | normalizedVertices: [], 65 | }, 66 | fdBoundingPoly: { 67 | vertices: [ 68 | { x: 15, y: 25 }, 69 | { x: 35, y: 25 }, 70 | { x: 35, y: 65 }, 71 | { x: 15, y: 65 }, 72 | ], 73 | normalizedVertices: [], 74 | }, 75 | landmarks: [], // Add your landmark data if needed 76 | rollAngle: 0, 77 | panAngle: 0, 78 | tiltAngle: 0, 79 | detectionConfidence: 0.9, 80 | landmarkingConfidence: 0.9, 81 | joyLikelihood: 0, 82 | sorrowLikelihood: 0, 83 | angerLikelihood: 0, 84 | surpriseLikelihood: 0, 85 | underExposedLikelihood: 0, 86 | blurredLikelihood: 0, 87 | headwearLikelihood: 0, 88 | }, 89 | ]; 90 | 91 | const { getAllByTestId } = render( 92 | 96 | ); 97 | 98 | // Check if the bounding box is rendered 99 | const boundingBoxes = getAllByTestId("bounding-box"); 100 | expect(boundingBoxes.length).toBe(1); 101 | }); 102 | 103 | test("renders the image correctly", () => { 104 | const { getByTestId } = render( 105 | 106 | ); 107 | 108 | const imageContainer = getByTestId("image-container"); 109 | expect(imageContainer).toHaveStyle({ 110 | backgroundImage: `url(${imageUrl})`, 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/UnifiedImageSelector.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { fireEvent, getByTestId, render } from "@testing-library/react"; 18 | import "@testing-library/jest-dom"; 19 | import { 20 | ImageSource, 21 | UnifiedImageSelector, 22 | } from "components/selection/UnifiedImageSelector"; 23 | import { QueryClient, QueryClientProvider } from "react-query"; 24 | import { vi } from "vitest"; 25 | 26 | const queryClient = new QueryClient(); 27 | 28 | const Wrapper = ({ children }: { children: React.ReactNode }) => ( 29 | {children} 30 | ); 31 | 32 | describe("UnifiedImageSelector", () => { 33 | const handleFileChangeMock = vi.fn().mockImplementation(() => {}); 34 | const handleAnnotateByUriMock = vi.fn().mockImplementation(() => {}); 35 | const handleAnnotateByImageInfoMock = vi.fn().mockImplementation(() => {}); 36 | 37 | const TEST_IMAGE_URI = "https://example.com/image.jpg"; 38 | 39 | const defaultProps = { 40 | isLoading: false, 41 | imageSource: ImageSource.Upload, 42 | handleFileChange: handleFileChangeMock, 43 | handleAnnotateByUri: handleAnnotateByUriMock, 44 | handleAnnotateByImageInfo: handleAnnotateByImageInfoMock, 45 | }; 46 | 47 | test("renders upload mode", () => { 48 | const { getByTestId } = render(, { 49 | wrapper: Wrapper, 50 | }); 51 | 52 | const input = getByTestId("upload") as HTMLInputElement; 53 | }); 54 | 55 | test("renders URL mode", () => { 56 | const { getByTestId } = render( 57 | , 58 | { wrapper: Wrapper } 59 | ); 60 | expect(getByTestId("uri")).toBeInTheDocument(); 61 | }); 62 | 63 | test("renders CloudStorage mode", () => { 64 | const { getByTestId } = render( 65 | , 69 | { wrapper: Wrapper } 70 | ); 71 | 72 | expect(getByTestId("cloud-storage")).toBeInTheDocument(); 73 | }); 74 | 75 | test("handles file change in upload mode", () => { 76 | const { getByTestId } = render(); 77 | const input = getByTestId("image-file-input") as HTMLInputElement; 78 | 79 | const file = new File(["test"], "test.jpg", { type: "image/jpeg" }); 80 | fireEvent.change(input, { target: { files: [file] } }); 81 | 82 | expect(handleFileChangeMock).toHaveBeenCalledWith(file); 83 | }); 84 | 85 | test("handles Annotate by URI in URL mode", () => { 86 | const { getByTestId, getByText } = render( 87 | , 88 | { wrapper: Wrapper } 89 | ); 90 | const input = getByTestId("image-uri-input") as HTMLInputElement; 91 | const annotateBtn = getByText("Annotate"); 92 | 93 | fireEvent.change(input, { 94 | target: { value: TEST_IMAGE_URI }, 95 | }); 96 | fireEvent.click(annotateBtn); 97 | 98 | expect(handleAnnotateByUriMock).toHaveBeenCalledWith(TEST_IMAGE_URI); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/ResultsContainer.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { ImageAnnotationResult } from "queries"; 18 | import { render, waitFor } from "@testing-library/react"; 19 | import userEvent from "@testing-library/user-event"; 20 | import "@testing-library/jest-dom"; 21 | import ResultContainer from "components/ResultsContainer"; 22 | 23 | const mockImageUrl = "http://example.com/image.jpg"; 24 | const mockResult: ImageAnnotationResult = { 25 | faceAnnotations: [ 26 | { 27 | boundingPoly: { 28 | vertices: [ 29 | { x: 100, y: 100 }, 30 | { x: 200, y: 100 }, 31 | { x: 200, y: 200 }, 32 | { x: 100, y: 200 }, 33 | ], 34 | normalizedVertices: [], 35 | }, 36 | fdBoundingPoly: { 37 | vertices: [ 38 | { x: 100, y: 100 }, 39 | { x: 200, y: 100 }, 40 | { x: 200, y: 200 }, 41 | { x: 100, y: 200 }, 42 | ], 43 | normalizedVertices: [], 44 | }, 45 | landmarks: [], 46 | rollAngle: 0, 47 | panAngle: 0, 48 | tiltAngle: 0, 49 | detectionConfidence: 0.8, 50 | landmarkingConfidence: 0.8, 51 | joyLikelihood: 3, 52 | sorrowLikelihood: 1, 53 | angerLikelihood: 1, 54 | surpriseLikelihood: 1, 55 | underExposedLikelihood: 1, 56 | blurredLikelihood: 1, 57 | headwearLikelihood: 1, 58 | }, 59 | ], 60 | labelAnnotations: [ 61 | { 62 | mid: "mock_mid", 63 | description: "mock_description", 64 | score: 0.9, 65 | locale: "en", 66 | confidence: 0.8, 67 | topicality: 0.7, 68 | properties: [], 69 | }, 70 | ], 71 | safeSearchAnnotation: { 72 | adult: 1, 73 | spoof: 1, 74 | medical: 1, 75 | violence: 1, 76 | racy: 1, 77 | }, 78 | imagePropertiesAnnotation: { 79 | dominantColors: { 80 | colors: [ 81 | { 82 | color: { red: 255, green: 255, blue: 255 }, 83 | score: 0.5, 84 | pixelFraction: 0.5, 85 | }, 86 | ], 87 | }, 88 | }, 89 | localizedObjectAnnotations: [ 90 | { 91 | name: "mock_object", 92 | mid: "mock_mid", 93 | score: 0.8, 94 | boundingPoly: { 95 | vertices: [ 96 | { x: 100, y: 100 }, 97 | { x: 200, y: 100 }, 98 | { x: 200, y: 200 }, 99 | { x: 100, y: 200 }, 100 | ], 101 | normalizedVertices: [], 102 | }, 103 | languageCode: "en", 104 | }, 105 | ], 106 | }; 107 | 108 | test("ResultContainer renders and switches tabs correctly", async () => { 109 | const { getAllByRole, getByTestId } = render( 110 | 111 | ); 112 | 113 | const tabButtons = getAllByRole("tab-button"); 114 | expect(tabButtons).toHaveLength(5); 115 | 116 | // Test tab switching 117 | for (let tabIndex = 0; tabIndex < tabButtons.length; tabIndex++) { 118 | userEvent.click(tabButtons[tabIndex]); 119 | 120 | // Wait for the component to re-render with the updated content 121 | await waitFor(() => { 122 | const tabContent = getByTestId(`tab-content-${tabIndex}`); 123 | expect(tabContent).toBeInTheDocument(); 124 | }); 125 | } 126 | }); 127 | -------------------------------------------------------------------------------- /src/frontend/src/components/results/ObjectDetectionResultView.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import clsx from "clsx"; 18 | import { LocalizedObjectAnnotation } from "queries"; 19 | import ConfidenceLabelRow from "components/results/ConfidenceLabelRow"; 20 | import Alert from "components/Alert"; 21 | 22 | interface Props { 23 | annotations: LocalizedObjectAnnotation[]; 24 | showTopResult: boolean; 25 | onIndexSelected?: (index?: number) => void; 26 | } 27 | 28 | const ResultsTable = ({ 29 | annotations, 30 | showTopResult, 31 | onIndexSelected, 32 | }: Props) => { 33 | // Display message if no objects detected 34 | 35 | // Sort object detections by confidence 36 | const objectDetections = annotations.sort((a, b) => 37 | a.score < b.score ? 1 : -1 38 | ); 39 | 40 | const renderObjectDetections = () => { 41 | // Get highest confidence label and percentage 42 | const highestConfidence = objectDetections[0]; 43 | const label = highestConfidence.name; 44 | const confidencePercentage = (highestConfidence.score * 100).toFixed(0); 45 | 46 | return ( 47 |
48 | {showTopResult && ( 49 | 53 | )} 54 | 55 | 56 | {objectDetections.map(({ name, score }, index) => ( 57 | { 60 | if (onIndexSelected) { 61 | onIndexSelected(index); 62 | } 63 | }} 64 | onMouseLeave={() => { 65 | if (onIndexSelected) { 66 | onIndexSelected(undefined); 67 | } 68 | }} 69 | > 70 | 77 | 78 | ))} 79 | 80 |
71 | 76 |
81 |
82 | ); 83 | }; 84 | 85 | return ( 86 |
87 | 88 | The Vision API can detect and extract multiple objects in an image with 89 | Object Localization. Each result identifies information about the 90 | object, the position of the object, and rectangular bounds for the 91 | region of the image that contains the object. 92 | 93 | {annotations.length === 0 ? ( 94 |
101 | No objects detected. 102 |
103 | ) : ( 104 | renderObjectDetections() 105 | )} 106 |
107 | ); 108 | }; 109 | 110 | export default ResultsTable; 111 | -------------------------------------------------------------------------------- /src/frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /infra/modules/cloudfunctions/metadata.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: blueprints.cloud.google.com/v1alpha1 16 | kind: BlueprintMetadata 17 | metadata: 18 | name: terraform-ml-image-annotation-gcf-cloudfunctions 19 | annotations: 20 | config.kubernetes.io/local-config: "true" 21 | spec: 22 | info: 23 | title: cloudfunctions module 24 | source: 25 | repo: https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf.git 26 | sourceType: git 27 | dir: cloudfunctions 28 | actuationTool: 29 | flavor: Terraform 30 | version: '>= 0.13' 31 | description: {} 32 | content: 33 | examples: 34 | - name: simple_example 35 | location: examples/simple_example 36 | interfaces: 37 | variables: 38 | - name: annotations-bucket 39 | description: Annotations bucket name 40 | varType: string 41 | required: true 42 | - name: gcf_annotation_features 43 | description: Requested annotation features. 44 | varType: string 45 | required: true 46 | - name: gcf_http_ingress_type_index 47 | description: Ingres type index. 48 | varType: number 49 | required: true 50 | - name: gcf_http_ingress_types_list 51 | description: Ingres type values 52 | varType: list(any) 53 | defaultValue: 54 | - ALLOW_ALL 55 | - ALLOW_INTERNAL_ONLY 56 | - ALLOW_INTERNAL_AND_GCLB 57 | - name: gcf_location 58 | description: GCF deployment region 59 | varType: string 60 | required: true 61 | - name: gcf_log_level 62 | description: Set logging level for cloud functions. 63 | varType: string 64 | required: true 65 | - name: gcf_max_instance_count 66 | description: MAX number of GCF instances 67 | varType: number 68 | required: true 69 | - name: gcf_require_http_authentication 70 | description: Create HTTP API with public, unauthorized access. 71 | varType: bool 72 | required: true 73 | - name: gcf_timeout_seconds 74 | description: GCF execution timeout 75 | varType: number 76 | required: true 77 | - name: gcr_invoker_members 78 | description: IAM members. 79 | varType: list(string) 80 | defaultValue: 81 | - allUsers 82 | - name: gcr_role_invoker 83 | description: IAM role GCR invoker. 84 | varType: string 85 | defaultValue: roles/run.invoker 86 | - name: input-bucket 87 | description: Input bucket name 88 | varType: string 89 | required: true 90 | - name: labels 91 | description: A map of key/value label pairs to assign to the resources. 92 | varType: map(string) 93 | required: true 94 | outputs: 95 | - name: annotate_gcs_function_name 96 | description: The name of the cloud function that annotates an image triggered by a GCS event. 97 | - name: function_uri 98 | description: Cloud Function URI and ingress parameters. 99 | - name: gcf_sa 100 | description: Cloud Functions SA. 101 | - name: gcs_account 102 | description: Cloud StorageS SA. 103 | requirements: 104 | roles: 105 | - level: Project 106 | roles: 107 | - roles/owner 108 | services: 109 | - cloudresourcemanager.googleapis.com 110 | - iam.googleapis.com 111 | - storage.googleapis.com 112 | - serviceusage.googleapis.com 113 | -------------------------------------------------------------------------------- /infra/main.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | data "google_project" "project" { 18 | project_id = var.project_id 19 | } 20 | 21 | module "project-services" { 22 | source = "terraform-google-modules/project-factory/google//modules/project_services" 23 | version = "15.0" 24 | disable_services_on_destroy = false 25 | 26 | project_id = var.project_id 27 | enable_apis = var.enable_apis 28 | 29 | activate_apis = [ 30 | "compute.googleapis.com", 31 | # required for GCF operation 32 | "cloudfunctions.googleapis.com", 33 | "logging.googleapis.com", 34 | "artifactregistry.googleapis.com", 35 | "pubsub.googleapis.com", 36 | "cloudbuild.googleapis.com", 37 | "run.googleapis.com", 38 | # Vision API 39 | "vision.googleapis.com", 40 | "appengine.googleapis.com", 41 | # events 42 | "eventarc.googleapis.com", 43 | "storage.googleapis.com", 44 | # other: 45 | "iam.googleapis.com", 46 | "secretmanager.googleapis.com", 47 | ] 48 | 49 | activate_api_identities = [ 50 | { 51 | api = "eventarc.googleapis.com" 52 | roles = [ 53 | "roles/eventarc.serviceAgent", 54 | ] 55 | }, 56 | ] 57 | } 58 | 59 | resource "null_resource" "previous_time" {} 60 | 61 | # gate resource creation until APIs are enabled, using approximate timeout 62 | # if terraform reports an error, run "apply" again 63 | resource "time_sleep" "wait_for_apis" { 64 | depends_on = [ 65 | module.project-services 66 | ] 67 | 68 | create_duration = var.time_to_enable_apis 69 | } 70 | 71 | data "google_compute_zones" "cz_available" { 72 | depends_on = [ 73 | module.project-services 74 | ] 75 | project = var.project_id 76 | region = var.region 77 | } 78 | 79 | # Service Account for GCS, generates/publishes bucket events. 80 | data "google_storage_project_service_account" "gcs_account" { 81 | depends_on = [time_sleep.wait_for_apis] 82 | } 83 | 84 | data "google_compute_default_service_account" "default" { 85 | depends_on = [time_sleep.wait_for_apis] 86 | } 87 | 88 | module "storage" { 89 | source = "./modules/storage" 90 | depends_on = [ 91 | data.google_project.project, 92 | time_sleep.wait_for_apis, # this prevents errors in the initial apply due to APIs not being ready 93 | data.google_compute_default_service_account.default, # gate until this exists, created by the API 94 | data.google_storage_project_service_account.gcs_account, # gate until this exists, created by the API 95 | data.google_compute_zones.cz_available 96 | ] 97 | 98 | gcf_location = var.region 99 | labels = var.labels 100 | } 101 | 102 | module "cloudfunctions" { 103 | source = "./modules/cloudfunctions" 104 | depends_on = [time_sleep.wait_for_apis] 105 | 106 | gcf_location = var.region 107 | gcf_max_instance_count = var.gcf_max_instance_count 108 | gcf_timeout_seconds = var.gcf_timeout_seconds 109 | 110 | input-bucket = module.storage.gcs_input 111 | annotations-bucket = module.storage.gcs_annotations 112 | 113 | gcf_http_ingress_type_index = var.gcf_http_ingress_type_index 114 | gcf_require_http_authentication = var.gcf_require_http_authentication 115 | 116 | gcf_annotation_features = var.gcf_annotation_features 117 | gcf_log_level = var.gcf_log_level 118 | labels = var.labels 119 | } 120 | -------------------------------------------------------------------------------- /infra/metadata.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: blueprints.cloud.google.com/v1alpha1 16 | kind: BlueprintMetadata 17 | metadata: 18 | name: terraform-ml-image-annotation-gcf 19 | annotations: 20 | config.kubernetes.io/local-config: "true" 21 | spec: 22 | info: 23 | title: Infrastructure for image annotation with ML and GCF 24 | source: 25 | repo: https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf.git 26 | sourceType: git 27 | description: 28 | tagline: Sample infrastructure tagline. 29 | detailed: Sample infrastructure detailed description. 30 | architecture: 31 | - Cloud Functions 32 | - Vision API 33 | - Cloud Storage 34 | content: 35 | documentation: 36 | - title: Architecture Diagram 37 | url: todo 38 | subBlueprints: 39 | - name: cloudfunctions 40 | location: modules/cloudfunctions 41 | - name: storage 42 | location: modules/storage 43 | examples: 44 | - name: simple_example 45 | location: examples/simple_example 46 | interfaces: 47 | variables: 48 | - name: gcf_annotation_features 49 | description: Requested annotation features. 50 | varType: string 51 | defaultValue: FACE_DETECTION,PRODUCT_SEARCH,SAFE_SEARCH_DETECTION 52 | - name: gcf_http_ingress_type_index 53 | description: Ingres type index. 54 | varType: number 55 | defaultValue: 0 56 | - name: gcf_log_level 57 | description: Set logging level for cloud functions. 58 | varType: string 59 | defaultValue: "" 60 | - name: gcf_max_instance_count 61 | description: MAX number of GCF instances 62 | varType: number 63 | defaultValue: 10 64 | - name: gcf_require_http_authentication 65 | description: Require authentication. Manage authorized users with Cloud IAM. 66 | varType: bool 67 | defaultValue: false 68 | - name: gcf_timeout_seconds 69 | description: GCF execution timeout 70 | varType: number 71 | defaultValue: 120 72 | - name: labels 73 | description: A map of key/value label pairs to assign to the resources. 74 | varType: map(string) 75 | defaultValue: 76 | app: terraform-ml-image-annotation-gcf 77 | - name: project_id 78 | description: GCP project ID. 79 | varType: string 80 | required: true 81 | - name: region 82 | description: GCF deployment location/region. 83 | varType: string 84 | defaultValue: us-west4 85 | - name: time_to_enable_apis 86 | description: Time to enable APIs, approximate estimate is 5 minutes, can be more. 87 | varType: string 88 | defaultValue: 420s 89 | outputs: 90 | - name: annotate_gcs_function_name 91 | description: The name of the cloud function that annotates an image triggered by a GCS event. 92 | - name: neos_walkthrough_url 93 | description: Neos Tutorial URL 94 | - name: vision_annotations_gcs 95 | description: Output GCS bucket name. 96 | - name: vision_input_gcs 97 | description: Input GCS bucket name. 98 | - name: vision_prediction_url 99 | description: The URL for requesting online prediction with HTTP request. 100 | requirements: 101 | roles: 102 | - level: Project 103 | roles: 104 | - roles/owner 105 | services: 106 | - cloudresourcemanager.googleapis.com 107 | - iam.googleapis.com 108 | - storage.googleapis.com 109 | - serviceusage.googleapis.com 110 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/FaceAnnotationsResultView.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { render, fireEvent } from "@testing-library/react"; 18 | import "@testing-library/jest-dom"; 19 | import { FaceAnnotation } from "queries"; 20 | import FaceAnnotationsResultView from "components/results/FaceAnnotationsResultView"; 21 | import { vi } from "vitest"; 22 | 23 | describe("FaceAnnotationsResultView", () => { 24 | const mockFaceAnnotations: FaceAnnotation[] = [ 25 | { 26 | boundingPoly: { 27 | vertices: [ 28 | { x: 1, y: 1 }, 29 | { x: 2, y: 1 }, 30 | { x: 2, y: 2 }, 31 | { x: 1, y: 2 }, 32 | ], 33 | normalizedVertices: [], 34 | }, 35 | fdBoundingPoly: { 36 | vertices: [ 37 | { x: 1, y: 1 }, 38 | { x: 2, y: 1 }, 39 | { x: 2, y: 2 }, 40 | { x: 1, y: 2 }, 41 | ], 42 | normalizedVertices: [], 43 | }, 44 | landmarks: [], 45 | rollAngle: 0, 46 | panAngle: 0, 47 | tiltAngle: 0, 48 | detectionConfidence: 0.95, 49 | landmarkingConfidence: 0.5, 50 | joyLikelihood: 0.5, 51 | sorrowLikelihood: 0.5, 52 | angerLikelihood: 0.5, 53 | surpriseLikelihood: 0.5, 54 | underExposedLikelihood: 0.5, 55 | blurredLikelihood: 0.5, 56 | headwearLikelihood: 0.5, 57 | }, 58 | { 59 | boundingPoly: { 60 | vertices: [ 61 | { x: 3, y: 3 }, 62 | { x: 4, y: 3 }, 63 | { x: 4, y: 4 }, 64 | { x: 3, y: 4 }, 65 | ], 66 | normalizedVertices: [], 67 | }, 68 | fdBoundingPoly: { 69 | vertices: [ 70 | { x: 3, y: 3 }, 71 | { x: 4, y: 3 }, 72 | { x: 4, y: 4 }, 73 | { x: 3, y: 4 }, 74 | ], 75 | normalizedVertices: [], 76 | }, 77 | landmarks: [], 78 | rollAngle: 0, 79 | panAngle: 0, 80 | tiltAngle: 0, 81 | detectionConfidence: 0.85, 82 | landmarkingConfidence: 0.5, 83 | joyLikelihood: 0.5, 84 | sorrowLikelihood: 0.5, 85 | angerLikelihood: 0.5, 86 | surpriseLikelihood: 0.5, 87 | underExposedLikelihood: 0.5, 88 | blurredLikelihood: 0.5, 89 | headwearLikelihood: 0.5, 90 | }, 91 | ]; 92 | 93 | const mockOnIndexSelected = vi.fn().mockImplementation(() => {}); 94 | 95 | test("renders 'No faces detected' when no annotations are provided", () => { 96 | const { getByText } = render( 97 | 98 | ); 99 | expect(getByText("No faces detected")).toBeInTheDocument(); 100 | }); 101 | 102 | test("renders a table with face annotations", () => { 103 | const { getByRole } = render( 104 | 105 | ); 106 | const table = getByRole("table"); 107 | expect(table).toBeInTheDocument(); 108 | }); 109 | 110 | test("renders correct number of face rows", () => { 111 | const { getAllByRole } = render( 112 | 113 | ); 114 | 115 | // Get all rows 116 | const faceRows = getAllByRole("row"); 117 | 118 | expect(faceRows.length).toEqual(mockFaceAnnotations.length); 119 | }); 120 | 121 | test("calls onIndexSelected with index on mouse enter and with undefined on mouse leave", () => { 122 | const { getAllByRole } = render( 123 | 127 | ); 128 | 129 | // Get all rows 130 | const faceRows = getAllByRole("row"); 131 | fireEvent.mouseEnter(faceRows[0]); 132 | expect(mockOnIndexSelected).toHaveBeenCalledWith(0); 133 | fireEvent.mouseLeave(faceRows[0]); 134 | expect(mockOnIndexSelected).toHaveBeenCalledWith(undefined); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /infra/test/integration/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/HSA-Integration/Annotate-images-with-ML-GCF/infra/test/integration 2 | 3 | go 1.22 4 | 5 | toolchain go1.22.3 6 | 7 | require ( 8 | github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test v0.15.1 9 | github.com/parnurzeal/gorequest v0.3.0 10 | github.com/stretchr/testify v1.9.0 11 | ) 12 | 13 | require ( 14 | cloud.google.com/go v0.110.7 // indirect 15 | cloud.google.com/go/compute v1.23.0 // indirect 16 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 17 | cloud.google.com/go/iam v1.1.2 // indirect 18 | cloud.google.com/go/storage v1.33.0 // indirect 19 | github.com/agext/levenshtein v1.2.3 // indirect 20 | github.com/alexflint/go-filemutex v1.3.0 // indirect 21 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 22 | github.com/aws/aws-sdk-go v1.45.5 // indirect 23 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 25 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a // indirect 26 | github.com/go-errors/errors v1.5.0 // indirect 27 | github.com/go-openapi/jsonpointer v0.20.0 // indirect 28 | github.com/go-openapi/jsonreference v0.20.2 // indirect 29 | github.com/go-openapi/swag v0.22.4 // indirect 30 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 31 | github.com/golang/protobuf v1.5.3 // indirect 32 | github.com/google/gnostic-models v0.6.8 // indirect 33 | github.com/google/go-cmp v0.6.0 // indirect 34 | github.com/google/s2a-go v0.1.7 // indirect 35 | github.com/google/uuid v1.3.1 // indirect 36 | github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect 37 | github.com/googleapis/gax-go/v2 v2.12.0 // indirect 38 | github.com/gruntwork-io/terratest v0.46.15 // indirect 39 | github.com/hashicorp/errwrap v1.1.0 // indirect 40 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 41 | github.com/hashicorp/go-getter v1.7.5 // indirect 42 | github.com/hashicorp/go-multierror v1.1.1 // indirect 43 | github.com/hashicorp/go-safetemp v1.0.0 // indirect 44 | github.com/hashicorp/go-version v1.6.0 // indirect 45 | github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f // indirect 46 | github.com/hashicorp/hcl/v2 v2.20.1 // indirect 47 | github.com/hashicorp/terraform-config-inspect v0.0.0-20240509232506-4708120f8f30 // indirect 48 | github.com/hashicorp/terraform-json v0.22.1 // indirect 49 | github.com/jinzhu/copier v0.4.0 // indirect 50 | github.com/jmespath/go-jmespath v0.4.0 // indirect 51 | github.com/josharian/intern v1.0.0 // indirect 52 | github.com/klauspost/compress v1.16.7 // indirect 53 | github.com/mailru/easyjson v0.7.7 // indirect 54 | github.com/mattn/go-zglob v0.0.4 // indirect 55 | github.com/mitchellh/go-homedir v1.1.0 // indirect 56 | github.com/mitchellh/go-testing-interface v1.14.2-0.20210821155943-2d9075ca8770 // indirect 57 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 58 | github.com/moul/http2curl v1.0.0 // indirect 59 | github.com/pkg/errors v0.9.1 // indirect 60 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 61 | github.com/smartystreets/goconvey v1.8.1 // indirect 62 | github.com/tidwall/gjson v1.17.1 // indirect 63 | github.com/tidwall/match v1.1.1 // indirect 64 | github.com/tidwall/pretty v1.2.1 // indirect 65 | github.com/tidwall/sjson v1.2.5 // indirect 66 | github.com/tmccombs/hcl2json v0.6.0 // indirect 67 | github.com/ulikunitz/xz v0.5.11 // indirect 68 | github.com/zclconf/go-cty v1.14.4 // indirect 69 | go.opencensus.io v0.24.0 // indirect 70 | golang.org/x/crypto v0.21.0 // indirect 71 | golang.org/x/mod v0.17.0 // indirect 72 | golang.org/x/net v0.23.0 // indirect 73 | golang.org/x/oauth2 v0.12.0 // indirect 74 | golang.org/x/sync v0.4.0 // indirect 75 | golang.org/x/sys v0.18.0 // indirect 76 | golang.org/x/text v0.14.0 // indirect 77 | golang.org/x/tools v0.13.0 // indirect 78 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 79 | google.golang.org/api v0.138.0 // indirect 80 | google.golang.org/appengine v1.6.8 // indirect 81 | google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect 82 | google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect 83 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect 84 | google.golang.org/grpc v1.58.3 // indirect 85 | google.golang.org/protobuf v1.33.0 // indirect 86 | gopkg.in/yaml.v3 v3.0.1 // indirect 87 | k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect 88 | sigs.k8s.io/kustomize/kyaml v0.17.1 // indirect 89 | sigs.k8s.io/yaml v1.4.0 // indirect 90 | ) 91 | -------------------------------------------------------------------------------- /src/frontend/src/components/ImageWithBoundingBoxes.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React, { useEffect, useState } from "react"; 18 | import { FaceAnnotation, LocalizedObjectAnnotation, Poly } from "queries"; 19 | import clsx from "clsx"; 20 | 21 | export const BoundingBox = ({ 22 | index, 23 | box, 24 | selectedIndex, 25 | imageWidth, 26 | imageHeight, 27 | }: { 28 | index: number; 29 | box: Poly; 30 | imageWidth: number; 31 | imageHeight: number; 32 | selectedIndex?: number; 33 | }) => { 34 | let percentX: number; 35 | let percentY: number; 36 | let percentWidth: number; 37 | let percentHeight: number; 38 | 39 | if (box.vertices.length == 4) { 40 | percentX = box.vertices[0].x / imageWidth; 41 | percentY = box.vertices[0].y / imageHeight; 42 | percentWidth = (box.vertices[2].x - box.vertices[0].x) / imageWidth; 43 | percentHeight = (box.vertices[2].y - box.vertices[0].y) / imageHeight; 44 | } else if (box.normalizedVertices.length == 4) { 45 | percentX = box.normalizedVertices[0].x; 46 | percentY = box.normalizedVertices[0].y; 47 | percentWidth = box.normalizedVertices[2].x - box.normalizedVertices[0].x; 48 | percentHeight = box.normalizedVertices[2].y - box.normalizedVertices[0].y; 49 | } else { 50 | return null; 51 | } 52 | 53 | return ( 54 |
67 | ); 68 | }; 69 | 70 | const ImageWithBoundingBoxes = ({ 71 | imageUrl, 72 | objectAnnotations, 73 | faceAnnotations, 74 | selectedIndex, 75 | }: { 76 | imageUrl: string; 77 | objectAnnotations?: LocalizedObjectAnnotation[]; 78 | faceAnnotations?: FaceAnnotation[]; 79 | selectedIndex?: number; 80 | }) => { 81 | const [imageSize, setImageSize] = useState({ width: 0, height: 0 }); 82 | 83 | useEffect(() => { 84 | const image = new Image(); 85 | image.onload = () => { 86 | setImageSize({ width: image.width, height: image.height }); 87 | }; 88 | image.src = imageUrl; 89 | }, [imageUrl]); 90 | 91 | let boundingBoxElements: React.ReactNode[] = []; 92 | 93 | if (objectAnnotations != null) { 94 | boundingBoxElements = objectAnnotations.map((annotation, index) => { 95 | const box = annotation.boundingPoly; 96 | 97 | return ( 98 | 106 | ); 107 | }); 108 | } else if (faceAnnotations != null) { 109 | boundingBoxElements = faceAnnotations.map((annotation, index) => { 110 | const box = annotation.fdBoundingPoly; 111 | 112 | return ( 113 | 121 | ); 122 | }); 123 | } 124 | const aspectRatio = imageSize.width / imageSize.height || 0; 125 | const boxClasses = clsx( 126 | "w-full", 127 | "relative", 128 | "bg-no-repeat", 129 | "bg-contain", 130 | "bg-center", 131 | "object-cover" 132 | // Add custom classes or other conditional classes here 133 | ); 134 | 135 | return ( 136 |
145 | {boundingBoxElements} 146 |
147 | ); 148 | }; 149 | 150 | export default ImageWithBoundingBoxes; 151 | -------------------------------------------------------------------------------- /src/frontend/src/tests/components/StickyHeadTable.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { render, fireEvent } from "@testing-library/react"; 18 | import "@testing-library/jest-dom"; 19 | import { StickyHeadTable } from "components/selection/CloudImageSelector"; 20 | import { CloudImageInfo } from "queries"; 21 | import { describe, expect, test, vi } from "vitest"; 22 | 23 | describe("StickyHeadTable", () => { 24 | const infos: CloudImageInfo[] = [ 25 | { imageId: "Image 1", annotation: "annotation 1" }, 26 | { imageId: "Image 2", annotation: "annotation 2" }, 27 | { imageId: "Image 3", annotation: "annotation 3" }, 28 | { imageId: "Image 4", annotation: "annotation 4" }, 29 | { imageId: "Image 5", annotation: "annotation 5" }, 30 | { imageId: "Image 6", annotation: "annotation 6" }, 31 | { imageId: "Image 7", annotation: "annotation 7" }, 32 | { imageId: "Image 8", annotation: "annotation 8" }, 33 | { imageId: "Image 9", annotation: "annotation 9" }, 34 | { imageId: "Image 10", annotation: "annotation 10" }, 35 | ]; 36 | 37 | test("renders loading state correctly", () => { 38 | const { queryByText, queryByRole } = render( 39 | {}} 44 | onRefreshClicked={() => {}} 45 | /> 46 | ); 47 | 48 | expect(queryByText("Loading images from Cloud Storage")).not.toBeNull(); 49 | }); 50 | 51 | test("renders table rows correctly", () => { 52 | const { queryByText, getByText, getAllByRole } = render( 53 | {}} 58 | onRefreshClicked={() => {}} 59 | /> 60 | ); 61 | 62 | expect(queryByText("Loading images from Cloud Storage")).toBeNull(); 63 | expect(getByText("Image 1")).not.toBeNull(); 64 | expect(getByText("Image 2")).not.toBeNull(); 65 | expect(getAllByRole("row")).toHaveLength(6); // 1 header row + 5 data rows 66 | }); 67 | 68 | test("calls onInfoSelected when row is clicked", () => { 69 | const mock = vi.fn().mockImplementation(() => {}); 70 | 71 | const { getByText } = render( 72 | {}} 78 | /> 79 | ); 80 | 81 | fireEvent.click(getByText("Image 2")); 82 | expect(mock).toHaveBeenCalledTimes(1); 83 | expect(mock).toHaveBeenCalledWith(infos[1]); 84 | }); 85 | 86 | test("previous/next buttons disabled when no items exist", () => { 87 | const { getByRole } = render( 88 | {}} 93 | onRefreshClicked={() => {}} 94 | /> 95 | ); 96 | 97 | // Previous button is disabled 98 | expect(getByRole("button", { name: "Previous" })).toBeDisabled(); 99 | expect(getByRole("button", { name: "Next" })).toBeDisabled(); 100 | }); 101 | 102 | test("changes pagination correctly", () => { 103 | const { getByRole } = render( 104 | {}} 109 | onRefreshClicked={() => {}} 110 | /> 111 | ); 112 | 113 | // Previous button is disabled 114 | expect(getByRole("button", { name: "Previous" })).toBeDisabled(); 115 | expect(getByRole("button", { name: "Next" })).not.toBeDisabled(); 116 | 117 | // change page to 1 118 | fireEvent.click(getByRole("button", { name: "Next" })); 119 | expect(getByRole("row", { name: "Image 6" })).not.toBeFalsy(); 120 | 121 | // change page to 0 122 | fireEvent.click(getByRole("button", { name: "Previous" })); 123 | expect(getByRole("row", { name: "Image 1" })).not.toBeFalsy(); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/frontend/src/components/selection/CloudImageSelector.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { getImageInfo, CloudImageInfo } from "queries"; 18 | import { useState } from "react"; 19 | import { useQuery } from "react-query"; 20 | import * as React from "react"; 21 | import clsx from "clsx"; 22 | import Alert from "components/Alert"; 23 | 24 | const rowsPerPage = 5; 25 | 26 | export const StickyHeadTable = ({ 27 | selectedValue, 28 | listInfos, 29 | isLoading, 30 | onInfoSelected, 31 | onRefreshClicked, 32 | }: { 33 | selectedValue?: CloudImageInfo; 34 | listInfos: CloudImageInfo[]; 35 | isLoading: Boolean; 36 | onInfoSelected: (info: CloudImageInfo) => void; 37 | onRefreshClicked: () => void; 38 | }) => { 39 | const [page, setPage] = React.useState(0); 40 | 41 | const handleChangePage = (_event: unknown, newPage: number) => { 42 | setPage(newPage); 43 | }; 44 | 45 | if (isLoading) { 46 | return ; 47 | } 48 | 49 | const startIndex = page * rowsPerPage; 50 | const endIndex = page * rowsPerPage + rowsPerPage; 51 | const infoSlice = listInfos.slice(startIndex, endIndex); 52 | 53 | return ( 54 |
55 | 56 | 57 | 58 | 61 | 62 | 63 | 64 | {infoSlice.map((info, index) => ( 65 | onInfoSelected(info)} 69 | > 70 | 81 | 82 | ))} 83 | 84 |
59 | Select an image below 60 |
79 | {info.imageId} 80 |
85 |
86 | 89 | 97 | 98 | Items {startIndex} to {endIndex} 99 | 100 | 107 |
108 |
109 | ); 110 | }; 111 | 112 | export default ({ 113 | onImageInfoSelected, 114 | }: { 115 | onImageInfoSelected: (info: CloudImageInfo) => void; 116 | }) => { 117 | const [selectedImageInfo, setSelectedImageInfo] = useState(); 118 | const getImageInfoQuery = useQuery( 119 | ["getImageInfo"], 120 | () => { 121 | return getImageInfo().then((infos) => 122 | infos.filter((info) => info.annotation != null) 123 | ); 124 | }, 125 | { refetchOnWindowFocus: false } 126 | ); 127 | 128 | console.log(getImageInfoQuery); 129 | return ( 130 | { 135 | setSelectedImageInfo(info); 136 | onImageInfoSelected(info); 137 | }} 138 | onRefreshClicked={() => { 139 | getImageInfoQuery.refetch(); 140 | }} 141 | /> 142 | ); 143 | }; 144 | -------------------------------------------------------------------------------- /src/frontend/src/components/selection/UnifiedImageSelector.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import clsx from "clsx"; 18 | import { useEffect, useState } from "react"; 19 | import CloudImageInfoSelector from "components/selection/CloudImageSelector"; 20 | import { CloudImageInfo as CloudImageInfo } from "queries"; 21 | 22 | export enum ImageSource { 23 | Upload, 24 | URL, 25 | CloudStorage, 26 | } 27 | 28 | type ImageSelectorProps = { 29 | isLoading: boolean; 30 | imageSource: ImageSource; 31 | handleFileChange: (file: File | null) => void; 32 | handleAnnotateByUri: (uri: string) => void; 33 | handleAnnotateByImageInfo: (info: CloudImageInfo) => void; 34 | }; 35 | 36 | const AnnotateByUri = ({ 37 | imageUri, 38 | isButtonDisabled, 39 | onImageUriChanged, 40 | onConfirmClicked, 41 | }: { 42 | imageUri: string; 43 | isButtonDisabled: boolean; 44 | onImageUriChanged: (text: string) => void; 45 | onConfirmClicked?: () => void; 46 | }) => { 47 | return ( 48 |
49 |
50 | 51 | ) => { 55 | onImageUriChanged(event.target.value); 56 | }} 57 | className="w-full mb-2 input input-bordered" 58 | onKeyUp={(event) => { 59 | if (event.key === "Enter" && onConfirmClicked != null) { 60 | onConfirmClicked(); 61 | } 62 | }} 63 | /> 64 |
65 | 80 |
81 | ); 82 | }; 83 | 84 | export const UnifiedImageSelector = ({ 85 | isLoading, 86 | imageSource, 87 | handleFileChange, 88 | handleAnnotateByUri, 89 | handleAnnotateByImageInfo, 90 | }: ImageSelectorProps) => { 91 | const [imageUri, setImageUri] = useState(""); 92 | 93 | const renderUpload = () => ( 94 |
95 |
96 | 97 | ) => { 102 | const files = event.target.files; 103 | if (files && files.length > 0) { 104 | handleFileChange(files[0]); 105 | } 106 | }} 107 | className="w-full file-input input-bordered max-w-md" 108 | /> 109 |
110 |
111 | ); 112 | 113 | const renderURL = () => ( 114 |
115 | setImageUri(text)} 118 | isButtonDisabled={imageUri.length == 0 || isLoading} 119 | onConfirmClicked={() => handleAnnotateByUri(imageUri)} 120 | /> 121 |
122 | ); 123 | 124 | const renderCloudStorage = () => ( 125 |
126 | 127 |
128 | ); 129 | 130 | // Fetch info on init 131 | useEffect(() => { 132 | console.log(`UnifiedImageSelector init: imageSource = ${imageSource}`); 133 | }, []); 134 | 135 | useEffect(() => { 136 | console.log(`UnifiedImageSelector:imageSource changed to ${imageSource}`); 137 | }, [imageSource]); 138 | 139 | switch (imageSource) { 140 | case ImageSource.Upload: 141 | return renderUpload(); 142 | case ImageSource.URL: 143 | return renderURL(); 144 | case ImageSource.CloudStorage: 145 | return renderCloudStorage(); 146 | default: 147 | return null; 148 | } 149 | }; 150 | -------------------------------------------------------------------------------- /infra/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code Reviews 20 | 21 | All submissions, including submissions by project members, require review. We 22 | use GitHub pull requests for this purpose. Consult 23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 24 | information on using pull requests. 25 | 26 | ## Development 27 | 28 | The following dependencies must be installed on the development system: 29 | 30 | - [Docker Engine][docker-engine] 31 | - [Google Cloud SDK][google-cloud-sdk] 32 | - [make] 33 | 34 | ### Generating Documentation for Inputs and Outputs 35 | 36 | The Inputs and Outputs tables in the READMEs of the root module, 37 | submodules, and example modules are automatically generated based on 38 | the `variables` and `outputs` of the respective modules. These tables 39 | must be refreshed if the module interfaces are changed. 40 | 41 | #### Execution 42 | 43 | Run `make generate_docs` to generate new Inputs and Outputs tables. 44 | 45 | ### Integration Testing 46 | 47 | Integration tests are used to verify the behavior of each stage in this repo. 48 | Additions, changes, and fixes should be accompanied with tests. 49 | 50 | The integration tests are run using the [Blueprint test][blueprint-test] framework. The framework is packaged within a Docker image for convenience. 51 | 52 | The general strategy for these tests is to verify the behaviour of the 53 | [example modules](./examples/), thus ensuring that the root module, 54 | submodules, and example modules are all functionally correct. 55 | 56 | #### Test Environment 57 | The easiest way to test the module is in an isolated test project. The setup for such a project is defined in [test/setup](./test/setup/) directory. 58 | 59 | To use this setup, you need a service account with these permissions (on a Folder or Organization): 60 | - Project Creator 61 | - Project Billing Manager 62 | 63 | The project that the service account belongs to must have the following APIs enabled (the setup won't 64 | create any resources on the service account's project): 65 | - Cloud Resource Manager 66 | - Cloud Billing 67 | - Service Usage 68 | - Identity and Access Management (IAM) 69 | 70 | Export the Service Account credentials to your environment like so: 71 | 72 | ``` 73 | export SERVICE_ACCOUNT_JSON=$(< credentials.json) 74 | ``` 75 | 76 | You will also need to set a few environment variables: 77 | ``` 78 | export TF_VAR_org_id="your_org_id" 79 | export TF_VAR_folder_id="your_folder_id" 80 | export TF_VAR_billing_account="your_billing_account_id" 81 | ``` 82 | 83 | With these settings in place, you can prepare a test project using Docker: 84 | ``` 85 | make docker_test_prepare 86 | ``` 87 | 88 | #### Interactive Execution 89 | 90 | 1. Run `make docker_run` to start the testing Docker container in 91 | interactive mode. 92 | 93 | 1. Run `cd test/integration` to go to the integration test directory. 94 | 95 | 1. Run `cft test list --test-dir /workspace/test/integration` to list the available test. 96 | 97 | 1. Run `cft test run --stage init --verbose` to initialize the working 98 | directory for the stage. 99 | 100 | 1. Run `cft test run --stage apply --verbose` to apply the stage. 101 | 102 | 1. Run `cft test run --stage verify --verbose ` to test the resources created in the current stage. 103 | 104 | After iterating on `verify` stage, you can teardown resources. 105 | 106 | 1. Run `cft test run --stage destroy --verbose ` to destroy the stage. 107 | 108 | ### Linting and Formatting 109 | 110 | Many of the files in the repository can be linted or formatted to 111 | maintain a standard of quality. 112 | 113 | #### Execution 114 | 115 | Run `make docker_test_lint`. 116 | 117 | [docker-engine]: https://www.docker.com/products/docker-engine 118 | [flake8]: https://flake8.pycqa.org/en/latest/ 119 | [fmt]: https://www.terraform.io/cli/commands/fmt 120 | [gofmt]: https://golang.org/cmd/gofmt/ 121 | [google-cloud-sdk]: https://cloud.google.com/sdk/install 122 | [hadolint]: https://github.com/hadolint/hadolint 123 | [make]: https://en.wikipedia.org/wiki/Make_(software) 124 | [shellcheck]: https://www.shellcheck.net/ 125 | [terraform-docs]: https://github.com/segmentio/terraform-docs 126 | [terraform]: https://terraform.io/ 127 | [blueprint-test]: https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/tree/master/infra/blueprint-test 128 | -------------------------------------------------------------------------------- /src/frontend/src/components/ResultsContainer.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React, { useState } from "react"; 18 | import clsx from "clsx"; 19 | import { ImageAnnotationResult } from "queries"; 20 | import ImageWithBoundingBoxes from "components/ImageWithBoundingBoxes"; 21 | import LabelDetectionResultView from "components/results/LabelDetectionResultView"; 22 | import ObjectDetectionResultView from "components/results/ObjectDetectionResultView"; 23 | import SafeSearchResultView from "components/results/SafeSearchResultView"; 24 | import ImagePropertiesResultView from "components/results/ImagePropertiesResultView"; 25 | import FaceAnnotationsResultView from "components/results/FaceAnnotationsResultView"; 26 | 27 | const tabLabels: { [key: string]: keyof ImageAnnotationResult } = { 28 | Objects: "localizedObjectAnnotations", 29 | Labels: "labelAnnotations", 30 | Properties: "imagePropertiesAnnotation", 31 | "Safe Search": "safeSearchAnnotation", 32 | Faces: "faceAnnotations", 33 | }; 34 | 35 | const ResultContainer = ({ 36 | result, 37 | imageUrl, 38 | }: { 39 | imageUrl: string; 40 | result: ImageAnnotationResult; 41 | }) => { 42 | const [selectedTab, setSelectedTab] = useState(0); 43 | const [selectedIndex, setSelectedIndex] = useState(); 44 | 45 | const handleChange = (newValue: number) => { 46 | setSelectedTab(newValue); 47 | }; 48 | 49 | return ( 50 |
51 |
52 |
53 | 63 |
64 |
65 |
66 | {Object.keys(tabLabels).map((label, index) => ( 67 | 82 | ))} 83 |
84 |
85 | {selectedTab === 0 && result.localizedObjectAnnotations != null && ( 86 |
87 | setSelectedIndex(index)} 91 | /> 92 |
93 | )} 94 | {selectedTab === 1 && result.labelAnnotations != null && ( 95 |
96 | 99 |
100 | )} 101 | {selectedTab === 2 && result.imagePropertiesAnnotation != null && ( 102 |
103 | 106 |
107 | )} 108 | {selectedTab === 3 && result.safeSearchAnnotation != null && ( 109 |
110 | 113 |
114 | )} 115 | {selectedTab === 4 && result.faceAnnotations != null && ( 116 |
117 | setSelectedIndex(index)} 120 | /> 121 |
122 | )} 123 |
124 |
125 |
126 |
127 | ); 128 | }; 129 | 130 | export default ResultContainer; 131 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.3.0](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/compare/v0.2.0...v0.3.0) (2024-04-29) 4 | 5 | 6 | ### Features 7 | 8 | * add architecture diagram link ([#140](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/140)) ([3e96c59](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/3e96c5993d9eae6d65188ded7fedfb09a8d2662d)) 9 | * allow list in features parameter ([#128](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/128)) ([63ef69e](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/63ef69e3e37b5c22864966d93d1e70f6d07d2617)) 10 | * move api to project services and reduce wait ([#112](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/112)) ([56aac98](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/56aac98ebc9e2958683c0e59de707e1eecc1febe)) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **deps:** update dependency daisyui to v4 ([#162](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/162)) ([c27c2ec](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/c27c2eca78befa0e71c7f9c8cce25995a0847a49)) 16 | * **deps:** update dependency eslint-config-prettier to v9 ([#61](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/61)) ([30ac5f2](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/30ac5f22a07b608b89a171a9f4833b3407b2fdbd)) 17 | * **deps:** Update Terraform time to ~> 0.10.0 ([#126](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/126)) ([42fb307](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/42fb3079821943aa8c9c6406e3f03cbb4aea193a)) 18 | * disabling module swapper as a quick fix ([#113](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/113)) ([dbefe4d](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/dbefe4d67eec435737f38ef682fe62a4e6e79fe0)) 19 | 20 | ## [0.2.0](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/compare/v0.1.2...v0.2.0) (2023-12-19) 21 | 22 | 23 | ### Features 24 | 25 | * allow json post data ([#100](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/100)) ([cf0f484](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/cf0f4844fb85719dde0c668d2c64047a9da86a94)) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * **deps:** update module github.com/googlecloudplatform/cloud-foundation-toolkit/infra/blueprint-test to v0.8.1 ([#56](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/56)) ([2d9318d](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/2d9318de8b657ffc93a2cc1d10654f354124854c)) 31 | 32 | ## [0.1.2](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/compare/v0.1.1...v0.1.2) (2023-07-26) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * **deps:** update dependency clsx to v2 ([#51](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/51)) ([bfc7734](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/bfc7734873c032d9d63edf819bf3fbf29d426284)) 38 | * pinning google provider < 4.75.0 ([#53](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/53)) ([a5d543d](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/a5d543d14fe87d8a28fbb774388d260ce3996f57)) 39 | 40 | ## [0.1.1](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/compare/v0.1.0...v0.1.1) (2023-07-20) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * Added labels to TF config ([#23](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/23)) ([c744eee](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/c744eee3001a26b0bdcb0df3e432793d22331bcd)) 46 | * change var.gcf_location to region ([#24](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/24)) ([e7cc9d3](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/e7cc9d32bb71bc87b93dfedae81243a8ee13e9cd)) 47 | * **deps:** update dependency daisyui to v3 ([#48](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/48)) ([abae46c](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/abae46c37cc385b9dd6ed30553fd54818ca9bdc5)) 48 | * fixes eventarc enablement on first deploy of cloud function ([#21](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/21)) ([bd15c4f](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/bd15c4f085f1c18d7c8ed7e8c2e79efbd8f79665)) 49 | * Removed tracing ([#27](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/27)) ([435f83a](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/435f83abb92d9f4978cddae0a5a71d250b914270)) 50 | 51 | ## 0.1.0 (2023-06-22) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * Update int.cloudbuild.yaml to use LR billing ([#17](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/17)) ([62dfef5](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/62dfef50f7b7898cf46f94e8e19b4a464255b906)) 57 | * update metadata generation and add metadata ([#9](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/9)) ([107140b](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/107140bb21b33a610288d1a392f188949b063281)) 58 | * update neos toc url ([#20](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/issues/20)) ([2a7304c](https://github.com/GoogleCloudPlatform/terraform-ml-image-annotation-gcf/commit/2a7304c71f16dc62da8a2eea37c75b2a2548cec5)) 59 | -------------------------------------------------------------------------------- /src/frontend/src/queries.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import axios from "axios"; 18 | 19 | const client = axios.create({ baseURL: import.meta.env.VITE_API_SERVER }); 20 | 21 | export interface Color { 22 | red: number; 23 | green: number; 24 | blue: number; 25 | } 26 | export interface ImagePropertiesAnnotation { 27 | dominantColors: { 28 | colors: { color: Color; score: number; pixelFraction: number }[]; 29 | }; 30 | } 31 | 32 | interface Vertex { 33 | x: number; 34 | y: number; 35 | } 36 | export interface Poly { 37 | vertices: Vertex[]; 38 | normalizedVertices: Vertex[]; // What is this? 39 | } 40 | 41 | export interface Annotation { 42 | mid: string; 43 | description: string; 44 | score: number; 45 | locale: string; 46 | confidence: number; 47 | topicality: number; 48 | properties: any[]; // What is this? 49 | } 50 | 51 | interface Landmark { 52 | type: number; 53 | position: { x: number; y: number; z: number }; 54 | } 55 | 56 | export interface FaceAnnotation { 57 | boundingPoly: Poly; 58 | fdBoundingPoly: Poly; 59 | landmarks: Landmark[]; 60 | rollAngle: number; 61 | panAngle: number; 62 | tiltAngle: number; 63 | detectionConfidence: number; 64 | landmarkingConfidence: number; 65 | joyLikelihood: number; 66 | sorrowLikelihood: number; 67 | angerLikelihood: number; 68 | surpriseLikelihood: number; 69 | underExposedLikelihood: number; 70 | blurredLikelihood: number; 71 | headwearLikelihood: number; 72 | } 73 | 74 | export interface LocalizedObjectAnnotation { 75 | name: string; 76 | mid: string; 77 | score: number; 78 | boundingPoly: Poly; 79 | languageCode: string; 80 | } 81 | interface Location { 82 | latLng: { latitude: number; longitude: number }; 83 | } 84 | 85 | export interface LandmarkAnnotation extends Annotation { 86 | boundingPoly: Poly; 87 | locations: Location[]; 88 | } 89 | 90 | export interface SafeSearchAnnotation { 91 | adult: number; 92 | spoof: number; 93 | medical: number; 94 | violence: number; 95 | racy: number; 96 | } 97 | export interface ImageAnnotationResult { 98 | faceAnnotations?: FaceAnnotation[]; 99 | landmarkAnnotations?: LandmarkAnnotation[]; 100 | labelAnnotations?: Annotation[]; 101 | textAnnotations?: LandmarkAnnotation[]; 102 | safeSearchAnnotation?: SafeSearchAnnotation; 103 | imagePropertiesAnnotation?: ImagePropertiesAnnotation; 104 | localizedObjectAnnotations?: LocalizedObjectAnnotation[]; 105 | 106 | cropHintsAnnotation?: any; 107 | fullTextAnnotation?: any; 108 | webDetection?: any; 109 | logoAnnotations?: any; 110 | 111 | error?: string; 112 | } 113 | 114 | const ALL_TYPES = 115 | "CROP_HINTS,DOCUMENT_TEXT_DETECTION,FACE_DETECTION,IMAGE_PROPERTIES,LABEL_DETECTION,LANDMARK_DETECTION,LOGO_DETECTION,OBJECT_LOCALIZATION,PRODUCT_SEARCH,SAFE_SEARCH_DETECTION,TEXT_DETECTION,TYPE_UNSPECIFIED,WEB_DETECTION"; 116 | 117 | const LIMITED_TYPES = 118 | "FACE_DETECTION,IMAGE_PROPERTIES,LABEL_DETECTION,OBJECT_LOCALIZATION,SAFE_SEARCH_DETECTION"; 119 | 120 | export async function annotateImageByFile( 121 | file: File, 122 | features: string[] 123 | ): Promise { 124 | const formData = new FormData(); 125 | formData.append("image", file); 126 | formData.append("features", features.join(",")); 127 | return client 128 | .post("/annotate", formData) 129 | .then((response) => response.data); 130 | } 131 | 132 | export async function annotateImageByUri( 133 | imageUri: string, 134 | features: string[] 135 | ): Promise { 136 | const formData = new FormData(); 137 | formData.append("image_uri", imageUri); 138 | formData.append("features", features.join(",")); 139 | return client 140 | .post("/annotate", formData) 141 | .then((response) => response.data); 142 | } 143 | 144 | export async function annotateImageByCloudImageInfo( 145 | info: CloudImageInfo 146 | ): Promise { 147 | const annotation = info.annotation; 148 | 149 | if (annotation != null) { 150 | return client 151 | .get(`/bucket/annotation/${annotation}`, { 152 | params: { image_uri: info.annotation }, 153 | }) 154 | .then((response) => response.data); 155 | } else { 156 | throw Error("No annotation exists for this image"); 157 | } 158 | } 159 | 160 | export interface ListInfoDictionary { 161 | [key: string]: { annotation?: string; image: string }; 162 | } 163 | export interface CloudImageInfo { 164 | imageId: string; 165 | annotation?: string; 166 | } 167 | 168 | export async function getImageInfo( 169 | start?: number, 170 | end?: number 171 | ): Promise { 172 | return client 173 | .get("/bucket/list", { 174 | params: { start, end }, 175 | }) 176 | .then((response) => response.data) 177 | .then((listInfoDict) => 178 | Object.entries(listInfoDict).map(([key, value]) => { 179 | return { 180 | ...value, 181 | imageId: key, 182 | }; 183 | }) 184 | ); 185 | } 186 | 187 | export function getImageDataURL(info: CloudImageInfo): string { 188 | return `${import.meta.env.VITE_API_SERVER}/bucket/imagedata/${info.imageId}`; 189 | } 190 | -------------------------------------------------------------------------------- /.github/workflows/periodic-reporter.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023-2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # NOTE: This file is automatically generated from: 16 | # https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/blob/main/infra/terraform/modules/workflow_files/periodic-reporter.yaml 17 | 18 | name: 'reporter' 19 | 20 | on: 21 | schedule: 22 | # 2 hours after scheduled periodic and once again in the evening 23 | - cron: '0 5,17 * * *' 24 | workflow_dispatch: 25 | 26 | jobs: 27 | report: 28 | if: github.repository_owner == 'GoogleCloudPlatform' || github.repository_owner == 'terraform-google-modules' 29 | 30 | permissions: 31 | issues: 'write' 32 | 33 | runs-on: 'ubuntu-latest' 34 | 35 | steps: 36 | - uses: 'actions/github-script@v8' 37 | with: 38 | script: |- 39 | // label for all issues opened by reporter 40 | const periodicLabel = 'periodic-failure'; 41 | 42 | // check if any reporter opened any issues previously 43 | const prevIssues = await github.paginate(github.rest.issues.listForRepo, { 44 | ...context.repo, 45 | state: 'open', 46 | creator: 'github-actions[bot]', 47 | labels: [periodicLabel] 48 | }); 49 | // createOrCommentIssue creates a new issue or comments on an existing issue. 50 | const createOrCommentIssue = async function (title, txt) { 51 | if (prevIssues.length < 1) { 52 | console.log('no previous issues found, creating one'); 53 | await github.rest.issues.create({ 54 | ...context.repo, 55 | title: title, 56 | body: txt, 57 | labels: [periodicLabel] 58 | }); 59 | return; 60 | } 61 | if (prevIssues.length > 1) { 62 | console.warn( 63 | `found ${prevIssues.length} issues but only adding comment to ${prevIssues[0].html_url}` 64 | ); 65 | } 66 | console.log( 67 | `found previous issue ${prevIssues[0].html_url}, adding comment` 68 | ); 69 | await github.rest.issues.createComment({ 70 | ...context.repo, 71 | issue_number: prevIssues[0].number, 72 | body: txt 73 | }); 74 | }; 75 | 76 | // updateAndCloseIssues comments on any existing issues and closes them. No-op if no issue exists. 77 | const updateAndCloseIssues = async function (txt) { 78 | if (prevIssues.length < 1) { 79 | console.log('no previous issues found, skipping close'); 80 | return; 81 | } 82 | for (const prevIssue of prevIssues) { 83 | console.log(`found previous issue ${prevIssue.html_url}, adding comment`); 84 | await github.rest.issues.createComment({ 85 | ...context.repo, 86 | issue_number: prevIssue.number, 87 | body: txt 88 | }); 89 | console.log(`closing ${prevIssue.html_url}`); 90 | await github.rest.issues.update({ 91 | ...context.repo, 92 | issue_number: prevIssue.number, 93 | body: txt, 94 | state: 'closed' 95 | }); 96 | } 97 | }; 98 | 99 | // Find status of check runs. 100 | // We will find check runs for each commit and then filter for the periodic. 101 | // Checks API only allows for ref and if we use main there could be edge cases where 102 | // the check run happened on a SHA that is different from head. 103 | const commits = await github.paginate(github.rest.repos.listCommits, { 104 | ...context.repo 105 | }); 106 | 107 | var foundCheck = false; 108 | let periodicCheck = {}; 109 | 110 | for (const commit of commits) { 111 | console.log( 112 | `checking runs at ${commit.html_url}: ${commit.commit.message}` 113 | ); 114 | const checks = await github.rest.checks.listForRef({ 115 | ...context.repo, 116 | ref: commit.sha 117 | }); 118 | // find runs for this commit 119 | for (const check of checks.data.check_runs) { 120 | console.log(`found run ${check.name} for ${commit.html_url}`); 121 | if (check.name.includes('periodic-int-trigger')) { 122 | foundCheck = true; 123 | periodicCheck = check; 124 | break; 125 | } 126 | } 127 | 128 | if (foundCheck) { 129 | if ( 130 | periodicCheck.status === 'completed' && 131 | periodicCheck.conclusion === 'success' 132 | ) { 133 | updateAndCloseIssues( 134 | `[Passing periodic](${periodicCheck.html_url}) at ${commit.html_url}. Closing this issue.` 135 | ); 136 | } else if (periodicCheck.status === 'in_progress') { 137 | console.log( 138 | `Check is pending ${periodicCheck.html_url} for ${commit.html_url}. Retry again later.` 139 | ); 140 | } 141 | // error case 142 | else { 143 | createOrCommentIssue( 144 | 'Failing periodic', 145 | `[Failing periodic](${periodicCheck.html_url}) at ${commit.html_url}.` 146 | ); 147 | } 148 | // exit early as check was found 149 | return; 150 | } 151 | } 152 | 153 | // no periodic-int-trigger checks found across all commits, report it 154 | createOrCommentIssue( 155 | 'Missing periodic', 156 | `Periodic test has not run in the past 24hrs. Last checked from ${ 157 | commits[0].html_url 158 | } to ${commits[commits.length - 1].html_url}.` 159 | ); 160 | --------------------------------------------------------------------------------