2014年からアーティストとして活動する傍ら、個人開発でいくつかの壁紙アプリを運営しています。今は AdMob で収益化した壁紙系・癒し系のアプリを中心に動かしているのですが、最近 2.5 Flash Image(通称 Nano Banana)を使って「既存の壁紙からカラーバリエーションを派生させる」パイプラインを組みました。ところが、参照画像を渡しているはずなのに、出てくるのは全く別の絵。「明るくしただけのバージョンが欲しい」と指示しても、構図ごと変わったコンセプトアートが返ってきて呆然とする、という症状にぶつかりました。
似た悩みを持つ方は多いはずなので、原因の切り分け順序を共有します。私が壁紙生成のバッチで安定動作させるまでに踏んだ落とし穴を、優先順位の高い順から並べました。
まず再現する症状を整理する
実用上、参照画像が無視される症状は次の3パターンに分かれます。原因が違うので、まず自分のケースがどれかを確認してください。
- A: 全く別物の画像が返る(参照画像の被写体・構図・色が全く残らない)
- B: テキストだけ返る(「申し訳ありませんが画像を生成できません」など)
- C: 参照画像はうっすら活きているが、編集指示(「色だけ変える」など)が無視される
A と B は「リクエストの組み立て」に問題があるケースがほとんどです。C は「プロンプト設計」と「parts の順序」が原因の中心になります。
原因1: response_modalities に IMAGE を含めていない
Nano Banana で最初に確認すべき設定です。response_modalities を省略すると、SDK のデフォルトが TEXT になり、モデルが「画像を編集した上でそれを言葉で説明する」モードに入ってしまうことがあります。これが症状 B の主因です。
from google import genai
from google.genai import types
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
with open("base_wallpaper.png", "rb") as f:
base_image_bytes = f.read()
response = client.models.generate_content(
model="gemini-2.5-flash-image",
contents=[
types.Part.from_bytes(data=base_image_bytes, mime_type="image/png"),
"この画像をベースに、全体を夜の青系に統一してください。構図と被写体はそのまま。",
],
config=types.GenerateContentConfig(
response_modalities=["IMAGE"], # ← ここが抜けるとテキストだけ返る場合がある
),
)response_modalities は ["IMAGE"] 単独でも、["IMAGE", "TEXT"] 併記でも構いません。ただし ["TEXT"] だけにしてしまうと、画像編集をお願いしたつもりでも応答に画像 part が含まれません。
原因2: parts の順序とMIMEタイプ
Nano Banana は順序にやや敏感です。ドキュメント上は「順序は問わない」と読めますが、実運用では 画像 part を先に、テキスト指示を後に 置いた方が編集の安定度が上がります。逆順だと「テキストを主な指示と解釈し、画像はおまけの参考」として扱われがちで、症状 A・C を誘発します。
MIME タイプも忘れずに明示してください。image/jpeg を image/png と宣言するとデコードに失敗して、内部的にリクエストごと破棄されるケースがあります。実体に合わせること、これだけで「なぜか1回目だけ画像が無視される」現象が消えることが多いです。
import imghdr
mime_map = {"png": "image/png", "jpeg": "image/jpeg", "jpg": "image/jpeg", "webp": "image/webp"}
fmt = imghdr.what(None, h=base_image_bytes)
mime = mime_map.get(fmt or "", "image/png")原因3: 画像サイズが大きすぎる・小さすぎる
私が個人開発のパイプラインで一番ハマったのがこれです。元の壁紙が 4K(3840×2160)のままだと、リクエストはエラーなく通るのに、出力は「サムネイル品質の別物」が返ってきました。逆に 128×128 以下で渡すと、モデルが「画像と認識できない」状態になります。
実用的な落としどころは 長辺 1024〜1568px です。これより大きいと内部でリサイズされて細部が失われ、編集精度が落ちます。事前にこちらでリサイズしておくことで、出力の一貫性が大きく改善しました。
from PIL import Image
import io
def normalize_for_nano_banana(path: str, max_side: int = 1568) -> bytes:
img = Image.open(path).convert("RGB")
w, h = img.size
scale = min(max_side / max(w, h), 1.0)
if scale < 1.0:
img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS)
buf = io.BytesIO()
img.save(buf, format="PNG", optimize=True)
return buf.getvalue()ここで convert("RGB") を挟んでいるのは、PNG のアルファチャネルが残っているとモデルが「透過部分を新しい背景で塗り直そう」と判断して、結果的に構図を作り直してしまうことがあるためです。透過情報が不要なら落とした方が安定します。
原因4: プロンプトが「生成」と「編集」を混同している
Nano Banana は同じモデルが画像生成と画像編集を両方こなします。プロンプトが曖昧だと、内部で「これは新規生成のリクエストだ」と判断して、参照画像をスタイル参考程度に格下げしてしまいます。症状 A と C の本丸はここです。
下の表現は、私の壁紙パイプラインでヒット率が大きく違いました。
- ❌ 「夜のバージョンを作って」 → 生成寄りに振れて別物になる
- ❌ 「同じ感じで夜にして」 → 「感じ」が曖昧で構図が変わる
- ✅ 「添付した画像の 構図と被写体を保ったまま、色調だけ 夜の青系に変更してください。輪郭・配置・ポーズは変更しない」
ポイントは「何を保つか(構図・被写体・ポーズ)」と「何を変えるか(色調のみ)」を分けて明示することです。「変えない部分」を先に書くと、モデルは編集モードで動き続けてくれます。
原因5: chat session の2ターン目以降で参照画像が抜ける
複数ターンで画像を少しずつ編集していくと、2ターン目以降で「最初の画像を覚えていない」状態になることがあります。これは chats.create() を使った場合に、過去ターンの画像 part がトークン圧縮の対象になり、モデルが参照できなくなるためです。
私のパイプラインでは、編集系のターンでは 毎回最新の出力画像を再添付 する設計に変えました。チャットセッションを長く維持するより、ステートレスに「直前の生成結果」を新しいリクエストの入力にする方が、編集精度が安定します。
def edit_iteratively(client, prev_image_bytes: bytes, instruction: str) -> bytes:
response = client.models.generate_content(
model="gemini-2.5-flash-image",
contents=[
types.Part.from_bytes(data=prev_image_bytes, mime_type="image/png"),
instruction,
],
config=types.GenerateContentConfig(response_modalities=["IMAGE"]),
)
for part in response.candidates[0].content.parts:
if part.inline_data and part.inline_data.data:
return part.inline_data.data
raise RuntimeError("画像 part が応答に含まれませんでした。プロンプトと response_modalities を確認してください。")応答が画像になっているか必ず assert する
Nano Banana の応答は、画像 part が応答の parts 配列に混ざってくる構造です。response.text だけを取り出して保存しようとすると、None や説明テキストだけを保存してしまい、後段のパイプラインで気づかぬまま「中身が壊れた壁紙」が量産されることがあります。
実運用では、応答を取り出すヘルパーを必ず通すようにしておくと安心です。私の壁紙バッチでは、画像 part がゼロだった場合は即座にプロンプトを変えて再試行するロジックにしています。
def extract_image_or_raise(response) -> bytes:
parts = response.candidates[0].content.parts if response.candidates else []
for p in parts:
if getattr(p, "inline_data", None) and p.inline_data.data:
return p.inline_data.data
text = " | ".join(getattr(p, "text", "") or "" for p in parts)
raise RuntimeError(f"応答に画像が含まれていません。テキスト: {text[:200]}")ここでテキストを一緒にログに残しておくと、安全フィルターでブロックされたのか、プロンプトが生成寄りに振れたのかが即座にわかります。原因切り分けのスピードが何倍も上がります。
切り分けの順序まとめ
派生バリエーション生成を 5,000万DL 規模のアプリ群に組み込むときは、デバッグ時間が積もると致命的なので、私は次の順番で必ず確認するようにしています。
response_modalitiesにIMAGEが入っているか(症状 B の8割はここ)- parts の順序が「画像 → テキスト」になっているか
- MIME タイプが実体と一致しているか
- 長辺が 1024〜1568px に収まっているか
- プロンプトに「保つもの/変えるもの」が明示されているか
- chat session ではなく毎回入力を組み立て直しているか
順番に潰していけば、ほとんどの「参照画像が無視される」症状は1時間以内に解消できます。同じ落とし穴で時間を溶かしている方の助けになれば幸いです。