Matanech commited on
Commit
87643bf
·
verified ·
1 Parent(s): ee035cd

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -201
app.py DELETED
@@ -1,201 +0,0 @@
1
- """
2
- Chest X-ray Recommender - HuggingFace Space entry point.
3
-
4
- Loads pre-computed CLIP embeddings (embeddings.parquet, built by the
5
- companion notebook) and serves a Gradio UI that returns the 3 most
6
- visually similar X-rays for a given text or image query.
7
-
8
- Educational demo only. NOT a medical device.
9
- """
10
- from __future__ import annotations
11
-
12
- import base64
13
- import io
14
- import os
15
-
16
- import gradio as gr
17
- import numpy as np
18
- import pandas as pd
19
- import torch
20
- from PIL import Image
21
- from transformers import CLIPModel, CLIPProcessor
22
-
23
- # ---------------------------------------------------------------------------
24
- # Config
25
- # ---------------------------------------------------------------------------
26
- MODEL_ID = os.environ.get("CLIP_MODEL_ID", "openai/clip-vit-base-patch32")
27
- EMBEDDINGS_FILE = os.environ.get("EMBEDDINGS_FILE", "embeddings.parquet")
28
- TOP_K = 3
29
-
30
- # Optional: a YouTube embed for your walk-through video. Replace the id.
31
- VIDEO_EMBED_ID = os.environ.get("VIDEO_EMBED_ID", "")
32
-
33
- device = "cuda" if torch.cuda.is_available() else "cpu"
34
- print(f"[startup] device = {device}")
35
-
36
- # ---------------------------------------------------------------------------
37
- # Load model
38
- # ---------------------------------------------------------------------------
39
- print(f"[startup] loading CLIP model: {MODEL_ID}")
40
- clip_model = CLIPModel.from_pretrained(MODEL_ID).to(device).eval()
41
- clip_processor = CLIPProcessor.from_pretrained(MODEL_ID)
42
-
43
- # ---------------------------------------------------------------------------
44
- # Load catalog (embeddings + thumbnails + reports)
45
- # ---------------------------------------------------------------------------
46
- print(f"[startup] loading catalog: {EMBEDDINGS_FILE}")
47
- df = pd.read_parquet(EMBEDDINGS_FILE)
48
- print(f"[startup] catalog rows: {len(df):,}")
49
-
50
- EMB_MATRIX = np.vstack(df["embedding"].values).astype("float32")
51
- # Make sure the catalog vectors are L2-normalised (cheap, idempotent)
52
- norms = np.linalg.norm(EMB_MATRIX, axis=1, keepdims=True)
53
- EMB_MATRIX = EMB_MATRIX / np.where(norms == 0, 1, norms)
54
-
55
- def _b64_to_pil(b64: str) -> Image.Image:
56
- img = Image.open(io.BytesIO(base64.b64decode(b64)))
57
- img.load() # force-load before BytesIO is garbage-collected
58
- return img.convert("RGB") # RGB renders more reliably in Gradio than "L"
59
-
60
- THUMBS = [_b64_to_pil(b) for b in df["image_b64"]]
61
- REPORTS = df["report"].fillna("").tolist()
62
- CLUSTER = df["cluster"].astype(int).tolist() if "cluster" in df.columns else [0] * len(df)
63
-
64
-
65
- # ---------------------------------------------------------------------------
66
- # Embedding helpers
67
- # ---------------------------------------------------------------------------
68
- def _to_tensor(out):
69
- """Unwrap a CLIP output that may be a tensor or a HuggingFace ModelOutput."""
70
- if torch.is_tensor(out):
71
- return out
72
- if hasattr(out, "image_embeds"):
73
- return out.image_embeds
74
- if hasattr(out, "text_embeds"):
75
- return out.text_embeds
76
- if hasattr(out, "pooler_output"):
77
- return out.pooler_output
78
- if hasattr(out, "last_hidden_state"):
79
- return out.last_hidden_state[:, 0]
80
- raise TypeError(f"Cannot unwrap CLIP output of type {type(out)}")
81
-
82
-
83
- @torch.no_grad()
84
- def _embed_image(pil_img: Image.Image) -> np.ndarray:
85
- inputs = clip_processor(images=pil_img.convert("RGB"), return_tensors="pt").to(device)
86
- vision_out = clip_model.vision_model(pixel_values=inputs["pixel_values"])
87
- pooled = _to_tensor(vision_out)
88
- emb = clip_model.visual_projection(pooled)
89
- emb = emb / emb.norm(p=2, dim=-1, keepdim=True)
90
- return emb.cpu().numpy()[0]
91
-
92
-
93
- @torch.no_grad()
94
- def _embed_text(text: str) -> np.ndarray:
95
- inputs = clip_processor(
96
- text=[text], return_tensors="pt",
97
- padding=True, truncation=True, max_length=77,
98
- ).to(device)
99
- text_out = clip_model.text_model(
100
- input_ids=inputs["input_ids"],
101
- attention_mask=inputs.get("attention_mask"),
102
- )
103
- pooled = _to_tensor(text_out)
104
- emb = clip_model.text_projection(pooled)
105
- emb = emb / emb.norm(p=2, dim=-1, keepdim=True)
106
- return emb.cpu().numpy()[0]
107
-
108
-
109
- def _top_k(query_vec: np.ndarray, k: int = TOP_K):
110
- scores = EMB_MATRIX @ query_vec.astype("float32")
111
- idx = np.argsort(-scores)[:k]
112
- return [
113
- {
114
- "index" : int(i),
115
- "score" : float(scores[i]),
116
- "image" : THUMBS[i],
117
- "report" : REPORTS[i],
118
- "cluster" : CLUSTER[i],
119
- }
120
- for i in idx
121
- ]
122
-
123
-
124
- # ---------------------------------------------------------------------------
125
- # Gradio handler
126
- # ---------------------------------------------------------------------------
127
- def recommend(text_query: str, image_query: Image.Image | None):
128
- if image_query is not None:
129
- q = _embed_image(image_query)
130
- used = "image"
131
- elif text_query and text_query.strip():
132
- q = _embed_text(text_query.strip())
133
- used = "text"
134
- else:
135
- return [], "Please type a description **or** upload an X-ray."
136
-
137
- results = _top_k(q, k=TOP_K)
138
- gallery = [(r["image"], f"#{r['index']} - score {r['score']:.3f}") for r in results]
139
- details = "\n\n".join(
140
- f"### Match {n+1} (score {r['score']:.3f}, cluster {r['cluster']})\n"
141
- f"{r['report'][:600]}{'...' if len(r['report']) > 600 else ''}"
142
- for n, r in enumerate(results)
143
- )
144
- details = f"_Query used: **{used}**_\n\n" + details
145
- return gallery, details
146
-
147
-
148
- # ---------------------------------------------------------------------------
149
- # UI
150
- # ---------------------------------------------------------------------------
151
- DESCRIPTION = """
152
- # Chest X-ray Recommender - CLIP embeddings
153
-
154
- Upload a chest X-ray **or** describe a finding in words, and the app will
155
- retrieve the 3 most visually similar X-rays from a pre-computed catalog
156
- drawn from `MLforHealthcare/mimic-cxr`.
157
-
158
- > Educational demo only. **Not** a medical device. Do not use for clinical decisions.
159
- """
160
-
161
- with gr.Blocks(title="Chest X-ray Recommender") as demo:
162
- gr.Markdown(DESCRIPTION)
163
-
164
- with gr.Row():
165
- with gr.Column(scale=1):
166
- text_in = gr.Textbox(
167
- lines=2,
168
- label="Describe a finding (English)",
169
- placeholder='e.g. "bilateral pleural effusion with cardiomegaly"',
170
- )
171
- image_in = gr.Image(type="pil", label="...or upload an X-ray")
172
- btn = gr.Button("Recommend", variant="primary")
173
- gr.Examples(
174
- examples=[
175
- ["bilateral pleural effusion with cardiomegaly", None],
176
- ["clear lungs, no acute cardiopulmonary process", None],
177
- ["right lower lobe pneumonia", None],
178
- ["pneumothorax", None],
179
- ],
180
- inputs=[text_in, image_in],
181
- )
182
- with gr.Column(scale=2):
183
- gallery = gr.Gallery(label="Top-3 similar X-rays", columns=3, height=350)
184
- details = gr.Markdown()
185
-
186
- btn.click(recommend, inputs=[text_in, image_in], outputs=[gallery, details])
187
-
188
- if VIDEO_EMBED_ID:
189
- gr.HTML(
190
- f"""
191
- <h3>Walk-through video</h3>
192
- <iframe width="720" height="405"
193
- src="https://www.youtube.com/embed/{VIDEO_EMBED_ID}"
194
- title="Assignment 3 walk-through" frameborder="0"
195
- allow="autoplay; encrypted-media; picture-in-picture" allowfullscreen>
196
- </iframe>
197
- """
198
- )
199
-
200
- if __name__ == "__main__":
201
- demo.launch()