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 policy: VQA `image_path` and manifest `image_relpath` should be the
293
- # same string, but historical subset-build scripts differ in whether the
294
- # `files/` prefix is included. Build a normalized image_index that maps
295
- # BOTH the original key and its files/-stripped variant back to the
296
- # canonical manifest path. On a lookup miss, also try stripping/adding
297
- # `files/` from the VQA path. This handles the three known formats:
298
- # manifest "files/p10/...", vqa "files/p10/..." → exact match
299
- # manifest "files/p10/...", vqa "p10/..." → +files/ on VQA
300
- # manifest "p10/...", vqa "files/p10/..." → -files/ on VQA
 
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
- def _resolve_vqa_path(p: str) -> Optional[str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- canonical = _resolve_vqa_path(img_path)
 
 
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
- # Loud warning if ALL vqa rows were dropped — this almost always means a
376
- # path-format mismatch the heuristic above didn't cover, e.g. different
377
- # file extension (.dcm vs .jpg) or different study/dicom id encoding.
 
 
 
 
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 mismatch not handled by the heuristic.")
381
- print(f" First 10 unresolved VQA image_paths:")
382
  for p in _dropped_samples:
383
- print(f" {p!r}")
384
- print(f" First 5 manifest image_relpath keys:")
385
- for k in list(image_index.keys())[:5]:
386
- print(f" {k!r}")
387
- print(f" Inspect both sets and adjust _resolve_vqa_path() in "
388
- f"mimic_cxr_resized_builder.py.")
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