-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain_semantic.py
More file actions
3142 lines (2930 loc) · 134 KB
/
Copy pathmain_semantic.py
File metadata and controls
3142 lines (2930 loc) · 134 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import argparse
import datetime
import json
import pathlib
import os
import sys
import time
import subprocess
import atexit
import cv2
import numpy as np
import lietorch
import torch
import tqdm
import yaml
from mast3r_slam.global_opt import FactorGraph
from mast3r_slam.config import load_config, config, set_global_config
from mast3r_slam.dataloader import Intrinsics, load_dataset
import mast3r_slam.evaluate as eval
from mast3r_slam.frame import Mode, SharedKeyframes, SharedStates, create_frame, create_frame_semantic
from mast3r_slam.mast3r_utils import (
load_mast3r,
load_retriever,
mast3r_inference_mono,
)
from mast3r_slam.multiprocess_utils import new_queue, try_get_msg
from mast3r_slam.tracker import FrameTracker
from mast3r_slam.visualizatio_semantics import WindowMsg, run_visualization
from mast3r_slam.visualization_utils import depth2rgb
from mast3r_slam.semantic_stabilizer import ensure_hard_label_hw, label_code_to_rgb
from mast3r_slam.lietorch_utils import as_SE3
import torch.multiprocessing as mp
# -----------------------------------------------------------------------------
# Lightweight timing / profiling
#
# Why:
# Speedy MASt3R accelerates only some parts of the pipeline (e.g., attention /
# RoPE / AMP). End-to-end SLAM FPS may still be dominated by other components
# (semantic segmentation, matching backend, GN, I/O, etc.).
#
# Design goals:
# - When disabled, have negligible overhead (single `if` checks; no monkeypatching).
# - When enabled, print averages every N frames and (optionally) time key MASt3R
# and matching functions via monkeypatching. Profiling mode WILL affect FPS
# because it introduces CUDA synchronization to measure GPU time.
# -----------------------------------------------------------------------------
# IMPORTANT:
# This profiling mode *forces CUDA synchronization* via `torch.cuda.Event` timing.
# That can severely reduce FPS and distort real-time performance.
#
# Keep it OFF by default. Enable explicitly via:
# MAST3R_SLAM_ENABLE_TIMING=1 python main_semantic.py ...
#
# We intentionally avoid adding more CLI flags here to keep the interface stable.
ENABLE_TIMING = bool(int(os.environ.get("MAST3R_SLAM_ENABLE_TIMING", "0")))
TIMING_PRINT_EVERY = 30
_timing = None
if ENABLE_TIMING:
import os
from collections import defaultdict
class _Timing:
def __init__(self, print_every: int = 30):
self.print_every = int(print_every)
self._sum_ms = defaultdict(float)
self._count = defaultdict(int)
self._t0 = time.perf_counter()
self._frames = 0
def add_ms(self, key: str, ms: float):
self._sum_ms[key] += float(ms)
self._count[key] += 1
def tick_frame(self):
self._frames += 1
def maybe_print(self, frame_idx: int):
if self.print_every <= 0:
return
if frame_idx <= 0 or frame_idx % self.print_every != 0:
return
dt = time.perf_counter() - self._t0
fps = (self._frames / dt) if dt > 0 else 0.0
def avg(key: str) -> float:
c = self._count.get(key, 0)
return (self._sum_ms.get(key, 0.0) / c) if c else 0.0
pid = os.getpid()
proc_name = mp.current_process().name if hasattr(mp, "current_process") else "unknown"
print(
"[TIMING]"
f" pid={pid}"
f" proc={proc_name}"
f" frames={self._frames}"
f" fps={fps:.2f}"
f" dataset_ms={avg('dataset_ms'):.2f}"
f" frame_ms={avg('frame_ms'):.2f}"
f" track_ms={avg('track_ms'):.2f}"
f" mono_ms={avg('mono_ms'):.2f}"
f" mast3r_asym_ms={avg('mast3r_asym_ms'):.2f}"
f" mast3r_sym_ms={avg('mast3r_sym_ms'):.2f}"
f" match_ms={avg('match_ms'):.2f}"
f" backend_add_ms={avg('backend_add_ms'):.2f}"
f" backend_solve_ms={avg('backend_solve_ms'):.2f}"
)
# Reset window
self._sum_ms.clear()
self._count.clear()
self._t0 = time.perf_counter()
self._frames = 0
_timing = _Timing(print_every=TIMING_PRINT_EVERY)
def _wrap_cuda_ms(key: str, fn):
def wrapped(*args, **kwargs):
if not torch.cuda.is_available():
t0 = time.perf_counter()
out = fn(*args, **kwargs)
_timing.add_ms(key, (time.perf_counter() - t0) * 1000.0)
return out
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
start.record()
out = fn(*args, **kwargs)
end.record()
end.synchronize()
_timing.add_ms(key, start.elapsed_time(end))
return out
return wrapped
# Monkeypatch a few hotspots to isolate where time goes.
# This affects BOTH the main and backend processes (spawn imports this module),
# but only when ENABLE_TIMING=True.
import mast3r_slam.mast3r_utils as _mu
import mast3r_slam.matching as _mm
_mu.mast3r_asymmetric_inference = _wrap_cuda_ms(
"mast3r_asym_ms", _mu.mast3r_asymmetric_inference
)
_mu.mast3r_decode_symmetric_batch = _wrap_cuda_ms(
"mast3r_sym_ms", _mu.mast3r_decode_symmetric_batch
)
_mu.mast3r_inference_mono = _wrap_cuda_ms("mono_ms", _mu.mast3r_inference_mono)
_mm.match = _wrap_cuda_ms("match_ms", _mm.match)
# Ensure this module's imported symbol points at the wrapped function too.
mast3r_inference_mono = _mu.mast3r_inference_mono
def maybe_save_depth_image(frame, depth_dir, scale=1.0):
return
if depth_dir is None or frame.X_canon is None:
return
h, w = frame.img_shape.flatten().long().cpu().tolist()
depth = frame.X_canon.view(h, w, 3)[..., 2].detach().cpu().numpy()
# depth = depth * float(scale)
# print(depth.mean())
depth_vis = depth2rgb(depth)
out_path = depth_dir / f"{int(frame.frame_id):06d}.png"
cv2.imwrite(str(out_path), (depth_vis * 255).astype("uint8"))
def semantic_depth_hook(label_hw: torch.Tensor, depth_hw: torch.Tensor, frame_id: int) -> None:
"""
User hook for downstream planning (disabled by default).
Parameters:
label_hw: (H, W) int64
Hard semantic label IDs on the MASt3R match grid. The label space is whatever your
segmentation network outputs (e.g., 0..C-1). If your semantics came in as an RGB mask,
this may be a 24-bit packed "label code".
depth_hw: (H, W) float32
Depth map on the same grid as `label_hw`. Depending on `--depth_source` this can be:
- raw_z : per-frame pointmap Z (fast, can jitter)
- kf_warp_z : keyframe-warp Z + fast hole fill (more stable)
frame_id: int
Current frame index.
Notes:
- This function is intentionally a no-op. To integrate your planner, either:
(a) replace its body, or
(b) uncomment the single call site in the main loop.
- Keeping the hook out of the SLAM modules avoids unintended feedback loops.
"""
# No-op by design.
return
def _pose_matrix_from_json_entry(entry: dict, pose_key: str) -> np.ndarray:
if pose_key not in entry:
raise KeyError(f"pose key {pose_key!r} not found")
T = np.asarray(entry[pose_key], dtype=np.float64)
if T.shape != (4, 4):
raise ValueError(f"pose matrix has shape {T.shape}, expected (4, 4)")
if abs(float(T[3, 3])) > 1e-12 and abs(float(T[3, 3]) - 1.0) > 1e-9:
T = T / float(T[3, 3])
return T
def _rotation_matrix_to_quat_xyzw(R: np.ndarray) -> np.ndarray:
# Project slightly non-orthogonal pose-json rotations back to SO(3).
U, _, Vt = np.linalg.svd(R)
R = U @ Vt
if np.linalg.det(R) < 0:
U[:, -1] *= -1.0
R = U @ Vt
tr = float(np.trace(R))
if tr > 0.0:
s = np.sqrt(tr + 1.0) * 2.0
qw = 0.25 * s
qx = (R[2, 1] - R[1, 2]) / s
qy = (R[0, 2] - R[2, 0]) / s
qz = (R[1, 0] - R[0, 1]) / s
elif R[0, 0] > R[1, 1] and R[0, 0] > R[2, 2]:
s = np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2]) * 2.0
qw = (R[2, 1] - R[1, 2]) / s
qx = 0.25 * s
qy = (R[0, 1] + R[1, 0]) / s
qz = (R[0, 2] + R[2, 0]) / s
elif R[1, 1] > R[2, 2]:
s = np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2]) * 2.0
qw = (R[0, 2] - R[2, 0]) / s
qx = (R[0, 1] + R[1, 0]) / s
qy = 0.25 * s
qz = (R[1, 2] + R[2, 1]) / s
else:
s = np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1]) * 2.0
qw = (R[1, 0] - R[0, 1]) / s
qx = (R[0, 2] + R[2, 0]) / s
qy = (R[1, 2] + R[2, 1]) / s
qz = 0.25 * s
q = np.asarray([qx, qy, qz, qw], dtype=np.float64)
q /= max(float(np.linalg.norm(q)), 1e-12)
if q[3] < 0.0:
q = -q
return q
def _load_external_tracking_pose_override(args):
pose_json = str(getattr(args, "external_tracking_pose_json", "") or "")
if not pose_json:
return None
pose_path = pathlib.Path(pose_json)
table = json.loads(pose_path.read_text())
pose_key = str(args.external_tracking_pose_key)
frame_pattern = str(args.external_tracking_pose_frame_pattern)
frame_stride = int(args.external_tracking_pose_frame_stride)
coord_space = str(args.external_tracking_pose_space)
first_key = frame_pattern.format(frame_id=0)
if first_key not in table:
candidates = [k for k, v in table.items() if isinstance(v, dict) and pose_key in v]
if not candidates:
raise ValueError(f"No pose entries with key {pose_key!r} in {pose_path}")
first_key = sorted(candidates)[0]
T0 = _pose_matrix_from_json_entry(table[first_key], pose_key)
print(
"[ExternalTrackingPose] overriding frame.T_WC from"
f" {pose_path} key={pose_key} stride={frame_stride}"
f" space={coord_space} anchor={first_key}"
)
return {
"table": table,
"pose_key": pose_key,
"frame_pattern": frame_pattern,
"frame_stride": frame_stride,
"coord_space": coord_space,
"T0_inv": np.linalg.inv(T0),
}
def _external_tracking_sim3_for_frame(pose_override, frame_id: int, device, dtype):
src_frame_id = int(frame_id) * int(pose_override["frame_stride"])
pose_key = pose_override["pose_key"]
frame_key = pose_override["frame_pattern"].format(frame_id=src_frame_id)
table = pose_override["table"]
if frame_key not in table:
raise KeyError(f"external tracking pose missing frame key {frame_key!r}")
T = _pose_matrix_from_json_entry(table[frame_key], pose_key)
if pose_override["coord_space"] == "first-frame":
T = pose_override["T0_inv"] @ T
elif pose_override["coord_space"] != "aligned-world":
raise ValueError(f"Unsupported external tracking pose space {pose_override['coord_space']!r}")
qx, qy, qz, qw = _rotation_matrix_to_quat_xyzw(T[:3, :3])
tx, ty, tz = T[:3, 3].astype(np.float64).tolist()
data = torch.tensor(
[[tx, ty, tz, qx, qy, qz, qw, 1.0]],
device=device,
dtype=dtype,
)
return lietorch.Sim3(data)
def min_depth_per_class(
label_hw: torch.Tensor, depth_hw: torch.Tensor, valid_hw: torch.Tensor, num_classes: int
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Compute per-class minimum depth (and counts) using a fast scatter-reduction.
Inputs:
label_hw: (H, W) int64
depth_hw: (H, W) float32
valid_hw: (H, W) bool
num_classes: int
Outputs:
min_depth: (num_classes,) float32 (inf if a class has no valid pixels)
count: (num_classes,) int64
Performance:
- O(H*W) without sorting/unique.
- Requires label IDs to be in [0, num_classes).
"""
labels = label_hw.reshape(-1).to(torch.int64)
depth = depth_hw.reshape(-1).to(torch.float32)
valid = valid_hw.reshape(-1).to(torch.bool) & torch.isfinite(depth) & (depth > 0.0)
if labels.numel() == 0:
return (
torch.full((num_classes,), float("inf"), device=label_hw.device, dtype=torch.float32),
torch.zeros((num_classes,), device=label_hw.device, dtype=torch.int64),
)
# Filter to labels in range.
in_range = (labels >= 0) & (labels < int(num_classes))
valid = valid & in_range
if not valid.any():
return (
torch.full((num_classes,), float("inf"), device=label_hw.device, dtype=torch.float32),
torch.zeros((num_classes,), device=label_hw.device, dtype=torch.int64),
)
labels_v = labels[valid]
depth_v = depth[valid]
# Count per class.
count = torch.bincount(labels_v, minlength=int(num_classes)).to(torch.int64)
# Min depth per class via scatter_reduce.
min_depth = torch.full(
(int(num_classes),), float("inf"), device=label_hw.device, dtype=torch.float32
)
# `scatter_reduce_` is available in PyTorch 2.0+.
min_depth.scatter_reduce_(
0, labels_v, depth_v, reduce="amin", include_self=True
)
return min_depth, count
def min_depth_and_argmin_per_class(
label_hw: torch.Tensor,
depth_hw: torch.Tensor,
valid_hw: torch.Tensor,
num_classes: int,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""
Compute per-class minimum depth AND the pixel location that attains that minimum.
Inputs:
label_hw: (H, W) int64
depth_hw: (H, W) float32
valid_hw: (H, W) bool
num_classes: int
Outputs:
min_depth: (C,) float32 (inf if class absent)
argmin_idx: (C,) int64 (linear index in [0, H*W), or -1 if class absent)
count: (C,) int64 (#valid pixels per class)
Why this is designed this way:
- For debug/planning, we often want to *visualize* which pixel produced the minimum depth.
- We avoid Python loops over classes by using scatter reductions.
Implementation details:
- First compute `min_depth[c]` via scatter_reduce(amin).
- Then find pixels that match this min (depth == min_depth[label]) and scatter_reduce(amin)
over their linear indices to pick a deterministic argmin per class.
"""
h, w = (int(label_hw.shape[0]), int(label_hw.shape[1]))
n = h * w
labels = label_hw.reshape(-1).to(torch.int64)
depth = depth_hw.reshape(-1).to(torch.float32)
valid = valid_hw.reshape(-1).to(torch.bool) & torch.isfinite(depth) & (depth > 0.0)
# Filter to labels in range.
in_range = (labels >= 0) & (labels < int(num_classes))
valid = valid & in_range
min_depth = torch.full((int(num_classes),), float("inf"), device=label_hw.device, dtype=torch.float32)
count = torch.zeros((int(num_classes),), device=label_hw.device, dtype=torch.int64)
argmin_idx = torch.full((int(num_classes),), -1, device=label_hw.device, dtype=torch.int64)
if not valid.any():
return min_depth, argmin_idx, count
labels_v = labels[valid]
depth_v = depth[valid]
idx_v = torch.arange(n, device=label_hw.device, dtype=torch.int64)[valid]
count = torch.bincount(labels_v, minlength=int(num_classes)).to(torch.int64)
min_depth.scatter_reduce_(0, labels_v, depth_v, reduce="amin", include_self=True)
# Identify which valid pixels attain the per-class minimum.
min_for_pixel = min_depth[labels_v]
is_min = depth_v == min_for_pixel
if is_min.any():
labels_min = labels_v[is_min]
idx_min = idx_v[is_min]
# Scatter-reduce the smallest linear index among min-attaining pixels.
argmin_tmp = torch.full((int(num_classes),), n + 1, device=label_hw.device, dtype=torch.int64)
argmin_tmp.scatter_reduce_(0, labels_min, idx_min, reduce="amin", include_self=True)
argmin_idx = torch.where(count > 0, argmin_tmp, torch.full_like(argmin_tmp, -1))
# For safety: any still-large entries (should not happen) are treated as "absent".
argmin_idx = torch.where(argmin_idx <= n, argmin_idx, torch.full_like(argmin_idx, -1))
return min_depth, argmin_idx, count
def relocalization(frame, keyframes, factor_graph, retrieval_database):
# we are adding and then removing from the keyframe, so we need to be careful.
# The lock slows viz down but safer this way...
with keyframes.lock:
kf_idx = []
retrieval_inds = retrieval_database.update(
frame,
add_after_query=False,
k=config["retrieval"]["k"],
min_thresh=config["retrieval"]["min_thresh"],
)
kf_idx += retrieval_inds
successful_loop_closure = False
if kf_idx:
keyframes.append(frame)
n_kf = len(keyframes)
kf_idx = list(kf_idx) # convert to list
frame_idx = [n_kf - 1] * len(kf_idx)
print("RELOCALIZING against kf ", n_kf - 1, " and ", kf_idx)
if factor_graph.add_factors(
frame_idx,
kf_idx,
config["reloc"]["min_match_frac"],
is_reloc=config["reloc"]["strict"],
):
retrieval_database.update(
frame,
add_after_query=True,
k=config["retrieval"]["k"],
min_thresh=config["retrieval"]["min_thresh"],
)
print("Success! Relocalized")
successful_loop_closure = True
keyframes.T_WC[n_kf - 1] = keyframes.T_WC[kf_idx[0]].clone()
else:
keyframes.pop_last()
print("Failed to relocalize")
if successful_loop_closure:
if config["use_calib"]:
factor_graph.solve_GN_calib()
else:
factor_graph.solve_GN_rays()
return successful_loop_closure
def run_backend(cfg, model, states, keyframes, K):
set_global_config(cfg)
device = keyframes.device
factor_graph = FactorGraph(model, keyframes, K, device)
retrieval_database = load_retriever(model)
mode = states.get_mode()
while mode is not Mode.TERMINATED:
mode = states.get_mode()
if mode == Mode.INIT or states.is_paused():
time.sleep(0.01)
continue
if mode == Mode.RELOC:
frame = states.get_frame()
success = relocalization(frame, keyframes, factor_graph, retrieval_database)
if success:
states.set_mode(Mode.TRACKING)
states.dequeue_reloc()
continue
idx = -1
with states.lock:
if len(states.global_optimizer_tasks) > 0:
idx = states.global_optimizer_tasks[0]
if idx == -1:
time.sleep(0.01)
continue
# Graph Construction
kf_idx = []
# k to previous consecutive keyframes
n_consec = 1
for j in range(min(n_consec, idx)):
kf_idx.append(idx - 1 - j)
frame = keyframes[idx]
retrieval_inds = retrieval_database.update(
frame,
add_after_query=True,
k=config["retrieval"]["k"],
min_thresh=config["retrieval"]["min_thresh"],
)
kf_idx += retrieval_inds
lc_inds = set(retrieval_inds)
lc_inds.discard(idx - 1)
if len(lc_inds) > 0:
print("Database retrieval", idx, ": ", lc_inds)
kf_idx = set(kf_idx) # Remove duplicates by using set
kf_idx.discard(idx) # Remove current kf idx if included
kf_idx = list(kf_idx) # convert to list
frame_idx = [idx] * len(kf_idx)
if kf_idx:
if _timing is None:
factor_graph.add_factors(
kf_idx, frame_idx, config["local_opt"]["min_match_frac"]
)
else:
t0 = time.perf_counter()
factor_graph.add_factors(
kf_idx, frame_idx, config["local_opt"]["min_match_frac"]
)
torch.cuda.synchronize()
_timing.add_ms("backend_add_ms", (time.perf_counter() - t0) * 1000.0)
with states.lock:
states.edges_ii[:] = factor_graph.ii.cpu().tolist()
states.edges_jj[:] = factor_graph.jj.cpu().tolist()
if _timing is None:
if config["use_calib"]:
factor_graph.solve_GN_calib()
else:
factor_graph.solve_GN_rays()
else:
t0 = time.perf_counter()
if config["use_calib"]:
factor_graph.solve_GN_calib()
else:
factor_graph.solve_GN_rays()
torch.cuda.synchronize()
_timing.add_ms("backend_solve_ms", (time.perf_counter() - t0) * 1000.0)
with states.lock:
if len(states.global_optimizer_tasks) > 0:
idx = states.global_optimizer_tasks.pop(0)
if _timing is not None:
_timing.tick_frame()
_timing.maybe_print(int(idx))
if __name__ == "__main__":
mp.set_start_method("spawn")
torch.backends.cuda.matmul.allow_tf32 = True
torch.set_grad_enabled(False)
device = "cuda:0"
save_frames = False
datetime_now = str(datetime.datetime.now()).replace(" ", "_")
parser = argparse.ArgumentParser()
parser.add_argument("--dataset", default="datasets/tum/rgbd_dataset_freiburg1_desk")
parser.add_argument("--config", default="config/base.yaml")
parser.add_argument("--save-as", default="default")
parser.add_argument("--no-viz", action="store_true")
parser.add_argument("--calib", default="")
# -------------------------------------------------------------------------
# Input source selection (dataset vs. streaming)
#
# Motivation:
# For online deployment (e.g., AirSim), we want to run SLAM on a live RGB stream
# without blocking the main loop on streaming latency.
#
# Design:
# - "dataset": existing behavior (read images from dataset path).
# - "airsim": stream RGB frames asynchronously from AirSim (latest-only).
# -------------------------------------------------------------------------
parser.add_argument(
"--input_source",
choices=["dataset", "airsim", "limo_zmq"],
default="dataset",
help="Input source: dataset, airsim streaming, or limo_zmq streaming (default: dataset).",
)
parser.add_argument("--airsim_ip", default="127.0.0.1", help="AirSim IP (default: 127.0.0.1).")
parser.add_argument("--airsim_camera", default="0", help="AirSim camera name/index (default: 0).")
parser.add_argument("--airsim_vehicle_name", default="", help="AirSim vehicle name (default: empty).")
parser.add_argument("--airsim_target_fps", type=int, default=30, help="AirSim grab target FPS (default: 30).")
parser.add_argument(
"--airsim_image_order",
choices=["bgr", "rgb"],
default="bgr",
help=(
"Color order of AirSim `image_data_uint8` (default: rgb). "
"The rest of the code assumes RGB; if your AirSim build returns BGR, set this to 'bgr'."
),
)
# -------------------------------------------------------------------------
# LIMO ZMQ streaming (ros_zmq_bridge)
#
# Motivation:
# Some deployments stream camera images over ZMQ (e.g., a ROS bridge on the robot).
# This mirrors the AirSim "latest-only" design: SLAM must not block on network latency.
#
# Behavior:
# - Video: SUB receives JPEG bytes and decodes them to RGB uint8.
# - Cmd: (optional) joystick teleop publishes JSON {"v":..., "w":...} over PUB.
#
# IMPORTANT:
# - ZMQ and OpenCV are imported lazily only when `--input_source limo_zmq` is used.
# - Joystick teleop is also optional and only starts if `--enable_joystick` is set.
# -------------------------------------------------------------------------
parser.add_argument("--bridge_ip", type=str, default="127.0.0.1", help="IP of ros_zmq_bridge host (default: 127.0.0.1).")
parser.add_argument("--bridge_vid_port", type=int, default=5555, help="ZMQ video port (SUB) (default: 5555).")
parser.add_argument("--bridge_cmd_port", type=int, default=5556, help="ZMQ cmd port (PUB) (default: 5556).")
parser.add_argument(
"--enable_joystick",
action="store_true",
help="Enable joystick teleop -> ZMQ cmd publisher (only meaningful for --input_source limo_zmq).",
)
parser.add_argument("--max_linear", type=float, default=0.5, help="Max linear velocity for joystick teleop (m/s).")
parser.add_argument("--max_angular", type=float, default=1.0, help="Max angular velocity for joystick teleop (rad/s).")
parser.add_argument("--deadzone", type=float, default=0.05, help="Joystick deadzone (default: 0.05).")
parser.add_argument("--joystick_lin_axis", type=int, default=1, help="Joystick axis index for linear velocity (default: 1).")
parser.add_argument("--joystick_ang_axis", type=int, default=2, help="Joystick axis index for angular velocity (default: 2).")
parser.add_argument("--joystick_rate_hz", type=float, default=20.0, help="Joystick command publish rate (Hz).")
parser.add_argument(
"--stream_img_size",
type=int,
default=224,
help="Streaming resize/crop size fed into MASt3R resize_img (default: 224).",
)
parser.add_argument(
"--stream_sleep_ms",
type=int,
default=1,
help="When no new streaming frame is available, sleep this many ms (default: 1).",
)
parser.add_argument(
"--stream_max_frames",
type=int,
default=0,
help="Max frames to process in streaming mode; 0 means run until terminated (default: 0).",
)
# -------------------------------------------------------------------------
# Streaming segmentation controls (avoid slowing SLAM)
#
# Motivation:
# EfficientViT segmentation (even if fast) can still contend with SLAM on the GPU.
# To keep SLAM real-time, we can run segmentation asynchronously at a capped FPS and
# use the latest available labels without blocking tracking.
# -------------------------------------------------------------------------
parser.add_argument(
"--stream_async_semantic",
dest="stream_async_semantic",
action="store_true",
default=True,
help="Run streaming segmentation asynchronously (default: enabled).",
)
parser.add_argument(
"--stream_sync_semantic",
dest="stream_async_semantic",
action="store_false",
help="Run streaming segmentation synchronously (debug; may slow SLAM).",
)
parser.add_argument(
"--stream_semantic_fps",
type=int,
default=10,
help="Segmentation target FPS in streaming async mode (default: 10).",
)
# -------------------------------------------------------------------------
# EfficientViT head selection (ADE20K vs Cityscapes)
#
# Motivation:
# EfficientViT provides dataset-specific segmentation heads (different class counts).
# For ablation / deployment, users may want to switch between ADE20K and Cityscapes
# without editing code.
#
# Weight file convention used in this repo:
# - ADE20K : `efficientvit/l2.pt`
# - Cityscapes : `efficientvit/l2_cityscapes.pt`
# -------------------------------------------------------------------------
parser.add_argument(
"--efficientvit_dataset",
choices=["ade20k", "cityscapes"],
default="ade20k",
help="EfficientViT segmentation head/dataset (default: ade20k).",
)
# -------------------------------------------------------------------------
# Streaming debug visualization (optional)
#
# This spawns a separate process with an OpenCV window so GUI latency cannot
# block the SLAM main loop. It is purely for debugging.
# -------------------------------------------------------------------------
parser.add_argument(
"--enable_stream_debug_viz",
action="store_true",
default=False,
help="Show a debug window with semantic+depth overlays and sampled depth stats (default: disabled).",
)
parser.add_argument(
"--stream_viz_headless",
action="store_true",
default=False,
help=(
"Headless debug mode: do not open an OpenCV window, but keep publishing the latest "
"debug semantic/depth payload for other processes to attach to (default: disabled)."
),
)
parser.add_argument("--stream_viz_fps", type=int, default=10, help="Debug viz update FPS (default: 10).")
parser.add_argument(
"--stream_viz_topk",
type=int,
default=9,
help="Show top-K semantic classes by nearest depth in the debug window (default: 9).",
)
parser.add_argument("--stream_viz_alpha", type=float, default=0.6, help="Segmentation overlay alpha (default: 0.6).")
parser.add_argument("--stream_viz_scale", type=int, default=2, help="Debug viz scale factor (default: 2).")
parser.add_argument(
"--stream_viz_semantic_source",
choices=["stable", "raw"],
default="stable",
help=(
"Semantic source for the debug overlay (default: stable). "
"'stable' uses the post-warp stabilized semantic (if available), "
"'raw' uses the per-frame EfficientViT output."
),
)
parser.add_argument(
"--stream_viz_depth_source",
choices=["stable", "raw"],
default="stable",
help=(
"Depth source shown in the debug window (default: stable). "
"'stable' prefers keyframe-warp depth when available; "
"'raw' shows current-frame pointmap Z."
),
)
# -------------------------------------------------------------------------
# Stream debug viz: planning point cloud reprojection layout (optional).
#
# Motivation:
# We previously provided a separate "reader" script to visualize the planning point cloud.
# Users requested moving that visualization into the existing stream debug window so
# everything is visible in a single OpenCV window.
#
# IMPORTANT:
# - This is visualization-only.
# - It relies on the planning pointcloud publisher shared memory
# (typically enabled via --enable_planning_pointcloud_publish).
# -------------------------------------------------------------------------
parser.add_argument(
"--stream_viz_layout",
choices=["legacy", "planning_pointcloud"],
default="planning_pointcloud",
help=(
"Stream debug window layout (default: planning_pointcloud). "
"'legacy' shows the original semantic/depth 3-column view; "
"'planning_pointcloud' shows pointcloud reprojection triplet (+ optional panorama)."
),
)
parser.add_argument(
"--stream_viz_pc_outdir",
type=str,
default="",
help=(
"Outdir used to locate planning pointcloud shm_info.json for debug viz. "
"If empty, uses --planning_pointcloud_outdir (default: empty)."
),
)
parser.add_argument(
"--stream_viz_pc_info_filename",
type=str,
default="shm_info.json",
help="Planning pointcloud shared-memory info filename (default: shm_info.json).",
)
parser.add_argument(
"--stream_viz_pc_radius_m",
type=float,
default=2.0,
help="Panorama radius (meters) around current pose for pointcloud debug viz (default: 2.0).",
)
parser.add_argument(
"--stream_viz_pc_pano",
dest="stream_viz_pc_pano",
action="store_true",
default=True,
help="Enable the panorama (second row) in pointcloud debug viz (default: enabled).",
)
parser.add_argument(
"--stream_viz_pc_no_pano",
dest="stream_viz_pc_pano",
action="store_false",
help="Disable the panorama (second row) in pointcloud debug viz.",
)
parser.add_argument(
"--stream_viz_pc_pano_h",
type=int,
default=96,
help="Panorama height in pointcloud debug viz (default: 96).",
)
parser.add_argument(
"--stream_viz_pc_pano_vfov_deg",
type=float,
default=60.0,
help="Panorama vertical FOV in degrees (default: 60).",
)
parser.add_argument(
"--stream_viz_pc_pano_mode",
choices=["rgb", "sem", "depth"],
default="sem",
help="Panorama coloring mode in pointcloud debug viz (default: sem).",
)
parser.add_argument(
"--stream_viz_pc_fov_deg",
type=float,
default=60.0,
help="Assumed pinhole FOV in degrees for triplet reprojection (default: 60).",
)
parser.add_argument(
"--stream_viz_pc_max_points",
type=int,
default=10_000,
help=(
"Consumer-side max points for pointcloud reprojection (default: 10000). "
"Lower values reduce CPU load/latency in debug visualization."
),
)
parser.add_argument(
"--stream_viz_pc_esdf",
action="store_true",
default=False,
help="Enable local 3D ESDF/occupancy visualization from the planning pointcloud (default: disabled).",
)
parser.add_argument(
"--stream_viz_pc_esdf_radius",
type=float,
default=2.0,
help="Local ESDF cube half-size in meters (default: 2.0).",
)
parser.add_argument(
"--stream_viz_pc_esdf_voxel",
type=float,
default=0.1,
help="ESDF voxel size in meters (default: 0.1).",
)
parser.add_argument(
"--stream_viz_pc_esdf_use_semantic",
action="store_true",
default=False,
help="If set, only obstacle labels contribute to occupancy when computing ESDF.",
)
parser.add_argument(
"--stream_viz_pc_esdf_obstacle_labels",
type=str,
default="",
help="Space/comma-separated obstacle label IDs for ESDF. Empty means all labels are obstacles.",
)
# -------------------------------------------------------------------------
# Optional: Kalman smoothing for debug min-depth readouts (visualization only)
#
# Motivation:
# Even when depth is produced by kf-warp + hole-fill, per-class minimum depth can
# still fluctuate frame-to-frame due to:
# - argmin pixel hopping within a region
# - noisy/partial depth coverage
# - segmentation boundary jitter
#
# This filter MUST NOT affect SLAM. It only smooths the values shown in the debug window.
# -------------------------------------------------------------------------
parser.add_argument(
"--stream_viz_kalman",
action="store_true",
default=False,
help="Enable a simple 1D Kalman filter on displayed min-depth values (default: disabled).",
)
parser.add_argument(
"--stream_viz_kalman_q",
type=float,
default=1e-4,
help=(
"Kalman process noise Q for debug depth smoothing (default: 1e-4). "
"Smaller Q => stronger smoothing (slower to react)."
),
)
parser.add_argument(
"--stream_viz_kalman_r",
type=float,
default=0.20,
help=(
"Kalman measurement noise R for debug depth smoothing (default: 0.20). "
"Larger R => stronger smoothing (trust measurements less)."
),
)
# -------------------------------------------------------------------------
# Debug visualization depth filter mode (visualization only).
#
# Motivation:
# A per-pixel temporal filter can "break" when the camera moves because the same pixel
# no longer corresponds to the same scene point. To debug stability in both hover and motion,
# we provide three modes in the debug window:
# - none : show depth as-is
# - pixel : pixel-wise Kalman (strong smoothing, may smear under motion)
# - pose : pose-aware pixel Kalman (reset smoothing state when pose changes too much)
#
# IMPORTANT:
# This affects ONLY the debug window. It must NOT affect SLAM tracking/optimization.
# -------------------------------------------------------------------------
parser.add_argument(
"--stream_viz_filter_mode",
choices=["none", "pixel", "pose"],
default="none",
help=(
"Depth filtering mode in the debug window (default: none). "
"Use 'pixel' for per-pixel Kalman smoothing, or 'pose' for pose-aware smoothing."
),
)
parser.add_argument(
"--stream_viz_sample_grid",
type=int,
default=3,
help="Grid size for depth sampling in debug window (default: 3 => 3x3 samples).",
)
parser.add_argument(
"--stream_viz_sample_patch",
type=int,
default=3,
help="Patch size for each sampled depth statistic (default: 3 => 3x3 mean).",
)
parser.add_argument(
"--stream_viz_info_width",
type=int,
default=220,
help="Width (pixels) of the debug info panel (default: 220).",
)
parser.add_argument(
"--stream_viz_depth_vis_max",
type=float,
default=10.0,
help="Max depth (meters) for depth colormap visualization (default: 10.0).",
)
parser.add_argument(
"--stream_viz_pose_reset_trans",
type=float,
default=0.02,
help="Pose-aware filter reset translation threshold in meters (default: 0.02).",
)
parser.add_argument(
"--stream_viz_pose_reset_rot_deg",
type=float,
default=2.0,
help="Pose-aware filter reset rotation threshold in degrees (default: 2.0).",
)
# -------------------------------------------------------------------------
# Debug visualization: semantic smoothing (visualization only)
#
# Motivation:
# Even with stabilized semantics, the displayed segmentation can flicker due to:
# - per-frame network noise (raw)
# - incomplete warp coverage (stable)
# - noisy boundaries / small regions
#
# We provide an optional per-pixel hard-label EMA-like filter in the debug process.
# It stores only (label, weight) per pixel (no logits/probabilities) and is fast.
#
# IMPORTANT:
# This affects ONLY the debug window. It must NOT affect SLAM.
# -------------------------------------------------------------------------
parser.add_argument(
"--stream_viz_semantic_filter",
choices=["none", "ema"],