◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 8 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 8 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/Advanced
⬡ Advanced/2026-09-17Intermediate

The color label Gemini returned for the same wallpaper changed — and the model was not the reason

I counted color-label agreement across five ways of exporting the same wallpaper. Stripping the ICC profile was the worst of them. Here is how I pin the color space down before asking, and how I check the answers against a number I compute myself.

Gemini91Image understandingColor spaceWallpaper appPython51

✦ Premium Article

I was classifying the new batch of ukiyo-e wallpapers late one evening when an image that had come back as blue the week before came back as gray. The prompt had not changed. The model was the same. The one thing I had changed was the tool I used to export the files.

It took me two nights to find the cause. It was not the model having an off day — it was the image I was handing over. More precisely, I had been discarding the one hint that tells a reader which color space those pixel values belong to.

So I lined up five ways of exporting the same picture and counted. The material is 120 new images from the wallpaper app I run as an indie developer.

Only the color field was moving

The first thing I did was narrow the drift. My classifier returns four fields: subject, composition, dominant color, and usage tags. I re-read them side by side to see which one was actually changing.

Subject and composition came back identical on a second pass. So did the usage tags. The only field that moved was the dominant color.

That settled the direction. Instead of rewriting the whole prompt, I could measure one field. Narrowing the scope is why 120 images across five export variants finished in a single night.

Output rarely drifts as a whole. Count the fields separately and the thing you need to fix tends to collapse into one place.

Five export variants, defined before any measuring

The conditions

The originals are 4K images in Display P3. I produced five variants from each, with the long edge normalized to 1,024 pixels so that resolution would not get mixed into the result.

  1. Converted to sRGB, saved as JPEG at quality 95 — this is my baseline
  2. Left in Display P3, ICC profile embedded, JPEG at quality 95
  3. Left in Display P3, ICC profile stripped, JPEG at quality 95
  4. Converted to sRGB, saved as PNG
  5. Converted to sRGB, saved as WebP at quality 80

Variant 3 is what I had actually been shipping. A profile is tens of kilobytes, and when you are trimming delivery size, it looks like free weight to drop.

The normalization itself is a few lines with Pillow's ImageCms.

# normalize_srgb.py — decide the color space yourself, before handing it over
from io import BytesIO
from PIL import Image, ImageCms
 
SRGB = ImageCms.createProfile("sRGB")
 
def to_srgb_jpeg(path: str, long_edge: int = 1024, quality: int = 95) -> bytes:
    img = Image.open(path)
    raw = img.info.get("icc_profile")
    if raw:
        # Only convert when a profile exists; converting an untagged image applies it twice
        src = ImageCms.ImageCmsProfile(BytesIO(raw))
        img = ImageCms.profileToProfile(img, src, SRGB, outputMode="RGB")
    else:
        # With no hint, sRGB is the only reasonable assumption. Log this branch
        img = img.convert("RGB")
    img.thumbnail((long_edge, long_edge), Image.LANCZOS)
    buf = BytesIO()
    # After conversion the reading is unambiguous whether or not a profile is attached
    img.save(buf, format="JPEG", quality=quality, optimize=True)
    return buf.getvalue()

profileToProfile rewrites the pixel values. Embedding a profile does not. Treating those two operations as the same thing is where my misreading started.

How the requests were sent

One image per request, same prompt, temperature pinned to 0, and the color restricted to a closed vocabulary of eight words. With free-form answers, wording differences creep in, and you can no longer tell whether the color changed or only the word for it did.

What counts as agreement

If a variant returned the same word as the baseline, I counted it as agreement. I also ran the baseline twice to measure its self-agreement. If the baseline itself drifts, there is nothing to compare against.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You will be able to check, with 20 images from your own library, whether your export settings are changing what the model calls the color
✦You will be able to avoid the mistake that cost me two nights: the size optimization that strips metadata is the one that damages the judgment most
✦You will be able to put a closed color vocabulary and a self-computed reference value into your own pipeline as a cross-check
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Advanced2026-09-24
I Stopped Asking Gemini 'Is This Right?' and Started Hiding My Answer Instead
Ask Gemini to confirm a label you already chose and it tends to agree. Here is why I compared three ways of asking, chose blind re-classification, how my answer still leaked in, and the routing code that sends only disagreements to a human.
⬡ Advanced2026-10-14
Fine on Desktop, Blank on Safari: Four Things to Gather Before You Ask an AI to Diagnose It
After a shuffle, my lazy-loaded images never loaded again, but only in Safari on iPhone. Here is what to collect by hand before asking Gemini or any other model to diagnose a bug that does not reproduce on desktop, plus a request template you can paste.
⬡ Advanced2026-10-10
Keeping Money and Health Questions Out of Your History: Gemini Temporary Chat and the Settings to Check
Before you ask Gemini about household spending or a health worry, here is the order I use to check Temporary Chat and the Activity setting, so you can decide what gets kept and what doesn't.