HMCLIP / scripts /prepare_cxr_from_single_json.py
SteveZeyuZhang's picture
Add source files and streaming backup restore instructions
b91f38a
Raw History Blame Contribute Delete
6.19 kB
#!/usr/bin/env python3
"""
prepare_cxr_from_single_json.py
Utility to split a single JSON/JSONL dataset into train/valid and prepare
the minimal files expected by the CXR-only training path in RadIR:
- train.jsonl / valid.jsonl : each line JSON with keys `name` and `img_path`
- csv/all.csv : contains a `File Path` column listing sample names (train set)
- npy/all.npy : similarity matrix (float32, values in [0,1]) for the CSV rows
By default similarity is computed with TF-IDF + cosine similarity. If you
have `sentence-transformers` installed you can pass `--sim-method st` to use
a semantic embedding model (better but requires the package and a model).
Examples:
python scripts/prepare_cxr_from_single_json.py \
--input dataset/all.jsonl \
--out /tmp/radir_debug --split 0.9 --seed 42 --sim-method tfidf
"""
import os
import sys
import json
import argparse
from pathlib import Path
import numpy as np
def read_input(path):
path = Path(path)
text = path.read_text()
# Try JSONL (one json per line)
try:
lines = [l.strip() for l in text.splitlines() if l.strip()]
data = [json.loads(l) for l in lines]
return data
except Exception:
pass
# Try a single JSON array
try:
data = json.loads(text)
if isinstance(data, list):
return data
except Exception:
pass
raise ValueError(f"Unsupported input format for {path}. Expect JSONL or JSON array of objects.")
def ensure_name_field(entry):
if 'name' in entry:
return entry['name']
if 'img_path' in entry:
return os.path.basename(entry['img_path'])
raise ValueError('Entry lacks both `name` and `img_path`')
def save_jsonl(entries, path):
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
with p.open('w') as f:
for e in entries:
f.write(json.dumps(e, ensure_ascii=False) + '\n')
def save_csv_filepaths(names, path):
import csv
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
with p.open('w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['File Path'])
for n in names:
writer.writerow([n])
def compute_similarity_tfidf(texts):
try:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
except Exception as e:
raise RuntimeError('TF-IDF dependencies missing: install scikit-learn') from e
vec = TfidfVectorizer(max_features=8192)
X = vec.fit_transform(texts)
sim = cosine_similarity(X)
# clamp to [0,1]
sim = (sim + 1.0) / 2.0
sim = np.clip(sim, 0.0, 1.0).astype(np.float32)
return sim
def compute_similarity_sentence_transformers(texts, model_name='all-MiniLM-L6-v2'):
try:
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
except Exception as e:
raise RuntimeError('sentence-transformers or sklearn not installed') from e
model = SentenceTransformer(model_name)
emb = model.encode(texts, convert_to_numpy=True, show_progress_bar=False)
sim = cosine_similarity(emb)
sim = (sim + 1.0) / 2.0
sim = np.clip(sim, 0.0, 1.0).astype(np.float32)
return sim
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--input', required=True, help='path to input JSON or JSONL file (list of samples)')
parser.add_argument('--out', required=True, help='output directory to write train/valid/json/csv/npy')
parser.add_argument('--split', type=float, default=0.9, help='train fraction')
parser.add_argument('--seed', type=int, default=42)
parser.add_argument('--sim-method', choices=['tfidf', 'st'], default='tfidf', help='similarity method: tfidf (fast) or st (sentence-transformers)')
parser.add_argument('--st-model', default='all-MiniLM-L6-v2', help='sentence-transformers model name (if --sim-method st)')
args = parser.parse_args()
data = read_input(args.input)
# Normalize entries: ensure fields name, img_path, text (text optional)
entries = []
for d in data:
e = dict(d)
if 'img_path' not in e and 'image' in e:
e['img_path'] = e.pop('image')
if 'name' not in e:
e['name'] = ensure_name_field(e)
entries.append(e)
import random
random.seed(args.seed)
indices = list(range(len(entries)))
random.shuffle(indices)
n_train = int(len(entries) * args.split)
train_idx = set(indices[:n_train])
train_entries = [entries[i] for i in indices[:n_train]]
valid_entries = [entries[i] for i in indices[n_train:]]
out = Path(args.out)
out.mkdir(parents=True, exist_ok=True)
# For RadIR Stage2 we need train.jsonl/valid.jsonl entries with keys name & img_path
train_jsonl = out / 'train.jsonl'
valid_jsonl = out / 'valid.jsonl'
save_jsonl([{'name':e['name'], 'img_path': e['img_path'], 'text': e.get('text','')} for e in train_entries], train_jsonl)
save_jsonl([{'name':e['name'], 'img_path': e['img_path'], 'text': e.get('text','')} for e in valid_entries], valid_jsonl)
# Create csv dir and npy dir
csv_dir = out / 'csv'
npy_dir = out / 'npy'
csv_dir.mkdir(parents=True, exist_ok=True)
npy_dir.mkdir(parents=True, exist_ok=True)
# CSV should list File Path as names (use train set by default)
names = [e['name'] for e in train_entries]
save_csv_filepaths(names, csv_dir / 'all.csv')
# compute similarity matrix from texts (train set rows order)
texts = [e.get('text','') or '' for e in train_entries]
if len(texts) == 0:
sim = np.eye(len(names), dtype=np.float32)
else:
if args.sim_method == 'st':
sim = compute_similarity_sentence_transformers(texts, model_name=args.st_model)
else:
sim = compute_similarity_tfidf(texts)
np.save(npy_dir / 'all.npy', sim.astype(np.float32))
print('Wrote:')
print(' -', train_jsonl)
print(' -', valid_jsonl)
print(' -', csv_dir / 'all.csv')
print(' -', npy_dir / 'all.npy')
if __name__ == '__main__':
main()