CLIPによる画像とテキストの類似性算出(ソースコードと実行結果)

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 open-clip-torch
CLIPによる画像とテキストの類似性算出プログラム
特徴
このプログラムは、プログラムは事前学習済みモデルを使用し、追加の学習なしで画像と任意のテキスト記述との類似度を計算できる。
主要技術
- CLIP (Contrastive Language-Image Pre-training)
CLIPは2021年にOpenAIが発表した技術である[1]。4億組の画像とテキストのペアで事前学習され、画像とテキストを共通の埋め込み空間にマッピングすることで、両者の意味的な類似度を計算できる。画像エンコーダ(Vision Transformer)とテキストエンコーダ(Transformer)の2つのエンコーダで構成され、コントラスティブ学習により訓練される。 - Vision Transformer (ViT)
Vision Transformerは、自然言語処理で成功したTransformerアーキテクチャをコンピュータビジョンに適用した技術である。画像を固定サイズのパッチに分割し、各パッチをトークンとして扱い、自己注意機構により画像の異なる部分間の大域的な関係を捉える。CNNと異なり、局所的な畳み込みではなく、画像全体の文脈を理解できる。
参考文献
- [1] Radford, A., Kim, J. W., Hallacy, C., Ramesh, A., Goh, G., Agarwal, S., ... & Sutskever, I. (2021). Learning transferable visual models from natural language supervision. In International Conference on Machine Learning (pp. 8748-8763). PMLR.
- [2] Dosovitskiy, A., Beyer, L., Kolesnikov, A., Weissenborn, D., Zhai, X., Unterthiner, T., ... & Houlsby, N. (2021). An image is worth 16x16 words: Transformers for image recognition at scale. In International Conference on Learning Representations.
- [3] OpenAI. (2021). CLIP: Connecting text and images. https://openai.com/research/clip
ソースコード
# プログラム名: CLIPによる画像とテキストの類似性算出プログラム
# 特徴技術名: CLIP (Contrastive Language-Image Pre-training) / OpenCLIP
# 出典: Radford, A., et al. (2021). Learning Transferable Visual Models From Natural Language Supervision. ICML 2021.
# 特徴機能: ゼロショット画像分類 - 事前学習済みモデルが新しいカテゴリの画像を追加学習なしで分類可能
# 学習済みモデル: 複数のCLIPモデルから選択可能 - OpenAI CLIPおよびOpenCLIP(多言語対応含む)。URL: https://huggingface.co/openai/ および https://github.com/mlfoundations/open_clip
# 方式設計:
# - 関連利用技術:
# - Transformers (Hugging Face): 事前学習済みモデルの読み込みと推論
# - PyTorch: テンソル演算と深層学習処理
# - PIL (Pillow): 画像の読み込みと前処理、日本語テキスト描画
# - OpenCLIP: 多言語対応CLIPモデル
# - 入力と出力: 入力: 画像(ユーザは「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画面内に処理結果を日本語テキストで表示.同時にprint()で処理結果を表示.プログラム終了時にprint()で表示した処理結果をresult.txtファイルに保存し,「result.txtに保存しました」とprint()で表示.プログラム開始時に,プログラムの概要,ユーザが行う必要がある操作をprint()で表示.
# - 処理手順: 1)画像とテキストクエリを入力、2)CLIPプロセッサで前処理、3)CLIPモデルで特徴抽出、4)コサイン類似度計算、5)類似度スコアを表示
# - 前処理、後処理: 前処理: 画像をモデルに応じたサイズにリサイズ、正規化。後処理: 類似度スコアをそのまま表示(ソフトマックスによる確率化は行わない)
# - 追加処理: カスタムテキストクエリ機能 - 標準分析後にユーザ独自のカテゴリで追加分析可能。日本語表示 - PillowによるBGR↔RGB変換とImageDrawを使用した日本語テキスト描画
# - 調整を必要とする設定値: TEXT_QUERIES - 分析対象のテキストクエリリスト。画像の内容に応じて適切なカテゴリを設定
# 将来方策: TEXT_QUERIESの自動生成機能 - WordNetやConceptNetなどの知識ベースを活用して、関連するカテゴリを自動的に生成
# その他の重要事項: 初回実行時はモデルのダウンロードが必要(ViT-B/32: 340MB、ViT-L/14: 1.2GB)
# 前準備: pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
# pip install transformers pillow opencv-python open-clip-torch
import cv2
import tkinter as tk
from tkinter import filedialog
import urllib.request
import os
import torch
from PIL import Image, ImageFont, ImageDraw
from transformers import CLIPProcessor, CLIPModel
import numpy as np
import time
import open_clip
# 設定値
TEXT_QUERIES = [
'猫の写真',
'犬の写真',
'風景写真',
'料理の写真',
'人物の写真',
'建物の写真'
]
FONT_SCALE = 0.7 # Pillow日本語表示用フォント調整(未使用)
FONT_THICKNESS = 2 # Pillow日本語表示用フォント調整(未使用)
Y_OFFSET_START = 30 # テキスト表示開始位置
Y_OFFSET_STEP = 30 # テキスト表示行間
FONT_PATH = "C:/Windows/Fonts/msgothic.ttc"
FONT_SIZE = 30
# 利用可能なモデルと言語対応情報
AVAILABLE_MODELS = {
'0': ('openai/clip-vit-base-patch32', 'ViT-B/32 (標準速度・標準精度)', '英語のみ', 'huggingface'),
'1': ('openai/clip-vit-base-patch16', 'ViT-B/16 (中速度・精度向上)', '英語のみ', 'huggingface'),
'2': ('openai/clip-vit-large-patch14', 'ViT-L/14 (低速度・高精度)', '英語のみ', 'huggingface'),
'3': ('openai/clip-vit-large-patch14-336', 'ViT-L/14@336px (最低速度・最高精度)', '英語のみ', 'huggingface'),
'4': ('ViT-B-32-xlm-roberta-base-laion5B-s13B-b90k', 'OpenCLIP ViT-B/32 多言語版 (中速度・精度向上)', '多言語対応(日本語含む)', 'openclip'),
'5': ('ViT-B-32', 'OpenCLIP ViT-B/32 (標準速度・標準精度)', '英語のみ', 'openclip')
}
# グローバル変数で結果を保存
log = []
# プログラム開始時の説明
print('CLIP画像分析プログラム')
print('画像とテキストの類似度を計算します')
print('')
# モデル選択
print('使用するCLIPモデルを選択してください:')
for key, (_, desc, lang_support, _) in AVAILABLE_MODELS.items():
print(f'{key}: {desc} - {lang_support}')
model_choice = input('選択 (デフォルト: 0): ').strip()
if model_choice not in AVAILABLE_MODELS:
model_choice = '0'
MODEL_NAME, model_desc, lang_support, model_type = AVAILABLE_MODELS[model_choice]
print(f'選択されたモデル: {model_desc}')
print(f'対応言語: {lang_support}')
print('')
# 言語に関する注意事項
if model_type == 'openclip' and 'xlm-roberta' in MODEL_NAME:
print('【重要】このモデルは多言語対応で、日本語クエリも処理可能です')
print('日本語・英語どちらでも同等の精度が期待できます')
else:
print('【重要】このモデルは英語データで学習されています')
print('日本語クエリも処理可能ですが、英語クエリの方が精度が向上します')
print('例: 「猫の写真」→「cat photo」の方が精度が向上します')
print('')
# デバイス設定
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'使用デバイス: {device}')
# CLIPモデルとプロセッサのセットアップ
print('CLIPモデルを読み込んでいます...')
if model_type == 'huggingface':
model = CLIPModel.from_pretrained(MODEL_NAME).to(device)
processor = CLIPProcessor.from_pretrained(MODEL_NAME)
tokenizer = None
preprocess = None
else: # openclip
if MODEL_NAME == 'ViT-B-32-xlm-roberta-base-laion5B-s13B-b90k':
model, _, preprocess = open_clip.create_model_and_transforms(
'ViT-B-32',
pretrained='xlm-roberta-base-laion5B-s13B-b90k'
)
else:
model, _, preprocess = open_clip.create_model_and_transforms(
MODEL_NAME,
pretrained='openai'
)
model = model.to(device)
tokenizer = open_clip.get_tokenizer(MODEL_NAME)
processor = None
print('モデルの読み込みが完了しました')
print('')
def image_processing(img, queries, query_type='標準'):
# OpenCV画像をPIL画像に変換
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(img_rgb)
if processor is not None: # Hugging Face CLIP
# CLIPで処理
inputs = processor(
text=queries,
images=pil_image,
return_tensors='pt',
padding=True
)
# デバイスに転送
inputs = {k: v.to(device) for k, v in inputs.items()}
# 推論
with torch.no_grad():
outputs = model(**inputs)
# 類似度スコアを取得(softmaxなし)
logits_per_image = outputs.logits_per_image
scores = logits_per_image.detach().cpu().numpy()[0]
else: # OpenCLIP
# 画像の前処理
image_tensor = preprocess(pil_image).unsqueeze(0).to(device)
# テキストのトークナイズ
text_tokens = tokenizer(queries).to(device)
# 推論
with torch.no_grad():
image_features = model.encode_image(image_tensor)
text_features = model.encode_text(text_tokens)
# 正規化
image_features /= image_features.norm(dim=-1, keepdim=True)
text_features /= text_features.norm(dim=-1, keepdim=True)
# コサイン類似度の計算
logits_per_image = 100.0 * image_features @ text_features.T
scores = logits_per_image.detach().cpu().numpy()[0]
# 結果を表示用画像に追加
result_img = img.copy()
h, w = result_img.shape[:2]
# 結果をテキストで表示
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
result_text = f'[{timestamp}] {query_type}分析結果:'
print(result_text)
log.append(result_text)
# 日本語表示のためPillowを使用
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
img_pil = Image.fromarray(cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
for i, (text, score) in enumerate(zip(queries, scores)):
result_line = f'「{text}」: {score:.2f}' # 類似度スコア表示
print(result_line)
log.append(result_line)
# Pillow で日本語テキストを描画
draw.text((10, Y_OFFSET_START + i * Y_OFFSET_STEP), result_line, font=font, fill=(0, 255, 0))
# PIL画像をOpenCV形式に戻す
result_img = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
print('') # 空行
log.append('')
return result_img
def show_processed_image(img, window_name):
if img is None:
print('画像の読み込みに失敗しました')
return
# 標準クエリで分析
result = image_processing(img, TEXT_QUERIES, '標準')
cv2.imshow(window_name, result)
cv2.waitKey(1) # 画像を確実に表示
print('カスタムクエリを入力できます(空行でこの画像の分析を終了)')
while True:
print('')
if model_type == 'openclip' and 'xlm-roberta' in MODEL_NAME:
print('カスタムクエリを入力(日本語・英語可、空行で終了):')
else:
print('カスタムクエリを入力(英語推奨、空行で終了):')
query = input('> ')
if query == '':
print('この画像の分析を終了します')
break
# カスタムクエリで再分析
result = image_processing(img, [query], 'カスタム')
cv2.imshow(window_name, result)
cv2.waitKey(1) # 画像を確実に更新
print('次のカスタムクエリを入力できます(空行で終了)')
print('操作説明:')
print('- 標準分析後、カスタムクエリを入力できます')
print('- 空行入力で次の画像に進みます')
print('')
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)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
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'
]
temp_files = []
for i, url in enumerate(urls):
filename = os.path.join('.', f'sample_{i}.jpg') # パス操作の統一
try:
urllib.request.urlretrieve(url, filename)
temp_files.append(filename)
show_processed_image(cv2.imread(filename), 'Sample Image')
except Exception as e:
print(f'画像のダウンロードに失敗しました: {url}')
print(f'エラー: {e}')
exit()
# 一時ファイルの削除
for filename in temp_files:
try:
os.remove(filename)
except OSError:
pass
cv2.destroyAllWindows()
# 結果をファイルに保存
if log:
with open('result.txt', 'w', encoding='utf-8') as f:
f.write('\n'.join(log))
print('result.txtに保存しました')