Boost: COLMAP plane refinement + junction constraints + snap fixes
Browse filesInference-only improvements (no retraining required) targeting both
corner_f1 and edge_iou:
- Integrate refine_vertices_multiview_plane in main loop. Built but
never called; snaps each vertex onto the roof plane visible at its
2D projection across views, weighted by per-view PCA quality.
Directly targets corner_f1 (currently the largest gap vs leaders).
- Integrate apply_junction_constraints after hybrid_merge: drops
collinear duplicate vertices, antiparallel duplicate edges, and
short leaf edges. Improves edge_iou by removing structurally
invalid topology. Falls back to pre-junction graph if it would
produce zero edges.
- snap_to_point_cloud now includes flashing_end_point (class 3) as
a snap target. Previously only classes 1+2 were used.
- snap_horizontal now only moves a vertex when ALL of its incident
edges are near-horizontal — prevents corruption of sloped edges
that share a vertex with an eave.
- hybrid_merge: accept track edges between two existing predicted
vertices when both are within a tight 0.3 m radius (high-confidence
topology correction). Previously only edges touching new vertices
were accepted.
Each new integration is wrapped in try/except so a failure on any
sample falls back to the previous behaviour.
- s23dr_2026_example/postprocess_v2.py +33 -8
- script.py +64 -11
|
@@ -6,7 +6,7 @@ def snap_to_point_cloud(vertices, xyz, class_id, snap_radius=0.5,
|
|
| 6 |
target_classes=None):
|
| 7 |
"""Snap vertices to nearby point cloud clusters of specific semantic classes."""
|
| 8 |
if target_classes is None:
|
| 9 |
-
target_classes = [1, 2] # apex, eave_end_point
|
| 10 |
|
| 11 |
snapped = vertices.copy()
|
| 12 |
mask = np.isin(class_id, target_classes)
|
|
@@ -26,14 +26,39 @@ def snap_to_point_cloud(vertices, xyz, class_id, snap_radius=0.5,
|
|
| 26 |
|
| 27 |
|
| 28 |
def snap_horizontal(vertices, edges, max_slope=0.05):
|
| 29 |
-
"""Snap near-horizontal edges to be exactly horizontal.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
verts = vertices.copy()
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
a, b = int(a), int(b)
|
| 33 |
dy = abs(verts[a, 1] - verts[b, 1])
|
| 34 |
-
dxz = np.sqrt((verts[a, 0] - verts[b, 0])**2 + (verts[a, 2] - verts[b, 2])**2)
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
return verts
|
|
|
|
| 6 |
target_classes=None):
|
| 7 |
"""Snap vertices to nearby point cloud clusters of specific semantic classes."""
|
| 8 |
if target_classes is None:
|
| 9 |
+
target_classes = [1, 2, 3] # apex, eave_end_point, flashing_end_point
|
| 10 |
|
| 11 |
snapped = vertices.copy()
|
| 12 |
mask = np.isin(class_id, target_classes)
|
|
|
|
| 26 |
|
| 27 |
|
| 28 |
def snap_horizontal(vertices, edges, max_slope=0.05):
|
| 29 |
+
"""Snap near-horizontal edges to be exactly horizontal.
|
| 30 |
+
|
| 31 |
+
Only snaps a vertex if ALL of its incident edges are near-horizontal —
|
| 32 |
+
otherwise we would corrupt sloped edges that share the vertex.
|
| 33 |
+
"""
|
| 34 |
verts = vertices.copy()
|
| 35 |
+
n = len(verts)
|
| 36 |
+
if n == 0 or len(edges) == 0:
|
| 37 |
+
return verts
|
| 38 |
+
|
| 39 |
+
edge_is_h = []
|
| 40 |
+
incident = [[] for _ in range(n)]
|
| 41 |
+
for ei, (a, b) in enumerate(edges):
|
| 42 |
a, b = int(a), int(b)
|
| 43 |
dy = abs(verts[a, 1] - verts[b, 1])
|
| 44 |
+
dxz = np.sqrt((verts[a, 0] - verts[b, 0]) ** 2 + (verts[a, 2] - verts[b, 2]) ** 2)
|
| 45 |
+
is_h = dxz > 0.1 and dy / dxz < max_slope
|
| 46 |
+
edge_is_h.append(is_h)
|
| 47 |
+
incident[a].append(ei)
|
| 48 |
+
incident[b].append(ei)
|
| 49 |
+
|
| 50 |
+
purely_h = [
|
| 51 |
+
len(incident[v]) > 0 and all(edge_is_h[ei] for ei in incident[v])
|
| 52 |
+
for v in range(n)
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
for ei, (a, b) in enumerate(edges):
|
| 56 |
+
if not edge_is_h[ei]:
|
| 57 |
+
continue
|
| 58 |
+
a, b = int(a), int(b)
|
| 59 |
+
if not (purely_h[a] and purely_h[b]):
|
| 60 |
+
continue
|
| 61 |
+
avg_y = 0.5 * (verts[a, 1] + verts[b, 1])
|
| 62 |
+
verts[a, 1] = avg_y
|
| 63 |
+
verts[b, 1] = avg_y
|
| 64 |
return verts
|
|
@@ -293,18 +293,38 @@ def hybrid_merge(pred_v, pred_e, track_v, track_e, merge_radius=0.8):
|
|
| 293 |
for u, v in final_e:
|
| 294 |
existing_edges.add((min(u, v), max(u, v)))
|
| 295 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
for u_t, v_t in track_e:
|
| 297 |
u_f = track_to_final.get(u_t)
|
| 298 |
v_f = track_to_final.get(v_t)
|
| 299 |
-
if u_f is
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
return np.array(final_v), final_e
|
| 309 |
|
| 310 |
# ---------------------------------------------------------------------------
|
|
@@ -397,11 +417,44 @@ if __name__ == "__main__":
|
|
| 397 |
from triangulation import predict_wireframe_tracks
|
| 398 |
# Use min_views=3 for highly precise, conservative geometric tracks
|
| 399 |
track_v, track_e = predict_wireframe_tracks(sample, min_views=3)
|
| 400 |
-
|
| 401 |
pred_v, pred_e = hybrid_merge(pred_v, pred_e, track_v, track_e, merge_radius=0.8)
|
| 402 |
except Exception as track_e_err:
|
| 403 |
print(f" Track ensemble failed for {order_id}: {track_e_err}")
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
except Exception as e:
|
| 406 |
import traceback
|
| 407 |
print(f" Predict failed for {order_id}:\n{traceback.format_exc()}")
|
|
|
|
| 293 |
for u, v in final_e:
|
| 294 |
existing_edges.add((min(u, v), max(u, v)))
|
| 295 |
|
| 296 |
+
# Tight-confidence radius for accepting a track edge between two
|
| 297 |
+
# already-existing predicted vertices (topology correction).
|
| 298 |
+
tight_radius = 0.3
|
| 299 |
+
|
| 300 |
for u_t, v_t in track_e:
|
| 301 |
u_f = track_to_final.get(u_t)
|
| 302 |
v_f = track_to_final.get(v_t)
|
| 303 |
+
if u_f is None or v_f is None or u_f == v_f:
|
| 304 |
+
continue
|
| 305 |
+
e = (min(u_f, v_f), max(u_f, v_f))
|
| 306 |
+
if e in existing_edges:
|
| 307 |
+
continue
|
| 308 |
+
|
| 309 |
+
u_is_new = u_f >= len(pred_v)
|
| 310 |
+
v_is_new = v_f >= len(pred_v)
|
| 311 |
+
|
| 312 |
+
if u_is_new or v_is_new:
|
| 313 |
+
# New vertex: trust the track edge.
|
| 314 |
+
final_e.append(e)
|
| 315 |
+
existing_edges.add(e)
|
| 316 |
+
continue
|
| 317 |
+
|
| 318 |
+
# Both endpoints are existing predicted vertices — only accept the
|
| 319 |
+
# track edge as a topology correction when both are very confidently
|
| 320 |
+
# matched (tight distance). This recovers missing edges without
|
| 321 |
+
# letting the tracker rewire the learned topology.
|
| 322 |
+
d_u = dists[u_t] if u_t < len(dists) else np.inf
|
| 323 |
+
d_v = dists[v_t] if v_t < len(dists) else np.inf
|
| 324 |
+
if d_u <= tight_radius and d_v <= tight_radius:
|
| 325 |
+
final_e.append(e)
|
| 326 |
+
existing_edges.add(e)
|
| 327 |
+
|
| 328 |
return np.array(final_v), final_e
|
| 329 |
|
| 330 |
# ---------------------------------------------------------------------------
|
|
|
|
| 417 |
from triangulation import predict_wireframe_tracks
|
| 418 |
# Use min_views=3 for highly precise, conservative geometric tracks
|
| 419 |
track_v, track_e = predict_wireframe_tracks(sample, min_views=3)
|
| 420 |
+
|
| 421 |
pred_v, pred_e = hybrid_merge(pred_v, pred_e, track_v, track_e, merge_radius=0.8)
|
| 422 |
except Exception as track_e_err:
|
| 423 |
print(f" Track ensemble failed for {order_id}: {track_e_err}")
|
| 424 |
+
|
| 425 |
+
# COLMAP multi-view plane refinement: snaps each vertex to the
|
| 426 |
+
# roof plane visible at its 2D projection. Targets corner_f1.
|
| 427 |
+
try:
|
| 428 |
+
if isinstance(pred_v, np.ndarray) and len(pred_v) >= 1:
|
| 429 |
+
from colmap_refine import refine_vertices_multiview_plane
|
| 430 |
+
refined, snapped_mask = refine_vertices_multiview_plane(
|
| 431 |
+
pred_v, sample,
|
| 432 |
+
knn_2d_px=30.0, knn_k=12, min_neighbours=6,
|
| 433 |
+
max_displacement=0.5, min_quality=0.5, min_views=2,
|
| 434 |
+
)
|
| 435 |
+
if snapped_mask.any():
|
| 436 |
+
pred_v = refined
|
| 437 |
+
except Exception as refine_err:
|
| 438 |
+
print(f" COLMAP refine failed for {order_id}: {refine_err}")
|
| 439 |
+
|
| 440 |
+
# Junction constraints: drop collinear/duplicate/short-leaf edges
|
| 441 |
+
try:
|
| 442 |
+
if isinstance(pred_v, np.ndarray) and len(pred_v) >= 2 and len(pred_e) >= 1:
|
| 443 |
+
from junction import apply_junction_constraints
|
| 444 |
+
jv, je = apply_junction_constraints(
|
| 445 |
+
pred_v, pred_e,
|
| 446 |
+
collinear_cos=0.97,
|
| 447 |
+
duplicate_cos=0.985,
|
| 448 |
+
leaf_min_len=0.4,
|
| 449 |
+
max_passes=3,
|
| 450 |
+
)
|
| 451 |
+
# Only accept if at least one edge survives — never let
|
| 452 |
+
# the post-processing reduce us to an empty graph.
|
| 453 |
+
if len(je) >= 1:
|
| 454 |
+
pred_v, pred_e = jv, je
|
| 455 |
+
except Exception as junc_err:
|
| 456 |
+
print(f" Junction constraints failed for {order_id}: {junc_err}")
|
| 457 |
+
|
| 458 |
except Exception as e:
|
| 459 |
import traceback
|
| 460 |
print(f" Predict failed for {order_id}:\n{traceback.format_exc()}")
|