convitom Claude Opus 4.7 commited on
Commit
21fa652
·
1 Parent(s): 47a30f7

chore(data): WIP EDA notebooks + labeler comparison tooling

Browse files

Pre-existing in-progress work (not part of the resize tooling): expanded
EDA notebooks, eval_labelers tweak, new img_stat.py and
labeler_comparison.csv. Committed to clean the working tree.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

data/build_subset_colab.ipynb CHANGED
@@ -3,7 +3,27 @@
3
  {
4
  "cell_type": "markdown",
5
  "metadata": {},
6
- "source": "# Build Subset — PHASE 2 (Google Colab)\n\nChạy **sau** `build_subset_local.ipynb`. Input = `subset_bundle.zip` đã upload lên Drive.\n\n**Việc:**\n1. Giải nén bundle (manifest + reports + vqa)\n2. Tải ~50k ảnh JPG từ PhysioNet vào **đúng path gốc** `files/pXX/pSUBJ/sSTUDY/<dicom>.jpg` (resume — đứt thì chạy lại tiếp)\n3. Copy reports (giữ path gốc) + vqa.json vào package\n4. Push Hugging Face\n\n**Cấu trúc kết quả** (`MIMIC-CXR_processed/`):\n```\nfiles/pXX/pSUBJ/sSTUDY/<dicom>.jpg ← ảnh (giữ tên gốc)\nfiles/pXX/pSUBJ/sSTUDY.txt ← report\nmanifest_{train,val,test}.json/.csv ← split + nhãn (đối chiếu khi tải đứt)\nvqa.json / vqa_val.json / vqa_test.json\n```\n\n> Không cần GPU. Ảnh tải thẳng vào Drive → resume an toàn khi Colab ngắt; đối chiếu manifest để biết còn thiếu study/ảnh nào."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  },
8
  {
9
  "cell_type": "markdown",
@@ -16,6 +36,7 @@
16
  "cell_type": "code",
17
  "execution_count": null,
18
  "metadata": {},
 
19
  "source": [
20
  "import sys, os\n",
21
  "IN_COLAB = \"google.colab\" in sys.modules\n",
@@ -24,15 +45,63 @@
24
  " drive.mount(\"/content/drive\")\n",
25
  " !pip -q install huggingface_hub tqdm\n",
26
  "print(\"IN_COLAB =\", IN_COLAB)"
27
- ],
28
- "outputs": []
29
  },
30
  {
31
  "cell_type": "code",
32
  "execution_count": null,
 
33
  "metadata": {},
34
- "source": "from pathlib import Path\nimport os, getpass, zipfile, json, time, shutil\n\nDRIVE = Path(\"/content/drive/MyDrive\")\nBUNDLE_ZIP = DRIVE / \"subset_bundle.zip\"\nBUNDLE_DIR = Path(\"/content/subset_bundle\")\nOUT = DRIVE / \"MIMIC-CXR_processed\"\n\nHF_REPO_ID = \"hieu3636/cxr-vlm-data\"\nHF_REPO_TYPE = \"dataset\"\nHF_PATH_IN_REPO = \"MIMIC-CXR_processed\"\n\n# ── CREDENTIALS ──────────────────────────────────────────────────────────────\n# Cách 1 (KHUYẾN NGHỊ, an toàn + không hỏi lại): Colab Secrets.\n# Bấm icon CHÌA KHOÁ 🔑 cột trái -> Add new secret, tạo 3 secret:\n# PHYSIONET_USER , PHYSIONET_PASS , HF_TOKEN (bật \"Notebook access\")\n# -> set 1 lần, dùng mãi mọi session, KHÔNG nằm trong code.\n#\n# Cách 2 (bạn muốn): gõ thẳng vào đây. Nhanh nhưng LỘ nếu push/chia sẻ notebook.\n# -> điền vào 3 dòng _HARDCODE_* bên dưới.\n#\n# Cách 3: để trống tất cả -> nó hỏi nhập tay khi chạy (như cũ).\n\n_HARDCODE_USER = \"\" # <- điền PhysioNet username (vd \"convitom\")\n_HARDCODE_PASS = \"\" # <- điền PhysioNet password\n_HARDCODE_HFTOK = \"\" # <- điền HF write token\n\ndef _get(name, hard):\n if hard: # ưu tiên giá trị gõ thẳng\n return hard\n try: # rồi tới Colab Secrets\n from google.colab import userdata\n v = userdata.get(name)\n if v:\n return v\n except Exception:\n pass\n return os.environ.get(name) # rồi tới biến môi trường\n\nPHYSIONET_USER = _get(\"PHYSIONET_USER\", _HARDCODE_USER) or input(\"PhysioNet username: \")\nPHYSIONET_PASS = _get(\"PHYSIONET_PASS\", _HARDCODE_PASS) or getpass.getpass(\"PhysioNet password: \")\nHF_TOKEN = _get(\"HF_TOKEN\", _HARDCODE_HFTOK) or getpass.getpass(\"HF write token: \")\n\nVQA_OUT = {\"train\":\"vqa.json\",\"val\":\"vqa_val.json\",\"test\":\"vqa_test.json\"}\nOUT.mkdir(parents=True, exist_ok=True)\nprint(\"Credentials OK |\", \"bundle zip exists:\", BUNDLE_ZIP.exists())\nprint(\"⚠️ Nếu gõ thẳng pass/token vào code: ĐỪNG commit/push notebook này lên git/HF.\")",
35
- "outputs": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  },
37
  {
38
  "cell_type": "markdown",
@@ -45,6 +114,7 @@
45
  "cell_type": "code",
46
  "execution_count": null,
47
  "metadata": {},
 
48
  "source": [
49
  "BUNDLE_DIR.mkdir(parents=True, exist_ok=True)\n",
50
  "with zipfile.ZipFile(BUNDLE_ZIP) as z:\n",
@@ -56,46 +126,196 @@
56
  " print(f\"{sp}: {len(r):,} studies\")\n",
57
  "all_rows = manifests[\"train\"]+manifests[\"val\"]+manifests[\"test\"]\n",
58
  "print(\"TOTAL ảnh cần tải:\", len(all_rows))"
59
- ],
60
- "outputs": []
61
  },
62
  {
63
  "cell_type": "markdown",
64
  "id": "918e0272",
65
- "source": "## 2. Tải ảnh JPG từ PhysioNet (qua `wget`) → thẳng vào package trên Drive\n\nPhysioNet từ chối `requests` basic-auth nhưng chấp nhận `wget` → dùng `wget` per-file, 12 luồng song song.\n\nĐứt giữa chừng → reconnect → chạy lại cell này, file đã tải (>10KB) được bỏ qua.",
66
  "metadata": {},
67
- "execution_count": null,
68
- "outputs": []
 
 
 
 
 
69
  },
70
  {
71
  "cell_type": "code",
72
- "metadata": {},
73
- "source": "import subprocess, threading, shutil\nfrom pathlib import Path as _P\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom collections import Counter\nfrom tqdm.auto import tqdm\n\n# Log per-file ghi vào /content (SSD, ~µs, không bóp tải).\n# Cứ mỗi CHECKPOINT_EVERY ảnh -> copy log sang Drive (1 thao tác, rẻ).\n# Session mới: đọc log Drive (tức thì) thay vì os.walk (chậm).\nDL_LOG_LOCAL = _P(\"/content/downloaded.txt\")\nDL_LOG_DRIVE = OUT / \"downloaded.txt\"\nCHECKPOINT_EVERY = 500\n\n_log_lk = threading.Lock()\n_log_cnt = 0\n\ndef mark_done(relpath):\n global _log_cnt\n with _log_lk:\n with open(DL_LOG_LOCAL, \"a\") as f:\n f.write(relpath + \"\\n\")\n _log_cnt += 1\n if _log_cnt % CHECKPOINT_EVERY == 0:\n try:\n shutil.copy(DL_LOG_LOCAL, DL_LOG_DRIVE) # checkpoint -> Drive\n except Exception as e:\n print(\" [warn] copy log -> Drive lỗi:\", e)\n\ndef flush_log_to_drive():\n with _log_lk:\n if DL_LOG_LOCAL.exists():\n shutil.copy(DL_LOG_LOCAL, DL_LOG_DRIVE)\n\n# PhysioNet chặn requests-basic-auth nhưng OK với wget. Cell này CHỈ định nghĩa dl().\ndef dl(row):\n rp = row[\"image_relpath\"]\n out = OUT / rp # files/pXX/pSUBJ/sSTUDY/<dicom>.jpg\n if out.exists() and out.stat().st_size > 10_000:\n mark_done(rp)\n return \"skip\"\n out.parent.mkdir(parents=True, exist_ok=True)\n tmp = out.with_suffix(\".part\")\n cmd = [\"wget\", \"-q\", \"-T\", \"60\", \"-t\", \"3\", \"-O\", str(tmp),\n \"--user\", PHYSIONET_USER, \"--password\", PHYSIONET_PASS, row[\"jpg_url\"]]\n rc = subprocess.run(cmd).returncode\n if rc == 0 and tmp.exists() and tmp.stat().st_size > 10_000:\n tmp.replace(out)\n mark_done(rp)\n return \"ok\"\n if tmp.exists():\n tmp.unlink()\n return f\"fail(rc={rc})\"\n\nprint(f\"dl() sẵn sàng. Log local={DL_LOG_LOCAL}, checkpoint -> {DL_LOG_DRIVE} mỗi {CHECKPOINT_EVERY} ảnh.\")",
74
  "execution_count": null,
75
- "outputs": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  },
77
  {
78
  "cell_type": "code",
 
79
  "id": "a78d23a9",
80
- "source": "# ── TEST NHANH + ĐO TỐC ĐỘ trước khi tải 50k ─────────────────────────────────\n# Lần đầu: tải thật 10 ảnh để đo tốc độ + xác nhận auth.\n# Khi RESUME (Run all lại): ảnh đã có -> bỏ qua, không phí thời gian.\nimport time as _t\nsample = all_rows[:10]\nneed = [r for r in sample\n if not ((OUT/r[\"image_relpath\"]).exists()\n and (OUT/r[\"image_relpath\"]).stat().st_size > 10_000)]\n\nif not need:\n print(f\"{len(sample)}/{len(sample)} ảnh test đã có sẵn (resume) — bỏ qua test.\")\n print(\"✓ Chạy cell TẢI 50k bên dưới.\")\nelse:\n print(f\"Test tải {len(need)} ảnh (đo tốc độ)...\")\n t0 = _t.time(); tot = 0; ok = 0\n for row in need:\n st = dl(row)\n p = OUT/row[\"image_relpath\"]\n if p.exists() and p.stat().st_size > 10_000:\n tot += p.stat().st_size; ok += 1\n print(f\" {row['study_name']:>10s} -> {st}\")\n dt = _t.time() - t0\n kbs = (tot/1024)/dt if dt > 0 else 0\n avg = (tot/1e6)/ok if ok else 0\n print(f\"\\n{ok}/{len(need)} OK | {tot/1e6:.1f} MB / {dt:.1f}s \"\n f\"= {kbs:,.0f} KB/s ({kbs/1024:.2f} MB/s) [1 luồng]\")\n if ok:\n n = len(all_rows)\n h = (n*avg)/(kbs/1024)/3600\n print(f\"Ảnh ~{avg:.2f} MB | {n:,} ảnh ≈ {n*avg/1000:.0f} GB\")\n print(f\"ETA: ~{h:.1f}h (1 luồng) → ~{h/12*1.6:.1f}-{h/12*3:.1f}h (12 luồng)\")\n print(\"\\n\" + (\"✓ OK — chạy cell TẢI 50k bên dưới.\" if ok == len(need)\n else \"✗ Lỗi — kiểm tra user/pass, ĐỪNG chạy tải 50k.\"))",
81
  "metadata": {},
82
- "execution_count": null,
83
- "outputs": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  },
85
  {
86
  "cell_type": "code",
 
87
  "id": "0f8b3dec",
88
- "source": "# ── TẢI ẢNH (chỉ chạy khi cell TEST báo ✓ OK) ────────────────────────────────\n# Resume ưu tiên: log local -> log Drive (copy về local) -> os.walk (fallback).\nif DL_LOG_LOCAL.exists():\n done_set = set(l.strip() for l in open(DL_LOG_LOCAL) if l.strip())\n print(f\"[log local] {len(done_set):,} ảnh đã tải (tức thì).\")\nelif DL_LOG_DRIVE.exists():\n shutil.copy(DL_LOG_DRIVE, DL_LOG_LOCAL) # session mới: lấy checkpoint từ Drive\n done_set = set(l.strip() for l in open(DL_LOG_LOCAL) if l.strip())\n print(f\"[log Drive] session mới, đọc checkpoint: {len(done_set):,} ảnh (tức thì).\")\nelse:\n print(\"Chưa có log -> quét Drive 1 lần để dựng log...\")\n done_set = set()\n froot = OUT / \"files\"\n if froot.exists():\n for dp, _, fns in os.walk(froot):\n for fn in fns:\n if fn.endswith(\".jpg\"):\n done_set.add(os.path.relpath(os.path.join(dp, fn), OUT).replace(\"\\\\\", \"/\"))\n with open(DL_LOG_LOCAL, \"w\") as f:\n f.write(\"\\n\".join(sorted(done_set)) + (\"\\n\" if done_set else \"\"))\n if done_set:\n shutil.copy(DL_LOG_LOCAL, DL_LOG_DRIVE)\n print(f\"Dựng log xong: {len(done_set):,} ảnh.\")\n\ntodo = [r for r in all_rows if r[\"image_relpath\"] not in done_set]\nn_done = len(all_rows) - len(todo)\nprint(f\"Đã có : {n_done:,} / {len(all_rows):,} ({n_done/len(all_rows)*100:.1f}%)\")\nprint(f\"Cần tải: {len(todo):,}\")\n\nif not todo:\n print(\"\\n✓ Đã tải đủ toàn bộ.\")\n flush_log_to_drive()\nelse:\n res = Counter({\"skip\": n_done})\n try:\n with ThreadPoolExecutor(max_workers=12) as ex:\n futs = [ex.submit(dl, r) for r in todo]\n for f in tqdm(as_completed(futs), total=len(todo), desc=\"downloading\"):\n res[f.result().split(\"(\")[0]] += 1\n finally:\n flush_log_to_drive() # luôn lưu log Drive khi kết thúc/lỗi\n print(dict(res))\n if any(k.startswith(\"fail\") for k in res):\n print(\"Còn fail -> chạy lại cell này (chỉ tải phần thiếu).\")",
89
  "metadata": {},
90
- "execution_count": null,
91
- "outputs": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  },
93
  {
94
  "cell_type": "code",
95
  "execution_count": null,
96
  "metadata": {},
97
- "source": "# Kiểm tra còn thiếu ảnh nào không (đối chiếu manifest). Còn thì chạy lại cell tải.\nmiss = {sp: [] for sp in (\"train\",\"val\",\"test\")}\nfor row in all_rows:\n p = OUT / row[\"image_relpath\"]\n if not (p.exists() and p.stat().st_size > 0):\n miss[row[\"split\"]].append(row[\"study_name\"])\nprint(\"ảnh còn thiếu:\", {k: len(v) for k, v in miss.items()},\n \"| tổng:\", sum(len(v) for v in miss.values()))\n# In vài study còn thiếu để đối chiếu\nfor sp, names in miss.items():\n if names:\n print(f\" [{sp}] thiếu {len(names)}, vd: {names[:5]}\")",
98
- "outputs": []
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  },
100
  {
101
  "cell_type": "markdown",
@@ -108,27 +328,76 @@
108
  "cell_type": "code",
109
  "execution_count": null,
110
  "metadata": {},
111
- "source": "# Copy reports (giữ path gốc) + vqa + manifest vào package\n# reports: bundle/reports/files/pXX/.../sSTUDY.txt -> OUT/files/pXX/.../sSTUDY.txt\nshutil.copytree(BUNDLE_DIR/\"reports\", OUT, dirs_exist_ok=True)\n\nfor sp in (\"train\",\"val\",\"test\"):\n shutil.copy(BUNDLE_DIR/\"vqa\"/VQA_OUT[sp], OUT/VQA_OUT[sp])\n shutil.copy(BUNDLE_DIR/f\"manifest_{sp}.json\", OUT/f\"manifest_{sp}.json\")\n src_csv = BUNDLE_DIR/f\"manifest_{sp}.csv\"\n if src_csv.exists():\n shutil.copy(src_csv, OUT/f\"manifest_{sp}.csv\")\n nv = len(json.load(open(OUT/VQA_OUT[sp], encoding=\"utf-8\")))\n print(f\"{sp}: vqa={nv:,} manifest copied\")\n\nn_rep = sum(1 for _ in (OUT/'files').rglob('s*.txt'))\nprint(f\"reports trong package: {n_rep:,}\")",
112
- "outputs": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  },
114
  {
115
  "cell_type": "code",
116
  "execution_count": null,
117
  "metadata": {},
118
- "source": "# Sanity check cuối — đếm theo manifest (image + report tồn tại thực sự)\nfor sp in (\"train\",\"val\",\"test\"):\n rows = manifests[sp]\n ni = sum(1 for x in rows if (OUT/x[\"image_relpath\"]).exists())\n nr = sum(1 for x in rows if (OUT/x[\"report_relpath\"]).exists())\n print(f\"{sp:5s} manifest={len(rows):,} images_ok={ni:,} reports_ok={nr:,}\")\nprint(\"\\nPackage:\", OUT)",
119
- "outputs": []
 
 
 
 
 
 
 
 
120
  },
121
  {
122
  "cell_type": "markdown",
123
  "metadata": {},
124
- "source": "## 4. Upload lên Hugging Face\n\nPush vào repo **có sẵn** `hieu3636/cxr-vlm-data`, nằm trong thư mục con `MIMIC-CXR_processed/` (dùng `path_in_repo`, không tạo repo mới)."
 
 
 
 
125
  },
126
  {
127
  "cell_type": "code",
128
  "execution_count": null,
129
  "metadata": {},
130
- "source": "RUN_HF = False # ← bật khi sẵn sàng push\nif RUN_HF:\n from huggingface_hub import HfApi\n api = HfApi(token=HF_TOKEN)\n # repo đã tồn tại sẵn -> exist_ok=True chỉ no-op, không tạo mới\n api.create_repo(HF_REPO_ID, repo_type=HF_REPO_TYPE, exist_ok=True)\n # upload_folder hỗ trợ path_in_repo -> đẩy vào thư mục con MIMIC-CXR_processed\n # (chạy lại nếu đứt: file đã có trên repo được bỏ qua theo hash)\n api.upload_folder(\n repo_id = HF_REPO_ID,\n repo_type = HF_REPO_TYPE,\n folder_path = str(OUT),\n path_in_repo = HF_PATH_IN_REPO,\n commit_message = \"Add MIMIC-CXR_processed subset\",\n )\n print(\"pushed →\",\n f\"https://huggingface.co/{HF_REPO_TYPE}s/{HF_REPO_ID}/tree/main/{HF_PATH_IN_REPO}\")\nelse:\n print(\"RUN_HF=False — bật True để push vào\",\n f\"{HF_REPO_ID}/{HF_PATH_IN_REPO}\")",
131
- "outputs": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  },
133
  {
134
  "cell_type": "markdown",
@@ -143,15 +412,36 @@
143
  "cell_type": "code",
144
  "execution_count": null,
145
  "metadata": {},
146
- "source": "RUN_ZIP = False\nif RUN_ZIP:\n # zip cả package (giữ cấu trúc gốc) thành 1 file\n shutil.make_archive(\"/content/MIMIC-CXR_processed\", \"zip\", OUT)\n shutil.copy(\"/content/MIMIC-CXR_processed.zip\", DRIVE/\"MIMIC-CXR_processed.zip\")\n print(\"zipped -> Drive/MIMIC-CXR_processed.zip\")\nelse:\n print(\"RUN_ZIP=False\")",
147
- "outputs": []
 
 
 
 
 
 
 
 
 
148
  },
149
  {
150
  "cell_type": "code",
151
  "execution_count": null,
152
  "metadata": {},
153
- "source": "print(\"=\"*54)\nprint(\" PHASE 2 (COLAB) DONE\")\nprint(\"=\"*54)\nfor sp in (\"train\",\"val\",\"test\"):\n rows=manifests[sp]\n ni=sum(1 for x in rows if (OUT/x[\"image_relpath\"]).exists())\n print(f\" {sp:5s} images={ni:,}/{len(rows):,}\")\nprint(f\" package: {OUT}\")\nprint(\" cấu trúc gốc: files/pXX/pSUBJ/sSTUDY/<dicom>.jpg + .txt\")\nprint(\" Flags: RUN_HF / RUN_ZIP (mặc định False)\")\nprint(\"=\"*54)",
154
- "outputs": []
 
 
 
 
 
 
 
 
 
 
 
 
155
  }
156
  ],
157
  "metadata": {
@@ -167,4 +457,4 @@
167
  },
168
  "nbformat": 4,
169
  "nbformat_minor": 5
170
- }
 
3
  {
4
  "cell_type": "markdown",
5
  "metadata": {},
6
+ "source": [
7
+ "# Build Subset — PHASE 2 (Google Colab)\n",
8
+ "\n",
9
+ "Chạy **sau** `build_subset_local.ipynb`. Input = `subset_bundle.zip` đã upload lên Drive.\n",
10
+ "\n",
11
+ "**Việc:**\n",
12
+ "1. Giải nén bundle (manifest + reports + vqa)\n",
13
+ "2. Tải ~50k ảnh JPG từ PhysioNet vào **đúng path gốc** `files/pXX/pSUBJ/sSTUDY/<dicom>.jpg` (resume — đứt thì chạy lại tiếp)\n",
14
+ "3. Copy reports (giữ path gốc) + vqa.json vào package\n",
15
+ "4. Push Hugging Face\n",
16
+ "\n",
17
+ "**Cấu trúc kết quả** (`MIMIC-CXR_processed/`):\n",
18
+ "```\n",
19
+ "files/pXX/pSUBJ/sSTUDY/<dicom>.jpg ← ảnh (giữ tên gốc)\n",
20
+ "files/pXX/pSUBJ/sSTUDY.txt ← report\n",
21
+ "manifest_{train,val,test}.json/.csv ← split + nhãn (đối chiếu khi tải đứt)\n",
22
+ "vqa.json / vqa_val.json / vqa_test.json\n",
23
+ "```\n",
24
+ "\n",
25
+ "> Không cần GPU. Ảnh tải thẳng vào Drive → resume an toàn khi Colab ngắt; đối chiếu manifest để biết còn thiếu study/ảnh nào."
26
+ ]
27
  },
28
  {
29
  "cell_type": "markdown",
 
36
  "cell_type": "code",
37
  "execution_count": null,
38
  "metadata": {},
39
+ "outputs": [],
40
  "source": [
41
  "import sys, os\n",
42
  "IN_COLAB = \"google.colab\" in sys.modules\n",
 
45
  " drive.mount(\"/content/drive\")\n",
46
  " !pip -q install huggingface_hub tqdm\n",
47
  "print(\"IN_COLAB =\", IN_COLAB)"
48
+ ]
 
49
  },
50
  {
51
  "cell_type": "code",
52
  "execution_count": null,
53
+ "id": "08b13eef",
54
  "metadata": {},
55
+ "outputs": [],
56
+ "source": [
57
+ "from pathlib import Path\n",
58
+ "import os, getpass, zipfile, json, time, shutil\n",
59
+ "\n",
60
+ "DRIVE = Path(\"/content/drive/MyDrive\")\n",
61
+ "BUNDLE_ZIP = DRIVE / \"subset_bundle.zip\"\n",
62
+ "BUNDLE_DIR = Path(\"/content/subset_bundle\")\n",
63
+ "OUT = DRIVE / \"MIMIC-CXR_processed\"\n",
64
+ "\n",
65
+ "HF_REPO_ID = \"hieu3636/cxr-vlm-data\"\n",
66
+ "HF_REPO_TYPE = \"dataset\"\n",
67
+ "HF_PATH_IN_REPO = \"MIMIC-CXR_processed\"\n",
68
+ "\n",
69
+ "# ── CREDENTIALS ──────────────────────────────────────────────────────────────\n",
70
+ "# Cách 1 (KHUYẾN NGHỊ, an toàn + không hỏi lại): Colab Secrets.\n",
71
+ "# Bấm icon CHÌA KHOÁ 🔑 cột trái -> Add new secret, tạo 3 secret:\n",
72
+ "# PHYSIONET_USER , PHYSIONET_PASS , HF_TOKEN (bật \"Notebook access\")\n",
73
+ "# -> set 1 lần, dùng mãi mọi session, KHÔNG nằm trong code.\n",
74
+ "#\n",
75
+ "# Cách 2 (bạn muốn): gõ thẳng vào đây. Nhanh nhưng LỘ nếu push/chia sẻ notebook.\n",
76
+ "# -> điền vào 3 dòng _HARDCODE_* bên dưới.\n",
77
+ "#\n",
78
+ "# Cách 3: để trống tất cả -> nó hỏi nhập tay khi chạy (như cũ).\n",
79
+ "\n",
80
+ "_HARDCODE_USER = \"\" # <- điền PhysioNet username (vd \"convitom\")\n",
81
+ "_HARDCODE_PASS = \"\" # <- điền PhysioNet password\n",
82
+ "_HARDCODE_HFTOK = \"\" # <- điền HF write token\n",
83
+ "\n",
84
+ "def _get(name, hard):\n",
85
+ " if hard: # ưu tiên giá trị gõ thẳng\n",
86
+ " return hard\n",
87
+ " try: # rồi tới Colab Secrets\n",
88
+ " from google.colab import userdata\n",
89
+ " v = userdata.get(name)\n",
90
+ " if v:\n",
91
+ " return v\n",
92
+ " except Exception:\n",
93
+ " pass\n",
94
+ " return os.environ.get(name) # rồi tới biến môi trường\n",
95
+ "\n",
96
+ "PHYSIONET_USER = _get(\"PHYSIONET_USER\", _HARDCODE_USER) or input(\"PhysioNet username: \")\n",
97
+ "PHYSIONET_PASS = _get(\"PHYSIONET_PASS\", _HARDCODE_PASS) or getpass.getpass(\"PhysioNet password: \")\n",
98
+ "HF_TOKEN = _get(\"HF_TOKEN\", _HARDCODE_HFTOK) or getpass.getpass(\"HF write token: \")\n",
99
+ "\n",
100
+ "VQA_OUT = {\"train\":\"vqa.json\",\"val\":\"vqa_val.json\",\"test\":\"vqa_test.json\"}\n",
101
+ "OUT.mkdir(parents=True, exist_ok=True)\n",
102
+ "print(\"Credentials OK |\", \"bundle zip exists:\", BUNDLE_ZIP.exists())\n",
103
+ "print(\"⚠️ Nếu gõ thẳng pass/token vào code: ĐỪNG commit/push notebook này lên git/HF.\")"
104
+ ]
105
  },
106
  {
107
  "cell_type": "markdown",
 
114
  "cell_type": "code",
115
  "execution_count": null,
116
  "metadata": {},
117
+ "outputs": [],
118
  "source": [
119
  "BUNDLE_DIR.mkdir(parents=True, exist_ok=True)\n",
120
  "with zipfile.ZipFile(BUNDLE_ZIP) as z:\n",
 
126
  " print(f\"{sp}: {len(r):,} studies\")\n",
127
  "all_rows = manifests[\"train\"]+manifests[\"val\"]+manifests[\"test\"]\n",
128
  "print(\"TOTAL ảnh cần tải:\", len(all_rows))"
129
+ ]
 
130
  },
131
  {
132
  "cell_type": "markdown",
133
  "id": "918e0272",
 
134
  "metadata": {},
135
+ "source": [
136
+ "## 2. Tải ảnh JPG từ PhysioNet (qua `wget`) → thẳng vào package trên Drive\n",
137
+ "\n",
138
+ "PhysioNet từ chối `requests` basic-auth nhưng chấp nhận `wget` → dùng `wget` per-file, 12 luồng song song.\n",
139
+ "\n",
140
+ "Đứt giữa chừng → reconnect → chạy lại cell này, file đã tải (>10KB) được bỏ qua."
141
+ ]
142
  },
143
  {
144
  "cell_type": "code",
 
 
145
  "execution_count": null,
146
+ "metadata": {},
147
+ "outputs": [],
148
+ "source": [
149
+ "import subprocess, threading, shutil\n",
150
+ "from pathlib import Path as _P\n",
151
+ "from concurrent.futures import ThreadPoolExecutor, as_completed\n",
152
+ "from collections import Counter\n",
153
+ "from tqdm.auto import tqdm\n",
154
+ "\n",
155
+ "# Log per-file ghi vào /content (SSD, ~µs, không bóp tải).\n",
156
+ "# Cứ mỗi CHECKPOINT_EVERY ảnh -> copy log sang Drive (1 thao tác, rẻ).\n",
157
+ "# Session mới: đọc log Drive (tức thì) thay vì os.walk (chậm).\n",
158
+ "DL_LOG_LOCAL = _P(\"/content/downloaded.txt\")\n",
159
+ "DL_LOG_DRIVE = OUT / \"downloaded.txt\"\n",
160
+ "CHECKPOINT_EVERY = 500\n",
161
+ "\n",
162
+ "_log_lk = threading.Lock()\n",
163
+ "_log_cnt = 0\n",
164
+ "\n",
165
+ "def mark_done(relpath):\n",
166
+ " global _log_cnt\n",
167
+ " with _log_lk:\n",
168
+ " with open(DL_LOG_LOCAL, \"a\") as f:\n",
169
+ " f.write(relpath + \"\\n\")\n",
170
+ " _log_cnt += 1\n",
171
+ " if _log_cnt % CHECKPOINT_EVERY == 0:\n",
172
+ " try:\n",
173
+ " shutil.copy(DL_LOG_LOCAL, DL_LOG_DRIVE) # checkpoint -> Drive\n",
174
+ " except Exception as e:\n",
175
+ " print(\" [warn] copy log -> Drive lỗi:\", e)\n",
176
+ "\n",
177
+ "def flush_log_to_drive():\n",
178
+ " with _log_lk:\n",
179
+ " if DL_LOG_LOCAL.exists():\n",
180
+ " shutil.copy(DL_LOG_LOCAL, DL_LOG_DRIVE)\n",
181
+ "\n",
182
+ "# PhysioNet chặn requests-basic-auth nhưng OK với wget. Cell này CHỈ định nghĩa dl().\n",
183
+ "def dl(row):\n",
184
+ " rp = row[\"image_relpath\"]\n",
185
+ " out = OUT / rp # files/pXX/pSUBJ/sSTUDY/<dicom>.jpg\n",
186
+ " if out.exists() and out.stat().st_size > 10_000:\n",
187
+ " mark_done(rp)\n",
188
+ " return \"skip\"\n",
189
+ " out.parent.mkdir(parents=True, exist_ok=True)\n",
190
+ " tmp = out.with_suffix(\".part\")\n",
191
+ " cmd = [\"wget\", \"-q\", \"-T\", \"60\", \"-t\", \"3\", \"-O\", str(tmp),\n",
192
+ " \"--user\", PHYSIONET_USER, \"--password\", PHYSIONET_PASS, row[\"jpg_url\"]]\n",
193
+ " rc = subprocess.run(cmd).returncode\n",
194
+ " if rc == 0 and tmp.exists() and tmp.stat().st_size > 10_000:\n",
195
+ " tmp.replace(out)\n",
196
+ " mark_done(rp)\n",
197
+ " return \"ok\"\n",
198
+ " if tmp.exists():\n",
199
+ " tmp.unlink()\n",
200
+ " return f\"fail(rc={rc})\"\n",
201
+ "\n",
202
+ "print(f\"dl() sẵn sàng. Log local={DL_LOG_LOCAL}, checkpoint -> {DL_LOG_DRIVE} mỗi {CHECKPOINT_EVERY} ảnh.\")"
203
+ ]
204
  },
205
  {
206
  "cell_type": "code",
207
+ "execution_count": null,
208
  "id": "a78d23a9",
 
209
  "metadata": {},
210
+ "outputs": [],
211
+ "source": [
212
+ "# ── TEST NHANH + ĐO TỐC ĐỘ trước khi tải 50k ─────────────────────────────────\n",
213
+ "# Lần đầu: tải thật 10 ảnh để đo tốc độ + xác nhận auth.\n",
214
+ "# Khi RESUME (Run all lại): ảnh đã có -> bỏ qua, không phí thời gian.\n",
215
+ "import time as _t\n",
216
+ "sample = all_rows[:10]\n",
217
+ "need = [r for r in sample\n",
218
+ " if not ((OUT/r[\"image_relpath\"]).exists()\n",
219
+ " and (OUT/r[\"image_relpath\"]).stat().st_size > 10_000)]\n",
220
+ "\n",
221
+ "if not need:\n",
222
+ " print(f\"{len(sample)}/{len(sample)} ảnh test đã có sẵn (resume) — bỏ qua test.\")\n",
223
+ " print(\"✓ Chạy cell TẢI 50k bên dưới.\")\n",
224
+ "else:\n",
225
+ " print(f\"Test tải {len(need)} ảnh (đo tốc độ)...\")\n",
226
+ " t0 = _t.time(); tot = 0; ok = 0\n",
227
+ " for row in need:\n",
228
+ " st = dl(row)\n",
229
+ " p = OUT/row[\"image_relpath\"]\n",
230
+ " if p.exists() and p.stat().st_size > 10_000:\n",
231
+ " tot += p.stat().st_size; ok += 1\n",
232
+ " print(f\" {row['study_name']:>10s} -> {st}\")\n",
233
+ " dt = _t.time() - t0\n",
234
+ " kbs = (tot/1024)/dt if dt > 0 else 0\n",
235
+ " avg = (tot/1e6)/ok if ok else 0\n",
236
+ " print(f\"\\n{ok}/{len(need)} OK | {tot/1e6:.1f} MB / {dt:.1f}s \"\n",
237
+ " f\"= {kbs:,.0f} KB/s ({kbs/1024:.2f} MB/s) [1 luồng]\")\n",
238
+ " if ok:\n",
239
+ " n = len(all_rows)\n",
240
+ " h = (n*avg)/(kbs/1024)/3600\n",
241
+ " print(f\"Ảnh ~{avg:.2f} MB | {n:,} ảnh ≈ {n*avg/1000:.0f} GB\")\n",
242
+ " print(f\"ETA: ~{h:.1f}h (1 luồng) → ~{h/12*1.6:.1f}-{h/12*3:.1f}h (12 luồng)\")\n",
243
+ " print(\"\\n\" + (\"✓ OK — chạy cell TẢI 50k bên dưới.\" if ok == len(need)\n",
244
+ " else \"✗ Lỗi — kiểm tra user/pass, ĐỪNG chạy tải 50k.\"))"
245
+ ]
246
  },
247
  {
248
  "cell_type": "code",
249
+ "execution_count": null,
250
  "id": "0f8b3dec",
 
251
  "metadata": {},
252
+ "outputs": [],
253
+ "source": [
254
+ "# ── TẢI ẢNH (chỉ chạy khi cell TEST báo ✓ OK) ────────────────────────────────\n",
255
+ "# Resume ưu tiên: log local -> log Drive (copy về local) -> os.walk (fallback).\n",
256
+ "if DL_LOG_LOCAL.exists():\n",
257
+ " done_set = set(l.strip() for l in open(DL_LOG_LOCAL) if l.strip())\n",
258
+ " print(f\"[log local] {len(done_set):,} ảnh đã tải (tức thì).\")\n",
259
+ "elif DL_LOG_DRIVE.exists():\n",
260
+ " shutil.copy(DL_LOG_DRIVE, DL_LOG_LOCAL) # session mới: lấy checkpoint từ Drive\n",
261
+ " done_set = set(l.strip() for l in open(DL_LOG_LOCAL) if l.strip())\n",
262
+ " print(f\"[log Drive] session mới, đọc checkpoint: {len(done_set):,} ảnh (tức thì).\")\n",
263
+ "else:\n",
264
+ " print(\"Chưa có log -> quét Drive 1 lần để dựng log...\")\n",
265
+ " done_set = set()\n",
266
+ " froot = OUT / \"files\"\n",
267
+ " if froot.exists():\n",
268
+ " for dp, _, fns in os.walk(froot):\n",
269
+ " for fn in fns:\n",
270
+ " if fn.endswith(\".jpg\"):\n",
271
+ " done_set.add(os.path.relpath(os.path.join(dp, fn), OUT).replace(\"\\\\\", \"/\"))\n",
272
+ " with open(DL_LOG_LOCAL, \"w\") as f:\n",
273
+ " f.write(\"\\n\".join(sorted(done_set)) + (\"\\n\" if done_set else \"\"))\n",
274
+ " if done_set:\n",
275
+ " shutil.copy(DL_LOG_LOCAL, DL_LOG_DRIVE)\n",
276
+ " print(f\"Dựng log xong: {len(done_set):,} ảnh.\")\n",
277
+ "\n",
278
+ "todo = [r for r in all_rows if r[\"image_relpath\"] not in done_set]\n",
279
+ "n_done = len(all_rows) - len(todo)\n",
280
+ "print(f\"Đã có : {n_done:,} / {len(all_rows):,} ({n_done/len(all_rows)*100:.1f}%)\")\n",
281
+ "print(f\"Cần tải: {len(todo):,}\")\n",
282
+ "\n",
283
+ "if not todo:\n",
284
+ " print(\"\\n✓ Đã tải đủ toàn bộ.\")\n",
285
+ " flush_log_to_drive()\n",
286
+ "else:\n",
287
+ " res = Counter({\"skip\": n_done})\n",
288
+ " try:\n",
289
+ " with ThreadPoolExecutor(max_workers=12) as ex:\n",
290
+ " futs = [ex.submit(dl, r) for r in todo]\n",
291
+ " for f in tqdm(as_completed(futs), total=len(todo), desc=\"downloading\"):\n",
292
+ " res[f.result().split(\"(\")[0]] += 1\n",
293
+ " finally:\n",
294
+ " flush_log_to_drive() # luôn lưu log Drive khi kết thúc/lỗi\n",
295
+ " print(dict(res))\n",
296
+ " if any(k.startswith(\"fail\") for k in res):\n",
297
+ " print(\"Còn fail -> chạy lại cell này (chỉ tải phần thiếu).\")"
298
+ ]
299
  },
300
  {
301
  "cell_type": "code",
302
  "execution_count": null,
303
  "metadata": {},
304
+ "outputs": [],
305
+ "source": [
306
+ "# Kiểm tra còn thiếu ảnh nào không (đối chiếu manifest). Còn thì chạy lại cell tải.\n",
307
+ "miss = {sp: [] for sp in (\"train\",\"val\",\"test\")}\n",
308
+ "for row in all_rows:\n",
309
+ " p = OUT / row[\"image_relpath\"]\n",
310
+ " if not (p.exists() and p.stat().st_size > 0):\n",
311
+ " miss[row[\"split\"]].append(row[\"study_name\"])\n",
312
+ "print(\"ảnh còn thiếu:\", {k: len(v) for k, v in miss.items()},\n",
313
+ " \"| tổng:\", sum(len(v) for v in miss.values()))\n",
314
+ "# In vài study còn thiếu để đối chiếu\n",
315
+ "for sp, names in miss.items():\n",
316
+ " if names:\n",
317
+ " print(f\" [{sp}] thiếu {len(names)}, vd: {names[:5]}\")"
318
+ ]
319
  },
320
  {
321
  "cell_type": "markdown",
 
328
  "cell_type": "code",
329
  "execution_count": null,
330
  "metadata": {},
331
+ "outputs": [],
332
+ "source": [
333
+ "# Copy reports (giữ path gốc) + vqa + manifest vào package\n",
334
+ "# reports: bundle/reports/files/pXX/.../sSTUDY.txt -> OUT/files/pXX/.../sSTUDY.txt\n",
335
+ "shutil.copytree(BUNDLE_DIR/\"reports\", OUT, dirs_exist_ok=True)\n",
336
+ "\n",
337
+ "for sp in (\"train\",\"val\",\"test\"):\n",
338
+ " shutil.copy(BUNDLE_DIR/\"vqa\"/VQA_OUT[sp], OUT/VQA_OUT[sp])\n",
339
+ " shutil.copy(BUNDLE_DIR/f\"manifest_{sp}.json\", OUT/f\"manifest_{sp}.json\")\n",
340
+ " src_csv = BUNDLE_DIR/f\"manifest_{sp}.csv\"\n",
341
+ " if src_csv.exists():\n",
342
+ " shutil.copy(src_csv, OUT/f\"manifest_{sp}.csv\")\n",
343
+ " nv = len(json.load(open(OUT/VQA_OUT[sp], encoding=\"utf-8\")))\n",
344
+ " print(f\"{sp}: vqa={nv:,} manifest copied\")\n",
345
+ "\n",
346
+ "n_rep = sum(1 for _ in (OUT/'files').rglob('s*.txt'))\n",
347
+ "print(f\"reports trong package: {n_rep:,}\")"
348
+ ]
349
  },
350
  {
351
  "cell_type": "code",
352
  "execution_count": null,
353
  "metadata": {},
354
+ "outputs": [],
355
+ "source": [
356
+ "# Sanity check cuối — đếm theo manifest (image + report tồn tại thực sự)\n",
357
+ "for sp in (\"train\",\"val\",\"test\"):\n",
358
+ " rows = manifests[sp]\n",
359
+ " ni = sum(1 for x in rows if (OUT/x[\"image_relpath\"]).exists())\n",
360
+ " nr = sum(1 for x in rows if (OUT/x[\"report_relpath\"]).exists())\n",
361
+ " print(f\"{sp:5s} manifest={len(rows):,} images_ok={ni:,} reports_ok={nr:,}\")\n",
362
+ "print(\"\\nPackage:\", OUT)"
363
+ ]
364
  },
365
  {
366
  "cell_type": "markdown",
367
  "metadata": {},
368
+ "source": [
369
+ "## 4. Upload lên Hugging Face\n",
370
+ "\n",
371
+ "Push vào repo **có sẵn** `hieu3636/cxr-vlm-data`, nằm trong thư mục con `MIMIC-CXR_processed/` (dùng `path_in_repo`, không tạo repo mới)."
372
+ ]
373
  },
374
  {
375
  "cell_type": "code",
376
  "execution_count": null,
377
  "metadata": {},
378
+ "outputs": [],
379
+ "source": [
380
+ "RUN_HF = False # ← bật khi sẵn sàng push\n",
381
+ "if RUN_HF:\n",
382
+ " from huggingface_hub import HfApi\n",
383
+ " api = HfApi(token=HF_TOKEN)\n",
384
+ " # repo đã tồn tại sẵn -> exist_ok=True chỉ no-op, không tạo mới\n",
385
+ " api.create_repo(HF_REPO_ID, repo_type=HF_REPO_TYPE, exist_ok=True)\n",
386
+ " # upload_folder hỗ trợ path_in_repo -> đẩy vào thư mục con MIMIC-CXR_processed\n",
387
+ " # (chạy lại nếu đứt: file đã có trên repo được bỏ qua theo hash)\n",
388
+ " api.upload_folder(\n",
389
+ " repo_id = HF_REPO_ID,\n",
390
+ " repo_type = HF_REPO_TYPE,\n",
391
+ " folder_path = str(OUT),\n",
392
+ " path_in_repo = HF_PATH_IN_REPO,\n",
393
+ " commit_message = \"Add MIMIC-CXR_processed subset\",\n",
394
+ " )\n",
395
+ " print(\"pushed →\",\n",
396
+ " f\"https://huggingface.co/{HF_REPO_TYPE}s/{HF_REPO_ID}/tree/main/{HF_PATH_IN_REPO}\")\n",
397
+ "else:\n",
398
+ " print(\"RUN_HF=False — bật True để push vào\",\n",
399
+ " f\"{HF_REPO_ID}/{HF_PATH_IN_REPO}\")"
400
+ ]
401
  },
402
  {
403
  "cell_type": "markdown",
 
412
  "cell_type": "code",
413
  "execution_count": null,
414
  "metadata": {},
415
+ "outputs": [],
416
+ "source": [
417
+ "RUN_ZIP = False\n",
418
+ "if RUN_ZIP:\n",
419
+ " # zip cả package (giữ cấu trúc gốc) thành 1 file\n",
420
+ " shutil.make_archive(\"/content/MIMIC-CXR_processed\", \"zip\", OUT)\n",
421
+ " shutil.copy(\"/content/MIMIC-CXR_processed.zip\", DRIVE/\"MIMIC-CXR_processed.zip\")\n",
422
+ " print(\"zipped -> Drive/MIMIC-CXR_processed.zip\")\n",
423
+ "else:\n",
424
+ " print(\"RUN_ZIP=False\")"
425
+ ]
426
  },
427
  {
428
  "cell_type": "code",
429
  "execution_count": null,
430
  "metadata": {},
431
+ "outputs": [],
432
+ "source": [
433
+ "print(\"=\"*54)\n",
434
+ "print(\" PHASE 2 (COLAB) DONE\")\n",
435
+ "print(\"=\"*54)\n",
436
+ "for sp in (\"train\",\"val\",\"test\"):\n",
437
+ " rows=manifests[sp]\n",
438
+ " ni=sum(1 for x in rows if (OUT/x[\"image_relpath\"]).exists())\n",
439
+ " print(f\" {sp:5s} images={ni:,}/{len(rows):,}\")\n",
440
+ "print(f\" package: {OUT}\")\n",
441
+ "print(\" cấu trúc gốc: files/pXX/pSUBJ/sSTUDY/<dicom>.jpg + .txt\")\n",
442
+ "print(\" Flags: RUN_HF / RUN_ZIP (mặc định False)\")\n",
443
+ "print(\"=\"*54)"
444
+ ]
445
  }
446
  ],
447
  "metadata": {
 
457
  },
458
  "nbformat": 4,
459
  "nbformat_minor": 5
460
+ }
data/build_subset_local.ipynb CHANGED
The diff for this file is too large to render. See raw diff
 
data/eda_full.ipynb CHANGED
The diff for this file is too large to render. See raw diff
 
data/eda_p18.ipynb CHANGED
@@ -26,13 +26,43 @@
26
  {
27
  "cell_type": "code",
28
  "execution_count": null,
 
29
  "metadata": {},
30
  "outputs": [],
31
- "source": "from pathlib import Path\n\nDATA_DIR = Path(r\"D:\\USTH\\KLTN\\cxr-vlm-data\")\nCXR_ROOT = DATA_DIR / \"mimic-cxr-reports\" # files/p10…p19/pXXXXXX/sYYYYYY.txt\n\nSPLIT_CSV = DATA_DIR / \"mimic-cxr-2.0.0-split.csv\"\nMETA_CSV = DATA_DIR / \"mimic-cxr-2.0.0-metadata.csv\"\nCHEXPERT_CSV = DATA_DIR / \"mimic-cxr-2.0.0-chexpert.csv\"\n\n_VQA_DIR = (DATA_DIR\n / \"mimic-ext-mimic-cxr-vqa-a-complex-diverse-and-large-scale-visual-question-answering-dataset-for-chest-x-ray-images-1.0.0\"\n / \"MIMIC-Ext-MIMIC-CXR-VQA\"\n / \"dataset\")\nVQA_TRAIN = _VQA_DIR / \"train.json\"\nVQA_VALID = _VQA_DIR / \"valid.json\"\nVQA_TEST = _VQA_DIR / \"test.json\"\n\n# Kiểm tra nhanh\nfor name, p in [(\"SPLIT_CSV\", SPLIT_CSV),\n (\"META_CSV\", META_CSV),\n (\"CHEXPERT_CSV\", CHEXPERT_CSV),\n (\"CXR_ROOT\", CXR_ROOT),\n (\"VQA_TRAIN\", VQA_TRAIN)]:\n status = \"✓\" if p.exists() else \"✗ NOT FOUND\"\n print(f\" {status} {name}: {p}\")\n\nprint(\"\\nPaths configured.\")"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  },
33
  {
34
  "cell_type": "code",
35
  "execution_count": null,
 
36
  "metadata": {},
37
  "outputs": [],
38
  "source": [
@@ -61,6 +91,7 @@
61
  },
62
  {
63
  "cell_type": "markdown",
 
64
  "metadata": {},
65
  "source": [
66
  "## 1. Load & lọc subset p18"
@@ -69,6 +100,7 @@
69
  {
70
  "cell_type": "code",
71
  "execution_count": null,
 
72
  "metadata": {},
73
  "outputs": [],
74
  "source": [
@@ -89,6 +121,7 @@
89
  {
90
  "cell_type": "code",
91
  "execution_count": null,
 
92
  "metadata": {},
93
  "outputs": [],
94
  "source": [
@@ -104,6 +137,7 @@
104
  },
105
  {
106
  "cell_type": "markdown",
 
107
  "metadata": {},
108
  "source": [
109
  "## 2. Tổng quan số lượng ảnh & report theo split"
@@ -112,6 +146,7 @@
112
  {
113
  "cell_type": "code",
114
  "execution_count": null,
 
115
  "metadata": {},
116
  "outputs": [],
117
  "source": [
@@ -136,6 +171,7 @@
136
  {
137
  "cell_type": "code",
138
  "execution_count": null,
 
139
  "metadata": {},
140
  "outputs": [],
141
  "source": [
@@ -153,6 +189,7 @@
153
  },
154
  {
155
  "cell_type": "markdown",
 
156
  "metadata": {},
157
  "source": [
158
  "## 3. Số ảnh mỗi study (1 study → bao nhiêu ảnh?)"
@@ -161,6 +198,7 @@
161
  {
162
  "cell_type": "code",
163
  "execution_count": null,
 
164
  "metadata": {},
165
  "outputs": [],
166
  "source": [
@@ -176,6 +214,7 @@
176
  {
177
  "cell_type": "code",
178
  "execution_count": null,
 
179
  "metadata": {},
180
  "outputs": [],
181
  "source": [
@@ -192,6 +231,7 @@
192
  },
193
  {
194
  "cell_type": "markdown",
 
195
  "metadata": {},
196
  "source": [
197
  "## 4. Phân bố View Position (AP, PA, Lateral, ...)"
@@ -200,6 +240,7 @@
200
  {
201
  "cell_type": "code",
202
  "execution_count": null,
 
203
  "metadata": {},
204
  "outputs": [],
205
  "source": [
@@ -212,6 +253,7 @@
212
  {
213
  "cell_type": "code",
214
  "execution_count": null,
 
215
  "metadata": {},
216
  "outputs": [],
217
  "source": [
@@ -237,6 +279,7 @@
237
  {
238
  "cell_type": "code",
239
  "execution_count": null,
 
240
  "metadata": {},
241
  "outputs": [],
242
  "source": [
@@ -256,27 +299,104 @@
256
  {
257
  "cell_type": "markdown",
258
  "id": "ae9f3d3c",
259
- "source": "## 4b. Frontal-Only Sampling Strategy (AP > PA)\n\nChiến lược train: **1 report + 1 ảnh frontal** mỗi study.\n- Chỉ giữ AP hoặc PA; nếu study có cả hai thì **ưu tiên AP**.\n- Study không có ảnh frontal nào → loại khỏi tập train.",
260
- "metadata": {}
 
 
 
 
 
 
261
  },
262
  {
263
  "cell_type": "code",
 
264
  "id": "d2ce6beb",
265
- "source": "frontal = df[df[\"ViewPosition\"].isin([\"AP\", \"PA\"])].copy()\n\n# Với mỗi study: chọn AP trước, nếu không có thì chọn PA (lấy 1 ảnh duy nhất)\ndef pick_frontal_view(group):\n ap = group[group[\"ViewPosition\"] == \"AP\"]\n if len(ap) > 0:\n return ap.iloc[[0]]\n return group[group[\"ViewPosition\"] == \"PA\"].iloc[[0]]\n\nfrontal_1img = (\n frontal.groupby(\"study_id\", group_keys=False)\n .apply(pick_frontal_view)\n .reset_index(drop=True)\n)\n\n# Thống kê tổng quan\nn_study_total = df[\"study_id\"].nunique()\nn_study_frontal = frontal_1img[\"study_id\"].nunique()\nn_study_no_front = n_study_total - n_study_frontal\n\nprint(\"=== Frontal-Only Sampling (p18) ===\")\nprint(f\"Tổng số study : {n_study_total:,}\")\nprint(f\"Study có ảnh frontal (AP/PA) : {n_study_frontal:,} ({n_study_frontal/n_study_total*100:.1f}%)\")\nprint(f\"Study bị loại (không có frontal): {n_study_no_front:,} ({n_study_no_front/n_study_total*100:.1f}%)\")\nprint()\nprint(f\"Ảnh được chọn theo view:\")\nprint(frontal_1img[\"ViewPosition\"].value_counts().to_string())\nprint()\nprint(\"=== Mẫu train sau khi filter (split) ===\")\nsplit_frontal = frontal_1img[\"split\"].value_counts().reindex([\"train\", \"validate\", \"test\"])\nsplit_all = df.drop_duplicates(\"study_id\")[\"split\"].value_counts().reindex([\"train\", \"validate\", \"test\"])\ncompare = pd.DataFrame({\n \"All studies\": split_all,\n \"Frontal-only\": split_frontal,\n \"Giảm (%)\": ((split_all - split_frontal) / split_all * 100).round(1)\n})\nprint(compare.to_string())",
266
  "metadata": {},
267
- "execution_count": null,
268
- "outputs": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  },
270
  {
271
  "cell_type": "code",
 
272
  "id": "9d4aaf5c",
273
- "source": "fig, axes = plt.subplots(1, 3, figsize=(14, 4))\n\n# 1. All vs Frontal-only (study count)\ncats = [\"All studies\", \"Frontal-only\"]\nvals = [n_study_total, n_study_frontal]\nbars = axes[0].bar(cats, vals, color=[\"#4C72B0\", \"#55A868\"], width=0.5)\naxes[0].bar_label(bars, fmt=\"%d\")\naxes[0].set_title(\"Study count: All vs Frontal-only\")\naxes[0].set_ylabel(\"Số study\")\n\n# 2. View breakdown của ảnh được chọn\nvc = frontal_1img[\"ViewPosition\"].value_counts()\naxes[1].pie(vc.values, labels=vc.index, autopct=\"%1.1f%%\",\n colors=[\"#4C72B0\", \"#DD8452\"])\naxes[1].set_title(\"View được chọn (AP ưu tiên)\")\n\n# 3. So sánh train/val/test\nx = np.arange(3)\nw = 0.35\nsplits = [\"train\", \"validate\", \"test\"]\naxes[2].bar(x - w/2, split_all.values, w, label=\"All\", color=\"#4C72B0\", alpha=0.85)\naxes[2].bar(x + w/2, split_frontal.values, w, label=\"Frontal-only\", color=\"#55A868\", alpha=0.85)\naxes[2].set_xticks(x)\naxes[2].set_xticklabels(splits)\naxes[2].set_title(\"Frontal-only vs All (per split)\")\naxes[2].set_ylabel(\"Số study\")\naxes[2].legend()\n\nplt.suptitle(\"Frontal-Only Sampling Strategy — p18\", fontsize=13)\nplt.tight_layout()\nplt.show()",
274
  "metadata": {},
275
- "execution_count": null,
276
- "outputs": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  },
278
  {
279
  "cell_type": "markdown",
 
280
  "metadata": {},
281
  "source": [
282
  "## 5. CheXpert Labels — 14 nhãn bệnh lý"
@@ -285,6 +405,7 @@
285
  {
286
  "cell_type": "code",
287
  "execution_count": null,
 
288
  "metadata": {},
289
  "outputs": [],
290
  "source": [
@@ -311,13 +432,33 @@
311
  {
312
  "cell_type": "code",
313
  "execution_count": null,
 
314
  "metadata": {},
315
  "outputs": [],
316
- "source": "# Headers hành chính — không phải findings\nADMIN_HEADERS = {\n 'EXAMINATION', 'INDICATION', 'CLINICAL INDICATION', 'TECHNIQUE',\n 'COMPARISON', 'HISTORY', 'REASON', 'REASON FOR EXAM',\n 'REASON FOR EXAMINATION', 'PROCEDURE', 'FINAL REPORT',\n 'NOTIFICATION', 'RECOMMENDATION', 'ADDENDUM'\n}\n\n# Detect section header: dòng bắt đầu bằng ALL-CAPS (có thể có space/dấu câu) rồi đến \":\"\nSECTION_RE = re.compile(r'^[ \\t]*([A-Z][A-Z ,/()\\-]{1,70}?):\\s*', re.MULTILINE)\n\ndef parse_report(txt_path: Path) -> dict:\n \"\"\"\n Parse report .txt thành dict {'findings': str|None, 'impression': str|None}.\n\n Quy luật detect section: mọi header đều VIẾT HOA TOÀN BỘ và kết thúc bằng ':',\n ví dụ: FINDINGS:, IMPRESSION:, FRONTAL AND LATERAL VIEWS OF THE CHEST:\n → dùng regex bắt pattern đó, không hardcode từng keyword.\n\n Nếu không có section FINDINGS tường minh, fallback lấy section\n descriptive đầu tiên (không phải admin header).\n \"\"\"\n try:\n text = txt_path.read_text(encoding=\"utf-8\", errors=\"ignore\")\n except FileNotFoundError:\n return {\"findings\": None, \"impression\": None}\n\n matches = list(SECTION_RE.finditer(text))\n if not matches:\n return {\"findings\": None, \"impression\": None}\n\n # Tách từng section thành (header, content)\n sections = []\n for i, m in enumerate(matches):\n header = m.group(1).strip()\n start = m.end()\n end = matches[i + 1].start() if i + 1 < len(matches) else len(text)\n content = text[start:end].strip()\n sections.append((header, content))\n\n findings = impression = None\n for header, content in sections:\n h = header.upper()\n if \"FINDING\" in h and findings is None:\n findings = content or None\n elif \"IMPRESSION\" in h and impression is None:\n impression = content or None\n\n # Fallback: không có FINDINGS tường minh → lấy section descriptive đầu tiên\n if findings is None:\n for header, content in sections:\n h = header.upper()\n if h not in ADMIN_HEADERS and \"IMPRESSION\" not in h and content:\n findings = content\n break\n\n return {\"findings\": findings, \"impression\": impression}\n\n\n# Lấy danh sách unique studies trong p18\np18_studies = (\n df[[\"subject_id\", \"study_id\"]]\n .drop_duplicates(\"study_id\")\n .reset_index(drop=True)\n)\n\nprint(f\"Số study cần parse: {len(p18_studies):,}\")\nprint(\"Parsing reports...\")\n\nrecords = []\nfor _, row in p18_studies.iterrows():\n sid = str(row[\"subject_id\"])\n stid = str(row[\"study_id\"])\n txt_path = CXR_ROOT / \"files\" / \"p18\" / f\"p{sid}\" / f\"s{stid}.txt\"\n parsed = parse_report(txt_path)\n records.append({\"study_id\": stid, **parsed})\n\nreport_df = pd.DataFrame(records)\nreport_df[\"findings_len\"] = report_df[\"findings\"].str.split().str.len()\nreport_df[\"impression_len\"] = report_df[\"impression\"].str.split().str.len()\n\ntotal = len(report_df)\nprint(f\"\\nFindings found : {report_df['findings'].notna().sum():,} / {total:,} ({report_df['findings'].notna().mean()*100:.1f}%)\")\nprint(f\"Impression found : {report_df['impression'].notna().sum():,} / {total:,} ({report_df['impression'].notna().mean()*100:.1f}%)\")\nboth = (report_df['findings'].notna() & report_df['impression'].notna()).sum()\nneither = (report_df['findings'].isna() & report_df['impression'].isna()).sum()\nprint(f\"Cả hai : {both:,} / {total:,} ({both/total*100:.1f}%)\")\nprint(f\"Không có cả hai : {neither:,} / {total:,} ({neither/total*100:.1f}%)\")"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  },
318
  {
319
  "cell_type": "code",
320
  "execution_count": null,
 
321
  "metadata": {},
322
  "outputs": [],
323
  "source": [
@@ -338,6 +479,7 @@
338
  },
339
  {
340
  "cell_type": "markdown",
 
341
  "metadata": {},
342
  "source": [
343
  "## 6. Phân tích Report — Findings & Impression"
@@ -346,6 +488,7 @@
346
  {
347
  "cell_type": "code",
348
  "execution_count": null,
 
349
  "metadata": {},
350
  "outputs": [],
351
  "source": [
@@ -405,6 +548,7 @@
405
  {
406
  "cell_type": "code",
407
  "execution_count": null,
 
408
  "metadata": {},
409
  "outputs": [],
410
  "source": [
@@ -418,6 +562,7 @@
418
  {
419
  "cell_type": "code",
420
  "execution_count": null,
 
421
  "metadata": {},
422
  "outputs": [],
423
  "source": [
@@ -451,6 +596,7 @@
451
  {
452
  "cell_type": "code",
453
  "execution_count": null,
 
454
  "metadata": {},
455
  "outputs": [],
456
  "source": [
@@ -471,6 +617,7 @@
471
  },
472
  {
473
  "cell_type": "markdown",
 
474
  "metadata": {},
475
  "source": [
476
  "## 7. VQA — phân tích câu hỏi & đáp"
@@ -479,6 +626,7 @@
479
  {
480
  "cell_type": "code",
481
  "execution_count": null,
 
482
  "metadata": {},
483
  "outputs": [],
484
  "source": [
@@ -506,6 +654,7 @@
506
  {
507
  "cell_type": "code",
508
  "execution_count": null,
 
509
  "metadata": {},
510
  "outputs": [],
511
  "source": [
@@ -517,6 +666,7 @@
517
  {
518
  "cell_type": "code",
519
  "execution_count": null,
 
520
  "metadata": {},
521
  "outputs": [],
522
  "source": [
@@ -534,6 +684,7 @@
534
  {
535
  "cell_type": "code",
536
  "execution_count": null,
 
537
  "metadata": {},
538
  "outputs": [],
539
  "source": [
@@ -559,31 +710,10 @@
559
  "plt.show()"
560
  ]
561
  },
562
- {
563
- "cell_type": "markdown",
564
- "id": "c313b9c3",
565
- "source": "### VQA × View Position — mẫu hỏi đáp thuộc ảnh view nào",
566
- "metadata": {}
567
- },
568
- {
569
- "cell_type": "code",
570
- "id": "0791482f",
571
- "source": "# image_id trong VQA = dicom_id trong metadata\nvqa_view = vqa_p18.merge(\n p18_meta[[\"dicom_id\", \"ViewPosition\"]],\n left_on=\"image_id\", right_on=\"dicom_id\",\n how=\"left\"\n)\n\nmissing_view_vqa = vqa_view[\"ViewPosition\"].isna().sum()\nvqa_view[\"ViewPosition\"] = vqa_view[\"ViewPosition\"].fillna(\"Unknown\")\n\nview_vqa_counts = vqa_view[\"ViewPosition\"].value_counts()\nprint(\"=== VQA samples theo View Position (p18) ===\")\nprint(view_vqa_counts.to_string())\nprint(f\"\\nKhông map được ViewPosition: {missing_view_vqa:,} ({missing_view_vqa/len(vqa_view)*100:.1f}%)\")",
572
- "metadata": {},
573
- "execution_count": null,
574
- "outputs": []
575
- },
576
- {
577
- "cell_type": "code",
578
- "id": "049baaef",
579
- "source": "fig, axes = plt.subplots(1, 3, figsize=(15, 4))\n\n# 1. Bar: số mẫu VQA theo view\nbars = axes[0].bar(view_vqa_counts.index, view_vqa_counts.values,\n color=sns.color_palette(\"Set2\", len(view_vqa_counts)))\naxes[0].bar_label(bars, fmt=\"%d\")\naxes[0].set_title(\"Số mẫu VQA theo View Position\")\naxes[0].set_ylabel(\"Số mẫu\")\n\n# 2. Pie\naxes[1].pie(view_vqa_counts.values, labels=view_vqa_counts.index,\n autopct=\"%1.1f%%\", colors=sns.color_palette(\"Set2\", len(view_vqa_counts)))\naxes[1].set_title(\"Tỉ lệ VQA theo View Position\")\n\n# 3. Semantic type × View (stacked bar)\nsem_view = vqa_view.groupby([\"ViewPosition\", \"semantic_type\"]).size().unstack(fill_value=0)\nsem_view.plot(kind=\"bar\", ax=axes[2], color=sns.color_palette(\"Set1\", sem_view.shape[1]),\n width=0.7, stacked=True)\naxes[2].set_title(\"Semantic Type × View Position\")\naxes[2].set_xlabel(\"View Position\")\naxes[2].set_ylabel(\"Số mẫu\")\naxes[2].tick_params(axis=\"x\", rotation=30)\naxes[2].legend(title=\"Semantic Type\", fontsize=8)\n\nplt.suptitle(\"VQA × View Position — p18\", fontsize=13)\nplt.tight_layout()\nplt.show()\n\n# Content type × View\nprint(\"\\nContent type theo View Position:\")\nprint(vqa_view.groupby([\"ViewPosition\", \"content_type\"]).size()\n .unstack(fill_value=0).to_string())",
580
- "metadata": {},
581
- "execution_count": null,
582
- "outputs": []
583
- },
584
  {
585
  "cell_type": "code",
586
  "execution_count": null,
 
587
  "metadata": {},
588
  "outputs": [],
589
  "source": [
@@ -602,6 +732,7 @@
602
  {
603
  "cell_type": "code",
604
  "execution_count": null,
 
605
  "metadata": {},
606
  "outputs": [],
607
  "source": [
@@ -627,6 +758,7 @@
627
  {
628
  "cell_type": "code",
629
  "execution_count": null,
 
630
  "metadata": {},
631
  "outputs": [],
632
  "source": [
@@ -657,6 +789,7 @@
657
  },
658
  {
659
  "cell_type": "markdown",
 
660
  "metadata": {},
661
  "source": [
662
  "## 8. Gợi ý thêm — Missing data & Data Quality"
@@ -665,6 +798,7 @@
665
  {
666
  "cell_type": "code",
667
  "execution_count": null,
 
668
  "metadata": {},
669
  "outputs": [],
670
  "source": [
@@ -682,6 +816,7 @@
682
  {
683
  "cell_type": "code",
684
  "execution_count": null,
 
685
  "metadata": {},
686
  "outputs": [],
687
  "source": [
@@ -693,6 +828,7 @@
693
  {
694
  "cell_type": "code",
695
  "execution_count": null,
 
696
  "metadata": {},
697
  "outputs": [],
698
  "source": [
@@ -711,6 +847,7 @@
711
  {
712
  "cell_type": "code",
713
  "execution_count": null,
 
714
  "metadata": {},
715
  "outputs": [],
716
  "source": [
@@ -733,6 +870,7 @@
733
  {
734
  "cell_type": "code",
735
  "execution_count": null,
 
736
  "metadata": {},
737
  "outputs": [],
738
  "source": [
@@ -743,12 +881,14 @@
743
  "\n",
744
  " res_counts = df.groupby([\"Rows\", \"Columns\"]).size().sort_values(ascending=False).head(15)\n",
745
  " print(\"\\nTop-15 resolutions:\")\n",
746
- " print(res_counts.to_string())\nelse:\n",
 
747
  " print(\"Cột Rows/Columns không có trong metadata.\")"
748
  ]
749
  },
750
  {
751
  "cell_type": "markdown",
 
752
  "metadata": {},
753
  "source": [
754
  "## 9. Tóm tắt (Summary)"
@@ -757,6 +897,7 @@
757
  {
758
  "cell_type": "code",
759
  "execution_count": null,
 
760
  "metadata": {},
761
  "outputs": [],
762
  "source": [
@@ -789,9 +930,9 @@
789
  },
790
  "language_info": {
791
  "name": "python",
792
- "version": "3.10.0"
793
  }
794
  },
795
  "nbformat": 4,
796
  "nbformat_minor": 5
797
- }
 
26
  {
27
  "cell_type": "code",
28
  "execution_count": null,
29
+ "id": "a4238924",
30
  "metadata": {},
31
  "outputs": [],
32
+ "source": [
33
+ "from pathlib import Path\n",
34
+ "\n",
35
+ "DATA_DIR = Path(r\"D:\\USTH\\KLTN\\cxr-vlm-data\")\n",
36
+ "CXR_ROOT = DATA_DIR / \"mimic-cxr-reports\" # files/p10…p19/pXXXXXX/sYYYYYY.txt\n",
37
+ "\n",
38
+ "SPLIT_CSV = DATA_DIR / \"mimic-cxr-2.0.0-split.csv\"\n",
39
+ "META_CSV = DATA_DIR / \"mimic-cxr-2.0.0-metadata.csv\"\n",
40
+ "CHEXPERT_CSV = DATA_DIR / \"mimic-cxr-2.0.0-chexpert.csv\"\n",
41
+ "\n",
42
+ "_VQA_DIR = (DATA_DIR\n",
43
+ " / \"mimic-ext-mimic-cxr-vqa-a-complex-diverse-and-large-scale-visual-question-answering-dataset-for-chest-x-ray-images-1.0.0\"\n",
44
+ " / \"MIMIC-Ext-MIMIC-CXR-VQA\"\n",
45
+ " / \"dataset\")\n",
46
+ "VQA_TRAIN = _VQA_DIR / \"train.json\"\n",
47
+ "VQA_VALID = _VQA_DIR / \"valid.json\"\n",
48
+ "VQA_TEST = _VQA_DIR / \"test.json\"\n",
49
+ "\n",
50
+ "# Kiểm tra nhanh\n",
51
+ "for name, p in [(\"SPLIT_CSV\", SPLIT_CSV),\n",
52
+ " (\"META_CSV\", META_CSV),\n",
53
+ " (\"CHEXPERT_CSV\", CHEXPERT_CSV),\n",
54
+ " (\"CXR_ROOT\", CXR_ROOT),\n",
55
+ " (\"VQA_TRAIN\", VQA_TRAIN)]:\n",
56
+ " status = \"✓\" if p.exists() else \"✗ NOT FOUND\"\n",
57
+ " print(f\" {status} {name}: {p}\")\n",
58
+ "\n",
59
+ "print(\"\\nPaths configured.\")"
60
+ ]
61
  },
62
  {
63
  "cell_type": "code",
64
  "execution_count": null,
65
+ "id": "99828a70",
66
  "metadata": {},
67
  "outputs": [],
68
  "source": [
 
91
  },
92
  {
93
  "cell_type": "markdown",
94
+ "id": "4674dd4f",
95
  "metadata": {},
96
  "source": [
97
  "## 1. Load & lọc subset p18"
 
100
  {
101
  "cell_type": "code",
102
  "execution_count": null,
103
+ "id": "9f1d59fe",
104
  "metadata": {},
105
  "outputs": [],
106
  "source": [
 
121
  {
122
  "cell_type": "code",
123
  "execution_count": null,
124
+ "id": "6657d6ec",
125
  "metadata": {},
126
  "outputs": [],
127
  "source": [
 
137
  },
138
  {
139
  "cell_type": "markdown",
140
+ "id": "5a7bff47",
141
  "metadata": {},
142
  "source": [
143
  "## 2. Tổng quan số lượng ảnh & report theo split"
 
146
  {
147
  "cell_type": "code",
148
  "execution_count": null,
149
+ "id": "81be327d",
150
  "metadata": {},
151
  "outputs": [],
152
  "source": [
 
171
  {
172
  "cell_type": "code",
173
  "execution_count": null,
174
+ "id": "80fa39e7",
175
  "metadata": {},
176
  "outputs": [],
177
  "source": [
 
189
  },
190
  {
191
  "cell_type": "markdown",
192
+ "id": "4fed2aa0",
193
  "metadata": {},
194
  "source": [
195
  "## 3. Số ảnh mỗi study (1 study → bao nhiêu ảnh?)"
 
198
  {
199
  "cell_type": "code",
200
  "execution_count": null,
201
+ "id": "39b23ccb",
202
  "metadata": {},
203
  "outputs": [],
204
  "source": [
 
214
  {
215
  "cell_type": "code",
216
  "execution_count": null,
217
+ "id": "b8c6560b",
218
  "metadata": {},
219
  "outputs": [],
220
  "source": [
 
231
  },
232
  {
233
  "cell_type": "markdown",
234
+ "id": "0262e14a",
235
  "metadata": {},
236
  "source": [
237
  "## 4. Phân bố View Position (AP, PA, Lateral, ...)"
 
240
  {
241
  "cell_type": "code",
242
  "execution_count": null,
243
+ "id": "cad06cc2",
244
  "metadata": {},
245
  "outputs": [],
246
  "source": [
 
253
  {
254
  "cell_type": "code",
255
  "execution_count": null,
256
+ "id": "d86b2102",
257
  "metadata": {},
258
  "outputs": [],
259
  "source": [
 
279
  {
280
  "cell_type": "code",
281
  "execution_count": null,
282
+ "id": "d8f24892",
283
  "metadata": {},
284
  "outputs": [],
285
  "source": [
 
299
  {
300
  "cell_type": "markdown",
301
  "id": "ae9f3d3c",
302
+ "metadata": {},
303
+ "source": [
304
+ "## 4b. Frontal-Only Sampling Strategy (AP > PA)\n",
305
+ "\n",
306
+ "Chiến lược train: **1 report + 1 ảnh frontal** mỗi study.\n",
307
+ "- Chỉ giữ AP hoặc PA; nếu study có cả hai thì **ưu tiên AP**.\n",
308
+ "- Study không có ảnh frontal nào → loại khỏi tập train."
309
+ ]
310
  },
311
  {
312
  "cell_type": "code",
313
+ "execution_count": null,
314
  "id": "d2ce6beb",
 
315
  "metadata": {},
316
+ "outputs": [],
317
+ "source": [
318
+ "frontal = df[df[\"ViewPosition\"].isin([\"AP\", \"PA\"])].copy()\n",
319
+ "\n",
320
+ "# Với mỗi study: chọn AP trước, nếu không có thì chọn PA (lấy 1 ảnh duy nhất)\n",
321
+ "def pick_frontal_view(group):\n",
322
+ " ap = group[group[\"ViewPosition\"] == \"AP\"]\n",
323
+ " if len(ap) > 0:\n",
324
+ " return ap.iloc[[0]]\n",
325
+ " return group[group[\"ViewPosition\"] == \"PA\"].iloc[[0]]\n",
326
+ "\n",
327
+ "frontal_1img = (\n",
328
+ " frontal.groupby(\"study_id\", group_keys=False)\n",
329
+ " .apply(pick_frontal_view)\n",
330
+ " .reset_index(drop=True)\n",
331
+ ")\n",
332
+ "\n",
333
+ "# Thống kê tổng quan\n",
334
+ "n_study_total = df[\"study_id\"].nunique()\n",
335
+ "n_study_frontal = frontal_1img[\"study_id\"].nunique()\n",
336
+ "n_study_no_front = n_study_total - n_study_frontal\n",
337
+ "\n",
338
+ "print(\"=== Frontal-Only Sampling (p18) ===\")\n",
339
+ "print(f\"Tổng số study : {n_study_total:,}\")\n",
340
+ "print(f\"Study có ảnh frontal (AP/PA) : {n_study_frontal:,} ({n_study_frontal/n_study_total*100:.1f}%)\")\n",
341
+ "print(f\"Study bị loại (không có frontal): {n_study_no_front:,} ({n_study_no_front/n_study_total*100:.1f}%)\")\n",
342
+ "print()\n",
343
+ "print(f\"Ảnh được chọn theo view:\")\n",
344
+ "print(frontal_1img[\"ViewPosition\"].value_counts().to_string())\n",
345
+ "print()\n",
346
+ "print(\"=== Mẫu train sau khi filter (split) ===\")\n",
347
+ "split_frontal = frontal_1img[\"split\"].value_counts().reindex([\"train\", \"validate\", \"test\"])\n",
348
+ "split_all = df.drop_duplicates(\"study_id\")[\"split\"].value_counts().reindex([\"train\", \"validate\", \"test\"])\n",
349
+ "compare = pd.DataFrame({\n",
350
+ " \"All studies\": split_all,\n",
351
+ " \"Frontal-only\": split_frontal,\n",
352
+ " \"Giảm (%)\": ((split_all - split_frontal) / split_all * 100).round(1)\n",
353
+ "})\n",
354
+ "print(compare.to_string())"
355
+ ]
356
  },
357
  {
358
  "cell_type": "code",
359
+ "execution_count": null,
360
  "id": "9d4aaf5c",
 
361
  "metadata": {},
362
+ "outputs": [],
363
+ "source": [
364
+ "fig, axes = plt.subplots(1, 3, figsize=(14, 4))\n",
365
+ "\n",
366
+ "# 1. All vs Frontal-only (study count)\n",
367
+ "cats = [\"All studies\", \"Frontal-only\"]\n",
368
+ "vals = [n_study_total, n_study_frontal]\n",
369
+ "bars = axes[0].bar(cats, vals, color=[\"#4C72B0\", \"#55A868\"], width=0.5)\n",
370
+ "axes[0].bar_label(bars, fmt=\"%d\")\n",
371
+ "axes[0].set_title(\"Study count: All vs Frontal-only\")\n",
372
+ "axes[0].set_ylabel(\"Số study\")\n",
373
+ "\n",
374
+ "# 2. View breakdown của ảnh được chọn\n",
375
+ "vc = frontal_1img[\"ViewPosition\"].value_counts()\n",
376
+ "axes[1].pie(vc.values, labels=vc.index, autopct=\"%1.1f%%\",\n",
377
+ " colors=[\"#4C72B0\", \"#DD8452\"])\n",
378
+ "axes[1].set_title(\"View được chọn (AP ưu tiên)\")\n",
379
+ "\n",
380
+ "# 3. So sánh train/val/test\n",
381
+ "x = np.arange(3)\n",
382
+ "w = 0.35\n",
383
+ "splits = [\"train\", \"validate\", \"test\"]\n",
384
+ "axes[2].bar(x - w/2, split_all.values, w, label=\"All\", color=\"#4C72B0\", alpha=0.85)\n",
385
+ "axes[2].bar(x + w/2, split_frontal.values, w, label=\"Frontal-only\", color=\"#55A868\", alpha=0.85)\n",
386
+ "axes[2].set_xticks(x)\n",
387
+ "axes[2].set_xticklabels(splits)\n",
388
+ "axes[2].set_title(\"Frontal-only vs All (per split)\")\n",
389
+ "axes[2].set_ylabel(\"Số study\")\n",
390
+ "axes[2].legend()\n",
391
+ "\n",
392
+ "plt.suptitle(\"Frontal-Only Sampling Strategy — p18\", fontsize=13)\n",
393
+ "plt.tight_layout()\n",
394
+ "plt.show()"
395
+ ]
396
  },
397
  {
398
  "cell_type": "markdown",
399
+ "id": "28847d0b",
400
  "metadata": {},
401
  "source": [
402
  "## 5. CheXpert Labels — 14 nhãn bệnh lý"
 
405
  {
406
  "cell_type": "code",
407
  "execution_count": null,
408
+ "id": "410fbdbe",
409
  "metadata": {},
410
  "outputs": [],
411
  "source": [
 
432
  {
433
  "cell_type": "code",
434
  "execution_count": null,
435
+ "id": "50c9a91d",
436
  "metadata": {},
437
  "outputs": [],
438
+ "source": [
439
+ "fig, ax = plt.subplots(figsize=(12, 5))\n",
440
+ "x = np.arange(len(label_cols))\n",
441
+ "w = 0.25\n",
442
+ "\n",
443
+ "ordered_labels = label_summary.sort_values(\"Positive\", ascending=False).index.tolist()\n",
444
+ "\n",
445
+ "ax.bar(x - w, label_summary.loc[ordered_labels, \"Positive\"], width=w, label=\"Positive\", color=\"#e74c3c\")\n",
446
+ "ax.bar(x, label_summary.loc[ordered_labels, \"Uncertain\"], width=w, label=\"Uncertain\", color=\"#f39c12\")\n",
447
+ "ax.bar(x + w, label_summary.loc[ordered_labels, \"Negative\"], width=w, label=\"Negative\", color=\"#2ecc71\")\n",
448
+ "\n",
449
+ "ax.set_xticks(x)\n",
450
+ "ax.set_xticklabels(ordered_labels, rotation=40, ha=\"right\", fontsize=9)\n",
451
+ "ax.set_ylabel(\"Số study\")\n",
452
+ "ax.set_title(\"CheXpert Labels — Positive / Uncertain / Negative (p18)\")\n",
453
+ "ax.legend()\n",
454
+ "plt.tight_layout()\n",
455
+ "plt.show()"
456
+ ]
457
  },
458
  {
459
  "cell_type": "code",
460
  "execution_count": null,
461
+ "id": "1e1209c5",
462
  "metadata": {},
463
  "outputs": [],
464
  "source": [
 
479
  },
480
  {
481
  "cell_type": "markdown",
482
+ "id": "f0aa1ba8",
483
  "metadata": {},
484
  "source": [
485
  "## 6. Phân tích Report — Findings & Impression"
 
488
  {
489
  "cell_type": "code",
490
  "execution_count": null,
491
+ "id": "8b1e562e",
492
  "metadata": {},
493
  "outputs": [],
494
  "source": [
 
548
  {
549
  "cell_type": "code",
550
  "execution_count": null,
551
+ "id": "c49a401a",
552
  "metadata": {},
553
  "outputs": [],
554
  "source": [
 
562
  {
563
  "cell_type": "code",
564
  "execution_count": null,
565
+ "id": "942959d1",
566
  "metadata": {},
567
  "outputs": [],
568
  "source": [
 
596
  {
597
  "cell_type": "code",
598
  "execution_count": null,
599
+ "id": "170b0971",
600
  "metadata": {},
601
  "outputs": [],
602
  "source": [
 
617
  },
618
  {
619
  "cell_type": "markdown",
620
+ "id": "5512e3aa",
621
  "metadata": {},
622
  "source": [
623
  "## 7. VQA — phân tích câu hỏi & đáp"
 
626
  {
627
  "cell_type": "code",
628
  "execution_count": null,
629
+ "id": "7caa394c",
630
  "metadata": {},
631
  "outputs": [],
632
  "source": [
 
654
  {
655
  "cell_type": "code",
656
  "execution_count": null,
657
+ "id": "ddb012a8",
658
  "metadata": {},
659
  "outputs": [],
660
  "source": [
 
666
  {
667
  "cell_type": "code",
668
  "execution_count": null,
669
+ "id": "86eec60e",
670
  "metadata": {},
671
  "outputs": [],
672
  "source": [
 
684
  {
685
  "cell_type": "code",
686
  "execution_count": null,
687
+ "id": "4567a110",
688
  "metadata": {},
689
  "outputs": [],
690
  "source": [
 
710
  "plt.show()"
711
  ]
712
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
713
  {
714
  "cell_type": "code",
715
  "execution_count": null,
716
+ "id": "f968f772",
717
  "metadata": {},
718
  "outputs": [],
719
  "source": [
 
732
  {
733
  "cell_type": "code",
734
  "execution_count": null,
735
+ "id": "97179573",
736
  "metadata": {},
737
  "outputs": [],
738
  "source": [
 
758
  {
759
  "cell_type": "code",
760
  "execution_count": null,
761
+ "id": "9ffe116e",
762
  "metadata": {},
763
  "outputs": [],
764
  "source": [
 
789
  },
790
  {
791
  "cell_type": "markdown",
792
+ "id": "37f8ee29",
793
  "metadata": {},
794
  "source": [
795
  "## 8. Gợi ý thêm — Missing data & Data Quality"
 
798
  {
799
  "cell_type": "code",
800
  "execution_count": null,
801
+ "id": "c0a10b57",
802
  "metadata": {},
803
  "outputs": [],
804
  "source": [
 
816
  {
817
  "cell_type": "code",
818
  "execution_count": null,
819
+ "id": "f2fe0d2e",
820
  "metadata": {},
821
  "outputs": [],
822
  "source": [
 
828
  {
829
  "cell_type": "code",
830
  "execution_count": null,
831
+ "id": "4b3a9176",
832
  "metadata": {},
833
  "outputs": [],
834
  "source": [
 
847
  {
848
  "cell_type": "code",
849
  "execution_count": null,
850
+ "id": "ea4da928",
851
  "metadata": {},
852
  "outputs": [],
853
  "source": [
 
870
  {
871
  "cell_type": "code",
872
  "execution_count": null,
873
+ "id": "9b990ae5",
874
  "metadata": {},
875
  "outputs": [],
876
  "source": [
 
881
  "\n",
882
  " res_counts = df.groupby([\"Rows\", \"Columns\"]).size().sort_values(ascending=False).head(15)\n",
883
  " print(\"\\nTop-15 resolutions:\")\n",
884
+ " print(res_counts.to_string())\n",
885
+ "else:\n",
886
  " print(\"Cột Rows/Columns không có trong metadata.\")"
887
  ]
888
  },
889
  {
890
  "cell_type": "markdown",
891
+ "id": "a03900eb",
892
  "metadata": {},
893
  "source": [
894
  "## 9. Tóm tắt (Summary)"
 
897
  {
898
  "cell_type": "code",
899
  "execution_count": null,
900
+ "id": "f8cc6c50",
901
  "metadata": {},
902
  "outputs": [],
903
  "source": [
 
930
  },
931
  "language_info": {
932
  "name": "python",
933
+ "version": "3.11.7"
934
  }
935
  },
936
  "nbformat": 4,
937
  "nbformat_minor": 5
938
+ }
data/eda_reports.ipynb CHANGED
The diff for this file is too large to render. See raw diff
 
data/img_stat.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import matplotlib.pyplot as plt
3
+
4
+ # Đọc file CSV
5
+ # Thay "images.csv" bằng đường dẫn file của bạn
6
+ df = pd.read_csv(r"D:\USTH\KLTN\cxr-vlm-data\mimic-cxr-2.0.0-metadata.csv")
7
+
8
+ # Kiểm tra các cột cần thiết
9
+ required_cols = ["Rows", "Columns"]
10
+ for col in required_cols:
11
+ if col not in df.columns:
12
+ raise ValueError(f"Thiếu cột: {col}")
13
+
14
+ # Tạo thêm cột diện tích ảnh
15
+ df["TotalPixels"] = df["Rows"] * df["Columns"]
16
+
17
+ # Thống kê cơ bản
18
+ print("===== THỐNG KÊ KÍCH THƯỚC ẢNH =====")
19
+ print(df[["Rows", "Columns", "TotalPixels"]].describe())
20
+
21
+ # Tỉ lệ khung hình
22
+ df["AspectRatio"] = df["Columns"] / df["Rows"]
23
+
24
+ print("\n===== THỐNG KÊ TỈ LỆ KHUNG HÌNH =====")
25
+ print(df["AspectRatio"].describe())
26
+
27
+ # -------------------------
28
+ # Histogram chiều cao
29
+ # -------------------------
30
+ plt.figure(figsize=(8, 5))
31
+ plt.hist(df["Rows"], bins=30)
32
+ plt.xlabel("Rows (Height)")
33
+ plt.ylabel("Number of Images")
34
+ plt.title("Distribution of Image Heights")
35
+ plt.grid(True)
36
+ plt.show()
37
+
38
+ # -------------------------
39
+ # Histogram chiều rộng
40
+ # -------------------------
41
+ plt.figure(figsize=(8, 5))
42
+ plt.hist(df["Columns"], bins=30)
43
+ plt.xlabel("Columns (Width)")
44
+ plt.ylabel("Number of Images")
45
+ plt.title("Distribution of Image Widths")
46
+ plt.grid(True)
47
+ plt.show()
48
+
49
+ # -------------------------
50
+ # Histogram tổng pixel
51
+ # -------------------------
52
+ plt.figure(figsize=(8, 5))
53
+ plt.hist(df["TotalPixels"], bins=30)
54
+ plt.xlabel("Total Pixels")
55
+ plt.ylabel("Number of Images")
56
+ plt.title("Distribution of Image Sizes")
57
+ plt.grid(True)
58
+ plt.show()
59
+
60
+ # -------------------------
61
+ # Scatter plot Width vs Height
62
+ # -------------------------
63
+ plt.figure(figsize=(7, 7))
64
+ plt.scatter(df["Columns"], df["Rows"], alpha=0.5)
65
+ plt.xlabel("Width (Columns)")
66
+ plt.ylabel("Height (Rows)")
67
+ plt.title("Image Resolution Distribution")
68
+ plt.grid(True)
69
+ plt.show()
70
+
71
+ # -------------------------
72
+ # Top resolution phổ biến nhất
73
+ # -------------------------
74
+ resolution_counts = (
75
+ df.groupby(["Rows", "Columns"])
76
+ .size()
77
+ .reset_index(name="Count")
78
+ .sort_values("Count", ascending=False)
79
+ )
80
+
81
+ print("\n===== TOP RESOLUTION PHỔ BIẾN =====")
82
+ print(resolution_counts.head(10))
dev/eval_labelers.py CHANGED
@@ -11,15 +11,15 @@ from sklearn.metrics import (
11
  )
12
 
13
  # ── Cấu hình — chỉnh 4 dòng này ──────────────────────────────────────────────
14
- CHEXPERT_PATH = r"mimic-cxr-2.0.0-chexpert.csv.gz"
15
- NEGBIO_PATH = r"mimic-cxr-2.0.0-negbio.csv.gz"
16
- GT_PATH = r"mimic-cxr-2.1.0-test-set-labeled.csv"
17
 
18
  # Cách xử lý nhãn uncertain (-1):
19
  # "positive" → coi là có bệnh (mặc định, conservative)
20
  # "negative" → coi là không có bệnh
21
  # "drop" → bỏ hẳn các study có uncertain
22
- UNCERTAIN_STRATEGY = "positive"
23
  # ─────────────────────────────────────────────────────────────────────────────
24
 
25
  PATHOLOGIES = [
@@ -133,7 +133,7 @@ def main():
133
 
134
  summary = pd.DataFrame([res_chx, res_neg]).set_index("tool")
135
  print("=" * 60)
136
- print("OVERALL METRICS (uncertain strategy: '{}')".format(args.uncertain))
137
  print("=" * 60)
138
  print(summary.to_string(float_format="{:.4f}".format))
139
 
 
11
  )
12
 
13
  # ── Cấu hình — chỉnh 4 dòng này ──────────────────────────────────────────────
14
+ CHEXPERT_PATH = r"D:\USTH\KLTN\cxr-vlm-data\mimic-cxr-2.0.0-chexpert.csv"
15
+ NEGBIO_PATH = r"D:\USTH\KLTN\cxr-vlm-data\mimic-cxr-2.0.0-negbio.csv"
16
+ GT_PATH = r"D:\USTH\KLTN\cxr-vlm-data\mimic-cxr-2.1.0-test-set-labeled.csv"
17
 
18
  # Cách xử lý nhãn uncertain (-1):
19
  # "positive" → coi là có bệnh (mặc định, conservative)
20
  # "negative" → coi là không có bệnh
21
  # "drop" → bỏ hẳn các study có uncertain
22
+ UNCERTAIN_STRATEGY = "negative"
23
  # ─────────────────────────────────────────────────────────────────────────────
24
 
25
  PATHOLOGIES = [
 
133
 
134
  summary = pd.DataFrame([res_chx, res_neg]).set_index("tool")
135
  print("=" * 60)
136
+ print("OVERALL METRICS (uncertain strategy: '{}')".format(UNCERTAIN_STRATEGY))
137
  print("=" * 60)
138
  print(summary.to_string(float_format="{:.4f}".format))
139
 
dev/labeler_comparison.csv ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ,CheXpert_F1,NegBio_F1,Winner,Δ
2
+ Atelectasis,0.9465648854961832,0.9518987341772152,NegBio,-0.0053
3
+ Cardiomegaly,0.8363636363636363,0.8466666666666667,NegBio,-0.0103
4
+ Consolidation,0.832,0.907563025210084,NegBio,-0.0756
5
+ Edema,0.8897058823529411,0.8805970149253731,CheXpert,0.0091
6
+ Enlarged Cardiomediastinum,0.5,0.49019607843137253,CheXpert,0.0098
7
+ Fracture,0.8131868131868132,0.8470588235294118,NegBio,-0.0339
8
+ Lung Lesion,0.7889908256880734,0.7850467289719626,CheXpert,0.0039
9
+ No Finding,0.5376344086021505,0.5252525252525253,CheXpert,0.0124
10
+ Pleural Effusion,0.9265536723163842,0.9274809160305344,NegBio,-0.0009
11
+ Pleural Other,0.4918032786885246,0.5,NegBio,-0.0082
12
+ Pneumonia,0.5362318840579711,0.5801526717557252,NegBio,-0.0439
13
+ Pneumothorax,0.7083333333333334,0.7560975609756098,NegBio,-0.0478
14
+ Support Devices,0.8830645161290323,0.8806584362139918,CheXpert,0.0024