convitom commited on
Commit ·
d1ed3ca
1
Parent(s): 3f2d758
data/mimic_cxr_resized_builder.py
CHANGED
|
@@ -289,15 +289,16 @@ def build_mimic_cxr_resized_instruct_json(
|
|
| 289 |
"structured_findings": structured})
|
| 290 |
|
| 291 |
# ── Pass 2: optional VQA attach ─────────────────────────────────────────
|
| 292 |
-
# Match
|
| 293 |
-
#
|
| 294 |
-
# `files/` prefix
|
| 295 |
-
#
|
| 296 |
-
#
|
| 297 |
-
#
|
| 298 |
-
#
|
| 299 |
-
#
|
| 300 |
-
#
|
|
|
|
| 301 |
def _strip_files(p: str) -> str:
|
| 302 |
return p[len("files/"):] if p.startswith("files/") else p
|
| 303 |
|
|
@@ -306,18 +307,40 @@ def build_mimic_cxr_resized_instruct_json(
|
|
| 306 |
norm_index[k] = k # exact
|
| 307 |
norm_index[_strip_files(k)] = k # without files/
|
| 308 |
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
if p in norm_index:
|
| 311 |
return norm_index[p]
|
|
|
|
| 312 |
stripped = _strip_files(p)
|
| 313 |
if stripped in norm_index:
|
| 314 |
return norm_index[stripped]
|
| 315 |
with_prefix = f"files/{p}" if not p.startswith("files/") else p
|
| 316 |
if with_prefix in norm_index:
|
| 317 |
return norm_index[with_prefix]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
return None
|
| 319 |
|
| 320 |
n_vqa = n_vqa_dropped = 0
|
|
|
|
| 321 |
_logged_diag = False
|
| 322 |
_dropped_samples: List[str] = [] # for diagnostic on total drop
|
| 323 |
if vqa_dir:
|
|
@@ -338,22 +361,35 @@ def build_mimic_cxr_resized_instruct_json(
|
|
| 338 |
_idx_samples = list(image_index.keys())[:3]
|
| 339 |
_vqa_samples = [str(r.get("image_path", "")).lstrip("/")
|
| 340 |
for r in rows[:3]]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
print(f" manifest image_relpath samples : {_idx_samples}")
|
| 342 |
print(f" vqa image_path samples : {_vqa_samples}")
|
|
|
|
|
|
|
| 343 |
|
| 344 |
for row in rows:
|
| 345 |
img_path = str(row.get("image_path", "")).lstrip("/")
|
| 346 |
-
|
|
|
|
|
|
|
| 347 |
if canonical is None:
|
| 348 |
n_vqa_dropped += 1
|
| 349 |
if len(_dropped_samples) < 10:
|
| 350 |
-
_dropped_samples.append(img_path)
|
| 351 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
ans = row.get("answer", [])
|
| 353 |
answer = (", ".join(map(str, ans)) if isinstance(ans, list)
|
| 354 |
else str(ans)) or "No."
|
| 355 |
-
subj = str(row.get("subject_id", "")).strip()
|
| 356 |
-
study = str(row.get("study_id", "")).strip()
|
| 357 |
# Abnormality-guided VQA: reuse the manifest's CheXpert PNU
|
| 358 |
# for this study (same context as findings/impression). None
|
| 359 |
# if the study wasn't in any manifest — should not happen
|
|
@@ -372,20 +408,24 @@ def build_mimic_cxr_resized_instruct_json(
|
|
| 372 |
})
|
| 373 |
n_vqa += 1
|
| 374 |
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
if vqa_dir and n_vqa == 0 and n_vqa_dropped > 0:
|
| 379 |
print(f"[mimic_cxr_resized_builder] !! ALL {n_vqa_dropped:,} VQA rows "
|
| 380 |
-
f"dropped — path
|
| 381 |
-
print(f" First 10 unresolved VQA
|
| 382 |
for p in _dropped_samples:
|
| 383 |
-
print(f" {p
|
| 384 |
-
print(f" First 5 manifest
|
| 385 |
-
for k in list(
|
| 386 |
-
print(f" {k
|
| 387 |
-
print(f"
|
| 388 |
-
f"
|
| 389 |
|
| 390 |
# ── Write ───────────────────────────────────────────────────────────────
|
| 391 |
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 289 |
"structured_findings": structured})
|
| 290 |
|
| 291 |
# ── Pass 2: optional VQA attach ─────────────────────────────────────────
|
| 292 |
+
# Match strategy (in order of preference):
|
| 293 |
+
# 1. EXACT path match — image_path == image_relpath (ideal case).
|
| 294 |
+
# 2. FILES/ heuristic — strip/add `files/` prefix on either side.
|
| 295 |
+
# 3. (subj, study) join — when paths are structurally unrelated (e.g.
|
| 296 |
+
# the subset was rebuilt with flat filenames like `Study_N.jpg` but
|
| 297 |
+
# VQA was generated against the ORIGINAL PhysioNet paths). Look up
|
| 298 |
+
# the study's image_relpath via the manifest's (subject_id, study_id)
|
| 299 |
+
# key instead of by path. Always falls back to the study's FIRST
|
| 300 |
+
# available image_relpath — fine when image_mode collapsed multi-view
|
| 301 |
+
# studies to one frontal anyway.
|
| 302 |
def _strip_files(p: str) -> str:
|
| 303 |
return p[len("files/"):] if p.startswith("files/") else p
|
| 304 |
|
|
|
|
| 307 |
norm_index[k] = k # exact
|
| 308 |
norm_index[_strip_files(k)] = k # without files/
|
| 309 |
|
| 310 |
+
# (subj, study) → first image_relpath of that study (after on-disk filter)
|
| 311 |
+
study_to_image: Dict[Tuple[str, str], str] = {}
|
| 312 |
+
# Reverse pass: walk samples we already appended (findings/impression) and
|
| 313 |
+
# extract one image_relpath per study. Faster + more reliable than
|
| 314 |
+
# re-walking manifests because we already know which images survived the
|
| 315 |
+
# on-disk filter. Handles all 3 image_modes: single-image samples carry
|
| 316 |
+
# `image_path`; multi_image_merged samples carry `image_paths` (list).
|
| 317 |
+
for s in samples:
|
| 318 |
+
ip = s.get("image_path") or (
|
| 319 |
+
s["image_paths"][0] if s.get("image_paths") else None
|
| 320 |
+
)
|
| 321 |
+
if ip:
|
| 322 |
+
key = (s["subject_id"].lstrip("p"), s["study_id"].lstrip("s"))
|
| 323 |
+
study_to_image.setdefault(key, ip)
|
| 324 |
+
|
| 325 |
+
def _resolve_vqa_path(p: str, subj: str, study: str) -> Optional[str]:
|
| 326 |
+
# 1) exact
|
| 327 |
if p in norm_index:
|
| 328 |
return norm_index[p]
|
| 329 |
+
# 2) files/ heuristic
|
| 330 |
stripped = _strip_files(p)
|
| 331 |
if stripped in norm_index:
|
| 332 |
return norm_index[stripped]
|
| 333 |
with_prefix = f"files/{p}" if not p.startswith("files/") else p
|
| 334 |
if with_prefix in norm_index:
|
| 335 |
return norm_index[with_prefix]
|
| 336 |
+
# 3) (subj, study) join — robust when paths are unrelated
|
| 337 |
+
key = (subj.lstrip("p"), study.lstrip("s"))
|
| 338 |
+
if key in study_to_image:
|
| 339 |
+
return study_to_image[key]
|
| 340 |
return None
|
| 341 |
|
| 342 |
n_vqa = n_vqa_dropped = 0
|
| 343 |
+
n_resolved_by_path = n_resolved_by_study = 0
|
| 344 |
_logged_diag = False
|
| 345 |
_dropped_samples: List[str] = [] # for diagnostic on total drop
|
| 346 |
if vqa_dir:
|
|
|
|
| 361 |
_idx_samples = list(image_index.keys())[:3]
|
| 362 |
_vqa_samples = [str(r.get("image_path", "")).lstrip("/")
|
| 363 |
for r in rows[:3]]
|
| 364 |
+
_vqa_keys = [(str(r.get("subject_id", "")).strip(),
|
| 365 |
+
str(r.get("study_id", "")).strip())
|
| 366 |
+
for r in rows[:3]]
|
| 367 |
+
_idx_keys = list(study_to_image.keys())[:3]
|
| 368 |
print(f" manifest image_relpath samples : {_idx_samples}")
|
| 369 |
print(f" vqa image_path samples : {_vqa_samples}")
|
| 370 |
+
print(f" manifest (subj,study) samples : {_idx_keys}")
|
| 371 |
+
print(f" vqa (subj,study) samples : {_vqa_keys}")
|
| 372 |
|
| 373 |
for row in rows:
|
| 374 |
img_path = str(row.get("image_path", "")).lstrip("/")
|
| 375 |
+
subj = str(row.get("subject_id", "")).strip()
|
| 376 |
+
study = str(row.get("study_id", "")).strip()
|
| 377 |
+
canonical = _resolve_vqa_path(img_path, subj, study)
|
| 378 |
if canonical is None:
|
| 379 |
n_vqa_dropped += 1
|
| 380 |
if len(_dropped_samples) < 10:
|
| 381 |
+
_dropped_samples.append(f"path={img_path!r} subj={subj!r} study={study!r}")
|
| 382 |
continue
|
| 383 |
+
# Track which strategy resolved it so the log shows the mix.
|
| 384 |
+
if img_path in norm_index or _strip_files(img_path) in norm_index \
|
| 385 |
+
or (f"files/{img_path}" if not img_path.startswith("files/") else img_path) in norm_index:
|
| 386 |
+
n_resolved_by_path += 1
|
| 387 |
+
else:
|
| 388 |
+
n_resolved_by_study += 1
|
| 389 |
+
|
| 390 |
ans = row.get("answer", [])
|
| 391 |
answer = (", ".join(map(str, ans)) if isinstance(ans, list)
|
| 392 |
else str(ans)) or "No."
|
|
|
|
|
|
|
| 393 |
# Abnormality-guided VQA: reuse the manifest's CheXpert PNU
|
| 394 |
# for this study (same context as findings/impression). None
|
| 395 |
# if the study wasn't in any manifest — should not happen
|
|
|
|
| 408 |
})
|
| 409 |
n_vqa += 1
|
| 410 |
|
| 411 |
+
if vqa_dir:
|
| 412 |
+
print(f"[mimic_cxr_resized_builder] VQA resolution: by_path={n_resolved_by_path:,} "
|
| 413 |
+
f"by_(subj,study)={n_resolved_by_study:,} dropped={n_vqa_dropped:,}")
|
| 414 |
+
|
| 415 |
+
# Loud warning if ALL vqa rows were dropped — by now both path AND
|
| 416 |
+
# (subj,study) join failed. Means the manifest and VQA file describe
|
| 417 |
+
# different studies entirely (wrong subset, wrong split, etc.).
|
| 418 |
if vqa_dir and n_vqa == 0 and n_vqa_dropped > 0:
|
| 419 |
print(f"[mimic_cxr_resized_builder] !! ALL {n_vqa_dropped:,} VQA rows "
|
| 420 |
+
f"dropped — neither path nor (subj,study) match anything in the manifest.")
|
| 421 |
+
print(f" First 10 unresolved VQA rows:")
|
| 422 |
for p in _dropped_samples:
|
| 423 |
+
print(f" {p}")
|
| 424 |
+
print(f" First 5 manifest (subj,study) keys:")
|
| 425 |
+
for k in list(study_to_image.keys())[:5]:
|
| 426 |
+
print(f" {k}")
|
| 427 |
+
print(f" Most likely cause: VQA file was generated against a different "
|
| 428 |
+
f"subject/study pool than the current manifest_*.csv.")
|
| 429 |
|
| 430 |
# ── Write ───────────────────────────────────────────────────────────────
|
| 431 |
output_path.parent.mkdir(parents=True, exist_ok=True)
|
scripts/vertex_finetune_vqa_job.yaml
CHANGED
|
@@ -52,7 +52,7 @@ workerPoolSpecs:
|
|
| 52 |
env:
|
| 53 |
# ── Required ────────────────────────────────────────────────────────────
|
| 54 |
- name: HF_TOKEN
|
| 55 |
-
value:
|
| 56 |
- name: DATASET_NAME
|
| 57 |
value: MIMIC-CXR_resized
|
| 58 |
- name: SOURCE_RUN_ID
|
|
|
|
| 52 |
env:
|
| 53 |
# ── Required ────────────────────────────────────────────────────────────
|
| 54 |
- name: HF_TOKEN
|
| 55 |
+
value:
|
| 56 |
- name: DATASET_NAME
|
| 57 |
value: MIMIC-CXR_resized
|
| 58 |
- name: SOURCE_RUN_ID
|