File size: 7,573 Bytes
481d782
 
 
 
 
 
 
 
9116554
c81c57f
481d782
a9110af
481d782
 
 
 
 
 
 
9116554
a9110af
481d782
a9110af
481d782
 
 
9116554
1c20170
481d782
cbd59bf
481d782
 
9116554
481d782
 
 
 
 
 
 
 
 
9116554
481d782
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9116554
481d782
 
 
9116554
 
 
 
 
 
 
481d782
 
 
 
9116554
481d782
 
 
9116554
481d782
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9116554
481d782
137f859
9116554
 
e17b146
 
 
 
 
 
 
 
 
9116554
e17b146
 
 
 
9116554
e17b146
 
 
9116554
cda7f8d
c81c57f
481d782
 
 
9116554
481d782
137f859
481d782
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9116554
481d782
23a9730
9754663
9116554
 
 
0c38478
23a9730
 
 
 
 
9754663
9a785ac
 
9754663
 
 
 
9116554
9754663
 
 
 
e608753
 
 
 
 
 
 
cbd59bf
 
 
 
 
 
 
 
c2fb0dd
aa9769c
c2fb0dd
aa9769c
 
 
 
c2fb0dd
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
import sys
import platform
import os
import torch
import pickle
import numpy as np
from PIL import Image
from tqdm import tqdm
import clip  # ✅ Required for custom model queries using OpenAI's CLIP
from pathlib import Path

# SQLite patch for Streamlit deployment (non-Windows)
if platform.system() != "Windows":
    try:
        import pysqlite3
        sys.modules["sqlite3"] = pysqlite3
    except ImportError:
        raise ImportError("pysqlite3-binary is not installed. Add it to requirements.txt for deployment.")

# ✅ HuggingFace CLIP model for ChromaDB
from transformers import CLIPModel, CLIPProcessor

# ✅ ChromaDB and embedding functions
import chromadb
from chromadb.utils.embedding_functions import EmbeddingFunction

# Optional dataset downloader
from download import download_flickr8k_dataset

TOP_K = 5 

# -------------------------
# HuggingFace CLIP Wrapper
# -------------------------
class CLIPEmbeddingFunction(EmbeddingFunction):
    def __init__(self):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(self.device).eval()
        self.processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

    def embed_image(self, image):
        if isinstance(image, str):
            image = Image.open(image).convert("RGB")
        inputs = self.processor(images=image, return_tensors="pt").to(self.device)
        with torch.no_grad():
            image_features = self.model.get_image_features(**inputs)
        return image_features.cpu().numpy()[0].tolist()

    def embed_text(self, text):
        inputs = self.processor(text=text, return_tensors="pt", padding=True, truncation=True).to(self.device)
        with torch.no_grad():
            text_features = self.model.get_text_features(**inputs)
        return text_features.cpu().numpy()[0].tolist()

    def embed_combined(self, text, image):
        text_emb = torch.tensor(self.embed_text(text))
        image_emb = torch.tensor(self.embed_image(image))
        avg_emb = (text_emb + image_emb) / 2
        return avg_emb.numpy().tolist()


# -------------------------
# CLIP Setup
# -------------------------
clip_embedder = CLIPEmbeddingFunction()

device = "cuda" if torch.cuda.is_available() else "cpu"
clip_model, clip_preprocess = clip.load("ViT-B/32", device=device)


# -------------------------
# Load Custom Model Files
# -------------------------
with open("nn_model.pkl", "rb") as f:
    custom_nn_model = pickle.load(f)
with open("image_paths.pkl", "rb") as f:
    custom_image_paths = pickle.load(f)
custom_image_embeddings = np.load("image_embeddings.npy")


# -------------------------
# ChromaDB Training
# -------------------------
def train_rag_on_images(image_folder):
    if not os.path.exists(image_folder):
        print(f"{image_folder} not found. Downloading dataset...")
        download_flickr8k_dataset()

    client = chromadb.PersistentClient(path="./chroma_db")
    collection = client.get_or_create_collection(name="image_search")

    image_paths = []
    for root, _, files in os.walk(image_folder):
        for file in files:
            if file.lower().endswith((".jpg", ".jpeg", ".png")):
                image_paths.append(os.path.join(root, file))

    image_paths = image_paths[:4000]
    print(f"Embedding {len(image_paths)} images...")

    for idx, path in enumerate(tqdm(image_paths)):
        embedding = clip_embedder.embed_image(path)
        collection.add(
            documents=[path],
            embeddings=[embedding],
            ids=[f"img_{idx}"]
        )

    print("Training complete. Embeddings stored in ChromaDB.")


# -------------------------
# Unified Custom Query
# -------------------------
def query_custom_model(prompt=None, image=None, k=4):
    if prompt is None and image is None:
        raise ValueError("You must provide at least a prompt or an image.")

    query_embed = None

    if prompt:
        tokenized = clip.tokenize(prompt).to(device)
        with torch.no_grad():
            text_embed = clip_model.encode_text(tokenized).cpu().numpy()
        query_embed = text_embed

    if image:
        tensor = clip_preprocess(image).unsqueeze(0).to(device)
        with torch.no_grad():
            image_embed = clip_model.encode_image(tensor).cpu().numpy()
        if query_embed is not None:
            query_embed = (query_embed + image_embed) / 2.0
        else:
            query_embed = image_embed

    dists, indices = custom_nn_model.kneighbors(query_embed, n_neighbors=k)
    # return [custom_image_paths[i] for i in indices[0]]
    return [Path(custom_image_paths[i]).as_posix() for i in indices[0]]


# -------------------------
# ChromaDB Query
# -------------------------
def query_similar_images(prompt=None, image=None, k=4, show_custom=True):
    client = chromadb.PersistentClient(path="./chroma_db")
    collection = client.get_collection("image_search")

    if prompt and image:
        query_vector = clip_embedder.embed_combined(prompt, image)
    elif image:
        query_vector = clip_embedder.embed_image(image)
    elif prompt:
        query_vector = clip_embedder.embed_text(prompt)
    else:
        raise ValueError("Please provide at least a prompt or an image.")

    results = collection.query(query_embeddings=[query_vector], n_results=k)
    clip_image_paths = results['documents'][0]

    if any(not os.path.exists(path) for path in clip_image_paths):
        print("Some image files are missing. Re-downloading dataset...")
        download_flickr8k_dataset()

    custom_results = query_custom_model(prompt, image, k=k) if show_custom else []
    return clip_image_paths, custom_results


# -------------------------
# Optional Fallback Loader
# -------------------------
def load_custom_model(model_dir=".", base_image_dir="flickr8k/Flicker8k_Dataset"):
    with open(f"{model_dir}/nn_model.pkl", "rb") as f:
        nn_model = pickle.load(f)
    with open(f"{model_dir}/image_paths.pkl", "rb") as f:
        image_paths = pickle.load(f)
    image_embeddings = np.load(f"{model_dir}/image_embeddings.npy")

    # 🔧 Normalize paths for Linux environments
    image_paths = [p.replace("\\", "/") for p in image_paths]
    fixed_paths = []
    for path in image_paths:
        if not os.path.exists(path):
            filename = os.path.basename(path)
            fixed_paths.append(os.path.join(base_image_dir, filename))
        else:
            fixed_paths.append(path)

    return nn_model, image_embeddings, fixed_paths

# Inside image_search.py

def update_custom_image_paths(folder_path):
    global custom_image_paths
    custom_image_paths = [os.path.join(folder_path, name) for name in os.listdir(folder_path) if name.lower().endswith((".jpg", ".jpeg", ".png"))]

def query_by_image(image_path, nn_model, image_paths):
    image = Image.open(image_path).convert("RGB")
    tensor = clip_preprocess(image).unsqueeze(0).to(device)
    with torch.no_grad():
        image_features = clip_model.encode_image(tensor).cpu().numpy()
    dists, indices = nn_model.kneighbors(image_features, n_neighbors=TOP_K)
    return [image_paths[i] for i in indices[0]]

def query_by_text(prompt, nn_model, image_paths):
    # Tokenize and encode the text
    with torch.no_grad():
        text_tokens = clip.tokenize([prompt]).to(device)
        text_features = clip_model.encode_text(text_tokens).cpu().numpy()

    # Get top matches
    dists, indices = nn_model.kneighbors(text_features, n_neighbors=TOP_K)
    return [image_paths[i] for i in indices[0]]