Visual_Product_Matcher / build_index.py
TarunSarojSingh's picture
Rename app.py to build_index.py
6b92d5c verified
Raw History Blame Contribute Delete
5.1 kB
import os
import io
import json
import faiss
import torch
import pandas as pd
import numpy as np
from PIL import Image
import requests
import gradio as gr
from sentence_transformers import SentenceTransformer
DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
CSV_PATH = os.path.join(DATA_DIR, "products.csv")
EMB_PATH = os.path.join(DATA_DIR, "embeddings.npy")
INDEX_PATH = os.path.join(DATA_DIR, "index.faiss")
MAP_PATH = os.path.join(DATA_DIR, "row_mapping.json")
MODEL_NAME = "clip-ViT-B-32"
# Lazy globals
_df = None
_index = None
_row_map = None
_model = None
def load_resources():
global _df, _index, _row_map, _model
if _df is None:
_df = pd.read_csv(CSV_PATH)
if _index is None:
if not os.path.exists(INDEX_PATH) or not os.path.exists(EMB_PATH) or not os.path.exists(MAP_PATH):
raise gr.Error("Index not found. Please run build_index.py first (or deploy to Spaces and let it run once).")
_index = faiss.read_index(INDEX_PATH)
with open(MAP_PATH, "r", encoding="utf-8") as f:
_row_map = json.load(f)
if _model is None:
_model = SentenceTransformer(MODEL_NAME)
def fetch_image_from_url(url: str) -> Image.Image:
r = requests.get(url, timeout=15)
r.raise_for_status()
return Image.open(io.BytesIO(r.content)).convert("RGB")
def embed_image(pil_img: Image.Image) -> np.ndarray:
with torch.no_grad():
emb = _model.encode([pil_img], convert_to_tensor=True)
emb = torch.nn.functional.normalize(emb, p=2, dim=1)
return emb.cpu().numpy().astype(np.float32)
def search(query_img, top_k=12, min_score=0.2, categories=None):
try:
load_resources()
except Exception as e:
raise gr.Error(str(e))
if query_img is None:
raise gr.Error("Please upload an image or provide an image URL.")
q_emb = embed_image(query_img) # shape (1, d)
scores, idxs = _index.search(q_emb, top_k + 50) # oversample, we will filter by category/score
scores = scores.flatten().tolist()
idxs = idxs.flatten().tolist()
# Build results list
rows = []
for score, local_idx in zip(scores, idxs):
row_id = _row_map[local_idx]
row = _df.iloc[row_id]
rows.append({
"id": int(row["id"]),
"name": row["name"],
"category": row["category"],
"price": float(row["price"]),
"image_url": row["image_url"],
"similarity": float(score),
})
# Filter by category (if any) and score
if categories:
rows = [r for r in rows if r["category"] in categories]
rows = [r for r in rows if r["similarity"] >= min_score]
# Take top_k after filtering
rows = rows[:top_k]
# Prepare gallery & table
gallery = [[r["image_url"], f'{r["name"]} | {r["category"]} | ₹{r["price"]} | sim={r["similarity"]:.3f}'] for r in rows]
table = pd.DataFrame(rows, columns=["id","name","category","price","similarity","image_url"])
return gallery, table
def infer(upload_image, image_url, top_k, min_score, categories):
# Prefer uploaded image; if absent, try URL
img = None
if upload_image is not None:
img = upload_image
elif image_url and image_url.strip():
try:
img = fetch_image_from_url(image_url.strip())
except Exception as e:
raise gr.Error(f"Could not fetch image from URL: {e}")
else:
raise gr.Error("Please upload an image or enter a valid image URL.")
return search(img, top_k=top_k, min_score=min_score, categories=categories)
with gr.Blocks(css="footer {visibility: hidden}") as demo:
gr.Markdown("# 🔎 Visual Product Matcher\nUpload a product photo (or paste an image URL) to find visually similar items.")
with gr.Row():
with gr.Column():
upload = gr.Image(type="pil", label="Upload Image")
url = gr.Textbox(label="...or Image URL")
topk = gr.Slider(1, 30, value=12, step=1, label="Results to show")
mins = gr.Slider(0.0, 1.0, value=0.2, step=0.01, label="Minimum similarity")
cats = gr.CheckboxGroup(choices=sorted(list(set(pd.read_csv(CSV_PATH)["category"]))), label="Filter by category (optional)")
btn = gr.Button("Search", variant="primary")
with gr.Column():
gr.Markdown("### Query preview")
# The query preview is just the uploaded image; URL-only flow won't preview until fetched internally.
preview = gr.Image(label="Uploaded Image", interactive=False)
gallery = gr.Gallery(label="Similar Products").style(grid=[3], height="auto")
table = gr.Dataframe(headers=["id","name","category","price","similarity","image_url"],
label="Results (details)", wrap=True)
upload.change(lambda i: i, inputs=upload, outputs=preview)
btn.click(infer, inputs=[upload, url, topk, mins, cats], outputs=[gallery, table])
if __name__ == "__main__":
# For local run: `python build_index.py` first, then `python app.py`
demo.launch()