TB-Guard / prepare_data_v3.py
Vignesh19's picture
Upload prepare_data_v3.py with huggingface_hub
40db296 verified
Raw
History Blame Contribute Delete
10.1 kB
"""
TB Dataset Preprocessor v3 - Per-source splitting to prevent data leakage.
Uses pre-extracted IN-CXR PNGs + other dataset zips.
"""
import zipfile
import hashlib
import shutil
import csv
import io
import os
import sys
import random
import argparse
from pathlib import Path
from collections import Counter
from PIL import Image
SEED = 42
TEST_SPLIT = 0.15
VAL_SPLIT = 0.15
def md5(data):
return hashlib.md5(data).hexdigest()
def write_image(dest_dir, stem, data):
ext = ".png"
try:
img = Image.open(io.BytesIO(data))
ext = ".png" if img.format == "PNG" else ".jpg"
except Exception:
pass
path = dest_dir / f"{stem}{ext}"
dest_dir.mkdir(parents=True, exist_ok=True)
with open(path, "wb") as f:
f.write(data)
return path
def extract_images_from_zip(zf):
images = []
for name in zf.namelist():
if name.endswith("/") or "__MACOSX" in name or ".DS_Store" in name:
continue
ext = Path(name).suffix.lower()
if ext not in (".png", ".jpg", ".jpeg"):
continue
images.append((name, zf.read(name)))
return images
def process_incxr_preprocessed(data_dir):
"""Use pre-extracted IN-CXR PNGs from TB_DATASETS/train|val|test."""
print("\n" + "=" * 60)
print("IN-CXR (using pre-extracted PNGs)")
print("=" * 60)
images = []
for split in ["train", "val", "test"]:
for cls_name, label in [("TB", 1), ("Normal", 0)]:
cls_dir = data_dir / split / cls_name
if not cls_dir.exists():
continue
for img_path in cls_dir.glob("*"):
if img_path.suffix.lower() not in (".png", ".jpg", ".jpeg"):
continue
with open(img_path, "rb") as f:
data = f.read()
images.append((md5(data), data, label, "incxr"))
print(f" Total: {len(images)} images")
return images
def process_other_datasets(downloads_dir):
"""Process all other datasets from zip files."""
print("\n" + "=" * 60)
print("Other Datasets")
print("=" * 60)
all_images = []
zip_configs = [
("belarus.zip", "belarus", None),
("DA_DB_tbxpredict.zip", "dadb", None),
("DA_DB_archive.zip", "dadb", None),
("Mendeley_Dataset.zip", "mendeley", None),
("Mendeley_Pakistan_Dataset.zip", "mendeley", None),
("Montgomery_archive.zip", "montgomery", None),
("shenzhen_archive.zip", "shenzhen", None),
("qatar_archive.zip", "qatar", None),
("Sakha-TB_Russia.zip", "sakha", None),
("TBX11K.zip", "tbx11k", "csv"),
("TBX11K_archive.zip", "tbx11k", "csv"),
("Shenzhen + Montgomery_archive.zip", "szmc", None),
]
processed = set()
for zip_name, src_name, mode in zip_configs:
if src_name in processed:
continue
zip_path = downloads_dir / zip_name
if not zip_path.exists():
continue
print(f"\n[{src_name}] {zip_name}")
zf = zipfile.ZipFile(zip_path)
if mode == "csv":
# TBX11K: determine labels by directory structure
# imgs/tb/ -> TB (label 1)
# imgs/health/ -> Normal (label 0)
# imgs/sick/ -> Normal (non-TB disease, label 0)
# imgs/test/ -> no labels (skip)
# imgs/extra/ -> no labels (skip)
source_images = []
label_map = {"tb": 1, "health": 0, "sick": 0}
for name in zf.namelist():
if name.endswith("/") or Path(name).suffix.lower() not in (".png", ".jpg", ".jpeg"):
continue
parts = name.replace("\\", "/").split("/")
if len(parts) < 3:
continue
subdir = parts[2] # e.g., "tb", "health", "sick", "test", "extra"
if subdir not in label_map:
continue
data = zf.read(name)
source_images.append((md5(data), data, label_map[subdir], src_name))
else:
source_images = []
for name, data in extract_images_from_zip(zf):
fname = Path(name).name.lower()
label = None
if src_name == "belarus":
label = 1 if "tb" in fname else 0
elif src_name == "dadb":
if fname.startswith("p") or fname.startswith("px"):
label = 1
elif fname.startswith("n") or fname.startswith("nx"):
label = 0
elif src_name == "mendeley":
if "tb" in name or "TB" in name:
label = 1
elif "normal" in name:
label = 0
elif src_name == "szmc":
if name.lower().endswith("_1.png") or name.lower().endswith("_1.jpg"):
label = 1
elif name.lower().endswith("_0.png") or name.lower().endswith("_0.jpg"):
label = 0
elif src_name in ("montgomery", "shenzhen", "qatar", "sakha"):
stem = Path(name).stem.lower()
if stem.endswith("_1") or "tb" in stem:
label = 1
elif stem.endswith("_0") or "normal" in stem:
label = 0
if label is None:
continue
source_images.append((md5(data), data, label, src_name))
zf.close()
all_images.extend(source_images)
tb = sum(1 for _, _, l, _ in source_images if l == 1)
norm = len(source_images) - tb
print(f" -> {len(source_images)} images ({tb} TB, {norm} Normal)")
processed.add(src_name)
print(f"\nTotal other datasets: {len(all_images)} images")
return all_images
def main():
parser = argparse.ArgumentParser(description="TB Dataset Preprocessor v3")
parser.add_argument("--downloads", type=str,
default=str(Path.home() / "Downloads" / "TB_DATASETS"),
help="Directory with TB datasets")
parser.add_argument("--output", type=str, default="datasets_processed",
help="Output directory")
args = parser.parse_args()
DOWNLOADS = Path(args.downloads)
PROCESSED = Path(args.output)
if not DOWNLOADS.exists():
print(f"ERROR: Downloads directory not found: {DOWNLOADS}")
return
if PROCESSED.exists():
print(f"Removing existing {PROCESSED}...")
shutil.rmtree(PROCESSED)
random.seed(SEED)
# Step 1: IN-CXR pre-extracted
incxr = process_incxr_preprocessed(DOWNLOADS)
# Step 2: Other datasets
other = process_other_datasets(DOWNLOADS)
# Step 3: Group by source
print("\n" + "=" * 60)
print("PER-SOURCE DEDUP + STRATIFIED SPLIT")
print("=" * 60)
by_source = {}
for h, data, label, src in incxr + other:
by_source.setdefault(src, []).append((h, data, label))
def deduplicate(items):
seen = {}
for h, data, label in items:
if h not in seen:
seen[h] = (data, label)
result = list(seen.values())
random.shuffle(result)
return result
def split_group(items):
random.shuffle(items)
n = len(items)
n_test = int(n * TEST_SPLIT)
n_val = int(n * VAL_SPLIT)
test = items[:n_test]
val = items[n_test:n_test + n_val]
train = items[n_test + n_val:]
return train, val, test
train_all, val_all, test_all = [], [], []
for src, items in sorted(by_source.items()):
deduped = deduplicate(items)
tb = [(d, l) for d, l in deduped if l == 1]
norm = [(d, l) for d, l in deduped if l == 0]
tb_train, tb_val, tb_test = split_group(tb)
norm_train, norm_val, norm_test = split_group(norm)
train_all.extend([(d, l, src) for d, l in tb_train + norm_train])
val_all.extend([(d, l, src) for d, l in tb_val + norm_val])
test_all.extend([(d, l, src) for d, l in tb_test + norm_test])
n_tb = sum(1 for _, l in deduped if l == 1)
n_norm = len(deduped) - n_tb
print(f" {src}: {len(deduped)} ({n_tb} TB, {n_norm} Normal) -> train {len(tb_train)+len(norm_train)} | val {len(tb_val)+len(norm_val)} | test {len(tb_test)+len(norm_test)}")
random.shuffle(train_all)
random.shuffle(val_all)
random.shuffle(test_all)
def count_tb(items):
return sum(1 for _, l, _ in items if l == 1)
def count_norm(items):
return sum(1 for _, l, _ in items if l == 0)
print(f"\n Final:")
print(f" Train: {len(train_all)} ({count_tb(train_all)} TB, {count_norm(train_all)} Normal)")
print(f" Val: {len(val_all)} ({count_tb(val_all)} TB, {count_norm(val_all)} Normal)")
print(f" Test: {len(test_all)} ({count_tb(test_all)} TB, {count_norm(test_all)} Normal)")
# Step 4: Write
print("\nWriting datasets_processed/ ...")
for split_name, items in [("train", train_all), ("val", val_all), ("test", test_all)]:
tb_dir = PROCESSED / split_name / "TB"
norm_dir = PROCESSED / split_name / "Normal"
for data, label, src in items:
stem = f"{src}_{md5(data)[:12]}"
write_image(tb_dir if label == 1 else norm_dir, stem, data)
print(f" {split_name}: {count_tb(items)} TB + {count_norm(items)} Normal = {len(items)}")
grand_total = len(train_all) + len(val_all) + len(test_all)
total_tb = count_tb(train_all) + count_tb(val_all) + count_tb(test_all)
total_norm = count_norm(train_all) + count_norm(val_all) + count_norm(test_all)
print(f"\n{'=' * 60}")
print(f"DONE! {grand_total} images ({total_tb} TB, {total_norm} Normal)")
print(f"Output: {PROCESSED.resolve()}")
print(f"Run: python train_ensemble_v2.py")
print(f"{'=' * 60}")
if __name__ == "__main__":
main()