Rollback to proven 0.4584 baseline, add plane_wireframe to ensemble
Browse filesTwo changes in one commit:
1. ROLLBACK: revert script.py and postprocess_v2.py to commit 56f1ec6 contents
(the highest-scoring submission at hss_mean=0.4584). Removes snap_vertical +
snap_manhattan (regressed edge_iou -0.009 in 72962c4) and the
filter_by_colmap_support step (added in 6ecd5f8 but never independently
evaluated, so its effect is unknown).
2. NEW: wire plane_wireframe.predict_wireframe_planes into the ensemble as a
third recall source after triangulation tracks. RANSAC-segments roof planes
from the COLMAP cloud and intersects them to recover ridge/eave edges the
learned model misses. The module was fully implemented but never wired in.
Uses the same append-only hybrid_merge(radius=0.8) pattern as triangulation,
so it only adds genuinely new vertices/edges; cannot corrupt existing
topology. Wrapped in try/except so any failure falls back silently to the
pre-plane prediction.
Runtime deps open3d and scikit-spatial added via install_if_missing (extended
to accept a separate pip_name for packages whose import name differs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- s23dr_2026_example/postprocess_v2.py +0 -108
- script.py +20 -73
|
@@ -37,111 +37,3 @@ def snap_horizontal(vertices, edges, max_slope=0.05):
|
|
| 37 |
verts[a, 1] = avg_y
|
| 38 |
verts[b, 1] = avg_y
|
| 39 |
return verts
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def snap_vertical(vertices, edges, max_slope=0.05):
|
| 43 |
-
"""Snap near-vertical edges to be exactly vertical (shared X and Z).
|
| 44 |
-
|
| 45 |
-
Mirror of snap_horizontal across the up-axis. A near-vertical edge has
|
| 46 |
-
a small horizontal spread (dxz) relative to its vertical spread (dy).
|
| 47 |
-
"""
|
| 48 |
-
verts = vertices.copy()
|
| 49 |
-
for a, b in edges:
|
| 50 |
-
a, b = int(a), int(b)
|
| 51 |
-
dy = abs(verts[a, 1] - verts[b, 1])
|
| 52 |
-
dxz = np.sqrt((verts[a, 0] - verts[b, 0]) ** 2 + (verts[a, 2] - verts[b, 2]) ** 2)
|
| 53 |
-
if dy > 0.1 and dxz / dy < max_slope:
|
| 54 |
-
avg_x = 0.5 * (verts[a, 0] + verts[b, 0])
|
| 55 |
-
avg_z = 0.5 * (verts[a, 2] + verts[b, 2])
|
| 56 |
-
verts[a, 0] = avg_x
|
| 57 |
-
verts[a, 2] = avg_z
|
| 58 |
-
verts[b, 0] = avg_x
|
| 59 |
-
verts[b, 2] = avg_z
|
| 60 |
-
return verts
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def snap_manhattan(vertices, edges, tol_deg=3.0, min_count=4, n_iters=3):
|
| 64 |
-
"""Snap near-axis-aligned horizontal edges to a single dominant building direction.
|
| 65 |
-
|
| 66 |
-
Detects the dominant XZ-plane direction of near-horizontal edges via a
|
| 67 |
-
doubled-angle circular mean (so direction and its 90° rotation reinforce
|
| 68 |
-
each other). Edges within ``tol_deg`` of that direction or its perpendicular
|
| 69 |
-
are rotated about their midpoint to align exactly. Endpoints shared between
|
| 70 |
-
multiple snapped edges are averaged (Gauss-Seidel, ``n_iters`` passes) to
|
| 71 |
-
converge to a Manhattan-consistent position without drift.
|
| 72 |
-
|
| 73 |
-
Only operates in the XZ plane; Y is preserved (use snap_horizontal /
|
| 74 |
-
snap_vertical for Y handling). Returns vertices unchanged if fewer than
|
| 75 |
-
``min_count`` near-horizontal edges exist (insufficient evidence).
|
| 76 |
-
"""
|
| 77 |
-
verts = np.asarray(vertices, dtype=np.float64).copy()
|
| 78 |
-
n_verts = len(verts)
|
| 79 |
-
n_edges = len(edges)
|
| 80 |
-
if n_verts < 2 or n_edges < min_count:
|
| 81 |
-
return vertices
|
| 82 |
-
|
| 83 |
-
# Classify each edge once: near-horizontal? full-angle in XZ?
|
| 84 |
-
edge_data = [] # list of (a, b, full_ang) for near-horizontal edges only
|
| 85 |
-
for a, b in edges:
|
| 86 |
-
a, b = int(a), int(b)
|
| 87 |
-
dx = verts[b, 0] - verts[a, 0]
|
| 88 |
-
dy = verts[b, 1] - verts[a, 1]
|
| 89 |
-
dz = verts[b, 2] - verts[a, 2]
|
| 90 |
-
dxz = np.sqrt(dx * dx + dz * dz)
|
| 91 |
-
if dxz < 0.2:
|
| 92 |
-
continue
|
| 93 |
-
if abs(dy) / dxz > 0.1:
|
| 94 |
-
continue
|
| 95 |
-
full_ang = np.arctan2(dz, dx)
|
| 96 |
-
edge_data.append((a, b, full_ang))
|
| 97 |
-
|
| 98 |
-
if len(edge_data) < min_count:
|
| 99 |
-
return vertices
|
| 100 |
-
|
| 101 |
-
# Dominant Manhattan direction via 4×-angle circular mean: a building grid
|
| 102 |
-
# has 90° symmetry, so edges at θ, θ+90°, θ+180°, θ+270° must reinforce each
|
| 103 |
-
# other in the estimate. Multiply by 4 to fold [0, π/2) → [0, 2π), take the
|
| 104 |
-
# circular mean, divide by 4.
|
| 105 |
-
quad = np.array([4.0 * e[2] for e in edge_data])
|
| 106 |
-
peak_ang = 0.25 * np.arctan2(np.sin(quad).mean(), np.cos(quad).mean())
|
| 107 |
-
|
| 108 |
-
# Identify edges to snap and pick the best axis (peak, peak+π/2, peak+π, peak+3π/2)
|
| 109 |
-
tol = np.deg2rad(tol_deg)
|
| 110 |
-
snap_targets = [] # (a, b, ux, uz)
|
| 111 |
-
candidates = [peak_ang + k * (np.pi / 2.0) for k in range(4)]
|
| 112 |
-
for a, b, full_ang in edge_data:
|
| 113 |
-
diffs = [abs(((full_ang - c + np.pi) % (2 * np.pi)) - np.pi) for c in candidates]
|
| 114 |
-
k = int(np.argmin(diffs))
|
| 115 |
-
if diffs[k] >= tol:
|
| 116 |
-
continue
|
| 117 |
-
target = candidates[k]
|
| 118 |
-
snap_targets.append((a, b, float(np.cos(target)), float(np.sin(target))))
|
| 119 |
-
|
| 120 |
-
if not snap_targets:
|
| 121 |
-
return vertices
|
| 122 |
-
|
| 123 |
-
# Gauss-Seidel: each iteration, accumulate target endpoint positions per
|
| 124 |
-
# vertex and average. Y is left untouched.
|
| 125 |
-
for _ in range(n_iters):
|
| 126 |
-
sum_xz = np.zeros((n_verts, 2), dtype=np.float64)
|
| 127 |
-
count = np.zeros(n_verts, dtype=np.int64)
|
| 128 |
-
for a, b, ux, uz in snap_targets:
|
| 129 |
-
mx = 0.5 * (verts[a, 0] + verts[b, 0])
|
| 130 |
-
mz = 0.5 * (verts[a, 2] + verts[b, 2])
|
| 131 |
-
dx = verts[b, 0] - verts[a, 0]
|
| 132 |
-
dz = verts[b, 2] - verts[a, 2]
|
| 133 |
-
length = abs(dx * ux + dz * uz) # projection of edge onto snap dir
|
| 134 |
-
half = 0.5 * length
|
| 135 |
-
sum_xz[a, 0] += mx - ux * half
|
| 136 |
-
sum_xz[a, 1] += mz - uz * half
|
| 137 |
-
sum_xz[b, 0] += mx + ux * half
|
| 138 |
-
sum_xz[b, 1] += mz + uz * half
|
| 139 |
-
count[a] += 1
|
| 140 |
-
count[b] += 1
|
| 141 |
-
moved = count > 0
|
| 142 |
-
if not moved.any():
|
| 143 |
-
break
|
| 144 |
-
verts[moved, 0] = sum_xz[moved, 0] / count[moved]
|
| 145 |
-
verts[moved, 2] = sum_xz[moved, 1] / count[moved]
|
| 146 |
-
|
| 147 |
-
return verts.astype(np.asarray(vertices).dtype)
|
|
|
|
| 37 |
verts[a, 1] = avg_y
|
| 38 |
verts[b, 1] = avg_y
|
| 39 |
return verts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -8,14 +8,16 @@ os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
|
|
| 8 |
import subprocess
|
| 9 |
import sys
|
| 10 |
|
| 11 |
-
def install_if_missing(package):
|
| 12 |
try:
|
| 13 |
__import__(package.split("==")[0])
|
| 14 |
except ImportError:
|
| 15 |
-
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
|
| 16 |
|
| 17 |
install_if_missing("scipy")
|
| 18 |
install_if_missing("pandas")
|
|
|
|
|
|
|
| 19 |
|
| 20 |
from pathlib import Path
|
| 21 |
from tqdm import tqdm
|
|
@@ -50,9 +52,7 @@ from s23dr_2026_example.tokenizer import EdgeDepthSequenceConfig
|
|
| 50 |
from s23dr_2026_example.model import EdgeDepthSegmentsModel
|
| 51 |
from s23dr_2026_example.segment_postprocess import merge_vertices_iterative
|
| 52 |
from s23dr_2026_example.varifold import segments_to_vertices_edges
|
| 53 |
-
from s23dr_2026_example.postprocess_v2 import
|
| 54 |
-
snap_to_point_cloud, snap_horizontal, snap_vertical, snap_manhattan,
|
| 55 |
-
)
|
| 56 |
|
| 57 |
SEQ_LEN = 4096
|
| 58 |
COLMAP_QUOTA = 3072
|
|
@@ -234,11 +234,8 @@ def predict_sample(sample_dict, model, device):
|
|
| 234 |
cid_valid = cid[mask]
|
| 235 |
pv = snap_to_point_cloud(pv, xyz_world, cid_valid, snap_radius=SNAP_RADIUS)
|
| 236 |
|
| 237 |
-
#
|
| 238 |
-
# Each pass is a no-op on edges that don't qualify, so order is safe.
|
| 239 |
pv = snap_horizontal(pv, pe)
|
| 240 |
-
pv = snap_vertical(pv, pe)
|
| 241 |
-
pv = snap_manhattan(pv, pe)
|
| 242 |
|
| 243 |
if len(pv) < 2 or len(pe) < 1:
|
| 244 |
return empty_solution()
|
|
@@ -312,62 +309,6 @@ def hybrid_merge(pred_v, pred_e, track_v, track_e, merge_radius=0.8):
|
|
| 312 |
|
| 313 |
return np.array(final_v), final_e
|
| 314 |
|
| 315 |
-
|
| 316 |
-
def filter_by_colmap_support(pv, pe, sample, support_radius=0.6):
|
| 317 |
-
"""Drop predicted vertices that have NO COLMAP point within support_radius.
|
| 318 |
-
|
| 319 |
-
Hallucinated vertices from the model (predicted in 3D space with no real
|
| 320 |
-
geometric evidence) typically appear in regions with no COLMAP point cloud.
|
| 321 |
-
Filtering by COLMAP-presence is a precision-only operation: real vertices
|
| 322 |
-
survive (the COLMAP cloud covers all reconstructed regions of the building),
|
| 323 |
-
spurious model outputs in empty space get dropped.
|
| 324 |
-
|
| 325 |
-
Returns the filtered (vertices, edges). On any failure or empty result,
|
| 326 |
-
falls back to the unfiltered input to avoid an empty submission.
|
| 327 |
-
"""
|
| 328 |
-
try:
|
| 329 |
-
if not isinstance(pv, np.ndarray) or len(pv) < 2 or len(pe) < 1:
|
| 330 |
-
return pv, pe
|
| 331 |
-
from hoho2025.example_solutions import convert_entry_to_human_readable
|
| 332 |
-
good = convert_entry_to_human_readable(sample)
|
| 333 |
-
colmap_rec = good.get('colmap') or good.get('colmap_binary')
|
| 334 |
-
if colmap_rec is None:
|
| 335 |
-
return pv, pe
|
| 336 |
-
colmap_xyz = np.array(
|
| 337 |
-
[p.xyz for p in colmap_rec.points3D.values()], dtype=np.float64
|
| 338 |
-
)
|
| 339 |
-
if len(colmap_xyz) < 5:
|
| 340 |
-
return pv, pe
|
| 341 |
-
|
| 342 |
-
from scipy.spatial import cKDTree
|
| 343 |
-
tree = cKDTree(colmap_xyz)
|
| 344 |
-
dists, _ = tree.query(np.asarray(pv, dtype=np.float64), k=1)
|
| 345 |
-
keep_mask = dists <= support_radius
|
| 346 |
-
|
| 347 |
-
if keep_mask.all():
|
| 348 |
-
return pv, pe # nothing to filter
|
| 349 |
-
|
| 350 |
-
n_keep = int(keep_mask.sum())
|
| 351 |
-
# Require at least 2 vertices and 1 edge to remain after filtering.
|
| 352 |
-
if n_keep < 2:
|
| 353 |
-
return pv, pe
|
| 354 |
-
|
| 355 |
-
old_to_new = {int(old): new for new, old in enumerate(np.where(keep_mask)[0])}
|
| 356 |
-
new_pv = pv[keep_mask]
|
| 357 |
-
new_pe = []
|
| 358 |
-
for u, v in pe:
|
| 359 |
-
u, v = int(u), int(v)
|
| 360 |
-
if u in old_to_new and v in old_to_new and u != v:
|
| 361 |
-
new_pe.append((old_to_new[u], old_to_new[v]))
|
| 362 |
-
|
| 363 |
-
if len(new_pe) < 1:
|
| 364 |
-
return pv, pe # do not drop all edges
|
| 365 |
-
|
| 366 |
-
return new_pv, new_pe
|
| 367 |
-
except Exception:
|
| 368 |
-
return pv, pe
|
| 369 |
-
|
| 370 |
-
|
| 371 |
# ---------------------------------------------------------------------------
|
| 372 |
# Main
|
| 373 |
# ---------------------------------------------------------------------------
|
|
@@ -463,14 +404,20 @@ if __name__ == "__main__":
|
|
| 463 |
except Exception as track_e_err:
|
| 464 |
print(f" Track ensemble failed for {order_id}: {track_e_err}")
|
| 465 |
|
| 466 |
-
#
|
| 467 |
-
#
|
| 468 |
-
#
|
| 469 |
-
#
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
except Exception as e:
|
| 475 |
import traceback
|
| 476 |
print(f" Predict failed for {order_id}:\n{traceback.format_exc()}")
|
|
|
|
| 8 |
import subprocess
|
| 9 |
import sys
|
| 10 |
|
| 11 |
+
def install_if_missing(package, pip_name=None):
|
| 12 |
try:
|
| 13 |
__import__(package.split("==")[0])
|
| 14 |
except ImportError:
|
| 15 |
+
subprocess.check_call([sys.executable, "-m", "pip", "install", pip_name or package])
|
| 16 |
|
| 17 |
install_if_missing("scipy")
|
| 18 |
install_if_missing("pandas")
|
| 19 |
+
install_if_missing("open3d")
|
| 20 |
+
install_if_missing("skspatial", pip_name="scikit-spatial")
|
| 21 |
|
| 22 |
from pathlib import Path
|
| 23 |
from tqdm import tqdm
|
|
|
|
| 52 |
from s23dr_2026_example.model import EdgeDepthSegmentsModel
|
| 53 |
from s23dr_2026_example.segment_postprocess import merge_vertices_iterative
|
| 54 |
from s23dr_2026_example.varifold import segments_to_vertices_edges
|
| 55 |
+
from s23dr_2026_example.postprocess_v2 import snap_to_point_cloud, snap_horizontal
|
|
|
|
|
|
|
| 56 |
|
| 57 |
SEQ_LEN = 4096
|
| 58 |
COLMAP_QUOTA = 3072
|
|
|
|
| 234 |
cid_valid = cid[mask]
|
| 235 |
pv = snap_to_point_cloud(pv, xyz_world, cid_valid, snap_radius=SNAP_RADIUS)
|
| 236 |
|
| 237 |
+
# Horizontal snap
|
|
|
|
| 238 |
pv = snap_horizontal(pv, pe)
|
|
|
|
|
|
|
| 239 |
|
| 240 |
if len(pv) < 2 or len(pe) < 1:
|
| 241 |
return empty_solution()
|
|
|
|
| 309 |
|
| 310 |
return np.array(final_v), final_e
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
# ---------------------------------------------------------------------------
|
| 313 |
# Main
|
| 314 |
# ---------------------------------------------------------------------------
|
|
|
|
| 404 |
except Exception as track_e_err:
|
| 405 |
print(f" Track ensemble failed for {order_id}: {track_e_err}")
|
| 406 |
|
| 407 |
+
# Apply plane-intersection wireframe: RANSAC roof planes from the
|
| 408 |
+
# COLMAP cloud, intersect them to recover ridge/eave edges the
|
| 409 |
+
# learned model may miss. Recall-focused, geometry-only signal.
|
| 410 |
+
# hybrid_merge keeps it append-only at merge_radius=0.8.
|
| 411 |
+
try:
|
| 412 |
+
from plane_wireframe import predict_wireframe_planes
|
| 413 |
+
plane_v, plane_e = predict_wireframe_planes(sample)
|
| 414 |
+
if len(plane_v) > 0 and len(plane_e) > 0:
|
| 415 |
+
pred_v, pred_e = hybrid_merge(
|
| 416 |
+
pred_v, pred_e, plane_v, plane_e, merge_radius=0.8,
|
| 417 |
+
)
|
| 418 |
+
except Exception as plane_err:
|
| 419 |
+
print(f" Plane ensemble failed for {order_id}: {plane_err}")
|
| 420 |
+
|
| 421 |
except Exception as e:
|
| 422 |
import traceback
|
| 423 |
print(f" Predict failed for {order_id}:\n{traceback.format_exc()}")
|