G-gen の福井です。当記事では、Google が提供するマルチモーダル生成 AI モデル Geminiと、画像生成 AI モデル Imagen を使用して、アップロード画像から類似画像を生成する Web アプリを開発する手順をご紹介します。
はじめに
当記事の概要
当記事では、Google が提供するマルチモーダル生成 AI モデル Geminiと、画像生成 AI モデル Imagen を使用して、アップロードした画像から類似する画像を生成する Web アプリを開発してみます。
このアプリでは、以下の機能を提供します。
- 画像の特徴を抽出するためのテキストプロンプトを入力するインターフェイス
- Gemini 1.5 Pro を使用して、アップロードした画像の特徴を抽出
- 抽出した画像の特徴情報を編集
- Imagen 2 を使用して、抽出した画像の特徴情報から類似画像を生成
実行イメージ
Web インターフェイスで、プロンプトをテキストで入力し、画像をアップロードします。その後「アップロード画像の特徴抽出」ボタンを押下すると、アップロードした画像の特徴が表示されます。
その後さらに「画像の特徴から類似画像を生成」ボタンを押下すると、画像の特徴情報をもとにした類似画像が生成されます。
利用サービス・ライブラリ
当記事では、以下の要素を使ってアプリを開発しました。
Gemini Pro
- Google が提供する生成 AI モデルであり、テキストや画像、動画などの複数の種類のデータを扱うことができるマルチモーダルな生成 AI モデルです。
- 参考 : Gemini Proを使ってみた。Googleの最新生成AIモデル
Imagen
- Google が提供する画像生成 AI モデルです。
- 2024年7月現在、Imagen を使用するためには申請が必要となります。
- 参考 : Imagenを使ったシンプルな画像生成AIアプリを開発してみた
Gradio
- 機械学習 Web アプリを容易に構築できる Python フレームワークです。
- 参考 : Gradio Docs - Blocks
Cloud Run
- Google Cloud の、コンテナを実行のためのフルマネージドサービス
- 参考 : Cloud Run を徹底解説!
ソースコード
Python のバージョン
当記事では、Python 3.12.4 を使って開発しています。
$ python --version Python 3.12.4
requirements.txt
使用するライブラリを、以下のとおり requirements.txt
に定義します。
google-cloud-aiplatform==1.56.0 google-generativeai==0.5.4 gradio==4.37.2
main.py
開発したコードの全文を以下に記載します。
import os import sys import traceback import gradio as gr import gradio.blocks as blocks import vertexai from vertexai.preview.generative_models import GenerationConfig, GenerativeModel, Part from vertexai.preview.vision_models import ImageGenerationModel SUPPORTED_IMAGE_EXTENSIONS = { "png", "jpeg", "jpg", } MAX_PROMPT_SIZE_MB = 4.0 multimodal_model = None # 初期化処理 def initialize_variables() -> None: global multimodal_model try: project_id = os.environ.get("PROJECT_ID") if not project_id: raise ValueError("環境変数に「PROJECT_ID」が設定されていません") location = os.environ.get("LOCATION") if not location: raise ValueError("環境変数に「LOCATION」が設定されていません") # Vertex AI インスタンスの初期化 vertexai.init(project=project_id, location=location) # Gemini モデルの初期化 multimodal_model = GenerativeModel("gemini-1.5-pro-001") except Exception: print(traceback.format_exc()) raise # ファイルの拡張子を取得 def get_file_extension(file_path: str) -> str: _, extension = os.path.splitext(file_path) return extension[1:] # extension がサポートされているか判定 def is_supported_image_extensions(extension: str) -> bool: return extension in SUPPORTED_IMAGE_EXTENSIONS # mime_type を取得 def get_mime_type(extension: str) -> str: # サポートされていない拡張子の場合 if not is_supported_image_extensions(extension): raise ValueError(f'サポートしている形式は {", ".join(SUPPORTED_IMAGE_EXTENSIONS)} です。') return "image/jpeg" if extension in ["jpg", "jpeg"] else f"image/{extension}" # プロンプトサイズの計算 def calculate_prompt_size_mb(text: str, file_path: str) -> float: # テキストサイズをバイト単位で取得 text_size_bytes = sys.getsizeof(text) # ファイルサイズをバイト単位で取得 file_size_bytes = os.path.getsize(file_path) # バイトからメガバイトに単位変換 prompt_size_mb = (text_size_bytes + file_size_bytes) / 1048576 return prompt_size_mb # Gemini からの出力を取得 def extraction_image_feature( prompt: str, file_path: str, temperature: float, max_output_tokens: int, top_k: int, top_p: float, ) -> str: try: # テキストと画像の両方が入力されていない場合 if not prompt or not file_path: raise ValueError("テキストと画像を入力して下さい。") # プロンプトサイズを取得 prompt_size_mb = calculate_prompt_size_mb(text=prompt, file_path=file_path) # プロンプトサイズが上限を超えた時 if prompt_size_mb > MAX_PROMPT_SIZE_MB: raise ValueError( "画像とテキストを含むプロンプトサイズは{MAX_PROMPT_SIZE_MB}MB未満として下さい。\n現在は{round(prompt_size_mb, 1)}MBです。" ) # ファイルの拡張子を取得 extension = get_file_extension(file_path) # サポートされていない拡張子の場合 if not is_supported_image_extensions(extension): raise ValueError(f'サポートしている形式は {", ".join(SUPPORTED_IMAGE_EXTENSIONS)} です。') # Gemini に渡す画像情報を生成 with open(file_path, "rb") as f: image_content = Part.from_data(data=f.read(), mime_type=get_mime_type(extension)) # Gemini にリクエストを送信 response = multimodal_model.generate_content( contents=[image_content, prompt], generation_config=GenerationConfig( temperature=temperature, top_p=top_p, top_k=top_k, max_output_tokens=max_output_tokens ), ) return response.text except ValueError as e: raise gr.Error(str(e)) except Exception: print(traceback.format_exc()) raise gr.Error("予期せぬエラーが発生しました") # 類似画像を生成 def generate_similar_image( model_name: str, prompt: str, negative_prompt: str, guidance_scale: float, aspect_ratio: str, number_of_images: int, seed: int, ): PROMPT_PREFIX = """ 画像の特徴を抽出した文章を以下に記載します。 記載された特徴を元に類似した画像を生成してください。 """ try: if not prompt: raise ValueError("画像の特徴を入力して下さい。") prompt = PROMPT_PREFIX + prompt if not negative_prompt: negative_prompt = None if seed < 0 or seed > 2147483647: seed = None model = ImageGenerationModel.from_pretrained(model_name) generate_response = model.generate_images( prompt=prompt, negative_prompt=negative_prompt, number_of_images=number_of_images, guidance_scale=float(guidance_scale), aspect_ratio=aspect_ratio, language="ja", seed=seed, ) images = [] for index, result in enumerate(generate_response): images.append(generate_response[index]._pil_image) return images except ValueError as e: raise gr.Error(str(e)) except Exception: print(traceback.format_exc()) raise gr.Error("予期せぬエラーが発生しました") # メインコンテンツの Gradio ソースを生成 def generate_main_content(base_app: blocks) -> None: with base_app: with gr.Row(): gr.Markdown( """ # 1. アップロードした画像の特徴を抽出 """ ) with gr.Row(): with gr.Column(): txt_extraction_image_feature_in_prompt = gr.Textbox( placeholder="テキストを入力して下さい。", value="何を表した画像であるかと、物の特徴(ジャンル、色、形状など)を箇条書きで教えてください", container=True, label="アップロード画像の特徴を抽出するプロンプト(修正可)", scale=1, ) img_extraction_image_feature_in_image = gr.Image(type="filepath", sources=["upload"], scale=1) with gr.Column(): txt_extraction_image_feature_out = gr.Textbox(scale=2, label="画像の特徴(修正可)") with gr.Row(): with gr.Column(): with gr.Accordion("パラメータチューニング項目", open=False): with gr.Row(): sld_extraction_image_feature_in_temperature = gr.Slider( label="Temperature", minimum=0, maximum=1, step=0.1, value=0.4, interactive=True ) sld_extraction_image_feature_in_max_output_tokens = gr.Slider( label="Max Output Token", minimum=1, maximum=2048, step=1, value=1024, interactive=True ) sld_extraction_image_feature_in_top_k = gr.Slider( label="Top-K", minimum=1, maximum=40, step=1, value=32, interactive=True ) sld_extraction_image_feature_in_top_p = gr.Slider( label="Top-P", minimum=0.1, maximum=1, step=0.1, value=1, interactive=True ) with gr.Row(): btn_refresh = gr.Button(value="画面全体をクリア") btn_extraction_image_feature = gr.Button(value="アップロード画像の特徴抽出") with gr.Row(): gr.Markdown( """ # 2. 抽出した画像の特徴をもとに類似の画像を生成 """ ) with gr.Row(): glr_generate_similar_image_out = gr.Gallery( label="Generated Images", show_label=True, elem_id="gallery", columns=[2], object_fit="contain", height="auto", ) with gr.Row(): with gr.Accordion("パラメータチューニング項目", open=False): drp_generate_similar_image_in_model = gr.Dropdown( label="使用するモデル", choices=["imagegeneration@002", "imagegeneration@006"], value="imagegeneration@006", ) txt_generate_similar_image_in_negative_prompt = gr.Textbox( label="ネガティブプロンプト", placeholder="表示したくない内容を定義します", value="", ) drp_generate_similar_image_in_guidance_scale = gr.Dropdown( label="出力イメージサイズ", choices=[256.0, 1024.0, 1536.0], value="1536", ) drp_generate_similar_image_in_aspect_ratio = gr.Dropdown( label="アスペクト比", choices=["1:1", "9:16", "16:9", "3:4", "4:3"], value="1:1", ) num_generate_similar_image_in_number_of_images = gr.Number( label="表示件数", info="生成される画像の数。指定できる整数値: 1~4。デフォルト値: 4", value=4, ) num_generate_similar_image_in_seed = gr.Number( label="seed", info="必要に応じて結果を再現できるように、可能であればシードを使用してください。整数範囲: (0, 2147483647)", value=-1, ) with gr.Row(): btn_refresh2 = gr.Button(value="画面全体をクリア") btn_generate_similar_image = gr.Button(value="画像の特徴から類似画像を生成") # Submitボタンが押下されたときの処理 btn_extraction_image_feature.click( extraction_image_feature, [ txt_extraction_image_feature_in_prompt, img_extraction_image_feature_in_image, sld_extraction_image_feature_in_temperature, sld_extraction_image_feature_in_max_output_tokens, sld_extraction_image_feature_in_top_k, sld_extraction_image_feature_in_top_p, ], txt_extraction_image_feature_out, ) btn_generate_similar_image.click( fn=generate_similar_image, inputs=[ drp_generate_similar_image_in_model, txt_extraction_image_feature_out, txt_generate_similar_image_in_negative_prompt, drp_generate_similar_image_in_guidance_scale, drp_generate_similar_image_in_aspect_ratio, num_generate_similar_image_in_number_of_images, num_generate_similar_image_in_seed, ], outputs=glr_generate_similar_image_out, ) # Refreshボタンが押下されたときの処理 btn_refresh.click(None, js="window.location.reload()") btn_refresh2.click(None, js="window.location.reload()") app = gr.Blocks() try: initialize_variables() generate_main_content(app) except Exception as e: error_mesasge = "" if isinstance(e, ValueError): error_mesasge = str(e) else: error_mesasge = "予期せぬエラーが発生しました" with app: output_error = gr.Text(value=error_mesasge, label="Error") app.launch(server_name="0.0.0.0", server_port=7860)
ローカルでの動作確認
ローカル実行
main.py と同じディレクトリで以下のコマンドを実行することで、ローカルホスト(127.0.0.1)のポート 7860 で Web アプリが起動します。
$ python3 main.py Running on local URL: http://127.0.0.1:7860
ローカルで起動したアプリへ接続
ローカルで起動した Web アプリの URL(http://127.0.0.1:7860
)にブラウザでアクセスして、類似画像生成 Web アプリに接続します。
Google Cloud へのデプロイ
Cloud Run の使用
開発した類似画像生成 Web アプリを、Google Cloud 上にデプロイします。当記事ではデプロイ先のサービスとして、サーバーレス コンテナ コンピューティングサービスである Cloud Run を使用します。Cloud Run の詳細については以下の記事をご一読ください。
ディレクトリ構成
今回開発した画像生成 Web アプリのディレクトリ構成は以下のとおりです。
imagen-app |-- main.py |-- requirements.txt |-- Dockerfile
Dockerfile の作成
Cloud Run へのデプロイには Docker イメージを用意する必要があるため、Dockerfile を作成します。
FROM python:3.12-slim WORKDIR /usr/src/app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 7860 CMD [ "python", "./main.py" ]
Cloud Run にデプロイ
Dockerfile の存在するディレクトリで以下の gcloud コマンドを順次実行します。
- 環境変数
PROJECT_ID
に定義するYour-Project-ID
の部分は、ご自身が使用する Google Cloud プロジェクトの IDに置き換えてください。
# 環境変数の設定 PROJECT_ID=Your-Project-ID REGION=asia-northeast1 SA_NAME=similar-image-generation # サービスアカウント作成 gcloud iam service-accounts create $SA_NAME \ --description="類似画像生成Webアプリ用" \ --display-name="類似画像生成Webアプリ" # サービスアカウントへ権限付与 gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/aiplatform.user" gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/logging.logWriter" # Cloud Run サービスをデプロイ gcloud run deploy similar-image-generation --source . \ --region=asia-northeast1 \ --allow-unauthenticated \ --port 7860 \ --memory=1Gi \ --min-instances=1 \ --max-instances=1 \ --service-account=$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com \ --set-env-vars=PROJECT_ID=$PROJECT_ID,LOCATION=$REGION
ビルドされたコンテナイメージは、指定したリージョンに自動で作成される「cloud-run-source-deploy」という名前の Artifact Registory リポジトリに格納されます。
動作確認
Cloud Run のデプロイが完了すると、標準出力に Cloud Run のエンドポイントが Service URL として出力されます。この URL に、ブラウザからアクセスします。
$ gcloud run deploy similar-image-generation --source . \ --region=asia-northeast1 \ --allow-unauthenticated \ --port 7860 \ --memory=1Gi \ --min-instances=1 \ --max-instances=1 \ --service-account=$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com \ --set-env-vars=PROJECT_ID=$PROJECT_ID,LOCATION=$REGION Building using Dockerfile and deploying container to Cloud Run service [similar-image-generation] in project [Your-Project-ID] region [asia-northeast1] OK Building and deploying... Done. OK Uploading sources... OK Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds/xxx?project=yyy]. OK Creating Revision... OK Routing traffic... OK Setting IAM Policy... Done. Service [similar-image-generation] revision [similar-image-generation-00007-zxh] has been deployed and is serving 100 percent of traffic. Service URL: https://similar-image-generation-XXXXXXXXX-an.a.run.app
アクセスできたら、ひととおりの動作確認をしてください。
Cloud Run のアクセス元制御について
Cloud Run にデプロイした Web アプリのアクセス元制御を行いたい場合、Cloud Run の前段にロードバランサーを配置し、Identity Aware Proxy(IAP)による IAM 認証や Cloud Armor による IP アドレスの制限を実装することができます。
以下の記事もご参照ください。
福井 達也(記事一覧)
カスタマーサクセス課 エンジニア
2024年2月 G-gen JOIN
元はアプリケーションエンジニア(インフラはAWS)として、PM/PL・上流工程を担当。G-genのGoogle Cloudへの熱量、Google Cloudの魅力を味わいながら日々精進