LLaVa による画像理解(静止画用)(ソースコードと実行結果)


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/
CUDA 12.6のインストール
AIプログラムのGPU実行に便利なCUDA、管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行してインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。
REM CUDA をシステム領域にインストール
winget install --scope machine --id Nvidia.CUDA --version 12.6 -e
REM CUDA のパス設定
set "CUDA_PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.6"
if exist "%CUDA_PATH%" setx CUDA_PATH "%CUDA_PATH%" /M >nul
if exist "%CUDA_PATH%" setx CUDNN_PATH "%CUDA_PATH%" /M >nul
必要なライブラリのインストール
コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する
pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install transformers pillow requests accelerate bitsandbytes opencv-python
LLaVAによる画像理解プログラム
概要
LLaVAは人間の視覚に相当するAI画像理解システムである。画像や動画を認識し、その内容や意味を理解する機能を持つ。人間が写真を見て「これは犬の画像」と判断するのと同じ処理をAIが技術的に実現している。
機能
写真や動画内の人、物、動物、文字などを識別し、それらの位置や特徴を把握する。人間が無意識に行う感覚的理解をコンピュータで再現した技術である。
応用例医療分野では、CTやMRI画像から病気の兆候を発見する用途に応用されている。LLaVA-Medは医療画像理解に特化して開発されており、CT、MRI、病理組織画像などを解析する能力を持つ[1]。
製造業では、工場の生産ラインにおける製品品質の自動検査に活用されている。人間の目では発見困難な微細な欠陥も検出可能である。ViP-LLaVAを基盤とした視覚検査モデルがMVTec異常検出データセットで製品の欠陥検出に成功している[2]。
参考文献
[1] C. Li et al., "LLaVA-Med: Training a Large Language-and-Vision Assistant for Biomedicine in One Day," arXiv preprint arXiv:2306.00890, 2023.
[2] "Vision-Language In-Context Learning Driven Few-Shot Visual Inspection Model," arXiv preprint arXiv:2502.09057v1, 2025.
ソースコード
# プログラム名: LLaVAによる画像理解プログラム
# 特徴技術名: Visual Instruction Tuning(視覚指示調整)
# 出典: Liu, Haotian et al. "Visual Instruction Tuning" NeurIPS 2023, arXiv:2304.08485
# 特徴機能: GPT-4で生成された158Kマルチモーダル指示追従データを用いた視覚エンコーダとLLMの統合学習により、画像と自然言語の双方向理解を実現
# 学習済みモデル: llava-hf/llava-1.5-7b-hf - 7Bパラメータのマルチモーダル対話モデル、GPT-4に対して85.1%の相対スコア達成、https://huggingface.co/llava-hf/llava-1.5-7b-hf
# 方式設計:
# - 関連利用技術: CLIP ViT-L/14@336px(視覚エンコーダ)、Vicuna v1.5-7B(Llama 2ベース言語モデル)、2層MLPプロジェクション行列(視覚-言語特徴空間統合)
# - 入力と出力: 入力: 画像(ユーザは「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()で処理結果を表示.プログラム終了時にprint()で表示した処理結果をresult.txtファイルに保存し,「result.txtに保存」したことをprint()で表示.プログラム開始時に,プログラムの概要,ユーザが行う必要がある操作(もしあれば)をprint()で表示.
# - 処理手順: CLIP視覚エンコーダで画像を特徴ベクトルに変換、テキストプロンプトをトークナイザで数値列に変換、プロジェクション行列で視覚特徴を言語空間に投影、統合された特徴をVicunaで自己回帰的テキスト生成
# - 前処理、後処理: 前処理:CLIP画像正規化による視覚特徴抽出精度向上、テキストトークン化による言語理解精度向上。後処理:生成テキストのデコードによる自然言語出力、特殊トークン除去による応答品質向上
# - 追加処理: 4bit量子化(BitsAndBytesConfig)によるメモリ使用量削減(14GB→3.5GB)でWindows GPU環境対応、temperature=0.2設定による適度な創造性を持つ自然な応答生成
# - 調整を必要とする設定値: max_new_tokens(生成トークン数上限、現在メイン質問300/追加質問150設定)- 応答文の最大長度制御、値が大きいほど詳細な応答
# 将来方策: max_new_tokensの最適値決定のため、複数の画像で生成文字数の分布を収集し、ヒストグラム(横軸:文字数、縦軸:頻度)を作成して95パーセンタイル値を採用
# その他の重要事項: Windows環境での4bit量子化によりGPUメモリ制約下でも動作可能、<image>トークンをプロンプトに含めることが必須
# 前準備: pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
# pip install transformers pillow requests accelerate bitsandbytes opencv-python
import cv2
import tkinter as tk
from tkinter import filedialog
import urllib.request
import os
import torch
from PIL import Image, ImageDraw, ImageFont
from transformers import AutoProcessor, LlavaForConditionalGeneration, BitsAndBytesConfig
import sys
import io
import numpy as np
import textwrap
import time
# Windows文字エンコーディング設定
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
# プログラム開始時の説明表示
print('='*60)
print('LLaVAによる画像理解プログラム')
print('='*60)
print('このプログラムは画像の内容を自然言語で説明します。')
print('画像選択後、LLaVAモデルが画像を分析し、詳細な説明を生成します。')
print('処理結果はOpenCVウィンドウに表示され、result.txtに保存されます。')
print('OpenCVウィンドウで「q」キーを押すと次の画像に進みます。')
print('='*60)
print()
# 処理結果を保存するリスト
all_results = []
# Windows対応4bit量子化設定
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type='nf4'
)
# モデルとプロセッサの自動ダウンロードと初期化
print('LLaVAモデルを初期化中...')
model_id = 'llava-hf/llava-1.5-7b-hf'
processor = AutoProcessor.from_pretrained(model_id, use_fast=True)
model = LlavaForConditionalGeneration.from_pretrained(
model_id,
quantization_config=quantization_config,
device_map='auto'
)
print('モデルの初期化が完了しました。')
print()
def detect_scene_type(text):
"""画像解析結果からシーンタイプを推定"""
text_lower = text.lower()
# 屋内施設
if any(keyword in text for keyword in ['家', 'リビング', 'キッチン', '寝室', '部屋', 'home', 'room', 'kitchen']):
return 'indoor_home'
elif any(keyword in text for keyword in ['オフィス', 'デスク', '会議', 'office', 'desk', 'meeting']):
return 'indoor_office'
elif any(keyword in text for keyword in ['店', 'ショップ', '商品', '棚', 'shop', 'store', 'product']):
return 'indoor_shop'
elif any(keyword in text for keyword in ['レストラン', '食堂', 'カフェ', 'restaurant', 'cafe']):
return 'indoor_restaurant'
elif any(keyword in text for keyword in ['駅', 'プラットフォーム', '電車', 'station', 'platform', 'train']):
return 'transport_station'
elif any(keyword in text for keyword in ['空港', 'ゲート', '飛行機', 'airport', 'gate', 'plane']):
return 'transport_airport'
# 屋外
elif any(keyword in text for keyword in ['道路', '車', '信号', '標識', 'road', 'car', 'traffic']):
return 'outdoor_road'
elif any(keyword in text for keyword in ['山', '森', '木', '自然', 'mountain', 'forest', 'nature']):
return 'outdoor_nature'
elif any(keyword in text for keyword in ['海', '川', '水', 'sea', 'river', 'water']):
return 'outdoor_water'
elif any(keyword in text for keyword in ['観光', '名所', '建物', 'tourist', 'landmark']):
return 'outdoor_tourist'
# 詳細・クローズアップ
elif any(keyword in text for keyword in ['ラベル', '表示', '文字', 'label', 'text', 'sign']):
return 'detail_text'
elif any(keyword in text for keyword in ['食品', '料理', '食べ物', 'food', 'meal']):
return 'detail_food'
return 'general'
# シーンタイプごとのプロンプトテンプレート
SCENE_PROMPTS = {
'indoor_home': 'この家庭内の画像について、以下の観点で詳しく分析してください:\n1. 安全性の問題(コンセント、配線、滑りやすい箇所、鋭利な物など)\n2. 異常や故障の兆候(水漏れ、ひび割れ、汚れなど)\n3. 整理整頓の状態\n4. 注目すべき物品や設備\n5. その他気づいた点',
'indoor_office': 'このオフィスの画像について、以下の観点で詳しく分析してください:\n1. セキュリティ関連(機密情報、アクセス制限エリアなど)\n2. 設備の状態(故障、異常など)\n3. 掲示物や重要な通知\n4. 作業環境の安全性\n5. その他気づいた点',
'indoor_shop': 'この店舗の画像について、以下の観点で詳しく分析してください:\n1. 商品情報(新商品、特売品、価格など)\n2. 営業時間やサービス情報\n3. 店内の案内表示\n4. 混雑状況\n5. その他気づいた点',
'indoor_restaurant': 'この飲食店の画像について、以下の観点で詳しく分析してください:\n1. メニューや料理情報\n2. 価格情報\n3. 営業時間や定休日\n4. 衛生状態\n5. その他気づいた点',
'transport_station': 'この駅の画像について、以下の観点で詳しく分析してください:\n1. プラットフォーム番号、路線名、行き先\n2. 時刻表、運行情報、遅延情報\n3. 出口や乗り換え案内\n4. 施設案内(トイレ、売店など)\n5. その他気づいた点',
'transport_airport': 'この空港の画像について、以下の観点で詳しく分析してください:\n1. ゲート番号、フライト情報\n2. 出発・到着時刻\n3. チェックイン、保安検査の案内\n4. 施設案内(レストラン、ショップなど)\n5. その他気づいた点',
'outdoor_road': 'この道路の画像について、以下の観点で詳しく分析してください:\n1. 交通標識や信号\n2. 道路状況(工事、渋滞、危険箇所など)\n3. 歩行者や自転車の安全\n4. 周辺の建物や施設\n5. その他気づいた点',
'outdoor_nature': 'この自然環境の画像について、以下の観点で詳しく分析してください:\n1. 道標や案内表示\n2. 危険な地形や箇所\n3. 天候や季節の状況\n4. 動植物の存在\n5. その他気づいた点',
'outdoor_water': 'この水辺の画像について、以下の観点で詳しく分析してください:\n1. 安全に関する標識や警告\n2. 水位や潮の状態\n3. 施設や設備の情報\n4. 生物や環境の特徴\n5. その他気づいた点',
'outdoor_tourist': 'この観光地の画像について、以下の観点で詳しく分析してください:\n1. 施設名や歴史的情報\n2. 営業時間、入場料などの情報\n3. 案内表示や地図\n4. 混雑状況\n5. その他気づいた点',
'detail_text': 'この文字情報の画像について、以下の観点で詳しく分析してください:\n1. 重要な日付や期限\n2. 連絡先や問い合わせ情報\n3. 注意事項や警告\n4. 価格や数値情報\n5. その他気づいた点',
'detail_food': 'この食品の画像について、以下の観点で詳しく分析してください:\n1. 商品名や内容\n2. 賞味期限や消費期限\n3. 原材料やアレルギー情報\n4. 価格や量\n5. その他気づいた点',
'general': 'この画像について、以下の観点で詳しく分析してください:\n1. 場所や状況の説明\n2. 安全に関する情報\n3. 重要な表示や標識\n4. 注目すべき物や現象\n5. その他気づいた点'
}
def image_processing(img):
# OpenCV画像をPIL画像に変換
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(img_rgb)
# 第1段階: 基本的なシーン認識
initial_prompt = 'USER: <image>\nこの画像の場所や状況を簡潔に説明してください。\nASSISTANT:'
inputs = processor(initial_prompt, pil_image, return_tensors='pt').to(model.device)
with torch.inference_mode():
output = model.generate(
**inputs,
max_new_tokens=100,
do_sample=True,
temperature=0.2,
top_p=0.9,
pad_token_id=processor.tokenizer.eos_token_id
)
generated_text = processor.decode(output[0], skip_special_tokens=True)
initial_response = generated_text.split('ASSISTANT:')[-1].strip()
# シーンタイプの判定
scene_type = detect_scene_type(initial_response)
# 第2段階: シーン特化した詳細分析
detailed_prompt = f'USER: <image>\n{SCENE_PROMPTS[scene_type]}\nASSISTANT:'
inputs = processor(detailed_prompt, pil_image, return_tensors='pt').to(model.device)
with torch.inference_mode():
output = model.generate(
**inputs,
max_new_tokens=300,
do_sample=True,
temperature=0.2,
top_p=0.9,
pad_token_id=processor.tokenizer.eos_token_id
)
generated_text = processor.decode(output[0], skip_special_tokens=True)
detailed_response = generated_text.split('ASSISTANT:')[-1].strip()
# 結果を保存
result_text = f'【シーンタイプ】: {scene_type}\n【初期分析】:\n{initial_response}\n\n【詳細分析】:\n{detailed_response}\n'
all_results.append(result_text)
# 結果をprint表示
print(result_text)
sys.stdout.flush() # 即座に出力を表示
# OpenCV画像に結果を描画
result_img = img.copy()
height, width = result_img.shape[:2]
# 半透明の背景を追加
overlay = result_img.copy()
cv2.rectangle(overlay, (10, 10), (width-10, height//2), (0, 0, 0), -1)
cv2.addWeighted(overlay, 0.7, result_img, 0.3, 0, result_img)
# 日本語フォントの設定
font = ImageFont.truetype("C:/Windows/Fonts/msgothic.ttc", 14)
# OpenCV画像をPIL画像に変換
img_pil = Image.fromarray(cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
# シーンタイプを表示
draw.text((20, 20), f'シーン: {scene_type}', font=font, fill=(255, 255, 0))
# テキストを折り返して描画
display_text = detailed_response
wrapped_text = textwrap.wrap(display_text, width=60)
y_offset = 50
for line in wrapped_text[:10]: # 最大10行まで表示
draw.text((20, y_offset), line, font=font, fill=(255, 255, 255))
y_offset += 20
# PIL画像をOpenCV画像に変換
result_img = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
return result_img
def show_processed_image(img, window_name):
if img is None:
print('画像の読み込みに失敗しました')
return
print(f'\n処理中: {window_name}')
processed_img = image_processing(img)
# 処理完了後、ユーザーが'q'キーを押すまで画像を表示
while True:
cv2.imshow(window_name, processed_img)
key = cv2.waitKey(100) & 0xFF
if key == ord('q'):
break
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_processed_image(cv2.imread(path), 'Image')
elif choice == '1':
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
print('\nカメラが起動しました。')
print('スペースキー: 撮影')
print('qキー: 終了')
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_processed_image(frame, 'Image')
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'
]
downloaded_files = []
for i, url in enumerate(urls):
filename = f'sample_{i}.jpg'
try:
print(f'\nサンプル画像をダウンロード中: {url}')
urllib.request.urlretrieve(url, filename)
downloaded_files.append(filename)
show_processed_image(cv2.imread(filename), 'Sample Image')
except Exception as e:
print(f'画像のダウンロードに失敗しました: {url}')
print(f'エラー: {e}')
continue
for filename in downloaded_files:
try:
os.remove(filename)
except OSError:
pass
cv2.destroyAllWindows()
# 結果をファイルに保存
if all_results:
with open('result.txt', 'w', encoding='utf-8') as f:
f.write('\n'.join(all_results))
print('\nresult.txtに保存しました')