Spaces:
Sleeping
Sleeping
unknown commited on
Commit ·
56e0e9b
1
Parent(s): cc93464
feat: full jet colormap composite heatmap (matches medical Grad-CAM style)
Browse files
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
|
| 1165 |
-
"""
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1170 |
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1175 |
"""
|
| 1176 |
import numpy as np
|
| 1177 |
|
| 1178 |
-
#
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
#
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|