ProfRom commited on
Commit
f35a17f
Β·
verified Β·
1 Parent(s): 4d08a0f

Ejesi - Unit 8 Assignment - Final submission

Browse files
Files changed (2) hide show
  1. app.py +383 -92
  2. requirements.txt +12 -6
app.py CHANGED
@@ -1,92 +1,383 @@
1
- from datasets import load_dataset
2
- from PIL import Image
3
- from pathlib import Path
4
-
5
- import torch
6
- from transformers import pipeline
7
- import gradio as gr
8
-
9
- #1. Load dataset and prepare 10 sample images
10
- dataset = load_dataset("rokmr/pets", split="test")
11
- label_names = dataset.features["label"].names
12
-
13
- LOCAL_TEST_DIR = Path("test_images")
14
- LOCAL_TEST_DIR.mkdir(exist_ok=True)
15
-
16
- NUM_SAMPLES = 10
17
- sampled = dataset.select(range(NUM_SAMPLES))
18
-
19
- sample_paths = []
20
- for idx, example in enumerate(sampled):
21
- img: Image.Image = example["image"]
22
- label_id = example["label"]
23
- label_name = label_names[label_id]
24
-
25
- filename = f"pets_{idx:03d}_{label_name}.jpg"
26
- save_path = LOCAL_TEST_DIR / filename
27
- img.save(save_path)
28
- sample_paths.append((save_path, label_name))
29
-
30
- #2. Set up image classification models
31
- device = 0 if torch.cuda.is_available() else -1
32
-
33
- model1_id = "google/vit-base-patch16-224"
34
- model2_id = "microsoft/resnet-50"
35
-
36
- clf1 = pipeline("image-classification", model=model1_id, device=device)
37
- clf2 = pipeline("image-classification", model=model2_id, device=device)
38
-
39
- #3. Simple explanation logic
40
- def simple_explanation(true_label, top1_1, top1_2):
41
- label1, score1 = top1_1["label"], top1_1["score"]
42
- label2, score2 = top1_2["label"], top1_2["score"]
43
-
44
- label1_matches = true_label.lower() in label1.lower() if true_label != "unknown" else False
45
- label2_matches = true_label.lower() in label2.lower() if true_label != "unknown" else False
46
-
47
- if label1_matches and not label2_matches:
48
- winner = "Model A"
49
- elif label2_matches and not label1_matches:
50
- winner = "Model B"
51
- else:
52
- winner = "Model A" if score1 >= score2 else "Model B"
53
-
54
- explanation = (
55
- f"Model A predicted '{label1}' (score {score1:.2f}), "
56
- f"and Model B predicted '{label2}' (score {score2:.2f}). "
57
- f"Overall, {winner} appears more confident for this image."
58
- )
59
- return explanation
60
-
61
- #4. Gradio interface
62
- def classify_and_explain(image):
63
- image = image.convert("RGB")
64
-
65
- preds1 = clf1(image, top_k=3)
66
- preds2 = clf2(image, top_k=3)
67
-
68
- top1_1 = preds1[0]
69
- top1_2 = preds2[0]
70
-
71
- explanation = simple_explanation("unknown", top1_1, top1_2)
72
-
73
- return (
74
- f"{top1_1['label']} ({top1_1['score']:.2f})",
75
- f"{top1_2['label']} ({top1_2['score']:.2f})",
76
- explanation,
77
- )
78
-
79
- demo = gr.Interface(
80
- fn=classify_and_explain,
81
- inputs=gr.Image(type="pil"),
82
- outputs=[
83
- gr.Textbox(label="Model 1 (ViT) top prediction"),
84
- gr.Textbox(label="Model 2 (ResNet) top prediction"),
85
- gr.Textbox(label="Generated explanation"),
86
- ],
87
- title="Pets Classifier + Text Explanation",
88
- description="Upload a pet image and compare two vision models with a generated explanation.",
89
- )
90
-
91
- if __name__ == "__main__":
92
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==============================
2
+ # SECTION 1 β€” INSTALL + IMPORTS.
3
+ # ==============================
4
+
5
+ import torch
6
+ import gradio as gr
7
+ from PIL import Image
8
+ from transformers import pipeline, BlipProcessor, BlipForQuestionAnswering
9
+ import lpips
10
+ import clip
11
+ from bert_score import score
12
+ import torchvision.transforms as T
13
+ from sentence_transformers import SentenceTransformer
14
+ from rouge_score import rouge_scorer
15
+ import numpy as np
16
+ from sklearn.metrics.pairwise import cosine_similarity
17
+
18
+ device = "cuda" if torch.cuda.is_available() else "cpu"
19
+
20
+ def free_gpu_cache():
21
+ if torch.cuda.is_available():
22
+ torch.cuda.empty_cache()
23
+
24
+ # ==============================
25
+ # SECTION 2 β€” LOAD LIGHTWEIGHT MODELS
26
+ # ==============================
27
+ blip_large_captioner = pipeline(
28
+ "image-to-text",
29
+ model="Salesforce/blip-image-captioning-large",
30
+ device=0 if device=="cuda" else -1
31
+ )
32
+
33
+ vit_gpt2_captioner = pipeline(
34
+ "image-to-text",
35
+ model="nlpconnect/vit-gpt2-image-captioning",
36
+ device=0 if device=="cuda" else -1
37
+ )
38
+
39
+ # --- NLP Pipelines ---
40
+ sentiment_model = pipeline("sentiment-analysis")
41
+ ner_model = pipeline("ner", aggregation_strategy="simple")
42
+ topic_model = pipeline("zero-shot-classification",
43
+ model="facebook/bart-large-mnli")
44
+
45
+ # --- Metrics ---
46
+ clip_model, clip_preprocess = clip.load("ViT-B/32", device=device)
47
+ lpips_model = lpips.LPIPS(net='alex').to(device)
48
+ lpips_transform = T.Compose([T.ToTensor(), T.Resize((128,128))])
49
+ sentence_model = SentenceTransformer("all-MiniLM-L6-v2") # for cosine similarity
50
+
51
+ # ==============================
52
+ # SECTION 2b β€” LAZY LOAD HEAVY MODELS
53
+ # ==============================
54
+ blip2_captioner = None
55
+ vqa_processor = BlipProcessor.from_pretrained("Salesforce/blip-vqa-base")
56
+ vqa_model = None
57
+
58
+ def get_blip2():
59
+ global blip2_captioner
60
+ if blip2_captioner is None:
61
+ blip2_captioner = pipeline(
62
+ "image-to-text",
63
+ model="Salesforce/blip2-opt-2.7b",
64
+ device=0 if device=="cuda" else -1
65
+ )
66
+ return blip2_captioner
67
+
68
+ def get_vqa_model():
69
+ global vqa_model
70
+ if vqa_model is None:
71
+ vqa_model = BlipForQuestionAnswering.from_pretrained("Salesforce/blip-vqa-base").to(device)
72
+ return vqa_model
73
+
74
+ # ==============================
75
+ # SECTION 3 β€” FUNCTIONS
76
+ # ==============================
77
+ def make_captions(img):
78
+ captions = []
79
+ try: captions.append(blip_large_captioner(img)[0]["generated_text"])
80
+ except: captions.append("BLIP-large failed.")
81
+ try: captions.append(vit_gpt2_captioner(img)[0]["generated_text"])
82
+ except: captions.append("ViT-GPT2 failed.")
83
+ try:
84
+ blip2 = get_blip2()
85
+ captions.append(blip2(img)[0]["generated_text"])
86
+ except: captions.append("BLIP2-opt failed.")
87
+ return captions
88
+
89
+ # ---------------- Metrics Computation ---------------------
90
+ def compute_metrics_button(images, captions, idx1, idx2):
91
+ # CLIP similarity
92
+ img1_clip = clip_preprocess(images[idx1]).unsqueeze(0).to(device)
93
+ img2_clip = clip_preprocess(images[idx2]).unsqueeze(0).to(device)
94
+ with torch.no_grad():
95
+ feat1 = clip_model.encode_image(img1_clip)
96
+ feat2 = clip_model.encode_image(img2_clip)
97
+ clip_sim = float(torch.cosine_similarity(feat1, feat2).item())
98
+
99
+ # LPIPS
100
+ img1_lp = lpips_transform(images[idx1]).unsqueeze(0).to(device) * 2 - 1
101
+ img2_lp = lpips_transform(images[idx2]).unsqueeze(0).to(device) * 2 - 1
102
+ with torch.no_grad():
103
+ lpips_score = float(lpips_model(img1_lp, img2_lp).item())
104
+
105
+ # BERTScore
106
+ _, _, F1 = score([captions[idx1]], [captions[idx2]], lang="en", verbose=False)
107
+ bert_f1 = float(F1.mean().item())
108
+
109
+ # Cosine similarity of embeddings
110
+ emb1 = sentence_model.encode([captions[idx1]])
111
+ emb2 = sentence_model.encode([captions[idx2]])
112
+ cosine_sim = float(cosine_similarity(emb1, emb2)[0][0])
113
+
114
+ # Jaccard similarity
115
+ tokens1 = set(captions[idx1].lower().split())
116
+ tokens2 = set(captions[idx2].lower().split())
117
+ jaccard_sim = float(len(tokens1 & tokens2) / len(tokens1 | tokens2))
118
+
119
+ # ROUGE
120
+ scorer = rouge_scorer.RougeScorer(['rouge1','rougeL'], use_stemmer=True)
121
+ rouge_scores = scorer.score(captions[idx1], captions[idx2])
122
+
123
+ return f"""
124
+ - CLIP: {clip_sim:.4f}
125
+ - LPIPS: {lpips_score:.4f}
126
+ - BERT-F1: {bert_f1:.4f}
127
+ - Cosine: {cosine_sim:.4f}
128
+ - Jaccard: {jaccard_sim:.4f}
129
+ - ROUGE-1: {rouge_scores['rouge1'].fmeasure:.4f}
130
+ - ROUGE-L: {rouge_scores['rougeL'].fmeasure:.4f}
131
+ """
132
+
133
+ # ---- NLP ----
134
+ def nlp_bundle(caption):
135
+ try:
136
+ sentiment = sentiment_model(caption)
137
+ sentiment = "<br>".join([f"{s['label']}: {s['score']:.2f}" for s in sentiment])
138
+ except: sentiment = "Sentiment failed."
139
+
140
+ try:
141
+ ents_list = ner_model(caption)
142
+ ents = "<br>".join([f"{e['entity_group']}: {e['word']}" for e in ents_list]) or "None"
143
+ except: ents = "NER failed."
144
+
145
+ try:
146
+ topics_raw = topic_model(caption, candidate_labels=["people","animals","objects","food","nature"])
147
+ topics = "<br>".join([f"{lbl}: {float(scr):.2f}" for lbl, scr in zip(topics_raw["labels"], topics_raw["scores"])])
148
+ except: topics = "Topics failed."
149
+
150
+ return sentiment, ents, topics
151
+
152
+ # ---------------- VQA ----------------
153
+ def answer_vqa(question, image):
154
+ if image is None or question.strip() == "":
155
+ return "Upload an image and enter a question."
156
+ model = get_vqa_model()
157
+ inputs = vqa_processor(images=image, text=question, return_tensors="pt").to(device)
158
+ with torch.no_grad():
159
+ generated_ids = model.generate(**inputs)
160
+ answer = vqa_processor.decode(generated_ids[0], skip_special_tokens=True)
161
+ free_gpu_cache()
162
+ return answer
163
+
164
+ # Convert a PIL.Image to PNG byte stream
165
+ def to_bytes(img):
166
+ import io
167
+ buf = io.BytesIO()
168
+ img.save(buf, format="PNG")
169
+ return buf.getvalue()
170
+
171
+ # ==============================
172
+ # SECTION 4 β€” UI (GRADIO)
173
+ # ==============================
174
+ def build_ui():
175
+ with gr.Blocks(title="Multimodal AI Image Studio") as demo:
176
+
177
+ gr.HTML("""
178
+ <style>
179
+ .heading-orange h2, .heading-orange h3 { color: #ff5500 !important; }
180
+ .orange-btn button { background-color:#ff5500; color:white; border-radius:6px; height:36px; font-weight:bold; }
181
+ .teal-btn button { background-color:#008080; color:white; border-radius:6px; height:36px; font-weight:bold; }
182
+ .loading-line {
183
+ height:4px; background:linear-gradient(90deg,#008080 0%,#00cccc 50%,#008080 100%);
184
+ background-size:200% 100%; animation: loading 1s linear infinite;
185
+ }
186
+ @keyframes loading { 0% {background-position:200% 0;} 100% {background-position:-200% 0;} }
187
+ .circular-img img {
188
+ border-radius: 21%;
189
+ object-fit: cover;
190
+ width: 400px;
191
+ height: 200px;
192
+ box-shadow: inset -10px -10px 30px rgba(255,255,255,0.3),
193
+ 5px 5px 15px rgba(0,0,0,0.3);
194
+ border: 2px solid rgba(255,255,255,0.6);
195
+ }
196
+ .metrics-row {
197
+ display: flex;
198
+ flex-direction: row;
199
+ gap: 20px;
200
+ }
201
+ .metrics-row > div {
202
+ flex: 1;
203
+ }
204
+ </style>
205
+ """)
206
+
207
+ gr.Markdown("## Multimodal AI Image Studio: Comparative Image-to-Text Analysis", elem_classes="heading-orange")
208
+ images_state = gr.State([])
209
+ captions_state = gr.State([])
210
+
211
+ # ---------------- Image Input ----------------
212
+ gr.Markdown("### Select Image Source", elem_classes="heading-orange")
213
+ with gr.Tabs():
214
+ with gr.Tab("πŸ“ Upload Image"):
215
+ upload_input = gr.Image(type="pil", sources=["upload"], label="Upload Image", height=600, elem_classes="circular-img")
216
+ upload_btn = gr.Button("Generate Captions", elem_classes="orange-btn")
217
+ with gr.Tab("πŸ“· Webcam"):
218
+ webcam_input = gr.Image(type="pil", sources=["webcam"], label="Webcam", height=600, elem_classes="circular-img")
219
+ webcam_btn = gr.Button("Capture & Generate Captions", elem_classes="orange-btn")
220
+ with gr.Tab("πŸ”— From URL"):
221
+ url_input = gr.Textbox(label="Paste Image URL")
222
+ url_btn = gr.Button("Fetch & Generate Captions", elem_classes="orange-btn")
223
+
224
+ # ---------------- Previews ----------------
225
+ with gr.Row():
226
+ with gr.Column(scale=1, min_width=200):
227
+ preview1 = gr.Image(type="pil",label="Preview 1", interactive=False, height=230)
228
+ blip_caption_box = gr.Markdown()
229
+ with gr.Column(scale=1, min_width=200):
230
+ preview2 = gr.Image(type="pil",label="Preview 2", interactive=False, height=230)
231
+ vit_caption_box = gr.Markdown()
232
+ with gr.Column(scale=1, min_width=200):
233
+ preview3 = gr.Image(type="pil",label="Preview 3", interactive=False, height=230)
234
+ blip2_caption_box = gr.Markdown()
235
+
236
+ # ---------------- Generate Captions ----------------
237
+ def generate_all(img, images_state, captions_state):
238
+ if img is None:
239
+ return (None, None, None, "No image.", "No image.", "No image.", [], [])
240
+ captions = make_captions(img)
241
+ return (img, img, img, captions[0], captions[1], captions[2], [img], captions)
242
+
243
+ upload_btn.click(generate_all, inputs=[upload_input, images_state, captions_state],
244
+ outputs=[preview1, preview2, preview3, blip_caption_box, vit_caption_box, blip2_caption_box, images_state, captions_state])
245
+ webcam_btn.click(generate_all, inputs=[webcam_input, images_state, captions_state],
246
+ outputs=[preview1, preview2, preview3, blip_caption_box, vit_caption_box, blip2_caption_box, images_state, captions_state])
247
+
248
+ def load_from_url(url, images_state, captions_state):
249
+ import requests
250
+ from io import BytesIO
251
+ try:
252
+ img = Image.open(BytesIO(requests.get(url).content))
253
+ except:
254
+ return (None, None, None, "Bad URL.", "Bad URL.", "Bad URL.", [], [])
255
+ return generate_all(img, images_state, captions_state)
256
+
257
+ url_btn.click(load_from_url, inputs=[url_input, images_state, captions_state],
258
+ outputs=[preview1, preview2, preview3, blip_caption_box, vit_caption_box, blip2_caption_box, images_state, captions_state])
259
+
260
+ # ---------------- Metrics ----------------
261
+ gr.Markdown("### Compute Pairwise Metrics", elem_classes="heading-orange")
262
+ metrics_btn = gr.Button("Compute Metrics for All Pairs", elem_classes="teal-btn")
263
+ with gr.Row(elem_classes="metrics-row"):
264
+ metrics_A = gr.Markdown()
265
+ metrics_B = gr.Markdown()
266
+ metrics_C = gr.Markdown()
267
+
268
+ def compute_metrics_all_pairs_ui(images, captions):
269
+ # 3 spinners
270
+ yield (
271
+ "<div class='loading-line'></div>",
272
+ "<div class='loading-line'></div>",
273
+ "<div class='loading-line'></div>"
274
+ )
275
+
276
+ if len(images) < 1 or len(captions) < 3:
277
+ msg = "<b>Upload 1 image and generate all 3 captions.</b>"
278
+ yield (msg, msg, msg)
279
+ return
280
+
281
+ imgs = images * 3
282
+ A = compute_metrics_button(imgs, captions, 0, 1)
283
+ B = compute_metrics_button(imgs, captions, 0, 2)
284
+ C = compute_metrics_button(imgs, captions, 1, 2)
285
+
286
+ yield (
287
+ f"### BLIP-large ↔ ViT-GPT2\n{A}",
288
+ f"### BLIP-large ↔ BLIP2\n{B}",
289
+ f"### ViT-GPT2 ↔ BLIP2\n{C}"
290
+ )
291
+
292
+ metrics_btn.click(
293
+ compute_metrics_all_pairs_ui,
294
+ inputs=[images_state, captions_state],
295
+ outputs=[metrics_A, metrics_B, metrics_C]
296
+ )
297
+
298
+ # ---------------- NLP ----------------
299
+ gr.Markdown("### NLP Analysis", elem_classes="heading-orange")
300
+ nlp_btn = gr.Button("Analyze Captions", elem_classes="teal-btn")
301
+
302
+ with gr.Row(elem_classes="metrics-row"): # reuse metrics-row for flex layout
303
+ nlp_A = gr.Markdown()
304
+ nlp_B = gr.Markdown()
305
+ nlp_C = gr.Markdown()
306
+
307
+ def do_nlp_all(captions):
308
+ # 3 spinners like metrics
309
+ yield (
310
+ "<div class='loading-line'></div>",
311
+ "<div class='loading-line'></div>",
312
+ "<div class='loading-line'></div>"
313
+ )
314
+
315
+ if len(captions) < 3:
316
+ msg = "<b>All 3 captions required.</b>"
317
+ yield (msg, msg, msg)
318
+ return
319
+
320
+ labels = ["BLIP-large", "ViT-GPT2", "BLIP2"]
321
+ results = []
322
+ for label, cap in zip(labels, captions):
323
+ s, e, t = nlp_bundle(cap)
324
+ block = f"""
325
+ <h3><u>{label}</u></h3>
326
+ <b>Sentiment</b><br>{s}<br><br>
327
+ <b>Entities</b><br>{e}<br><br>
328
+ <b>Topics</b><br>{t}
329
+ """
330
+ results.append(block)
331
+
332
+ yield (results[0], results[1], results[2])
333
+
334
+ nlp_btn.click(do_nlp_all, inputs=[captions_state], outputs=[nlp_A, nlp_B, nlp_C])
335
+
336
+ """
337
+ # ---------------- NLP ---------------- COMMented out NLP
338
+ gr.Markdown("### NLP Analysis", elem_classes="heading-orange")
339
+ nlp_btn = gr.Button("Analyze Captions", elem_classes="teal-btn")
340
+ nlp_out = gr.HTML()
341
+ def do_nlp(captions):
342
+ yield "<div class='loading-line'></div>"
343
+ if len(captions) < 3:
344
+ yield "<b>All captions required.</b>"
345
+ return
346
+ labels = ["BLIP-large", "ViT-GPT2", "BLIP2"]
347
+ blocks = []
348
+ for label, cap in zip(labels, captions):
349
+ s, e, t = nlp_bundle(cap)
350
+ block = f""
351
+ <div style='flex:1;padding:10px;min-width:240px;'>
352
+ <h3><u>{label}</u></h3>
353
+ <b>Sentiment</b><br>{s}<br><br>
354
+ <b>Entities</b><br>{e}<br><br>
355
+ <b>Topics</b><br>{t}
356
+ </div>
357
+ ""
358
+ blocks.append(block)
359
+ yield f"<div style='display:flex; gap:20px;'>{''.join(blocks)}</div>"
360
+ nlp_btn.click(do_nlp, inputs=[captions_state], outputs=[nlp_out])"""
361
+
362
+
363
+
364
+ # ---------------- VQA ----------------
365
+ gr.Markdown("### Visual Question Answering (VQA)", elem_classes="heading-orange")
366
+ with gr.Row():
367
+ vqa_input = gr.Textbox(label="Ask about the image")
368
+ vqa_btn = gr.Button("Get Answer", elem_classes="teal-btn")
369
+ vqa_out = gr.Markdown()
370
+
371
+ def vqa_ui(question, image):
372
+ yield "<div class='loading-line'></div>"
373
+ yield answer_vqa(question, image)
374
+
375
+ vqa_btn.click(vqa_ui, inputs=[vqa_input, preview1], outputs=[vqa_out])
376
+
377
+ return demo
378
+
379
+ # ==============================
380
+ # LAUNCH
381
+ # ==============================
382
+ demo = build_ui()
383
+ demo.launch(debug=False)
requirements.txt CHANGED
@@ -1,6 +1,12 @@
1
- datasets
2
- transformers
3
- torch
4
- accelerate
5
- pillow
6
- gradio
 
 
 
 
 
 
 
1
+ torch>=2.1.0
2
+ transformers>=4.35.0
3
+ diffusers>=0.20.0
4
+ gradio>=3.49.0
5
+ Pillow>=10.0.0
6
+ lpips>=0.1.4
7
+ git+https://github.com/openai/CLIP.git
8
+ bert-score>=0.3.13
9
+ sentence-transformers>=2.2.2
10
+ rouge-score>=0.1.2
11
+ scikit-learn>=1.3.0
12
+ numpy>=1.25.0