CLIPスコアで画像品質を定量評価する方法

CLIPスコアで画像品質を定量評価する方法

この記事でわかること

  • CLIPスコアとは何か — テキストと画像の類似度を数値化する仕組み
  • Pythonで実際に計算する方法 — transformersライブラリを使った完全なコード例
  • スコアの読み方と注意点 — 高ければ良いわけではない、その限界
  • 検証記事への応用 — 複数条件の比較にCLIPスコアを使う方法

CLIPスコアとは

CLIPモデルの概要

CLIP(Contrastive Language-Image Pre-training)は、OpenAIが2021年に公開したマルチモーダルモデルです(論文: Learning Transferable Visual Models From Natural Language Supervision)。

CLIPはテキストと画像を同じベクトル空間に埋め込むモデルで、4億組のテキスト-画像ペアで学習されています。テキストエンコーダーと画像エンコーダーの2つで構成され、それぞれの出力ベクトルのコサイン類似度を計算することで、テキストと画像がどの程度「意味的に一致しているか」を数値化できます。

画像生成の文脈での使い道

AI画像生成では、CLIPスコアを以下の目的で使えます。

  • プロンプトと生成画像の一致度を数値化する — 「このプロンプトで生成した画像は、どの程度プロンプトの意図を反映しているか」
  • 複数のプロンプトやパラメータを比較する — 同じ被写体で条件を変えたとき、どちらがプロンプトに忠実か
  • 主観評価の補助 — 「なんとなく良い」ではなく、数値で傾向を把握する

CLIPスコアの計算方法

環境準備

必要なパッケージをインストールします。

pip install transformers torch pillow

計算スクリプト

以下のスクリプトは、画像ファイルとプロンプトテキストを受け取り、CLIPスコア(0〜1)を出力します。

"""CLIPスコア計算スクリプト"""

import sys
from pathlib import Path

import torch
from PIL import Image
from transformers import CLIPModel, CLIPProcessor

# モデルの読み込み(初回はダウンロードが発生する)
MODEL_NAME = "openai/clip-vit-base-patch32"
model = CLIPModel.from_pretrained(MODEL_NAME)
model.eval()
processor = CLIPProcessor.from_pretrained(MODEL_NAME)


def calc_clip_score(image_path: str, text: str) -> float:
    """画像とテキストのCLIPスコアを計算する。

    Args:
        image_path: 画像ファイルのパス
        text: プロンプトテキスト

    Returns:
        0〜1のCLIPスコア(コサイン類似度を0-1に正規化)
    """
    image = Image.open(image_path).convert("RGB")

    inputs = processor(text=[text], images=image, return_tensors="pt", padding=True)

    with torch.no_grad():
        outputs = model(**inputs)
        # logits_per_imageはコサイン類似度 x 温度パラメータ(logitスケール)
        # 0〜1の範囲に収めるためsigmoidを適用
        score = torch.sigmoid(outputs.logits_per_image).item()

    return score


if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("使い方: python clip_score.py <画像パス> <プロンプト>")
        print('例: python clip_score.py image.webp "a fluffy cat sleeping"')
        sys.exit(1)

    image_path = sys.argv[1]
    prompt = sys.argv[2]

    if not Path(image_path).exists():
        print(f"エラー: {image_path} が見つかりません")
        sys.exit(1)

    score = calc_clip_score(image_path, prompt)
    print(f"画像: {image_path}")
    print(f"プロンプト: {prompt}")
    print(f"CLIPスコア: {score:.4f}")

使い方はシンプルです。

python clip_score.py image.webp "a beautiful mountain landscape with lake"

コードのポイント

  • openai/clip-vit-base-patch32 は最も標準的なCLIPモデルで、多くのベンチマークの基準として使われています
  • logits_per_image はコサイン類似度に温度パラメータ(学習済み)を掛けた値で、生の類似度より解釈しやすいスケールになっています
  • torch.sigmoid で0〜1の範囲に変換しています。生のlogit値は負の値を取ることもあるため、この変換を通すことで直感的なスコアになります

実際に計算してみる

ここでは3つの異なるジャンルの画像で、CLIPスコアがどのような値を取るか見てみます。

例1: 山岳風景

風景プロンプト
a beautiful mountain landscape with a crystal clear lake, snow-capped peaks reflected in still water, golden hour lighting, photorealistic

山岳風景の例

CLIPスコア: 0.89

画像には岩肌と草地が広がる山岳地帯の俯瞰が写っており、中央にエメラルドグリーンの湖、山頂付近には残雪が見えます。プロンプトの「mountain」「lake」「snow-capped」は反映されていますが、「reflected in still water」(湖面の反射)や「golden hour lighting」(夕方の光)は画像上確認できません。それでもスコアは0.89と高めに出ており、風景写真は主要な被写体要素が明確なためCLIPスコアが高く出やすいジャンルといえます。

例2: 猫

猫プロンプト
a fluffy orange tabby cat sleeping on a sofa, soft natural lighting, cozy living room

猫の例

CLIPスコア: 0.87

画像にはグレーのソファの上で眠っている茶トラ猫が写っています。毛色(orange tabby)、姿勢(sleeping)、場所(sofa)、自然光の柔らかい雰囲気(soft natural lighting)がプロンプトとよく一致しています。ただし「fluffy」のような質感の形容詞がどこまでスコアに反映されるかは、画像によってばらつきがあります。

例3: 料理

ラーメンプロンプト
a bowl of ramen with a soft-boiled egg and green onions, steam rising, overhead shot, food photography

ラーメンの例

CLIPスコア: 0.84

画像には丼に入ったラーメンが俯瞰で写っており、半熟卵が2つと刻みネギが確認できます。プロンプトの「soft-boiled egg」「green onions」「overhead shot」はよく一致していますが、「steam rising」(立ち上る湯気)は画像上確認できません。料理の画像はスコアがやや低めに出る傾向があり、細部の要素すべてが反映されているかどうかでスコアが変動します。

スコアの傾向

プロンプトスコア
山岳風景0.89
0.87
ラーメン0.84

ラボ長コメント: 0.05差だと目で見ても正直わからないかな。0.1以上開いてやっと体感と一致するイメージ。

スコアの解釈と注意点

スコアの目安

CLIPスコア(sigmoid適用後)のおおまかな目安です。ただし絶対的な基準ではなく、プロンプトの複雑さやジャンルによって変わります。

スコア解釈
0.90以上プロンプトの主要素がほぼすべて反映されている
0.80〜0.89概ね意図通り。細部に差異がある可能性
0.70〜0.79大枠は合っているが、一部要素が欠落・変質
0.70未満プロンプトとの乖離が大きい

CLIPスコアの限界

CLIPスコアが高い画像が「良い画像」とは限りません。以下の点に注意が必要です。

1. 構図や美的品質は評価できない

CLIPは「テキストと画像の意味的な一致度」を測るモデルです。画像の構図が良いか、色彩が美しいかといった美的品質は評価対象外です。プロンプトに「beautiful」と書いてあっても、CLIPが「美しさ」を正確にスコアリングしているわけではありません。

2. 英語と日本語でスコアが大きく異なる

CLIPの学習データは英語のテキスト-画像ペアが大半を占めています。日本語プロンプトを入力すると、英語の場合と比べてスコアが著しく低くなります。CLIPスコアで評価する際は英語プロンプトを使うのが前提です。

3. テキストの長さでスコアが変わる

短いプロンプト(a cat)は高スコアが出やすく、長く詳細なプロンプトはスコアが下がりやすい傾向があります。要素が増えるほど「すべてを満たす」ハードルが上がるためです。異なる長さのプロンプト間でスコアを直接比較するのは適切ではありません。

4. 手指や解剖学的な正確さは検出できない

「指が6本ある」「腕が不自然に曲がっている」といった破綻は、CLIPスコアには反映されません。CLIPはテキストと画像のセマンティックな対応を見ているため、画像内の物理的な整合性は評価範囲外です。

ラボ長コメント: えっと、ここ大事なんですけど、CLIPスコアだけで画像の良し悪しは判定できないです。構図も美的品質も指の破綻も評価対象外なので、あくまでプロンプトとの意味的な一致度の参考値として使うのが正しいかなと。

応用: 複数条件の比較に使う

CLIPスコアが最も力を発揮するのは、同一プロンプトで条件を変えた画像群の比較です。

比較スクリプト

以下のスクリプトは、ディレクトリ内の画像をまとめてスコア計算し、CSVで出力します。

"""複数画像のCLIPスコアをバッチ計算するスクリプト"""

import csv
import sys
from pathlib import Path

import torch
from PIL import Image
from transformers import CLIPModel, CLIPProcessor

MODEL_NAME = "openai/clip-vit-base-patch32"
model = CLIPModel.from_pretrained(MODEL_NAME)
model.eval()
processor = CLIPProcessor.from_pretrained(MODEL_NAME)


def calc_clip_score(image_path: str, text: str) -> float:
    """画像とテキストのCLIPスコアを計算する。"""
    image = Image.open(image_path).convert("RGB")
    inputs = processor(text=[text], images=image, return_tensors="pt", padding=True)
    with torch.no_grad():
        outputs = model(**inputs)
        score = torch.sigmoid(outputs.logits_per_image).item()
    return score


def batch_score(image_dir: str, prompt: str, output_csv: str = "clip_scores.csv"):
    """ディレクトリ内の画像をすべてスコア計算し、CSVに出力する。"""
    image_dir = Path(image_dir)
    extensions = {".png", ".jpg", ".jpeg", ".webp"}
    image_files = sorted(
        p for p in image_dir.iterdir() if p.suffix.lower() in extensions
    )

    if not image_files:
        print(f"エラー: {image_dir} に画像ファイルが見つかりません")
        sys.exit(1)

    results = []
    for img_path in image_files:
        score = calc_clip_score(str(img_path), prompt)
        results.append({"file": img_path.name, "score": score})
        print(f"  {img_path.name}: {score:.4f}")

    # スコア順にソート
    results.sort(key=lambda x: x["score"], reverse=True)

    # CSV出力
    with open(output_csv, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=["file", "score"])
        writer.writeheader()
        writer.writerows(results)

    scores = [r["score"] for r in results]
    avg = sum(scores) / len(scores)
    print(f"\n平均スコア: {avg:.4f}")
    print(f"最高: {results[0]['file']} ({results[0]['score']:.4f})")
    print(f"最低: {results[-1]['file']} ({results[-1]['score']:.4f})")
    print(f"結果を {output_csv} に保存しました")


if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("使い方: python clip_score_batch.py <画像ディレクトリ> <プロンプト>")
        print('例: python clip_score_batch.py ./images/ "a mountain landscape"')
        sys.exit(1)

    image_dir = sys.argv[1]
    prompt = sys.argv[2]
    output_csv = sys.argv[3] if len(sys.argv) > 3 else "clip_scores.csv"

    batch_score(image_dir, prompt, output_csv)
python clip_score_batch.py ./output_images/ "a mountain landscape with lake" results.csv

検証記事での活用例

当ブログの検証記事では、同じseedでプロンプトの一部を変更して画像を生成することが多いです。たとえば以下のような比較に使えます。

  • プロンプトAとBのどちらがより忠実か — 同じseedで2つのプロンプトの画像を生成し、それぞれのCLIPスコアを比較
  • 特定の語句を追加した効果 — 語句追加前後でスコアがどう変化するか
  • 複数seedでの安定性 — seed 3つ分のスコアのばらつき(標準偏差)を見る

ただし、CLIPスコアはあくまで補助的な指標です。最終的な判断は目視で行い、CLIPスコアは傾向の把握に使うのが適切です。

実データ: 検証記事の画像でCLIPスコアを計算してみた

ここまで概念とコードを紹介してきましたが、実際に当ブログの検証記事で使った画像でCLIPスコアを計算してみます。使用モデルは openai/clip-vit-base-patch32、スコアはコサイン類似度(0〜1)です。

ライティング検証記事の画像

ライティング比較記事のコントロール画像とネオンライティング画像を使って、プロンプト一致度を比較します。

画像プロンプトコサイン類似度
コントロール画像 (control_seed42)..., full body, photorealistic(一致)0.3694
ネオン画像 (neon-lighting_seed42)..., full body, neon lighting, photorealistic(一致)0.3449
ネオン画像 (neon-lighting_seed42)..., full body, photorealistic(不一致)0.3332

※プロンプトの共通部分: 1girl, 32yo japanese actress, bikini, standing on sandy beach, ocean background

ネオン画像に対して「neon lighting」を含む正しいプロンプトを与えた場合(0.3449)と、含まないコントロール用プロンプトを与えた場合(0.3332)では、0.0117の差が出ました。数値としてはわずかですが、プロンプトに含まれる要素が画像に反映されていれば、スコアが高くなるという期待通りの方向性です。

コントロール画像のスコア(0.3694)がネオン画像(0.3449)より高いのは、コントロール画像のほうがプロンプトの各要素(ビーチ、海、全身)を素直に反映した構図になっているためと考えられます。

コスプレ検証記事の画像

コスプレ衣装比較記事のメイド服とシェフ服の画像です。

画像プロンプトコサイン類似度
メイド画像 (a01_maid_seed1)..., maid outfit, ...(一致)0.2934
シェフ画像 (a05_chef_seed1)..., chef uniform, ...(一致)0.2951

※プロンプトの共通部分: 1girl, 32yo japanese actress, [衣装], standing, looking at viewer, indoor studio, soft studio lighting, full body, photorealistic

メイドとシェフでスコアはほぼ同等(差0.0017)でした。どちらもプロンプト通りの衣装が描かれており、CLIPスコア上も同程度の一致度と評価されています。

ライティング記事の画像(0.33〜0.37)に比べてコスプレ記事の画像(0.29台)のスコアが低いですが、これはプロンプトの長さの違いが影響しています。コスプレ記事のプロンプトのほうがトークン数が多く、すべての要素を満たすハードルが上がるため、スコアが下がる傾向があります。異なるプロンプト間でのスコアの絶対値比較には意味がないことを示す良い例です。

スコアの読み方

今回の実データからわかることをまとめます。

  • 同一プロンプトでの比較は有効: ネオン画像に対する一致/不一致プロンプトで0.01程度の差が出た
  • 異なるプロンプト間の絶対値比較は不適切: プロンプトの長さや要素数が違うとスコアのベースラインが変わる
  • コサイン類似度の絶対値は低め: 0.3前後の値が出ているが、これはCLIPの特性上正常な範囲。1.0に近い値は通常出ない

ラボ長コメント: 0.01差だと「ふーん」って感じですけど、同じプロンプトで条件変えた画像を大量比較するときには傾向が数字で見えるのは助かりますね。目視の「なんとなくこっち」より根拠になると思います。

まとめ

  • CLIPスコアはプロンプトと画像の意味的な一致度を0〜1で数値化する指標
  • Pythonスクリプトで簡単に計算できる — transformersライブラリで数十行のコード
  • 万能ではない — 構図、美的品質、解剖学的正確さは評価できない。英語プロンプト前提
  • 複数条件の比較には有用 — 同一プロンプトでの条件比較や、プロンプト変更の効果測定に使える
  • 当ブログの今後の検証記事でも、主観評価の補助として活用予定

関連記事