File size: 8,101 Bytes
4c3d957
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python
"""PASS 1 (REFERENCE ENV) — dump the ORACLE-VISIBILITY SS seed latent z1.

Three-pass SSFlow eval driver, pass 1 of 3:

    dump_seed.py      (reference env, production sam3d_objects)  -> z1 per object
    ssflow_coords.py  (TRAINING repo, vendored sam3d_objects)    -> coords per object
    finish_cache.py   (reference env)                            -> the stage-1 npz cache

WHY THREE PROCESSES: ``tools/val_stage1_ref.py`` deliberately pops PYTHONPATH so
that the PRODUCTION ``sam3d_objects`` (/lp-dev/jonghoon/mv-mesh/mv-sam3d) is
imported.  The trained ``SSFlowModel`` lives in the TRAINING repo's vendored
``sam3d_objects``.  Importing both in one interpreter is unsafe, so the pipeline
is split and the two sides only exchange .npz files.

WHAT THIS SCRIPT DOES — steps 1-4 of ``val_stage1_ref.stage1_and_seed()``,
BYTE-IDENTICAL:

  1. loads the input view PNGs ``EXP/inputs/<obj>_<tag>.png`` (existence +
     decodability check; they have NO numerical effect on z1 — in the reference
     they feed only the SS-generator conditioning, which is exactly the part
     pass 2 replaces).
  2. ``gt_vis, gt_full, free = baf.load_gt_synth(Path(exp), obj, views_tags, n=baf.VOX)``
  3. ``rng = np.random.default_rng(seed); occ = baf.build_input_grid(gt_vis, free, "zeros", rng)``
     -- the 64^3 ORACLE-VISIBILITY seed, union over the input views.
  4. ``z1 = baf.grid_to_shape_latent(ss_enc, occ)``  -- the SS-encoded partial
     occupancy == ``z_partial``.

Lines 2-4 are the reference block verbatim; ``noise="zeros"`` consumes nothing
from ``rng``, so z1 is a deterministic function of (exp, obj, views).

DELIBERATE DEVIATION (documented): the reference builds the WHOLE
``baf.Models()`` (production Inference pipeline + SLAT encoder + DINO).  z1 needs
only ``baf.load_ss_encoder()``, so that is all we build.  ``grid_to_shape_latent``
is a pure function of (ss_enc, occ), so the dumped z1 is bit-identical to the
reference's -- just ~100x cheaper to produce.  The da3 pointmaps
(``npz_{n}v/<obj>/da3_output.npz``) are NOT read: they only feed the SS
conditioning that pass 2 replaces.

Output: ``<out>/<obj>.npz`` with
    z1  (1,4096,8) float32   exactly as grid_to_shape_latent produced it
    occ (64,64,64) uint8     the partial-occupancy grid, for the record
    views (V,) str, seed int

Launch (re-execs itself into the reference env; caller sets CUDA_VISIBLE_DEVICES
to ONE gpu -- baf.DEVICE is hardcoded 'cuda'):
  CUDA_VISIBLE_DEVICES=7 python dump_seed.py \
      --exp /lp-dev/jonghoon/mv-mesh/exp_faithfulness/toys4k100_tex \
      --views 2 --out /lp-dev/jonghoon/ss_flow_data/eval_20260916/seed/toys4k100_tex/2v
"""
import os
import sys

ENV_PY = "/lp-dev/jonghoon/mv-mesh/envs/mv-sam3d/bin/python"
ENV = "/lp-dev/jonghoon/mv-mesh/envs/mv-sam3d"
MVMESH = "/lp-dev/jonghoon/mv-mesh"
METRICS_DIR = os.path.join(MVMESH, "metrics")

# --- re-exec preamble: VERBATIM from tools/val_stage1_ref.py -----------------
if not os.environ.get("_VAL_STAGE1_INENV"):
    env = dict(os.environ)
    env["_VAL_STAGE1_INENV"] = "1"
    env["_APPFORCE_SAM3D_INENV"] = "1"       # batch_appforce must not re-exec
    env.pop("PYTHONPATH", None)              # never the training repo's sam3d_objects
    env["PATH"] = os.path.join(ENV, "bin") + os.pathsep + env.get("PATH", "")
    assert env.get("CUDA_VISIBLE_DEVICES"), "set CUDA_VISIBLE_DEVICES to one GPU"
    os.execve(ENV_PY, [ENV_PY, os.path.abspath(__file__)] + sys.argv[1:], env)

os.environ["_APPFORCE_SAM3D_INENV"] = "1"
sys.path.insert(0, METRICS_DIR)
import batch_appforce_sam3d as baf  # noqa: E402  (chdir REPO, sys.path, env defaults)

import argparse  # noqa: E402
import json      # noqa: E402
import time      # noqa: E402
import traceback  # noqa: E402
from pathlib import Path  # noqa: E402

import numpy as np  # noqa: E402
import torch        # noqa: E402
from PIL import Image  # noqa: E402

# --- VIEW TAG TABLE: verbatim from tools/val_stage1_ref.py ------------------
VIEW_TAGS = ["front", "side", "back", "oside", "top", "bottom", "top2", "bottom2"]


def views_tags_for(n: int):
    assert 1 <= n <= len(VIEW_TAGS), f"--views {n} unsupported (max {len(VIEW_TAGS)})"
    return VIEW_TAGS[:n]


def _objects(args):
    """Verbatim from tools/val_stage1_ref.py::_objects."""
    if args.objects_file:
        objs = [l.strip() for l in open(args.objects_file) if l.strip()]
    elif args.objects:
        objs = list(args.objects)
    else:
        data = json.loads(Path(os.path.join(args.exp, "selection.json")).read_text())
        sels = data["selections"] if isinstance(data, dict) else data
        objs = [s["object"] for s in sels]
    if args.limit:
        objs = objs[: args.limit]
    if args.nshards > 1:
        objs = objs[args.shard::args.nshards]
    return objs


def seed_for_object(ss_enc, obj, exp, inputs_dir, views_tags, seed):
    """Steps 1-4 of val_stage1_ref.stage1_and_seed, unchanged."""
    # ---- step 1 (verbatim; existence/decodability check only) --------------
    for t in views_tags:
        p = os.path.join(inputs_dir, f"{obj}_{t}.png")
        if not os.path.isfile(p):
            raise FileNotFoundError(p)
        np.array(Image.open(p).convert("RGBA"))

    # ---- steps 2-4 (verbatim) ----------------------------------------------
    gt_vis, gt_full, free = baf.load_gt_synth(Path(exp), obj, views_tags, n=baf.VOX)
    rng = np.random.default_rng(seed)
    occ = baf.build_input_grid(gt_vis, free, "zeros", rng)
    z1 = baf.grid_to_shape_latent(ss_enc, occ)

    return dict(z1=z1.detach().float().cpu().numpy(),      # (1,4096,8) float32
                occ=(occ > 0.5).astype(np.uint8))          # (64,64,64) uint8


def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--exp", required=True,
                    help="EXPDIR (renders/, inputs/, selection.json)")
    ap.add_argument("--views", type=int, default=2,
                    help="number of condition views (1..8); tags = VIEW_TAGS[:views]")
    ap.add_argument("--out", required=True, help="seed dump dir (<obj>.npz each)")
    ap.add_argument("--seed", type=int, default=42)
    ap.add_argument("--objects", nargs="+", default=None)
    ap.add_argument("--objects-file", default=None)
    ap.add_argument("--limit", type=int, default=0)
    ap.add_argument("--shard", type=int, default=0)
    ap.add_argument("--nshards", type=int, default=1)
    ap.add_argument("--force", action="store_true")
    args = ap.parse_args()

    exp = os.path.abspath(args.exp)
    inputs_dir = os.path.join(exp, "inputs")
    views_tags = views_tags_for(args.views)
    out = Path(args.out)
    out.mkdir(parents=True, exist_ok=True)
    objs = _objects(args)
    print(f"[seed] exp={exp} views={args.views} n_obj={len(objs)} "
          f"shard={args.shard}/{args.nshards} seed={args.seed} "
          f"CUDA={os.environ.get('CUDA_VISIBLE_DEVICES')}", flush=True)
    t0 = time.time()
    ss_enc = baf.load_ss_encoder()
    print(f"[seed] ss_encoder ready {time.time() - t0:.1f}s", flush=True)

    n_ok = n_fail = n_skip = 0
    for i, obj in enumerate(objs, 1):
        f = out / f"{obj}.npz"
        if f.is_file() and not args.force:
            n_skip += 1
            continue
        t1 = time.time()
        try:
            d = seed_for_object(ss_enc, obj, exp, inputs_dir, views_tags, args.seed)
            tmp = str(f) + ".tmp.npz"
            np.savez_compressed(tmp, **d, views=np.array(views_tags), seed=args.seed)
            os.replace(tmp, f)
            n_ok += 1
            print(f"[seed {i}/{len(objs)}] OK {obj} occ={int(d['occ'].sum())} "
                  f"z1={d['z1'].shape} {time.time() - t1:.1f}s", flush=True)
        except Exception:
            n_fail += 1
            traceback.print_exc()
            print(f"[seed {i}/{len(objs)}] FAIL {obj}", flush=True)
        torch.cuda.empty_cache()
    print(f"DUMPSEED DONE ok={n_ok} fail={n_fail} skip={n_skip} total={len(objs)}",
          flush=True)


if __name__ == "__main__":
    main()