unknown commited on
Commit
56e0e9b
·
1 Parent(s): cc93464

feat: full jet colormap composite heatmap (matches medical Grad-CAM style)

Browse files
Files changed (1) hide show
  1. main.py +69 -40
main.py CHANGED
@@ -1161,50 +1161,71 @@ def _compute_xray_pseudo_cam(model, img_tensor: torch.Tensor, target_idx: int):
1161
  handle.remove()
1162
 
1163
 
1164
- def _heatmap_to_png_base64(heatmap, percentile: float = 70.0) -> str:
1165
- """Convert a (H, W) [0,1] numpy array to a jet-colored RGBA base64 PNG.
1166
-
1167
- Applies percentile thresholding so only the most salient regions
1168
- (top ~30% of attention) are visible. This produces focused heatmaps
1169
- instead of uniform red tint when the raw CAM is diffuse.
 
 
 
 
 
1170
 
1171
- Colormap: jet-like gradient
1172
- low → transparent
1173
- mid → green / yellow
1174
- high red
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1175
  """
1176
  import numpy as np
1177
 
1178
- # Percentile threshold only show top (100 - percentile)% of attention
1179
- thresh = np.percentile(heatmap, percentile)
1180
- focused = np.clip(heatmap - thresh, 0, None)
1181
- if focused.max() > 0:
1182
- focused = focused / focused.max()
1183
- else:
1184
- focused = np.zeros_like(heatmap)
1185
-
1186
- h, w = focused.shape
1187
- rgba = np.zeros((h, w, 4), dtype=np.uint8)
1188
-
1189
- # Jet-like colormap computed in pure numpy
1190
- # 0.00 → blue (0,0,255)
1191
- # 0.25 cyan (0,255,255)
1192
- # 0.50 green (0,255,0)
1193
- # 0.75 → yellow (255,255,0)
1194
- # 1.00 → red (255,0,0)
1195
- r = np.clip(1.5 - np.abs(4 * focused - 3), 0, 1)
1196
- g = np.clip(1.5 - np.abs(4 * focused - 2), 0, 1)
1197
- b = np.clip(1.5 - np.abs(4 * focused - 1), 0, 1)
1198
-
1199
- rgba[..., 0] = (r * 255).astype(np.uint8)
1200
- rgba[..., 1] = (g * 255).astype(np.uint8)
1201
- rgba[..., 2] = (b * 255).astype(np.uint8)
1202
- # Alpha ramps up quickly so low-attention areas are transparent
1203
- rgba[..., 3] = (np.sqrt(focused) * 220).astype(np.uint8)
1204
-
1205
- overlay = Image.fromarray(rgba, mode="RGBA")
1206
  buf = io.BytesIO()
1207
- overlay.save(buf, format="PNG")
1208
  b64 = base64.b64encode(buf.getvalue()).decode("ascii")
1209
  return f"data:image/png;base64,{b64}"
1210
 
@@ -1270,7 +1291,15 @@ async def predict_xray(
1270
  cam = _compute_xray_pseudo_cam(model, tensor_cam, top_idx)
1271
  else:
1272
  cam = _compute_xray_gradcam(model, tensor_cam, top_idx)
1273
- heatmap_b64 = _heatmap_to_png_base64(cam)
 
 
 
 
 
 
 
 
1274
  except Exception as cam_err:
1275
  print(f"Heatmap computation failed: {cam_err}")
1276
 
 
1161
  handle.remove()
1162
 
1163
 
1164
+ def _cam_to_jet_rgb(cam):
1165
+ """Map a (H, W) [0, 1] heatmap to a (H, W, 3) uint8 RGB jet colormap.
1166
+
1167
+ Matches matplotlib's jet colormap:
1168
+ 0.00 dark blue (0, 0, 128)
1169
+ 0.25 blue/cyan
1170
+ 0.50 → green
1171
+ 0.75 → yellow
1172
+ 1.00 → dark red (128, 0, 0)
1173
+ """
1174
+ import numpy as np
1175
 
1176
+ cam = np.clip(cam, 0.0, 1.0)
1177
+ r = np.clip(1.5 - np.abs(4.0 * cam - 3.0), 0, 1)
1178
+ g = np.clip(1.5 - np.abs(4.0 * cam - 2.0), 0, 1)
1179
+ b = np.clip(1.5 - np.abs(4.0 * cam - 1.0), 0, 1)
1180
+
1181
+ rgb = np.zeros((*cam.shape, 3), dtype=np.uint8)
1182
+ rgb[..., 0] = (r * 255).astype(np.uint8)
1183
+ rgb[..., 1] = (g * 255).astype(np.uint8)
1184
+ rgb[..., 2] = (b * 255).astype(np.uint8)
1185
+ return rgb
1186
+
1187
+
1188
+ def _composite_heatmap_on_xray(
1189
+ cam,
1190
+ xray_gray: "np.ndarray",
1191
+ alpha: float = 0.5,
1192
+ gamma: float = 0.7,
1193
+ ) -> str:
1194
+ """Composite a jet heatmap onto a grayscale X-ray and return a base64 PNG.
1195
+
1196
+ Produces the classic "medical Grad-CAM" look:
1197
+ - Full jet colormap across the image (blue→cyan→green→yellow→red)
1198
+ - Base X-ray always visible underneath via alpha blending
1199
+ - Gamma < 1 boosts contrast of mid-range attention
1200
+
1201
+ Args:
1202
+ cam: (H, W) float array in [0, 1]
1203
+ xray_gray: (H, W) uint8 grayscale X-ray matching CAM dimensions
1204
+ alpha: weight of the heatmap (0 = only X-ray, 1 = only heatmap)
1205
+ gamma: gamma correction on the heatmap (<1 brightens mid values)
1206
  """
1207
  import numpy as np
1208
 
1209
+ # Smooth and sharpen the CAM
1210
+ cam = np.clip(cam, 0.0, 1.0)
1211
+ if cam.max() > 0:
1212
+ cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)
1213
+ cam = np.power(cam, gamma) # gamma correction for contrast
1214
+
1215
+ # Jet colormap
1216
+ heatmap_rgb = _cam_to_jet_rgb(cam) # (H, W, 3) uint8
1217
+
1218
+ # Grayscale X-ray RGB
1219
+ xray_rgb = np.stack([xray_gray, xray_gray, xray_gray], axis=-1).astype(np.float32)
1220
+ heatmap_f = heatmap_rgb.astype(np.float32)
1221
+
1222
+ # Alpha blend: composite = alpha * heatmap + (1 - alpha) * xray
1223
+ composite = alpha * heatmap_f + (1.0 - alpha) * xray_rgb
1224
+ composite = np.clip(composite, 0, 255).astype(np.uint8)
1225
+
1226
+ overlay = Image.fromarray(composite, mode="RGB")
 
 
 
 
 
 
 
 
 
 
1227
  buf = io.BytesIO()
1228
+ overlay.save(buf, format="PNG", optimize=True)
1229
  b64 = base64.b64encode(buf.getvalue()).decode("ascii")
1230
  return f"data:image/png;base64,{b64}"
1231
 
 
1291
  cam = _compute_xray_pseudo_cam(model, tensor_cam, top_idx)
1292
  else:
1293
  cam = _compute_xray_gradcam(model, tensor_cam, top_idx)
1294
+
1295
+ # Reconstruct the 224x224 grayscale X-ray for compositing.
1296
+ # torchxrayvision normalizes to [-1024, 1024]; invert the mapping.
1297
+ xray_float = (arr[0] + 1024.0) / 2048.0 * 255.0
1298
+ xray_uint8 = np.clip(xray_float, 0, 255).astype(np.uint8)
1299
+
1300
+ heatmap_b64 = _composite_heatmap_on_xray(
1301
+ cam, xray_uint8, alpha=0.5, gamma=0.7
1302
+ )
1303
  except Exception as cam_err:
1304
  print(f"Heatmap computation failed: {cam_err}")
1305