File size: 8,673 Bytes
41c8683 | 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 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | """
Randomly sample N real chest X-rays by base ID, then build combined
train sets with each synthesis model.
Rules
-----
- A "base ID" is the trailing folder name in `file_name`, with any
``-1`` / ``-2`` / ``-N`` suffix stripped. e.g. ``36255-1`` -> ``36255``.
- In the REAL jsonl we ONLY keep the base row (whose raw ID has no
``-N`` suffix). Variant rows like ``36255-1`` are discarded entirely.
- We sample base IDs from the REAL jsonl, but only from base IDs that
ALSO exist in every syn jsonl (cheff / roentgen / roentgenv2 / ours).
This guarantees the four ``+model`` output files share the same ID set.
- Each syn file is assumed to have exactly one row per base ID.
- Output row counts:
train_real{N}.json : exactly N rows
train_real{N}+<model>.json : N real + N syn = 2N rows
- Outputs (jsonl, one record per line — keep extension as requested):
train_real{N}.json # real rows only
train_real{N}+cheff.json # real rows + cheff syn rows
train_real{N}+roentgen.json
train_real{N}+roentgenv2.json
train_real{N}+ours.json
Usage
-----
python sample_real500.py \
--real real.jsonl \
--cheff cheff.jsonl \
--roentgen roentgen.jsonl \
--roentgenv2 roentgenv2.jsonl \
--ours ours.jsonl \
--n 500 \
--seed 42 \
--outdir out/
"""
import argparse
import json
import os
import random
import re
import sys
from collections import defaultdict
# Strip trailing "-<digits>" from the base name (e.g. 36255-1 -> 36255).
# We deliberately only strip a SINGLE numeric suffix, since the variant
# scheme is -1, -2, ... Ids that legitimately contain a dash followed by
# non-digits are left alone.
_SUFFIX_RE = re.compile(r"-\d+$")
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--real", required=True,
help="Path to the real jsonl.")
p.add_argument("--cheff", required=True)
p.add_argument("--roentgen", required=True)
p.add_argument("--roentgenv2", required=True)
p.add_argument("--ours", required=True)
p.add_argument("--n", type=int, default=500,
help="Number of base IDs to sample.")
p.add_argument("--seed", type=int, default=42)
p.add_argument("--outdir", default=".")
p.add_argument("--prefix", default=None,
help="Output filename prefix. Default: train_real{N}")
return p.parse_args()
def extract_id(file_name: str) -> str:
"""
Return the raw ID (folder name) from a record's ``file_name``.
Examples
--------
>>> extract_id("AURAD_dataset/chestdet/dataset/train/36255-1/36255-1.png")
'36255-1'
>>> extract_id("AURAD_infer/det-train-real-mask-image-total-filter/AURA/dataset/train/46038/syn_46038.png")
'46038'
"""
# Take the directory of the image -> last path component.
# This is robust to whether the basename has a "syn_" prefix or not.
parent = os.path.basename(os.path.dirname(file_name))
return parent
def base_id(raw_id: str) -> str:
"""Strip trailing ``-N`` numeric variant suffix."""
return _SUFFIX_RE.sub("", raw_id)
def load_jsonl(path: str):
"""Yield (lineno, record_dict) for each non-empty line."""
with open(path, "r", encoding="utf-8") as f:
for i, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
yield i, json.loads(line)
except json.JSONDecodeError as e:
print(f"[warn] {path}:{i} JSON decode error: {e}",
file=sys.stderr)
def index_real(path: str):
"""
Return {base_id: record} where ``record`` is the base (non-variant) row,
i.e. the row whose raw ID has no ``-N`` suffix.
-1/-2/... variant rows are skipped entirely. If a base ID appears in
the file only as variants (no base row), it is dropped and a warning
is printed.
"""
out = {}
dropped_variant = 0
seen_only_variant = defaultdict(int)
for lineno, rec in load_jsonl(path):
rid = extract_id(rec["file_name"])
bid = base_id(rid)
if rid != bid:
# This is a -1 / -2 / ... variant; skip per spec.
dropped_variant += 1
seen_only_variant[bid] += 1
continue
if bid in out:
# Duplicate base row for the same ID — keep first, warn.
print(f"[warn] real:{lineno} duplicate base row for ID {bid}, "
f"keeping first.", file=sys.stderr)
continue
out[bid] = rec
seen_only_variant.pop(bid, None) # this id has a base row now
if dropped_variant:
print(f"[info] real: skipped {dropped_variant} -N variant row(s).",
file=sys.stderr)
if seen_only_variant:
print(f"[warn] real: {len(seen_only_variant)} base ID(s) had only "
f"variant rows and no base row — they are unusable and dropped.",
file=sys.stderr)
return out
def index_syn(path: str, label: str):
"""
Return {base_id: record}. Warn on duplicates and keep the first.
"""
out = {}
dup = 0
for lineno, rec in load_jsonl(path):
rid = extract_id(rec["file_name"])
bid = base_id(rid)
if bid in out:
dup += 1
continue
out[bid] = rec
if dup:
print(f"[warn] {label}: {dup} duplicate base IDs skipped "
f"(kept first occurrence).", file=sys.stderr)
return out
def write_jsonl(path: str, records):
with open(path, "w", encoding="utf-8") as f:
for r in records:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
def main():
args = parse_args()
os.makedirs(args.outdir, exist_ok=True)
print("Loading real ...")
real_idx = index_real(args.real)
print(f" real: {len(real_idx)} usable base IDs (base rows only)")
syn_paths = {
"cheff": args.cheff,
"roentgen": args.roentgen,
"roentgenv2": args.roentgenv2,
"ours": args.ours,
}
syn_idx = {}
for name, p in syn_paths.items():
print(f"Loading {name} ...")
syn_idx[name] = index_syn(p, name)
print(f" {name}: {len(syn_idx[name])} unique base IDs")
# Candidates = real ∩ every syn
common = set(real_idx.keys())
for name, idx in syn_idx.items():
before = len(common)
common &= set(idx.keys())
print(f"After intersect with {name}: {len(common)} "
f"(dropped {before - len(common)})")
if len(common) < args.n:
raise SystemExit(
f"Only {len(common)} base IDs are common to all sources; "
f"cannot sample {args.n}. Lower --n or check your inputs."
)
rng = random.Random(args.seed)
# Sort for determinism before sampling (set order is nondeterministic).
pool = sorted(common)
sampled = rng.sample(pool, args.n)
sampled_set = set(sampled)
print(f"Sampled {len(sampled)} base IDs (seed={args.seed}).")
# Build real records: exactly one row per sampled base ID (the base row).
real_records = [real_idx[bid] for bid in sampled]
print(f"Real rows in output: {len(real_records)} "
f"(1 per base ID; -N variants discarded)")
prefix = args.prefix or f"train_real{args.n}"
# 1) real-only
real_only_path = os.path.join(args.outdir, f"{prefix}.json")
write_jsonl(real_only_path, real_records)
print(f"Wrote {real_only_path} ({len(real_records)} rows)")
# 2) real + each syn
for name in syn_paths:
syn_records = [syn_idx[name][bid] for bid in sampled]
combined = real_records + syn_records
out_path = os.path.join(args.outdir, f"{prefix}+{name}.json")
write_jsonl(out_path, combined)
print(f"Wrote {out_path} ({len(combined)} rows: "
f"{len(real_records)} real + {len(syn_records)} {name})")
# Sanity log: save the chosen ID list for reproducibility.
ids_path = os.path.join(args.outdir, f"{prefix}.ids.txt")
with open(ids_path, "w") as f:
for bid in sampled:
f.write(bid + "\n")
print(f"Wrote {ids_path}")
if __name__ == "__main__":
main()
"""
python select_500.py \
--real /home/jovyan/AURAD/detection/json/train_real.json \
--cheff /home/jovyan/AURAD/detection/json/train_cheff.json \
--roentgen /home/jovyan/AURAD/detection/json/train_roentgen.json \
--roentgenv2 /home/jovyan/AURAD/detection/json/train_roentgenv2.json \
--ours /home/jovyan/AURAD/detection/json/train_ours.json \
--n 500 \
--seed 42 \
--outdir .
""" |