webstk3 commited on
Commit
cdb98ed
·
1 Parent(s): 8884a36

Add detailed logging to diagnose startup failure

Browse files
Files changed (1) hide show
  1. app.py +151 -228
app.py CHANGED
@@ -1,24 +1,42 @@
1
  """
2
  Chest X-Ray Classifier with Grad-CAM Explainability
3
- =====================================================
4
- A Hugging Face Space that classifies chest X-rays into 14 thoracic
5
- conditions and generates Grad-CAM heatmaps for explainability.
6
  """
7
 
8
- import torch
9
- import numpy as np
10
- import gradio as gr
11
- from PIL import Image
12
- from torchvision import transforms
13
- import timm
14
- from pytorch_grad_cam import GradCAM
15
- from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget
16
- from pytorch_grad_cam.utils.image import show_cam_on_image
17
-
18
- # ─────────────────────────────────────────────────────────────
19
- # CONFIGURATION
20
- # ─────────────────────────────────────────────────────────────
21
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  CHEXPERT_LABELS = [
23
  "No Finding",
24
  "Enlarged Cardiomediastinum",
@@ -36,239 +54,144 @@ CHEXPERT_LABELS = [
36
  "Support Devices",
37
  ]
38
 
39
- DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
40
 
 
 
 
 
 
 
41
 
42
- # ─────────────────────────────────────────────────────────────
43
- # MODEL LOADING
44
- # ─────────────────────────────────────────────────────────────
45
 
46
  def load_model():
47
- """
48
- Load a DenseNet-121 model (CheXNet architecture).
49
- Uses ImageNet pretrained weights as a demo.
50
- """
51
- print("Loading DenseNet-121 model...")
52
  model = timm.create_model("densenet121", pretrained=True, num_classes=14)
53
  model = model.to(DEVICE)
54
  model.eval()
55
- print(f"Model loaded on {DEVICE}")
 
56
  return model
57
 
58
 
59
- # ─────────────────────────────────────────────────────────────
60
- # IMAGE PREPROCESSING
61
- # ─────────────────────────────────────────────────────────────
62
-
63
- preprocess = transforms.Compose([
64
- transforms.Resize((224, 224)),
65
- transforms.ToTensor(),
66
- transforms.Normalize(
67
- mean=[0.485, 0.456, 0.406],
68
- std=[0.229, 0.224, 0.225],
69
- ),
70
- ])
71
-
72
-
73
- # ─────────────────────────────────────────────────────────────
74
- # GRAD-CAM GENERATION
75
- # ─────────────────────────────────────────────────────────────
76
-
77
- def generate_gradcam(model, input_tensor, target_class_idx, target_layers):
78
- """Generate a Grad-CAM heatmap for the predicted class."""
79
- cam = GradCAM(model=model, target_layers=target_layers)
80
- targets = [ClassifierOutputTarget(target_class_idx)]
81
- grayscale_cam = cam(input_tensor=input_tensor, targets=targets)
82
- return grayscale_cam[0]
83
-
84
-
85
- # ─────────────────────────────────────────────────────────────
86
- # MAIN INFERENCE FUNCTION
87
- # ─────────────────────────────────────────────────────────────
88
-
89
  def classify_and_explain(image):
90
- """Run chest X-ray classification and generate Grad-CAM visualization."""
91
- if image is None:
92
- return None, "Please upload a chest X-ray image."
93
-
94
- # Preprocess
95
- image_rgb = image.convert("RGB")
96
- input_tensor = preprocess(image_rgb).unsqueeze(0).to(DEVICE)
97
- rgb_display = np.array(image_rgb.resize((224, 224))) / 255.0
98
-
99
- # Predict
100
- with torch.no_grad():
101
- logits = model(input_tensor)
102
- probabilities = torch.sigmoid(logits).cpu().numpy()[0]
103
-
104
- # Build results
105
- top_idx = int(np.argmax(probabilities))
106
- top_label = CHEXPERT_LABELS[top_idx]
107
- top_conf = float(probabilities[top_idx])
108
-
109
- # Sort all conditions by probability
110
- sorted_indices = np.argsort(probabilities)[::-1]
111
- sorted_labels = [
112
- (CHEXPERT_LABELS[i], float(probabilities[i]))
113
- for i in sorted_indices
114
- ]
115
-
116
- # Grad-CAM
117
- grayscale_cam = generate_gradcam(
118
- model, input_tensor, top_idx, target_layers
119
- )
120
- heatmap_overlay = show_cam_on_image(
121
- rgb_display, grayscale_cam, use_rgb=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  )
123
 
124
- # Format markdown output
125
- result_md = f"## Top Prediction: **{top_label}** ({top_conf:.1%})\n\n"
126
- result_md += "### All Condition Probabilities\n\n"
127
- result_md += "| Rank | Condition | Probability | Confidence |\n"
128
- result_md += "|:----:|-----------|:-----------:|:----------:|\n"
129
-
130
- for rank, (label, prob) in enumerate(sorted_labels, 1):
131
- bar_len = int(prob * 20)
132
- bar = "X" * bar_len + "-" * (20 - bar_len)
133
- emoji = "HIGH" if prob > 0.5 else "MED" if prob > 0.3 else "LOW"
134
- result_md += (
135
- f"| {rank} | {label} | {prob:.1%} | {emoji} `{bar}` |\n"
136
- )
137
-
138
- result_md += "\n---\n"
139
- result_md += f"**Grad-CAM target:** {top_label} - "
140
- result_md += "red regions indicate areas the model focused on.\n"
141
-
142
- return heatmap_overlay, result_md
143
 
144
-
145
- # ─────────────────────────────────────────────────────────────
146
- # GRADIO UI
147
- # ─────────────────────────────────────────────────────────────
148
-
149
- def build_ui():
150
- """Construct the Gradio Blocks interface."""
151
-
152
- with gr.Blocks(
153
- title="Chest X-Ray Classifier with Grad-CAM",
154
- theme=gr.themes.Soft(),
155
- ) as demo:
156
-
157
- # Header
158
  gr.Markdown(
159
  """
160
- # Chest X-Ray Classifier with Grad-CAM Explainability
161
 
162
- Upload a chest X-ray to classify **14 thoracic conditions** and see
163
- **where the model is looking** via Grad-CAM heatmaps.
 
164
 
165
- > **Disclaimer:** This is an educational tool only.
166
- > It is NOT a medical device and must NOT be used for clinical diagnosis.
167
- > Always consult a qualified physician.
168
  """
169
  )
170
 
171
- with gr.Row():
172
-
173
- # Left Column: Input
174
- with gr.Column(scale=1):
175
- input_image = gr.Image(
176
- type="pil",
177
- label="Upload Chest X-Ray",
178
- height=400,
179
- )
180
- classify_btn = gr.Button(
181
- "Classify and Explain",
182
- variant="primary",
183
- size="lg",
184
- )
185
-
186
- gr.Markdown("### Example Images")
187
- gr.Markdown(
188
- "Upload any PA/AP chest X-ray. "
189
- "Best results with standard radiograph formats (JPEG, PNG)."
190
- )
191
-
192
- # Right Column: Output
193
- with gr.Column(scale=1):
194
- output_image = gr.Image(
195
- label="Grad-CAM Heatmap Overlay",
196
- height=400,
197
- )
198
- output_text = gr.Markdown(
199
- label="Classification Results",
200
- value="*Upload an image and click Classify to see results.*",
201
- )
202
-
203
- # How It Works Section
204
- with gr.Accordion("How Grad-CAM Works", open=False):
205
- gr.Markdown(
206
- """
207
- **Grad-CAM** (Gradient-weighted Class Activation Mapping) produces
208
- a coarse localization map highlighting important regions in the image
209
- for predicting a particular class.
210
-
211
- ### The Process:
212
- 1. **Forward pass** - the image passes through the network
213
- 2. **Backward pass** - gradients of the target class score flow back
214
- to the final convolutional layer
215
- 3. **Weighting** - feature maps are weighted by the average gradient
216
- 4. **ReLU** - only positive contributions are kept
217
- 5. **Overlay** - the heatmap is superimposed on the original image
218
-
219
- ### Interpreting the Heatmap:
220
- - **Red** = High attention (model focused heavily here)
221
- - **Yellow** = Moderate attention
222
- - **Green/Blue** = Low attention (model largely ignored)
223
-
224
- ### Reference:
225
- Selvaraju et al., "Grad-CAM: Visual Explanations from Deep Networks
226
- via Gradient-based Localization", ICCV 2017.
227
- """
228
- )
229
-
230
- # Model Info Section
231
- with gr.Accordion("Model Architecture", open=False):
232
- gr.Markdown(
233
- """
234
- | Component | Detail |
235
- |-----------|--------|
236
- | **Architecture** | DenseNet-121 (CheXNet-style) |
237
- | **Parameters** | ~8M |
238
- | **Input Size** | 224 x 224 x 3 |
239
- | **Output** | 14 condition probabilities (sigmoid) |
240
- | **Grad-CAM Layer** | model.features[-1] (final conv block) |
241
- | **Pretraining** | ImageNet-1K (demo) |
242
-
243
- ### For Production Use:
244
- Fine-tune on a medical imaging dataset:
245
- - NIH ChestX-ray14
246
- - CheXpert
247
- - MIMIC-CXR
248
- """
249
- )
250
-
251
- # Wire up the button
252
- classify_btn.click(
253
- fn=classify_and_explain,
254
- inputs=[input_image],
255
- outputs=[output_image, output_text],
256
- )
257
-
258
- return demo
259
-
260
 
261
- # ─────────────────────────────────────────────────────────────
262
- # ENTRY POINT
263
- # ─────────────────────────────────────────────────────────────
264
 
265
- if __name__ == "__main__":
266
- # Load model at startup
267
- model = load_model()
268
 
269
- # Target layer for Grad-CAM
270
- target_layers = [model.features[-1]]
271
 
272
- # Build and launch
273
- demo = build_ui()
274
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
  """
2
  Chest X-Ray Classifier with Grad-CAM Explainability
 
 
 
3
  """
4
 
5
+ import traceback
6
+ import sys
7
+
8
+ print("=" * 50)
9
+ print("STEP 1: Starting imports...")
10
+ sys.stdout.flush()
11
+
12
+ try:
13
+ import torch
14
+ print(" torch imported OK")
15
+ import numpy as np
16
+ print(" numpy imported OK")
17
+ import gradio as gr
18
+ print(" gradio imported OK")
19
+ from PIL import Image
20
+ print(" PIL imported OK")
21
+ from torchvision import transforms
22
+ print(" torchvision transforms imported OK")
23
+ import timm
24
+ print(" timm imported OK")
25
+ from pytorch_grad_cam import GradCAM
26
+ print(" GradCAM imported OK")
27
+ from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget
28
+ print(" ClassifierOutputTarget imported OK")
29
+ from pytorch_grad_cam.utils.image import show_cam_on_image
30
+ print(" show_cam_on_image imported OK")
31
+ except Exception as e:
32
+ print(f"IMPORT ERROR: {e}")
33
+ traceback.print_exc()
34
+ sys.stdout.flush()
35
+ raise
36
+
37
+ sys.stdout.flush()
38
+
39
+ # CONFIG
40
  CHEXPERT_LABELS = [
41
  "No Finding",
42
  "Enlarged Cardiomediastinum",
 
54
  "Support Devices",
55
  ]
56
 
57
+ DEVICE = torch.device("cpu")
58
 
59
+ # PREPROCESS
60
+ preprocess = transforms.Compose([
61
+ transforms.Resize((224, 224)),
62
+ transforms.ToTensor(),
63
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
64
+ ])
65
 
 
 
 
66
 
67
  def load_model():
68
+ """Load DenseNet-121."""
69
+ print("STEP 2: Loading model...")
70
+ sys.stdout.flush()
 
 
71
  model = timm.create_model("densenet121", pretrained=True, num_classes=14)
72
  model = model.to(DEVICE)
73
  model.eval()
74
+ print("STEP 3: Model loaded OK")
75
+ sys.stdout.flush()
76
  return model
77
 
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  def classify_and_explain(image):
80
+ """Run classification and Grad-CAM."""
81
+ try:
82
+ if image is None:
83
+ return None, "Please upload a chest X-ray image."
84
+
85
+ print("INFERENCE: Starting...")
86
+ sys.stdout.flush()
87
+
88
+ image_rgb = image.convert("RGB")
89
+ input_tensor = preprocess(image_rgb).unsqueeze(0).to(DEVICE)
90
+ rgb_display = np.array(image_rgb.resize((224, 224))).astype(np.float32) / 255.0
91
+
92
+ with torch.no_grad():
93
+ logits = model(input_tensor)
94
+ probabilities = torch.sigmoid(logits).cpu().numpy()[0]
95
+
96
+ top_idx = int(np.argmax(probabilities))
97
+ top_label = CHEXPERT_LABELS[top_idx]
98
+ top_conf = float(probabilities[top_idx])
99
+
100
+ print(f"INFERENCE: Top prediction = {top_label} ({top_conf:.1%})")
101
+ sys.stdout.flush()
102
+
103
+ # Grad-CAM
104
+ print("INFERENCE: Computing Grad-CAM...")
105
+ sys.stdout.flush()
106
+ cam = GradCAM(model=model, target_layers=target_layers)
107
+ targets = [ClassifierOutputTarget(top_idx)]
108
+ grayscale_cam = cam(input_tensor=input_tensor, targets=targets)[0]
109
+
110
+ print("INFERENCE: Creating overlay...")
111
+ sys.stdout.flush()
112
+ heatmap_overlay = show_cam_on_image(rgb_display, grayscale_cam, use_rgb=True)
113
+
114
+ print("INFERENCE: Done!")
115
+ sys.stdout.flush()
116
+
117
+ # Results
118
+ sorted_indices = np.argsort(probabilities)[::-1]
119
+ result_md = f"## Top Prediction: **{top_label}** ({top_conf:.1%})\n\n"
120
+ result_md += "### All Condition Probabilities\n\n"
121
+ result_md += "| Rank | Condition | Probability | Confidence |\n"
122
+ result_md += "|:----:|-----------|:-----------:|:----------:|\n"
123
+
124
+ for rank, idx in enumerate(sorted_indices, 1):
125
+ label = CHEXPERT_LABELS[idx]
126
+ prob = float(probabilities[idx])
127
+ bar_len = int(prob * 20)
128
+ bar = "X" * bar_len + "-" * (20 - bar_len)
129
+ emoji = "HIGH" if prob > 0.5 else "MED" if prob > 0.3 else "LOW"
130
+ result_md += f"| {rank} | {label} | {prob:.1%} | {emoji} `{bar}` |\n"
131
+
132
+ result_md += "\n---\n"
133
+ result_md += f"**Grad-CAM target:** {top_label} - red regions indicate areas the model focused on.\n"
134
+
135
+ return heatmap_overlay, result_md
136
+
137
+ except Exception as e:
138
+ error_msg = f"Error during inference: {e}\n{traceback.format_exc()}"
139
+ print(error_msg)
140
+ sys.stdout.flush()
141
+ return None, f"**Error:** {e}"
142
+
143
+
144
+ # BUILD UI
145
+ print("STEP 4: Building Gradio UI...")
146
+ sys.stdout.flush()
147
+
148
+ with gr.Blocks(
149
+ title="Chest X-Ray Classifier with Grad-CAM",
150
+ theme=gr.themes.Soft(),
151
+ ) as demo:
152
+
153
+ gr.Markdown(
154
+ """
155
+ # Chest X-Ray Classifier with Grad-CAM Explainability
156
+
157
+ Upload a chest X-ray to classify **14 thoracic conditions** and see
158
+ **where the model is looking** via Grad-CAM heatmaps.
159
+
160
+ > **Disclaimer:** This is an educational tool only. NOT a medical device.
161
+ """
162
  )
163
 
164
+ with gr.Row():
165
+ with gr.Column(scale=1):
166
+ input_image = gr.Image(type="pil", label="Upload Chest X-Ray", height=400)
167
+ classify_btn = gr.Button("Classify and Explain", variant="primary", size="lg")
168
+ with gr.Column(scale=1):
169
+ output_image = gr.Image(label="Grad-CAM Heatmap Overlay", height=400)
170
+ output_text = gr.Markdown(value="*Upload an image and click Classify to see results.*")
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
+ with gr.Accordion("How Grad-CAM Works", open=False):
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  gr.Markdown(
174
  """
175
+ **Grad-CAM** highlights which parts of the image the model focused on.
176
 
177
+ - **Red** = High attention
178
+ - **Yellow** = Moderate attention
179
+ - **Green/Blue** = Low attention
180
 
181
+ Reference: Selvaraju et al., "Grad-CAM", ICCV 2017.
 
 
182
  """
183
  )
184
 
185
+ classify_btn.click(fn=classify_and_explain, inputs=[input_image], outputs=[output_image, output_text])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
+ # LOAD MODEL AND LAUNCH
188
+ print("STEP 5: Loading model at startup...")
189
+ sys.stdout.flush()
190
 
191
+ model = load_model()
192
+ target_layers = [model.features[-1]]
 
193
 
194
+ print("STEP 6: Launching server...")
195
+ sys.stdout.flush()
196
 
197
+ demo.launch(server_name="0.0.0.0", server_port=7860)