File size: 13,854 Bytes
1d4bac8 | 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 | """
Audit the legacy dataset tree against the repository dataset and (optionally) complete the
repository dataset with every image used in the paper, plus split manifests.
Step 1 - audit only (nothing is copied):
python -m experiments.prepare_dataset --legacy-root C:/Users/Kaveh/Documents/cellforce/dataset
Step 2 - copy missing images into S2F/dataset and write the manifests:
python -m experiments.prepare_dataset --legacy-root C:/Users/Kaveh/Documents/cellforce/dataset --write
What the audit answers (written to experiments/results/dataset_audit/):
counts.csv number of samples per legacy and repository set
published_vs_legacy.csv is S2F/dataset/singlecell (236/46) identical to legacy fibro_split? spheroids?
alltune_composition.csv where every image of all_tune/train (280) and all_tune/test (81) comes from
missing_from_published.csv all_tune images that are NOT in S2F/dataset (i.e. not in the Zenodo v1 deposit)
overlap_matrix.csv shared bright-field images between every pair of sets (leakage check)
substrate_codes.csv substrate code the original code derived from each all_tune folder name
report.txt human-readable summary
Images are matched by the MD5 of the bright-field file, so renamed/copied folders are still recognised.
With --write:
dataset/singlecell_extra/<set>/<folder>/ all_tune images missing from dataset/singlecell
dataset/singlecell_external/<set>/<folder>/ independent test sets
experiments/splits/*.csv manifests (commit these to git):
alltune_train.csv, alltune_val.csv split of the released single-cell model (280 / 81)
zenodo_v1_train.csv, zenodo_v1_test.csv published 6 kPa split (236 / 46)
spheroid_train.csv, spheroid_val.csv spheroid split (129 / 28)
test_<set>.csv one per independent test set
"""
import argparse
import collections
import csv
import os
import shutil
from experiments.layout import (DEFAULT_DATASET_ROOT, LEGACY_SETS, PUBLISHED_SETS, RESULTS_DIR,
legacy_code_to_config_key, legacy_substrate_code, manifest_path,
md5, nominal_config_key, sample_folders, write_manifest)
def index_tree(root, rel):
"""List samples of one set: [{folder, bf, hm, dir, md5}]."""
out = []
for folder, bf, hm in sample_folders(os.path.join(root, rel)):
out.append({"folder": folder, "bf": bf, "hm": hm, "dir": os.path.dirname(bf), "md5": md5(bf)})
return out
def discover_unlisted(root, listed_rel, max_depth=4):
"""Directories under root that contain sample folders but are not in LEGACY_SETS (so nothing is missed)."""
found = []
listed = {os.path.normpath(os.path.join(root, r)) for r in listed_rel}
base_depth = os.path.normpath(root).count(os.sep)
for dp, dn, _ in os.walk(root):
if os.path.normpath(dp).count(os.sep) - base_depth >= max_depth:
dn[:] = []
continue
if os.path.normpath(dp) in listed:
dn[:] = []
continue
n = sum(1 for _ in sample_folders(dp))
if n:
found.append((os.path.relpath(dp, root), n))
dn[:] = []
return found
def row_for(sample_dir, split, set_name, meta, legacy_folder, bf_md5):
code = legacy_substrate_code(legacy_folder) if legacy_folder else ""
return {"sample_dir": sample_dir.replace(os.sep, "/"), "split": split, "set": set_name,
"condition": meta.get("condition", ""), "cell_type": meta.get("cell_type", ""),
"coating": meta.get("coating", ""), "young_Pa": meta.get("young_Pa", ""),
"tfm_pixelsize": meta.get("tfm_pixelsize", ""), "ythreshold_Pa": meta.get("ythreshold_Pa", ""),
"legacy_folder": legacy_folder, "legacy_substrate_code": code,
"substrate_legacy": legacy_code_to_config_key(code) if meta.get("cell_type") != "LS174T spheroid" else "",
"substrate_nominal": (nominal_config_key(meta.get("young_Pa"), meta.get("tfm_pixelsize"), meta.get("cell_type"))
if meta.get("cell_type") != "LS174T spheroid" else "") or "",
"bf_md5": bf_md5}
def copy_sample(src_dir, dst_dir, expected_md5):
if os.path.isdir(dst_dir):
bf = [f for f in os.listdir(dst_dir) if f.endswith("BF_001.tif")]
if bf and md5(os.path.join(dst_dir, bf[0])) == expected_md5:
return "exists"
raise RuntimeError(f"{dst_dir} exists with different content; refusing to overwrite")
os.makedirs(dst_dir)
for f in os.listdir(src_dir):
if os.path.isfile(os.path.join(src_dir, f)):
shutil.copy2(os.path.join(src_dir, f), os.path.join(dst_dir, f))
return "copied"
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--legacy-root", required=True, help="old dataset folder (contains single_cell/ and spheroid/)")
ap.add_argument("--dataset-root", default=DEFAULT_DATASET_ROOT, help="repository dataset (default: S2F/dataset)")
ap.add_argument("--write", action="store_true", help="copy missing images and write split manifests")
ap.add_argument("--out", default=os.path.join(RESULTS_DIR, "dataset_audit"))
a = ap.parse_args()
os.makedirs(a.out, exist_ok=True)
rep = []
def say(s=""):
print(s); rep.append(s)
# ---------------- index ----------------
legacy = {k: index_tree(a.legacy_root, v[0]) for k, v in LEGACY_SETS.items()}
pub = {k: index_tree(a.dataset_root, v) for k, v in PUBLISHED_SETS.items()}
say(f"legacy root : {a.legacy_root}\ndataset root: {a.dataset_root}\n")
with open(os.path.join(a.out, "counts.csv"), "w", newline="") as f:
w = csv.writer(f); w.writerow(["tree", "set", "path", "n_samples", "n_unique_bf"])
for k, v in legacy.items():
w.writerow(["legacy", k, LEGACY_SETS[k][0], len(v), len({s['md5'] for s in v})])
say(f" legacy {k:20s} n={len(v):4d}")
for k, v in pub.items():
w.writerow(["repository", k, PUBLISHED_SETS[k], len(v), len({s['md5'] for s in v})])
say(f" repository {k:19s} n={len(v):4d}")
unlisted = discover_unlisted(a.legacy_root, [v[0] for v in LEGACY_SETS.values()])
if unlisted:
say("\nFolders with samples in the legacy tree that are NOT listed in experiments/layout.py:")
for rel, n in unlisted:
say(f" {rel} ({n} samples)")
if not legacy["all_tune/train"]:
say("\nERROR: single_cell/all_tune/train not found under --legacy-root."); open(os.path.join(a.out, "report.txt"), "w").write("\n".join(rep)); return
# ---------------- published vs legacy ----------------
say("\nRepository dataset vs legacy:")
rows = []
for p, l in [("singlecell/train", "fibro_split/train"), ("singlecell/test", "fibro_split/test"),
("spheroid/train", "spheroid/train"), ("spheroid/test", "spheroid/test")]:
P, Lg = {s["md5"] for s in pub[p]}, {s["md5"] for s in legacy[l]}
r = {"repository_set": p, "legacy_set": l, "n_repository": len(P), "n_legacy": len(Lg),
"shared": len(P & Lg), "only_repository": len(P - Lg), "only_legacy": len(Lg - P)}
rows.append(r); say(f" {p:18s} vs {l:18s}: shared {r['shared']}, only repo {r['only_repository']}, only legacy {r['only_legacy']}")
with open(os.path.join(a.out, "published_vs_legacy.csv"), "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
# ---------------- all_tune composition ----------------
by_md5_pub = {s["md5"]: (k, s) for k, v in pub.items() for s in v}
src_keys = [k for k, v in LEGACY_SETS.items() if v[1] in ("published", "source", "external")]
by_md5_leg = collections.defaultdict(list)
for k in src_keys:
for s in legacy[k]:
by_md5_leg[s["md5"]].append((k, s))
comp, missing, codes = [], [], collections.Counter()
for split_key, split in [("all_tune/train", "train"), ("all_tune/test", "val")]:
for s in legacy[split_key]:
in_pub = by_md5_pub.get(s["md5"])
srcs = [k for k, _ in by_md5_leg.get(s["md5"], [])]
src = next((k for k in srcs if LEGACY_SETS[k][1] != "external"), srcs[0] if srcs else "UNMATCHED")
code = legacy_substrate_code(s["folder"])
codes[(split, code)] += 1
r = {"alltune_split": split, "alltune_folder": s["folder"], "source_legacy_set": src,
"all_matching_sets": ";".join(srcs), "in_repository_dataset": in_pub[0] if in_pub else "",
"legacy_substrate_code": code, "bf_md5": s["md5"]}
comp.append(r)
if not in_pub:
missing.append(r)
for name, data in [("alltune_composition.csv", comp), ("missing_from_published.csv", missing)]:
with open(os.path.join(a.out, name), "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=list(comp[0])); w.writeheader(); w.writerows(data)
with open(os.path.join(a.out, "substrate_codes.csv"), "w", newline="") as f:
w = csv.writer(f); w.writerow(["alltune_split", "legacy_substrate_code", "n", "config_key_fed_to_model"])
for (sp, c), n in sorted(codes.items()):
w.writerow([sp, c, n, legacy_code_to_config_key(c)])
say("\nall_tune composition (split x source set):")
cc = collections.Counter((r["alltune_split"], r["source_legacy_set"]) for r in comp)
for (sp, src), n in sorted(cc.items()):
say(f" {sp:5s} {src:20s} {n:4d}")
say(f"\nall_tune images NOT in the repository dataset (Zenodo v1): {len(missing)}")
for (sp, src), n in sorted(collections.Counter((r['alltune_split'], r['source_legacy_set']) for r in missing).items()):
say(f" {sp:5s} {src:20s} {n:4d}")
say("\nSubstrate code the original code fed for all_tune folders (folder_name[:4]):")
for (sp, c), n in sorted(codes.items()):
say(f" {sp:5s} '{c}' -> {legacy_code_to_config_key(c):18s} {n:4d}")
# ---------------- overlaps / leakage ----------------
names = list(legacy)
sets = {k: {s["md5"] for s in legacy[k]} for k in names}
with open(os.path.join(a.out, "overlap_matrix.csv"), "w", newline="") as f:
w = csv.writer(f); w.writerow(["set"] + names)
for x in names:
w.writerow([x] + [len(sets[x] & sets[y]) for y in names])
tv = sets["all_tune/train"] | sets["all_tune/test"]
say("\nLeakage check - independent test sets vs all_tune train+val (must be 0):")
for k, v in LEGACY_SETS.items():
if v[1] == "external":
say(f" {k:20s} {len(sets[k] & tv):3d} (of {len(sets[k])})")
say("Validation-set content - legacy 'test' folders inside all_tune/test (val):")
for k in ["fibro_split/test", "1kpa_new/test", "10kpa_new/test"]:
say(f" {k:20s} {len(sets[k] & sets['all_tune/test']):3d} of {len(sets[k])} in val; {len(sets[k] & sets['all_tune/train']):3d} in train")
# ---------------- write ----------------
if a.write:
say("\nCopying and writing manifests ...")
n_copied = collections.Counter()
alltune_rows = {"train": [], "val": []}
for r, s in zip(comp, legacy["all_tune/train"] + legacy["all_tune/test"]):
split, src = r["alltune_split"], r["source_legacy_set"]
meta = LEGACY_SETS[src][3] if src in LEGACY_SETS else {}
if r["in_repository_dataset"]:
pk, ps = by_md5_pub[s["md5"]]
sample_dir = os.path.join(PUBLISHED_SETS[pk], ps["folder"])
else:
clean = LEGACY_SETS[src][2] if src in LEGACY_SETS and LEGACY_SETS[src][2] else "unmatched"
sample_dir = os.path.join("singlecell_extra", clean, s["folder"])
n_copied[copy_sample(s["dir"], os.path.join(a.dataset_root, sample_dir), s["md5"])] += 1
alltune_rows[split].append(row_for(sample_dir, split, src, meta, s["folder"], s["md5"]))
write_manifest(manifest_path("alltune_train"), alltune_rows["train"])
write_manifest(manifest_path("alltune_val"), alltune_rows["val"])
for pk, man, lk in [("singlecell/train", "zenodo_v1_train", "fibro_split/train"),
("singlecell/test", "zenodo_v1_test", "fibro_split/test"),
("spheroid/train", "spheroid_train", "spheroid/train"),
("spheroid/test", "spheroid_val", "spheroid/test")]:
meta = LEGACY_SETS[lk][3]
write_manifest(manifest_path(man), [row_for(os.path.join(PUBLISHED_SETS[pk], s["folder"]), man.split("_")[-1],
lk, meta, s["folder"], s["md5"]) for s in pub[pk]])
for k, (rel, role, clean, meta) in LEGACY_SETS.items():
if role != "external" or not legacy[k]:
continue
rows_ = []
for s in legacy[k]:
sample_dir = os.path.join("singlecell_external", clean, s["folder"])
n_copied[copy_sample(s["dir"], os.path.join(a.dataset_root, sample_dir), s["md5"])] += 1
rows_.append(row_for(sample_dir, "test", k, meta, s["folder"], s["md5"]))
write_manifest(manifest_path(f"test_{clean}"), rows_)
say(f" sample folders: {dict(n_copied)}")
say(f" manifests written to experiments/splits/ - commit them to git")
else:
say("\n(audit only - rerun with --write to copy the missing images and write experiments/splits/*.csv)")
open(os.path.join(a.out, "report.txt"), "w").write("\n".join(rep))
print(f"\nReport: {os.path.join(a.out, 'report.txt')}")
if __name__ == "__main__":
main()
|