VJBharathkumar commited on
Commit
da1d399
·
verified ·
1 Parent(s): dc2dc55

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +60 -54
src/streamlit_app.py CHANGED
@@ -113,86 +113,88 @@ def predict_prob(x: np.ndarray) -> float:
113
  return max(0.0, min(1.0, prob))
114
 
115
  # -----------------------------
116
- # Grad-CAM (robust for nested "resnet50" submodel)
 
 
 
117
  # -----------------------------
118
  @st.cache_resource
119
- def build_gradcam_tools():
120
- # If your backbone layer name is different, change here:
121
- backbone_name = "resnet50"
122
  backbone = model.get_layer(backbone_name)
123
 
124
- # pick the last Conv2D inside the backbone
125
  last_conv = None
126
  for lyr in reversed(backbone.layers):
127
  if isinstance(lyr, tf.keras.layers.Conv2D):
128
  last_conv = lyr.name
129
  break
 
130
  if last_conv is None:
131
- raise ValueError("Could not find a Conv2D layer inside the ResNet backbone.")
132
 
133
- # outputs: last conv feature map + probability head
134
- conv_out = backbone.get_layer(last_conv).output
135
- prob_out = model.outputs[-1] if isinstance(model.outputs, (list, tuple)) else model.output
 
 
136
 
137
- grad_model = keras.Model(inputs=model.inputs, outputs=[conv_out, prob_out])
138
- return grad_model, last_conv
139
 
140
  def make_gradcam_heatmap(img_batch: np.ndarray) -> np.ndarray:
141
- grad_model, _ = build_gradcam_tools()
142
 
143
  x = tf.convert_to_tensor(img_batch, dtype=tf.float32)
144
 
145
  with tf.GradientTape() as tape:
146
- conv_out, preds = grad_model(x, training=False)
147
- loss = preds[:, 0] # binary sigmoid prob
 
 
 
 
 
 
 
148
 
149
  grads = tape.gradient(loss, conv_out)
150
  if grads is None:
151
- raise ValueError("Gradients are None. Grad-CAM cannot be computed for this model setup.")
 
 
152
 
153
  pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
154
- conv_out = conv_out[0] # (H,W,channels)
155
 
156
  heatmap = tf.reduce_sum(conv_out * pooled_grads, axis=-1)
157
- heatmap = tf.maximum(heatmap, 0)
158
 
159
  denom = tf.reduce_max(heatmap) + 1e-8
160
  heatmap = heatmap / denom
161
- return heatmap.numpy()
162
 
163
- def overlay_heatmap(img_2d: np.ndarray, heatmap: np.ndarray):
164
- # resize heatmap to img_size
165
- heat = tf.image.resize(heatmap[..., None], (img_size, img_size)).numpy().squeeze()
166
-
167
- # ensure base image displayed at img_size
168
- base = tf.image.resize(img_2d[..., None], (img_size, img_size)).numpy().squeeze()
169
 
170
- fig = plt.figure(figsize=(5, 5))
171
- plt.imshow(base, cmap="gray")
172
- plt.imshow(heat, cmap="jet", alpha=0.35)
173
- plt.axis("off")
174
- plt.tight_layout()
175
- return fig
176
 
177
  # -----------------------------
178
  # PDF generation (fix unicode issues)
179
  # -----------------------------
180
- def safe_text(s: str, max_len: int = 180) -> str:
181
- """
182
- Convert to something FPDF core fonts can render.
183
- Also trims very long strings to avoid layout failures.
184
- """
185
  if s is None:
186
  return ""
187
  s = str(s)
188
 
189
- # Replace common unicode dashes/quotes with ascii
190
  s = s.replace("–", "-").replace("—", "-").replace("’", "'").replace("“", '"').replace("”", '"')
191
 
192
- # Remove / replace any remaining unsupported chars (latin-1 fallback)
 
 
 
193
  s = s.encode("latin-1", "replace").decode("latin-1")
194
 
195
- # Avoid extremely long unbroken lines
196
  if len(s) > max_len:
197
  s = s[:max_len] + "..."
198
  return s
@@ -201,8 +203,12 @@ def build_pdf_report(df_ok: pd.DataFrame, threshold: float, model_version: str)
201
  pdf = FPDF()
202
  pdf.set_auto_page_break(auto=True, margin=12)
203
  pdf.add_page()
 
204
  pdf.set_font("Helvetica", size=12)
205
 
 
 
 
206
  pdf.cell(0, 8, safe_text("Pneumonia Detection Report"), ln=True)
207
  pdf.set_font("Helvetica", size=10)
208
  pdf.cell(0, 6, safe_text(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"), ln=True)
@@ -210,24 +216,23 @@ def build_pdf_report(df_ok: pd.DataFrame, threshold: float, model_version: str)
210
  pdf.cell(0, 6, safe_text(f"Decision Threshold: {threshold:.2f}"), ln=True)
211
  pdf.ln(4)
212
 
213
- pdf.set_font("Helvetica", size=10)
214
  for _, row in df_ok.iterrows():
215
- line1 = f"File: {row['file_name']}"
216
- line2 = f"Probability: {row['probability']*100:.2f}%"
217
- line3 = f"Confidence: {row['confidence_level']}"
218
- line4 = f"Prediction: {row['prediction']}"
219
-
220
- for line in [line1, line2, line3, line4]:
221
- pdf.multi_cell(0, 6, safe_text(line))
222
-
223
  pdf.ln(2)
224
 
225
  out = pdf.output(dest="S")
226
- # fpdf may return str in some versions
227
  if isinstance(out, str):
228
  out = out.encode("latin-1", "ignore")
229
  return out
230
 
 
231
  # -----------------------------
232
  # UI
233
  # -----------------------------
@@ -353,14 +358,15 @@ if submit:
353
  if len(df_ok) > 0:
354
  pdf_bytes = build_pdf_report(df_ok, threshold, MODEL_VERSION)
355
  st.download_button(
356
- "Download PDF Report",
357
- data=pdf_bytes,
358
- file_name="pneumonia_report.pdf",
359
- mime="application/pdf",
360
- use_container_width=True
361
- )
362
  else:
363
  st.info("PDF report is available only when at least one file is successfully processed.")
 
364
 
365
  st.divider()
366
  st.caption(
 
113
  return max(0.0, min(1.0, prob))
114
 
115
  # -----------------------------
116
+
117
+ # -----------------------------
118
+ # -----------------------------
119
+ # Grad-CAM (robust for nested ResNet backbone)
120
  # -----------------------------
121
  @st.cache_resource
122
+ def get_backbone_and_last_conv():
123
+ backbone_name = "resnet50" # your model has this layer name
 
124
  backbone = model.get_layer(backbone_name)
125
 
 
126
  last_conv = None
127
  for lyr in reversed(backbone.layers):
128
  if isinstance(lyr, tf.keras.layers.Conv2D):
129
  last_conv = lyr.name
130
  break
131
+
132
  if last_conv is None:
133
+ raise ValueError("No Conv2D layer found inside resnet50 backbone.")
134
 
135
+ # model that outputs the conv feature map
136
+ conv_model = keras.Model(
137
+ inputs=model.inputs,
138
+ outputs=backbone.get_layer(last_conv).output
139
+ )
140
 
141
+ return backbone_name, last_conv, conv_model
 
142
 
143
  def make_gradcam_heatmap(img_batch: np.ndarray) -> np.ndarray:
144
+ _, last_conv, conv_model = get_backbone_and_last_conv()
145
 
146
  x = tf.convert_to_tensor(img_batch, dtype=tf.float32)
147
 
148
  with tf.GradientTape() as tape:
149
+ conv_out = conv_model(x, training=False) # (1, h, w, ch)
150
+
151
+ preds = model(x, training=False)
152
+ if isinstance(preds, (list, tuple)):
153
+ prob = preds[-1]
154
+ else:
155
+ prob = preds
156
+
157
+ loss = prob[:, 0] # binary prob
158
 
159
  grads = tape.gradient(loss, conv_out)
160
  if grads is None:
161
+ raise ValueError(
162
+ f"Gradients are None (cannot compute Grad-CAM). Last conv was: {last_conv}"
163
+ )
164
 
165
  pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
166
+ conv_out = conv_out[0]
167
 
168
  heatmap = tf.reduce_sum(conv_out * pooled_grads, axis=-1)
169
+ heatmap = tf.maximum(heatmap, 0.0)
170
 
171
  denom = tf.reduce_max(heatmap) + 1e-8
172
  heatmap = heatmap / denom
 
173
 
174
+ return heatmap.numpy()
 
 
 
 
 
175
 
 
 
 
 
 
 
176
 
177
  # -----------------------------
178
  # PDF generation (fix unicode issues)
179
  # -----------------------------
180
+ # -----------------------------
181
+ # PDF generator (robust wrapping)
182
+ # -----------------------------
183
+ def safe_text(s: str, max_len: int = 220) -> str:
 
184
  if s is None:
185
  return ""
186
  s = str(s)
187
 
188
+ # replace unicode dashes/quotes
189
  s = s.replace("–", "-").replace("—", "-").replace("’", "'").replace("“", '"').replace("”", '"')
190
 
191
+ # add break opportunities for long tokens (UUIDs, filenames)
192
+ s = s.replace("-", "- ").replace("_", "_ ").replace("/", "/ ")
193
+
194
+ # latin-1 safe
195
  s = s.encode("latin-1", "replace").decode("latin-1")
196
 
197
+ # trim
198
  if len(s) > max_len:
199
  s = s[:max_len] + "..."
200
  return s
 
203
  pdf = FPDF()
204
  pdf.set_auto_page_break(auto=True, margin=12)
205
  pdf.add_page()
206
+
207
  pdf.set_font("Helvetica", size=12)
208
 
209
+ # effective width (prevents “not enough horizontal space”)
210
+ w = pdf.w - pdf.l_margin - pdf.r_margin
211
+
212
  pdf.cell(0, 8, safe_text("Pneumonia Detection Report"), ln=True)
213
  pdf.set_font("Helvetica", size=10)
214
  pdf.cell(0, 6, safe_text(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"), ln=True)
 
216
  pdf.cell(0, 6, safe_text(f"Decision Threshold: {threshold:.2f}"), ln=True)
217
  pdf.ln(4)
218
 
 
219
  for _, row in df_ok.iterrows():
220
+ lines = [
221
+ f"File: {row['file_name']}",
222
+ f"Probability: {row['probability']*100:.2f}%",
223
+ f"Confidence: {row['confidence_level']}",
224
+ f"Prediction: {row['prediction']}",
225
+ ]
226
+ for line in lines:
227
+ pdf.multi_cell(w, 6, safe_text(line))
228
  pdf.ln(2)
229
 
230
  out = pdf.output(dest="S")
 
231
  if isinstance(out, str):
232
  out = out.encode("latin-1", "ignore")
233
  return out
234
 
235
+
236
  # -----------------------------
237
  # UI
238
  # -----------------------------
 
358
  if len(df_ok) > 0:
359
  pdf_bytes = build_pdf_report(df_ok, threshold, MODEL_VERSION)
360
  st.download_button(
361
+ "Download PDF Report",
362
+ data=pdf_bytes,
363
+ file_name="pneumonia_report.pdf",
364
+ mime="application/pdf",
365
+ use_container_width=True
366
+ )
367
  else:
368
  st.info("PDF report is available only when at least one file is successfully processed.")
369
+
370
 
371
  st.divider()
372
  st.caption(