#!/usr/bin/env python """PASS 2 (TRAINING REPO) — trained SSFlowModel seed+imgcond -> stage-1 coords. Three-pass SSFlow eval driver, pass 2 of 3 (see dump_seed.py for the design). Replaces ONLY step 5 of ``tools/val_stage1_ref.py::stage1_and_seed`` (the pretrained SAM3D ``ss_generator`` sampling) with the newly trained ``SSFlowModel`` (seed + single-view image condition). Everything before it (the oracle-visibility 64^3 seed -> ``z1``) arrives as a .npz from pass 1; everything after it (stage-2 base noise + appearance seed) happens in pass 3. MODEL Built exactly as ``mvsam3d/train/validate.py`` / ``.debug_ssflow_launch/ batched_verify.py`` build it, from the ``config`` payload STORED INSIDE the checkpoint (authoritative: it is the config the run actually trained with), then the trained weights are loaded the way ``mvsam3d/train/val_appforce.py::_load_model`` does it: payload["trainable_state_dict"], strict=False, assert no unexpected keys. The frozen parts (DINO towers, PointPatchEmbed, d_embedder) come from the warm-start ``pretrained_ckpt`` (ss_generator.ckpt) applied in the constructor. ``ema.enable=false`` for this run, so there is NO ema sidecar and none is required (``--use-ema`` opts in if a sidecar ever appears). CONDITION (ONE view: the PRIMARY/front tag) The training config pins ``data.num_views_min == num_views_max == 1``, so the condition is a single view, V=1, anchor == itself. The tensors are built with the DATASET's OWN helpers (never re-implemented): SSImgCondDataset._full_frame_rgba(png, bbox, res_h, res_w) -> full-frame RGBA ss_preprocess.preprocess_ss_view(rgba, pointmap=None) -> image/rgb_image/ mask/rgb_image_mask ss_preprocess.ss_crop_box(alpha, box_size_factor=1.2) -> crop rect ss_imgcond_dataset.pad_square_rect(H, W) -> full rect mv_ss_condition.plucker_rays_for_view(...) -> the 4 RAY_KEYS grids This is exactly the ``_rgb_assets`` TOYS4K1K branch of the dataset, which already targets THIS eval layout: ``EXP/inputs/_.png`` (the tight CROP) + ``EXP/renders/_.npz`` carrying ``bbox`` in (y0, y1, x0, x1) order, ``res``, ``fx/fy/cx/cy`` and ``c2w_cv``. The eval renders store fx/fy/cx/cy as scalars rather than a (V,3,3) ``K``; the intrinsics are read from those scalars, which is the same convention. POINTMAP: ``cond_pointmap_mode="off"``. The eval inputs are depth-free, so the two pointmap token blocks MUST be the official zero force-drop. In ``MultiViewSSConditionEmbedder``, mode "off" makes ``_sample_pm_drop`` return all-True -> ``all_dropped`` -> ``torch.zeros(...)`` for both pointmap groups: BYTE-IDENTICAL to the mode-"auto" path the model trained under whenever ``has_pointmap == 0`` (the code comment states this and the branch confirms it). Mode is not a parameter of the module, so switching it does not perturb any weight. SEED LAYOUT: ``baf.grid_to_shape_latent(ss_enc, occ)`` returns ``z.view(1, 8, 4096).permute(0, 2, 1)``; ``mvsam3d.model.ss_vae. grid_latent_to_tokens`` returns ``z.reshape(B, 8, 4096).permute(0, 2, 1)`` of ``ss_encode(occ)`` == ``encoder(occ, sample_posterior=False, return_raw=False)``. Same encoder class, same architecture kwargs, same checkpoint file (md5 08536395562d0301911ed5c46ef0a539 in both trees) and the same defaults, so the two are THE SAME TENSOR -- no conversion is applied, z1 is fed to ``sample()`` as-is. Output ``/.npz``: coords (N,4) int32 = out["coords"] (PRIMARY) coords_original (M,4) int32 = out["coords_original"] (debug) occ64_packed uint8 np.packbits(logits[0,0] > 0) -> unpack to 64^3 downsample_factor, n_coords, ckpt_step, num_steps, view_tag CLI: python ssflow_coords.py --exp EXPDIR --views 2 --seeddir \ --out --ckpt --gpu 7 [--shard i --nshards n] NOTE ``--views`` selects the SAME object/tag table as pass 1/3, but the CONDITION is always the single primary view (tag "front"); --views only affects which seed dump dir / tag list the run is paired with. """ import os import sys REPO = "/lp-dev/jonghoon/mv-sam3d-6d-code/migrator/code/mv-sam3d-for-6d-v2" # --- env preamble: must be set BEFORE torch / sam3d_objects import ----------- if not os.environ.get("_SSFLOW_COORDS_INENV"): import argparse as _ap _p = _ap.ArgumentParser(add_help=False) _p.add_argument("--gpu", default=None) _known, _ = _p.parse_known_args() env = dict(os.environ) env["_SSFLOW_COORDS_INENV"] = "1" env["PYTHONPATH"] = REPO env["SPARSE_ATTN_BACKEND"] = "sdpa" env["SPCONV_ALGO"] = "native" env["MIGRATOR_CHECKPOINTS"] = os.path.join(REPO, "migrator", "checkpoints") env["MIGRATOR_CACHE"] = "/lp-dev/jonghoon/mv-sam3d-6d-code/migrator/cache" env["OMP_NUM_THREADS"] = "4" env["MKL_NUM_THREADS"] = "4" gpu = _known.gpu if _known.gpu is not None else env.get("CUDA_VISIBLE_DEVICES") assert gpu, "pass --gpu (or set CUDA_VISIBLE_DEVICES)" env["CUDA_VISIBLE_DEVICES"] = str(gpu) PY = "/lp-dev/jonghoon/mv-mesh/envs/mv-sam3d/bin/python" os.execve(PY, [PY, os.path.abspath(__file__)] + sys.argv[1:], env) sys.path.insert(0, REPO) 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 DEVICE = "cuda" SEED = 42 # The tag table is shared with pass 1 / pass 3 (tools/val_stage1_ref.VIEW_TAGS). 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 # --------------------------------------------------------------------------- # # model # --------------------------------------------------------------------------- # def load_model(ckpt: str, device: str = DEVICE, use_ema: bool = False): """SSFlowModel from the checkpoint's own ``config`` + its trained weights. Mirrors validate.py / batched_verify.py for construction and val_appforce.py::_load_model for the weight load. """ from mvsam3d.model.ssflow_model import SSFlowModel from mvsam3d import paths as _paths _paths.register_env() payload = torch.load(ckpt, map_location="cpu", weights_only=False) assert "trainable_state_dict" in payload, list(payload.keys()) mc = dict(payload["config"]["model"]) step = int(payload.get("step", -1)) # Eval overrides (see the module docstring): the eval inputs are depth-free, # so the pointmap blocks are the official zero force-drop. mc["ss_mode"] = "imgcond" mc["cond_pointmap_mode"] = "off" mc["with_condition_embedder"] = True mc["use_checkpoint"] = False def _x(p): return os.path.expandvars(os.path.expanduser(p)) if p else p model = SSFlowModel( model_channels=mc["model_channels"], num_blocks=mc["num_blocks"], num_heads=mc["num_heads"], cond_channels=mc["cond_channels"], with_condition_embedder=True, sigma_min=mc["sigma_min"], time_scale=mc["time_scale"], timestep_sampler=mc["timestep_sampler"], timestep_lognorm_mean=mc["timestep_lognorm_mean"], timestep_lognorm_std=mc["timestep_lognorm_std"], pretrained_ckpt=_x(mc["pretrained_ckpt"]) or None, use_checkpoint=False, ss_mode="imgcond", cond_pointmap_mode="off", cond_dropout_prob=mc.get("cond_dropout_prob", 0.1), cond_enable_plucker=mc.get("cond_enable_plucker", True), cond_batch_towers=mc.get("cond_batch_towers", True), ).to(device) missing, unexpected = model.load_state_dict(payload["trainable_state_dict"], strict=False) assert not unexpected, unexpected[:10] print(f"[ssflow] loaded {ckpt} (step {step}, " f"{len(payload['trainable_state_dict'])} trained tensors, " f"{len(missing)} frozen/pretrained tensors left untouched)", flush=True) if use_ema: from mvsam3d.train.ema import EmaShadow, load_ema_sidecar ema = EmaShadow(model, rate=1.0) if load_ema_sidecar(ckpt, ema, model): with torch.no_grad(): for n, p in model.named_parameters(): if n in ema.shadow: p.copy_(ema.shadow[n]) print("[ssflow] EMA sidecar applied", flush=True) else: print("[ssflow] no EMA sidecar (ema.enable=false for this run); " "using raw weights", flush=True) model.eval() from mvsam3d.train.validate import _get_decoder decoder = _get_decoder(torch.device(device), _x(mc["ss_decoder_ckpt"])) return model, decoder, step # --------------------------------------------------------------------------- # # condition (V=1, the PRIMARY view) — built with the DATASET's own helpers # --------------------------------------------------------------------------- # def build_cond(model, exp: str, obj: str, tag: str, device: str = DEVICE): from mvsam3d.data.ss_imgcond_dataset import ( SSImgCondDataset, pad_square_rect, COND_KEYS_BASE) from mvsam3d.data.ss_preprocess import preprocess_ss_view, ss_crop_box from mvsam3d.model.mv_ss_condition import plucker_rays_for_view, RAY_KEYS png = os.path.join(exp, "inputs", f"{obj}_{tag}.png") rz = np.load(os.path.join(exp, "renders", f"{obj}_{tag}.npz")) if not os.path.isfile(png): raise FileNotFoundError(png) bbox = rz["bbox"].astype(np.float64) # (y0, y1, x0, x1) — dataset order res = int(rz["res"]) fx, fy = float(rz["fx"]), float(rz["fy"]) cx, cy = float(rz["cx"]), float(rz["cy"]) c2w = torch.from_numpy(np.asarray(rz["c2w_cv"], dtype=np.float64)) # crop -> full frame (dataset staticmethod; square store => res_h == res_w) rgba = SSImgCondDataset._full_frame_rgba(png, bbox, res, res) out = preprocess_ss_view(rgba, pointmap=None) # 4 base keys, no batch dim crop_rect = ss_crop_box(rgba[..., 3], box_size_factor=1.2) full_rect = pad_square_rect(res, res) # V == 1 -> the anchor IS this view, so T = I (dataset: anchor_c2w = c2w[sub[0]]) r = plucker_rays_for_view(c2w, c2w, fx, fy, cx, cy, crop_rect, full_rect) cond_inputs = {k: out[k].unsqueeze(0).unsqueeze(0).to(device) # [1,1,C,518,518] for k in COND_KEYS_BASE} rays = {k: r[k].unsqueeze(0).unsqueeze(0).to(device) # [1,1,n*n,6] for k in RAY_KEYS} # pointmap keys deliberately ABSENT: cond_pointmap_mode='off' emits the # official zero force-drop blocks for both pointmap groups. cond = model.encode_condition_mv(cond_inputs, rays=rays, has_pm=torch.zeros(1, device=device)) return cond # --------------------------------------------------------------------------- # def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--exp", required=True) ap.add_argument("--views", type=int, default=2) ap.add_argument("--seeddir", required=True, help="pass-1 dump dir (.npz with z1)") ap.add_argument("--out", required=True, help="coords dir (.npz each)") ap.add_argument("--ckpt", required=True, help="step_XXXXXXX.pt") ap.add_argument("--gpu", default=None, help="single GPU id (consumed by the preamble)") ap.add_argument("--num-steps", type=int, default=25) ap.add_argument("--seed", type=int, default=SEED) ap.add_argument("--use-ema", action="store_true", help="apply an EMA sidecar if one exists (this run has none)") 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) views_tags = views_tags_for(args.views) tag = views_tags[0] # the PRIMARY view (V=1 condition) seeddir = Path(args.seeddir) out = Path(args.out) out.mkdir(parents=True, exist_ok=True) objs = _objects(args) print(f"[ssflow] exp={exp} views={args.views} cond_tag={tag} n_obj={len(objs)} " f"shard={args.shard}/{args.nshards} steps={args.num_steps} " f"CUDA={os.environ.get('CUDA_VISIBLE_DEVICES')}", flush=True) t0 = time.time() model, decoder, step = load_model(args.ckpt, DEVICE, use_ema=args.use_ema) print(f"[ssflow] model 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: sz = np.load(seeddir / f"{obj}.npz") z1 = torch.from_numpy(sz["z1"].astype(np.float32)).to(DEVICE) assert z1.shape == (1, 4096, 8), z1.shape with torch.no_grad(): cond = build_cond(model, exp, obj, tag, DEVICE) torch.manual_seed(args.seed) o = model.sample(z1, num_steps=args.num_steps, cond=cond, ss_decoder=decoder, use_fp32=True) coords = o["coords"].detach().cpu().numpy().astype(np.int32) coords_orig = o["coords_original"].detach().cpu().numpy().astype(np.int32) occ = (o["logits"][0, 0] > 0).detach().cpu().numpy() tmp = str(f) + ".tmp.npz" np.savez_compressed( tmp, coords=coords, coords_original=coords_orig, occ64_packed=np.packbits(occ.reshape(-1)), occ64_shape=np.array(occ.shape, dtype=np.int32), downsample_factor=int(o["downsample_factor"]), n_coords=int(coords.shape[0]), ckpt_step=int(step), num_steps=int(args.num_steps), view_tag=np.array(tag), views=np.array(views_tags), seed=int(args.seed)) os.replace(tmp, f) n_ok += 1 print(f"[ssflow {i}/{len(objs)}] OK {obj} N={coords.shape[0]} " f"Norig={coords_orig.shape[0]} occ={int(occ.sum())} " f"{time.time() - t1:.1f}s", flush=True) except Exception: n_fail += 1 traceback.print_exc() print(f"[ssflow {i}/{len(objs)}] FAIL {obj}", flush=True) torch.cuda.empty_cache() print(f"SSFLOW_COORDS DONE ok={n_ok} fail={n_fail} skip={n_skip} " f"total={len(objs)}", flush=True) if __name__ == "__main__": main()