File size: 6,527 Bytes
a2ffd07
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# experiment/probing/build_dctrl.py
"""Build a 1k-image D_ctrl jsonl from CC3M, excluding bathroom/toilet keywords.

Outputs (one line per image):
    {"image_id": "<id>", "image_path": "", "caption": "..."}

Note: CC3M is stored as WebDataset tar shards under
  <root>/preproc/CC3M-Dataset/cc3m-wds-train/cc3m-train-NNNN.tar
Each tar contains triplets: <key>.jpg, <key>.json, <key>.txt
The .txt file holds the caption; .jpg bytes are embedded in the tar
(no separate on-disk image files).  image_path is left as "" because
calibrate_tau.py will open images directly from the tar; downstream
scripts that need actual image bytes should use _open_from_tar().

D_ctrl is consumed by calibrate_tau.py and identify_toilet_features.py
(Method C — differential activation needs a clean control distribution).
"""
from __future__ import annotations

import argparse
import json
import os
import random
import re
import tarfile

EXCLUDE_RE = re.compile(r"\b(bathroom|toilets?|lavatory|restroom)\b", re.IGNORECASE)

# Relative subpath inside the root where the wds shards live.
_WDS_SUBDIR = os.path.join("preproc", "CC3M-Dataset", "cc3m-wds-train")


def main() -> None:
    p = argparse.ArgumentParser()
    p.add_argument("--cc3m_root", required=True, help="Root of CC3M-Dataset")
    p.add_argument("--output", required=True, help="Output jsonl path")
    p.add_argument("--n", type=int, default=1000)
    p.add_argument("--seed", type=int, default=0)
    p.add_argument(
        "--cache_dir",
        default="outputs/feature_ks/d_ctrl_images/",
        help="Directory where extracted JPEG images are cached (one file per image).",
    )
    args = p.parse_args()

    candidates = _scan_cc3m(args.cc3m_root)
    print(f"Scanned {len(candidates)} candidates")
    rng = random.Random(args.seed)
    rng.shuffle(candidates)

    kept: list[dict] = []
    for c in candidates:
        if EXCLUDE_RE.search(c.get("caption", "")):
            continue
        kept.append(c)
        if len(kept) >= args.n:
            break

    if len(kept) < args.n:
        raise RuntimeError(
            f"Only {len(kept)}/{args.n} samples pass the exclusion filter"
        )

    # Extract JPEG bytes for kept records whose image_path is a "tar::key" reference.
    os.makedirs(args.cache_dir, exist_ok=True)
    _extract_images(kept, args.cache_dir)

    out_dir = os.path.dirname(args.output)
    if out_dir:
        os.makedirs(out_dir, exist_ok=True)
    with open(args.output, "w") as f:
        for c in kept:
            f.write(json.dumps(c) + "\n")
    print(f"Wrote {len(kept)}{args.output}")


def _scan_cc3m(root: str) -> list[dict]:
    """Scan CC3M WebDataset tar shards and return (image_id, image_path, caption) dicts.

    Real layout (differs from the JSON-sidecar convention in the plan):
      <root>/preproc/CC3M-Dataset/cc3m-wds-train/cc3m-train-NNNN.tar
    Each tar entry is one of: <key>.jpg | <key>.json | <key>.txt
    The caption is the text in <key>.txt (stripped).
    image_path encodes the shard+key as "<tar_path>::<key>" so callers
    can re-open the tar to extract the image bytes if needed.
    """
    wds_dir = os.path.join(root, _WDS_SUBDIR)
    if not os.path.isdir(wds_dir):
        raise FileNotFoundError(
            f"Expected WebDataset shard directory not found: {wds_dir}"
        )

    out: list[dict] = []
    for fname in sorted(os.listdir(wds_dir)):
        if not fname.endswith(".tar"):
            continue
        tar_path = os.path.join(wds_dir, fname)
        try:
            with tarfile.open(tar_path, "r") as tf:
                # Collect all .txt members; each gives us one (key, caption) pair.
                for member in tf.getmembers():
                    if not member.name.endswith(".txt"):
                        continue
                    key = member.name[:-4]  # strip ".txt"
                    fobj = tf.extractfile(member)
                    if fobj is None:
                        continue
                    caption = fobj.read().decode("utf-8", errors="replace").strip()
                    out.append(
                        {
                            "image_id": key,
                            # Encode location so callers can extract the JPEG.
                            "image_path": f"{tar_path}::{key}.jpg",
                            "caption": caption,
                        }
                    )
        except tarfile.TarError:
            continue
    return out


def _extract_images(records: list[dict], cache_dir: str) -> None:
    """Extract JPEG bytes from tar shards for each record in *records*.

    Records whose ``image_path`` looks like ``"<tar_path>::<entry>"`` are
    extracted to ``<cache_dir>/<image_id>.jpg`` and the dict is updated in
    place.  Records that already point to a real file are left untouched.
    Extraction is skipped when the destination file already exists (idempotent).

    Tars are opened one at a time in sorted order to avoid repeatedly
    reopening the same archive.
    """
    # Group by tar path so we open each shard at most once.
    from collections import defaultdict

    by_tar: dict[str, list[dict]] = defaultdict(list)
    for c in records:
        ip = c.get("image_path", "")
        if "::" in ip:
            tar_path, _entry = ip.split("::", 1)
            by_tar[tar_path].append(c)

    for tar_path in sorted(by_tar):
        group = by_tar[tar_path]
        # Build a lookup: entry_name -> record
        entry_map = {}
        for c in group:
            _tar_path, entry = c["image_path"].split("::", 1)
            dest = os.path.join(cache_dir, c["image_id"] + ".jpg")
            if os.path.exists(dest):
                c["image_path"] = dest  # already cached
            else:
                entry_map[entry] = (c, dest)

        if not entry_map:
            continue

        try:
            with tarfile.open(tar_path, "r") as tf:
                for member in tf.getmembers():
                    if member.name not in entry_map:
                        continue
                    c, dest = entry_map[member.name]
                    fobj = tf.extractfile(member)
                    if fobj is None:
                        continue
                    with open(dest, "wb") as out_f:
                        out_f.write(fobj.read())
                    c["image_path"] = dest
        except tarfile.TarError as exc:
            raise RuntimeError(f"Failed to extract from {tar_path}: {exc}") from exc


if __name__ == "__main__":
    main()