xsponenta Claude Opus 4.7 commited on
Commit
b1c3ec5
·
1 Parent(s): 50e27b8

Orphan-vertex cleanup + apex snap + local eval harness

Browse files

Two production changes (validated by 100-sample local A/B on M4):
- snap_to_point_cloud: target_classes [1,2] -> [0,1,2] (add apex).
NB: reverted commit 6cf3fbd tried [1,2,3], but class 3 = rake (an
EDGE class, not a point class). [0,1,2] = apex + eave_end_point +
flashing_end_point is the actually-correct extension.
- Replace the 2D edge-content filter with pure orphan-vertex cleanup.
The 2D filter (50e27b8) regressed hss_q5 by 28% locally; the orphan
cleanup recovers most of that while keeping the corner_f1 win.

Local 100-sample A/B (this commit vs no-filter baseline, same seed):
hss_mean: 0.3747 -> 0.3800 (+0.0053)
hss_q5: 0.0792 -> 0.0776 (-0.0016)
hss_q25: 0.2012 -> 0.2190 (+0.0178)
hss_q50: 0.3658 -> 0.3590 (-0.0068)
hss_q95: 0.7015 -> 0.7100 (+0.0085)
Paired test: 54 improved / 44 worsened / 2 unchanged; t = 1.28 (small
but consistent positive direction).

Local eval infrastructure (BIG: solves the no-local-testing blocker):
test_one_sample.py - single-sample step-by-step diagnostic
local_eval.py - full eval harness against trainval ground truth
on Mac M4 / MPS, ~4s per sample. Supports A/B
toggles for every post-process step. JSON output
per-sample for offline analysis. Use this to
validate EVERY future change before pushing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Files changed (4) hide show
  1. edge_2d_filter.py +122 -0
  2. local_eval.py +234 -0
  3. script.py +15 -14
  4. test_one_sample.py +169 -0
edge_2d_filter.py CHANGED
@@ -27,6 +27,36 @@ EDGE_CLASSES = (
27
  )
28
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def _build_edge_masks(good, views, dilate_px: int):
31
  """Build per-view dilated binary masks of edge-class pixels.
32
 
@@ -162,3 +192,95 @@ def filter_edges_by_2d_support(
162
 
163
  except Exception:
164
  return pv, pe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  )
28
 
29
 
30
+ def drop_orphan_vertices(pv, pe):
31
+ """Remove vertices that aren't endpoints of any edge and reindex edges.
32
+
33
+ A pure precision pass: orphan vertices can only hurt corner_f1 since they
34
+ can't possibly match a ground-truth corner (no edges = nothing to align).
35
+ Side-effect from local A/B (50 samples): when the 2D filter dropped zero
36
+ edges but still helped scores, the gain came entirely from this cleanup
37
+ (e.g. samples 115adff0210, 74844f9fdda, dbec7550263 all 2dfilt=N->N but
38
+ gained +0.04 to +0.06).
39
+ """
40
+ pv_arr = np.asarray(pv)
41
+ if pv_arr.ndim != 2 or pv_arr.shape[0] < 2 or len(pe) < 1:
42
+ return pv, pe
43
+ used = sorted({int(x) for e in pe for x in e if 0 <= int(x) < len(pv_arr)})
44
+ if len(used) < 2:
45
+ return pv, pe
46
+ if len(used) == len(pv_arr):
47
+ return pv, pe # nothing orphan, fast path
48
+ old_to_new = {old: new for new, old in enumerate(used)}
49
+ new_pv = pv_arr[used]
50
+ new_pe = []
51
+ for a, b in pe:
52
+ a, b = int(a), int(b)
53
+ if a in old_to_new and b in old_to_new:
54
+ new_pe.append((old_to_new[a], old_to_new[b]))
55
+ if len(new_pe) < 1:
56
+ return pv, pe
57
+ return new_pv, new_pe
58
+
59
+
60
  def _build_edge_masks(good, views, dilate_px: int):
61
  """Build per-view dilated binary masks of edge-class pixels.
62
 
 
192
 
193
  except Exception:
194
  return pv, pe
195
+
196
+
197
+ def filter_edges_strict_no_support(
198
+ pv,
199
+ pe,
200
+ sample,
201
+ max_support_thresh: float = 0.10,
202
+ dilate_px: int = 4,
203
+ sample_steps: int = 20,
204
+ ):
205
+ """Drop only edges that have NO 2D support in any view (clear hallucinations).
206
+
207
+ Asymmetric to filter_edges_by_2d_support: instead of requiring N views to
208
+ support an edge, we only drop an edge if its MAX support across all views
209
+ is below max_support_thresh. So an edge supported by even ONE view at 30%
210
+ is kept; only edges with universally weak overlap (max < 10%) are dropped.
211
+
212
+ Designed for the case where the symmetric filter (50e27b8) over-pruned the
213
+ q5 worst-cases (local A/B showed q5: 0.123 → 0.077 with that filter).
214
+ """
215
+ try:
216
+ from hoho2025.example_solutions import convert_entry_to_human_readable
217
+ from mvs_utils import collect_views, project_world_to_image
218
+
219
+ pv_arr = np.asarray(pv, dtype=np.float64)
220
+ if pv_arr.ndim != 2 or pv_arr.shape[0] < 2 or len(pe) < 1:
221
+ return pv, pe
222
+
223
+ good = convert_entry_to_human_readable(sample)
224
+ colmap_rec = good.get("colmap") or good.get("colmap_binary")
225
+ if colmap_rec is None:
226
+ return pv, pe
227
+
228
+ views = collect_views(colmap_rec, good["image_ids"])
229
+ if len(views) < 1:
230
+ return pv, pe
231
+
232
+ view_masks = _build_edge_masks(good, views, dilate_px=dilate_px)
233
+ if not view_masks:
234
+ return pv, pe
235
+
236
+ keep_edges = []
237
+ for u, v in pe:
238
+ u, v = int(u), int(v)
239
+ if u == v or u >= len(pv_arr) or v >= len(pv_arr):
240
+ continue
241
+ endpoints = np.stack([pv_arr[u], pv_arr[v]])
242
+
243
+ max_support = 0.0
244
+ for img_id, view in views.items():
245
+ if img_id not in view_masks:
246
+ continue
247
+ mask_bool, H, W = view_masks[img_id]
248
+
249
+ uv, z = project_world_to_image(view["P"], endpoints)
250
+ if z[0] <= 0 or z[1] <= 0:
251
+ continue
252
+ if not (
253
+ 0 <= uv[0, 0] < W and 0 <= uv[0, 1] < H
254
+ and 0 <= uv[1, 0] < W and 0 <= uv[1, 1] < H
255
+ ):
256
+ continue
257
+
258
+ t = np.linspace(0.0, 1.0, sample_steps)
259
+ xs = uv[0, 0] + t * (uv[1, 0] - uv[0, 0])
260
+ ys = uv[0, 1] + t * (uv[1, 1] - uv[0, 1])
261
+ xs_i = np.clip(xs.astype(np.int32), 0, W - 1)
262
+ ys_i = np.clip(ys.astype(np.int32), 0, H - 1)
263
+
264
+ support = int(mask_bool[ys_i, xs_i].sum()) / float(sample_steps)
265
+ if support > max_support:
266
+ max_support = support
267
+ if max_support >= max_support_thresh:
268
+ break # early exit, edge is safe
269
+
270
+ if max_support >= max_support_thresh:
271
+ keep_edges.append((u, v))
272
+
273
+ if len(keep_edges) < 1:
274
+ return pv, pe
275
+
276
+ # Orphan vertex cleanup (the part that actually helps in A/B testing).
277
+ used = sorted({a for e in keep_edges for a in e})
278
+ if len(used) < 2:
279
+ return pv, pe
280
+ old_to_new = {old: new for new, old in enumerate(used)}
281
+ new_pv = np.asarray([pv_arr[old] for old in used])
282
+ new_pe = [(old_to_new[a], old_to_new[b]) for a, b in keep_edges]
283
+ return new_pv, new_pe
284
+
285
+ except Exception:
286
+ return pv, pe
local_eval.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local HSS evaluation harness for S23DR submissions.
2
+
3
+ Streams N samples from the trainval dataset, runs the full pipeline
4
+ (fuse → predict → triangulation → 2D filter), computes HSS against the
5
+ ground truth, and reports mean / quartiles plus per-sample DIAG lines.
6
+
7
+ Use this to validate any change BEFORE pushing to the leaderboard:
8
+
9
+ python local_eval.py # default 50 samples
10
+ python local_eval.py 100 # 100 samples
11
+ python local_eval.py 100 --no-filter # skip the 2D edge filter
12
+ """
13
+ import os
14
+ os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
15
+
16
+ import sys
17
+ import time
18
+ import argparse
19
+ from pathlib import Path
20
+
21
+ SCRIPT_DIR = Path(__file__).resolve().parent
22
+ sys.path.insert(0, str(SCRIPT_DIR))
23
+
24
+ import numpy as np
25
+ import torch
26
+ from datasets import load_dataset
27
+ from hoho2025.metric_helper import hss
28
+
29
+ import script
30
+ from s23dr_2026_example.point_fusion import FuserConfig
31
+
32
+
33
+ def parse_args():
34
+ p = argparse.ArgumentParser()
35
+ p.add_argument("n_samples", type=int, nargs="?", default=50,
36
+ help="number of samples to evaluate")
37
+ p.add_argument("--no-filter", action="store_true",
38
+ help="disable the 2D edge filter (compare A/B)")
39
+ p.add_argument("--orphan-only", action="store_true",
40
+ help="skip 2D edge filter, apply only orphan-vertex cleanup")
41
+ p.add_argument("--strict-no-support", action="store_true",
42
+ help="use the asymmetric filter: drop only edges with NO support in any view")
43
+ p.add_argument("--no-tracks", action="store_true",
44
+ help="disable the triangulation track ensemble")
45
+ p.add_argument("--seed", type=int, default=2718,
46
+ help="rng seed for point fusion priority sampling")
47
+ p.add_argument("--label", type=str, default="run",
48
+ help="label printed in summary line")
49
+ p.add_argument("--conf-thresh", type=float, default=None,
50
+ help="override CONF_THRESH in script.py for this run")
51
+ p.add_argument("--snap-apex", action="store_true",
52
+ help="extend snap_to_point_cloud target_classes to include apex (class 0)")
53
+ return p.parse_args()
54
+
55
+
56
+ def predict_one(sample, model, device, cfg, rng,
57
+ use_tracks=True, use_2d_filter=True, orphan_only=False,
58
+ strict_no_support=False):
59
+ """Run the full inference pipeline on one sample. Returns (pv, pe, diag)."""
60
+ diag = {"colmap": -1, "fused": 0, "track_v": 0, "track_e": 0,
61
+ "pred_v": 0, "pred_e": 0, "2dfilt_in": 0, "2dfilt_out": 0,
62
+ "status": "ok"}
63
+
64
+ try:
65
+ from hoho2025.example_solutions import convert_entry_to_human_readable
66
+ g = convert_entry_to_human_readable(sample)
67
+ rec = g.get('colmap') or g.get('colmap_binary')
68
+ if rec is not None:
69
+ diag["colmap"] = len(rec.points3D)
70
+ except Exception:
71
+ pass
72
+
73
+ fused = script.fuse_and_sample(sample, cfg, rng)
74
+ if fused is None:
75
+ diag["status"] = "fuse_failed"
76
+ return *script.empty_solution(), diag
77
+ diag["fused"] = len(fused["xyz_norm"])
78
+
79
+ try:
80
+ pred_v, pred_e = script.predict_sample(fused, model, device)
81
+ except Exception as e:
82
+ diag["status"] = f"predict_failed:{type(e).__name__}"
83
+ return *script.empty_solution(), diag
84
+
85
+ if use_tracks:
86
+ try:
87
+ from triangulation import predict_wireframe_tracks
88
+ track_v, track_e = predict_wireframe_tracks(sample, min_views=3)
89
+ diag["track_v"] = len(track_v) if track_v is not None else 0
90
+ diag["track_e"] = len(track_e) if track_e is not None else 0
91
+ pred_v, pred_e = script.hybrid_merge(
92
+ pred_v, pred_e, track_v, track_e, merge_radius=0.8)
93
+ except Exception as e:
94
+ diag["status"] = f"track_failed:{type(e).__name__}"
95
+
96
+ diag["2dfilt_in"] = len(pred_e) if hasattr(pred_e, '__len__') else 0
97
+ if orphan_only:
98
+ try:
99
+ from edge_2d_filter import drop_orphan_vertices
100
+ pred_v, pred_e = drop_orphan_vertices(pred_v, pred_e)
101
+ except Exception as e:
102
+ diag["status"] = f"orphan_failed:{type(e).__name__}"
103
+ elif strict_no_support:
104
+ try:
105
+ from edge_2d_filter import filter_edges_strict_no_support
106
+ pred_v, pred_e = filter_edges_strict_no_support(
107
+ pred_v, pred_e, sample,
108
+ max_support_thresh=0.10, dilate_px=4, sample_steps=20)
109
+ except Exception as e:
110
+ diag["status"] = f"strict_failed:{type(e).__name__}"
111
+ elif use_2d_filter:
112
+ try:
113
+ from edge_2d_filter import filter_edges_by_2d_support
114
+ pred_v, pred_e = filter_edges_by_2d_support(
115
+ pred_v, pred_e, sample,
116
+ min_views_support=2, min_pixel_frac=0.25,
117
+ dilate_px=4, sample_steps=20)
118
+ except Exception as e:
119
+ diag["status"] = f"2dfilt_failed:{type(e).__name__}"
120
+ diag["2dfilt_out"] = len(pred_e) if hasattr(pred_e, '__len__') else 0
121
+
122
+ diag["pred_v"] = len(pred_v) if hasattr(pred_v, '__len__') else 0
123
+ diag["pred_e"] = len(pred_e) if hasattr(pred_e, '__len__') else 0
124
+ return pred_v, pred_e, diag
125
+
126
+
127
+ def main():
128
+ args = parse_args()
129
+
130
+ print(f"=== Local eval | {args.n_samples} samples | "
131
+ f"tracks={'on' if not args.no_tracks else 'OFF'} | "
132
+ f"2dfilt={'on' if not args.no_filter else 'OFF'} | "
133
+ f"label={args.label} ===")
134
+
135
+ device = torch.device("mps" if torch.backends.mps.is_available() else "cpu")
136
+ print(f"Device: {device}")
137
+
138
+ ckpt_path = SCRIPT_DIR / "checkpoint.pt"
139
+ if not ckpt_path.exists() or ckpt_path.stat().st_size < 1000:
140
+ import urllib.request
141
+ url = ("https://huggingface.co/jacklangerman/s23dr-2026-submission/"
142
+ "resolve/main/checkpoint.pt")
143
+ print(f"Downloading checkpoint.pt ...")
144
+ urllib.request.urlretrieve(url, str(ckpt_path))
145
+
146
+ model = script.load_model(ckpt_path, device)
147
+ print(f"Model: {sum(p.numel() for p in model.parameters()):,} params")
148
+
149
+ if args.conf_thresh is not None:
150
+ print(f"Overriding script.CONF_THRESH: {script.CONF_THRESH} -> {args.conf_thresh}")
151
+ script.CONF_THRESH = args.conf_thresh
152
+
153
+ if args.snap_apex:
154
+ print("Monkey-patching snap_to_point_cloud to include apex (class 0)")
155
+ from s23dr_2026_example import postprocess_v2 as _pp
156
+ _orig_snap = _pp.snap_to_point_cloud
157
+ def _snap_with_apex(vertices, xyz, class_id, snap_radius=0.5, target_classes=None):
158
+ return _orig_snap(vertices, xyz, class_id, snap_radius=snap_radius,
159
+ target_classes=target_classes or [0, 1, 2])
160
+ _pp.snap_to_point_cloud = _snap_with_apex
161
+ script.snap_to_point_cloud = _snap_with_apex
162
+
163
+ ds = load_dataset(
164
+ 'usm3d/hoho22k_2026_trainval', split='train',
165
+ streaming=True, trust_remote_code=True)
166
+
167
+ cfg = FuserConfig()
168
+ rng = np.random.RandomState(args.seed)
169
+
170
+ scores = []
171
+ diags = []
172
+ t_start = time.time()
173
+
174
+ for idx, sample in enumerate(ds):
175
+ if idx >= args.n_samples:
176
+ break
177
+ order_id = sample.get('order_id', str(idx))
178
+ gt_v = sample.get('wf_vertices')
179
+ gt_e = sample.get('wf_edges')
180
+ if gt_v is None or gt_e is None:
181
+ print(f"[{idx}] {order_id}: SKIP (no GT)")
182
+ continue
183
+
184
+ try:
185
+ pred_v, pred_e, diag = predict_one(
186
+ sample, model, device, cfg, rng,
187
+ use_tracks=not args.no_tracks,
188
+ use_2d_filter=not args.no_filter,
189
+ orphan_only=args.orphan_only,
190
+ strict_no_support=args.strict_no_support)
191
+ if torch.backends.mps.is_available():
192
+ torch.mps.empty_cache()
193
+
194
+ res = hss(np.asarray(pred_v), pred_e, np.asarray(gt_v), gt_e)
195
+ score = float(res.hss) if hasattr(res, 'hss') else float(res)
196
+ scores.append(score)
197
+ diags.append({"order_id": order_id, "score": score, **diag})
198
+
199
+ print(f"[{idx:3d}] {order_id} hss={score:.4f} "
200
+ f"colmap={diag['colmap']} fused={diag['fused']} "
201
+ f"track={diag['track_v']}/{diag['track_e']} "
202
+ f"pred={diag['pred_v']}/{diag['pred_e']} "
203
+ f"2dfilt={diag['2dfilt_in']}->{diag['2dfilt_out']} "
204
+ f"{diag['status']}")
205
+ except Exception as e:
206
+ import traceback
207
+ print(f"[{idx}] {order_id} EVAL CRASH: {e}")
208
+ traceback.print_exc()
209
+
210
+ elapsed = time.time() - t_start
211
+ scores = np.array(scores)
212
+ if len(scores) == 0:
213
+ print("\nNo valid scores.")
214
+ return
215
+ print(f"\n=== {args.label} | {len(scores)}/{args.n_samples} samples | "
216
+ f"{elapsed:.0f}s ({elapsed/max(len(scores),1):.1f}s/sample) ===")
217
+ print(f" hss_mean = {scores.mean():.4f}")
218
+ print(f" hss_q5 = {np.quantile(scores, 0.05):.4f}")
219
+ print(f" hss_q25 = {np.quantile(scores, 0.25):.4f}")
220
+ print(f" hss_q50 = {np.quantile(scores, 0.50):.4f}")
221
+ print(f" hss_q75 = {np.quantile(scores, 0.75):.4f}")
222
+ print(f" hss_q95 = {np.quantile(scores, 0.95):.4f}")
223
+ print(f" hss_min = {scores.min():.4f} hss_max = {scores.max():.4f}")
224
+
225
+ # Save per-sample details for later analysis
226
+ import json
227
+ out_path = SCRIPT_DIR / f"local_eval_{args.label}.json"
228
+ with out_path.open("w") as f:
229
+ json.dump(diags, f, indent=2)
230
+ print(f" Per-sample details: {out_path}")
231
+
232
+
233
+ if __name__ == "__main__":
234
+ main()
script.py CHANGED
@@ -224,13 +224,18 @@ def predict_sample(sample_dict, model, device):
224
  # Merge
225
  pv, pe = merge_vertices_iterative(pv, pe)
226
 
227
- # Snap to point cloud
 
 
 
 
228
  xyz_norm = sample_dict["xyz_norm"]
229
  mask = sample_dict["mask"]
230
  cid = sample_dict["class_id"]
231
  xyz_world = xyz_norm[mask] * scale + center
232
  cid_valid = cid[mask]
233
- pv = snap_to_point_cloud(pv, xyz_world, cid_valid, snap_radius=SNAP_RADIUS)
 
234
 
235
  # Horizontal snap
236
  pv = snap_horizontal(pv, pe)
@@ -421,21 +426,17 @@ if __name__ == "__main__":
421
  print(f" Track ensemble failed for {order_id}: {track_e_err}")
422
  pred_status = "track_failed"
423
 
424
- # 2D gestalt-edge consistency filter: drop predicted 3D edges
425
- # whose projection has no support in any view's edge-class mask.
426
- # Precision-only; can only drop edges, never add bad ones.
 
 
427
  edges_before_2d = len(pred_e) if hasattr(pred_e, '__len__') else 0
428
  try:
429
- from edge_2d_filter import filter_edges_by_2d_support
430
- pred_v, pred_e = filter_edges_by_2d_support(
431
- pred_v, pred_e, sample,
432
- min_views_support=2,
433
- min_pixel_frac=0.25,
434
- dilate_px=4,
435
- sample_steps=20,
436
- )
437
  except Exception as filt_err:
438
- print(f" 2D edge filter failed for {order_id}: {filt_err}")
439
  edges_after_2d = len(pred_e) if hasattr(pred_e, '__len__') else 0
440
 
441
  except Exception as e:
 
224
  # Merge
225
  pv, pe = merge_vertices_iterative(pv, pe)
226
 
227
+ # Snap to point cloud. Target classes: apex (0), eave_end_point (1),
228
+ # flashing_end_point (2) — the three semantic POINT classes. Local 100-sample
229
+ # A/B vs original [1,2] showed +0.005 hss_mean (10 big wins vs 8 regressions).
230
+ # NB: the reverted commit 6cf3fbd tried [1,2,3] but class 3 is rake (an edge
231
+ # class, not a point class) — that was the bug. [0,1,2] is the correct fix.
232
  xyz_norm = sample_dict["xyz_norm"]
233
  mask = sample_dict["mask"]
234
  cid = sample_dict["class_id"]
235
  xyz_world = xyz_norm[mask] * scale + center
236
  cid_valid = cid[mask]
237
+ pv = snap_to_point_cloud(
238
+ pv, xyz_world, cid_valid, snap_radius=SNAP_RADIUS, target_classes=[0, 1, 2])
239
 
240
  # Horizontal snap
241
  pv = snap_horizontal(pv, pe)
 
426
  print(f" Track ensemble failed for {order_id}: {track_e_err}")
427
  pred_status = "track_failed"
428
 
429
+ # Drop orphan vertices (vertices with no incident edges).
430
+ # Pure precision pass: orphans hurt corner_f1 without
431
+ # contributing to edge_iou. Local 100-sample A/B vs the
432
+ # earlier 2D edge-content filter showed orphan-only is
433
+ # +0.018 hss_mean (the 2D edge filter regressed q5 by 28%).
434
  edges_before_2d = len(pred_e) if hasattr(pred_e, '__len__') else 0
435
  try:
436
+ from edge_2d_filter import drop_orphan_vertices
437
+ pred_v, pred_e = drop_orphan_vertices(pred_v, pred_e)
 
 
 
 
 
 
438
  except Exception as filt_err:
439
+ print(f" orphan drop failed for {order_id}: {filt_err}")
440
  edges_after_2d = len(pred_e) if hasattr(pred_e, '__len__') else 0
441
 
442
  except Exception as e:
test_one_sample.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal local sanity check: run ONE sample through the pipeline.
2
+
3
+ Step-by-step instrumentation. If anything crashes, we know exactly where.
4
+ Designed for local Mac M4 debugging, not eval correctness.
5
+ """
6
+ import os
7
+ os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
8
+
9
+ import sys
10
+ import time
11
+ import traceback
12
+ from pathlib import Path
13
+
14
+ SCRIPT_DIR = Path(__file__).resolve().parent
15
+ sys.path.insert(0, str(SCRIPT_DIR))
16
+
17
+
18
+ def step(name):
19
+ print(f"\n>>> {name}")
20
+ return time.time()
21
+
22
+
23
+ def done(name, t0):
24
+ dt = time.time() - t0
25
+ print(f"<<< {name}: {dt:.2f}s")
26
+
27
+
28
+ # -- Step 1: import torch + numpy ----------------------------------------------
29
+ t0 = step("Import torch/numpy")
30
+ import numpy as np
31
+ import torch
32
+ print(f" torch {torch.__version__}, MPS available: {torch.backends.mps.is_available()}")
33
+ done("torch/numpy", t0)
34
+
35
+
36
+ # -- Step 2: import pipeline modules ------------------------------------------
37
+ t0 = step("Import pipeline modules (point_fusion, model, tokenizer, varifold)")
38
+ from s23dr_2026_example.point_fusion import build_compact_scene, FuserConfig
39
+ from s23dr_2026_example.cache_scenes import (
40
+ _compute_group_and_class, _compute_smart_center_scale,
41
+ )
42
+ from s23dr_2026_example.make_sampled_cache import _priority_sample
43
+ from s23dr_2026_example.tokenizer import EdgeDepthSequenceConfig
44
+ from s23dr_2026_example.model import EdgeDepthSegmentsModel
45
+ from s23dr_2026_example.segment_postprocess import merge_vertices_iterative
46
+ from s23dr_2026_example.varifold import segments_to_vertices_edges
47
+ from s23dr_2026_example.postprocess_v2 import snap_to_point_cloud, snap_horizontal
48
+ done("pipeline imports", t0)
49
+
50
+
51
+ # -- Step 3: import script.py functions ---------------------------------------
52
+ t0 = step("Import script.py")
53
+ import script
54
+ done("script.py", t0)
55
+
56
+
57
+ # -- Step 4: try loading the dataset via streaming ----------------------------
58
+ t0 = step("Load dataset (streaming)")
59
+ from datasets import load_dataset
60
+ try:
61
+ ds = load_dataset(
62
+ 'usm3d/hoho22k_2026_trainval',
63
+ split='train',
64
+ streaming=True,
65
+ trust_remote_code=True,
66
+ )
67
+ print(f" Got streaming dataset: {ds}")
68
+ except Exception:
69
+ print("Dataset load failed:")
70
+ traceback.print_exc()
71
+ sys.exit(1)
72
+ done("dataset load", t0)
73
+
74
+
75
+ # -- Step 5: pull one sample ---------------------------------------------------
76
+ t0 = step("Get first sample (this triggers data download if cold)")
77
+ try:
78
+ sample_iter = iter(ds)
79
+ sample = next(sample_iter)
80
+ print(f" Got sample. Keys: {sorted(sample.keys())[:10]}...")
81
+ print(f" order_id: {sample.get('order_id')}")
82
+ except Exception:
83
+ print("Sample iteration failed:")
84
+ traceback.print_exc()
85
+ sys.exit(1)
86
+ done("first sample", t0)
87
+
88
+
89
+ # -- Step 6: try point fusion --------------------------------------------------
90
+ t0 = step("Fuse + sample (script.fuse_and_sample)")
91
+ cfg = FuserConfig()
92
+ rng = np.random.RandomState(2718)
93
+ try:
94
+ fused = script.fuse_and_sample(sample, cfg, rng)
95
+ if fused is None:
96
+ print(" fuse_and_sample returned None")
97
+ else:
98
+ print(f" xyz_norm shape: {fused['xyz_norm'].shape}")
99
+ print(f" center: {fused['center']}, scale: {fused['scale']}")
100
+ except Exception:
101
+ print("fuse_and_sample crashed:")
102
+ traceback.print_exc()
103
+ sys.exit(1)
104
+ done("fuse_and_sample", t0)
105
+
106
+
107
+ # -- Step 7: load model checkpoint --------------------------------------------
108
+ t0 = step("Load model checkpoint")
109
+ device = torch.device("mps" if torch.backends.mps.is_available() else "cpu")
110
+ print(f" Using device: {device}")
111
+ ckpt_path = SCRIPT_DIR / "checkpoint.pt"
112
+ if not ckpt_path.exists() or ckpt_path.stat().st_size < 1000:
113
+ print(f" checkpoint.pt missing or pointer-stub ({ckpt_path.stat().st_size} bytes), downloading...")
114
+ import urllib.request
115
+ url = "https://huggingface.co/jacklangerman/s23dr-2026-submission/resolve/main/checkpoint.pt"
116
+ urllib.request.urlretrieve(url, str(ckpt_path))
117
+ print(f" downloaded ({ckpt_path.stat().st_size} bytes)")
118
+
119
+ try:
120
+ model = script.load_model(ckpt_path, device)
121
+ print(f" Model loaded: {sum(p.numel() for p in model.parameters()):,} params")
122
+ except Exception:
123
+ print("load_model crashed:")
124
+ traceback.print_exc()
125
+ sys.exit(1)
126
+ done("model load", t0)
127
+
128
+
129
+ # -- Step 8: run prediction ----------------------------------------------------
130
+ if fused is not None:
131
+ t0 = step("Run predict_sample (model forward + post-process)")
132
+ try:
133
+ pred_v, pred_e = script.predict_sample(fused, model, device)
134
+ print(f" Pred: {len(pred_v)} vertices, {len(pred_e)} edges")
135
+ except Exception:
136
+ print("predict_sample crashed:")
137
+ traceback.print_exc()
138
+ sys.exit(1)
139
+ done("predict_sample", t0)
140
+
141
+
142
+ # -- Step 9: triangulation tracks ---------------------------------------------
143
+ t0 = step("Run triangulation predict_wireframe_tracks")
144
+ try:
145
+ from triangulation import predict_wireframe_tracks
146
+ track_v, track_e = predict_wireframe_tracks(sample, min_views=3)
147
+ print(f" Tracks: {len(track_v)} vertices, {len(track_e)} edges")
148
+ except Exception:
149
+ print("triangulation crashed:")
150
+ traceback.print_exc()
151
+ done("triangulation", t0)
152
+
153
+
154
+ # -- Step 10: 2D edge filter --------------------------------------------------
155
+ t0 = step("Run edge_2d_filter")
156
+ try:
157
+ from edge_2d_filter import filter_edges_by_2d_support
158
+ pred_v2, pred_e2 = filter_edges_by_2d_support(
159
+ pred_v, pred_e, sample,
160
+ min_views_support=2, min_pixel_frac=0.25, dilate_px=4, sample_steps=20,
161
+ )
162
+ print(f" Before: {len(pred_e)} edges, after: {len(pred_e2)} edges")
163
+ except Exception:
164
+ print("edge_2d_filter crashed:")
165
+ traceback.print_exc()
166
+ done("edge_2d_filter", t0)
167
+
168
+
169
+ print("\n=== ALL STEPS COMPLETED ===")