◉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, 6 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, 6 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/API / SDK
◈ API / SDK/2026-05-03Intermediate

Auto-Categorizing 3,000 Wallpaper Images With Gemini Vision API — A Real Production Account

Manually categorizing thousands of wallpaper images doesn't scale. This is a hands-on account of building an auto-classification pipeline with Gemini Vision API — covering design, implementation, actual cost, and the failure patterns I hit running 3,000 images through it.

Gemini API249Vision APIImage Classification5Wallpaper AppBatch Processing6Indie Dev7

If you run a wallpaper app long enough, you hit the same wall: images accumulate faster than you can classify them.

My wallpaper app has over 3,000 images. Early on I categorized everything by hand. At a few hundred additions per month, that turned into several hours of classification work every week — just labeling, not building.

So I built an auto-classification pipeline using Gemini Vision API. The short result: 93% accuracy on custom categories, at roughly a few cents per hundred images. Here's the full account of how I built it, what broke, and what I'd do differently.

Why Manual Classification Breaks Down

Wallpaper categorization looks trivial from the outside. "This is nature scenery. This is minimalist. This is dark theme." Straightforward.

Three problems emerge in practice:

Inconsistency. My judgment varies depending on focus level. Several App Store reviews mentioned categories that felt mismatched — they were right. I wasn't consistent across 3,000 decisions.

Scale. 100 images by hand is fine. 1,000 takes a day. 3,000 breaks the release cycle.

Multilingual overhead. My app supports multiple languages. Every manual category decision also meant translating category names and descriptions. The cognitive overhead compounds.

Why Gemini Vision API

Other options exist — Google Cloud Vision API, AWS Rekognition. I chose Gemini for three reasons:

Custom category support. Standard vision APIs return generic labels: "mountain," "sky," "person." My app uses custom categories: "Lo-fi chill," "Gradient," "Cyberpunk neon." Gemini accepts category lists in natural language, so I can define the exact taxonomy my app uses.

Simultaneous description generation. The same API call that classifies an image can also generate a short description — useful for App Store metadata and language-specific tags.

Predictable cost. With Gemini Flash, processing 1,000 images runs to a few cents. For an indie app, that's a manageable and foreseeable cost.

Implementation: The Classification Pipeline

The flow is straightforward:

List image files → Send image + category list to Gemini
→ Parse response → Save to DB → Low-confidence → manual review queue
import google.generativeai as genai
import json
from pathlib import Path
 
genai.configure(api_key="YOUR_GEMINI_API_KEY")
model = genai.GenerativeModel("gemini-2.0-flash")
 
CATEGORIES = [
    "Nature Scenery", "Minimalist", "Dark Theme", "Gradient",
    "Abstract Art", "Cityscape", "Sunset", "Ocean",
    "Forest", "Space", "Flowers & Plants", "Animals",
    "Texture", "Pattern", "Typography", "Architecture",
    "Food", "Sports", "Technology", "Gaming",
    "Anime & Illustration", "Watercolor & Art", "Monochrome", "Vintage",
    "Neon & Cyberpunk", "Japanese Style", "Nordic Style", "Lo-fi",
    "Seasonal", "Other"
]
 
def classify_wallpaper(image_path: str) -> dict:
    image_file = genai.upload_file(image_path)
 
    prompt = f"""Analyze this wallpaper image and select the most appropriate category.
 
Available categories:
{json.dumps(CATEGORIES)}
 
Respond in this exact JSON format:
{{
  "primary_category": "category name from the list",
  "secondary_category": "optional secondary category",
  "confidence": 0.0 to 1.0,
  "description_en": "atmosphere description in 30 words or fewer",
  "tags": ["tag1", "tag2", "tag3"]
}}
 
Return JSON only."""
 
    response = model.generate_content([image_file, prompt])
 
    try:
        return json.loads(response.text.strip())
    except json.JSONDecodeError:
        return {"primary_category": "Other", "confidence": 0.0, "error": response.text}

Batch Processing 3,000 Images Without Hitting Rate Limits

Processing one image at a time runs into rate limits quickly. Add a brief delay between calls and log progress to recover gracefully from interruptions.

import time
from pathlib import Path
 
def classify_batch(image_dir: str, output_file: str):
    images = list(Path(image_dir).glob("*.jpg")) + list(Path(image_dir).glob("*.png"))
    results = []
    manual_review_queue = []
 
    print(f"Processing {len(images)} images")
 
    for i, img_path in enumerate(images):
        result = classify_wallpaper(str(img_path))
        result["filename"] = img_path.name
        results.append(result)
 
        if result.get("confidence", 0) < 0.7:
            manual_review_queue.append(img_path.name)
 
        if (i + 1) % 50 == 0:
            print(f"Progress: {i+1}/{len(images)} | Manual review queue: {len(manual_review_queue)}")
 
        time.sleep(0.5)  # Rate limit buffer
 
    with open(output_file, "w", encoding="utf-8") as f:
        json.dump({
            "total": len(images),
            "manual_review_count": len(manual_review_queue),
            "manual_review_queue": manual_review_queue,
            "results": results
        }, f, indent=2)
 
    print(f"Done. Manual review needed: {len(manual_review_queue)} images")

What Actually Happened When I Ran This

Accuracy. 93% of images were classified at confidence ≥ 0.7 and routed to automatic assignment. The remaining 7% (~210 images) landed in the manual queue. Almost all were images that genuinely span two categories — "nature scenery" that's also "minimalist," for example. That ambiguity isn't a model failure; it's a real classification problem that humans find equally difficult.

Cost. Processing 3,000 images with Gemini 2.0 Flash, including image input tokens, cost approximately ¥450 (roughly $3 USD). Compare that to the time cost of manual classification and it's not a close comparison.

Failure patterns I hit:

  • Near-black or near-white images: the model wavers between "Monochrome" and "Minimalist"
  • Complex art pieces: classification stability drops when an image has competing visual elements
  • Low-resolution thumbnails: detail is lost, accuracy falls

Prompt refinements improved these cases significantly. Adding "If uncertain between categories, prefer 'Other'" reduced the number of overconfident wrong answers.

What's Next: Closing the Feedback Loop

I'm currently experimenting with feeding user engagement data — save rate, favorites count, time spent viewing — back into the classification prompts. The hypothesis is that images users strongly prefer share classifiable visual characteristics, and Gemini can learn to weight those.

This is still early, but it's the direction: not just "classify what the image looks like" but "classify how users respond to it." The gap between those two things is where interesting product decisions live.

For an indie app, replacing 5–10 hours per month of manual classification with a $3 API call is the kind of leverage that makes the difference between sustainable and not. Small automations compound.

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 →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⟐ Dev Tools2026-09-02
An agreement-rate gate approves the model swap, then your category shares move
Swapping the model behind an image classification batch can pass a golden-set agreement gate while category shares quietly shift. I measured the sample sizes each check really needs and rebuilt the gate as a paired comparison.
◈ API / SDK2026-10-14
When Your config Raises "Extra inputs are not permitted", Check the Spelling of Every Key
If a google-genai call fails with a pydantic ValidationError (extra_forbidden) the moment you pass a config, it is almost always a one-letter typo. Here is the output I reproduced, a small function that checks keys before the call, and how to tell why response.text came back as None.
◈ API / SDK2026-10-08
When a Deprecation Has No Shutdown Date: Re-Picking an Image Model on Three Conditions
gemini-nano-banana-2.1 went GA on October 6, and gemini-3.1-flash-image was deprecated with no shutdown date. Here is a table and a short script for choosing a replacement by resolution, reference images, and search grounding.