Natural-language semantic image search β type "a dog running on grass" and instantly get the matching photos from a 3,000-image Flickr8k subset, ranked by how well they visually match your words.
No captions, no tags, no text metadata are used for retrieval β the system understands the images themselves. Images are embedded locally with the CLIP image encoder, the query with the CLIP text encoder, and retrieval is cosine similarity in their shared vector space: one numpy matmul over a cached index, so search is instant and works fully offline.
βΆ Watch the full walkthrough on YouTube β semantic search, the 2D embedding Atlas, and the agent pipeline in action.
- π Semantic search (ΠΠΎΠΈΡΠΊ) β describe a scene in natural language, get ranked photos with a match %, metadata, and an explanation of why each image matched.
- πΊοΈ Semantic Atlas (ΠΡΠ»Π°Ρ) β the entire 512-D CLIP index projected to 2D with PCA, clustered with KMeans, and each cluster auto-labeled with CLIP itself (zero-shot against candidate label prompts). Hover and click to explore how the model organizes the dataset.
- π€ Agent-style pipeline β the query is parsed into visual intent + metadata filters (location / date), filters are applied before ranking, results are explained, and a Markdown/JSON report is saved.
- β‘ Offline-first β CLIP runs locally (GPU if available); the whole retrieval path makes zero API calls. An
OPENAI_API_KEYis optional and only adds generated captions/tags and richer explanations. - β
Honest retrieval β Flickr8k's
captions.txtis never used for indexing, retrieval, or ranking. The point of the project is that the system understands images directly.
offline (once) per query (instant)
ββββββββββββββββββ ββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββ
β data/images/ ββββΊβ CLIP image encoder β β "people near water in Astana" β
β 3,000 photos β β (ViT-B/32, batched, β β β β
β + metadata.csv β β L2-normalized) β β βΌ β
ββββββββββββββββββ ββββββββββββ¬ββββββββββββ β parse β visual query + filters β
βΌ β β β
cache/image_embeddings.npy β βΌ β
cache/embeddings_meta.json β metadata filter (before ranking) β
β β β β
βΌ β βΌ β
ββββββββββββββββββββββββ β CLIP text encoder β cosine top-k β
β local vector index βββββ€ β β
ββββββββββββββββββββββββ β βΌ β
β explanations + Markdown/JSON report β
ββββββββββββββββββββββββββββββββββββββββ
- Model:
open_clipViT-B-32 (laion2b_s34b_b79k) β embeds 3,000 images in ~1β2 minutes on a 4 GB GPU. - Index: pure CLIP image embeddings. Captions/metadata never enter the index vectors.
- Vision enrichment (optional): retrieved images can be captioned/tagged lazily by a vision model β used only for explanations and reports, never for retrieval.
git clone https://github.com/rassulz/image_extractor_agent.git
cd image_extractor_agent
conda activate practical-ai-engineering # or your own env: conda create -n image-extractor python=3.11
pip install -r requirements.txt # torch, open-clip, fastapi, β¦
# 1. Dataset β either unzip the bundled subset:
unzip images.zip -d data/ # -> data/images/ (3,000 images, ~65 MB)
# β¦or rebuild it deterministically from Kaggle:
# python scripts/prepare_flickr8k_subset.py --count 3000 --remove-raw
# 2. Frontend deps
cd frontend && npm install && cd ..
# 3. One command: build the CLIP index (once) + start backend + frontend
./run_demo.sh # CONDA_ENV=<name> ./run_demo.sh to override the env
# β open http://localhost:5173OPENAI_API_KEY in .env is optional (see .env.example): it only enables generated captions/tags and LLM explanations. The backend does pure-CLIP search by default, so a demo costs zero API tokens.
python scripts/build_index.py # build/refresh the CLIP index
uvicorn backend.main:app --port 8000 # backend β http://localhost:8000
cd frontend && npm run dev # frontend β http://localhost:5173The course-required notebook demo is image_extractor_agent_day5.ipynb β the same pipeline end-to-end (dataset β embeddings β search β filters β agent β report), regenerable with python scripts/make_notebook.py.
| Endpoint | Method | What it does |
|---|---|---|
/api/search |
POST | Natural-language query β parsed intent, filters, top-k ranked images with scores and explanations |
/api/atlas?k=6 |
GET | 2D PCA projection of the whole index + KMeans clusters auto-labeled with CLIP |
/api/samples?n=8 |
GET | Random sample images for the landing gallery |
/api/health |
GET | Index status, model name, image count |
/images/β¦ |
GET | Static dataset images |
image_extractor_agent/
βββ image_extractor_agent_day5.ipynb # notebook demo (required deliverable)
βββ run_demo.sh # build index + start backend + frontend
βββ requirements.txt / .env.example
βββ scripts/ prepare_flickr8k_subset.py Β· build_index.py Β· make_notebook.py
βββ src/ dataset_loader Β· embedding_index Β· search_tools Β· vision_extractor Β· report_writer Β· agent
βββ backend/ main.py # FastAPI
βββ frontend/ src/App.jsx Β· Atlas.jsx # React + Vite + Tailwind
βββ report/ Image_Extractor_Agent_Report.pdf
βββ data/ images/ Β· metadata.csv # local only (gitignored)
βββ cache/ image_embeddings.npy Β· embeddings_meta.json Β· generated_image_descriptions.json
βββ outputs/ search_results.json Β· search_report.md
- CLIP cosine scores are low in absolute terms (~0.2β0.32 for good cross-modal matches). They are meaningful for ranking, not thresholding β the UI maps them to a friendly match %.
- Synthetic metadata: Flickr8k has no real capture metadata, so
locationisunknown(none is invented) anddateis generated from a seeded RNG β purely to demonstrate filter-before-ranking metadata filtering. - Stale-cache protection: the index is validated against
metadata.csv(ids + order + model name) on load; a mismatched cache is rejected and rebuilt instead of silently returning wrong images. - Deterministic dataset: the 3,000-image subset is selected with sorted filenames + a fixed seed, so anyone can reproduce the exact same index.
