1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

{作成}LLMのブラウザ操作で課題となっている座標指定について試してみた

Last updated at Posted at 2024-12-25

はじめに

最近LLMを使用したブラウザ操作が話題になっており気になってので似たようなことを再現できないか試しました。
PCのスクショを撮影し、pytesseractなどのOCRを利用して、テキスト認識と座標取得とLLMを組み合わせて、PCを操作することは可能ですが、検索ボックスやアイコンなどのテキストがない場合が問題でした。また、LLMが座標を認識し、クリックすることは難しいようでした。そこで、グリッドを使用することでLLMが座標を認識できるか試しました。

コード


def launch_browser_and_capture():
    WIDTH = 500  # ブラウザの幅
    HEIGHT = 500  # ブラウザの高さ
    
    chrome_options = Options()
    chrome_options.add_argument(f"--window-size={WIDTH},{HEIGHT}")
    chrome_options.add_argument('--log-level=3')
    chrome_options.add_experimental_option('excludeSwitches', ['enable-logging'])
    chrome_options.add_argument("--disable-infobars")
    chrome_options.add_argument("--disable-extensions")
    
    driver = webdriver.Chrome(options=chrome_options)
    driver.set_window_size(WIDTH, HEIGHT)
    driver.get("https://qiita.com/")
    driver.set_window_position(0, 0)
    
    driver.execute_script("""
        document.body.style.transform = 'scale(0.7)';
        document.body.style.transformOrigin = '0 0';
        document.body.style.width = '143%';
    """)
    
    time.sleep(2)
    return driver, WIDTH, HEIGHT

def capture_and_annotate_screenshot():
    try:
        driver, WIDTH, HEIGHT = launch_browser_and_capture()
        
        # 上部10%を除外したスクリーンショット領域の計算
        top_offset = int(HEIGHT * 0.3)  # 上部10%の高さ
        actual_height = HEIGHT - top_offset  # 実際のスクリーンショット高さ
        
        window_location = driver.get_window_position()
        screenshot = pyautogui.screenshot(region=(
            window_location['x'],
            window_location['y'] + top_offset,  # 上部をスキップ
            WIDTH,
            actual_height  # 調整後の高さ
        ))
        
        draw = ImageDraw.Draw(screenshot)
        EDGE_PADDING = 10
        GRID_SPACING = 40
        
        try:
            font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12)
        except:
            try:
                font = ImageFont.truetype("arial.ttf", 12)
            except:
                font = ImageFont.load_default()
        
        # 調整後の高さに基づいてグリッドを描画
        counter = 1
        for y in range(EDGE_PADDING, actual_height - EDGE_PADDING, GRID_SPACING):
            for x in range(EDGE_PADDING, WIDTH - EDGE_PADDING, GRID_SPACING):
                draw.point((x, y), fill='red')
                text = str(counter)
                
                text_bbox = draw.textbbox((x+2, y+2), text, font=font)
                padded_bbox = (
                    text_bbox[0] - 2,
                    text_bbox[1] - 2,
                    text_bbox[2] + 2,
                    text_bbox[3] + 2
                )
                draw.rectangle(padded_bbox, fill='white')
                draw.text((x+2, y+2), text, fill='red', font=font)
                counter += 1
        
        timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"screenshot_{timestamp}.png"
        save_path = os.path.join(os.getcwd(), filename)
        
        screenshot.save(save_path)
        print(f"スクリーンショットを保存しました: {save_path}")
        
        driver.quit()
        return filename
        
    except Exception as e:
        print(f"エラーが発生しました: {str(e)}")
        if 'driver' in locals():
            driver.quit()
        return None


if __name__ == "__main__":
    try:
        filename = capture_and_annotate_screenshot()
        if filename:
            path = str(filename)
            print(path)

            genai.configure(api_key=APIk)

            def upload_to_gemini(path, mime_type=None):
                file = genai.upload_file(path, mime_type=mime_type)
                print(f"Uploaded file '{file.display_name}' as: {file.uri}")
                return file

            generation_config = {
                "temperature": 1,
                "top_p": 0.95,
                "top_k": 40,
                "max_output_tokens": 8192,
                "response_mime_type": "text/plain",
            }

            model = genai.GenerativeModel(
                model_name="gemini-1.5-flash",
                generation_config=generation_config,
            )

            files = [
                upload_to_gemini(path, mime_type="image/png"),
            ]

            chat_session = model.start_chat(
                history=[
                    {
                        "role": "user",
                        "parts": [
                            files[0],
                            "以下のスクリーンショットでは、画像のxy軸に番号が連番で割り振られている。「検索を行いたい場合にクリックするべき番号を一つ出力してください。」",
                        ],
                    },
                ]
            )

            response = chat_session.send_message("画像を分析して、questionの位置に最も近い番号を教えてください。")
            print(response.text)
            
    except Exception as e:
        print(f"エラーが発生しました: {str(e)}")
        

すべてAIでコーディングしています。
historyは不要ですがそのまま実行してしまいました。

結果

questionボタンについて聞いてみた

1000x1000の場合

screenshot_20241225_022047.png

画像内の「question」のすぐ隣にある番号は 76 です。

800x800の場合

screenshot_20241225_022128.png

質問をクリックするには、番号 215 をクリックする必要があります。

GPT4oの場合
スクリーンショット内で「question」をクリックしたい場合に、クリックすべき番号を確認します。少しお待ちください。

分析が終了しました
python
常に詳細を表示する
画像を開きましたので、「question」がどの位置にあるか確認し、クリックすべき番号を特定します。 ​​
画像を確認した結果、「question」をクリックするための番号は 66 です。

=>おしい

600x600の場合

screenshot_20241225_022158.png

質問をクリックするには、番号 52 をクリックする必要があります。

500x500の場合

screenshot_20241225_022008.png

質問をクリックしたい場合にクリックする必要がある番号は 43 です。

600x400

screenshot_20241225_022831.png
画像から、"Question" という単語に最も近い番号は 51 です。

600x500

screenshot_20241225_023001.png

画像内の「Question」という単語に最も近い番号は 50 です。

次にブラウザの文字を70%に変更し、検索方法を質問してみた。

screenshot_20241225_025010.png
8です。
スクリーンショットに「Login」ボタンがあり、検索を行うにはログインが必要なため、ログインボタンの番号である8をクリックする必要があります。

番号の点数を増やすと正解しなくなったので70個が限界みたいです。

無理やり倍率をそろえると
screenshot_20241225_025638.png
画像内の「検索」に相当する機能は、画面上部に表示されている虫眼鏡アイコンです。このアイコンは番号「7」のセルにあります。

したがって、答えは 7 です。

まとめ

合計1000pxを越えなければ認識してくれるようです。GPT4oでも結果は似た感じでした。現状だと、番号がかぶっている個所しか操作できないため、「番号と番号の間」といった要素を追加する必要がありそうです。

補足

以下のように、番号の位置を出力するように指示したところ精度が落ち、左右上下等の説明もないことから、不可能な可能性が高いです。

ブラウザスクリーンショットでは、画像のxy軸に番号が連番で割り振られている。詳細に分析してください。
指示:実行希望内容を確認し条件に従い、画像を見て最適なアクションを一つ提供してください。イラストやマークで提供されている場合もあります。
実行希望内容:サイトで検索を行いたい
条件:
- 押すべき場所に存在する番号と補足情報で答える。
- 指示にあるものが、番号と重なっていない場合、番号より左右上下、番号mと番号nのの間に存在するなどの補足を追加
出力:
番号:
補足情報

番号:8
補足情報:Loginボタン

検索マークを探すようにした場合

検索に相当するボタンやマークを画像に割り振られた番号で答えてください。また、番号から左右上下ずれている場合はその趣旨をも出力すること

The search button or mark is number 8.

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?