File size: 6,189 Bytes
b91f38a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/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()