cloudanalyzer 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/PKG-INFO +5 -1
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/__init__.py +1 -1
- cloudanalyzer-0.4.0/ca/core/slam_run.py +329 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/process_docs.py +10 -0
- cloudanalyzer-0.4.0/ca/experiments/slam_run/__init__.py +46 -0
- cloudanalyzer-0.4.0/ca/experiments/slam_run/common.py +244 -0
- cloudanalyzer-0.4.0/ca/experiments/slam_run/evaluate.py +242 -0
- cloudanalyzer-0.4.0/ca/experiments/slam_run/identity_passthrough.py +90 -0
- cloudanalyzer-0.4.0/ca/experiments/slam_run/kiss_icp_driver.py +126 -0
- cloudanalyzer-0.4.0/ca/experiments/slam_run/kiss_slam_driver.py +175 -0
- cloudanalyzer-0.4.0/ca/experiments/slam_run/small_gicp_driver.py +179 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer.egg-info/PKG-INFO +5 -1
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer.egg-info/SOURCES.txt +9 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer.egg-info/requires.txt +5 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer_cli/main.py +210 -1
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/pyproject.toml +5 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_process_docs.py +3 -0
- cloudanalyzer-0.4.0/tests/test_slam_run.py +638 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/LICENSE +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/README.md +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/align.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/baseline_history.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/batch.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/benchmark.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/bundle.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/compare.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/convert.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/__init__.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/check_baseline_evolution.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/check_scaffolding.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/check_triage.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/checks.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/ground_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/map_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/web_progressive_loading.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/web_sampling.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/web_trajectory_sampling.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/crop.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/density_map.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/detection.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/diff.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/downsample.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/__init__.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_baseline_evolution/__init__.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_baseline_evolution/common.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_baseline_evolution/evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_baseline_evolution/pareto_promote.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_baseline_evolution/stability_window.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_baseline_evolution/threshold_guard.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_scaffolding/__init__.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_scaffolding/common.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_scaffolding/evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_scaffolding/literal_profiles.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_scaffolding/object_sections.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_scaffolding/pipeline_overlays.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_triage/__init__.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_triage/common.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_triage/evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_triage/pareto_frontier.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_triage/severity_weighted.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_triage/signature_cluster.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/ground_evaluate/__init__.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/ground_evaluate/common.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/ground_evaluate/evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/ground_evaluate/height_band.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/ground_evaluate/nearest_neighbor.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/ground_evaluate/voxel_confusion.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/map_evaluate/__init__.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/map_evaluate/common.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/map_evaluate/evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/map_evaluate/nn_thresholds.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/map_evaluate/voxel_entropy.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_progressive_loading/__init__.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_progressive_loading/common.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_progressive_loading/distance_shells.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_progressive_loading/evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_progressive_loading/grid_tiles.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_progressive_loading/spatial_shuffle.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_sampling/__init__.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_sampling/common.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_sampling/evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_sampling/functional_voxel.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_sampling/object_random.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_sampling/pipeline_hybrid.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_trajectory_sampling/__init__.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_trajectory_sampling/common.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_trajectory_sampling/distance_accumulator.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_trajectory_sampling/evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_trajectory_sampling/turn_aware.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_trajectory_sampling/uniform_stride.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/filter.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/geometry.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/ground_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/history.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/info.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/io.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/kitti.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/log.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/loop_closure_report.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/merge.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/metrics.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/mme.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/normals.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/object_eval.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/pareto.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/pipeline.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/plot.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/point_summary.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/posegraph.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/pr_comment.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/registration.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/report.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/run_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/sample.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/scan_match_debug.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/slam_debug.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/split.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/stats.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/tracking.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/trajectory.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/view.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/visualization.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/web.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer.egg-info/dependency_links.txt +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer.egg-info/entry_points.txt +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer.egg-info/top_level.txt +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer_cli/__init__.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/setup.cfg +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/setup.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_align.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_baseline_history.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_batch.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_benchmark.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_bundle.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_check_baseline_evolution_process.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_check_scaffolding_process.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_check_suite.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_check_triage_process.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_cli.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_compare.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_convert.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_crop.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_density_map.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_detection_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_detection_tracking_report.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_diff.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_downsample.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_filter.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_geometry.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_geometry_splat_ellipsoid.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_ground_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_ground_evaluate_process.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_history.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_info.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_io.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_kitti.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_logging.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_loop_closure_report.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_map_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_map_evaluate_core.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_merge.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_metrics.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_mme.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_normals.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_object_eval_iou.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_output_json.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_pareto.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_phase16_large_scale.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_phase17_vectorize.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_phase18_vectorize.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_pipeline.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_plot.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_posegraph.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_pr_comment.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_public_benchmark_pack.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_public_object_eval_examples.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_registration.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_report.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_run_batch.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_run_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_sample.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_scan_match_debug.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_slam_debug.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_split.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_stats.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_threshold.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_tracking_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_traj_batch.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_trajectory.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_visualization.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_progressive_loading_core.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_progressive_loading_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_progressive_loading_process.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_progressive_loading_strategies.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_sampling_core.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_sampling_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_sampling_process.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_sampling_strategies.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_trajectory_sampling_core.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_trajectory_sampling_evaluate.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_trajectory_sampling_process.py +0 -0
- {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_trajectory_sampling_strategies.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudanalyzer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: CLI-first QA toolkit for point cloud, trajectory, and 3D perception outputs
|
|
5
5
|
Author: rsasaki0109
|
|
6
6
|
License-Expression: MIT
|
|
@@ -36,6 +36,10 @@ Requires-Dist: mypy; extra == "dev"
|
|
|
36
36
|
Requires-Dist: pkginfo>=1.12.0; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest; extra == "dev"
|
|
38
38
|
Requires-Dist: twine>=6.0.0; extra == "dev"
|
|
39
|
+
Provides-Extra: slam
|
|
40
|
+
Requires-Dist: kiss-icp>=1.2.0; extra == "slam"
|
|
41
|
+
Requires-Dist: kiss-slam>=0.0.2; extra == "slam"
|
|
42
|
+
Requires-Dist: small_gicp>=1.0.0; extra == "slam"
|
|
39
43
|
Dynamic: license-file
|
|
40
44
|
|
|
41
45
|
# CloudAnalyzer
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Stable contract for ``ca slam-run``: drive a LiDAR-odometry pipeline
|
|
2
|
+
end-to-end on a sequence of input scans and produce the artifacts the rest of
|
|
3
|
+
CloudAnalyzer's evaluation stack expects (estimated map PLY + TUM trajectory).
|
|
4
|
+
|
|
5
|
+
The slice has two implementations under ``ca.experiments.slam_run``:
|
|
6
|
+
|
|
7
|
+
- ``KissICPSlamDriver`` (adopted, also re-exported here): wraps the
|
|
8
|
+
``kiss-icp`` package — a small, well-known scan-to-map LiDAR-odometry
|
|
9
|
+
registration pipeline. Picks up KITTI / Newer College sequences with
|
|
10
|
+
``deskew`` enabled.
|
|
11
|
+
- ``IdentityPassthroughSlamDriver``: a zero-motion sentinel that emits identity
|
|
12
|
+
poses and concatenates input frames as the "map". It is intentionally
|
|
13
|
+
bad — its only job is to prove the harness is wired and to provide a floor
|
|
14
|
+
for the slice's evaluator.
|
|
15
|
+
|
|
16
|
+
The contract:
|
|
17
|
+
|
|
18
|
+
- :class:`SlamRunRequest` carries the inputs you actually have (frame paths,
|
|
19
|
+
optional per-frame timestamps, driver knobs).
|
|
20
|
+
- :class:`SlamRunResult` carries everything needed to feed the existing
|
|
21
|
+
``ca run-evaluate`` pipeline plus enough metadata for ``ca check`` /
|
|
22
|
+
``ca report-pr-comment``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any, Protocol, runtime_checkable
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(slots=True)
|
|
35
|
+
class SlamRunRequest:
|
|
36
|
+
"""Inputs to a single SLAM driver run."""
|
|
37
|
+
|
|
38
|
+
frame_paths: tuple[Path, ...]
|
|
39
|
+
"""Per-frame scan paths in temporal order (``.pcd`` / ``.ply`` / ``.bin``)."""
|
|
40
|
+
|
|
41
|
+
timestamps_s: tuple[float, ...] | None = None
|
|
42
|
+
"""Optional per-frame timestamps in seconds (length must equal frame_paths).
|
|
43
|
+
Used for the TUM trajectory output and for drivers that consume timing.
|
|
44
|
+
If omitted, drivers fall back to ``index * frame_period_s``."""
|
|
45
|
+
|
|
46
|
+
frame_period_s: float = 0.1
|
|
47
|
+
"""Fallback timestamp spacing used when ``timestamps_s`` is None."""
|
|
48
|
+
|
|
49
|
+
max_range_m: float | None = None
|
|
50
|
+
"""Drop scan points farther than this from the sensor before registration.
|
|
51
|
+
``None`` keeps every point."""
|
|
52
|
+
|
|
53
|
+
voxel_size_m: float | None = None
|
|
54
|
+
"""Driver-side voxel grid size for the local map. ``None`` uses the driver's
|
|
55
|
+
own default."""
|
|
56
|
+
|
|
57
|
+
deskew: bool = True
|
|
58
|
+
"""Whether the driver should motion-deskew input frames (KISS-ICP default)."""
|
|
59
|
+
|
|
60
|
+
max_frames: int | None = None
|
|
61
|
+
"""Optional cap on the number of frames consumed. ``None`` runs everything."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(slots=True)
|
|
65
|
+
class SlamRunResult:
|
|
66
|
+
"""Outputs of a single SLAM driver run."""
|
|
67
|
+
|
|
68
|
+
driver: str
|
|
69
|
+
"""Identifier of the driver that produced this result."""
|
|
70
|
+
|
|
71
|
+
poses: np.ndarray
|
|
72
|
+
"""``(N, 4, 4)`` array of sensor-to-world poses, one per processed frame."""
|
|
73
|
+
|
|
74
|
+
timestamps_s: np.ndarray
|
|
75
|
+
"""``(N,)`` array of pose timestamps in seconds."""
|
|
76
|
+
|
|
77
|
+
map_points: np.ndarray
|
|
78
|
+
"""``(M, 3)`` accumulated map point cloud in the world frame."""
|
|
79
|
+
|
|
80
|
+
runtime_s: float
|
|
81
|
+
"""Total wall-clock seconds spent inside the driver (frame iteration only)."""
|
|
82
|
+
|
|
83
|
+
frames_processed: int
|
|
84
|
+
"""How many frames the driver consumed before producing the result."""
|
|
85
|
+
|
|
86
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
87
|
+
"""Driver-specific knobs / config snapshot. Echoed into the summary JSON
|
|
88
|
+
so a downstream baseline can prove two runs used the same settings."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@runtime_checkable
|
|
92
|
+
class SlamRunDriver(Protocol):
|
|
93
|
+
"""A SLAM driver consumes a request and returns the artifacts contract."""
|
|
94
|
+
|
|
95
|
+
name: str
|
|
96
|
+
|
|
97
|
+
def run(self, request: SlamRunRequest) -> SlamRunResult: # pragma: no cover - Protocol
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# Shared helpers used by every driver (loading frames, writing artifacts).
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def load_frame(path: Path) -> np.ndarray:
|
|
107
|
+
"""Load one LiDAR scan as an ``(N, 3)`` float64 ndarray.
|
|
108
|
+
|
|
109
|
+
Supports:
|
|
110
|
+
|
|
111
|
+
- ``.bin``: KITTI Velodyne format (float32 quadruples, XYZI; intensity dropped).
|
|
112
|
+
- ``.pcd`` / ``.ply``: read through Open3D.
|
|
113
|
+
- ``.csv``: x,y,z columns (optionally with a header), parsed by ``ca.io``.
|
|
114
|
+
|
|
115
|
+
Empty scans are returned as ``(0, 3)`` so drivers can skip them without
|
|
116
|
+
raising.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
suffix = path.suffix.lower()
|
|
120
|
+
if suffix == ".bin":
|
|
121
|
+
raw = np.fromfile(str(path), dtype=np.float32)
|
|
122
|
+
if raw.size == 0:
|
|
123
|
+
return np.empty((0, 3), dtype=np.float64)
|
|
124
|
+
if raw.size % 4 != 0:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"KITTI .bin frame {path} has size {raw.size} not divisible by 4"
|
|
127
|
+
)
|
|
128
|
+
return raw.reshape(-1, 4)[:, :3].astype(np.float64, copy=False)
|
|
129
|
+
if suffix in {".pcd", ".ply", ".csv"}:
|
|
130
|
+
from ca.io import load_point_cloud
|
|
131
|
+
|
|
132
|
+
pcd = load_point_cloud(str(path))
|
|
133
|
+
pts = np.asarray(pcd.points, dtype=np.float64)
|
|
134
|
+
if pts.size == 0:
|
|
135
|
+
return np.empty((0, 3), dtype=np.float64)
|
|
136
|
+
return pts
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"Unsupported frame format '{suffix}'. Expected one of .bin .pcd .ply .csv"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def discover_frame_paths(input_path: Path) -> list[Path]:
|
|
143
|
+
"""Resolve ``--input`` to an ordered list of frame paths.
|
|
144
|
+
|
|
145
|
+
Accepts either:
|
|
146
|
+
|
|
147
|
+
- A directory: pick up ``*.bin``, ``*.pcd``, ``*.ply`` (in that priority
|
|
148
|
+
order, picking whichever extension dominates) and sort lexicographically.
|
|
149
|
+
KITTI Velodyne dumps use lex-sortable zero-padded filenames so this
|
|
150
|
+
matches frame order.
|
|
151
|
+
- A list file (``*.txt``): one path per line. Relative paths are resolved
|
|
152
|
+
against the list file's parent directory.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
if input_path.is_file() and input_path.suffix.lower() == ".txt":
|
|
156
|
+
out: list[Path] = []
|
|
157
|
+
base = input_path.parent
|
|
158
|
+
for raw in input_path.read_text(encoding="utf-8").splitlines():
|
|
159
|
+
stripped = raw.strip()
|
|
160
|
+
if not stripped or stripped.startswith("#"):
|
|
161
|
+
continue
|
|
162
|
+
candidate = Path(stripped)
|
|
163
|
+
if not candidate.is_absolute():
|
|
164
|
+
candidate = base / candidate
|
|
165
|
+
out.append(candidate)
|
|
166
|
+
if not out:
|
|
167
|
+
raise ValueError(f"Frame list {input_path} is empty")
|
|
168
|
+
return out
|
|
169
|
+
|
|
170
|
+
if not input_path.is_dir():
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"--input must be a directory of scans or a .txt list; got {input_path}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
for ext in (".bin", ".pcd", ".ply"):
|
|
176
|
+
hits = sorted(input_path.glob(f"*{ext}"))
|
|
177
|
+
if hits:
|
|
178
|
+
return hits
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"No .bin/.pcd/.ply scans found under {input_path}. "
|
|
181
|
+
"Pass a directory containing LiDAR sweeps or a frames-list.txt."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def write_tum_trajectory(
|
|
186
|
+
path: Path, poses: np.ndarray, timestamps_s: np.ndarray
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Dump poses to TUM format (``timestamp tx ty tz qx qy qz qw``).
|
|
189
|
+
|
|
190
|
+
Matches the format ``ca traj-evaluate`` already consumes.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
if poses.ndim != 3 or poses.shape[1:] != (4, 4):
|
|
194
|
+
raise ValueError(f"poses must be (N, 4, 4); got {poses.shape}")
|
|
195
|
+
if timestamps_s.shape != (poses.shape[0],):
|
|
196
|
+
raise ValueError(
|
|
197
|
+
f"timestamps_s {timestamps_s.shape} must match poses {poses.shape[0]}"
|
|
198
|
+
)
|
|
199
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
quats = _rotation_matrices_to_quaternions(poses[:, :3, :3])
|
|
201
|
+
with path.open("w", encoding="utf-8") as f:
|
|
202
|
+
f.write("# timestamp tx ty tz qx qy qz qw\n")
|
|
203
|
+
for t, pose, q in zip(timestamps_s, poses, quats):
|
|
204
|
+
tx, ty, tz = pose[:3, 3]
|
|
205
|
+
qx, qy, qz, qw = q
|
|
206
|
+
f.write(
|
|
207
|
+
f"{t:.6f} {tx:.6f} {ty:.6f} {tz:.6f} "
|
|
208
|
+
f"{qx:.6f} {qy:.6f} {qz:.6f} {qw:.6f}\n"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def write_map_ply(path: Path, map_points: np.ndarray) -> None:
|
|
213
|
+
"""Save the accumulated SLAM map as an Open3D-readable PLY."""
|
|
214
|
+
|
|
215
|
+
if map_points.ndim != 2 or map_points.shape[1] != 3:
|
|
216
|
+
raise ValueError(f"map_points must be (M, 3); got {map_points.shape}")
|
|
217
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
218
|
+
import open3d as o3d
|
|
219
|
+
|
|
220
|
+
pcd = o3d.geometry.PointCloud()
|
|
221
|
+
pcd.points = o3d.utility.Vector3dVector(
|
|
222
|
+
np.ascontiguousarray(map_points, dtype=np.float64)
|
|
223
|
+
)
|
|
224
|
+
o3d.io.write_point_cloud(str(path), pcd, write_ascii=False)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _rotation_matrices_to_quaternions(rotmats: np.ndarray) -> np.ndarray:
|
|
228
|
+
"""Vectorized rotation-matrix → ``(qx, qy, qz, qw)`` quaternion.
|
|
229
|
+
|
|
230
|
+
Uses the standard Shepperd-style branchless trace selection so it works
|
|
231
|
+
cleanly even when most rotations are near-identity (the common case for
|
|
232
|
+
indoor LiDAR sweeps where motion is small per frame).
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
if rotmats.ndim != 3 or rotmats.shape[1:] != (3, 3):
|
|
236
|
+
raise ValueError(f"rotmats must be (N, 3, 3); got {rotmats.shape}")
|
|
237
|
+
|
|
238
|
+
n = rotmats.shape[0]
|
|
239
|
+
out = np.zeros((n, 4), dtype=np.float64)
|
|
240
|
+
|
|
241
|
+
m00 = rotmats[:, 0, 0]
|
|
242
|
+
m11 = rotmats[:, 1, 1]
|
|
243
|
+
m22 = rotmats[:, 2, 2]
|
|
244
|
+
trace = m00 + m11 + m22
|
|
245
|
+
|
|
246
|
+
# Branch on the largest diagonal entry to avoid numerical blowups.
|
|
247
|
+
case_w = trace > 0.0
|
|
248
|
+
case_x = (~case_w) & (m00 >= m11) & (m00 >= m22)
|
|
249
|
+
case_y = (~case_w) & (~case_x) & (m11 >= m22)
|
|
250
|
+
case_z = (~case_w) & (~case_x) & (~case_y)
|
|
251
|
+
|
|
252
|
+
if np.any(case_w):
|
|
253
|
+
idx = np.where(case_w)[0]
|
|
254
|
+
s = np.sqrt(trace[idx] + 1.0) * 2.0
|
|
255
|
+
qw = 0.25 * s
|
|
256
|
+
qx = (rotmats[idx, 2, 1] - rotmats[idx, 1, 2]) / s
|
|
257
|
+
qy = (rotmats[idx, 0, 2] - rotmats[idx, 2, 0]) / s
|
|
258
|
+
qz = (rotmats[idx, 1, 0] - rotmats[idx, 0, 1]) / s
|
|
259
|
+
out[idx, 0] = qx
|
|
260
|
+
out[idx, 1] = qy
|
|
261
|
+
out[idx, 2] = qz
|
|
262
|
+
out[idx, 3] = qw
|
|
263
|
+
|
|
264
|
+
if np.any(case_x):
|
|
265
|
+
idx = np.where(case_x)[0]
|
|
266
|
+
s = np.sqrt(1.0 + m00[idx] - m11[idx] - m22[idx]) * 2.0
|
|
267
|
+
out[idx, 0] = 0.25 * s
|
|
268
|
+
out[idx, 1] = (rotmats[idx, 0, 1] + rotmats[idx, 1, 0]) / s
|
|
269
|
+
out[idx, 2] = (rotmats[idx, 0, 2] + rotmats[idx, 2, 0]) / s
|
|
270
|
+
out[idx, 3] = (rotmats[idx, 2, 1] - rotmats[idx, 1, 2]) / s
|
|
271
|
+
|
|
272
|
+
if np.any(case_y):
|
|
273
|
+
idx = np.where(case_y)[0]
|
|
274
|
+
s = np.sqrt(1.0 + m11[idx] - m00[idx] - m22[idx]) * 2.0
|
|
275
|
+
out[idx, 0] = (rotmats[idx, 0, 1] + rotmats[idx, 1, 0]) / s
|
|
276
|
+
out[idx, 1] = 0.25 * s
|
|
277
|
+
out[idx, 2] = (rotmats[idx, 1, 2] + rotmats[idx, 2, 1]) / s
|
|
278
|
+
out[idx, 3] = (rotmats[idx, 0, 2] - rotmats[idx, 2, 0]) / s
|
|
279
|
+
|
|
280
|
+
if np.any(case_z):
|
|
281
|
+
idx = np.where(case_z)[0]
|
|
282
|
+
s = np.sqrt(1.0 + m22[idx] - m00[idx] - m11[idx]) * 2.0
|
|
283
|
+
out[idx, 0] = (rotmats[idx, 0, 2] + rotmats[idx, 2, 0]) / s
|
|
284
|
+
out[idx, 1] = (rotmats[idx, 1, 2] + rotmats[idx, 2, 1]) / s
|
|
285
|
+
out[idx, 2] = 0.25 * s
|
|
286
|
+
out[idx, 3] = (rotmats[idx, 1, 0] - rotmats[idx, 0, 1]) / s
|
|
287
|
+
|
|
288
|
+
return out
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# Adopted driver re-export. Concrete impl lives under ca.experiments.slam_run
|
|
293
|
+
# so the slice's evaluator can still benchmark it against the sentinel; the
|
|
294
|
+
# CLI imports from here so ``ca.core`` stays the only stable surface.
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _import_kiss_icp_driver() -> type[SlamRunDriver]:
|
|
299
|
+
from ca.experiments.slam_run.kiss_icp_driver import KissICPSlamDriver
|
|
300
|
+
|
|
301
|
+
return KissICPSlamDriver
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def make_default_driver() -> SlamRunDriver:
|
|
305
|
+
"""Return the currently adopted SLAM driver instance (KISS-ICP)."""
|
|
306
|
+
|
|
307
|
+
cls = _import_kiss_icp_driver()
|
|
308
|
+
return cls()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def run_slam(request: SlamRunRequest, driver: SlamRunDriver | None = None) -> SlamRunResult:
|
|
312
|
+
"""Convenience entry point used by the CLI; delegates to the supplied
|
|
313
|
+
driver or to the adopted default."""
|
|
314
|
+
|
|
315
|
+
drv = driver or make_default_driver()
|
|
316
|
+
return drv.run(request)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
__all__ = [
|
|
320
|
+
"SlamRunRequest",
|
|
321
|
+
"SlamRunResult",
|
|
322
|
+
"SlamRunDriver",
|
|
323
|
+
"load_frame",
|
|
324
|
+
"discover_frame_paths",
|
|
325
|
+
"write_tum_trajectory",
|
|
326
|
+
"write_map_ply",
|
|
327
|
+
"make_default_driver",
|
|
328
|
+
"run_slam",
|
|
329
|
+
]
|
|
@@ -54,6 +54,12 @@ from ca.experiments.map_evaluate.evaluate import (
|
|
|
54
54
|
render_interface_section as render_map_evaluate_interface_section,
|
|
55
55
|
run_map_evaluate_experiment,
|
|
56
56
|
)
|
|
57
|
+
from ca.experiments.slam_run.evaluate import (
|
|
58
|
+
render_decision_section as render_slam_run_decision_section,
|
|
59
|
+
render_experiment_section as render_slam_run_experiment_section,
|
|
60
|
+
render_interface_section as render_slam_run_interface_section,
|
|
61
|
+
run_slam_run_experiment,
|
|
62
|
+
)
|
|
57
63
|
|
|
58
64
|
|
|
59
65
|
def build_experiment_reports(repetitions: int = 3) -> list[dict]:
|
|
@@ -68,6 +74,7 @@ def build_experiment_reports(repetitions: int = 3) -> list[dict]:
|
|
|
68
74
|
run_check_baseline_evolution_experiment(repetitions=repetitions),
|
|
69
75
|
run_ground_evaluate_experiment(repetitions=repetitions),
|
|
70
76
|
run_map_evaluate_experiment(repetitions=repetitions),
|
|
77
|
+
run_slam_run_experiment(repetitions=1),
|
|
71
78
|
]
|
|
72
79
|
|
|
73
80
|
|
|
@@ -83,6 +90,7 @@ def render_experiments_markdown(reports: list[dict]) -> str:
|
|
|
83
90
|
render_check_baseline_evolution_experiment_section(reports[5]),
|
|
84
91
|
render_ground_evaluate_experiment_section(reports[6]),
|
|
85
92
|
render_map_evaluate_experiment_section(reports[7]),
|
|
93
|
+
render_slam_run_experiment_section(reports[8]),
|
|
86
94
|
]
|
|
87
95
|
return (
|
|
88
96
|
"# Experiments\n\n"
|
|
@@ -104,6 +112,7 @@ def render_decisions_markdown(reports: list[dict]) -> str:
|
|
|
104
112
|
render_check_baseline_evolution_decision_section(reports[5]),
|
|
105
113
|
render_ground_evaluate_decision_section(reports[6]),
|
|
106
114
|
render_map_evaluate_decision_section(reports[7]),
|
|
115
|
+
render_slam_run_decision_section(reports[8]),
|
|
107
116
|
]
|
|
108
117
|
return "# Decisions\n\n" + "\n\n".join(sections) + "\n"
|
|
109
118
|
|
|
@@ -120,6 +129,7 @@ def render_interfaces_markdown(reports: list[dict]) -> str:
|
|
|
120
129
|
render_check_baseline_evolution_interface_section(reports[5]),
|
|
121
130
|
render_ground_evaluate_interface_section(reports[6]),
|
|
122
131
|
render_map_evaluate_interface_section(reports[7]),
|
|
132
|
+
render_slam_run_interface_section(reports[8]),
|
|
123
133
|
]
|
|
124
134
|
return (
|
|
125
135
|
"# Interfaces\n\n"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Experimental SLAM drivers compared by the slam_run slice evaluator.
|
|
2
|
+
|
|
3
|
+
Four drivers participate:
|
|
4
|
+
|
|
5
|
+
- :class:`KissICPSlamDriver` — the adopted real driver, wrapping the
|
|
6
|
+
``kiss-icp`` package. Also re-exported from ``ca.core.slam_run`` so the
|
|
7
|
+
CLI keeps depending only on core. Scan-to-map with constant-velocity
|
|
8
|
+
prediction and adaptive max-correspondence-distance.
|
|
9
|
+
- :class:`KissSLAMSlamDriver` — wraps ``kiss-slam`` (KISS-ICP odometry +
|
|
10
|
+
pose-graph optimization + MapClosures loop closure). Same scan-to-map
|
|
11
|
+
inner loop as KISS-ICP with a PGO pass on top.
|
|
12
|
+
- :class:`SmallGICPSlamDriver` — wraps ``small_gicp`` (PyPI MIT).
|
|
13
|
+
Scan-to-map VGICP using ``small_gicp.GaussianVoxelMap`` as the
|
|
14
|
+
registration target; map output is scan-stitched + voxel-downsampled
|
|
15
|
+
(the voxel map stores quantized centers and can't clear the Chamfer
|
|
16
|
+
threshold against a dense reference on its own).
|
|
17
|
+
- :class:`IdentityPassthroughSlamDriver` — sentinel that returns identity
|
|
18
|
+
poses and concatenates input frames as the "map". It is intentionally
|
|
19
|
+
bad; its only job is to prove the harness runs and to set the floor for
|
|
20
|
+
``evaluate.py``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from ca.experiments.slam_run.identity_passthrough import IdentityPassthroughSlamDriver
|
|
24
|
+
from ca.experiments.slam_run.kiss_icp_driver import KissICPSlamDriver
|
|
25
|
+
from ca.experiments.slam_run.kiss_slam_driver import KissSLAMSlamDriver
|
|
26
|
+
from ca.experiments.slam_run.small_gicp_driver import SmallGICPSlamDriver
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_slam_run_drivers() -> list:
|
|
30
|
+
"""Return the concrete drivers compared by the slice evaluator."""
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
KissICPSlamDriver(),
|
|
34
|
+
KissSLAMSlamDriver(),
|
|
35
|
+
SmallGICPSlamDriver(),
|
|
36
|
+
IdentityPassthroughSlamDriver(),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"IdentityPassthroughSlamDriver",
|
|
42
|
+
"KissICPSlamDriver",
|
|
43
|
+
"KissSLAMSlamDriver",
|
|
44
|
+
"SmallGICPSlamDriver",
|
|
45
|
+
"get_slam_run_drivers",
|
|
46
|
+
]
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Shared datasets for slam_run driver experiments.
|
|
2
|
+
|
|
3
|
+
Each dataset case synthesizes a small set of LiDAR-style frames in a temp
|
|
4
|
+
directory, defines the ground-truth sensor poses, and packages them into a
|
|
5
|
+
:class:`SlamRunRequest`. The slice evaluator runs every concrete driver
|
|
6
|
+
against every case and compares against the GT trajectory.
|
|
7
|
+
|
|
8
|
+
The synthetic frames are tiny on purpose — the slice's job is to verify
|
|
9
|
+
that the harness wires through, not to benchmark large maps. Real-scale
|
|
10
|
+
benchmarking happens via ``ca slam-run`` + ``ca run-evaluate`` on the
|
|
11
|
+
KITTI-mini / Newer-College-mini benchmark suites.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import tempfile
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Callable
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from ca.core.slam_run import SlamRunRequest
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class SlamRunDatasetCase:
|
|
28
|
+
"""One comparable SLAM run for the slice's driver bake-off."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
description: str
|
|
32
|
+
build_request: Callable[[Path], SlamRunRequest]
|
|
33
|
+
"""Closure that materializes synthetic frames under the given temp dir
|
|
34
|
+
and returns the request pointing at them."""
|
|
35
|
+
|
|
36
|
+
gt_poses: np.ndarray
|
|
37
|
+
"""``(N, 4, 4)`` ground-truth sensor-to-world poses for ATE comparison."""
|
|
38
|
+
|
|
39
|
+
expected_kiss_icp_max_ate_m: float
|
|
40
|
+
"""Pass condition for KISS-ICP on this case."""
|
|
41
|
+
|
|
42
|
+
keep_files: tuple[str, ...] = field(default_factory=tuple)
|
|
43
|
+
"""Filenames the case promises to leave under the temp dir (helps tests
|
|
44
|
+
assert on driver outputs without re-running the synthesis)."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Synthetic frame builders
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _box_scene(seed: int, n_points: int = 1500) -> np.ndarray:
|
|
53
|
+
"""Build a deterministic synthetic 'room' scene in the world frame.
|
|
54
|
+
|
|
55
|
+
Returns ``(n_points, 3)`` of points lying on the floor and four walls of
|
|
56
|
+
a 20 × 20 m room — enough structure for ICP to actually have something
|
|
57
|
+
to register against.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
rng = np.random.default_rng(seed)
|
|
61
|
+
# Floor (z = 0)
|
|
62
|
+
floor = rng.uniform([-10, -10, -0.02], [10, 10, 0.02], size=(n_points // 2, 3))
|
|
63
|
+
# Four walls
|
|
64
|
+
rem = n_points - floor.shape[0]
|
|
65
|
+
per_wall = rem // 4
|
|
66
|
+
walls = []
|
|
67
|
+
for axis_pair in [(0, 1), (0, -1), (1, 1), (1, -1)]:
|
|
68
|
+
axis, sign = axis_pair
|
|
69
|
+
fixed = sign * 10.0
|
|
70
|
+
sweeping = rng.uniform(-10, 10, size=(per_wall, 1))
|
|
71
|
+
heights = rng.uniform(0.0, 3.0, size=(per_wall, 1))
|
|
72
|
+
if axis == 0:
|
|
73
|
+
pts = np.hstack(
|
|
74
|
+
[np.full((per_wall, 1), fixed), sweeping, heights]
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
pts = np.hstack(
|
|
78
|
+
[sweeping, np.full((per_wall, 1), fixed), heights]
|
|
79
|
+
)
|
|
80
|
+
pts += rng.normal(0, 0.02, size=pts.shape)
|
|
81
|
+
walls.append(pts)
|
|
82
|
+
return np.vstack([floor, *walls]).astype(np.float64)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _pose(tx: float, ty: float, tz: float, yaw_rad: float = 0.0) -> np.ndarray:
|
|
86
|
+
"""Build a 4×4 SE(3) pose with rotation about z (yaw) only."""
|
|
87
|
+
c, s = np.cos(yaw_rad), np.sin(yaw_rad)
|
|
88
|
+
pose = np.eye(4, dtype=np.float64)
|
|
89
|
+
pose[0, 0] = c
|
|
90
|
+
pose[0, 1] = -s
|
|
91
|
+
pose[1, 0] = s
|
|
92
|
+
pose[1, 1] = c
|
|
93
|
+
pose[0, 3] = tx
|
|
94
|
+
pose[1, 3] = ty
|
|
95
|
+
pose[2, 3] = tz
|
|
96
|
+
return pose
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _write_pcd(path: Path, points: np.ndarray) -> None:
|
|
100
|
+
"""Minimal ASCII PCD writer suitable for Open3D's reader."""
|
|
101
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
n = points.shape[0]
|
|
103
|
+
header = (
|
|
104
|
+
"# .PCD v.7 - Point Cloud Data file format\n"
|
|
105
|
+
"VERSION .7\n"
|
|
106
|
+
"FIELDS x y z\n"
|
|
107
|
+
"SIZE 4 4 4\n"
|
|
108
|
+
"TYPE F F F\n"
|
|
109
|
+
"COUNT 1 1 1\n"
|
|
110
|
+
f"WIDTH {n}\n"
|
|
111
|
+
"HEIGHT 1\n"
|
|
112
|
+
"VIEWPOINT 0 0 0 1 0 0 0\n"
|
|
113
|
+
f"POINTS {n}\n"
|
|
114
|
+
"DATA ascii\n"
|
|
115
|
+
)
|
|
116
|
+
with path.open("w", encoding="utf-8") as f:
|
|
117
|
+
f.write(header)
|
|
118
|
+
for x, y, z in points:
|
|
119
|
+
f.write(f"{x:.6f} {y:.6f} {z:.6f}\n")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _straight_line_dataset() -> SlamRunDatasetCase:
|
|
123
|
+
"""6 sensor poses sliding forward along +x by 0.5 m / frame in a static room."""
|
|
124
|
+
n_frames = 6
|
|
125
|
+
scene = _box_scene(seed=11, n_points=1200)
|
|
126
|
+
gt_poses = np.stack(
|
|
127
|
+
[_pose(i * 0.5, 0.0, 0.0, yaw_rad=0.0) for i in range(n_frames)], axis=0
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def build(tmp: Path) -> SlamRunRequest:
|
|
131
|
+
frame_dir = tmp / "straight"
|
|
132
|
+
frame_dir.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
paths = []
|
|
134
|
+
for i, gt in enumerate(gt_poses):
|
|
135
|
+
sensor_to_world = gt
|
|
136
|
+
world_to_sensor = np.linalg.inv(sensor_to_world)
|
|
137
|
+
ones = np.ones((scene.shape[0], 1))
|
|
138
|
+
world_h = np.hstack([scene, ones])
|
|
139
|
+
sensor_pts = (world_to_sensor @ world_h.T).T[:, :3]
|
|
140
|
+
p = frame_dir / f"frame_{i:04d}.pcd"
|
|
141
|
+
_write_pcd(p, sensor_pts)
|
|
142
|
+
paths.append(p)
|
|
143
|
+
return SlamRunRequest(
|
|
144
|
+
frame_paths=tuple(paths),
|
|
145
|
+
timestamps_s=tuple(float(i) * 0.1 for i in range(n_frames)),
|
|
146
|
+
max_range_m=50.0,
|
|
147
|
+
voxel_size_m=0.5,
|
|
148
|
+
deskew=False,
|
|
149
|
+
max_frames=None,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return SlamRunDatasetCase(
|
|
153
|
+
name="straight_line_6frames",
|
|
154
|
+
description=(
|
|
155
|
+
"6 sensor poses sliding +0.5 m/frame along +x in a static 20×20 m room. "
|
|
156
|
+
"Identity-passthrough collapses every pose to origin; KISS-ICP should "
|
|
157
|
+
"recover the slide within a few cm."
|
|
158
|
+
),
|
|
159
|
+
build_request=build,
|
|
160
|
+
gt_poses=gt_poses,
|
|
161
|
+
expected_kiss_icp_max_ate_m=0.20,
|
|
162
|
+
keep_files=tuple(f"frame_{i:04d}.pcd" for i in range(n_frames)),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _short_turn_dataset() -> SlamRunDatasetCase:
|
|
167
|
+
"""4 poses tracing a quarter-turn: translate + yaw by 22.5° each step."""
|
|
168
|
+
n_frames = 4
|
|
169
|
+
scene = _box_scene(seed=23, n_points=1500)
|
|
170
|
+
gt_poses = np.stack(
|
|
171
|
+
[
|
|
172
|
+
_pose(0.3 * i, 0.1 * i, 0.0, yaw_rad=0.392 * i) # ~22.5° per step
|
|
173
|
+
for i in range(n_frames)
|
|
174
|
+
],
|
|
175
|
+
axis=0,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def build(tmp: Path) -> SlamRunRequest:
|
|
179
|
+
frame_dir = tmp / "turn"
|
|
180
|
+
frame_dir.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
paths = []
|
|
182
|
+
for i, gt in enumerate(gt_poses):
|
|
183
|
+
world_to_sensor = np.linalg.inv(gt)
|
|
184
|
+
ones = np.ones((scene.shape[0], 1))
|
|
185
|
+
world_h = np.hstack([scene, ones])
|
|
186
|
+
sensor_pts = (world_to_sensor @ world_h.T).T[:, :3]
|
|
187
|
+
p = frame_dir / f"frame_{i:04d}.pcd"
|
|
188
|
+
_write_pcd(p, sensor_pts)
|
|
189
|
+
paths.append(p)
|
|
190
|
+
return SlamRunRequest(
|
|
191
|
+
frame_paths=tuple(paths),
|
|
192
|
+
timestamps_s=tuple(float(i) * 0.1 for i in range(n_frames)),
|
|
193
|
+
max_range_m=50.0,
|
|
194
|
+
voxel_size_m=0.5,
|
|
195
|
+
deskew=False,
|
|
196
|
+
max_frames=None,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return SlamRunDatasetCase(
|
|
200
|
+
name="short_turn_4frames",
|
|
201
|
+
description=(
|
|
202
|
+
"4 sensor poses tracing a short curve (~22.5° yaw + small translation "
|
|
203
|
+
"per step). Tests that the harness handles non-trivial rotation."
|
|
204
|
+
),
|
|
205
|
+
build_request=build,
|
|
206
|
+
gt_poses=gt_poses,
|
|
207
|
+
expected_kiss_icp_max_ate_m=0.30,
|
|
208
|
+
keep_files=tuple(f"frame_{i:04d}.pcd" for i in range(n_frames)),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def build_default_datasets() -> list[SlamRunDatasetCase]:
|
|
213
|
+
"""Return the deterministic dataset suite the slice evaluator runs."""
|
|
214
|
+
|
|
215
|
+
return [_straight_line_dataset(), _short_turn_dataset()]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def absolute_trajectory_error_m(
|
|
219
|
+
estimated_poses: np.ndarray, gt_poses: np.ndarray
|
|
220
|
+
) -> float:
|
|
221
|
+
"""RMS Euclidean distance between the translation components of two
|
|
222
|
+
pose sequences."""
|
|
223
|
+
|
|
224
|
+
if estimated_poses.shape[0] == 0 or gt_poses.shape[0] == 0:
|
|
225
|
+
return float("inf")
|
|
226
|
+
n = min(estimated_poses.shape[0], gt_poses.shape[0])
|
|
227
|
+
est_t = estimated_poses[:n, :3, 3]
|
|
228
|
+
gt_t = gt_poses[:n, :3, 3]
|
|
229
|
+
diffs = np.linalg.norm(est_t - gt_t, axis=1)
|
|
230
|
+
return float(np.sqrt(np.mean(diffs**2)))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def make_temp_dir() -> tempfile.TemporaryDirectory:
|
|
234
|
+
"""Helper used by the evaluator / tests to allocate a scratch dir."""
|
|
235
|
+
|
|
236
|
+
return tempfile.TemporaryDirectory(prefix="ca_slam_run_")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
__all__ = [
|
|
240
|
+
"SlamRunDatasetCase",
|
|
241
|
+
"build_default_datasets",
|
|
242
|
+
"absolute_trajectory_error_m",
|
|
243
|
+
"make_temp_dir",
|
|
244
|
+
]
|