GeminiとImagenで類似画像生成アプリを開発してみた

記事タイトルとURLをコピーする

G-gen の福井です。当記事では、Google が提供するマルチモーダル生成 AI モデル Geminiと、画像生成 AI モデル Imagen を使用して、アップロード画像から類似画像を生成する Web アプリを開発する手順をご紹介します。

はじめに

当記事の概要

当記事では、Google が提供するマルチモーダル生成 AI モデル Geminiと、画像生成 AI モデル Imagen を使用して、アップロードした画像から類似する画像を生成する Web アプリを開発してみます。

このアプリでは、以下の機能を提供します。

  • 画像の特徴を抽出するためのテキストプロンプトを入力するインターフェイス
  • Gemini 1.5 Pro を使用して、アップロードした画像の特徴を抽出
  • 抽出した画像の特徴情報を編集
  • Imagen 2 を使用して、抽出した画像の特徴情報から類似画像を生成

実行イメージ

Web インターフェイスで、プロンプトをテキストで入力し、画像をアップロードします。その後「アップロード画像の特徴抽出」ボタンを押下すると、アップロードした画像の特徴が表示されます。

画像の特徴を抽出

その後さらに「画像の特徴から類似画像を生成」ボタンを押下すると、画像の特徴情報をもとにした類似画像が生成されます。

類似画像を生成

利用サービス・ライブラリ

当記事では、以下の要素を使ってアプリを開発しました。

Gemini Pro

Imagen

Gradio

  • 機械学習 Web アプリを容易に構築できる Python フレームワークです。
  • 参考 : Gradio Docs - Blocks

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 の詳細については以下の記事をご一読ください。

blog.g-gen.co.jp

ディレクトリ構成

今回開発した画像生成 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 アドレスの制限を実装することができます。

以下の記事もご参照ください。

blog.g-gen.co.jp

福井 達也(記事一覧)

カスタマーサクセス課 エンジニア
2024年2月 G-gen JOIN

元はアプリケーションエンジニア(インフラはAWS)として、PM/PL・上流工程を担当。G-genのGoogle Cloudへの熱量、Google Cloudの魅力を味わいながら日々精進