From 3ef5d7c7d290cbcb124afb9f5fe654ad66765890 Mon Sep 17 00:00:00 2001 From: SamGu-NRX Date: Fri, 23 Oct 2026 08:21:13 -0500 Subject: [PATCH 1/3] fix: make dense geometry deterministic Co-Authored-By: Claude Code --- backend/server/dense/run.py | 9 +++++ backend/server/eval/extract.py | 14 ++++--- backend/server/tests/test_determinism.py | 48 ++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 backend/server/tests/test_determinism.py diff --git a/backend/server/dense/run.py b/backend/server/dense/run.py index c4331fe03..f99bb81e2 100644 --- a/backend/server/dense/run.py +++ b/backend/server/dense/run.py @@ -60,6 +60,11 @@ def _stage_result(artifacts: dict[str, str], metrics: dict, warnings: list[str]) return StageResult(artifacts=artifacts, metrics=numbers, warnings=list(warnings)) +def _canonical_point_order(points: np.ndarray, src: np.ndarray, tracks: list[np.ndarray]) -> np.ndarray: + """Return one stable order for point rows before fit and artifact writing.""" + return np.lexsort((points[:, 2], points[:, 1], points[:, 0], -src)) + + def _work_artifacts(work: Path, paths: list[Path]) -> dict[str, str]: """ยง3.9 key and work-dir relative path for each file that exists.""" artifacts = {} @@ -122,6 +127,10 @@ def run_dense( learned = _attach_learned_depth(models, cameras, params, report) points = np.asarray(points, float) src = np.asarray(src if src is not None else np.ones(len(points), int), int) + if len(points): + order = np.lexsort((points[:, 2], points[:, 1], points[:, 0])) + points = points[order] + src = src[order] if dense_tracks is None: dense_tracks = _injected_tracks(len(points), models) sparse = _sparse_points(poses_dir) diff --git a/backend/server/eval/extract.py b/backend/server/eval/extract.py index 85ffbe480..d3b8e7145 100644 --- a/backend/server/eval/extract.py +++ b/backend/server/eval/extract.py @@ -48,6 +48,8 @@ def end(self, k): def ransac_plane_up(X, up, rng, thr=0.05, iters=800, max_tilt_deg=12.0): best_k, best = -1, None cmin = np.cos(np.radians(max_tilt_deg)) + canonical = np.lexsort((X[:, 2], X[:, 1], X[:, 0])) + X = np.asarray(X)[canonical] Xs = X[rng.integers(0, len(X), min(len(X), 60000))] for _ in range(iters): s = Xs[rng.integers(0, len(Xs), 3)] @@ -71,8 +73,11 @@ def ransac_plane_up(X, up, rng, thr=0.05, iters=800, max_tilt_deg=12.0): def sequential_lines(Q, rng, thr=0.08, min_pts=120, min_len=1.5, gap=1.0, max_lines=14, iters=500): - """Plan points -> wall segments.""" + """Plan points -> wall segments with a canonical sampling order.""" segs: list[Segment] = [] + Q = np.asarray(Q, float) + order = np.lexsort((Q[:, 1], Q[:, 0])) + Q = Q[order] pool = np.arange(len(Q)) for _ in range(max_lines): if len(pool) < min_pts: @@ -97,8 +102,8 @@ def sequential_lines(Q, rng, thr=0.08, min_pts=120, min_len=1.5, gap=1.0, max_li nrm = np.array([-d[1], d[0]]) inl = np.abs((P - c) @ nrm) < thr t = (P[inl] - c) @ d - order = np.argsort(t) - ts, ids = t[order], pool[inl][order] + order_t = np.argsort(t) + ts, ids = t[order_t], order[pool[inl][order_t]] cuts = np.flatnonzero(np.diff(ts) > gap) + 1 for part_t, part_i in zip(np.split(ts, cuts), np.split(ids, cuts)): if len(part_t) < min_pts * 0.5: @@ -371,8 +376,7 @@ def cast(cam: Camera, u, v, allow=None, face=None, along_slack: float = 0.5, req return best hits = [ - cast(cams[ci], u, v, along_slack=p.meter_along_slack_m, require_high=True, near=M0) - for ci, u, v in meter_clicks + cast(cams[ci], u, v, along_slack=p.meter_along_slack_m, require_high=True, near=M0) for ci, u, v in meter_clicks ] hits = [x for x in hits if x is not None] if not hits and M0 is not None: diff --git a/backend/server/tests/test_determinism.py b/backend/server/tests/test_determinism.py new file mode 100644 index 000000000..17ec25cff --- /dev/null +++ b/backend/server/tests/test_determinism.py @@ -0,0 +1,48 @@ +import numpy as np + +from server.dense.run import _canonical_point_order +from server.eval.extract import sequential_lines + + +def _line_geometry(segments): + return sorted( + ( + round(float(segment.c[0]), 6), + round(float(segment.c[1]), 6), + round(float(segment.d[0]), 6), + round(float(segment.d[1]), 6), + round(float(segment.t0), 6), + round(float(segment.t1), 6), + ) + for segment in segments + ) + + +def test_dense_point_order_is_canonical(): + points = np.array([[2.0, 0.0, 1.0], [1.0, 0.0, 3.0], [1.0, 0.0, 2.0]]) + src = np.array([1, 0, 1]) + tracks = [np.array([2]), np.array([1]), np.array([0])] + permutation = np.array([2, 0, 1]) + + first_order = _canonical_point_order(points, src, tracks) + second_order = _canonical_point_order(points[permutation], src[permutation], [tracks[i] for i in permutation]) + + assert np.array_equal(points[first_order], points[permutation][second_order]) + assert np.array_equal(src[first_order], src[permutation][second_order]) + + +def test_sequential_lines_is_independent_of_point_order(): + rng = np.random.default_rng(1) + x = np.linspace(-4.0, 4.0, 100) + points = np.vstack( + ( + np.column_stack((x + rng.normal(0.0, 0.01, len(x)), rng.normal(0.0, 0.04, len(x)))), + np.column_stack((x + rng.normal(0.0, 0.01, len(x)), 1.0 + rng.normal(0.0, 0.04, len(x)))), + ) + ) + permuted = points[rng.permutation(len(points))] + + first = sequential_lines(points, np.random.default_rng(7), min_pts=60, max_lines=2) + second = sequential_lines(permuted, np.random.default_rng(7), min_pts=60, max_lines=2) + + assert _line_geometry(first) == _line_geometry(second) From d6be95ae6206b1aeb00dffb908674baf3374f9e1 Mon Sep 17 00:00:00 2001 From: SamGu-NRX Date: Fri, 23 Oct 2026 09:25:51 -0500 Subject: [PATCH 2/3] fix: keep dense tracks aligned during sorting Co-Authored-By: Claude Code --- backend/server/dense/run.py | 15 ++++++++------- backend/server/tests/test_determinism.py | 23 ++++++++++++++++------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/backend/server/dense/run.py b/backend/server/dense/run.py index f99bb81e2..bcfb4f3e8 100644 --- a/backend/server/dense/run.py +++ b/backend/server/dense/run.py @@ -60,9 +60,12 @@ def _stage_result(artifacts: dict[str, str], metrics: dict, warnings: list[str]) return StageResult(artifacts=artifacts, metrics=numbers, warnings=list(warnings)) -def _canonical_point_order(points: np.ndarray, src: np.ndarray, tracks: list[np.ndarray]) -> np.ndarray: - """Return one stable order for point rows before fit and artifact writing.""" - return np.lexsort((points[:, 2], points[:, 1], points[:, 0], -src)) +def _canonicalize_point_rows( + points: np.ndarray, src: np.ndarray, tracks: list[np.ndarray] +) -> tuple[np.ndarray, np.ndarray, list[np.ndarray]]: + """Sort point rows once and keep source and observation rows aligned.""" + order = np.lexsort((points[:, 2], points[:, 1], points[:, 0], -src)) + return points[order], src[order], [tracks[int(index)] for index in order] def _work_artifacts(work: Path, paths: list[Path]) -> dict[str, str]: @@ -127,12 +130,10 @@ def run_dense( learned = _attach_learned_depth(models, cameras, params, report) points = np.asarray(points, float) src = np.asarray(src if src is not None else np.ones(len(points), int), int) - if len(points): - order = np.lexsort((points[:, 2], points[:, 1], points[:, 0])) - points = points[order] - src = src[order] if dense_tracks is None: dense_tracks = _injected_tracks(len(points), models) + if len(points): + points, src, dense_tracks = _canonicalize_point_rows(points, src, dense_tracks) sparse = _sparse_points(poses_dir) sparse_tracks, sparse_ids = _sparse_tracks(poses_dir, len(sparse), report) if len(sparse): diff --git a/backend/server/tests/test_determinism.py b/backend/server/tests/test_determinism.py index 17ec25cff..88c9bd1e0 100644 --- a/backend/server/tests/test_determinism.py +++ b/backend/server/tests/test_determinism.py @@ -1,6 +1,6 @@ import numpy as np -from server.dense.run import _canonical_point_order +from server.dense.run import _canonicalize_point_rows from server.eval.extract import sequential_lines @@ -18,17 +18,26 @@ def _line_geometry(segments): ) -def test_dense_point_order_is_canonical(): +def test_dense_point_order_keeps_observations_with_their_points(): points = np.array([[2.0, 0.0, 1.0], [1.0, 0.0, 3.0], [1.0, 0.0, 2.0]]) src = np.array([1, 0, 1]) - tracks = [np.array([2]), np.array([1]), np.array([0])] + tracks = [np.array([20]), np.array([10]), np.array([30])] permutation = np.array([2, 0, 1]) - first_order = _canonical_point_order(points, src, tracks) - second_order = _canonical_point_order(points[permutation], src[permutation], [tracks[i] for i in permutation]) + canonical_points, canonical_src, canonical_tracks = _canonicalize_point_rows(points, src, tracks) + shuffled_tracks = [tracks[i] for i in permutation] + shuffled_points, shuffled_src, shuffled_tracks = _canonicalize_point_rows( + points[permutation], src[permutation], shuffled_tracks + ) - assert np.array_equal(points[first_order], points[permutation][second_order]) - assert np.array_equal(src[first_order], src[permutation][second_order]) + assert np.array_equal(canonical_points, shuffled_points) + assert np.array_equal(canonical_src, shuffled_src) + assert [row.tolist() for row in canonical_tracks] == [row.tolist() for row in shuffled_tracks] + assert {tuple(point): int(track[0]) for point, track in zip(canonical_points, canonical_tracks, strict=True)} == { + (2.0, 0.0, 1.0): 20, + (1.0, 0.0, 3.0): 10, + (1.0, 0.0, 2.0): 30, + } def test_sequential_lines_is_independent_of_point_order(): From bf171d408d154b51a385b9976793c2201f31f4e5 Mon Sep 17 00:00:00 2001 From: SamGu-NRX Date: Fri, 23 Oct 2026 09:45:27 -0500 Subject: [PATCH 3/3] test: cover dense track alignment through stage output Co-Authored-By: Claude Code --- backend/server/tests/test_determinism.py | 36 ++++++++++++++++-------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/backend/server/tests/test_determinism.py b/backend/server/tests/test_determinism.py index 88c9bd1e0..e2d96ac05 100644 --- a/backend/server/tests/test_determinism.py +++ b/backend/server/tests/test_determinism.py @@ -1,6 +1,8 @@ import numpy as np -from server.dense.run import _canonicalize_point_rows +from server.common.tracks import read_tracks +from server.dense.params import DenseParams +from server.dense.run import run_dense from server.eval.extract import sequential_lines @@ -18,22 +20,32 @@ def _line_geometry(segments): ) -def test_dense_point_order_keeps_observations_with_their_points(): +def test_dense_stage_keeps_observations_with_their_points(tmp_path): points = np.array([[2.0, 0.0, 1.0], [1.0, 0.0, 3.0], [1.0, 0.0, 2.0]]) - src = np.array([1, 0, 1]) tracks = [np.array([20]), np.array([10]), np.array([30])] permutation = np.array([2, 0, 1]) + cameras = [{"pose": np.eye(4).tolist(), "w": 4, "h": 4, "intrinsics": [1, 1, 0, 0]}] + poses_dir = tmp_path / "poses" + poses_dir.mkdir() + (poses_dir / "cameras.json").write_text("[]") - canonical_points, canonical_src, canonical_tracks = _canonicalize_point_rows(points, src, tracks) - shuffled_tracks = [tracks[i] for i in permutation] - shuffled_points, shuffled_src, shuffled_tracks = _canonicalize_point_rows( - points[permutation], src[permutation], shuffled_tracks + run_dense( + {}, + poses_dir, + {"scale_factor": 1.0, "origin_m": [0, 0, 0]}, + tmp_path / "dense", + params=DenseParams(), + models={ + "cameras": cameras, + "points_w": points[permutation], + "point_src": np.ones(len(points), int), + "point_tracks": [tracks[i] for i in permutation], + }, ) - - assert np.array_equal(canonical_points, shuffled_points) - assert np.array_equal(canonical_src, shuffled_src) - assert [row.tolist() for row in canonical_tracks] == [row.tolist() for row in shuffled_tracks] - assert {tuple(point): int(track[0]) for point, track in zip(canonical_points, canonical_tracks, strict=True)} == { + written_points = np.load(tmp_path / "dense" / "cloud.npz")["points"] + written_tracks, _ = read_tracks(tmp_path / "dense" / "tracks.npz") + assert written_tracks is not None + assert {tuple(point): int(track[0]) for point, track in zip(written_points, written_tracks, strict=True)} == { (2.0, 0.0, 1.0): 20, (1.0, 0.0, 3.0): 10, (1.0, 0.0, 2.0): 30,