InstructBLIP による Visual Question Answering(ソースコードと実行結果)

Python開発環境,ライブラリ類

ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。

Python 3.12 のインストール

インストール済みの場合は実行不要。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。

REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul

関連する外部ページ

Python の公式ページ: https://www.python.org/

AI エディタ Windsurf のインストール

Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。

winget install --scope machine Codeium.Windsurf -e --silent

関連する外部ページ

Windsurf の公式ページ: https://windsurf.com/

必要なライブラリのインストール

コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する


pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install transformers pillow opencv-python

InstructBLIP による Visual Question Answering with

概要

このプログラムは、画像の内容を理解し、自然言語による質問に対して回答する能力を示す。画像中の物体、場面、関係性を認識し、質問の意図を理解した上で、適切な回答を生成する。

主要技術

主要技術


# Visual Question Answering with InstructBLIP
# 特徴技術名: InstructBLIP (Instruction-aware BLIP)
# 出典: Dai, W., Li, J., Li, D., Tiong, A. M. H., Zhao, J., Wang, W., Li, B., Fung, P., & Hoi, S. (2023). InstructBLIP: Towards General-purpose Vision-Language Models with Instruction Tuning. Advances in Neural Information Processing Systems (NeurIPS 2023).
# 特徴機能: 指示追従型Q-Former(Instruction-aware Querying Transformer)による高精度な視覚的質問応答。指示チューニングにより、より正確で詳細な回答生成が可能。
# 学習済みモデル: Salesforce/instructblip-vicuna-7b - Vicuna-7B言語モデルとViT-g/14画像エンコーダを組み合わせた指示追従型マルチモーダルモデル、VQAv2データセットで85.3%の精度達成、https://huggingface.co/Salesforce/instructblip-vicuna-7b
# 方式設計:
#   関連利用技術: transformers(HuggingFace提供のTransformerモデルライブラリ)、PIL(画像処理ライブラリ)、OpenCV(コンピュータビジョンライブラリ)、tkinter(GUI操作)、urllib(HTTP通信)
#   入力と出力: 入力: 動画像,カメラ(ユーザは「0:画像ファイル,1:カメラ,2:サンプル画像」のメニューで選択.0:動画ファイルの場合はtkinterで複数ファイル選択可能.1の場合はOpenCVでカメラが開き,スペースキーで撮影(複数回可能).2の場合はhttps://github.com/opencv/opencv/raw/master/samples/data/fruits.jpg とhttps://github.com/opencv/opencv/raw/master/samples/data/messi5.jpgとhttps://github.com/opencv/opencv/raw/master/samples/data/aero3.jpgを使用)、出力: OpenCV画面でリアルタイムに表示,OpenCV画面内に処理結果をテキストで重畳表示,1秒間隔でprint()による処理結果表示,プログラム終了時にresult.txtファイルに保存
#   処理手順: 1.学習済みInstructBLIPモデルとプロセッサの読み込み、2.入力画像の前処理(リサイズ、正規化)、3.ユーザ入力質問でVQA推論実行、4.指示追従型Q-Formerによる画像特徴量抽出と言語モデルによるテキスト生成、5.生成されたテキスト回答の後処理と表示
#   前処理、後処理: 前処理: 画像のリサイズ(384x384)、RGB正規化、テンソル変換、後処理: 生成トークンのデコード、特殊トークン除去、信頼度に基づく回答品質評価
#   追加処理: 画像品質チェック(入力画像の解像度とアスペクト比確認による推論精度向上)、回答一貫性確保(同一画像に対する複数回推論結果の統合処理)
#   調整を必要とする設定値: max_length(生成する最大長、デフォルト256)、do_sample(サンプリング有無、False)
# 将来方策: max_lengthの動的調整機能(質問タイプに応じて、Yes/No質問は50、詳細説明質問は256など自動設定)
# その他の重要事項: GPU利用時のVRAM使用量監視が推奨、学習済みモデルの初回ダウンロード時間(約7.5GB)への配慮が必要
# 前準備: pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
# pip install transformers pillow opencv-python

import cv2
import tkinter as tk
from tkinter import filedialog
import urllib.request
import os
from transformers import InstructBlipProcessor, InstructBlipForConditionalGeneration
from PIL import Image
import torch

# 設定値
MAX_LENGTH = 256  # 生成する最大長(質問タイプに応じて調整)
FONT_SIZE = 0.7  # OpenCV表示のフォントサイズ
LINE_HEIGHT = 30  # テキスト表示の行間隔


def load_model():
    try:
        print('InstructBLIPモデルを読み込み中...')
        device = 'cuda' if torch.cuda.is_available() else 'cpu'
        print(f'使用デバイス: {device}')

        proc = InstructBlipProcessor.from_pretrained('Salesforce/instructblip-vicuna-7b')
        mdl = InstructBlipForConditionalGeneration.from_pretrained(
            'Salesforce/instructblip-vicuna-7b',
            torch_dtype=torch.float16 if device == 'cuda' else torch.float32,
            device_map='auto' if device == 'cuda' else None
        )
        if device == 'cpu':
            mdl = mdl.to(device)

        print('モデルの読み込みが完了しました')
        return proc, mdl
    except Exception as e:
        print(f'モデルの読み込みに失敗しました: {e}')
        exit()


def process_vqa(img, proc, mdl, res):
    if img is None:
        print('画像の読み込みに失敗しました')
        return img

    disp = img.copy()
    y_pos = 30

    # 最初に画像を表示
    cv2.imshow('Image', disp)
    cv2.waitKey(1)

    while True:
        q = input('質問を入力してください(英語、quitで終了): ')
        if q.lower() == 'quit':
            break

        try:
            pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))

            # InstructBLIPの公式使用方法
            device = mdl.device if hasattr(mdl, 'device') else 'cpu'
            inputs = proc(images=pil, text=q, return_tensors='pt').to(device)

            with torch.no_grad():
                outputs = mdl.generate(
                    **inputs,
                    do_sample=False,
                    num_beams=5,
                    max_length=MAX_LENGTH,
                    min_length=1,
                    repetition_penalty=1.5,
                    length_penalty=1.0,
                    temperature=1,
                )

            ans = proc.batch_decode(outputs, skip_special_tokens=True)[0].strip()

            res.append(f'Q: {q} A: {ans}')
            print(f'回答: {ans}')

            # 全ての質問と回答を表示
            cv2.putText(disp, f'Q: {q}', (10, y_pos), cv2.FONT_HERSHEY_SIMPLEX,
                       FONT_SIZE, (255, 255, 0), 2)
            # 長い回答の場合は省略表示
            display_ans = ans if len(ans) <= 50 else ans[:47] + '...'
            cv2.putText(disp, f'A: {display_ans}', (10, y_pos + LINE_HEIGHT), cv2.FONT_HERSHEY_SIMPLEX,
                       FONT_SIZE, (0, 255, 0), 2)
            y_pos += LINE_HEIGHT * 2

            # 更新した画像を表示
            cv2.imshow('Image', disp)
            cv2.waitKey(1)

        except Exception as e:
            print(f'VQA処理でエラーが発生しました: {e}')

    return disp


def show_img(img, win, proc, mdl, res):
    if img is None:
        print('画像の読み込みに失敗しました')
        return
    process_vqa(img, proc, mdl, res)
    cv2.waitKey(0)
    cv2.destroyWindow(win)


print('InstructBLIP Visual Question Answering プログラム')
print('概要: 画像に関する質問に対してAIが回答します')
print('操作方法:')
print('  - 画像選択後、英語で質問を入力してください')
print('  - 複数の質問が可能です(quitで次の画像へ)')
print('  - カメラモード: スペースキーで撮影、qキーで終了')

proc, mdl = load_model()
res = []

print('0: 画像ファイル')
print('1: カメラ')
print('2: サンプル画像')

choice = input('選択: ')

if choice == '0':
    root = tk.Tk()
    root.withdraw()
    paths = filedialog.askopenfilenames()
    if not paths:
        exit()
    for path in paths:
        show_img(cv2.imread(path), 'Image', proc, mdl, res)
elif choice == '1':
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
    try:
        while True:
            cap.grab()
            ret, frame = cap.retrieve()
            if not ret:
                break
            cv2.imshow('Camera', frame)
            key = cv2.waitKey(1) & 0xFF
            if key == ord(' '):
                show_img(frame, 'Image', proc, mdl, res)
            elif key == ord('q'):
                break
    finally:
        cap.release()
elif choice == '2':
    urls = [
        'https://github.com/opencv/opencv/raw/master/samples/data/fruits.jpg',
        'https://github.com/opencv/opencv/raw/master/samples/data/messi5.jpg',
        'https://github.com/opencv/opencv/raw/master/samples/data/aero3.jpg'
    ]
    files = []
    for i, url in enumerate(urls):
        fname = f'sample_{i}.jpg'
        try:
            urllib.request.urlretrieve(url, fname)
            files.append(fname)
            show_img(cv2.imread(fname), 'Sample Image', proc, mdl, res)
        except Exception as e:
            print(f'画像のダウンロードに失敗しました: {url}')
            print(f'エラー: {e}')
            continue
    for fname in files:
        try:
            os.remove(fname)
        except OSError:
            pass

cv2.destroyAllWindows()

if res:
    with open('result.txt', 'w', encoding='utf-8') as f:
        for i, r in enumerate(res):
            f.write(f'{i+1}: {r}\n')
    print('result.txtに保存')