| --- /Users/ihorivanyshyn/Documents/S23DR/s23dr-2026-submission/sklearn_submission.py 2026-04-26 12:52:14 |
| +++ /Users/ihorivanyshyn/Documents/S23DR/handcrafted_submission_2026/sklearn_submission.py 2026-05-06 12:53:15 |
| @@ -84,7 +84,8 @@ |
| # Stage 1 alone regressed in v16, but with DGCNN refinement the surviving |
| # candidates have median distance ~0.3 m to GT (vs ~1 m raw). |
| # v17 DGCNN vertex refinement — marginal on 100-sample sweep |
| -# (ΔHSS +0.001 at best). Disabled by default. |
| +# (ΔHSS +0.001 at best). Disabled by default. Keep this conservative: |
| +# adding/removing vertices has a larger blast radius than adding edges. |
| USE_DGCNN_REFINEMENT = False |
| DGCNN_CLS_THRESHOLD = 0.5 |
| DGCNN_DEDUP_RADIUS = 0.5 |
| @@ -100,7 +101,15 @@ |
| # t=0.7 +0.0039 (peak) t=0.8 +0.0031 |
| # Clean signal: F1 stable (±0.0006), IoU +0.0065 at t=0.7. |
| USE_DGCNN_EDGES = True |
| -DGCNN_EDGE_THRESHOLD = 0.7 |
| +# Ask the edge model for a wider candidate set, then apply our own |
| +# geometry gates below. This recovers medium-confidence true edges without |
| +# letting the classifier densify the graph unchecked. |
| +DGCNN_EDGE_THRESHOLD = 0.55 |
| +DGCNN_EDGE_STRONG_THRESHOLD = 0.70 |
| +DGCNN_EDGE_VERY_STRONG_THRESHOLD = 0.88 |
| +DGCNN_EDGE_MAX_LENGTH = 8.0 |
| +DGCNN_EDGE_MAX_PER_VERTEX = 2 |
| +DGCNN_EDGE_REPROJ_DILATE_PX = 4 |
| |
| # v16: 3D vertex candidates from the S23DR 2025 winner Stage 1 — DISABLED. |
| # Raw cluster centroids without PointNet Stage 2 refinement have median |
| @@ -191,7 +200,7 @@ |
| device = "cuda" if _torch.cuda.is_available() else "cpu" |
| except Exception: |
| device = "cpu" |
| - _DGCNN_EDGE_MODEL = load_edge_model("checkpoints/edge_model_dgcnn.pt", device=device) |
| + _DGCNN_EDGE_MODEL = load_edge_model("edge_model_dgcnn.pt", device=device) |
| return _DGCNN_EDGE_MODEL |
| |
| |
| @@ -214,7 +223,7 @@ |
| device = "cuda" if _torch.cuda.is_available() else "cpu" |
| except Exception: |
| device = "cpu" |
| - _DGCNN_VERTEX_MODEL = load_vertex_model("checkpoints/vertex_model_dgcnn.pt", device=device) |
| + _DGCNN_VERTEX_MODEL = load_vertex_model("vertex_model_dgcnn.pt", device=device) |
| return _DGCNN_VERTEX_MODEL |
| |
| # v7: ensemble with the standalone tracks-based predictor. |
| @@ -500,8 +509,96 @@ |
| if ok_views >= min_views: |
| return True |
| return ok_views >= min_views |
| + |
| + |
| +def _passes_dgcnn_edge_gates( |
| + v1: np.ndarray, |
| + v2: np.ndarray, |
| + prob: float, |
| + all_xyz: np.ndarray, |
| + kd_tree=None, |
| + masks: dict | None = None, |
| + views: dict | None = None, |
| +) -> bool: |
| + """Conservative accept rule for learned edge candidates. |
| + |
| + The DGCNN classifier is useful for recall, but raw learned edges can hurt |
| + IoU if accepted without geometry. Strong candidates need COLMAP support; |
| + very strong candidates may pass with looser sparse support; medium |
| + candidates must also reproject onto gestalt edge pixels. |
| + """ |
| + length = float(np.linalg.norm(v2 - v1)) |
| + if length < 0.25 or length > DGCNN_EDGE_MAX_LENGTH: |
| + return False |
| + |
| + strong_support = validate_edge( |
| + v1, v2, all_xyz, kd_tree, |
| + n_samples=24, radius=0.45, min_ratio=0.55, |
| + ) |
| + if prob >= DGCNN_EDGE_STRONG_THRESHOLD and strong_support: |
| + return True |
| + |
| + loose_support = validate_edge( |
| + v1, v2, all_xyz, kd_tree, |
| + n_samples=24, radius=0.60, min_ratio=0.35, |
| + ) |
| + if prob >= DGCNN_EDGE_VERY_STRONG_THRESHOLD and loose_support: |
| + return True |
| + |
| + if prob >= DGCNN_EDGE_STRONG_THRESHOLD and loose_support and masks and views: |
| + return validate_edge_reprojection( |
| + v1, v2, masks, views, |
| + n_samples=24, min_views=1, min_hit_frac=0.35, |
| + ) |
| + |
| + return False |
| + |
| + |
| +def _select_dgcnn_edges( |
| + final_v: np.ndarray, |
| + final_e: list, |
| + dgcnn_edges: list, |
| + all_xyz: np.ndarray, |
| + kd_tree=None, |
| + masks: dict | None = None, |
| + views: dict | None = None, |
| +) -> list[tuple[int, int]]: |
| + """Filter and degree-cap DGCNN edge proposals. |
| |
| + Existing edges are never removed here. At most |
| + ``DGCNN_EDGE_MAX_PER_VERTEX`` learned edges are added at each vertex, |
| + prioritising higher classifier probabilities. |
| + """ |
| + existing = {tuple(sorted(e)) for e in final_e} |
| + candidates = [] |
| + for i, j, prob in dgcnn_edges: |
| + lo, hi = (int(i), int(j)) if i < j else (int(j), int(i)) |
| + if lo == hi or (lo, hi) in existing: |
| + continue |
| + prob = float(prob) |
| + if _passes_dgcnn_edge_gates( |
| + final_v[lo], final_v[hi], prob, |
| + all_xyz, kd_tree, masks=masks, views=views, |
| + ): |
| + candidates.append((prob, lo, hi)) |
| |
| + candidates.sort(reverse=True) |
| + added_per_vertex = np.zeros(len(final_v), dtype=np.int32) |
| + accepted: list[tuple[int, int]] = [] |
| + accepted_set = set() |
| + for prob, lo, hi in candidates: |
| + if (lo, hi) in accepted_set: |
| + continue |
| + if (added_per_vertex[lo] >= DGCNN_EDGE_MAX_PER_VERTEX |
| + or added_per_vertex[hi] >= DGCNN_EDGE_MAX_PER_VERTEX): |
| + continue |
| + accepted.append((lo, hi)) |
| + accepted_set.add((lo, hi)) |
| + added_per_vertex[lo] += 1 |
| + added_per_vertex[hi] += 1 |
| + return accepted |
| + |
| + |
| def validate_edge(v1, v2, all_xyz, kd_tree=None, n_samples=20, radius=0.35, min_ratio=0.70): |
| """Check if edge v1→v2 is supported by COLMAP point cloud. |
| |
| @@ -1051,9 +1148,11 @@ |
| if len(final_v) < 2 or len(final_e) < 1: |
| return empty_solution() |
| |
| - # v18: DGCNN edge classifier — placed AFTER prune_not_connected so |
| - # that the vertex set is already fixed (no ghost vertices rescued by |
| - # spurious DGCNN edges). Only adds edges between surviving vertices. |
| + # v19: guarded DGCNN edge rescue. The learned model is queried at a |
| + # recall-friendly threshold, but new edges are accepted only if they |
| + # also have sparse-cloud or reprojection evidence, then degree-capped. |
| + # This targets the main weakness of v18: useful classifier recall |
| + # without raw learned edges turning roofs into dense graphs. |
| if USE_DGCNN_EDGES and len(final_v) >= 2: |
| edge_model = _get_dgcnn_edge_model() |
| if edge_model is not None: |
| @@ -1075,11 +1174,24 @@ |
| threshold=DGCNN_EDGE_THRESHOLD, |
| ) |
| if dgcnn_edges: |
| - existing = set(tuple(sorted(e)) for e in final_e) |
| - for i, j, prob in dgcnn_edges: |
| - lo, hi = (i, j) if i < j else (j, i) |
| - if (lo, hi) not in existing: |
| - final_e.append((lo, hi)) |
| + masks, mvs_views = {}, {} |
| + try: |
| + masks, mvs_views = _build_gestalt_edge_masks( |
| + entry, dilate_px=DGCNN_EDGE_REPROJ_DILATE_PX, |
| + ) |
| + except Exception: |
| + pass |
| + extra = _select_dgcnn_edges( |
| + np.asarray(final_v, dtype=np.float64), |
| + final_e, |
| + dgcnn_edges, |
| + all_xyz, |
| + kd_tree, |
| + masks=masks, |
| + views=mvs_views, |
| + ) |
| + if extra: |
| + final_e.extend(extra) |
| except Exception: |
| pass |
| |
|
|