cloudanalyzer 0.2.0__tar.gz → 0.3.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.2.0 → cloudanalyzer-0.3.0}/PKG-INFO +1 -1
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/__init__.py +1 -1
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/ground_evaluate.py +30 -8
- cloudanalyzer-0.3.0/ca/core/map_evaluate.py +302 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/height_band.py +5 -5
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/voxel_confusion.py +5 -4
- cloudanalyzer-0.3.0/ca/experiments/map_evaluate/common.py +99 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/map_evaluate/evaluate.py +19 -11
- cloudanalyzer-0.3.0/ca/experiments/map_evaluate/nn_thresholds.py +16 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/geometry.py +279 -73
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/split.py +19 -7
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer.egg-info/PKG-INFO +1 -1
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer.egg-info/SOURCES.txt +5 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer_cli/main.py +37 -4
- cloudanalyzer-0.3.0/tests/test_geometry_splat_ellipsoid.py +275 -0
- cloudanalyzer-0.3.0/tests/test_map_evaluate_core.py +81 -0
- cloudanalyzer-0.3.0/tests/test_phase17_vectorize.py +210 -0
- cloudanalyzer-0.3.0/tests/test_phase18_vectorize.py +150 -0
- cloudanalyzer-0.2.0/ca/experiments/map_evaluate/common.py +0 -201
- cloudanalyzer-0.2.0/ca/experiments/map_evaluate/nn_thresholds.py +0 -135
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/LICENSE +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/README.md +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/align.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/baseline_history.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/batch.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/benchmark.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/bundle.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/compare.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/convert.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/__init__.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/check_baseline_evolution.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/check_scaffolding.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/check_triage.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/checks.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/web_progressive_loading.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/web_sampling.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/web_trajectory_sampling.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/crop.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/density_map.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/detection.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/diff.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/downsample.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/__init__.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_baseline_evolution/__init__.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_baseline_evolution/common.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_baseline_evolution/evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_baseline_evolution/pareto_promote.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_baseline_evolution/stability_window.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_baseline_evolution/threshold_guard.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_scaffolding/__init__.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_scaffolding/common.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_scaffolding/evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_scaffolding/literal_profiles.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_scaffolding/object_sections.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_scaffolding/pipeline_overlays.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_triage/__init__.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_triage/common.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_triage/evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_triage/pareto_frontier.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_triage/severity_weighted.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_triage/signature_cluster.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/__init__.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/common.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/nearest_neighbor.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/map_evaluate/__init__.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/map_evaluate/voxel_entropy.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/process_docs.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_progressive_loading/__init__.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_progressive_loading/common.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_progressive_loading/distance_shells.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_progressive_loading/evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_progressive_loading/grid_tiles.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_progressive_loading/spatial_shuffle.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_sampling/__init__.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_sampling/common.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_sampling/evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_sampling/functional_voxel.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_sampling/object_random.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_sampling/pipeline_hybrid.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_trajectory_sampling/__init__.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_trajectory_sampling/common.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_trajectory_sampling/distance_accumulator.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_trajectory_sampling/evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_trajectory_sampling/turn_aware.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_trajectory_sampling/uniform_stride.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/filter.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/ground_evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/history.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/info.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/io.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/kitti.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/log.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/loop_closure_report.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/merge.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/metrics.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/mme.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/normals.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/object_eval.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/pareto.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/pipeline.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/plot.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/point_summary.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/posegraph.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/pr_comment.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/registration.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/report.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/run_evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/sample.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/scan_match_debug.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/slam_debug.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/stats.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/tracking.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/trajectory.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/view.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/visualization.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/web.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer.egg-info/dependency_links.txt +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer.egg-info/entry_points.txt +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer.egg-info/requires.txt +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer.egg-info/top_level.txt +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer_cli/__init__.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/pyproject.toml +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/setup.cfg +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/setup.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_align.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_baseline_history.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_batch.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_benchmark.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_bundle.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_check_baseline_evolution_process.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_check_scaffolding_process.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_check_suite.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_check_triage_process.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_cli.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_compare.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_convert.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_crop.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_density_map.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_detection_evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_detection_tracking_report.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_diff.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_downsample.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_filter.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_geometry.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_ground_evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_ground_evaluate_process.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_history.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_info.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_io.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_kitti.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_logging.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_loop_closure_report.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_map_evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_merge.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_metrics.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_mme.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_normals.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_object_eval_iou.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_output_json.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_pareto.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_phase16_large_scale.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_pipeline.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_plot.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_posegraph.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_pr_comment.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_process_docs.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_public_benchmark_pack.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_public_object_eval_examples.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_registration.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_report.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_run_batch.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_run_evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_sample.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_scan_match_debug.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_slam_debug.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_split.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_stats.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_threshold.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_tracking_evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_traj_batch.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_trajectory.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_visualization.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_progressive_loading_core.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_progressive_loading_evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_progressive_loading_process.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_progressive_loading_strategies.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_sampling_core.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_sampling_evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_sampling_process.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_sampling_strategies.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_trajectory_sampling_core.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_trajectory_sampling_evaluate.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_trajectory_sampling_process.py +0 -0
- {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_trajectory_sampling_strategies.py +0 -0
|
@@ -47,12 +47,34 @@ class GroundEvaluateStrategy(Protocol):
|
|
|
47
47
|
"""Evaluate ground segmentation quality."""
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def _voxel_keys(points: np.ndarray, voxel_size: float) ->
|
|
51
|
-
"""Compute voxel grid keys for an Nx3 point array.
|
|
50
|
+
def _voxel_keys(points: np.ndarray, voxel_size: float) -> np.ndarray:
|
|
51
|
+
"""Compute unique voxel grid keys for an Nx3 point array.
|
|
52
|
+
|
|
53
|
+
Returns an (M, 3) int64 array of unique (i, j, k) voxel indices, sorted
|
|
54
|
+
lexicographically. The original implementation built a ``set`` of Python
|
|
55
|
+
tuples via a per-row generator, which dominated runtime on city-scale
|
|
56
|
+
inputs (millions of points × 4 calls per evaluation).
|
|
57
|
+
"""
|
|
52
58
|
if points.shape[0] == 0:
|
|
53
|
-
return
|
|
59
|
+
return np.empty((0, 3), dtype=np.int64)
|
|
54
60
|
indices = np.floor(points / voxel_size).astype(np.int64)
|
|
55
|
-
return
|
|
61
|
+
return np.unique(indices, axis=0)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _voxel_intersection_size(a: np.ndarray, b: np.ndarray) -> int:
|
|
65
|
+
"""Count voxel keys present in both ``a`` and ``b``.
|
|
66
|
+
|
|
67
|
+
Both inputs are expected to be the unique (M, 3) int64 output of
|
|
68
|
+
:func:`_voxel_keys`. Packs each row into a single ``np.void`` of the row's
|
|
69
|
+
byte length so ``np.intersect1d`` can run in C.
|
|
70
|
+
"""
|
|
71
|
+
if a.size == 0 or b.size == 0:
|
|
72
|
+
return 0
|
|
73
|
+
item_size = a.dtype.itemsize * a.shape[1]
|
|
74
|
+
void_dtype = np.dtype((np.void, item_size))
|
|
75
|
+
a_view = np.ascontiguousarray(a).view(void_dtype).ravel()
|
|
76
|
+
b_view = np.ascontiguousarray(b).view(void_dtype).ravel()
|
|
77
|
+
return int(np.intersect1d(a_view, b_view, assume_unique=True).size)
|
|
56
78
|
|
|
57
79
|
|
|
58
80
|
def confusion_metrics(tp: int, fp: int, fn: int, tn: int) -> dict[str, float]:
|
|
@@ -83,10 +105,10 @@ class VoxelConfusionGroundEvaluateStrategy:
|
|
|
83
105
|
ref_ground_voxels = _voxel_keys(request.reference_ground, request.voxel_size)
|
|
84
106
|
ref_nonground_voxels = _voxel_keys(request.reference_nonground, request.voxel_size)
|
|
85
107
|
|
|
86
|
-
tp =
|
|
87
|
-
fp =
|
|
88
|
-
fn =
|
|
89
|
-
tn =
|
|
108
|
+
tp = _voxel_intersection_size(est_ground_voxels, ref_ground_voxels)
|
|
109
|
+
fp = _voxel_intersection_size(est_ground_voxels, ref_nonground_voxels)
|
|
110
|
+
fn = _voxel_intersection_size(est_nonground_voxels, ref_ground_voxels)
|
|
111
|
+
tn = _voxel_intersection_size(est_nonground_voxels, ref_nonground_voxels)
|
|
90
112
|
metrics = confusion_metrics(tp, fp, fn, tn)
|
|
91
113
|
|
|
92
114
|
return GroundEvaluateResult(
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Stable, minimal interface for point-cloud map evaluation.
|
|
2
|
+
|
|
3
|
+
This slice started as ``ca/experiments/map_evaluate/`` with two
|
|
4
|
+
non-comparable strategies on separate metric-family lanes:
|
|
5
|
+
|
|
6
|
+
- ``nn_thresholds`` — reference-based (GT-aware) MapEval-style accuracy and
|
|
7
|
+
completeness@τ via per-point nearest-neighbor distances (scipy cKDTree).
|
|
8
|
+
- ``voxel_entropy`` — reference-free self-consistency proxy via voxelized
|
|
9
|
+
neighborhood occupancy entropy.
|
|
10
|
+
|
|
11
|
+
Because the two strategies do not compete head-to-head, "promoting to core"
|
|
12
|
+
means lifting the **request/result contract plus the adopted reference-based
|
|
13
|
+
strategy** here, leaving ``voxel_entropy`` in ``ca/experiments/map_evaluate``
|
|
14
|
+
as the orthogonal reference-free option. Callers that just want GT-based
|
|
15
|
+
metrics can now depend on ``ca.core.map_evaluate`` directly and the CLI
|
|
16
|
+
no longer reaches into ``ca.experiments``.
|
|
17
|
+
|
|
18
|
+
The reference-free lane remains experimental until we settle on a single
|
|
19
|
+
GT-free metric — anyone needing it for now imports from
|
|
20
|
+
``ca.experiments.map_evaluate.voxel_entropy`` explicitly.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Protocol
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------- contract
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(slots=True)
|
|
36
|
+
class MapEvaluateRequest:
|
|
37
|
+
"""Compare an estimated point cloud map to a reference (optional).
|
|
38
|
+
|
|
39
|
+
Strategies with ``reference_required=True`` raise on ``reference_points=None``;
|
|
40
|
+
reference-free strategies treat the same field as unused.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
estimated_points: np.ndarray # (N, 3)
|
|
44
|
+
reference_points: np.ndarray | None = None # (M, 3) when present
|
|
45
|
+
# MapEval-inspired: allow using an external coarse alignment.
|
|
46
|
+
initial_transform_4x4: np.ndarray | None = None # shape (4, 4)
|
|
47
|
+
# "none" = use estimated_points as-is, "initial" = pre-apply initial_transform_4x4.
|
|
48
|
+
align_mode: str = "none"
|
|
49
|
+
downsample_voxel_size: float = 0.0
|
|
50
|
+
# Optional output dir for strategy-specific artifacts (colored PLYs, etc.)
|
|
51
|
+
artifact_dir: str | None = None
|
|
52
|
+
# MapEval "accuracy_level" thresholds for inlier ratios.
|
|
53
|
+
thresholds_m: tuple[float, ...] = (0.2, 0.1, 0.08, 0.05, 0.01)
|
|
54
|
+
# A coarse voxel size useful for structure-aware proxies.
|
|
55
|
+
structure_voxel_size: float = 0.5
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(slots=True)
|
|
59
|
+
class MapEvaluateResult:
|
|
60
|
+
"""Common output for all strategies.
|
|
61
|
+
|
|
62
|
+
Classification fields (``metric_family``, ``reference_required``, ``mode``,
|
|
63
|
+
``sampling_policy``) describe *how* the metrics were computed so consumers
|
|
64
|
+
(CI gates, reports, batch aggregators) can keep reference-based and
|
|
65
|
+
reference-free metrics in separate lanes without parsing metric names.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
strategy: str
|
|
69
|
+
design: str
|
|
70
|
+
metrics: dict[str, float]
|
|
71
|
+
artifacts: dict[str, Any]
|
|
72
|
+
metric_family: str = "unspecified"
|
|
73
|
+
reference_required: bool = False
|
|
74
|
+
mode: str = "exact"
|
|
75
|
+
sampling_policy: dict[str, Any] = field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MapEvaluateStrategy(Protocol):
|
|
79
|
+
"""Protocol kept in core after promoting the reference-based strategy."""
|
|
80
|
+
|
|
81
|
+
name: str
|
|
82
|
+
design: str
|
|
83
|
+
|
|
84
|
+
def evaluate(self, request: MapEvaluateRequest) -> MapEvaluateResult:
|
|
85
|
+
"""Evaluate a map and return classified metrics."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------- helpers
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _require_xyz(points: np.ndarray, name: str) -> np.ndarray:
|
|
92
|
+
arr = np.asarray(points, dtype=np.float64)
|
|
93
|
+
if arr.ndim != 2 or arr.shape[1] != 3:
|
|
94
|
+
raise ValueError(f"{name} must be shape (N, 3); got {arr.shape}")
|
|
95
|
+
return arr
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _require_transform_4x4(matrix: np.ndarray, name: str) -> np.ndarray:
|
|
99
|
+
mat = np.asarray(matrix, dtype=np.float64)
|
|
100
|
+
if mat.shape != (4, 4):
|
|
101
|
+
raise ValueError(f"{name} must be shape (4, 4); got {mat.shape}")
|
|
102
|
+
return mat
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def apply_transform(points: np.ndarray, transform_4x4: np.ndarray) -> np.ndarray:
|
|
106
|
+
pts = _require_xyz(points, "points")
|
|
107
|
+
t = _require_transform_4x4(transform_4x4, "transform_4x4")
|
|
108
|
+
if pts.shape[0] == 0:
|
|
109
|
+
return pts
|
|
110
|
+
hom = np.concatenate([pts, np.ones((pts.shape[0], 1), dtype=np.float64)], axis=1)
|
|
111
|
+
out = (hom @ t.T)[:, :3]
|
|
112
|
+
return np.asarray(out, dtype=np.float64)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def aligned_estimated_points(request: MapEvaluateRequest) -> np.ndarray:
|
|
116
|
+
est = _require_xyz(request.estimated_points, "estimated_points")
|
|
117
|
+
mode = (request.align_mode or "none").strip().lower()
|
|
118
|
+
if mode == "none":
|
|
119
|
+
return est
|
|
120
|
+
if mode == "initial":
|
|
121
|
+
if request.initial_transform_4x4 is None:
|
|
122
|
+
raise ValueError("align_mode='initial' requires initial_transform_4x4.")
|
|
123
|
+
return apply_transform(est, request.initial_transform_4x4)
|
|
124
|
+
raise ValueError(f"Unsupported align_mode: {request.align_mode!r}. Use 'none' or 'initial'.")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def ensure_artifact_dir(request: MapEvaluateRequest, subdir: str) -> Path | None:
|
|
128
|
+
"""Return an ensured artifact directory path or None if disabled."""
|
|
129
|
+
if request.artifact_dir is None:
|
|
130
|
+
return None
|
|
131
|
+
root = Path(request.artifact_dir)
|
|
132
|
+
out = root / subdir
|
|
133
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def voxel_downsample(points: np.ndarray, voxel_size: float) -> np.ndarray:
|
|
138
|
+
"""Deterministic voxel downsample by centroid per voxel."""
|
|
139
|
+
pts = _require_xyz(points, "points")
|
|
140
|
+
if pts.shape[0] == 0 or voxel_size <= 0:
|
|
141
|
+
return pts
|
|
142
|
+
keys = np.floor(pts / voxel_size).astype(np.int64)
|
|
143
|
+
order = np.lexsort((keys[:, 2], keys[:, 1], keys[:, 0]))
|
|
144
|
+
keys_sorted = keys[order]
|
|
145
|
+
pts_sorted = pts[order]
|
|
146
|
+
unique, start_idx = np.unique(keys_sorted, axis=0, return_index=True)
|
|
147
|
+
centroids: list[np.ndarray] = []
|
|
148
|
+
for i, s in enumerate(start_idx):
|
|
149
|
+
e = start_idx[i + 1] if i + 1 < len(start_idx) else pts_sorted.shape[0]
|
|
150
|
+
centroids.append(pts_sorted[s:e].mean(axis=0))
|
|
151
|
+
out = np.vstack(centroids) if centroids else np.zeros((0, 3), dtype=np.float64)
|
|
152
|
+
_ = unique # kept to mirror the index ordering for documentation
|
|
153
|
+
return np.asarray(out)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------------- adopted strategy
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _min_distances_kdtree(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
160
|
+
"""Per-point min Euclidean distance from ``a`` to ``b`` via scipy cKDTree."""
|
|
161
|
+
if a.shape[0] == 0:
|
|
162
|
+
return np.zeros((0,), dtype=np.float64)
|
|
163
|
+
if b.shape[0] == 0:
|
|
164
|
+
return np.full((a.shape[0],), np.inf, dtype=np.float64)
|
|
165
|
+
|
|
166
|
+
from scipy.spatial import cKDTree
|
|
167
|
+
|
|
168
|
+
tree = cKDTree(np.asarray(b, dtype=np.float64))
|
|
169
|
+
distances, _ = tree.query(np.asarray(a, dtype=np.float64), k=1)
|
|
170
|
+
return np.asarray(distances, dtype=np.float64)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _error_colors(dist_m: np.ndarray, vmax_m: float) -> np.ndarray:
|
|
174
|
+
"""Green→red ramp for distance visualization."""
|
|
175
|
+
vmax = float(max(vmax_m, 1e-12))
|
|
176
|
+
x = np.clip(dist_m / vmax, 0.0, 1.0)
|
|
177
|
+
r = x
|
|
178
|
+
g = 1.0 - x
|
|
179
|
+
b = np.zeros_like(x)
|
|
180
|
+
return np.column_stack([r, g, b]).astype(np.float64)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _write_colored_ply(points: np.ndarray, colors: np.ndarray, path: str) -> None:
|
|
184
|
+
import open3d as o3d
|
|
185
|
+
|
|
186
|
+
pcd = o3d.geometry.PointCloud()
|
|
187
|
+
pcd.points = o3d.utility.Vector3dVector(np.asarray(points, dtype=np.float64))
|
|
188
|
+
pcd.colors = o3d.utility.Vector3dVector(np.asarray(colors, dtype=np.float64))
|
|
189
|
+
o3d.io.write_point_cloud(path, pcd, write_ascii=False)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass(slots=True)
|
|
193
|
+
class NNThresholdMapEvaluateStrategy:
|
|
194
|
+
"""MapEval-style accuracy@τ / completeness@τ via per-point NN distances.
|
|
195
|
+
|
|
196
|
+
Promoted from ``ca/experiments/map_evaluate/nn_thresholds.py`` as the
|
|
197
|
+
adopted reference-based map evaluation strategy. The reference-free
|
|
198
|
+
``voxel_entropy`` lane stays under ``ca.experiments`` because the two
|
|
199
|
+
strategies do not compete on the same metric family.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
name: str = "nn_thresholds"
|
|
203
|
+
design: str = "functional"
|
|
204
|
+
|
|
205
|
+
def evaluate(self, request: MapEvaluateRequest) -> MapEvaluateResult:
|
|
206
|
+
est_aligned = aligned_estimated_points(request)
|
|
207
|
+
est = voxel_downsample(_require_xyz(est_aligned, "estimated_points"), request.downsample_voxel_size)
|
|
208
|
+
if request.reference_points is None:
|
|
209
|
+
raise ValueError("nn_thresholds requires reference_points (GT).")
|
|
210
|
+
ref = voxel_downsample(_require_xyz(request.reference_points, "reference_points"), request.downsample_voxel_size)
|
|
211
|
+
|
|
212
|
+
est_to_ref = _min_distances_kdtree(est, ref)
|
|
213
|
+
ref_to_est = _min_distances_kdtree(ref, est)
|
|
214
|
+
|
|
215
|
+
thresholds = tuple(float(x) for x in request.thresholds_m)
|
|
216
|
+
metrics: dict[str, float] = {
|
|
217
|
+
"n_est": float(est.shape[0]),
|
|
218
|
+
"n_ref": float(ref.shape[0]),
|
|
219
|
+
"mean_est_to_ref_m": float(np.mean(est_to_ref)) if est_to_ref.size else float("inf"),
|
|
220
|
+
"mean_ref_to_est_m": float(np.mean(ref_to_est)) if ref_to_est.size else float("inf"),
|
|
221
|
+
"chamfer_m": (
|
|
222
|
+
float(np.mean(est_to_ref)) + float(np.mean(ref_to_est))
|
|
223
|
+
if (est_to_ref.size and ref_to_est.size)
|
|
224
|
+
else float("inf")
|
|
225
|
+
),
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for t in thresholds:
|
|
229
|
+
metrics[f"accuracy@{t:.3f}m"] = float(np.mean(est_to_ref <= t)) if est_to_ref.size else 0.0
|
|
230
|
+
metrics[f"completeness@{t:.3f}m"] = float(np.mean(ref_to_est <= t)) if ref_to_est.size else 0.0
|
|
231
|
+
|
|
232
|
+
# One scalar summary (F-score) using the first threshold.
|
|
233
|
+
t0 = thresholds[0] if thresholds else 0.2
|
|
234
|
+
acc = metrics.get(f"accuracy@{t0:.3f}m", 0.0)
|
|
235
|
+
com = metrics.get(f"completeness@{t0:.3f}m", 0.0)
|
|
236
|
+
fscore = (2 * acc * com / (acc + com)) if (acc + com) > 0 else 0.0
|
|
237
|
+
metrics[f"fscore@{t0:.3f}m"] = float(fscore)
|
|
238
|
+
|
|
239
|
+
artifacts: dict[str, object] = {
|
|
240
|
+
"thresholds_m": thresholds,
|
|
241
|
+
"downsample_voxel_size": float(request.downsample_voxel_size),
|
|
242
|
+
"align_mode": request.align_mode,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# Optional: MapEval-style raw/inlier error visualization PLYs.
|
|
246
|
+
out_dir = ensure_artifact_dir(request, "map_evaluate/nn_thresholds")
|
|
247
|
+
if out_dir is not None and est.shape[0] > 0:
|
|
248
|
+
vmax = thresholds[0] if thresholds else 0.2
|
|
249
|
+
raw_colors = _error_colors(est_to_ref, vmax_m=vmax)
|
|
250
|
+
raw_path = str(out_dir / "estimated_error_raw.ply")
|
|
251
|
+
_write_colored_ply(est, raw_colors, raw_path)
|
|
252
|
+
|
|
253
|
+
inlier_mask = est_to_ref <= vmax
|
|
254
|
+
inlier_points = est[inlier_mask]
|
|
255
|
+
inlier_colors = raw_colors[inlier_mask]
|
|
256
|
+
inlier_path = str(out_dir / f"estimated_error_inlier_{vmax:.3f}m.ply")
|
|
257
|
+
_write_colored_ply(inlier_points, inlier_colors, inlier_path)
|
|
258
|
+
|
|
259
|
+
artifacts["estimated_error_raw_ply"] = raw_path
|
|
260
|
+
artifacts[f"estimated_error_inlier_{vmax:.3f}m_ply"] = inlier_path
|
|
261
|
+
|
|
262
|
+
downsample_voxel = float(request.downsample_voxel_size)
|
|
263
|
+
mode = "voxelized" if downsample_voxel > 0 else "exact"
|
|
264
|
+
sampling_policy: dict[str, object] = {
|
|
265
|
+
"downsample_voxel_size_m": downsample_voxel,
|
|
266
|
+
"thresholds_m": list(thresholds),
|
|
267
|
+
"align_mode": request.align_mode,
|
|
268
|
+
"nn_backend": "scipy_ckdtree",
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return MapEvaluateResult(
|
|
272
|
+
strategy=self.name,
|
|
273
|
+
design=self.design,
|
|
274
|
+
metrics=metrics,
|
|
275
|
+
artifacts=artifacts,
|
|
276
|
+
metric_family="reference_based_nn_thresholds",
|
|
277
|
+
reference_required=True,
|
|
278
|
+
mode=mode,
|
|
279
|
+
sampling_policy=sampling_policy,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def evaluate_map(
|
|
284
|
+
request: MapEvaluateRequest,
|
|
285
|
+
strategy: MapEvaluateStrategy | None = None,
|
|
286
|
+
) -> MapEvaluateResult:
|
|
287
|
+
"""Evaluate a map using the stabilized strategy (defaults to NNThreshold)."""
|
|
288
|
+
eval_strategy = strategy or NNThresholdMapEvaluateStrategy()
|
|
289
|
+
return eval_strategy.evaluate(request)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
__all__ = [
|
|
293
|
+
"MapEvaluateRequest",
|
|
294
|
+
"MapEvaluateResult",
|
|
295
|
+
"MapEvaluateStrategy",
|
|
296
|
+
"NNThresholdMapEvaluateStrategy",
|
|
297
|
+
"aligned_estimated_points",
|
|
298
|
+
"apply_transform",
|
|
299
|
+
"ensure_artifact_dir",
|
|
300
|
+
"evaluate_map",
|
|
301
|
+
"voxel_downsample",
|
|
302
|
+
]
|
|
@@ -33,7 +33,7 @@ def _per_band_confusion(
|
|
|
33
33
|
voxel_size: float,
|
|
34
34
|
) -> list[dict]:
|
|
35
35
|
"""Compute confusion matrix per height band using voxel matching."""
|
|
36
|
-
from ca.core.ground_evaluate import _voxel_keys
|
|
36
|
+
from ca.core.ground_evaluate import _voxel_intersection_size, _voxel_keys
|
|
37
37
|
|
|
38
38
|
num_bands = len(band_edges) - 1
|
|
39
39
|
bands: list[dict] = []
|
|
@@ -51,10 +51,10 @@ def _per_band_confusion(
|
|
|
51
51
|
rg = _voxel_keys(_filter(reference_ground), voxel_size)
|
|
52
52
|
rn = _voxel_keys(_filter(reference_nonground), voxel_size)
|
|
53
53
|
|
|
54
|
-
tp =
|
|
55
|
-
fp =
|
|
56
|
-
fn =
|
|
57
|
-
tn =
|
|
54
|
+
tp = _voxel_intersection_size(eg, rg)
|
|
55
|
+
fp = _voxel_intersection_size(eg, rn)
|
|
56
|
+
fn = _voxel_intersection_size(en, rg)
|
|
57
|
+
tn = _voxel_intersection_size(en, rn)
|
|
58
58
|
metrics = confusion_metrics(tp, fp, fn, tn)
|
|
59
59
|
bands.append({
|
|
60
60
|
"band": band_idx,
|
{cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/voxel_confusion.py
RENAMED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from ca.core.ground_evaluate import (
|
|
6
6
|
GroundEvaluateRequest,
|
|
7
7
|
GroundEvaluateResult,
|
|
8
|
+
_voxel_intersection_size,
|
|
8
9
|
_voxel_keys,
|
|
9
10
|
confusion_metrics,
|
|
10
11
|
)
|
|
@@ -22,10 +23,10 @@ class VoxelConfusionExperimentalStrategy:
|
|
|
22
23
|
ref_ground_voxels = _voxel_keys(request.reference_ground, request.voxel_size)
|
|
23
24
|
ref_nonground_voxels = _voxel_keys(request.reference_nonground, request.voxel_size)
|
|
24
25
|
|
|
25
|
-
tp =
|
|
26
|
-
fp =
|
|
27
|
-
fn =
|
|
28
|
-
tn =
|
|
26
|
+
tp = _voxel_intersection_size(est_ground_voxels, ref_ground_voxels)
|
|
27
|
+
fp = _voxel_intersection_size(est_ground_voxels, ref_nonground_voxels)
|
|
28
|
+
fn = _voxel_intersection_size(est_nonground_voxels, ref_ground_voxels)
|
|
29
|
+
tn = _voxel_intersection_size(est_nonground_voxels, ref_nonground_voxels)
|
|
29
30
|
metrics = confusion_metrics(tp, fp, fn, tn)
|
|
30
31
|
|
|
31
32
|
return GroundEvaluateResult(
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Re-exports for backward compatibility after the core promotion.
|
|
2
|
+
|
|
3
|
+
The map_evaluate request/result contract and helpers are now owned by
|
|
4
|
+
``ca.core.map_evaluate``. The experiment slice keeps this module as a thin
|
|
5
|
+
re-export so the remaining experimental strategies
|
|
6
|
+
(``voxel_entropy.VoxelEntropyMapEvaluateStrategy``) and any external callers
|
|
7
|
+
still work via the old import path.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ca.core.map_evaluate import (
|
|
13
|
+
MapEvaluateRequest,
|
|
14
|
+
MapEvaluateResult,
|
|
15
|
+
_require_xyz,
|
|
16
|
+
_require_transform_4x4,
|
|
17
|
+
aligned_estimated_points,
|
|
18
|
+
apply_transform,
|
|
19
|
+
ensure_artifact_dir,
|
|
20
|
+
voxel_downsample,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(slots=True)
|
|
28
|
+
class MapEvaluateDatasetCase:
|
|
29
|
+
"""Small fixture: human-readable name + the request to score."""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
description: str
|
|
33
|
+
request: MapEvaluateRequest
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_default_datasets() -> list[MapEvaluateDatasetCase]:
|
|
37
|
+
"""Small deterministic point-cloud scenarios for quick strategy comparison."""
|
|
38
|
+
rng = np.random.default_rng(7)
|
|
39
|
+
|
|
40
|
+
ref = rng.uniform([-5, -5, -1], [5, 5, 1], size=(400, 3))
|
|
41
|
+
|
|
42
|
+
est_drift = ref + np.array([0.15, -0.05, 0.0]) + rng.normal(0, 0.02, size=ref.shape)
|
|
43
|
+
|
|
44
|
+
mask = ref[:, 0] > -1.0
|
|
45
|
+
est_incomplete = ref[mask] + rng.normal(0, 0.02, size=(mask.sum(), 3))
|
|
46
|
+
|
|
47
|
+
scan_a = rng.uniform([-6, -6, -1], [6, 6, 1], size=(250, 3))
|
|
48
|
+
scan_b = scan_a + rng.normal(0, 0.03, size=scan_a.shape)
|
|
49
|
+
est_self = np.vstack([scan_a, scan_b])
|
|
50
|
+
|
|
51
|
+
return [
|
|
52
|
+
MapEvaluateDatasetCase(
|
|
53
|
+
name="gt_drift",
|
|
54
|
+
description="Estimated map has small rigid drift relative to reference.",
|
|
55
|
+
request=MapEvaluateRequest(
|
|
56
|
+
estimated_points=est_drift,
|
|
57
|
+
reference_points=ref,
|
|
58
|
+
downsample_voxel_size=0.0,
|
|
59
|
+
thresholds_m=(0.2, 0.1, 0.08, 0.05, 0.01),
|
|
60
|
+
structure_voxel_size=0.5,
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
MapEvaluateDatasetCase(
|
|
64
|
+
name="gt_incomplete",
|
|
65
|
+
description="Estimated map misses a region; completeness should drop.",
|
|
66
|
+
request=MapEvaluateRequest(
|
|
67
|
+
estimated_points=est_incomplete,
|
|
68
|
+
reference_points=ref,
|
|
69
|
+
downsample_voxel_size=0.0,
|
|
70
|
+
thresholds_m=(0.2, 0.1, 0.08, 0.05, 0.01),
|
|
71
|
+
structure_voxel_size=0.5,
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
MapEvaluateDatasetCase(
|
|
75
|
+
name="no_gt_self_consistency",
|
|
76
|
+
description="No GT; fused map from two noisy overlapping scans.",
|
|
77
|
+
request=MapEvaluateRequest(
|
|
78
|
+
estimated_points=est_self,
|
|
79
|
+
reference_points=None,
|
|
80
|
+
downsample_voxel_size=0.0,
|
|
81
|
+
thresholds_m=(0.2, 0.1, 0.08, 0.05, 0.01),
|
|
82
|
+
structure_voxel_size=0.5,
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
"MapEvaluateDatasetCase",
|
|
90
|
+
"MapEvaluateRequest",
|
|
91
|
+
"MapEvaluateResult",
|
|
92
|
+
"_require_xyz",
|
|
93
|
+
"_require_transform_4x4",
|
|
94
|
+
"aligned_estimated_points",
|
|
95
|
+
"apply_transform",
|
|
96
|
+
"build_default_datasets",
|
|
97
|
+
"ensure_artifact_dir",
|
|
98
|
+
"voxel_downsample",
|
|
99
|
+
]
|
|
@@ -79,9 +79,14 @@ def run_map_evaluate_experiment(
|
|
|
79
79
|
],
|
|
80
80
|
"results": rows,
|
|
81
81
|
"decision": {
|
|
82
|
-
"selected_experiment":
|
|
83
|
-
"stabilized_core_strategy":
|
|
84
|
-
"reason":
|
|
82
|
+
"selected_experiment": "nn_thresholds",
|
|
83
|
+
"stabilized_core_strategy": "nn_thresholds",
|
|
84
|
+
"reason": (
|
|
85
|
+
"Promoted to ca.core.map_evaluate as the reference-based "
|
|
86
|
+
"(GT-aware) MapEval-style nearest-neighbor threshold strategy. "
|
|
87
|
+
"voxel_entropy remains experimental as the orthogonal "
|
|
88
|
+
"reference-free lane until a single GT-free metric is settled."
|
|
89
|
+
),
|
|
85
90
|
},
|
|
86
91
|
}
|
|
87
92
|
|
|
@@ -136,16 +141,18 @@ def render_decision_section(report: dict) -> str:
|
|
|
136
141
|
"",
|
|
137
142
|
"### Adopted",
|
|
138
143
|
"",
|
|
139
|
-
"
|
|
144
|
+
"- `nn_thresholds` (reference-based, GT-aware MapEval-style accuracy/completeness@τ).",
|
|
145
|
+
" Promoted to `ca/core/map_evaluate.py` as `NNThresholdMapEvaluateStrategy`.",
|
|
140
146
|
"",
|
|
141
147
|
"### Not Adopted",
|
|
142
148
|
"",
|
|
143
|
-
"-
|
|
149
|
+
"- `voxel_entropy` (reference-free self-consistency proxy) stays under `ca/experiments`",
|
|
150
|
+
" as the orthogonal GT-free lane until a single reference-free metric is settled.",
|
|
144
151
|
"",
|
|
145
|
-
"### Trigger To Promote",
|
|
152
|
+
"### Trigger To Promote `voxel_entropy`",
|
|
146
153
|
"",
|
|
147
|
-
"-
|
|
148
|
-
"-
|
|
154
|
+
"- Pick one GT-free metric (entropy / structure / MME) as the canonical lane.",
|
|
155
|
+
"- Define a stable failure-mode contract (when does a GT-free score block CI?).",
|
|
149
156
|
]
|
|
150
157
|
)
|
|
151
158
|
|
|
@@ -155,10 +162,11 @@ def render_interface_section(report: dict) -> str:
|
|
|
155
162
|
[
|
|
156
163
|
"## map_evaluate",
|
|
157
164
|
"",
|
|
158
|
-
"### Current Minimal Interface
|
|
165
|
+
"### Current Minimal Interface",
|
|
159
166
|
"",
|
|
160
|
-
"
|
|
161
|
-
"`
|
|
167
|
+
"Promoted to `ca/core/map_evaluate.py`. The request/result contract,",
|
|
168
|
+
"shared helpers, and adopted `NNThresholdMapEvaluateStrategy` all live there.",
|
|
169
|
+
"`ca/experiments/map_evaluate/{common,nn_thresholds}.py` re-export from core for back-compat.",
|
|
162
170
|
"",
|
|
163
171
|
"Result objects carry classification fields so reference-based and reference-free metrics stay in separate lanes:",
|
|
164
172
|
"",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Backward-compatible re-export of the now-promoted nn_thresholds strategy.
|
|
2
|
+
|
|
3
|
+
The MapEval-style nearest-neighbor threshold strategy has been promoted to
|
|
4
|
+
``ca.core.map_evaluate`` as ``NNThresholdMapEvaluateStrategy``. This module
|
|
5
|
+
stays as a thin re-export so any existing imports (and the experimental
|
|
6
|
+
``map_evaluate`` registry) keep working.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from ca.core.map_evaluate import (
|
|
12
|
+
NNThresholdMapEvaluateStrategy,
|
|
13
|
+
_min_distances_kdtree,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = ["NNThresholdMapEvaluateStrategy", "_min_distances_kdtree"]
|