0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude API の画像入力(Vision)を実装する — Base64 vs URL 使い分けとトークン爆食いを防ぐリサイズ、3つのハマりどころ【2026】

0
Posted at

はじめに / 対象と前提

Claude API に画像を送って解析させたいけど、実装したらエラーが出た、あるいはエラーは出ないのにトークン消費が異常に多い——そんな詰まりどころを解消する記事です。

対象読者:Claude API(Anthropic SDK)で Vision 機能を初めて実装するエンジニア。Python 実装例ですが TypeScript SDK でも考え方は同じです。

前提環境:

  • Python 3.13
  • anthropic SDK 0.71.x 系
  • モデル:claude-sonnet-5(Vision 対応モデル全般に適用可能)

TL;DR

  • 画像は content 配列に type: "image" ブロックを追加するだけで送れる。base64url の 2 方式があり、外部アクセス不可な環境や機密画像は base64 一択
  • 画像 1 枚のトークン消費は概算で (幅px × 高さpx) / 750リサイズせずに送ると数千トークンを一瞬で溶かす
  • 複数画像を送るときは「画像 → その画像を指すテキスト」の順が Claude にとって解釈しやすい
  • 対応フォーマットは image/jpeg image/png image/gif image/webp の 4 種のみ。それ以外は 400 エラー

手順 / 動かし方

1. base64 で画像を送る(ローカルファイル)

外部からアクセスできないローカル画像やスクリーンショットは base64 でエンコードして送ります。

import base64
import anthropic

client = anthropic.Anthropic()

def encode_image(path: str) -> tuple[str, str]:
    with open(path, "rb") as f:
        data = base64.standard_b64encode(f.read()).decode("utf-8")
    media_type = "image/png" if path.endswith(".png") else "image/jpeg"
    return data, media_type

image_data, media_type = encode_image("./screenshot.png")

message = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": media_type,
                        "data": image_data,
                    },
                },
                {
                    "type": "text",
                    "text": "この画像に写っているエラーメッセージを要約して",
                },
            ],
        }
    ],
)
print(message.content[0].text)

2. URL で画像を送る(公開済み画像)

すでにインターネット上に公開されている画像であれば、source を差し替えるだけです。

content = [
    {"type": "image", "source": {"type": "url", "url": "https://example.com/chart.png"}},
    {"type": "text", "text": "このグラフの傾向を3行で説明して"},
]

base64 エンコードの手間がなくリクエストも軽量ですが、Claude 側がその URL に実際にアクセスできる必要があるので、社内限定画像や認証必須 URL では使えません。

3. 画像トークンコストを事前に見積もる

Claude の画像トークン消費は概算式で計算できます。

def estimate_image_tokens(width: int, height: int) -> int:
    return (width * height) // 750

# 4000x3000 のスマホ写真をそのまま送った場合
print(estimate_image_tokens(4000, 3000))  # -> 16000 トークン前後

4000×3000 のスマホ写真をノーリサイズで送ると、それだけで 1 万 6000 トークン前後が消える計算になります。テキストのやり取りと合わせると、あっという間にコンテキストと料金を圧迫します。

4. Pillow でリサイズしてから送る

長辺を 1568px 程度に収めると、精度を大きく落とさずにトークン消費を抑えられます(Claude 内部でも長辺 1568px を超える画像は自動的にダウンスケールされますが、アップロード前に自前でリサイズしておくとリクエストサイズ自体も減らせます)。

from PIL import Image
import io
import base64

def resize_and_encode(path: str, max_side: int = 1568) -> tuple[str, str]:
    img = Image.open(path)
    img.thumbnail((max_side, max_side))  # アスペクト比を保ったまま縮小

    buf = io.BytesIO()
    fmt = "PNG" if path.endswith(".png") else "JPEG"
    img.save(buf, format=fmt)
    data = base64.standard_b64encode(buf.getvalue()).decode("utf-8")
    media_type = f"image/{fmt.lower()}"
    return data, media_type

ハマりどころ

1. Could not process image で 400 エラー

BadRequestError: Error code: 400 - {'error': {'type': 'invalid_request_error',
'message': 'messages.0.content.0.image.source.base64.media_type: Input should be
"image/jpeg", "image/png", "image/gif" or "image/webp"'}}

原因:.heic(iPhone の標準形式)や .bmp をそのまま送っていた。Claude が対応するのは image/jpeg image/png image/gif image/webp の 4 種のみ。

回避策:送信前に Pillow で対応フォーマットへ変換する。

img = Image.open("photo.heic")
img.convert("RGB").save("photo.jpg", format="JPEG")

2. 複数画像を送ったら「どの画像の話か」を Claude が混同する

3 枚のスクリーンショットを比較させたくて、テキストを先頭にまとめて後ろに画像を並べたところ、回答が画像の順序と対応しませんでした。

原因:画像とテキストの対応関係が曖昧だと、モデルが画像同士を正しく紐付けにくい。

回避策:「画像 1 枚 → その画像の説明テキスト」の組を繰り返す。

content = []
for i, (path, caption) in enumerate(image_and_captions, start=1):
    data, media_type = encode_image(path)
    content.append({
        "type": "image",
        "source": {"type": "base64", "media_type": media_type, "data": data},
    })
    content.append({"type": "text", "text": f"上の画像が手順{i}のスクリーンショットです"})
content.append({"type": "text", "text": "3枚を比較して差分を教えて"})

これで画像とキャプションの対応が明確になり、回答の精度が安定しました。

3. base64 データが大きすぎてリクエストが reject される

高解像度画像を何枚も base64 でそのまま詰め込んだところ、リクエストサイズの上限に触れてエラーになりました。

原因:base64 エンコードは元データよりサイズが約 1.33 倍に膨れる。リサイズせずに複数枚まとめて送ると簡単に上限へ到達する。

回避策:前述の「3. Pillow でリサイズしてから送る」を必ず経由する。目安として 1 リクエストに含める画像は長辺 1568px・JPEG 品質 85 程度に揃えると安定して通ります。

背景・補足

画像トークンの概算式 (幅 × 高さ) / 750 は Anthropic 公式ドキュメントに記載の目安です。実際の消費は画像の複雑さで多少前後しますが、リサイズ有無で桁が変わるほど差が出るため、精度より「まずリサイズする」習慣の方が効果があります。

まとめ

  • 画像入力は base64(ローカル・機密)と URL(公開済み)の 2 方式、環境に応じて使い分ける
  • リサイズせずに送るとトークンも料金も無駄に消費する。長辺 1568px を目安に事前リサイズする
  • 複数画像は「画像→説明テキスト」の組で送ると対応関係がぶれない
  • 対応フォーマットは JPEG / PNG / GIF / WebP の 4 種のみ、HEIC などは事前変換が必須
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?