5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「この画像の、この部分」を"テキスト"で Gemini に伝える会話型セグメンテーション

Posted at

はじめに

Gemini 2.5 以降では、物体検出を行うだけではなくセマンティックセグメンテーションを行うことが可能になっています。

実際に自然言語で指示をして試してみました。

実行

対象画像

以下の画像内に写っているもののセグメンテーションを取得してみたいと思います。

方法

from io import BytesIO
from google import genai
from google.genai import types

image = "image.png"

# Load and resize image
im = Image.open(BytesIO(open(image, "rb").read()))
im.thumbnail([1024,1024], Image.Resampling.LANCZOS)

safety_settings = [
    types.SafetySetting(
        category="HARM_CATEGORY_DANGEROUS_CONTENT",
        threshold="BLOCK_ONLY_HIGH",
    ),
]

client = genai.Client(api_key=GOOGLE_API_KEY)

# 電卓とペンを指定
prompt = """
Give the segmentation masks for the calculator and pen items.
Output a JSON list of segmentation masks where each entry contains the 2D
bounding box in the key "box_2d", the segmentation mask in key "mask", and
the text label in the key "label". Use descriptive labels.
"""

# Run model to find segmentation masks
response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents=[prompt, im],
    config = types.GenerateContentConfig(
        temperature=0.5,
        safety_settings=safety_settings,
        thinking_config=types.ThinkingConfig(
          thinking_budget=0
        )
    )
)

print(response.text)

レスポンスの中身は以下のような JSON で返ってきます。

  • box_2d
    • 境界ボックス
  • mask
    • セグメンテーションマスク(base64エンコードされた png 形式)
  • label
    • ラベル
response.text
```json
[
  {"box_2d": [128, 545, 936, 983], "mask": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAAAAAB5Gfe6AAAB9klEQVR42u3dQY7CQAwEwP7/p81K3FFYQphJl8URIbpimwSEkqiVa+bv8bruGfut2vjo5vnu5+Pa6NCekHY/grmiiqMvSDC/qfb8Uxv8xwKzTpXGvlZglq7a4KcLzK7VmPkMgblPteef9vz/ERgA3QADoFwAQLsAAAAAugVGCwBwJmALAAAAAECvAAAzoAW0AAAAAAgAAACAAAAAAFwPATADAAgAsAUJAADQBDADoFsAgBkAYAYAAAAAoFcAgBnQAgAAAGgWAAAAgC0IwAwAAACAAAAAADoFAAAAYAsCMAMAAAAAAMDnIAAzAKByBqIFrEEAAABUAwSAMwEAAKoBogMIAAAAwOUAAN8KAQAAoFMAgM9BS8AMAABgCdwUYNpbAACAboCDe7BeAIAZAOB0WAsAAGAJADADAMwAAACWAAAzAABAIYDfyQEAsAZ1gA7QAgCKT4T8dUALuBrQAgAAEKjOD6AewP14AbQDBAAAAN0AAWAGtAAAM6AFmvNbAgAAWAIAAHQLBAAAAACqAQKAAAAA3QABYAa0AAAzoAUANAOkHiAACAAAUJ1fB+iAdoDUA6Q9f9rz30Yg6RZIOUBSLJDPqzx+Nr5Hec6r7vT7CeQLVR1+G4F8tZqzL22QS2up2FeHP8Pg1etlrWN90ODA07LSW1equB4alYSFCbakLgAAAABJRU5ErkJggg==", "label": "scientific calculator"},
  {"box_2d": [98, 14, 740, 178], "mask": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAAAAAB5Gfe6AAACAUlEQVR42u3dwXVCMQxFQfXftFIALMiBv7DvqIN5khxD+DATq93p1b5UnJ+KYDcdwG47gG0HsNsOYOMBbDyAbQew/OEA9pO69GXOflydVt/p32+rrT87g/1hxfnnJbAPVBp/UAD7XLX1JwSwmw4AH7+7/fjlALbtX+13+Icj4E/7l5+/699tB1D3GwB+fv5sAPz8/A5AA8DPz8/PLwAXAAGYfwHw8zsA+S3AzQHwWwD9138DYABcgfh9DobfAhgA/pDfCcDvDmwABMDvDuAOUOTnn4YyAPwCKPsFUPfnvxHCd0LkvxLn6Qhm2gnMMZUPYOr+RzKI8/V/2gnwtxPg5y8HwM/P3w1gDAC/BTAABoA/GcAIgJ+/G4ADwADwhwPgbwcwAkj7Jz4A/ALg56/67b8B4OfPBsDPz+8A5Od3AfYGYAt/oN9TMO2nIPjNv4eg+LN//j0Dpf323wAYAPc//TcA/F4AhQLwBogFwDf//Pz8DkB+fvdfAfA3/ObfAPB3/cufDoDf+ecA0H/9dwG2APwWwADovwD4+f0PhJ+f32cA9F//fQic3wLw89+3/8PPz8/vBsRvAfTfAPC3/H4Mue73Q1B+B8eXoP4igbmg2vpvEpi7KszX/Lb+3wlMPICZdALT9g9/OYC5ubryDwKYeABjAMoBTKXe0Dv4twFMrnL+P6PwyIDl7CspAAAAAElFTkSuQmCC", "label": "red pen"},
  {"box_2d": [38, 244, 686, 502], "mask": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAAAAAB5Gfe6AAAB+UlEQVR42u3dQW7cQAxE0br/pZmFgQCOncSJvbH+4w36ocjWtKTR9rbupVat+1n19TcF7l4L9BR+EegF4d6pPMDV158iAFDPQB7g9wIyIAIVg6sT3F8KwfMR8gAEri5wFyeoA9y1Be7DVV77cwReLeZyAvfZqq//uwsAuDjB1QUuL6ANtIE+IECAwFcBfGMCAiNgEsqADMgAAJcDMmAOEiBAwBkZAQIAXA7JgM1QBkSAAAGbIQAnZMYAAXOAAAECNkMCBAgQsBXIAABdQKAs4KycgBvHBJYHkAEvXcuAOaALCHyFwIaAAIE6wQwCGciHgACB1QlWF9jqBFvcYA8qAgj+k0AG9rCqr7/4L8WfJZgQyAABAsaADBAgAMAkJECAQP7H8eMMCAAgUBdwVry8gAwQIOC+Yf7GIQECeQFPEtkLCBAgQIAAAc8TuiaUAWOAgDHgt6E3jQjkBbxvJwP2AhmQAReFBHQBAREgoAkIECBAgAABAi6ICBAg4ICIAAECBAgQ8CQZAV1AAAABAgTshQQ0AQECjklFQAZshgQIECBgN5QBGZABAAQIGIQENAEBAv6EpgxAwIcbCPi6qW/4+JrZvxKsLjACdYERAFAnyAvswUVgeYAPEeQFVhdYXWB1AQCFygMsD/AHgtUFFqo8wHsEW1tgwXqz/B9OLtkQY2cybgAAAABJRU5ErkJggg==", "label": "black pen"}
]
```

このレスポンスを使用して、セグメンテーションの画像を作成します。

セグメンテーション画像作成コード
import os
import json
import base64
import io
from PIL import Image, ImageDraw
import numpy as np
from io import BytesIO


im = Image.open(BytesIO(open(image, "rb").read()))


def parse_json(json_output: str):
    # マークダウンのフェンシングを解析
    lines = json_output.splitlines()
    for i, line in enumerate(lines):
        if line == "```json":
            json_output = "\n".join(lines[i + 1 :])
            output = json_output.split("```")[0]
            return output
    return json_output


def extract_segmentation_masks(
    im: Image.Image, text: str, output_dir: str = "segmentation_outputs"
):
    items = json.loads(parse_json(text))

    # 出力ディレクトリを作成
    os.makedirs(output_dir, exist_ok=True)

    # すべてのマスク用の単一オーバーレイを作成
    combined_overlay = Image.new("RGBA", im.size, (0, 0, 0, 0))
    combined_draw = ImageDraw.Draw(combined_overlay)

    # 異なるオブジェクト用の色を定義
    colors = [
        (255, 0, 0, 150),  # 赤
        (0, 255, 0, 150),  # 緑
        (0, 0, 255, 150),  # 青
        (255, 255, 0, 150),  # 黄色
        (255, 0, 255, 150),  # マゼンタ
        (0, 255, 255, 150),  # シアン
    ]

    # 各マスクを処理して結合オーバーレイに追加
    for i, item in enumerate(items):
        # バウンディングボックスの座標を取得
        box = item["box_2d"]
        y0 = int(box[0] / 1000 * im.size[1])
        x0 = int(box[1] / 1000 * im.size[0])
        y1 = int(box[2] / 1000 * im.size[1])
        x1 = int(box[3] / 1000 * im.size[0])

        # 無効なボックスをスキップ
        if y0 >= y1 or x0 >= x1:
            continue

        # マスクを処理
        png_str = item["mask"]
        if not png_str.startswith("data:image/png;base64,"):
            continue

        # プレフィックスを削除
        png_str = png_str.removeprefix("data:image/png;base64,")
        mask_data = base64.b64decode(png_str)
        mask = Image.open(io.BytesIO(mask_data))

        # マスクをバウンディングボックスに合わせてリサイズ
        mask = mask.resize((x1 - x0, y1 - y0), Image.Resampling.BILINEAR)

        # マスクを処理用のnumpy配列に変換
        mask_array = np.array(mask)

        # 各オブジェクトに異なる色を使用
        color = colors[i % len(colors)]

        # このマスクを結合オーバーレイに追加
        for y in range(y0, y1):
            for x in range(x0, x1):
                if y - y0 < mask_array.shape[0] and x - x0 < mask_array.shape[1]:
                    if mask_array[y - y0, x - x0] > 128:  # マスクの閾値
                        combined_draw.point((x, y), fill=color)

        # バウンディングボックスの近くにラベルテキストを追加
        label = item["label"]
        label_x = x0
        label_y = max(0, y0 - 25)  # ラベルをバウンディングボックスの上に配置

        # 視認性向上のためテキスト背景を描画
        try:
            # テキストサイズを取得(新しいPillowバージョン)
            bbox = combined_draw.textbbox((0, 0), label)
            text_width = bbox[2] - bbox[0]
            text_height = bbox[3] - bbox[1]
        except AttributeError:
            # 古いPillowバージョンのフォールバック
            text_width, text_height = combined_draw.textsize(label)

        # テキスト用の背景矩形を描画
        bg_color = (0, 0, 0, 180)  # 半透明の黒背景
        combined_draw.rectangle(
            [label_x, label_y, label_x + text_width + 10, label_y + text_height + 5],
            fill=bg_color,
        )

        # ラベルテキストを描画
        text_color = (255, 255, 255, 255)  # 白テキスト
        combined_draw.text((label_x + 5, label_y + 2), label, fill=text_color)

        print(f"結合セグメンテーションに{item['label']}を追加しました")

    # すべてのマスクを含む最終合成画像を作成
    final_composite = Image.alpha_composite(im.convert("RGBA"), combined_overlay)

    # 結合結果を保存
    output_filename = "combined_segmentation_with_labels.png"
    final_composite.save(os.path.join(output_dir, output_filename))
    print(
        f"ラベル付き結合セグメンテーションを{os.path.join(output_dir, output_filename)}に保存しました"
    )

    return final_composite, text

extract_segmentation_masks(im, response.text)

以下が作成されたセグメンテーションの画像になります。

combined_segmentation_with_labels.png

プロンプトで電卓とペンと指定したので、それらのものの領域が塗りつぶされていることが確認できました!

別の指示

自然言語でもう少し詳しい部分の領域を指定してみたいと思います。

電卓の液晶部分

prompt
prompt = """
Give the segmentation masks for the LCD part of calculator items.
Output a JSON list of segmentation masks where each entry contains the 2D
bounding box in the key "box_2d", the segmentation mask in key "mask", and
the text label in the key "label". Use descriptive labels.
"""

combined_segmentation_with_labels.png

ちゃんと電卓の液晶部分を理解しているみたいです!

USB の金属部分

prompt
prompt = """
Give the segmentation masks for the USB metal part items.
Output a JSON list of segmentation masks where each entry contains the 2D
bounding box in the key "box_2d", the segmentation mask in key "mask", and
the text label in the key "label". Use descriptive labels.
"""

combined_segmentation_with_labels.png

細かい部分でしたが、しっかり USB の金属部分を理解していました!

以上のことから、自然言語で指示をした部分のセグメンテーションを取得することができました!

まとめ

Gemini のセマンティックセグメンテーションの機能を試してみました。
自然言語の指示で画像内の特定の領域を柔軟かつ正確に抽出することができそうです。
これにより、画像解析や編集などのタスクがより手軽に行えるようになると感じました!

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?