File size: 8,401 Bytes
37fbec9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
"""
data/download.py
----------------
Downloads the NIH ChestX-ray14 dataset from Kaggle using the Kaggle API.

SETUP (one-time):
  1. Create a Kaggle account at https://www.kaggle.com
  2. Go to: Account β†’ Settings β†’ Create New API Token β†’ downloads kaggle.json
  3. Place kaggle.json at:
       Windows : C:\\Users\\<YourName>\\.kaggle\\kaggle.json
       Linux   : ~/.kaggle/kaggle.json
  4. Run:  python data/download.py

Dataset: nih-chest-xrays/data
  - 112,120 frontal chest X-ray images (PNG, 1024Γ—1024)
  - Data_Entry_2017.csv  β€” image metadata + labels
  - train_val_list.txt   β€” official train+val split
  - test_list.txt        β€” official test split
  Total size: ~42 GB (compressed ~11 GB β€” the script downloads zip chunks)
"""

import os
import sys
import zipfile
import shutil
from pathlib import Path
from typing import Optional

# ── Rich for pretty console output ───────────────────────────────────────────
try:
    from rich.console import Console
    from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
    from rich.panel import Panel

    console = Console()
except ImportError:
    import builtins

    class _FallbackConsole:
        def print(self, *args, **kwargs):
            builtins.print(*args)
        def rule(self, *args, **kwargs):
            builtins.print("─" * 60)

    console = _FallbackConsole()


# ── Constants ─────────────────────────────────────────────────────────────────
KAGGLE_DATASET = "nih-chest-xrays/data"
DEFAULT_RAW_DIR = Path("./data/raw")

REQUIRED_FILES = [
    "Data_Entry_2017.csv",
    "train_val_list.txt",
    "test_list.txt",
]


def _check_kaggle_credentials() -> bool:
    """Verify kaggle.json exists and is readable."""
    kaggle_dir = Path.home() / ".kaggle"
    kaggle_json = kaggle_dir / "kaggle.json"
    if not kaggle_json.exists():
        console.print(
            "[bold red]βœ— kaggle.json not found![/bold red]\n"
            f"  Expected location: {kaggle_json}\n\n"
            "  Steps to fix:\n"
            "  1. Visit https://www.kaggle.com β†’ Account β†’ Settings\n"
            "  2. Click 'Create New API Token' β†’ downloads kaggle.json\n"
            f"  3. Move it to: {kaggle_dir}\n"
            "  4. Re-run this script.",
            style="red",
        )
        return False
    # Ensure correct permissions (important on Linux/macOS)
    if os.name != "nt":
        kaggle_json.chmod(0o600)
    return True


def _verify_images_dir(raw_dir: Path) -> bool:
    """
    Check if images are already extracted to avoid re-downloading.
    We consider extraction complete if >1000 .png files exist.
    """
    images_dir = raw_dir / "images"
    if not images_dir.exists():
        return False
    png_count = sum(1 for _ in images_dir.glob("*.png"))
    console.print(f"  Found {png_count:,} existing PNG files in {images_dir}")
    return png_count > 1000


def download_dataset(
    raw_dir: Optional[Path] = None,
    force: bool = False,
) -> Path:
    """
    Download and extract NIH ChestX-ray14 from Kaggle.

    Args:
        raw_dir: Directory to extract data into.
        force:   Re-download even if files already exist.

    Returns:
        Path to the extracted raw data directory.
    """
    raw_dir = Path(raw_dir or DEFAULT_RAW_DIR)
    raw_dir.mkdir(parents=True, exist_ok=True)

    # ── 1. Check credentials ──────────────────────────────────────────────────
    console.print(Panel("[bold cyan]NIH ChestX-ray14 β€” Dataset Download[/bold cyan]",
                        subtitle="Kaggle API"))
    if not _check_kaggle_credentials():
        sys.exit(1)

    # ── 2. Check if already downloaded ───────────────────────────────────────
    required_present = all((raw_dir / f).exists() for f in REQUIRED_FILES)
    images_present = _verify_images_dir(raw_dir)

    if required_present and images_present and not force:
        console.print("[bold green]βœ“ Dataset already downloaded and extracted.[/bold green]")
        console.print(f"  Data root: {raw_dir.resolve()}")
        return raw_dir

    # ── 3. Import kaggle (after credentials check) ────────────────────────────
    try:
        import kaggle  # noqa: F401 β€” triggers credential load
        from kaggle.api.kaggle_api_extended import KaggleApiExtended
    except ImportError:
        console.print("[red]βœ— kaggle package not installed. Run: pip install kaggle[/red]")
        sys.exit(1)

    api = KaggleApiExtended()
    api.authenticate()

    # ── 4. Download dataset zip(s) ────────────────────────────────────────────
    console.print(f"\n[yellow]⬇ Downloading '{KAGGLE_DATASET}' β†’ {raw_dir.resolve()}[/yellow]")
    console.print("  ⚠ This dataset is ~42 GB. Download time depends on your connection.")
    console.print("  ⚠ The Kaggle API downloads each image zip file sequentially.\n")

    api.dataset_download_files(
        KAGGLE_DATASET,
        path=str(raw_dir),
        unzip=False,     # We handle extraction ourselves for better control
        quiet=False,
    )
    console.print("[green]βœ“ Download complete.[/green]")

    # ── 5. Extract all zip files ──────────────────────────────────────────────
    images_dir = raw_dir / "images"
    images_dir.mkdir(exist_ok=True)

    zip_files = sorted(raw_dir.glob("*.zip")) + sorted(raw_dir.glob("images_*.tar.gz"))
    console.print(f"\n[yellow]πŸ“¦ Extracting {len(zip_files)} archive(s)...[/yellow]")

    for zip_path in zip_files:
        console.print(f"  Extracting: {zip_path.name}")
        if zip_path.suffix == ".zip":
            with zipfile.ZipFile(zip_path, "r") as zf:
                # Extract images to images_dir, metadata to raw_dir
                for member in zf.namelist():
                    if member.endswith(".png"):
                        # Extract flat into images_dir
                        target = images_dir / Path(member).name
                        if not target.exists():
                            with zf.open(member) as src, open(target, "wb") as dst:
                                shutil.copyfileobj(src, dst)
                    elif not Path(member).is_dir():
                        zf.extract(member, raw_dir)
        console.print(f"    βœ“ {zip_path.name} extracted.")

    # ── 6. Verify essential files ─────────────────────────────────────────────
    console.rule("Verification")
    all_ok = True
    for fname in REQUIRED_FILES:
        fpath = raw_dir / fname
        status = "[green]βœ“[/green]" if fpath.exists() else "[red]βœ— MISSING[/red]"
        console.print(f"  {status} {fname}")
        if not fpath.exists():
            all_ok = False

    png_count = sum(1 for _ in images_dir.glob("*.png"))
    console.print(f"  [cyan]PNG images found: {png_count:,}[/cyan]")

    if all_ok and png_count > 1000:
        console.print("\n[bold green]πŸŽ‰ Dataset ready![/bold green]")
        console.print(f"  Data root : {raw_dir.resolve()}")
        console.print(f"  Images    : {images_dir.resolve()}")
    else:
        console.print("\n[bold red]⚠ Some files may be missing. Check the output above.[/bold red]")

    return raw_dir


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Download NIH ChestX-ray14 from Kaggle")
    parser.add_argument(
        "--output-dir", type=str, default=str(DEFAULT_RAW_DIR),
        help="Directory to save the dataset (default: ./data/raw)",
    )
    parser.add_argument(
        "--force", action="store_true",
        help="Re-download even if dataset already exists",
    )
    args = parser.parse_args()
    download_dataset(raw_dir=Path(args.output_dir), force=args.force)