エラー画面のスクショをClaudeに投げたら「読めません」
最近、エラー画面やログのスクリーンショットをClaudeやCodexに投げてデバッグする方は多いのではないでしょうか。ターミナルの出力をそのままコピペできない環境で、スクショを撮ってAIに「これ何が起きてる?」と聞く。実務では日常的な光景です。
ところが、そのスクショの向きが正しくなかっただけで、AIの回答が使い物にならなくなることがあります。
スマホで撮ったモニターの写真。iPadで取り込んだホワイトボードの図。向きがバラバラなのは日常茶飯事です。「AIなんだから回転くらい補正してくれるでしょ」と思いがちですが、VLM(Vision Language Model)にとって画像の向きは想像以上に致命的です。
たとえるなら、VLMは「目は良いけど首が回らない人」です。正面を向いている画像は完璧に読めるのに、逆さまの画像を渡されると途端に読解力が幼稚園児レベルまで落ちる。どれくらい落ちるのか、実験で検証してみました。
実験設計
実験条件は以下の通りです。
- テスト画像: 12枚(テキスト / 図表 / コード / 混合コンテンツ)
- 回転パターン: 0°、90°、180°、270° の4パターン
- 合計条件数: 12枚 × 4回転 = 48条件
- 比較モデル: Claude 3.5 Sonnet vs GPT-4o
- 評価指標: テキスト抽出精度 + キーワード一致率
各画像を4方向で回転させ、同じプロンプトでテキスト抽出を依頼。正立画像(0°)の結果を正解として、精度を算出しています。
結果: 180°で精度が壊滅する
テキスト抽出精度
| 回転 | Claude | GPT-4o |
|---|---|---|
| 0° | 97.0% | 97.0% |
| 90° | 39.5% | 95.0% |
| 180° | 27.9% | 24.9% |
| 270° | 44.5% | 91.9% |
キーワード一致率
| 回転 | Claude | GPT-4o |
|---|---|---|
| 0° | 94.3% | 94.3% |
| 90° | 50.5% | 94.3% |
| 180° | 22.9% | 14.8% |
| 270° | 62.4% | 100% |
数字を見ると一目瞭然です。
180°(上下逆)は両モデルとも壊滅的。テキスト抽出精度は25〜28%まで落ちています。正立時の97%からおよそ70ポイントの低下です。キーワード一致率に至っては15〜23%。もはや「読んでいない」に等しいレベルです。首が回らない人に逆さまの本を渡したら、こうなりますよね。
モデル間で明暗が分かれた横回転
興味深いのは90°/270°の結果です。
- GPT-4o: 横回転でも90%以上を維持。ほぼ影響なし
- Claude: 40〜50%まで低下。約半分の精度
GPT-4oは横向きの画像に対してかなり頑健です。一方で上下逆(180°)になると、GPT-4oでもキーワード一致率が14.8%まで落ちます。「横には首を傾けて読めるが、逆さまは無理」というのが両モデル共通の傾向です。人間と同じですね。
ヒートマップで見る精度劣化
Claudeの精度劣化パターンです。90°と270°で大きく崩れているのが分かります。
GPT-4oは横回転には強いものの、180°では同様に壊滅しています。
カテゴリ別の劣化パターン
画像の種類によっても劣化の度合いは異なります。
テキスト中心の画像ほど回転の影響を強く受け、図表やコードスニペットは比較的マシという傾向がありました。文字の「読み」に空間的なパターン認識が強く関わっていることが分かります。
なぜ回転で精度が落ちるのか
「画像を回転させただけでここまで精度が落ちるのはおかしくないか?」と思うかもしれません。人間だって逆さまの文字は読みにくいですが、97%が28%になるほどではない。理由はVLMのアーキテクチャにあります。
パッチ分割 + Position Embedding
VLMは入力画像を小さなパッチ(たとえば14×14ピクセル)に分割し、各パッチにPosition Embedding(位置情報)を付与します。このPosition Embeddingは学習時に固定されています。
つまり「左上のパッチは画像の左上にある」という前提で学習しているので、画像が180°回転していると、実際には右下の内容が「左上」として処理されてしまいます。地図を逆さまに渡されて「北はどっち?」と聞かれるようなものです。
学習データの偏り
VLMの学習データはほぼ100%が正立画像です。Webからクロールされた画像も、書籍をスキャンした画像も、基本的に正しい向きになっています。そのため、モデルは「画像は正立している」という強い前提のもとで最適化されています。
テキスト認識は空間パターンに強く依存する
文字認識は本質的に空間パターンのマッチングです。「A」の文字は上に頂点があり下に2本の足がある、というパターンで認識します。180°回転すると「V」に近い形状になり、OCR的な処理が根本的に破綻します。
「回転している」と気づいているのに読めない
興味深いのは、最新のVLM(Claude Sonnet 4など)は画像が回転していることに 気づいている 点です。「画像が逆さまなので読み取りにくいです」と申告してくれます。
人間なら「逆さまだな」と気づいたら頭の中で回転させて読めます。しかしVLMは気づいても、内部のパッチ処理は正立前提のまま走り続けます。「問題を認識する能力」と「問題に対処する能力」が分離しているわけです。
つまり、VLMに「回転してるなら自分で直して読んで」と頼んでも解決しません。パッチ分割の段階で既に情報が崩れているため、テキストレベルの指示では取り返しがつかないのです。だからこそ、 画像を渡す前の前処理 が重要になります。
補足: 色認識には影響しない
面白いことに、同じ実験で「画像の主要な色を答えて」というタスクも試したところ、回転による精度低下はほぼ見られませんでした。色はピクセル単位の特徴で位置に依存しないため、回転の影響を受けにくいのです。VLMが苦手なのは「回転した画像」ではなく「回転した状態での空間パターン認識」だと言えます。
前処理関数の実装
問題が分かったので、対策です。VLMに画像を渡す前に方向を補正する前処理関数を実装しました。
2つのアプローチ
| 方式 | 仕組み | 適用場面 |
|---|---|---|
| EXIF方式 | EXIFのOrientationタグを読んで回転 | スマホ撮影画像(EXIF付き) |
| エントロピー方式 | テキストの空間パターンを分析して向きを推定 | スキャン画像、スクリーンショット(EXIF無し) |
スマホで撮影した画像にはEXIF情報が含まれているので、まずはEXIF方式を試します。EXIFがない場合(スキャン画像やスクリーンショット)は、エントロピー方式にフォールバックする設計です。
実装コード
以下がそのまま使える前処理関数です。依存は Pillow と NumPy のみです。
#!/usr/bin/env python3
"""画像方向補正の前処理関数集"""
import numpy as np
from PIL import Image, ImageFilter
from pathlib import Path
def auto_orient_exif(image_path: str) -> Image.Image:
"""EXIFベースの方向補正
EXIF Orientationタグに従って画像を正しい向きに補正する。
Pillowで生成した画像にはEXIFがないため、その場合はそのまま返す。
"""
img = Image.open(image_path)
exif = img.getexif()
if not exif:
return img
# Orientation tag = 0x0112
orientation = exif.get(0x0112)
if orientation is None:
return img
transforms = {
2: Image.FLIP_LEFT_RIGHT,
3: Image.ROTATE_180,
4: Image.FLIP_TOP_BOTTOM,
5: [Image.FLIP_LEFT_RIGHT, Image.ROTATE_90],
6: Image.ROTATE_270,
7: [Image.FLIP_LEFT_RIGHT, Image.ROTATE_270],
8: Image.ROTATE_90,
}
transform = transforms.get(orientation)
if transform is None:
return img
if isinstance(transform, list):
for t in transform:
img = img.transpose(t)
else:
img = img.transpose(transform)
return img
def auto_orient_entropy(image_path: str) -> Image.Image:
"""エントロピー+エッジ分析ベースの方向推定
テキスト画像では文字行は水平方向に並ぶ特性を利用。
水平エッジ vs 垂直エッジの比率と、上部 vs 下部のエッジ密度で方向を推定。
"""
img = Image.open(image_path)
arr = np.array(img.convert("L").resize((400, 400)))
best_angle = 0
best_score = -1
for angle in [0, 90, 180, 270]:
if angle == 0:
rotated = arr
elif angle == 90:
rotated = np.rot90(arr, k=3) # 90° clockwise
elif angle == 180:
rotated = np.rot90(arr, k=2)
else:
rotated = np.rot90(arr, k=1) # 270° clockwise
score = _compute_text_orientation_score(rotated)
if score > best_score:
best_score = score
best_angle = angle
if best_angle == 0:
return img
# PIL rotate is counter-clockwise, we need to undo the detected rotation
return img.rotate(best_angle, expand=True)
def _compute_text_orientation_score(arr: np.ndarray) -> float:
"""テキスト画像の「正立らしさ」スコアを計算
以下の特徴を組み合わせる:
1. 水平方向のエッジが優勢(テキスト行は水平)
2. 上部にエッジ密度が高い(タイトル/ヘッダ)
3. 行のバリアンスパターン(テキスト行と行間の交互パターン)
"""
# Sobel-like edge detection
h_edges = np.abs(np.diff(arr.astype(float), axis=1)) # horizontal edges
v_edges = np.abs(np.diff(arr.astype(float), axis=0)) # vertical edges
# Feature 1: horizontal edge dominance
h_sum = np.sum(h_edges)
v_sum = np.sum(v_edges)
# Feature 2: row variance pattern
row_means = np.mean(arr, axis=1)
row_variance = np.var(np.diff(row_means))
# Feature 3: top-heaviness
h, w = arr.shape
top_edge_density = np.mean(v_edges[:h//3, :])
bottom_edge_density = np.mean(v_edges[2*h//3:, :])
# Feature 4: left-alignment indicator
col_means = np.mean(arr, axis=0)
left_quarter = np.mean(col_means[:w//4])
right_quarter = np.mean(col_means[3*w//4:])
# Combine scores
score = 0.0
# Horizontal structure bonus
if h_sum + v_sum > 0:
score += (v_sum / (h_sum + v_sum)) * 30
# Row regularity bonus
score += min(row_variance / 100, 30)
# Top-heavy bonus
if top_edge_density > bottom_edge_density:
score += 20
# Whitespace gradient bonus
bottom_brightness = np.mean(arr[3*h//4:, :])
top_brightness = np.mean(arr[:h//4, :])
if bottom_brightness > top_brightness:
score += 10
return score
def correct_image(image_path: str, method: str = "entropy") -> Image.Image:
"""統合補正関数
Args:
image_path: 画像パス
method: 補正手法 ("exif", "entropy", "edge_lines")
Returns:
補正済み画像
"""
methods = {
"exif": auto_orient_exif,
"entropy": auto_orient_entropy,
}
func = methods.get(method)
if func is None:
raise ValueError(f"Unknown method: {method}. Choose from {list(methods.keys())}")
return func(image_path)
def correct_and_save(image_path: str, output_path: str, method: str = "entropy") -> str:
"""補正して保存"""
img = correct_image(image_path, method)
img.save(output_path)
return output_path
使い方
from orientation_preprocess import correct_image
# EXIF情報がある画像(スマホ撮影など)
img = correct_image("photo.jpg", method="exif")
# EXIF情報がない画像(スクリーンショット、スキャンなど)
img = correct_image("scan.png", method="entropy")
# 補正して保存
from orientation_preprocess import correct_and_save
correct_and_save("input.png", "output.png", method="entropy")
VLM呼び出し前に挟む例
実際のワークフローではこんな感じで使います。
from orientation_preprocess import correct_and_save
import anthropic
# Step 1: 画像の方向を補正
correct_and_save("receipt.jpg", "receipt_corrected.jpg", method="exif")
# Step 2: 補正済み画像をVLMに渡す
client = anthropic.Anthropic()
# ... 以降は通常のAPI呼び出し
たった1行の前処理を挟むだけで、97%の精度を取り戻せます。
前処理の効果
まとめ
今回の実験で分かったことをまとめます。
- 180°回転(上下逆)は壊滅的: 両モデルとも精度が25〜28%まで低下する
- モデルによって回転耐性が違う: GPT-4oは横回転に強い(90%維持)が、Claudeは40%まで落ちる
- 前処理1行で解決できる: 方向補正をVLM呼び出しの前に入れるだけで精度が回復する
VLMは「見る」能力が高いモデルですが、「正しい向きで見せてもらう」ことを前提に作られています。目は良いけど首が回らない。そんなAIの弱点は、画像を渡す前のほんの一手間でカバーできます。
エラー画面のスクショをAIに投げてデバッグする方、スマホで撮った写真をVLMに渡している方。前処理関数を1行挟むだけで、「読めません」が「ここがバグです」に変わるかもしれません。
VLMの実践的な使いこなし方に興味がある方は、Claude Code活用についてまとめた Zenn Book もご覧ください。



