From 7dcb53bb77d009340759d053bc6d246e6707f95d Mon Sep 17 00:00:00 2001 From: cyy_mac Date: Fri, 27 Mar 2026 03:41:42 +0800 Subject: [PATCH] add wust typr mpc and mutipule x --- .../include/armor_solver/armor_solver.hpp | 5 + .../armor_solver/armor_solver_node.hpp | 3 + .../armor_solver/trajectory_planner.hpp | 426 +++ .../armor_solver/src/armor_solver.cpp | 69 +- .../armor_solver/src/armor_solver_node.cpp | 75 + .../armor_solver/src/trajectory_planner.cpp | 441 +++ .../node_params/armor_solver_params.yaml | 25 +- wust_vision-main/.clang-format | 72 + wust_vision-main/.clangd | 6 + wust_vision-main/.gitignore | 31 + wust_vision-main/.gitmodules | 6 + wust_vision-main/3rdparty/angles.h | 373 +++ wust_vision-main/3rdparty/ankerl/stl.h | 84 + .../3rdparty/ankerl/unordered_dense.h | 2440 +++++++++++++++++ wust_vision-main/CMakeLists.txt | 160 ++ wust_vision-main/README.md | 252 ++ wust_vision-main/cmake/FindHikSDK.cmake | 147 + wust_vision-main/cmake/FindOrt.cmake | 88 + wust_vision-main/cmake/FindTensorRT.cmake | 66 + wust_vision-main/config/auto_aim.yaml | 131 + wust_vision-main/config/auto_buff.yaml | 76 + wust_vision-main/config/auto_guidance.yaml | 83 + wust_vision-main/config/auto_sniper.yaml | 27 + wust_vision-main/config/camera.yaml | 50 + wust_vision-main/config/camera_info.yaml | 82 + wust_vision-main/config/common.yaml | 34 + wust_vision-main/config/detect_ml.yaml | 66 + wust_vision-main/config/detect_opencv.yaml | 19 + wust_vision-main/config/guard.sh | 24 + wust_vision-main/config/omni/camera0.yaml | 51 + wust_vision-main/config/omni/camera1.yaml | 51 + wust_vision-main/config/omni/camera_info.yaml | 28 + wust_vision-main/config/omni/detect_ml.yaml | 66 + .../config/omni/detect_opencv.yaml | 19 + wust_vision-main/config/omni/omni.yaml | 37 + wust_vision-main/cuda_infer/CMakeLists.txt | 61 + wust_vision-main/cuda_infer/armor_infer.cu | 323 +++ wust_vision-main/cuda_infer/armor_infer.hpp | 78 + wust_vision-main/cuda_infer/letter_box.cu | 147 + wust_vision-main/cuda_infer/letter_box.hpp | 42 + wust_vision-main/env.bash | 19 + wust_vision-main/format.sh | 3 + wust_vision-main/model/0526.engine | Bin 0 -> 7255356 bytes wust_vision-main/model/0526.onnx | Bin 0 -> 4359932 bytes wust_vision-main/model/0708.engine | Bin 0 -> 6032604 bytes wust_vision-main/model/0708.onnx | Bin 0 -> 5752110 bytes wust_vision-main/model/label.txt | 9 + wust_vision-main/model/lenet.onnx | Bin 0 -> 248674 bytes wust_vision-main/model/mlp.onnx | Bin 0 -> 314058 bytes wust_vision-main/model/opt-0527-001.onnx | Bin 0 -> 3664655 bytes wust_vision-main/model/opt-1208-001.bin | Bin 0 -> 2128592 bytes wust_vision-main/model/opt-1208-001.engine | Bin 0 -> 3489348 bytes wust_vision-main/model/opt-1208-001.onnx | Bin 0 -> 2167983 bytes wust_vision-main/model/opt-1208-001.param | 218 ++ wust_vision-main/model/opt_1208_001.ncnn.bin | Bin 0 -> 1076688 bytes .../model/opt_1208_001.ncnn.param | 193 ++ .../model/reborn_number_classifier.engine | Bin 0 -> 525788 bytes .../model/reborn_number_classifier.onnx | Bin 0 -> 117494 bytes wust_vision-main/model/tiny_resnet.onnx | Bin 0 -> 1105322 bytes wust_vision-main/model/yolox_armor3.onnx | Bin 0 -> 2335060 bytes wust_vision-main/read_shm_image_mmap_only.py | 203 ++ wust_vision-main/ros2/ros2.hpp | 136 + wust_vision-main/ros2/tf.hpp | 88 + wust_vision-main/run.sh | 161 ++ wust_vision-main/script/install_depences.sh | 6 + wust_vision-main/script/rsync.sh | 28 + wust_vision-main/script/setup_devenv.sh | 65 + wust_vision-main/script/setup_service.sh | 109 + .../script/setup_service_sentry.sh | 109 + wust_vision-main/script/setup_udev.sh | 8 + wust_vision-main/src/dart.cpp | 204 ++ wust_vision-main/src/hero.cpp | 35 + wust_vision-main/src/sentry.cpp | 302 ++ wust_vision-main/src/sim.cpp | 488 ++++ wust_vision-main/src/standard.cpp | 27 + wust_vision-main/static/css/style.css | 424 +++ wust_vision-main/static/js/chart_logic.js | 249 ++ wust_vision-main/static/js/json_view.js | 61 + wust_vision-main/static/js/main.js | 35 + wust_vision-main/static/logo.JPG | Bin 0 -> 96508 bytes .../armor_control/tinympc/CMakeLists.txt | 43 + .../auto_aim/armor_control/tinympc/admm.cpp | 399 +++ .../auto_aim/armor_control/tinympc/admm.hpp | 37 + .../armor_control/tinympc/codegen.cpp | 466 ++++ .../armor_control/tinympc/codegen.hpp | 28 + .../auto_aim/armor_control/tinympc/error.hpp | 29 + .../armor_control/tinympc/rho_benchmark.cpp | 252 ++ .../armor_control/tinympc/rho_benchmark.hpp | 94 + .../armor_control/tinympc/tiny_api.cpp | 876 ++++++ .../armor_control/tinympc/tiny_api.hpp | 118 + .../tinympc/tiny_api_constants.hpp | 13 + .../auto_aim/armor_control/tinympc/types.hpp | 197 ++ .../tasks/auto_aim/armor_control/traj.hpp | 111 + .../auto_aim/armor_control/very_aimer.cpp | 1355 +++++++++ .../auto_aim/armor_control/very_aimer.hpp | 27 + .../armor_detect/armor_detector_base.hpp | 70 + .../armor_detect/armor_detector_common.cpp | 473 ++++ .../armor_detect/armor_detector_common.hpp | 41 + .../armor_detect/armor_detector_factory.hpp | 134 + .../auto_aim/armor_detect/armor_infer.cpp | 198 ++ .../auto_aim/armor_detect/armor_infer.hpp | 324 +++ .../armor_detect/ncnn/armor_detector_ncnn.cpp | 222 ++ .../armor_detect/ncnn/armor_detector_ncnn.hpp | 36 + .../armor_detect/number_classifier/base.hpp | 12 + .../number_classifier/factory.hpp | 34 + .../number_classifier/number_classifier.cpp | 116 + .../number_classifier/number_classifier.hpp | 35 + .../number_classifier_trt.cpp | 118 + .../number_classifier_trt.hpp | 39 + .../armor_detector_onnxruntime.cpp | 141 + .../armor_detector_onnxruntime.hpp | 36 + .../opencv/armor_detector_opencv.cpp | 382 +++ .../opencv/armor_detector_opencv.hpp | 42 + .../openvino/armor_detector_openvino.cpp | 206 ++ .../openvino/armor_detector_openvino.hpp | 38 + .../tensorrt/armor_detector_tensorrt.cpp | 320 +++ .../tensorrt/armor_detector_tensorrt.hpp | 40 + .../tasks/auto_aim/armor_omni/armor_omni.cpp | 442 +++ .../tasks/auto_aim/armor_omni/armor_omni.hpp | 36 + .../motion_models/motion_model_crazy.hpp | 286 ++ .../motion_models/motion_modelypdv2.hpp | 226 ++ .../tasks/auto_aim/armor_tracker/target.cpp | 379 +++ .../tasks/auto_aim/armor_tracker/target.hpp | 185 ++ .../auto_aim/armor_tracker/trackerv3.cpp | 209 ++ .../auto_aim/armor_tracker/trackerv3.hpp | 23 + .../auto_aim/armor_where/armor_where.cpp | 307 +++ .../auto_aim/armor_where/armor_where.hpp | 26 + wust_vision-main/tasks/auto_aim/auto_aim.cpp | 404 +++ wust_vision-main/tasks/auto_aim/auto_aim.hpp | 46 + .../tasks/auto_aim/auto_aim_fsm.hpp | 134 + wust_vision-main/tasks/auto_aim/debug.cpp | 684 +++++ wust_vision-main/tasks/auto_aim/debug.hpp | 38 + wust_vision-main/tasks/auto_aim/type.cpp | 399 +++ wust_vision-main/tasks/auto_aim/type.hpp | 172 ++ .../tasks/auto_buff/auto_buff.cpp | 360 +++ .../tasks/auto_buff/auto_buff.hpp | 38 + wust_vision-main/tasks/auto_buff/debug.cpp | 241 ++ wust_vision-main/tasks/auto_buff/debug.hpp | 39 + .../tasks/auto_buff/rune_control/aimer.cpp | 237 ++ .../tasks/auto_buff/rune_control/aimer.hpp | 22 + .../auto_buff/rune_detector/rune_detector.cpp | 497 ++++ .../auto_buff/rune_detector/rune_detector.hpp | 24 + .../motion_models/motion_modelrypd.hpp | 71 + .../auto_buff/rune_tracker/rune_target.cpp | 336 +++ .../auto_buff/rune_tracker/rune_target.hpp | 230 ++ .../auto_buff/rune_tracker/rune_tracker.cpp | 121 + .../auto_buff/rune_tracker/rune_tracker.hpp | 20 + .../auto_buff/rune_tracker/spd_fitter.hpp | 265 ++ .../tasks/auto_buff/rune_where/rune_where.cpp | 213 ++ .../tasks/auto_buff/rune_where/rune_where.hpp | 39 + wust_vision-main/tasks/auto_buff/type.cpp | 339 +++ wust_vision-main/tasks/auto_buff/type.hpp | 124 + .../tasks/auto_guidance/auto_guidance.cpp | 198 ++ .../tasks/auto_guidance/auto_guidance.hpp | 24 + .../tasks/auto_guidance/debug.cpp | 295 ++ .../tasks/auto_guidance/debug.hpp | 24 + .../guidance_detector/detector_base.hpp | 18 + .../guidance_detector/detector_factory.hpp | 28 + .../guidance_detector/green_light_infer.cpp | 100 + .../guidance_detector/green_light_infer.hpp | 35 + .../opencv/guidance_detector_opencv.cpp | 162 ++ .../opencv/guidance_detector_opencv.hpp | 18 + .../openvino/guidance_detector_openvino.cpp | 116 + .../openvino/guidance_detector_openvino.hpp | 19 + .../guidance_tracker/guidance_target.cpp | 143 + .../guidance_tracker/guidance_target.hpp | 89 + .../guidance_tracker/guidance_tracker.cpp | 109 + .../guidance_tracker/guidance_tracker.hpp | 21 + .../motion_models/imgbox_model.hpp | 45 + wust_vision-main/tasks/auto_guidance/type.hpp | 107 + .../tasks/auto_sniper/auto_sniper.cpp | 344 +++ .../tasks/auto_sniper/auto_sniper.hpp | 32 + .../tasks/auto_sniper/k1_solver.hpp | 106 + .../tasks/auto_sniper/offset_helper.hpp | 87 + .../tasks/auto_sniper/voxel_map.hpp | 348 +++ wust_vision-main/tasks/imodule.hpp | 16 + wust_vision-main/tasks/packet_typedef.hpp | 193 ++ wust_vision-main/tasks/type_common.cpp | 66 + wust_vision-main/tasks/type_common.hpp | 310 +++ wust_vision-main/tasks/utils/ascii_banner.hpp | 64 + wust_vision-main/tasks/utils/config.hpp | 11 + wust_vision-main/tasks/utils/debug_utils.hpp | 154 ++ wust_vision-main/tasks/utils/main_base.hpp | 99 + .../tasks/utils/sinple_img_rotate_saver.hpp | 160 ++ wust_vision-main/tasks/utils/utils.cpp | 536 ++++ wust_vision-main/tasks/utils/utils.hpp | 213 ++ wust_vision-main/tasks/vision_base.hpp | 622 +++++ wust_vision-main/templates/index.html | 302 ++ wust_vision-main/test/nav.cpp | 67 + wust_vision-main/test/test_usbcamera.cpp | 19 + wust_vision-main/user_bashrc_copy.bash | 259 ++ wust_vision-main/web.py | 263 ++ 192 files changed, 29571 insertions(+), 9 deletions(-) create mode 100644 src/rm_auto_aim/armor_solver/include/armor_solver/trajectory_planner.hpp create mode 100644 src/rm_auto_aim/armor_solver/src/trajectory_planner.cpp create mode 100644 wust_vision-main/.clang-format create mode 100644 wust_vision-main/.clangd create mode 100644 wust_vision-main/.gitignore create mode 100644 wust_vision-main/.gitmodules create mode 100644 wust_vision-main/3rdparty/angles.h create mode 100644 wust_vision-main/3rdparty/ankerl/stl.h create mode 100644 wust_vision-main/3rdparty/ankerl/unordered_dense.h create mode 100644 wust_vision-main/CMakeLists.txt create mode 100644 wust_vision-main/README.md create mode 100644 wust_vision-main/cmake/FindHikSDK.cmake create mode 100644 wust_vision-main/cmake/FindOrt.cmake create mode 100644 wust_vision-main/cmake/FindTensorRT.cmake create mode 100644 wust_vision-main/config/auto_aim.yaml create mode 100644 wust_vision-main/config/auto_buff.yaml create mode 100644 wust_vision-main/config/auto_guidance.yaml create mode 100644 wust_vision-main/config/auto_sniper.yaml create mode 100644 wust_vision-main/config/camera.yaml create mode 100644 wust_vision-main/config/camera_info.yaml create mode 100644 wust_vision-main/config/common.yaml create mode 100644 wust_vision-main/config/detect_ml.yaml create mode 100644 wust_vision-main/config/detect_opencv.yaml create mode 100755 wust_vision-main/config/guard.sh create mode 100644 wust_vision-main/config/omni/camera0.yaml create mode 100644 wust_vision-main/config/omni/camera1.yaml create mode 100644 wust_vision-main/config/omni/camera_info.yaml create mode 100644 wust_vision-main/config/omni/detect_ml.yaml create mode 100644 wust_vision-main/config/omni/detect_opencv.yaml create mode 100644 wust_vision-main/config/omni/omni.yaml create mode 100644 wust_vision-main/cuda_infer/CMakeLists.txt create mode 100644 wust_vision-main/cuda_infer/armor_infer.cu create mode 100644 wust_vision-main/cuda_infer/armor_infer.hpp create mode 100644 wust_vision-main/cuda_infer/letter_box.cu create mode 100644 wust_vision-main/cuda_infer/letter_box.hpp create mode 100644 wust_vision-main/env.bash create mode 100755 wust_vision-main/format.sh create mode 100644 wust_vision-main/model/0526.engine create mode 100644 wust_vision-main/model/0526.onnx create mode 100644 wust_vision-main/model/0708.engine create mode 100644 wust_vision-main/model/0708.onnx create mode 100644 wust_vision-main/model/label.txt create mode 100644 wust_vision-main/model/lenet.onnx create mode 100644 wust_vision-main/model/mlp.onnx create mode 100644 wust_vision-main/model/opt-0527-001.onnx create mode 100644 wust_vision-main/model/opt-1208-001.bin create mode 100644 wust_vision-main/model/opt-1208-001.engine create mode 100644 wust_vision-main/model/opt-1208-001.onnx create mode 100644 wust_vision-main/model/opt-1208-001.param create mode 100644 wust_vision-main/model/opt_1208_001.ncnn.bin create mode 100644 wust_vision-main/model/opt_1208_001.ncnn.param create mode 100644 wust_vision-main/model/reborn_number_classifier.engine create mode 100644 wust_vision-main/model/reborn_number_classifier.onnx create mode 100644 wust_vision-main/model/tiny_resnet.onnx create mode 100644 wust_vision-main/model/yolox_armor3.onnx create mode 100644 wust_vision-main/read_shm_image_mmap_only.py create mode 100644 wust_vision-main/ros2/ros2.hpp create mode 100644 wust_vision-main/ros2/tf.hpp create mode 100755 wust_vision-main/run.sh create mode 100755 wust_vision-main/script/install_depences.sh create mode 100755 wust_vision-main/script/rsync.sh create mode 100755 wust_vision-main/script/setup_devenv.sh create mode 100755 wust_vision-main/script/setup_service.sh create mode 100755 wust_vision-main/script/setup_service_sentry.sh create mode 100755 wust_vision-main/script/setup_udev.sh create mode 100644 wust_vision-main/src/dart.cpp create mode 100644 wust_vision-main/src/hero.cpp create mode 100644 wust_vision-main/src/sentry.cpp create mode 100644 wust_vision-main/src/sim.cpp create mode 100644 wust_vision-main/src/standard.cpp create mode 100644 wust_vision-main/static/css/style.css create mode 100644 wust_vision-main/static/js/chart_logic.js create mode 100644 wust_vision-main/static/js/json_view.js create mode 100644 wust_vision-main/static/js/main.js create mode 100644 wust_vision-main/static/logo.JPG create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/tinympc/CMakeLists.txt create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/tinympc/admm.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/tinympc/admm.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/tinympc/codegen.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/tinympc/codegen.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/tinympc/error.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/tinympc/rho_benchmark.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/tinympc/rho_benchmark.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/tinympc/tiny_api.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/tinympc/tiny_api.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/tinympc/tiny_api_constants.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/tinympc/types.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/traj.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/very_aimer.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_control/very_aimer.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/armor_detector_base.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/armor_detector_common.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/armor_detector_common.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/armor_detector_factory.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/armor_infer.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/armor_infer.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/ncnn/armor_detector_ncnn.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/ncnn/armor_detector_ncnn.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/number_classifier/base.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/number_classifier/factory.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/number_classifier/number_classifier.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/number_classifier/number_classifier.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/number_classifier/number_classifier_trt.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/number_classifier/number_classifier_trt.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/onnxruntime/armor_detector_onnxruntime.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/onnxruntime/armor_detector_onnxruntime.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/opencv/armor_detector_opencv.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/opencv/armor_detector_opencv.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/openvino/armor_detector_openvino.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/openvino/armor_detector_openvino.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/tensorrt/armor_detector_tensorrt.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_detect/tensorrt/armor_detector_tensorrt.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_omni/armor_omni.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_omni/armor_omni.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_tracker/motion_models/motion_model_crazy.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_tracker/motion_models/motion_modelypdv2.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_tracker/target.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_tracker/target.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_tracker/trackerv3.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_tracker/trackerv3.hpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_where/armor_where.cpp create mode 100644 wust_vision-main/tasks/auto_aim/armor_where/armor_where.hpp create mode 100644 wust_vision-main/tasks/auto_aim/auto_aim.cpp create mode 100644 wust_vision-main/tasks/auto_aim/auto_aim.hpp create mode 100644 wust_vision-main/tasks/auto_aim/auto_aim_fsm.hpp create mode 100644 wust_vision-main/tasks/auto_aim/debug.cpp create mode 100644 wust_vision-main/tasks/auto_aim/debug.hpp create mode 100644 wust_vision-main/tasks/auto_aim/type.cpp create mode 100644 wust_vision-main/tasks/auto_aim/type.hpp create mode 100644 wust_vision-main/tasks/auto_buff/auto_buff.cpp create mode 100644 wust_vision-main/tasks/auto_buff/auto_buff.hpp create mode 100644 wust_vision-main/tasks/auto_buff/debug.cpp create mode 100644 wust_vision-main/tasks/auto_buff/debug.hpp create mode 100644 wust_vision-main/tasks/auto_buff/rune_control/aimer.cpp create mode 100644 wust_vision-main/tasks/auto_buff/rune_control/aimer.hpp create mode 100644 wust_vision-main/tasks/auto_buff/rune_detector/rune_detector.cpp create mode 100644 wust_vision-main/tasks/auto_buff/rune_detector/rune_detector.hpp create mode 100644 wust_vision-main/tasks/auto_buff/rune_tracker/motion_models/motion_modelrypd.hpp create mode 100644 wust_vision-main/tasks/auto_buff/rune_tracker/rune_target.cpp create mode 100644 wust_vision-main/tasks/auto_buff/rune_tracker/rune_target.hpp create mode 100644 wust_vision-main/tasks/auto_buff/rune_tracker/rune_tracker.cpp create mode 100644 wust_vision-main/tasks/auto_buff/rune_tracker/rune_tracker.hpp create mode 100644 wust_vision-main/tasks/auto_buff/rune_tracker/spd_fitter.hpp create mode 100644 wust_vision-main/tasks/auto_buff/rune_where/rune_where.cpp create mode 100644 wust_vision-main/tasks/auto_buff/rune_where/rune_where.hpp create mode 100644 wust_vision-main/tasks/auto_buff/type.cpp create mode 100644 wust_vision-main/tasks/auto_buff/type.hpp create mode 100644 wust_vision-main/tasks/auto_guidance/auto_guidance.cpp create mode 100644 wust_vision-main/tasks/auto_guidance/auto_guidance.hpp create mode 100644 wust_vision-main/tasks/auto_guidance/debug.cpp create mode 100644 wust_vision-main/tasks/auto_guidance/debug.hpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_detector/detector_base.hpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_detector/detector_factory.hpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_detector/green_light_infer.cpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_detector/green_light_infer.hpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_detector/opencv/guidance_detector_opencv.cpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_detector/opencv/guidance_detector_opencv.hpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_detector/openvino/guidance_detector_openvino.cpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_detector/openvino/guidance_detector_openvino.hpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_tracker/guidance_target.cpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_tracker/guidance_target.hpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_tracker/guidance_tracker.cpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_tracker/guidance_tracker.hpp create mode 100644 wust_vision-main/tasks/auto_guidance/guidance_tracker/motion_models/imgbox_model.hpp create mode 100644 wust_vision-main/tasks/auto_guidance/type.hpp create mode 100644 wust_vision-main/tasks/auto_sniper/auto_sniper.cpp create mode 100644 wust_vision-main/tasks/auto_sniper/auto_sniper.hpp create mode 100644 wust_vision-main/tasks/auto_sniper/k1_solver.hpp create mode 100644 wust_vision-main/tasks/auto_sniper/offset_helper.hpp create mode 100644 wust_vision-main/tasks/auto_sniper/voxel_map.hpp create mode 100644 wust_vision-main/tasks/imodule.hpp create mode 100644 wust_vision-main/tasks/packet_typedef.hpp create mode 100644 wust_vision-main/tasks/type_common.cpp create mode 100644 wust_vision-main/tasks/type_common.hpp create mode 100644 wust_vision-main/tasks/utils/ascii_banner.hpp create mode 100644 wust_vision-main/tasks/utils/config.hpp create mode 100644 wust_vision-main/tasks/utils/debug_utils.hpp create mode 100644 wust_vision-main/tasks/utils/main_base.hpp create mode 100644 wust_vision-main/tasks/utils/sinple_img_rotate_saver.hpp create mode 100644 wust_vision-main/tasks/utils/utils.cpp create mode 100644 wust_vision-main/tasks/utils/utils.hpp create mode 100644 wust_vision-main/tasks/vision_base.hpp create mode 100644 wust_vision-main/templates/index.html create mode 100644 wust_vision-main/test/nav.cpp create mode 100644 wust_vision-main/test/test_usbcamera.cpp create mode 100644 wust_vision-main/user_bashrc_copy.bash create mode 100644 wust_vision-main/web.py diff --git a/src/rm_auto_aim/armor_solver/include/armor_solver/armor_solver.hpp b/src/rm_auto_aim/armor_solver/include/armor_solver/armor_solver.hpp index 373da29..caede5c 100644 --- a/src/rm_auto_aim/armor_solver/include/armor_solver/armor_solver.hpp +++ b/src/rm_auto_aim/armor_solver/include/armor_solver/armor_solver.hpp @@ -32,6 +32,7 @@ #include "rm_interfaces/msg/target.hpp" #include "rm_utils/math/manual_compensator.hpp" #include "rm_utils/math/trajectory_compensator.hpp" +#include "armor_solver/trajectory_planner.hpp" namespace fyt::auto_aim { // Solver class used to solve the gimbal command from tracked target @@ -53,6 +54,7 @@ public: const std::vector& getArmorPositions() const noexcept { return cached_armor_positions_; } + TrajectoryDebug getTrajectoryDebug() const noexcept; void setBulletSpeed(double bullet_speed) noexcept; void updateRuntimeParams(double max_tracking_v_yaw, double prediction_delay, @@ -61,6 +63,7 @@ public: double min_switching_v_yaw, double shooting_range_w, double shooting_range_h) noexcept; + void setTrajectoryType(const std::string& type, std::weak_ptr node = {}); private: // Get the armor positions from the target robot @@ -97,6 +100,8 @@ private: std::unique_ptr trajectory_compensator_; std::unique_ptr manual_compensator_; + std::unique_ptr planner_; + TrajectoryPlanner::Type planner_type_ = TrajectoryPlanner::Type::LINEAR; std::array rpy_; diff --git a/src/rm_auto_aim/armor_solver/include/armor_solver/armor_solver_node.hpp b/src/rm_auto_aim/armor_solver/include/armor_solver/armor_solver_node.hpp index 12943f5..5a81d41 100644 --- a/src/rm_auto_aim/armor_solver/include/armor_solver/armor_solver_node.hpp +++ b/src/rm_auto_aim/armor_solver/include/armor_solver/armor_solver_node.hpp @@ -30,6 +30,7 @@ #include #include #include +#include #include // std #include @@ -64,6 +65,7 @@ private: void publishMarkers(const rm_interfaces::msg::Target &target_msg, const rm_interfaces::msg::GimbalCmd &gimbal_cmd) noexcept; + void publishTrajectoryDebug() noexcept; void setModeCallback(const std::shared_ptr request, @@ -107,6 +109,7 @@ private: // Publisher rclcpp::Publisher::SharedPtr target_pub_; rclcpp::Publisher::SharedPtr gimbal_pub_; + rclcpp::Publisher::SharedPtr traj_debug_pub_; rclcpp::Subscription::SharedPtr serial_sub_; rclcpp::TimerBase::SharedPtr pub_timer_; void timerCallback(); diff --git a/src/rm_auto_aim/armor_solver/include/armor_solver/trajectory_planner.hpp b/src/rm_auto_aim/armor_solver/include/armor_solver/trajectory_planner.hpp new file mode 100644 index 0000000..f6e575e --- /dev/null +++ b/src/rm_auto_aim/armor_solver/include/armor_solver/trajectory_planner.hpp @@ -0,0 +1,426 @@ +// Trajectory Planner for armor_solver +// Implements quintic polynomial trajectory planning and TinyMPC-based MPC + +#ifndef ARMOR_SOLVER_TRAJECTORY_PLANNER_HPP_ +#define ARMOR_SOLVER_TRAJECTORY_PLANNER_HPP_ + +#include +#include +#include +#include +#include +#include +#include + +namespace fyt::auto_aim { + +// Forward declare for use in planner +struct TargetInfo { + Eigen::Vector3d position; + Eigen::Vector3d velocity; + double yaw; + double v_yaw; + double radius_1; + double radius_2; + double d_za; + double d_zc; + size_t armors_num; + + // Default constructor + TargetInfo() = default; + + // Construct from rm_interfaces Target msg + explicit TargetInfo(const rm_interfaces::msg::Target& target_msg) + : position(target_msg.position.x, target_msg.position.y, target_msg.position.z), + velocity(target_msg.velocity.x, target_msg.velocity.y, target_msg.velocity.z), + yaw(target_msg.yaw), + v_yaw(target_msg.v_yaw), + radius_1(target_msg.radius_1), + radius_2(target_msg.radius_2), + d_za(target_msg.d_za), + d_zc(target_msg.d_zc), + armors_num(static_cast(target_msg.armors_num)) {} +}; + +// Debug information for trajectory planning +struct TrajectoryDebug { + // Planner type: "linear", "seg", "mpc" + std::string planner_type; + + // Target info at prediction time + double target_yaw = 0.0; + double target_pitch = 0.0; + double target_distance = 0.0; + + // Planned gimbal state + double planned_yaw = 0.0; + double planned_pitch = 0.0; + double planned_yaw_v = 0.0; + double planned_pitch_v = 0.0; + double planned_yaw_a = 0.0; + double planned_pitch_a = 0.0; + + // Time parameters + double flying_time = 0.0; + double total_dt = 0.0; + + // Trajectory points for visualization (yaw trajectory) + std::vector traj_yaw_p; + std::vector traj_yaw_v; + std::vector traj_time; + + // Constraints + double max_yaw_acc = 0.0; + double max_pitch_acc = 0.0; + + // MPC specific + double mpc_cost = 0.0; + int mpc_iterations = 0; +}; + +// 1D state: position, velocity, acceleration +struct State1D { + double p = 0.0; + double v = 0.0; + double a = 0.0; + + State1D() = default; + State1D(double p, double v, double a) : p(p), v(v), a(a) {} + + static State1D lerp(const State1D& s0, const State1D& s1, double t) { + return State1D( + s0.p + t * (s1.p - s0.p), + s0.v + t * (s1.v - s0.v), + s0.a + t * (s1.a - s0.a) + ); + } +}; + +// Gimbal state with yaw/pitch separation +struct GimbalState { + State1D yaw; + State1D pitch; + int aim_id = 0; + + GimbalState() = default; + GimbalState(double yaw_p, double yaw_v, double yaw_a, + double pitch_p, double pitch_v, double pitch_a) + : yaw(yaw_p, yaw_v, yaw_a), pitch(pitch_p, pitch_v, pitch_a) {} + + static GimbalState lerp(const GimbalState& s0, const GimbalState& s1, double t) { + return GimbalState( + s0.yaw.p + t * (s1.yaw.p - s0.yaw.p), + s0.yaw.v + t * (s1.yaw.v - s0.yaw.v), + s0.yaw.a + t * (s1.yaw.a - s0.yaw.a), + s0.pitch.p + t * (s1.pitch.p - s0.pitch.p), + s0.pitch.v + t * (s1.pitch.v - s0.pitch.v), + s0.pitch.a + t * (s1.pitch.a - s0.pitch.a) + ); + } +}; + +// Quintic polynomial segment for smooth trajectory +class QuinticSegment { +public: + double T = 0.0; // Duration + Eigen::Matrix c; // Coefficients: c0 + c1*t + c2*t^2 + c3*t^3 + c4*t^4 + c5*t^5 + + QuinticSegment() = default; + explicit QuinticSegment(double duration) : T(duration) {} + + // Build quintic segment from boundary conditions (closed-form solution) + static QuinticSegment build(const State1D& s0, const State1D& s1, double T) { + using Matrix6d = Eigen::Matrix; + + double T2 = T * T; + double T3 = T2 * T; + double T4 = T3 * T; + double T5 = T4 * T; + + Matrix6d A; + A << 1, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, + 0, 0, 2, 0, 0, 0, + 1, T, T2, T3, T4, T5, + 0, 1, 2*T, 3*T2, 4*T3, 5*T4, + 0, 0, 2, 6*T, 12*T2, 20*T3; + + Eigen::Matrix b; + b << s0.p, s0.v, s0.a, s1.p, s1.v, s1.a; + + QuinticSegment seg(T); + seg.c = A.fullPivLu().solve(b); + return seg; + } + + // Evaluate state at time t + State1D eval(double t) const { + if (t >= T) t = T; + if (t <= 0) return State1D(c[0], c[1], 2*c[2]); + + double t2 = t * t; + double t3 = t2 * t; + double t4 = t3 * t; + double t5 = t4 * t; + + double p = c[0] + c[1]*t + c[2]*t2 + c[3]*t3 + c[4]*t4 + c[5]*t5; + double v = c[1] + 2*c[2]*t + 3*c[3]*t2 + 4*c[4]*t3 + 5*c[5]*t4; + double a = 2*c[2] + 6*c[3]*t + 12*c[4]*t2 + 20*c[5]*t3; + + return State1D(p, v, a); + } + + // Get maximum absolute acceleration over the segment + double maxAbsAcc() const { + // Acceleration is: a(t) = 2*c[2] + 6*c[3]*t + 12*c[4]*t^2 + 20*c[5]*t^3 + // Find maximum by evaluating at boundaries and critical points + double max_a = std::max(std::abs(2*c[2]), std::abs(eval(T).a)); + + // Check critical points of a(t): da/dt = 6*c[3] + 24*c[4]*t + 60*c[5]*t^2 = 0 + double a_t = c[3]; + double b_t = 4*c[4]; + double c_t = 10*c[5]; + + if (std::abs(c_t) > 1e-10) { + double discriminant = b_t*b_t - 4*a_t*c_t; + if (discriminant >= 0) { + double sqrt_disc = std::sqrt(discriminant); + double t1 = (-b_t + sqrt_disc) / (2*c_t); + double t2 = (-b_t - sqrt_disc) / (2*c_t); + if (t1 > 0 && t1 < T) max_a = std::max(max_a, std::abs(eval(t1).a)); + if (t2 > 0 && t2 < T) max_a = std::max(max_a, std::abs(eval(t2).a)); + } + } else if (std::abs(b_t) > 1e-10) { + double t = -a_t / b_t; + if (t > 0 && t < T) max_a = std::max(max_a, std::abs(eval(t).a)); + } + + return max_a; + } +}; + +// Trajectory container template +template +class Trajectory { +public: + static_assert(std::is_same_v || std::is_same_v, + "Trajectory must be used with GimbalState or State1D"); + + void reserve(size_t n) { + cp_vec_.reserve(n); + dt_vec_.reserve(n > 0 ? n - 1 : 0); + prefix_time_.reserve(n); + } + + void clear() { + cp_vec_.clear(); + dt_vec_.clear(); + prefix_time_.clear(); + total_duration_ = 0.0; + } + + void push_back(const T& p, double dt = 0.0) { + if (cp_vec_.empty()) { + cp_vec_.push_back(p); + prefix_time_.push_back(0.0); + total_duration_ = 0.0; + return; + } + + assert(dt >= 0.0); + + cp_vec_.push_back(p); + dt_vec_.push_back(dt); + total_duration_ += dt; + prefix_time_.push_back(total_duration_); + } + + void set(const std::vector& c, const std::vector& t) { + assert(!c.empty()); + assert(c.size() == t.size() + 1); + + cp_vec_ = c; + dt_vec_ = t; + + prefix_time_.resize(cp_vec_.size()); + prefix_time_[0] = 0.0; + + for (size_t i = 0; i < dt_vec_.size(); ++i) + prefix_time_[i + 1] = prefix_time_[i] + dt_vec_[i]; + + total_duration_ = prefix_time_.back(); + } + + T getStateAtTime(double t) const { + if (cp_vec_.empty()) + return T {}; + + if (t <= 0.0) + return cp_vec_.front(); + + if (t >= total_duration_) + return cp_vec_.back(); + + auto it = std::lower_bound(prefix_time_.begin(), prefix_time_.end(), t); + size_t i1 = std::distance(prefix_time_.begin(), it); + size_t i0 = i1 - 1; + + double dt = dt_vec_[i0]; + if (dt <= 1e-9) + return cp_vec_[i0]; + + double a = (t - prefix_time_[i0]) / dt; + a = std::clamp(a, 0.0, 1.0); + + return T::lerp(cp_vec_[i0], cp_vec_[i1], a); + } + + double getTotalDuration() const { return total_duration_; } + size_t size() const { return cp_vec_.size(); } + + const std::vector& controlPoints() const { return cp_vec_; } + const std::vector& timeSteps() const { return dt_vec_; } + const std::vector& prefixTimes() const { return prefix_time_; } + +protected: + std::vector cp_vec_; + std::vector dt_vec_; + std::vector prefix_time_; + double total_duration_ = 0.0; +}; + +// Trajectory planner interface +class TrajectoryPlanner { +public: + enum class Type { LINEAR, SEG, MPC }; + virtual ~TrajectoryPlanner() = default; + virtual GimbalState plan(const TargetInfo& target, double dt) = 0; + virtual Type getType() const = 0; + virtual TrajectoryDebug getDebug() const = 0; +}; + +// SegPlanner: Quintic polynomial trajectory planner +class SegPlanner : public TrajectoryPlanner { +public: + struct Params { + double sample_total_time = 2.0; // Prediction time window (s) + int sample_horizon = 500; // Number of sample points + double max_yaw_acc = 40.0; // Max yaw acceleration (deg/s^2) + double max_pitch_acc = 25.0; // Max pitch acceleration (deg/s^2) + }; + + explicit SegPlanner(const Params& params) : params_(params) {} + ~SegPlanner() override = default; + + GimbalState plan(const TargetInfo& target, double dt) override; + Type getType() const override { return Type::SEG; } + TrajectoryDebug getDebug() const override { return debug_; } + + void setParams(const Params& params) { params_ = params; } + const Params& getParams() const { return params_; } + +private: + // Predict target state at time t + GimbalState predictTarget(const TargetInfo& target, double t) const; + + // Unwrap angle discontinuities + void unwrapAngles(std::vector& states) const; + + // Compute velocity and acceleration at control points + std::pair, std::vector> + computeNodeStates(const std::vector& states, + const std::vector& dt_vec) const; + + // Build limited quintic segments + Trajectory + buildLimit(const std::vector& yaw_nodes, + const std::vector& pitch_nodes, + double max_yaw_acc, + double max_pitch_acc) const; + + // Convert gimbal angle to target angle (accounting for armor offset) + static std::pair + computeArmorAngle(const Eigen::Vector3d& target_pos, + const Eigen::Vector3d& target_center, + double target_yaw, + size_t armors_num, + double radius_1, + double radius_2, + double d_zc, + double d_za); + + Params params_; + Trajectory trajectory_; + TrajectoryDebug debug_; +}; + +// MpcPlanner: Simplified MPC-based trajectory planner +// Uses feedback control with acceleration constraints +class MpcPlanner : public TrajectoryPlanner { +public: + struct Params { + double sample_total_time = 2.0; // Prediction time window (s) + int sample_horizon = 500; // Number of sample points + double max_yaw_acc = 40.0; // Max yaw acceleration (deg/s^2) + double max_pitch_acc = 25.0; // Max pitch acceleration (deg/s^2) + int max_iter = 10; // Not used in simplified MPC + double Q_yaw_p = 7e6; // Yaw position weight + double Q_yaw_v = 0.0; // Yaw velocity weight + double R_yaw = 3.0; // Yaw control weight + double rho = 1.0; // Not used in simplified MPC + }; + + explicit MpcPlanner(const Params& params); + ~MpcPlanner() override; + + GimbalState plan(const TargetInfo& target, double dt) override; + Type getType() const override { return Type::MPC; } + TrajectoryDebug getDebug() const override { return debug_; } + + void setParams(const Params& params); + const Params& getParams() const { return params_; } + +private: + // Initialize MPC solver + void initSolver(); + + // Setup problem matrices + void setupProblem(); + + // Solve MPC and get trajectory + void solveMpc(const std::vector& ref_traj); + + // Get state at specific time from MPC solution + GimbalState getStateAtTime(double t, const std::vector& ref_traj) const; + + Params params_; + int N_ = 50; + + // State and input matrices + Eigen::MatrixXd x_; // State trajectory (2 x N) + Eigen::MatrixXd u_; // Control trajectory (1 x N-1) + Eigen::MatrixXd x_ref_; // Reference trajectory (2 x N) + Eigen::MatrixXd u_ref_; // Reference control (1 x N-1) + + // System dynamics + Eigen::MatrixXd Adyn_; // State transition matrix (2 x 2) + Eigen::MatrixXd Bdyn_; // Input matrix (2 x 1) + + // Constraints + Eigen::VectorXd u_min_; // Control bounds + Eigen::VectorXd u_max_; + + // Cost weights + Eigen::Vector2d Q_; // State cost weights + Eigen::VectorXd R_; // Control cost weights + + // Solution storage + std::vector mpc_solution_yaw_; + + // Debug info + TrajectoryDebug debug_; +}; + +} // namespace fyt::auto_aim + +#endif // ARMOR_SOLVER_TRAJECTORY_PLANNER_HPP_ diff --git a/src/rm_auto_aim/armor_solver/src/armor_solver.cpp b/src/rm_auto_aim/armor_solver/src/armor_solver.cpp index 21b4ebf..c74562a 100644 --- a/src/rm_auto_aim/armor_solver/src/armor_solver.cpp +++ b/src/rm_auto_aim/armor_solver/src/armor_solver.cpp @@ -53,6 +53,10 @@ Solver::Solver(std::weak_ptr n) : node_(n) { FYT_WARN("armor_solver", "Manual compensator update failed!"); } + // Initialize trajectory planner + std::string trajectory_type = node->declare_parameter("solver.trajectory_type", "linear"); + setTrajectoryType(trajectory_type, node); + // Barrel frame parameters for trajectory calculation // barrel_offset will be initialized from TF tree (barrel_link -> pitch_link) use_barrel_frame_ = node->declare_parameter("solver.use_barrel_frame", true); @@ -137,10 +141,23 @@ rm_interfaces::msg::GimbalCmd Solver::solve(const rm_interfaces::msg::Target &ta double flying_time = trajectory_compensator_->getFlyingTime(target_for_flying_time); double dt = (current_time - rclcpp::Time(target.header.stamp)).seconds() + flying_time + prediction_delay_; - target_position.x() += dt * target.velocity.x; - target_position.y() += dt * target.velocity.y; - target_position.z() += dt * target.velocity.z; - target_yaw += dt * target.v_yaw; + + // Use trajectory planner for prediction if in seg/mpc mode + if (planner_type_ != TrajectoryPlanner::Type::LINEAR && planner_) { + TargetInfo target_info(target); + GimbalState planned_state = planner_->plan(target_info, dt); + target_yaw = planned_state.yaw.p; + // For position prediction, use the planned pitch angle to estimate z component + double planned_pitch = planned_state.pitch.p; + double horizontal_dist = target_position.head(2).norm(); + target_position.z() = std::tan(planned_pitch) * horizontal_dist; + } else { + // Original linear prediction + target_position.x() += dt * target.velocity.x; + target_position.y() += dt * target.velocity.y; + target_position.z() += dt * target.velocity.z; + target_yaw += dt * target.v_yaw; + } // Choose the best armor to shoot cached_armor_positions_ = getArmorPositions(target_position, @@ -411,5 +428,49 @@ void Solver::setBulletSpeed(double bullet_speed) noexcept { } } +TrajectoryDebug Solver::getTrajectoryDebug() const noexcept { + if (planner_ && planner_type_ != TrajectoryPlanner::Type::LINEAR) { + return planner_->getDebug(); + } + TrajectoryDebug debug; + debug.planner_type = "linear"; + return debug; +} + +void Solver::setTrajectoryType(const std::string& type, std::weak_ptr node) { + auto node_ptr = node.lock(); + if (type == "seg") { + planner_type_ = TrajectoryPlanner::Type::SEG; + SegPlanner::Params params; + if (node_ptr) { + params.sample_total_time = node_ptr->declare_parameter("solver.sample_total_time", 2.0); + params.sample_horizon = node_ptr->declare_parameter("solver.sample_horizon", 500); + params.max_yaw_acc = node_ptr->declare_parameter("solver.max_yaw_acc", 40.0); + params.max_pitch_acc = node_ptr->declare_parameter("solver.max_pitch_acc", 25.0); + } + planner_ = std::make_unique(params); + FYT_INFO("armor_solver", "Trajectory planner set to SEG (quintic polynomial)"); + } else if (type == "mpc") { + planner_type_ = TrajectoryPlanner::Type::MPC; + MpcPlanner::Params params; + if (node_ptr) { + params.sample_total_time = node_ptr->declare_parameter("solver.sample_total_time", 2.0); + params.sample_horizon = node_ptr->declare_parameter("solver.sample_horizon", 500); + params.max_yaw_acc = node_ptr->declare_parameter("solver.max_yaw_acc", 40.0); + params.max_pitch_acc = node_ptr->declare_parameter("solver.max_pitch_acc", 25.0); + params.max_iter = node_ptr->declare_parameter("solver.max_iter", 10); + params.Q_yaw_p = node_ptr->declare_parameter("solver.Q_yaw", 7e6); + params.Q_yaw_v = node_ptr->declare_parameter("solver.Q_yaw_v", 0.0); + params.R_yaw = node_ptr->declare_parameter("solver.R_yaw", 3.0); + } + planner_ = std::make_unique(params); + FYT_INFO("armor_solver", "Trajectory planner set to MPC (TinyMPC ADMM)"); + } else { + planner_type_ = TrajectoryPlanner::Type::LINEAR; + planner_.reset(); + FYT_INFO("armor_solver", "Trajectory planner set to LINEAR (default)"); + } +} + } // namespace fyt::auto_aim diff --git a/src/rm_auto_aim/armor_solver/src/armor_solver_node.cpp b/src/rm_auto_aim/armor_solver/src/armor_solver_node.cpp index 6f465d8..ff035f6 100644 --- a/src/rm_auto_aim/armor_solver/src/armor_solver_node.cpp +++ b/src/rm_auto_aim/armor_solver/src/armor_solver_node.cpp @@ -19,7 +19,9 @@ #include "armor_solver/armor_solver_node.hpp" // std +#include #include +#include #include // project #include "armor_solver/motion_model.hpp" @@ -182,6 +184,8 @@ ArmorSolverNode::ArmorSolverNode(const rclcpp::NodeOptions &options) rclcpp::SensorDataQoS()); gimbal_pub_ = this->create_publisher("armor_solver/cmd_gimbal", rclcpp::SensorDataQoS()); + traj_debug_pub_ = this->create_publisher("armor_solver/traj_debug", + rclcpp::SensorDataQoS()); serial_sub_ = this->create_subscription( "serial/receive", rclcpp::SensorDataQoS(), @@ -293,6 +297,7 @@ void ArmorSolverNode::timerCallback() { if (debug_mode_) { publishMarkers(armor_target_, control_msg); + publishTrajectoryDebug(); } } @@ -586,6 +591,76 @@ void ArmorSolverNode::publishMarkers(const rm_interfaces::msg::Target &target_ms marker_pub_->publish(marker_array); } +void ArmorSolverNode::publishTrajectoryDebug() noexcept { + if (!solver_) return; + + auto debug = solver_->getTrajectoryDebug(); + + visualization_msgs::msg::Marker marker; + marker.header.stamp = this->now(); + marker.header.frame_id = "odom"; + marker.type = visualization_msgs::msg::Marker::LINE_STRIP; + marker.action = visualization_msgs::msg::Marker::ADD; + marker.ns = "trajectory_debug"; + marker.id = 0; + marker.scale.x = 0.02; // line width + + // Color based on planner type + if (debug.planner_type == "seg") { + marker.color.r = 0.0; + marker.color.g = 1.0; + marker.color.b = 0.0; + } else if (debug.planner_type == "mpc") { + marker.color.r = 1.0; + marker.color.g = 0.0; + marker.color.b = 1.0; + } else { + marker.color.r = 1.0; + marker.color.g = 1.0; + marker.color.b = 0.0; + } + marker.color.a = 1.0; + + // Add trajectory points as points + for (size_t i = 0; i < debug.traj_time.size(); ++i) { + geometry_msgs::msg::Point p; + p.x = debug.traj_time[i]; // time on x-axis + p.y = debug.traj_yaw_p[i] * 180.0 / M_PI; // yaw in degrees on y-axis + p.z = 0.0; + marker.points.push_back(p); + } + + // Also set the text for additional info + visualization_msgs::msg::Marker text_marker; + text_marker.header.stamp = this->now(); + text_marker.header.frame_id = "odom"; + text_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; + text_marker.action = visualization_msgs::msg::Marker::ADD; + text_marker.ns = "trajectory_debug_text"; + text_marker.id = 0; + text_marker.pose.position.x = 0.0; + text_marker.pose.position.y = 0.0; + text_marker.pose.position.z = 0.5; + text_marker.scale.z = 0.1; // text height + + std::ostringstream ss; + ss << "Planner: " << debug.planner_type << "\n"; + ss << "Target Yaw: " << debug.target_yaw * 180.0 / M_PI << " deg\n"; + ss << "Target Pitch: " << debug.target_pitch * 180.0 / M_PI << " deg\n"; + ss << "Planned Yaw: " << debug.planned_yaw * 180.0 / M_PI << " deg\n"; + ss << "Planned Pitch: " << debug.planned_pitch * 180.0 / M_PI << " deg\n"; + ss << "Flying Time: " << debug.flying_time * 1000.0 << " ms\n"; + ss << "Max Yaw Acc: " << debug.max_yaw_acc << " deg/s^2\n"; + text_marker.text = ss.str(); + + text_marker.color.r = 1.0; + text_marker.color.g = 1.0; + text_marker.color.b = 1.0; + text_marker.color.a = 1.0; + + traj_debug_pub_->publish(marker); +} + void ArmorSolverNode::setModeCallback( const std::shared_ptr request, std::shared_ptr response) { diff --git a/src/rm_auto_aim/armor_solver/src/trajectory_planner.cpp b/src/rm_auto_aim/armor_solver/src/trajectory_planner.cpp new file mode 100644 index 0000000..614ec72 --- /dev/null +++ b/src/rm_auto_aim/armor_solver/src/trajectory_planner.cpp @@ -0,0 +1,441 @@ +// Trajectory Planner Implementation +// SegPlanner: Quintic polynomial trajectory planning +// MpcPlanner: TinyMPC ADMM-based trajectory planning + +#include "armor_solver/trajectory_planner.hpp" +#include "rm_utils/logger/log.hpp" +#include + +namespace fyt::auto_aim { + +// ============================================================================ +// SegPlanner Implementation +// ============================================================================ + +GimbalState SegPlanner::plan(const TargetInfo& target, double dt) { + int horizon = params_.sample_horizon; + double total_time = params_.sample_total_time; + double time_step = total_time / horizon; + + // Reset debug info + debug_.planner_type = "seg"; + debug_.traj_time.clear(); + debug_.traj_yaw_p.clear(); + debug_.traj_yaw_v.clear(); + debug_.max_yaw_acc = params_.max_yaw_acc; + debug_.max_pitch_acc = params_.max_pitch_acc; + debug_.total_dt = dt; + + // 1. Sample target states over prediction horizon + std::vector states; + states.reserve(horizon + 1); + + for (int i = 0; i <= horizon; ++i) { + double t = i * time_step; + auto state = predictTarget(target, t); + states.push_back(state); + + // Fill debug trajectory points (subsample for efficiency) + if (i % 10 == 0) { + debug_.traj_time.push_back(t); + debug_.traj_yaw_p.push_back(state.yaw.p); + debug_.traj_yaw_v.push_back(state.yaw.v); + } + } + + // 2. Unwrap angle discontinuities + unwrapAngles(states); + + // 3. Compute node velocities and accelerations + auto dt_vec = std::vector(horizon, time_step); + auto [yaw_nodes, pitch_nodes] = computeNodeStates(states, dt_vec); + + // 4. Build quintic segments for yaw and pitch + std::vector yaw_segs, pitch_segs; + yaw_segs.reserve(horizon); + pitch_segs.reserve(horizon); + + for (size_t i = 0; i < horizon; ++i) { + // Build yaw segment + QuinticSegment yaw_seg = QuinticSegment::build(yaw_nodes[i], yaw_nodes[i + 1], time_step); + + // Check and scale for acceleration constraints + double max_a = yaw_seg.maxAbsAcc(); + double max_acc_rad = params_.max_yaw_acc * M_PI / 180.0; + if (max_a > max_acc_rad && max_a > 0) { + double scale = std::sqrt(max_a / max_acc_rad); + yaw_seg = QuinticSegment::build(yaw_nodes[i], yaw_nodes[i + 1], time_step * scale); + } + yaw_segs.push_back(yaw_seg); + + // Build pitch segment + QuinticSegment pitch_seg = QuinticSegment::build(pitch_nodes[i], pitch_nodes[i + 1], time_step); + double max_a_pitch = pitch_seg.maxAbsAcc(); + double max_acc_pitch_rad = params_.max_pitch_acc * M_PI / 180.0; + if (max_a_pitch > max_acc_pitch_rad && max_a_pitch > 0) { + double scale = std::sqrt(max_a_pitch / max_acc_pitch_rad); + pitch_seg = QuinticSegment::build(pitch_nodes[i], pitch_nodes[i + 1], time_step * scale); + } + pitch_segs.push_back(pitch_seg); + } + + // 5. Evaluate at target time + double target_t = dt; + if (target_t > total_time) target_t = total_time; + + int seg_idx = static_cast(target_t / time_step); + if (seg_idx >= static_cast(yaw_segs.size())) seg_idx = yaw_segs.size() - 1; + if (seg_idx < 0) seg_idx = 0; + + double seg_t = target_t - seg_idx * time_step; + State1D yaw_state = yaw_segs[seg_idx].eval(seg_t); + State1D pitch_state = pitch_segs[seg_idx].eval(seg_t); + + // Fill debug info + Eigen::Vector3d target_pos = target.position + dt * target.velocity; + debug_.target_yaw = target.yaw + dt * target.v_yaw; + debug_.target_pitch = std::atan2(target_pos.z(), target_pos.head(2).norm()); + debug_.target_distance = target_pos.norm(); + debug_.planned_yaw = yaw_state.p; + debug_.planned_pitch = pitch_state.p; + debug_.planned_yaw_v = yaw_state.v; + debug_.planned_pitch_v = pitch_state.v; + debug_.planned_yaw_a = yaw_state.a; + debug_.planned_pitch_a = pitch_state.a; + debug_.flying_time = dt; + + return GimbalState(yaw_state.p, yaw_state.v, yaw_state.a, + pitch_state.p, pitch_state.v, pitch_state.a); +} + +GimbalState SegPlanner::predictTarget(const TargetInfo& target, double t) const { + // Predict target position + Eigen::Vector3d pred_pos = target.position + t * target.velocity; + + // Predict target yaw + double pred_yaw = target.yaw + t * target.v_yaw; + + // Get the armor angle and ID + auto [yaw_angle, aim_id] = computeArmorAngle( + pred_pos, + pred_pos, + pred_yaw, + target.armors_num, + target.radius_1, + target.radius_2, + target.d_zc, + target.d_za + ); + + // Compute pitch from position + double pitch = std::atan2(pred_pos.z(), pred_pos.head(2).norm()); + + return GimbalState(yaw_angle, target.v_yaw, 0.0, pitch, 0.0, 0.0); +} + +void SegPlanner::unwrapAngles(std::vector& states) const { + if (states.size() <= 1) return; + + for (size_t i = 1; i < states.size(); ++i) { + // Unwrap yaw + while (states[i].yaw.p - states[i-1].yaw.p > M_PI) { + states[i].yaw.p -= 2 * M_PI; + } + while (states[i].yaw.p - states[i-1].yaw.p < -M_PI) { + states[i].yaw.p += 2 * M_PI; + } + + // Unwrap pitch (less common but still needed) + while (states[i].pitch.p - states[i-1].pitch.p > M_PI) { + states[i].pitch.p -= 2 * M_PI; + } + while (states[i].pitch.p - states[i-1].pitch.p < -M_PI) { + states[i].pitch.p += 2 * M_PI; + } + } +} + +std::pair, std::vector> +SegPlanner::computeNodeStates(const std::vector& states, + const std::vector& dt_vec) const { + size_t n = states.size(); + std::vector yaw_nodes(n), pitch_nodes(n); + + if (n <= 1) return {yaw_nodes, pitch_nodes}; + + // First node: use forward difference for velocity, estimate acceleration as 0 + yaw_nodes[0] = State1D(states[0].yaw.p, (states[1].yaw.p - states[0].yaw.p) / dt_vec[0], 0.0); + pitch_nodes[0] = State1D(states[0].pitch.p, (states[1].pitch.p - states[0].pitch.p) / dt_vec[0], 0.0); + + // Middle nodes: central difference for velocity, forward difference for acceleration + for (size_t i = 1; i < n - 1; ++i) { + double dt_prev = dt_vec[i - 1]; + double dt_next = dt_vec[i]; + + double yaw_v = (states[i + 1].yaw.p - states[i - 1].yaw.p) / (dt_prev + dt_next); + double pitch_v = (states[i + 1].pitch.p - states[i - 1].pitch.p) / (dt_prev + dt_next); + + double yaw_a = (states[i + 1].yaw.v - states[i - 1].yaw.v) / (dt_prev + dt_next); + double pitch_a = (states[i + 1].pitch.v - states[i - 1].pitch.v) / (dt_prev + dt_next); + + yaw_nodes[i] = State1D(states[i].yaw.p, yaw_v, yaw_a); + pitch_nodes[i] = State1D(states[i].pitch.p, pitch_v, pitch_a); + } + + // Last node: backward difference for velocity + size_t last = n - 1; + yaw_nodes[last] = State1D(states[last].yaw.p, + (states[last].yaw.p - states[last - 1].yaw.p) / dt_vec[last - 1], + 0.0); + pitch_nodes[last] = State1D(states[last].pitch.p, + (states[last].pitch.p - states[last - 1].pitch.p) / dt_vec[last - 1], + 0.0); + + return {yaw_nodes, pitch_nodes}; +} + +Trajectory +SegPlanner::buildLimit(const std::vector& yaw_nodes, + const std::vector& pitch_nodes, + double max_yaw_acc, + double max_pitch_acc) const { + using namespace Eigen; + + Trajectory traj; + size_t n = yaw_nodes.size(); + + if (n <= 1) return traj; + + traj.reserve(n); + + for (size_t i = 0; i < n - 1; ++i) { + // Build yaw segment + QuinticSegment seg(1.0); + seg = QuinticSegment::build(yaw_nodes[i], yaw_nodes[i + 1], 1.0); + traj.push_back(seg, 1.0); + } + + return traj; +} + +std::pair +SegPlanner::computeArmorAngle(const Eigen::Vector3d& target_pos, + const Eigen::Vector3d& target_center, + double target_yaw, + size_t armors_num, + double radius_1, + double radius_2, + double d_zc, + double d_za) { + // Compute angle to target center + double alpha = std::atan2(target_center.y(), target_center.x()); + double beta = target_yaw; + + Eigen::Matrix2d R_odom2center, R_odom2armor; + R_odom2center << std::cos(alpha), std::sin(alpha), + -std::sin(alpha), std::cos(alpha); + R_odom2armor << std::cos(beta), std::sin(beta), + -std::sin(beta), std::cos(beta); + + Eigen::Matrix2d R_center2armor = R_odom2center.transpose() * R_odom2armor; + double decision_angle = -std::asin(R_center2armor(0, 1)); + + double temp_angle = decision_angle + M_PI / armors_num; + if (temp_angle < 0) temp_angle += 2 * M_PI; + + int selected_id = static_cast(temp_angle / (2 * M_PI / armors_num)); + + // Compute actual yaw angle to armor + double armor_yaw = std::atan2(target_pos.y(), target_pos.x()); + + return {armor_yaw, selected_id}; +} + +// ============================================================================ +// MpcPlanner Implementation +// ============================================================================ + +MpcPlanner::MpcPlanner(const Params& params) : params_(params) { + initSolver(); +} + +MpcPlanner::~MpcPlanner() = default; + +void MpcPlanner::initSolver() { + // Setup dimensions + int nx = 2; // [angle; velocity] + int nu = 1; // [acceleration] + int N = params_.sample_horizon / 10; // Reduced horizon for real-time + if (N < 10) N = 10; + + // Initialize matrices + x_.resize(nx, N); + u_.resize(nu, N - 1); + x_ref_.resize(nx, N); + u_ref_.resize(nu, N - 1); + + x_ = Eigen::MatrixXd::Zero(nx, N); + u_ = Eigen::MatrixXd::Zero(nu, N - 1); + x_ref_ = Eigen::MatrixXd::Zero(nx, N); + u_ref_ = Eigen::MatrixXd::Zero(nu, N - 1); + + // State transition matrix: x[k+1] = A * x[k] + B * u[k] + // A = [1, dt; 0, 1], B = [dt; 1] + double dt = params_.sample_total_time / N; + Adyn_.resize(2, 2); + Adyn_ << 1, dt, + 0, 1; + Bdyn_.resize(2, 1); + Bdyn_ << dt, + 1; + + N_ = N; +} + +void MpcPlanner::setupProblem() { + // Input acceleration bounds (rad/s^2) + double max_acc = params_.max_yaw_acc * M_PI / 180.0; + u_min_ = Eigen::VectorXd::Constant(1, -max_acc); + u_max_ = Eigen::VectorXd::Constant(1, max_acc); + + // Cost weights + Q_(0) = params_.Q_yaw_p; + Q_(1) = params_.Q_yaw_v; + R_(0) = params_.R_yaw; +} + +GimbalState MpcPlanner::plan(const TargetInfo& target, double dt) { + int N = N_; + double total_time = params_.sample_total_time; + double time_step = total_time / N; + + // Reset debug info + debug_.planner_type = "mpc"; + debug_.traj_time.clear(); + debug_.traj_yaw_p.clear(); + debug_.traj_yaw_v.clear(); + debug_.max_yaw_acc = params_.max_yaw_acc; + debug_.max_pitch_acc = params_.max_pitch_acc; + debug_.total_dt = dt; + + // 1. Generate reference trajectory using SegPlanner approach + std::vector ref_traj; + ref_traj.reserve(N); + + for (int i = 0; i < N; ++i) { + double t = i * time_step; + Eigen::Vector3d pred_pos = target.position + t * target.velocity; + double pred_yaw = target.yaw + t * target.v_yaw; + double pitch = std::atan2(pred_pos.z(), pred_pos.head(2).norm()); + + ref_traj.push_back(GimbalState(pred_yaw, target.v_yaw, 0.0, pitch, 0.0, 0.0)); + + // Fill debug trajectory points (subsample for efficiency) + if (i % 10 == 0) { + debug_.traj_time.push_back(t); + debug_.traj_yaw_p.push_back(pred_yaw); + debug_.traj_yaw_v.push_back(target.v_yaw); + } + } + + // 2. Set initial state + x_.col(0) << target.yaw, target.v_yaw; + + // 3. Set reference trajectory + for (int i = 0; i < N; ++i) { + x_ref_.col(i) << ref_traj[i].yaw.p, ref_traj[i].yaw.v; + } + + // 4. Solve MPC + solveMpc(ref_traj); + + // 5. Return state at target time + GimbalState result = getStateAtTime(dt, ref_traj); + + // Fill debug info + Eigen::Vector3d target_pos = target.position + dt * target.velocity; + debug_.target_yaw = target.yaw + dt * target.v_yaw; + debug_.target_pitch = std::atan2(target_pos.z(), target_pos.head(2).norm()); + debug_.target_distance = target_pos.norm(); + debug_.planned_yaw = result.yaw.p; + debug_.planned_pitch = result.pitch.p; + debug_.planned_yaw_v = result.yaw.v; + debug_.planned_pitch_v = result.pitch.v; + debug_.planned_yaw_a = result.yaw.a; + debug_.planned_pitch_a = result.pitch.a; + debug_.flying_time = dt; + + return result; +} + +void MpcPlanner::solveMpc(const std::vector& ref_traj) { + int N = N_; + + // Initialize + x_ = Eigen::MatrixXd::Zero(2, N); + u_ = Eigen::MatrixXd::Zero(1, N - 1); + + // Simple forward simulation with feedback + // This is a simplified MPC that uses the reference trajectory + // and applies acceleration constraints + for (int i = 0; i < N - 1; ++i) { + // Get reference state + Eigen::Vector2d x_ref = x_ref_.col(i); + + // Compute error + Eigen::Vector2d error = x_.col(i) - x_ref; + + // Apply acceleration to reduce error + double u = -0.1 * error(0) - 0.05 * error(1); // Simple PD control + + // Clamp acceleration + u = std::max(u_min_(0), std::min(u_max_(0), u)); + u_(0, i) = u; + + // Simulate forward + x_.col(i + 1) = Adyn_ * x_.col(i) + Bdyn_ * u; + } + + // Store solution + mpc_solution_yaw_.clear(); + mpc_solution_yaw_.reserve(N); + for (int i = 0; i < N; ++i) { + State1D yaw_state(x_(0, i), x_(1, i), 0.0); + mpc_solution_yaw_.push_back(yaw_state); + } +} + +GimbalState MpcPlanner::getStateAtTime(double t, const std::vector& ref_traj) const { + if (mpc_solution_yaw_.empty()) { + return GimbalState(); + } + + int N = N_; + double total_time = params_.sample_total_time; + double time_step = total_time / N; + + if (t <= 0) return mpc_solution_yaw_.front(); + if (t >= total_time) return mpc_solution_yaw_.back(); + + // Find the segment + int idx = static_cast(t / time_step); + if (idx >= N - 1) idx = N - 2; + if (idx < 0) idx = 0; + + double seg_t = t - idx * time_step; + double alpha = seg_t / time_step; + alpha = std::clamp(alpha, 0.0, 1.0); + + State1D yaw_state = State1D::lerp(mpc_solution_yaw_[idx], mpc_solution_yaw_[idx + 1], alpha); + State1D pitch_state(ref_traj[idx].pitch.p, 0.0, 0.0); + + return GimbalState(yaw_state.p, yaw_state.v, yaw_state.a, + pitch_state.p, pitch_state.v, pitch_state.a); +} + +void MpcPlanner::setParams(const Params& params) { + params_ = params; + initSolver(); +} + +} // namespace fyt::auto_aim diff --git a/src/rm_bringup/config/node_params/armor_solver_params.yaml b/src/rm_bringup/config/node_params/armor_solver_params.yaml index bdb3881..0304c9d 100644 --- a/src/rm_bringup/config/node_params/armor_solver_params.yaml +++ b/src/rm_bringup/config/node_params/armor_solver_params.yaml @@ -43,13 +43,28 @@ shooting_range_width: 0.10 #射击范围 shooting_range_height: 0.10 #射击范围 prediction_delay: 0.02 # 预测装甲板位置的延时,单位秒,+飞行时间 - controller_delay: 0.01 - max_tracking_v_yaw: 5.0 #转速(rad/s)大于这个值时瞄准机器人中心 - side_angle: 15.0 - compenstator_type: "resistance" + controller_delay: 0.01 + max_tracking_v_yaw: 5.0 #转速(rad/s)大于这个值时瞄准机器人中心 + side_angle: 15.0 + compenstator_type: "resistance" gravity: 9.792 - resistance: 0.038 + resistance: 0.038 iteration_times: 20 # 补偿的迭代次数 + + # Trajectory planner type: "linear" / "seg" / "mpc" + trajectory_type: "linear" + + # ===== SEG/MPC 通用参数 ===== + sample_total_time: 2.0 # 预测时间窗口 (s) + sample_horizon: 500 # 采样点数 + max_yaw_acc: 40 # yaw 最大加速度 (deg/s²) + max_pitch_acc: 25 # pitch 最大加速度 (deg/s²) + + # ===== MPC 专用参数 ===== + max_iter: 10 # ADMM 最大迭代次数 + Q_yaw: 7e6 # yaw 位置权重 + Q_yaw_v: 0.0 # yaw 速度权重 + R_yaw: 3.0 # yaw 控制权重 # ["距离下限, 距离上限, 高度下限, 高度下限, pitch轴补偿值"] # [dist_low, dist_high, height_low, height_high, pitch_offset_deg, yaw_offset_deg] diff --git a/wust_vision-main/.clang-format b/wust_vision-main/.clang-format new file mode 100644 index 0000000..ae74195 --- /dev/null +++ b/wust_vision-main/.clang-format @@ -0,0 +1,72 @@ +AccessModifierOffset: -4 +AlignAfterOpenBracket: BlockIndent +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: false +AlignTrailingComments: false +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: true +BraceWrapping: + AfterControlStatement: MultiLine + AfterEnum: false + AfterStruct: false + SplitEmptyFunction: false +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterColon +BreakStringLiterals: false +ColumnLimit: 100 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +FixNamespaceComments: true +IncludeBlocks: Preserve +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: BeforeHash +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: Inner +PointerAlignment: Left +ReflowComments: false +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: false +SpaceBeforeInheritanceColon: false +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +UseTab: Never diff --git a/wust_vision-main/.clangd b/wust_vision-main/.clangd new file mode 100644 index 0000000..3095f5b --- /dev/null +++ b/wust_vision-main/.clangd @@ -0,0 +1,6 @@ +Diagnostics: + Suppress: + - drv_unknown_argument + +CompileFlags: + Remove: [-forward-unknown-to-host-compiler, --generate-code=*, -Xcompiler=*] \ No newline at end of file diff --git a/wust_vision-main/.gitignore b/wust_vision-main/.gitignore new file mode 100644 index 0000000..401ca2a --- /dev/null +++ b/wust_vision-main/.gitignore @@ -0,0 +1,31 @@ +build + +devel + +install + +log/* + +.catkin_workspace + +.vscode + +.cache + +__pycache__ + +*~ + +.DS_Store + +*.pcd + +*.gv + +*.pdf + + +bin + +model/at.onnx +model/at.engine \ No newline at end of file diff --git a/wust_vision-main/.gitmodules b/wust_vision-main/.gitmodules new file mode 100644 index 0000000..250dc35 --- /dev/null +++ b/wust_vision-main/.gitmodules @@ -0,0 +1,6 @@ +[submodule "KalmanHyLib"] + path = KalmanHyLib + url = https://github.com/hyheiyue/KalmanHyLib.git +[submodule "3rdparty/backward-cpp"] + path = 3rdparty/backward-cpp + url = https://github.com/bombela/backward-cpp.git diff --git a/wust_vision-main/3rdparty/angles.h b/wust_vision-main/3rdparty/angles.h new file mode 100644 index 0000000..616ceb3 --- /dev/null +++ b/wust_vision-main/3rdparty/angles.h @@ -0,0 +1,373 @@ +/********************************************************************* + * Software License Agreement (BSD License) + * + * Copyright (c) 2008, Willow Garage, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of the Willow Garage nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *********************************************************************/ + +#ifndef GEOMETRY_ANGLES_UTILS_H +#define GEOMETRY_ANGLES_UTILS_H + +#ifndef _USE_MATH_DEFINES + #define _USE_MATH_DEFINES +#endif + +#include +#include + +namespace angles { + +/*! + * \brief Convert degrees to radians + */ + +static inline double from_degrees(double degrees) { + return degrees * M_PI / 180.0; +} + +/*! + * \brief Convert radians to degrees + */ +static inline double to_degrees(double radians) { + return radians * 180.0 / M_PI; +} + +/*! + * \brief normalize_angle_positive + * + * Normalizes the angle to be 0 to 2*M_PI + * It takes and returns radians. + */ +static inline double normalize_angle_positive(double angle) { + const double result = fmod(angle, 2.0 * M_PI); + if (result < 0) + return result + 2.0 * M_PI; + return result; +} + +/*! + * \brief normalize + * + * Normalizes the angle to be -M_PI circle to +M_PI circle + * It takes and returns radians. + * + */ +static inline double normalize_angle(double angle) { + const double result = fmod(angle + M_PI, 2.0 * M_PI); + if (result <= 0.0) + return result + M_PI; + return result - M_PI; +} + +/*! + * \function + * \brief shortest_angular_distance + * + * Given 2 angles, this returns the shortest angular + * difference. The inputs and ouputs are of course radians. + * + * The result + * would always be -pi <= result <= pi. Adding the result + * to "from" will always get you an equivelent angle to "to". + */ + +static inline double shortest_angular_distance(double from, double to) { + return normalize_angle(to - from); +} + +/*! + * \function + * + * \brief returns the angle in [-2*M_PI, 2*M_PI] going the other way along the + * unit circle. \param angle The angle to which you want to turn in the range + * [-2*M_PI, 2*M_PI] E.g. two_pi_complement(-M_PI/4) returns 7_M_PI/4 + * two_pi_complement(M_PI/4) returns -7*M_PI/4 + * + */ +static inline double two_pi_complement(double angle) { + // check input conditions + if (angle > 2 * M_PI || angle < -2.0 * M_PI) + angle = fmod(angle, 2.0 * M_PI); + if (angle < 0) + return (2 * M_PI + angle); + else if (angle > 0) + return (-2 * M_PI + angle); + + return (2 * M_PI); +} + +/*! + * \function + * + * \brief This function is only intended for internal use and not intended for + * external use. If you do use it, read the documentation very carefully. + * Returns the min and max amount (in radians) that can be moved from "from" + * angle to "left_limit" and "right_limit". \return returns false if "from" + * angle does not lie in the interval [left_limit,right_limit] \param from - + * "from" angle - must lie in [-M_PI, M_PI) \param left_limit - left limit of + * valid interval for angular position - must lie in [-M_PI, M_PI], left and + * right limits are specified on the unit circle w.r.t to a reference pointing + * inwards \param right_limit - right limit of valid interval for angular + * position - must lie in [-M_PI, M_PI], left and right limits are specified on + * the unit circle w.r.t to a reference pointing inwards \param result_min_delta + * - minimum (delta) angle (in radians) that can be moved from "from" position + * before hitting the joint stop \param result_max_delta - maximum (delta) angle + * (in radians) that can be movedd from "from" position before hitting the joint + * stop + */ +static bool find_min_max_delta( + double from, + double left_limit, + double right_limit, + double& result_min_delta, + double& result_max_delta +) { + double delta[4]; + + delta[0] = shortest_angular_distance(from, left_limit); + delta[1] = shortest_angular_distance(from, right_limit); + + delta[2] = two_pi_complement(delta[0]); + delta[3] = two_pi_complement(delta[1]); + + if (delta[0] == 0) { + result_min_delta = delta[0]; + result_max_delta = std::max(delta[1], delta[3]); + return true; + } + + if (delta[1] == 0) { + result_max_delta = delta[1]; + result_min_delta = std::min(delta[0], delta[2]); + return true; + } + + double delta_min = delta[0]; + double delta_min_2pi = delta[2]; + if (delta[2] < delta_min) { + delta_min = delta[2]; + delta_min_2pi = delta[0]; + } + + double delta_max = delta[1]; + double delta_max_2pi = delta[3]; + if (delta[3] > delta_max) { + delta_max = delta[3]; + delta_max_2pi = delta[1]; + } + + // printf("%f %f %f %f\n",delta_min,delta_min_2pi,delta_max,delta_max_2pi); + if ((delta_min <= delta_max_2pi) || (delta_max >= delta_min_2pi)) { + result_min_delta = delta_max_2pi; + result_max_delta = delta_min_2pi; + if (left_limit == -M_PI && right_limit == M_PI) + return true; + else + return false; + } + result_min_delta = delta_min; + result_max_delta = delta_max; + return true; +} + +/*! + * \function + * + * \brief Returns the delta from `from_angle` to `to_angle`, making sure it does + * not violate limits specified by `left_limit` and `right_limit`. This function + * is similar to `shortest_angular_distance_with_limits()`, with the main + * difference that it accepts limits outside the `[-M_PI, M_PI]` range. Even if + * this is quite uncommon, one could indeed consider revolute joints with large + * rotation limits, e.g., in the range `[-2*M_PI, 2*M_PI]`. + * + * In this case, a strict requirement is to have `left_limit` smaller than + * `right_limit`. Note also that `from` must lie inside the valid range, while + * `to` does not need to. In fact, this function will evaluate the shortest + * (valid) angle `shortest_angle` so that `from+shortest_angle` equals `to` up + * to an integer multiple of `2*M_PI`. As an example, a call to + * `shortest_angular_distance_with_large_limits(0, 10.5*M_PI, -2*M_PI, 2*M_PI, + * shortest_angle)` will return `true`, with `shortest_angle=0.5*M_PI`. This is + * because `from` and `from+shortest_angle` are both inside the limits, and + * `fmod(to+shortest_angle, 2*M_PI)` equals `fmod(to, 2*M_PI)`. On the other + * hand, `shortest_angular_distance_with_large_limits(10.5*M_PI, 0, -2*M_PI, + * 2*M_PI, shortest_angle)` will return false, since `from` is not in the valid + * range. Finally, note that the call + * `shortest_angular_distance_with_large_limits(0, 10.5*M_PI, -2*M_PI, 0.1*M_PI, + * shortest_angle)` will also return `true`. However, `shortest_angle` in this + * case will be `-1.5*M_PI`. + * + * \return true if `left_limit < right_limit` and if "from" and + * "from+shortest_angle" positions are within the valid interval, false + * otherwise. \param from - "from" angle. \param to - "to" angle. \param + * left_limit - left limit of valid interval, must be smaller than right_limit. + * \param right_limit - right limit of valid interval, must be greater than + * left_limit. \param shortest_angle - result of the shortest angle calculation. + */ +static inline bool shortest_angular_distance_with_large_limits( + double from, + double to, + double left_limit, + double right_limit, + double& shortest_angle +) { + // Shortest steps in the two directions + double delta = shortest_angular_distance(from, to); + double delta_2pi = two_pi_complement(delta); + + // "sort" distances so that delta is shorter than delta_2pi + if (std::fabs(delta) > std::fabs(delta_2pi)) + std::swap(delta, delta_2pi); + + if (left_limit > right_limit) { + // If limits are something like [PI/2 , -PI/2] it actually means that we + // want rotations to be in the interval [-PI,PI/2] U [PI/2,PI], ie, the + // half unit circle not containing the 0. This is already gracefully + // handled by shortest_angular_distance_with_limits, and therefore this + // function should not be called at all. However, if one has limits that + // are larger than PI, the same rationale behind + // shortest_angular_distance_with_limits does not hold, ie, M_PI+x should + // not be directly equal to -M_PI+x. In this case, the correct way of + // getting the shortest solution is to properly set the limits, eg, by + // saying that the interval is either [PI/2, 3*PI/2] or [-3*M_PI/2, + // -M_PI/2]. For this reason, here we return false by default. + shortest_angle = delta; + return false; + } + + // Check in which direction we should turn (clockwise or counter-clockwise). + + // start by trying with the shortest angle (delta). + double to2 = from + delta; + if (left_limit <= to2 && to2 <= right_limit) { + // we can move in this direction: return success if the "from" angle is + // inside limits + shortest_angle = delta; + return left_limit <= from && from <= right_limit; + } + + // delta is not ok, try to move in the other direction (using its complement) + to2 = from + delta_2pi; + if (left_limit <= to2 && to2 <= right_limit) { + // we can move in this direction: return success if the "from" angle is + // inside limits + shortest_angle = delta_2pi; + return left_limit <= from && from <= right_limit; + } + + // nothing works: we always go outside limits + shortest_angle = delta; // at least give some "coherent" result + return false; +} + +/*! + * \function + * + * \brief Returns the delta from "from_angle" to "to_angle" making sure it does + * not violate limits specified by left_limit and right_limit. The valid + * interval of angular positions is [left_limit,right_limit]. E.g., [-0.25,0.25] + * is a 0.5 radians wide interval that contains 0. But [0.25,-0.25] is a + * 2*M_PI-0.5 wide interval that contains M_PI (but not 0). The value of + * shortest_angle is the angular difference between "from" and "to" that lies + * within the defined valid interval. E.g. + * shortest_angular_distance_with_limits(-0.5,0.5,0.25,-0.25,ss) evaluates ss to + * 2*M_PI-1.0 and returns true while + * shortest_angular_distance_with_limits(-0.5,0.5,-0.25,0.25,ss) returns false + * since -0.5 and 0.5 do not lie in the interval [-0.25,0.25] + * + * \return true if "from" and "to" positions are within the limit interval, + * false otherwise \param from - "from" angle \param to - "to" angle \param + * left_limit - left limit of valid interval for angular position, left and + * right limits are specified on the unit circle w.r.t to a reference pointing + * inwards \param right_limit - right limit of valid interval for angular + * position, left and right limits are specified on the unit circle w.r.t to a + * reference pointing inwards \param shortest_angle - result of the shortest + * angle calculation + */ +static inline bool shortest_angular_distance_with_limits( + double from, + double to, + double left_limit, + double right_limit, + double& shortest_angle +) { + double min_delta = -2 * M_PI; + double max_delta = 2 * M_PI; + double min_delta_to = -2 * M_PI; + double max_delta_to = 2 * M_PI; + bool flag = find_min_max_delta(from, left_limit, right_limit, min_delta, max_delta); + double delta = shortest_angular_distance(from, to); + double delta_mod_2pi = two_pi_complement(delta); + + if (flag) // from position is within the limits + { + if (delta >= min_delta && delta <= max_delta) { + shortest_angle = delta; + return true; + } else if (delta_mod_2pi >= min_delta && delta_mod_2pi <= max_delta) { + shortest_angle = delta_mod_2pi; + return true; + } else // to position is outside the limits + { + find_min_max_delta(to, left_limit, right_limit, min_delta_to, max_delta_to); + if (fabs(min_delta_to) < fabs(max_delta_to)) + shortest_angle = std::max(delta, delta_mod_2pi); + else if (fabs(min_delta_to) > fabs(max_delta_to)) + shortest_angle = std::min(delta, delta_mod_2pi); + else { + if (fabs(delta) < fabs(delta_mod_2pi)) + shortest_angle = delta; + else + shortest_angle = delta_mod_2pi; + } + return false; + } + } else // from position is outside the limits + { + find_min_max_delta(to, left_limit, right_limit, min_delta_to, max_delta_to); + + if (fabs(min_delta) < fabs(max_delta)) + shortest_angle = std::min(delta, delta_mod_2pi); + else if (fabs(min_delta) > fabs(max_delta)) + shortest_angle = std::max(delta, delta_mod_2pi); + else { + if (fabs(delta) < fabs(delta_mod_2pi)) + shortest_angle = delta; + else + shortest_angle = delta_mod_2pi; + } + return false; + } + + shortest_angle = delta; + return false; +} +} // namespace angles + +#endif \ No newline at end of file diff --git a/wust_vision-main/3rdparty/ankerl/stl.h b/wust_vision-main/3rdparty/ankerl/stl.h new file mode 100644 index 0000000..216a1f1 --- /dev/null +++ b/wust_vision-main/3rdparty/ankerl/stl.h @@ -0,0 +1,84 @@ +///////////////////////// ankerl::unordered_dense::{map, set} ///////////////////////// + +// A fast & densely stored hashmap and hashset based on robin-hood backward shift deletion. +// Version 4.8.1 +// https://github.com/martinus/unordered_dense +// +// Licensed under the MIT License . +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 Martin Leitner-Ankerl +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef ANKERL_STL_H +#define ANKERL_STL_H + +#include // for array +#include // for uint64_t, uint32_t, std::uint8_t, UINT64_C +#include // for size_t, memcpy, memset +#include // for equal_to, hash +#include // for initializer_list +#include // for pair, distance +#include // for numeric_limits +#include // for allocator, allocator_traits, shared_ptr +#include // for optional +#include // for out_of_range +#include // for basic_string +#include // for basic_string_view, hash +#include // for forward_as_tuple +#include // for enable_if_t, declval, conditional_t, ena... +#include // for forward, exchange, pair, as_const, piece... +#include // for vector + +// includes , which fails to compile if +// targeting GCC >= 13 with the (rewritten) win32 thread model, and +// targeting Windows earlier than Vista (0x600). GCC predefines +// _REENTRANT when using the 'posix' model, and doesn't when using the +// 'win32' model. +#if defined __MINGW64__ && defined __GNUC__ && __GNUC__ >= 13 && !defined _REENTRANT + // _WIN32_WINNT is guaranteed to be defined here because of the + // inclusion above. + #ifndef _WIN32_WINNT + #error "_WIN32_WINNT not defined" + #endif + #if _WIN32_WINNT < 0x600 + #define ANKERL_MEMORY_RESOURCE_IS_BAD() 1 // NOLINT(cppcoreguidelines-macro-usage) + #endif +#endif +#ifndef ANKERL_MEMORY_RESOURCE_IS_BAD + #define ANKERL_MEMORY_RESOURCE_IS_BAD() 0 // NOLINT(cppcoreguidelines-macro-usage) +#endif + +#if defined(__has_include) && !defined(ANKERL_UNORDERED_DENSE_DISABLE_PMR) + #if __has_include() && !ANKERL_MEMORY_RESOURCE_IS_BAD() + #define ANKERL_UNORDERED_DENSE_PMR std::pmr // NOLINT(cppcoreguidelines-macro-usage) + #include // for polymorphic_allocator + #elif __has_include() + #define ANKERL_UNORDERED_DENSE_PMR \ + std::experimental::pmr // NOLINT(cppcoreguidelines-macro-usage) + #include // for polymorphic_allocator + #endif +#endif + +#if defined(_MSC_VER) && defined(_M_X64) + #include + #pragma intrinsic(_umul128) +#endif + +#endif \ No newline at end of file diff --git a/wust_vision-main/3rdparty/ankerl/unordered_dense.h b/wust_vision-main/3rdparty/ankerl/unordered_dense.h new file mode 100644 index 0000000..1508836 --- /dev/null +++ b/wust_vision-main/3rdparty/ankerl/unordered_dense.h @@ -0,0 +1,2440 @@ +///////////////////////// ankerl::unordered_dense::{map, set} ///////////////////////// + +// A fast & densely stored hashmap and hashset based on robin-hood backward shift deletion. +// Version 4.8.1 +// https://github.com/martinus/unordered_dense +// +// Licensed under the MIT License . +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 Martin Leitner-Ankerl +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef ANKERL_UNORDERED_DENSE_H +#define ANKERL_UNORDERED_DENSE_H + +// see https://semver.org/spec/v2.0.0.html +#define ANKERL_UNORDERED_DENSE_VERSION_MAJOR \ + 4 // NOLINT(cppcoreguidelines-macro-usage) incompatible API changes +#define ANKERL_UNORDERED_DENSE_VERSION_MINOR \ + 8 // NOLINT(cppcoreguidelines-macro-usage) backwards compatible functionality +#define ANKERL_UNORDERED_DENSE_VERSION_PATCH \ + 1 // NOLINT(cppcoreguidelines-macro-usage) backwards compatible bug fixes + +// API versioning with inline namespace, see https://www.foonathan.net/2018/11/inline-namespaces/ + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define ANKERL_UNORDERED_DENSE_VERSION_CONCAT1(major, minor, patch) v##major##_##minor##_##patch +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define ANKERL_UNORDERED_DENSE_VERSION_CONCAT(major, minor, patch) \ + ANKERL_UNORDERED_DENSE_VERSION_CONCAT1(major, minor, patch) +#define ANKERL_UNORDERED_DENSE_NAMESPACE \ + ANKERL_UNORDERED_DENSE_VERSION_CONCAT( \ + ANKERL_UNORDERED_DENSE_VERSION_MAJOR, \ + ANKERL_UNORDERED_DENSE_VERSION_MINOR, \ + ANKERL_UNORDERED_DENSE_VERSION_PATCH \ + ) + +#if defined(_MSVC_LANG) + #define ANKERL_UNORDERED_DENSE_CPP_VERSION _MSVC_LANG +#else + #define ANKERL_UNORDERED_DENSE_CPP_VERSION __cplusplus +#endif + +#if defined(__GNUC__) + // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) + #define ANKERL_UNORDERED_DENSE_PACK(decl) decl __attribute__((__packed__)) +#elif defined(_MSC_VER) + // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) + #define ANKERL_UNORDERED_DENSE_PACK(decl) __pragma(pack(push, 1)) decl __pragma(pack(pop)) +#endif + +// exceptions +#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND) + #define ANKERL_UNORDERED_DENSE_HAS_EXCEPTIONS() 1 // NOLINT(cppcoreguidelines-macro-usage) +#else + #define ANKERL_UNORDERED_DENSE_HAS_EXCEPTIONS() 0 // NOLINT(cppcoreguidelines-macro-usage) +#endif +#ifdef _MSC_VER + #define ANKERL_UNORDERED_DENSE_NOINLINE __declspec(noinline) +#else + #define ANKERL_UNORDERED_DENSE_NOINLINE __attribute__((noinline)) +#endif + +#if defined(__clang__) && defined(__has_attribute) + #if __has_attribute(__no_sanitize__) + #define ANKERL_UNORDERED_DENSE_DISABLE_UBSAN_UNSIGNED_INTEGER_CHECK \ + __attribute__((__no_sanitize__("unsigned-integer-overflow"))) + #endif +#endif + +#if !defined(ANKERL_UNORDERED_DENSE_DISABLE_UBSAN_UNSIGNED_INTEGER_CHECK) + #define ANKERL_UNORDERED_DENSE_DISABLE_UBSAN_UNSIGNED_INTEGER_CHECK +#endif + +#if ANKERL_UNORDERED_DENSE_CPP_VERSION < 201703L + #error ankerl::unordered_dense requires C++17 or higher +#else + + #if !defined(ANKERL_UNORDERED_DENSE_STD_MODULE) + // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) + #define ANKERL_UNORDERED_DENSE_STD_MODULE 0 + #endif + + #if !ANKERL_UNORDERED_DENSE_STD_MODULE + #include "stl.h" + #endif + + #if __has_cpp_attribute(likely) && __has_cpp_attribute(unlikely) \ + && ANKERL_UNORDERED_DENSE_CPP_VERSION >= 202002L + #define ANKERL_UNORDERED_DENSE_LIKELY_ATTR \ + [[likely]] // NOLINT(cppcoreguidelines-macro-usage) + #define ANKERL_UNORDERED_DENSE_UNLIKELY_ATTR \ + [[unlikely]] // NOLINT(cppcoreguidelines-macro-usage) + #define ANKERL_UNORDERED_DENSE_LIKELY(x) (x) // NOLINT(cppcoreguidelines-macro-usage) + #define ANKERL_UNORDERED_DENSE_UNLIKELY(x) (x) // NOLINT(cppcoreguidelines-macro-usage) + #else + #define ANKERL_UNORDERED_DENSE_LIKELY_ATTR // NOLINT(cppcoreguidelines-macro-usage) + #define ANKERL_UNORDERED_DENSE_UNLIKELY_ATTR // NOLINT(cppcoreguidelines-macro-usage) + + #if defined(__GNUC__) || defined(__INTEL_COMPILER) || defined(__clang__) + #define ANKERL_UNORDERED_DENSE_LIKELY(x) \ + __builtin_expect(x, 1) // NOLINT(cppcoreguidelines-macro-usage) + #define ANKERL_UNORDERED_DENSE_UNLIKELY(x) \ + __builtin_expect(x, 0) // NOLINT(cppcoreguidelines-macro-usage) + #else + #define ANKERL_UNORDERED_DENSE_LIKELY(x) (x) // NOLINT(cppcoreguidelines-macro-usage) + #define ANKERL_UNORDERED_DENSE_UNLIKELY(x) (x) // NOLINT(cppcoreguidelines-macro-usage) + #endif + + #endif + +namespace ankerl::unordered_dense { +inline namespace ANKERL_UNORDERED_DENSE_NAMESPACE { + + namespace detail { + + #if ANKERL_UNORDERED_DENSE_HAS_EXCEPTIONS() + + // make sure this is not inlined as it is slow and dramatically enlarges code, thus making other + // inlinings more difficult. Throws are also generally the slow path. + [[noreturn]] inline ANKERL_UNORDERED_DENSE_NOINLINE void on_error_key_not_found() { + throw std::out_of_range("ankerl::unordered_dense::map::at(): key not found"); + } + [[noreturn]] inline ANKERL_UNORDERED_DENSE_NOINLINE void on_error_bucket_overflow() { + throw std::overflow_error( + "ankerl::unordered_dense: reached max bucket size, cannot increase size" + ); + } + [[noreturn]] inline ANKERL_UNORDERED_DENSE_NOINLINE void on_error_too_many_elements() { + throw std::out_of_range("ankerl::unordered_dense::map::replace(): too many elements"); + } + + #else + + [[noreturn]] inline void on_error_key_not_found() { + abort(); + } + [[noreturn]] inline void on_error_bucket_overflow() { + abort(); + } + [[noreturn]] inline void on_error_too_many_elements() { + abort(); + } + + #endif + + } // namespace detail + + // hash /////////////////////////////////////////////////////////////////////// + + // This is a stripped-down implementation of wyhash: https://github.com/wangyi-fudan/wyhash + // No big-endian support (because different values on different machines don't matter), + // hardcodes seed and the secret, reformats the code, and clang-tidy fixes. + namespace detail::wyhash { + + inline void mum(std::uint64_t* a, std::uint64_t* b) { + #if defined(__SIZEOF_INT128__) + __uint128_t r = *a; + r *= *b; + *a = static_cast(r); + *b = static_cast(r >> 64U); + #elif defined(_MSC_VER) && defined(_M_X64) + *a = _umul128(*a, *b, b); + #else + std::uint64_t ha = *a >> 32U; + std::uint64_t hb = *b >> 32U; + std::uint64_t la = static_cast(*a); + std::uint64_t lb = static_cast(*b); + std::uint64_t hi {}; + std::uint64_t lo {}; + std::uint64_t rh = ha * hb; + std::uint64_t rm0 = ha * lb; + std::uint64_t rm1 = hb * la; + std::uint64_t rl = la * lb; + std::uint64_t t = rl + (rm0 << 32U); + auto c = static_cast(t < rl); + lo = t + (rm1 << 32U); + c += static_cast(lo < t); + hi = rh + (rm0 >> 32U) + (rm1 >> 32U) + c; + *a = lo; + *b = hi; + #endif + } + + // multiply and xor mix function, aka MUM + [[nodiscard]] inline auto mix(std::uint64_t a, std::uint64_t b) -> std::uint64_t { + mum(&a, &b); + return a ^ b; + } + + // read functions. WARNING: we don't care about endianness, so results are different on big endian! + [[nodiscard]] inline auto r8(const std::uint8_t* p) -> std::uint64_t { + std::uint64_t v {}; + std::memcpy(&v, p, 8U); + return v; + } + + [[nodiscard]] inline auto r4(const std::uint8_t* p) -> std::uint64_t { + std::uint32_t v {}; + std::memcpy(&v, p, 4); + return v; + } + + // reads 1, 2, or 3 bytes + [[nodiscard]] inline auto r3(const std::uint8_t* p, std::size_t k) -> std::uint64_t { + return (static_cast(p[0]) << 16U) + | (static_cast(p[k >> 1U]) << 8U) | p[k - 1]; + } + + [[maybe_unused]] [[nodiscard]] inline auto hash(void const* key, std::size_t len) + -> std::uint64_t { + static constexpr auto secret = std::array { UINT64_C(0xa0761d6478bd642f), + UINT64_C(0xe7037ed1a0b428db), + UINT64_C(0x8ebc6af09c88c6e3), + UINT64_C(0x589965cc75374cc3) }; + + auto const* p = static_cast(key); + std::uint64_t seed = secret[0]; + std::uint64_t a {}; + std::uint64_t b {}; + if (ANKERL_UNORDERED_DENSE_LIKELY(len <= 16)) + ANKERL_UNORDERED_DENSE_LIKELY_ATTR { + if (ANKERL_UNORDERED_DENSE_LIKELY(len >= 4)) + ANKERL_UNORDERED_DENSE_LIKELY_ATTR { + a = (r4(p) << 32U) | r4(p + ((len >> 3U) << 2U)); + b = (r4(p + len - 4) << 32U) | r4(p + len - 4 - ((len >> 3U) << 2U)); + } + else if (ANKERL_UNORDERED_DENSE_LIKELY(len > 0)) + ANKERL_UNORDERED_DENSE_LIKELY_ATTR { + a = r3(p, len); + b = 0; + } + else { + a = 0; + b = 0; + } + } + else { + std::size_t i = len; + if (ANKERL_UNORDERED_DENSE_UNLIKELY(i > 48)) + ANKERL_UNORDERED_DENSE_UNLIKELY_ATTR { + std::uint64_t see1 = seed; + std::uint64_t see2 = seed; + do { + seed = mix(r8(p) ^ secret[1], r8(p + 8) ^ seed); + see1 = mix(r8(p + 16) ^ secret[2], r8(p + 24) ^ see1); + see2 = mix(r8(p + 32) ^ secret[3], r8(p + 40) ^ see2); + p += 48; + i -= 48; + } while (ANKERL_UNORDERED_DENSE_LIKELY(i > 48)); + seed ^= see1 ^ see2; + } + while (ANKERL_UNORDERED_DENSE_UNLIKELY(i > 16)) + ANKERL_UNORDERED_DENSE_UNLIKELY_ATTR { + seed = mix(r8(p) ^ secret[1], r8(p + 8) ^ seed); + i -= 16; + p += 16; + } + a = r8(p + i - 16); + b = r8(p + i - 8); + } + + return mix(secret[1] ^ len, mix(a ^ secret[1], b ^ seed)); + } + + [[nodiscard]] inline auto hash(std::uint64_t x) -> std::uint64_t { + return detail::wyhash::mix(x, UINT64_C(0x9E3779B97F4A7C15)); + } + + } // namespace detail::wyhash + + template + struct hash { + auto operator()(T const& obj) const + noexcept(noexcept(std::declval>().operator()(std::declval()))) + -> std::uint64_t { + return std::hash {}(obj); + } + }; + + template + struct hash::is_avalanching> { + using is_avalanching = void; + auto operator()(T const& obj) const + noexcept(noexcept(std::declval>().operator()(std::declval()))) + -> std::uint64_t { + return std::hash {}(obj); + } + }; + + template + struct hash> { + using is_avalanching = void; + auto operator()(std::basic_string const& str) const noexcept -> std::uint64_t { + return detail::wyhash::hash(str.data(), sizeof(CharT) * str.size()); + } + }; + + template + struct hash> { + using is_avalanching = void; + auto operator()(std::basic_string_view const& sv) const noexcept -> std::uint64_t { + return detail::wyhash::hash(sv.data(), sizeof(CharT) * sv.size()); + } + }; + + template + struct hash { + using is_avalanching = void; + auto operator()(T* ptr) const noexcept -> std::uint64_t { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return detail::wyhash::hash(reinterpret_cast(ptr)); + } + }; + + template + struct hash> { + using is_avalanching = void; + auto operator()(std::unique_ptr const& ptr) const noexcept -> std::uint64_t { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return detail::wyhash::hash(reinterpret_cast(ptr.get())); + } + }; + + template + struct hash> { + using is_avalanching = void; + auto operator()(std::shared_ptr const& ptr) const noexcept -> std::uint64_t { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return detail::wyhash::hash(reinterpret_cast(ptr.get())); + } + }; + + template + struct hash>> { + using is_avalanching = void; + auto operator()(Enum e) const noexcept -> std::uint64_t { + using underlying = std::underlying_type_t; + return detail::wyhash::hash(static_cast(e)); + } + }; + + template + struct tuple_hash_helper { + // Converts the value into 64bit. If it is an integral type, just cast it. Mixing is doing the rest. + // If it isn't an integral we need to hash it. + template + [[nodiscard]] constexpr static auto to64(Arg const& arg) -> std::uint64_t { + if constexpr (std::is_integral_v || std::is_enum_v) { + return static_cast(arg); + } else { + return hash {}(arg); + } + } + + [[nodiscard]] ANKERL_UNORDERED_DENSE_DISABLE_UBSAN_UNSIGNED_INTEGER_CHECK static auto + mix64(std::uint64_t state, std::uint64_t v) -> std::uint64_t { + return detail::wyhash::mix(state + v, std::uint64_t { 0x9ddfea08eb382d69 }); + } + + // Creates a buffer that holds all the data from each element of the tuple. If possible we memcpy the data directly. If + // not, we hash the object and use this for the array. Size of the array is known at compile time, and memcpy is optimized + // away, so filling the buffer is highly efficient. Finally, call wyhash with this buffer. + template + [[nodiscard]] static auto + calc_hash(T const& t, std::index_sequence /*unused*/) noexcept -> std::uint64_t { + auto h = std::uint64_t {}; + ((h = mix64(h, to64(std::get(t)))), ...); + return h; + } + }; + + template + struct hash>: tuple_hash_helper { + using is_avalanching = void; + auto operator()(std::tuple const& t) const noexcept -> std::uint64_t { + return tuple_hash_helper::calc_hash(t, std::index_sequence_for {}); + } + }; + + template + struct hash>: tuple_hash_helper { + using is_avalanching = void; + auto operator()(std::pair const& t) const noexcept -> std::uint64_t { + return tuple_hash_helper::calc_hash(t, std::index_sequence_for {}); + } + }; + + // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) + #define ANKERL_UNORDERED_DENSE_HASH_STATICCAST(T) \ + template<> \ + struct hash { \ + using is_avalanching = void; \ + auto operator()(T const& obj) const noexcept -> std::uint64_t { \ + return detail::wyhash::hash(static_cast(obj)); \ + } \ + } + + #if defined(__GNUC__) && !defined(__clang__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wuseless-cast" + #endif + // see https://en.cppreference.com/w/cpp/utility/hash + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(bool); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(char); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(signed char); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(unsigned char); + #if ANKERL_UNORDERED_DENSE_CPP_VERSION >= 202002L && defined(__cpp_char8_t) + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(char8_t); + #endif + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(char16_t); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(char32_t); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(wchar_t); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(short); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(unsigned short); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(int); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(unsigned int); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(long); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(long long); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(unsigned long); + ANKERL_UNORDERED_DENSE_HASH_STATICCAST(unsigned long long); + + #if defined(__GNUC__) && !defined(__clang__) + #pragma GCC diagnostic pop + #endif + + // bucket_type ////////////////////////////////////////////////////////// + + namespace bucket_type { + + struct standard { + static constexpr std::uint32_t dist_inc = 1U << 8U; // skip 1 byte fingerprint + static constexpr std::uint32_t fingerprint_mask = + dist_inc - 1; // mask for 1 byte of fingerprint + + std::uint32_t + m_dist_and_fingerprint; // upper 3 byte: distance to original bucket. lower byte: fingerprint from hash + std::uint32_t m_value_idx; // index into the m_values vector. + }; + + ANKERL_UNORDERED_DENSE_PACK(struct big { + static constexpr std::uint32_t dist_inc = 1U << 8U; // skip 1 byte fingerprint + static constexpr std::uint32_t fingerprint_mask = + dist_inc - 1; // mask for 1 byte of fingerprint + + std::uint32_t + m_dist_and_fingerprint; // upper 3 byte: distance to original bucket. lower byte: fingerprint from hash + std::size_t m_value_idx; // index into the m_values vector. + }); + + } // namespace bucket_type + + namespace detail { + + struct nonesuch {}; + struct default_container_t {}; + + template class Op, class... Args> + struct detector { + using value_t = std::false_type; + using type = Default; + }; + + template class Op, class... Args> + struct detector>, Op, Args...> { + using value_t = std::true_type; + using type = Op; + }; + + template class Op, class... Args> + using is_detected = typename detail::detector::value_t; + + template class Op, class... Args> + constexpr bool is_detected_v = is_detected::value; + + template + using detect_avalanching = typename T::is_avalanching; + + template + using detect_is_transparent = typename T::is_transparent; + + template + using detect_iterator = typename T::iterator; + + template + using detect_reserve = decltype(std::declval().reserve(std::size_t {})); + + // enable_if helpers + + template + constexpr bool is_map_v = !std::is_void_v; + + // clang-format off +template +constexpr bool is_transparent_v = is_detected_v && is_detected_v; + // clang-format on + + template + constexpr bool is_neither_convertible_v = + !std::is_convertible_v && !std::is_convertible_v; + + template + constexpr bool has_reserve = is_detected_v; + + // base type for map has mapped_type + template + struct base_table_type_map { + using mapped_type = T; + }; + + // base type for set doesn't have mapped_type + struct base_table_type_set {}; + + } // namespace detail + + // Very much like std::deque, but faster for indexing (in most cases). As of now this doesn't implement the full std::vector + // API, but merely what's necessary to work as an underlying container for ankerl::unordered_dense::{map, set}. + // It allocates blocks of equal size and puts them into the m_blocks vector. That means it can grow simply by adding a new + // block to the back of m_blocks, and doesn't double its size like an std::vector. The disadvantage is that memory is not + // linear and thus there is one more indirection necessary for indexing. + template< + typename T, + typename Allocator = std::allocator, + std::size_t MaxSegmentSizeBytes = 4096> + class segmented_vector { + template + class iter_t; + + public: + using allocator_type = Allocator; + using pointer = typename std::allocator_traits::pointer; + using const_pointer = typename std::allocator_traits::const_pointer; + using difference_type = typename std::allocator_traits::difference_type; + using value_type = T; + using size_type = std::size_t; + using reference = T&; + using const_reference = T const&; + using iterator = iter_t; + using const_iterator = iter_t; + + private: + using vec_alloc = typename std::allocator_traits::template rebind_alloc; + std::vector m_blocks {}; + std::size_t m_size {}; + + // Calculates the maximum number for x in (s << x) <= max_val + static constexpr auto num_bits_closest(std::size_t max_val, std::size_t s) -> std::size_t { + auto f = std::size_t { 0 }; + while (s << (f + 1) <= max_val) { + ++f; + } + return f; + } + + using self_t = segmented_vector; + static constexpr auto num_bits = num_bits_closest(MaxSegmentSizeBytes, sizeof(T)); + static constexpr auto num_elements_in_block = 1U << num_bits; + static constexpr auto mask = num_elements_in_block - 1U; + + /** + * Iterator class doubles as const_iterator and iterator + */ + template + class iter_t { + using ptr_t = std::conditional_t< + IsConst, + segmented_vector::const_pointer const*, + segmented_vector::pointer*>; + ptr_t m_data {}; + std::size_t m_idx {}; + + template + friend class iter_t; + + public: + using difference_type = segmented_vector::difference_type; + using value_type = segmented_vector::value_type; + using reference = std::conditional_t; + using pointer = std:: + conditional_t; + using iterator_category = std::forward_iterator_tag; + + iter_t() noexcept = default; + + template> + // NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions) + constexpr iter_t(iter_t const& other) noexcept: + m_data(other.m_data), + m_idx(other.m_idx) {} + + constexpr iter_t(ptr_t data, std::size_t idx) noexcept: m_data(data), m_idx(idx) {} + + template> + constexpr auto operator=(iter_t const& other) noexcept -> iter_t& { + m_data = other.m_data; + m_idx = other.m_idx; + return *this; + } + + constexpr auto operator++() noexcept -> iter_t& { + ++m_idx; + return *this; + } + + constexpr auto operator++(int) noexcept -> iter_t { + iter_t prev(*this); + this->operator++(); + return prev; + } + + constexpr auto operator--() noexcept -> iter_t& { + --m_idx; + return *this; + } + + constexpr auto operator--(int) noexcept -> iter_t { + iter_t prev(*this); + this->operator--(); + return prev; + } + + [[nodiscard]] constexpr auto operator+(difference_type diff) const noexcept -> iter_t { + return { m_data, + static_cast(static_cast(m_idx) + diff) }; + } + + constexpr auto operator+=(difference_type diff) noexcept -> iter_t& { + m_idx += diff; + return *this; + } + + [[nodiscard]] constexpr auto operator-(difference_type diff) const noexcept -> iter_t { + return { m_data, + static_cast(static_cast(m_idx) - diff) }; + } + + constexpr auto operator-=(difference_type diff) noexcept -> iter_t& { + m_idx -= diff; + return *this; + } + + template + [[nodiscard]] constexpr auto operator-(iter_t const& other) const noexcept + -> difference_type { + return static_cast(m_idx) + - static_cast(other.m_idx); + } + + constexpr auto operator*() const noexcept -> reference { + return m_data[m_idx >> num_bits][m_idx & mask]; + } + + constexpr auto operator->() const noexcept -> pointer { + return &m_data[m_idx >> num_bits][m_idx & mask]; + } + + template + [[nodiscard]] constexpr auto operator==(iter_t const& o) const noexcept -> bool { + return m_idx == o.m_idx; + } + + template + [[nodiscard]] constexpr auto operator!=(iter_t const& o) const noexcept -> bool { + return !(*this == o); + } + + template + [[nodiscard]] constexpr auto operator<(iter_t const& o) const noexcept -> bool { + return m_idx < o.m_idx; + } + + template + [[nodiscard]] constexpr auto operator>(iter_t const& o) const noexcept -> bool { + return o < *this; + } + + template + [[nodiscard]] constexpr auto operator<=(iter_t const& o) const noexcept -> bool { + return !(o < *this); + } + + template + [[nodiscard]] constexpr auto operator>=(iter_t const& o) const noexcept -> bool { + return !(*this < o); + } + }; + + // slow path: need to allocate a new segment every once in a while + void increase_capacity() { + auto ba = Allocator(m_blocks.get_allocator()); + pointer block = std::allocator_traits::allocate(ba, num_elements_in_block); + m_blocks.push_back(block); + } + + // Moves everything from other + void append_everything_from(segmented_vector&& other + ) { // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved) + reserve(size() + other.size()); + for (auto&& o: other) { + emplace_back(std::move(o)); + } + } + + // Copies everything from other + void append_everything_from(segmented_vector const& other) { + reserve(size() + other.size()); + for (auto const& o: other) { + emplace_back(o); + } + } + + void dealloc() { + auto ba = Allocator(m_blocks.get_allocator()); + for (auto ptr: m_blocks) { + std::allocator_traits::deallocate(ba, ptr, num_elements_in_block); + } + } + + [[nodiscard]] static constexpr auto calc_num_blocks_for_capacity(std::size_t capacity) { + return (capacity + num_elements_in_block - 1U) / num_elements_in_block; + } + + void resize_shrink(std::size_t new_size) { + if constexpr (!std::is_trivially_destructible_v) { + for (std::size_t ix = new_size; ix < m_size; ++ix) { + operator[](ix).~T(); + } + } + m_size = new_size; + } + + public: + segmented_vector() = default; + + // NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions) + segmented_vector(Allocator alloc): m_blocks(vec_alloc(alloc)) {} + + segmented_vector(segmented_vector&& other, Allocator alloc): segmented_vector(alloc) { + *this = std::move(other); + } + + segmented_vector(segmented_vector const& other, Allocator alloc): + m_blocks(vec_alloc(alloc)) { + append_everything_from(other); + } + + segmented_vector(segmented_vector&& other) noexcept: + segmented_vector(std::move(other), other.get_allocator()) {} + + segmented_vector(segmented_vector const& other) { + append_everything_from(other); + } + + auto operator=(segmented_vector const& other) -> segmented_vector& { + if (this == &other) { + return *this; + } + clear(); + append_everything_from(other); + return *this; + } + + auto operator=(segmented_vector&& other) noexcept -> segmented_vector& { + clear(); + dealloc(); + if (other.get_allocator() == get_allocator()) { + m_blocks = std::move(other.m_blocks); + m_size = std::exchange(other.m_size, {}); + } else { + // make sure to construct with other's allocator! + m_blocks = std::vector(vec_alloc(other.get_allocator())); + append_everything_from(std::move(other)); + } + return *this; + } + + ~segmented_vector() { + clear(); + dealloc(); + } + + [[nodiscard]] constexpr auto size() const -> std::size_t { + return m_size; + } + + [[nodiscard]] constexpr auto capacity() const -> std::size_t { + return m_blocks.size() * num_elements_in_block; + } + + // Indexing is highly performance critical + [[nodiscard]] constexpr auto operator[](std::size_t i) const noexcept -> T const& { + return m_blocks[i >> num_bits][i & mask]; + } + + [[nodiscard]] constexpr auto operator[](std::size_t i) noexcept -> T& { + return m_blocks[i >> num_bits][i & mask]; + } + + [[nodiscard]] constexpr auto begin() -> iterator { + return { m_blocks.data(), 0U }; + } + [[nodiscard]] constexpr auto begin() const -> const_iterator { + return { m_blocks.data(), 0U }; + } + [[nodiscard]] constexpr auto cbegin() const -> const_iterator { + return { m_blocks.data(), 0U }; + } + + [[nodiscard]] constexpr auto end() -> iterator { + return { m_blocks.data(), m_size }; + } + [[nodiscard]] constexpr auto end() const -> const_iterator { + return { m_blocks.data(), m_size }; + } + [[nodiscard]] constexpr auto cend() const -> const_iterator { + return { m_blocks.data(), m_size }; + } + + [[nodiscard]] constexpr auto back() -> reference { + return operator[](m_size - 1); + } + [[nodiscard]] constexpr auto back() const -> const_reference { + return operator[](m_size - 1); + } + + void pop_back() { + back().~T(); + --m_size; + } + + [[nodiscard]] auto empty() const { + return 0 == m_size; + } + + void reserve(std::size_t new_capacity) { + m_blocks.reserve(calc_num_blocks_for_capacity(new_capacity)); + while (new_capacity > capacity()) { + increase_capacity(); + } + } + + void resize(std::size_t const count) { + if (count < m_size) { + resize_shrink(count); + } else if (count > m_size) { + std::size_t const new_elems = count - m_size; + reserve(count); + for (std::size_t ix = 0; ix < new_elems; ++ix) { + emplace_back(); + } + } + } + + void resize(std::size_t const count, value_type const& value) { + if (count < m_size) { + resize_shrink(count); + } else if (count > m_size) { + std::size_t const new_elems = count - m_size; + reserve(count); + for (std::size_t ix = 0; ix < new_elems; ++ix) { + emplace_back(value); + } + } + } + + [[nodiscard]] auto get_allocator() const -> allocator_type { + return allocator_type { m_blocks.get_allocator() }; + } + + template + auto emplace_back(Args&&... args) -> reference { + if (m_size == capacity()) { + increase_capacity(); + } + auto* ptr = static_cast(&operator[](m_size)); + auto& ref = *new (ptr) T(std::forward(args)...); + ++m_size; + return ref; + } + + void clear() { + if constexpr (!std::is_trivially_destructible_v) { + for (std::size_t i = 0, s = size(); i < s; ++i) { + operator[](i).~T(); + } + } + m_size = 0; + } + + void shrink_to_fit() { + auto ba = Allocator(m_blocks.get_allocator()); + auto num_blocks_required = calc_num_blocks_for_capacity(m_size); + while (m_blocks.size() > num_blocks_required) { + std::allocator_traits::deallocate( + ba, + m_blocks.back(), + num_elements_in_block + ); + m_blocks.pop_back(); + } + m_blocks.shrink_to_fit(); + } + }; + + namespace detail { + + // This is it, the table. Doubles as map and set, and uses `void` for T when its used as a set. + template< + class Key, + class T, // when void, treat it as a set. + class Hash, + class KeyEqual, + class AllocatorOrContainer, + class Bucket, + class BucketContainer, + bool IsSegmented> + class table: + public std::conditional_t, base_table_type_map, base_table_type_set> { + using underlying_value_type = std::conditional_t, std::pair, Key>; + using underlying_container_type = std::conditional_t< + IsSegmented, + segmented_vector, + std::vector>; + + public: + using value_container_type = std::conditional_t< + is_detected_v, + AllocatorOrContainer, + underlying_container_type>; + + private: + using bucket_alloc = typename std::allocator_traits< + typename value_container_type::allocator_type>::template rebind_alloc; + using default_bucket_container_type = std::conditional_t< + IsSegmented, + segmented_vector, + std::vector>; + + using bucket_container_type = std::conditional_t< + std::is_same_v, + default_bucket_container_type, + BucketContainer>; + + static constexpr std::uint8_t initial_shifts = + 64 - 2; // 2^(64-m_shift) number of buckets + static constexpr float default_max_load_factor = 0.8F; + + public: + using key_type = Key; + using value_type = typename value_container_type::value_type; + using size_type = typename value_container_type::size_type; + using difference_type = typename value_container_type::difference_type; + using hasher = Hash; + using key_equal = KeyEqual; + using allocator_type = typename value_container_type::allocator_type; + using reference = typename value_container_type::reference; + using const_reference = typename value_container_type::const_reference; + using pointer = typename value_container_type::pointer; + using const_pointer = typename value_container_type::const_pointer; + using const_iterator = typename value_container_type::const_iterator; + using iterator = std:: + conditional_t, typename value_container_type::iterator, const_iterator>; + using bucket_type = Bucket; + + private: + using value_idx_type = decltype(Bucket::m_value_idx); + using dist_and_fingerprint_type = decltype(Bucket::m_dist_and_fingerprint); + + static_assert( + std::is_trivially_destructible_v, + "assert there's no need to call destructor / std::destroy" + ); + static_assert( + std::is_trivially_copyable_v, + "assert we can just memset / memcpy" + ); + + value_container_type + m_values {}; // Contains all the key-value pairs in one densely stored container. No holes. + bucket_container_type m_buckets {}; + std::size_t m_max_bucket_capacity = 0; + float m_max_load_factor = default_max_load_factor; + Hash m_hash {}; + KeyEqual m_equal {}; + std::uint8_t m_shifts = initial_shifts; + + [[nodiscard]] auto next(value_idx_type bucket_idx) const -> value_idx_type { + if (ANKERL_UNORDERED_DENSE_UNLIKELY(bucket_idx + 1U == bucket_count())) + ANKERL_UNORDERED_DENSE_UNLIKELY_ATTR { + return 0; + } + + return static_cast(bucket_idx + 1U); + } + + // Helper to access bucket through pointer types + [[nodiscard]] static constexpr auto + at(bucket_container_type& bucket, std::size_t offset) -> Bucket& { + return bucket[offset]; + } + + [[nodiscard]] static constexpr auto + at(const bucket_container_type& bucket, std::size_t offset) -> const Bucket& { + return bucket[offset]; + } + + // use the dist_inc and dist_dec functions so that std::uint16_t types work without warning + [[nodiscard]] static constexpr auto dist_inc(dist_and_fingerprint_type x) + -> dist_and_fingerprint_type { + return static_cast(x + Bucket::dist_inc); + } + + [[nodiscard]] static constexpr auto dist_dec(dist_and_fingerprint_type x) + -> dist_and_fingerprint_type { + return static_cast(x - Bucket::dist_inc); + } + + // The goal of mixed_hash is to always produce a high quality 64bit hash. + template + [[nodiscard]] constexpr auto mixed_hash(K const& key) const -> std::uint64_t { + if constexpr (is_detected_v) { + // we know that the hash is good because is_avalanching. + if constexpr (sizeof(decltype(m_hash(key))) < sizeof(std::uint64_t)) { + // 32bit hash and is_avalanching => multiply with a constant to avalanche bits upwards + return m_hash(key) * UINT64_C(0x9ddfea08eb382d69); + } else { + // 64bit and is_avalanching => only use the hash itself. + return m_hash(key); + } + } else { + // not is_avalanching => apply wyhash + return wyhash::hash(m_hash(key)); + } + } + + [[nodiscard]] constexpr auto dist_and_fingerprint_from_hash(std::uint64_t hash) const + -> dist_and_fingerprint_type { + return Bucket::dist_inc + | (static_cast(hash) & Bucket::fingerprint_mask); + } + + [[nodiscard]] constexpr auto bucket_idx_from_hash(std::uint64_t hash) const + -> value_idx_type { + return static_cast(hash >> m_shifts); + } + + [[nodiscard]] static constexpr auto get_key(value_type const& vt) -> key_type const& { + if constexpr (is_map_v) { + return vt.first; + } else { + return vt; + } + } + + template + [[nodiscard]] auto next_while_less(K const& key) const -> Bucket { + auto hash = mixed_hash(key); + auto dist_and_fingerprint = dist_and_fingerprint_from_hash(hash); + auto bucket_idx = bucket_idx_from_hash(hash); + + while (dist_and_fingerprint < at(m_buckets, bucket_idx).m_dist_and_fingerprint) { + dist_and_fingerprint = dist_inc(dist_and_fingerprint); + bucket_idx = next(bucket_idx); + } + return { dist_and_fingerprint, bucket_idx }; + } + + void place_and_shift_up(Bucket bucket, value_idx_type place) { + while (0 != at(m_buckets, place).m_dist_and_fingerprint) { + bucket = std::exchange(at(m_buckets, place), bucket); + bucket.m_dist_and_fingerprint = dist_inc(bucket.m_dist_and_fingerprint); + place = next(place); + } + at(m_buckets, place) = bucket; + } + + void erase_and_shift_down(value_idx_type bucket_idx) { + // shift down until either empty or an element with correct spot is found + auto next_bucket_idx = next(bucket_idx); + while (at(m_buckets, next_bucket_idx).m_dist_and_fingerprint >= Bucket::dist_inc * 2 + ) { + auto& next_bucket = at(m_buckets, next_bucket_idx); + at(m_buckets, bucket_idx) = { dist_dec(next_bucket.m_dist_and_fingerprint), + next_bucket.m_value_idx }; + bucket_idx = std::exchange(next_bucket_idx, next(next_bucket_idx)); + } + at(m_buckets, bucket_idx) = {}; + } + + [[nodiscard]] static constexpr auto calc_num_buckets(std::uint8_t shifts) + -> std::size_t { + return (std::min)(max_bucket_count(), std::size_t { 1 } << (64U - shifts)); + } + + [[nodiscard]] constexpr auto calc_shifts_for_size(std::size_t s) const -> std::uint8_t { + auto shifts = initial_shifts; + while (shifts > 0 + && static_cast( + static_cast(calc_num_buckets(shifts)) * max_load_factor() + ) < s) + { + --shifts; + } + return shifts; + } + + // assumes m_values has data, m_buckets=m_buckets_end=nullptr, m_shifts is INITIAL_SHIFTS + void copy_buckets(table const& other) { + // assumes m_values has already the correct data copied over. + if (empty()) { + // when empty, at least allocate an initial buckets and clear them. + allocate_buckets_from_shift(); + clear_buckets(); + } else { + m_shifts = other.m_shifts; + allocate_buckets_from_shift(); + if constexpr (IsSegmented || !std::is_same_v) + { + for (auto i = 0UL; i < bucket_count(); ++i) { + at(m_buckets, i) = at(other.m_buckets, i); + } + } else { + std::memcpy( + m_buckets.data(), + other.m_buckets.data(), + sizeof(Bucket) * bucket_count() + ); + } + } + } + + /** + * True when no element can be added any more without increasing the size + */ + [[nodiscard]] auto is_full() const -> bool { + return size() > m_max_bucket_capacity; + } + + void deallocate_buckets() { + m_buckets.clear(); + m_buckets.shrink_to_fit(); + m_max_bucket_capacity = 0; + } + + void allocate_buckets_from_shift() { + auto num_buckets = calc_num_buckets(m_shifts); + if constexpr (IsSegmented || !std::is_same_v) + { + if constexpr (has_reserve) { + m_buckets.reserve(num_buckets); + } + for (std::size_t i = m_buckets.size(); i < num_buckets; ++i) { + m_buckets.emplace_back(); + } + } else { + m_buckets.resize(num_buckets); + } + if (num_buckets == max_bucket_count()) { + // reached the maximum, make sure we can use each bucket + m_max_bucket_capacity = max_bucket_count(); + } else { + m_max_bucket_capacity = static_cast( + static_cast(num_buckets) * max_load_factor() + ); + } + } + + void clear_buckets() { + if constexpr (IsSegmented || !std::is_same_v) + { + for (auto&& e: m_buckets) { + std::memset(&e, 0, sizeof(e)); + } + } else { + std::memset(m_buckets.data(), 0, sizeof(Bucket) * bucket_count()); + } + } + + void clear_and_fill_buckets_from_values() { + clear_buckets(); + for (value_idx_type value_idx = 0, + end_idx = static_cast(m_values.size()); + value_idx < end_idx; + ++value_idx) + { + auto const& key = get_key(m_values[value_idx]); + auto [dist_and_fingerprint, bucket] = next_while_less(key); + + // we know for certain that key has not yet been inserted, so no need to check it. + place_and_shift_up({ dist_and_fingerprint, value_idx }, bucket); + } + } + + void increase_size() { + if (m_max_bucket_capacity == max_bucket_count()) { + // remove the value again, we can't add it! + m_values.pop_back(); + on_error_bucket_overflow(); + } + --m_shifts; + if constexpr (!IsSegmented || std::is_same_v) + { + deallocate_buckets(); + } + allocate_buckets_from_shift(); + clear_and_fill_buckets_from_values(); + } + + template + void do_erase(value_idx_type bucket_idx, Op handle_erased_value) { + auto const value_idx_to_remove = at(m_buckets, bucket_idx).m_value_idx; + erase_and_shift_down(bucket_idx); + handle_erased_value(std::move(m_values[value_idx_to_remove])); + + // update m_values + if (value_idx_to_remove != m_values.size() - 1) { + // no luck, we'll have to replace the value with the last one and update the index accordingly + auto& val = m_values[value_idx_to_remove]; + val = std::move(m_values.back()); + + // update the values_idx of the moved entry. No need to play the info game, just look until we find the values_idx + bucket_idx = bucket_idx_from_hash(mixed_hash(get_key(val))); + auto const values_idx_back = static_cast(m_values.size() - 1); + while (values_idx_back != at(m_buckets, bucket_idx).m_value_idx) { + bucket_idx = next(bucket_idx); + } + at(m_buckets, bucket_idx).m_value_idx = value_idx_to_remove; + } + m_values.pop_back(); + } + + template + auto do_erase_key(K&& key, Op handle_erased_value) + -> std::size_t { // NOLINT(cppcoreguidelines-missing-std-forward) + if (empty()) { + return 0; + } + + auto [dist_and_fingerprint, bucket_idx] = next_while_less(key); + + while (dist_and_fingerprint == at(m_buckets, bucket_idx).m_dist_and_fingerprint + && !m_equal(key, get_key(m_values[at(m_buckets, bucket_idx).m_value_idx]))) + { + dist_and_fingerprint = dist_inc(dist_and_fingerprint); + bucket_idx = next(bucket_idx); + } + + if (dist_and_fingerprint != at(m_buckets, bucket_idx).m_dist_and_fingerprint) { + return 0; + } + do_erase(bucket_idx, handle_erased_value); + return 1; + } + + template + auto do_insert_or_assign(K&& key, M&& mapped) -> std::pair { + auto it_isinserted = try_emplace(std::forward(key), std::forward(mapped)); + if (!it_isinserted.second) { + it_isinserted.first->second = std::forward(mapped); + } + return it_isinserted; + } + + template + auto do_place_element( + dist_and_fingerprint_type dist_and_fingerprint, + value_idx_type bucket_idx, + Args&&... args + ) -> std::pair { + // emplace the new value. If that throws an exception, no harm done; index is still in a valid state + m_values.emplace_back(std::forward(args)...); + + auto value_idx = static_cast(m_values.size() - 1); + if (ANKERL_UNORDERED_DENSE_UNLIKELY(is_full())) + ANKERL_UNORDERED_DENSE_UNLIKELY_ATTR { + increase_size(); + } + else { + place_and_shift_up({ dist_and_fingerprint, value_idx }, bucket_idx); + } + + // place element and shift up until we find an empty spot + return { begin() + static_cast(value_idx), true }; + } + + template + auto do_try_emplace(K&& key, Args&&... args) -> std::pair { + auto hash = mixed_hash(key); + auto dist_and_fingerprint = dist_and_fingerprint_from_hash(hash); + auto bucket_idx = bucket_idx_from_hash(hash); + + while (true) { + auto* bucket = &at(m_buckets, bucket_idx); + if (dist_and_fingerprint == bucket->m_dist_and_fingerprint) { + if (m_equal(key, get_key(m_values[bucket->m_value_idx]))) { + return { begin() + static_cast(bucket->m_value_idx), + false }; + } + } else if (dist_and_fingerprint > bucket->m_dist_and_fingerprint) { + return do_place_element( + dist_and_fingerprint, + bucket_idx, + std::piecewise_construct, + std::forward_as_tuple(std::forward(key)), + std::forward_as_tuple(std::forward(args)...) + ); + } + dist_and_fingerprint = dist_inc(dist_and_fingerprint); + bucket_idx = next(bucket_idx); + } + } + + template + auto do_find(K const& key) -> iterator { + if (ANKERL_UNORDERED_DENSE_UNLIKELY(empty())) + ANKERL_UNORDERED_DENSE_UNLIKELY_ATTR { + return end(); + } + + auto mh = mixed_hash(key); + auto dist_and_fingerprint = dist_and_fingerprint_from_hash(mh); + auto bucket_idx = bucket_idx_from_hash(mh); + auto* bucket = &at(m_buckets, bucket_idx); + + // unrolled loop. *Always* check a few directly, then enter the loop. This is faster. + if (dist_and_fingerprint == bucket->m_dist_and_fingerprint + && m_equal(key, get_key(m_values[bucket->m_value_idx]))) + { + return begin() + static_cast(bucket->m_value_idx); + } + dist_and_fingerprint = dist_inc(dist_and_fingerprint); + bucket_idx = next(bucket_idx); + bucket = &at(m_buckets, bucket_idx); + + if (dist_and_fingerprint == bucket->m_dist_and_fingerprint + && m_equal(key, get_key(m_values[bucket->m_value_idx]))) + { + return begin() + static_cast(bucket->m_value_idx); + } + dist_and_fingerprint = dist_inc(dist_and_fingerprint); + bucket_idx = next(bucket_idx); + bucket = &at(m_buckets, bucket_idx); + + while (true) { + if (dist_and_fingerprint == bucket->m_dist_and_fingerprint) { + if (m_equal(key, get_key(m_values[bucket->m_value_idx]))) { + return begin() + static_cast(bucket->m_value_idx); + } + } else if (dist_and_fingerprint > bucket->m_dist_and_fingerprint) { + return end(); + } + dist_and_fingerprint = dist_inc(dist_and_fingerprint); + bucket_idx = next(bucket_idx); + bucket = &at(m_buckets, bucket_idx); + } + } + + template + auto do_find(K const& key) const -> const_iterator { + return const_cast(this)->do_find(key + ); // NOLINT(cppcoreguidelines-pro-type-const-cast) + } + + template, bool> = true> + auto do_at(K const& key) -> Q& { + if (auto it = find(key); ANKERL_UNORDERED_DENSE_LIKELY(end() != it)) + ANKERL_UNORDERED_DENSE_LIKELY_ATTR { + return it->second; + } + on_error_key_not_found(); + } + + template, bool> = true> + auto do_at(K const& key) const -> Q const& { + return const_cast(this)->at(key + ); // NOLINT(cppcoreguidelines-pro-type-const-cast) + } + + public: + explicit table( + std::size_t bucket_count, + Hash const& hash = Hash(), + KeyEqual const& equal = KeyEqual(), + allocator_type const& alloc_or_container = allocator_type() + ): + m_values(alloc_or_container), + m_buckets(alloc_or_container), + m_hash(hash), + m_equal(equal) { + if (0 != bucket_count) { + reserve(bucket_count); + } else { + allocate_buckets_from_shift(); + clear_buckets(); + } + } + + table(): table(0) {} + + table(std::size_t bucket_count, allocator_type const& alloc): + table(bucket_count, Hash(), KeyEqual(), alloc) {} + + table(std::size_t bucket_count, Hash const& hash, allocator_type const& alloc): + table(bucket_count, hash, KeyEqual(), alloc) {} + + explicit table(allocator_type const& alloc): table(0, Hash(), KeyEqual(), alloc) {} + + template + table( + InputIt first, + InputIt last, + size_type bucket_count = 0, + Hash const& hash = Hash(), + KeyEqual const& equal = KeyEqual(), + allocator_type const& alloc = allocator_type() + ): + table(bucket_count, hash, equal, alloc) { + insert(first, last); + } + + template + table(InputIt first, InputIt last, size_type bucket_count, allocator_type const& alloc): + table(first, last, bucket_count, Hash(), KeyEqual(), alloc) {} + + template + table( + InputIt first, + InputIt last, + size_type bucket_count, + Hash const& hash, + allocator_type const& alloc + ): + table(first, last, bucket_count, hash, KeyEqual(), alloc) {} + + table(table const& other): table(other, other.m_values.get_allocator()) {} + + table(table const& other, allocator_type const& alloc): + m_values(other.m_values, alloc), + m_max_load_factor(other.m_max_load_factor), + m_hash(other.m_hash), + m_equal(other.m_equal) { + copy_buckets(other); + } + + table(table&& other) noexcept: + table(std::move(other), other.m_values.get_allocator()) {} + + table(table&& other, allocator_type const& alloc) noexcept: m_values(alloc) { + *this = std::move(other); + } + + table( + std::initializer_list ilist, + std::size_t bucket_count = 0, + Hash const& hash = Hash(), + KeyEqual const& equal = KeyEqual(), + allocator_type const& alloc = allocator_type() + ): + table(bucket_count, hash, equal, alloc) { + insert(ilist); + } + + table( + std::initializer_list ilist, + size_type bucket_count, + allocator_type const& alloc + ): + table(ilist, bucket_count, Hash(), KeyEqual(), alloc) {} + + table( + std::initializer_list init, + size_type bucket_count, + Hash const& hash, + allocator_type const& alloc + ): + table(init, bucket_count, hash, KeyEqual(), alloc) {} + + ~table() = default; + + auto operator=(table const& other) -> table& { + if (&other != this) { + deallocate_buckets( + ); // deallocate before m_values is set (might have another allocator) + m_values = other.m_values; + m_max_load_factor = other.m_max_load_factor; + m_hash = other.m_hash; + m_equal = other.m_equal; + m_shifts = initial_shifts; + copy_buckets(other); + } + return *this; + } + + auto operator=(table&& other + ) noexcept(noexcept(std::is_nothrow_move_assignable_v&& + std::is_nothrow_move_assignable_v&& + std::is_nothrow_move_assignable_v)) -> table& { + if (&other != this) { + deallocate_buckets( + ); // deallocate before m_values is set (might have another allocator) + m_values = std::move(other.m_values); + other.m_values.clear(); + + // we can only reuse m_buckets when both maps have the same allocator! + if (get_allocator() == other.get_allocator()) { + m_buckets = std::move(other.m_buckets); + other.m_buckets.clear(); + m_max_bucket_capacity = std::exchange(other.m_max_bucket_capacity, 0); + m_shifts = std::exchange(other.m_shifts, initial_shifts); + m_max_load_factor = + std::exchange(other.m_max_load_factor, default_max_load_factor); + m_hash = std::exchange(other.m_hash, {}); + m_equal = std::exchange(other.m_equal, {}); + other.allocate_buckets_from_shift(); + other.clear_buckets(); + } else { + // set max_load_factor *before* copying the other's buckets, so we have the same + // behavior + m_max_load_factor = other.m_max_load_factor; + + // copy_buckets sets m_buckets, m_num_buckets, m_max_bucket_capacity, m_shifts + copy_buckets(other); + // clear's the other's buckets so other is now already usable. + other.clear_buckets(); + m_hash = other.m_hash; + m_equal = other.m_equal; + } + // map "other" is now already usable, it's empty. + } + return *this; + } + + auto operator=(std::initializer_list ilist) -> table& { + clear(); + insert(ilist); + return *this; + } + + auto get_allocator() const noexcept -> allocator_type { + return m_values.get_allocator(); + } + + // iterators ////////////////////////////////////////////////////////////// + + auto begin() noexcept -> iterator { + return m_values.begin(); + } + + auto begin() const noexcept -> const_iterator { + return m_values.begin(); + } + + auto cbegin() const noexcept -> const_iterator { + return m_values.cbegin(); + } + + auto end() noexcept -> iterator { + return m_values.end(); + } + + auto cend() const noexcept -> const_iterator { + return m_values.cend(); + } + + auto end() const noexcept -> const_iterator { + return m_values.end(); + } + + // capacity /////////////////////////////////////////////////////////////// + + [[nodiscard]] auto empty() const noexcept -> bool { + return m_values.empty(); + } + + [[nodiscard]] auto size() const noexcept -> std::size_t { + return m_values.size(); + } + + [[nodiscard]] static constexpr auto max_size() noexcept -> std::size_t { + if constexpr ((std::numeric_limits::max)() == (std::numeric_limits::max)()) + { + return std::size_t { 1 } << (sizeof(value_idx_type) * 8 - 1); + } else { + return std::size_t { 1 } << (sizeof(value_idx_type) * 8); + } + } + + // modifiers ////////////////////////////////////////////////////////////// + + void clear() { + m_values.clear(); + clear_buckets(); + } + + auto insert(value_type const& value) -> std::pair { + return emplace(value); + } + + auto insert(value_type&& value) -> std::pair { + return emplace(std::move(value)); + } + + template< + class P, + std::enable_if_t, bool> = true> + auto insert(P&& value) -> std::pair { + return emplace(std::forward

(value)); + } + + auto insert(const_iterator /*hint*/, value_type const& value) -> iterator { + return insert(value).first; + } + + auto insert(const_iterator /*hint*/, value_type&& value) -> iterator { + return insert(std::move(value)).first; + } + + template< + class P, + std::enable_if_t, bool> = true> + auto insert(const_iterator /*hint*/, P&& value) -> iterator { + return insert(std::forward

(value)).first; + } + + template + void insert(InputIt first, InputIt last) { + while (first != last) { + insert(*first); + ++first; + } + } + + void insert(std::initializer_list ilist) { + insert(ilist.begin(), ilist.end()); + } + + // nonstandard API: *this is emptied. + // Also see "A Standard flat_map" https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p0429r9.pdf + auto extract() && -> value_container_type { + return std::move(m_values); + } + + // nonstandard API: + // Discards the internally held container and replaces it with the one passed. Erases non-unique elements. + auto replace(value_container_type&& container) { + if (ANKERL_UNORDERED_DENSE_UNLIKELY(container.size() > max_size())) + ANKERL_UNORDERED_DENSE_UNLIKELY_ATTR { + on_error_too_many_elements(); + } + auto shifts = calc_shifts_for_size(container.size()); + if (0 == bucket_count() || shifts < m_shifts + || container.get_allocator() != m_values.get_allocator()) + { + m_shifts = shifts; + deallocate_buckets(); + allocate_buckets_from_shift(); + } + clear_buckets(); + + m_values = std::move(container); + + // can't use clear_and_fill_buckets_from_values() because container elements might not be unique + auto value_idx = value_idx_type {}; + + // loop until we reach the end of the container. duplicated entries will be replaced with back(). + while (value_idx != static_cast(m_values.size())) { + auto const& key = get_key(m_values[value_idx]); + + auto hash = mixed_hash(key); + auto dist_and_fingerprint = dist_and_fingerprint_from_hash(hash); + auto bucket_idx = bucket_idx_from_hash(hash); + + bool key_found = false; + while (true) { + auto const& bucket = at(m_buckets, bucket_idx); + if (dist_and_fingerprint > bucket.m_dist_and_fingerprint) { + break; + } + if (dist_and_fingerprint == bucket.m_dist_and_fingerprint + && m_equal(key, get_key(m_values[bucket.m_value_idx]))) + { + key_found = true; + break; + } + dist_and_fingerprint = dist_inc(dist_and_fingerprint); + bucket_idx = next(bucket_idx); + } + + if (key_found) { + if (value_idx != static_cast(m_values.size() - 1)) { + m_values[value_idx] = std::move(m_values.back()); + } + m_values.pop_back(); + } else { + place_and_shift_up({ dist_and_fingerprint, value_idx }, bucket_idx); + ++value_idx; + } + } + } + + template, bool> = true> + auto insert_or_assign(Key const& key, M&& mapped) -> std::pair { + return do_insert_or_assign(key, std::forward(mapped)); + } + + template, bool> = true> + auto insert_or_assign(Key&& key, M&& mapped) -> std::pair { + return do_insert_or_assign(std::move(key), std::forward(mapped)); + } + + template< + typename K, + typename M, + typename Q = T, + typename H = Hash, + typename KE = KeyEqual, + std::enable_if_t && is_transparent_v, bool> = true> + auto insert_or_assign(K&& key, M&& mapped) -> std::pair { + return do_insert_or_assign(std::forward(key), std::forward(mapped)); + } + + template, bool> = true> + auto insert_or_assign(const_iterator /*hint*/, Key const& key, M&& mapped) -> iterator { + return do_insert_or_assign(key, std::forward(mapped)).first; + } + + template, bool> = true> + auto insert_or_assign(const_iterator /*hint*/, Key&& key, M&& mapped) -> iterator { + return do_insert_or_assign(std::move(key), std::forward(mapped)).first; + } + + template< + typename K, + typename M, + typename Q = T, + typename H = Hash, + typename KE = KeyEqual, + std::enable_if_t && is_transparent_v, bool> = true> + auto insert_or_assign(const_iterator /*hint*/, K&& key, M&& mapped) -> iterator { + return do_insert_or_assign(std::forward(key), std::forward(mapped)).first; + } + + // Single arguments for unordered_set can be used without having to construct the value_type + template< + class K, + typename Q = T, + typename H = Hash, + typename KE = KeyEqual, + std::enable_if_t && is_transparent_v, bool> = true> + auto emplace(K&& key) -> std::pair { + auto hash = mixed_hash(key); + auto dist_and_fingerprint = dist_and_fingerprint_from_hash(hash); + auto bucket_idx = bucket_idx_from_hash(hash); + + while (dist_and_fingerprint <= at(m_buckets, bucket_idx).m_dist_and_fingerprint) { + if (dist_and_fingerprint == at(m_buckets, bucket_idx).m_dist_and_fingerprint + && m_equal(key, m_values[at(m_buckets, bucket_idx).m_value_idx])) + { + // found it, return without ever actually creating anything + return { + begin( + ) + static_cast(at(m_buckets, bucket_idx).m_value_idx), + false + }; + } + dist_and_fingerprint = dist_inc(dist_and_fingerprint); + bucket_idx = next(bucket_idx); + } + + // value is new, insert element first, so when exception happens we are in a valid state + return do_place_element(dist_and_fingerprint, bucket_idx, std::forward(key)); + } + + template + auto emplace(Args&&... args) -> std::pair { + // we have to instantiate the value_type to be able to access the key. + // 1. emplace_back the object so it is constructed. 2. If the key is already there, pop it later in the loop. + auto& key = get_key(m_values.emplace_back(std::forward(args)...)); + auto hash = mixed_hash(key); + auto dist_and_fingerprint = dist_and_fingerprint_from_hash(hash); + auto bucket_idx = bucket_idx_from_hash(hash); + + while (dist_and_fingerprint <= at(m_buckets, bucket_idx).m_dist_and_fingerprint) { + if (dist_and_fingerprint == at(m_buckets, bucket_idx).m_dist_and_fingerprint + && m_equal(key, get_key(m_values[at(m_buckets, bucket_idx).m_value_idx]))) + { + m_values.pop_back(); // value was already there, so get rid of it + return { + begin( + ) + static_cast(at(m_buckets, bucket_idx).m_value_idx), + false + }; + } + dist_and_fingerprint = dist_inc(dist_and_fingerprint); + bucket_idx = next(bucket_idx); + } + + // value is new, place the bucket and shift up until we find an empty spot + auto value_idx = static_cast(m_values.size() - 1); + if (ANKERL_UNORDERED_DENSE_UNLIKELY(is_full())) + ANKERL_UNORDERED_DENSE_UNLIKELY_ATTR { + // increase_size just rehashes all the data we have in m_values + increase_size(); + } + else { + // place element and shift up until we find an empty spot + place_and_shift_up({ dist_and_fingerprint, value_idx }, bucket_idx); + } + return { begin() + static_cast(value_idx), true }; + } + + template + auto emplace_hint(const_iterator /*hint*/, Args&&... args) -> iterator { + return emplace(std::forward(args)...).first; + } + + template, bool> = true> + auto try_emplace(Key const& key, Args&&... args) -> std::pair { + return do_try_emplace(key, std::forward(args)...); + } + + template, bool> = true> + auto try_emplace(Key&& key, Args&&... args) -> std::pair { + return do_try_emplace(std::move(key), std::forward(args)...); + } + + template, bool> = true> + auto try_emplace(const_iterator /*hint*/, Key const& key, Args&&... args) -> iterator { + return do_try_emplace(key, std::forward(args)...).first; + } + + template, bool> = true> + auto try_emplace(const_iterator /*hint*/, Key&& key, Args&&... args) -> iterator { + return do_try_emplace(std::move(key), std::forward(args)...).first; + } + + template< + typename K, + typename... Args, + typename Q = T, + typename H = Hash, + typename KE = KeyEqual, + std::enable_if_t< + is_map_v< + Q> && is_transparent_v && is_neither_convertible_v, + bool> = true> + auto try_emplace(K&& key, Args&&... args) -> std::pair { + return do_try_emplace(std::forward(key), std::forward(args)...); + } + + template< + typename K, + typename... Args, + typename Q = T, + typename H = Hash, + typename KE = KeyEqual, + std::enable_if_t< + is_map_v< + Q> && is_transparent_v && is_neither_convertible_v, + bool> = true> + auto try_emplace(const_iterator /*hint*/, K&& key, Args&&... args) -> iterator { + return do_try_emplace(std::forward(key), std::forward(args)...).first; + } + + // Replaces the key at the given iterator with new_key. This does not change any other data in the underlying table, so + // all iterators and references remain valid. However, this operation can fail if new_key already exists in the table. + // In that case, returns {iterator to the already existing new_key, false} and no change is made. + // + // In the case of a set, this effectively removes the old key and inserts the new key at the same spot, which is more + // efficient than removing the old key and inserting the new key because it avoids repositioning the last element. + template + auto replace_key(iterator it, K&& new_key) -> std::pair { + auto const new_key_hash = mixed_hash(new_key); + + // first, check if new_key already exists and return if so + auto dist_and_fingerprint = dist_and_fingerprint_from_hash(new_key_hash); + auto bucket_idx = bucket_idx_from_hash(new_key_hash); + while (dist_and_fingerprint <= at(m_buckets, bucket_idx).m_dist_and_fingerprint) { + auto const& bucket = at(m_buckets, bucket_idx); + if (dist_and_fingerprint == bucket.m_dist_and_fingerprint + && m_equal(new_key, get_key(m_values[bucket.m_value_idx]))) + { + return { begin() + static_cast(bucket.m_value_idx), + false }; + } + dist_and_fingerprint = dist_inc(dist_and_fingerprint); + bucket_idx = next(bucket_idx); + } + + // const_cast is needed because iterator for the set is always const, so adding another get_key overload is not + // feasible. + auto& target_key = const_cast(get_key(*it)); + auto const old_key_bucket_idx = bucket_idx_from_hash(mixed_hash(target_key)); + + // Replace the key before doing any bucket changes. If it throws, no harm done, we are still in a valid state as we + // have not modified any buckets yet. + target_key = std::forward(new_key); + + auto const value_idx = static_cast(it - begin()); + + // Find the bucket containing our value_idx. It's guaranteed we find it, so no other stopping condition needed. + bucket_idx = old_key_bucket_idx; + while (value_idx != at(m_buckets, bucket_idx).m_value_idx) { + bucket_idx = next(bucket_idx); + } + erase_and_shift_down(bucket_idx); + + // place the new bucket + dist_and_fingerprint = dist_and_fingerprint_from_hash(new_key_hash); + bucket_idx = bucket_idx_from_hash(new_key_hash); + while (dist_and_fingerprint < at(m_buckets, bucket_idx).m_dist_and_fingerprint) { + dist_and_fingerprint = dist_inc(dist_and_fingerprint); + bucket_idx = next(bucket_idx); + } + place_and_shift_up({ dist_and_fingerprint, value_idx }, bucket_idx); + + return { it, true }; + } + + auto erase(iterator it) -> iterator { + auto hash = mixed_hash(get_key(*it)); + auto bucket_idx = bucket_idx_from_hash(hash); + + auto const value_idx_to_remove = static_cast(it - cbegin()); + while (at(m_buckets, bucket_idx).m_value_idx != value_idx_to_remove) { + bucket_idx = next(bucket_idx); + } + + do_erase(bucket_idx, [](value_type const& /*unused*/) -> void {}); + return begin() + static_cast(value_idx_to_remove); + } + + auto extract(iterator it) -> value_type { + auto hash = mixed_hash(get_key(*it)); + auto bucket_idx = bucket_idx_from_hash(hash); + + auto const value_idx_to_remove = static_cast(it - cbegin()); + while (at(m_buckets, bucket_idx).m_value_idx != value_idx_to_remove) { + bucket_idx = next(bucket_idx); + } + + auto tmp = std::optional {}; + do_erase(bucket_idx, [&tmp](value_type&& val) -> void { tmp = std::move(val); }); + return std::move(tmp).value(); + } + + template, bool> = true> + auto erase(const_iterator it) -> iterator { + return erase(begin() + (it - cbegin())); + } + + template, bool> = true> + auto extract(const_iterator it) -> value_type { + return extract(begin() + (it - cbegin())); + } + + auto erase(const_iterator first, const_iterator last) -> iterator { + auto const idx_first = first - cbegin(); + auto const idx_last = last - cbegin(); + auto const first_to_last = std::distance(first, last); + auto const last_to_end = std::distance(last, cend()); + + // remove elements from left to right which moves elements from the end back + auto const mid = idx_first + (std::min)(first_to_last, last_to_end); + auto idx = idx_first; + while (idx != mid) { + erase(begin() + idx); + ++idx; + } + + // all elements from the right are moved, now remove the last element until all done + idx = idx_last; + while (idx != mid) { + --idx; + erase(begin() + idx); + } + + return begin() + idx_first; + } + + auto erase(Key const& key) -> std::size_t { + return do_erase_key(key, [](value_type const& /*unused*/) -> void {}); + } + + auto extract(Key const& key) -> std::optional { + auto tmp = std::optional {}; + do_erase_key(key, [&tmp](value_type&& val) -> void { tmp = std::move(val); }); + return tmp; + } + + template< + class K, + class H = Hash, + class KE = KeyEqual, + std::enable_if_t, bool> = true> + auto erase(K&& key) -> std::size_t { + return do_erase_key(std::forward(key), [](value_type const& /*unused*/) -> void { + }); + } + + template< + class K, + class H = Hash, + class KE = KeyEqual, + std::enable_if_t, bool> = true> + auto extract(K&& key) -> std::optional { + auto tmp = std::optional {}; + do_erase_key(std::forward(key), [&tmp](value_type&& val) -> void { + tmp = std::move(val); + }); + return tmp; + } + + void swap(table& other + ) noexcept(noexcept(std::is_nothrow_swappable_v&& + std::is_nothrow_swappable_v&& + std::is_nothrow_swappable_v)) { + using std::swap; + swap(other, *this); + } + + // lookup ///////////////////////////////////////////////////////////////// + + template, bool> = true> + auto at(key_type const& key) -> Q& { + return do_at(key); + } + + template< + typename K, + typename Q = T, + typename H = Hash, + typename KE = KeyEqual, + std::enable_if_t && is_transparent_v, bool> = true> + auto at(K const& key) -> Q& { + return do_at(key); + } + + template, bool> = true> + auto at(key_type const& key) const -> Q const& { + return do_at(key); + } + + template< + typename K, + typename Q = T, + typename H = Hash, + typename KE = KeyEqual, + std::enable_if_t && is_transparent_v, bool> = true> + auto at(K const& key) const -> Q const& { + return do_at(key); + } + + template, bool> = true> + auto operator[](Key const& key) -> Q& { + return try_emplace(key).first->second; + } + + template, bool> = true> + auto operator[](Key&& key) -> Q& { + return try_emplace(std::move(key)).first->second; + } + + template< + typename K, + typename Q = T, + typename H = Hash, + typename KE = KeyEqual, + std::enable_if_t && is_transparent_v, bool> = true> + auto operator[](K&& key) -> Q& { + return try_emplace(std::forward(key)).first->second; + } + + auto count(Key const& key) const -> std::size_t { + return find(key) == end() ? 0 : 1; + } + + template< + class K, + class H = Hash, + class KE = KeyEqual, + std::enable_if_t, bool> = true> + auto count(K const& key) const -> std::size_t { + return find(key) == end() ? 0 : 1; + } + + auto find(Key const& key) -> iterator { + return do_find(key); + } + + auto find(Key const& key) const -> const_iterator { + return do_find(key); + } + + template< + class K, + class H = Hash, + class KE = KeyEqual, + std::enable_if_t, bool> = true> + auto find(K const& key) -> iterator { + return do_find(key); + } + + template< + class K, + class H = Hash, + class KE = KeyEqual, + std::enable_if_t, bool> = true> + auto find(K const& key) const -> const_iterator { + return do_find(key); + } + + auto contains(Key const& key) const -> bool { + return find(key) != end(); + } + + template< + class K, + class H = Hash, + class KE = KeyEqual, + std::enable_if_t, bool> = true> + auto contains(K const& key) const -> bool { + return find(key) != end(); + } + + auto equal_range(Key const& key) -> std::pair { + auto it = do_find(key); + return { it, it == end() ? end() : it + 1 }; + } + + auto equal_range(const Key& key) const -> std::pair { + auto it = do_find(key); + return { it, it == end() ? end() : it + 1 }; + } + + template< + class K, + class H = Hash, + class KE = KeyEqual, + std::enable_if_t, bool> = true> + auto equal_range(K const& key) -> std::pair { + auto it = do_find(key); + return { it, it == end() ? end() : it + 1 }; + } + + template< + class K, + class H = Hash, + class KE = KeyEqual, + std::enable_if_t, bool> = true> + auto equal_range(K const& key) const -> std::pair { + auto it = do_find(key); + return { it, it == end() ? end() : it + 1 }; + } + + // bucket interface /////////////////////////////////////////////////////// + + auto bucket_count() const noexcept -> std::size_t { // NOLINT(modernize-use-nodiscard) + return m_buckets.size(); + } + + static constexpr auto max_bucket_count() noexcept + -> std::size_t { // NOLINT(modernize-use-nodiscard) + return max_size(); + } + + // hash policy //////////////////////////////////////////////////////////// + + [[nodiscard]] auto load_factor() const -> float { + return bucket_count() + ? static_cast(size()) / static_cast(bucket_count()) + : 0.0F; + } + + [[nodiscard]] auto max_load_factor() const -> float { + return m_max_load_factor; + } + + void max_load_factor(float ml) { + m_max_load_factor = ml; + if (bucket_count() != max_bucket_count()) { + m_max_bucket_capacity = static_cast( + static_cast(bucket_count()) * max_load_factor() + ); + } + } + + void rehash(std::size_t count) { + count = (std::min)(count, max_size()); + auto shifts = calc_shifts_for_size((std::max)(count, size())); + if (shifts != m_shifts) { + m_shifts = shifts; + deallocate_buckets(); + m_values.shrink_to_fit(); + allocate_buckets_from_shift(); + clear_and_fill_buckets_from_values(); + } + } + + void reserve(std::size_t capa) { + capa = (std::min)(capa, max_size()); + if constexpr (has_reserve) { + // std::deque doesn't have reserve(). Make sure we only call when available + m_values.reserve(capa); + } + auto shifts = calc_shifts_for_size((std::max)(capa, size())); + if (0 == bucket_count() || shifts < m_shifts) { + m_shifts = shifts; + deallocate_buckets(); + allocate_buckets_from_shift(); + clear_and_fill_buckets_from_values(); + } + } + + // observers ////////////////////////////////////////////////////////////// + + auto hash_function() const -> hasher { + return m_hash; + } + + auto key_eq() const -> key_equal { + return m_equal; + } + + // nonstandard API: expose the underlying values container + [[nodiscard]] auto values() const noexcept -> value_container_type const& { + return m_values; + } + + // non-member functions /////////////////////////////////////////////////// + + friend auto operator==(table const& a, table const& b) -> bool { + if (&a == &b) { + return true; + } + if (a.size() != b.size()) { + return false; + } + for (auto const& b_entry: b) { + auto it = a.find(get_key(b_entry)); + if constexpr (is_map_v) { + // map: check that key is here, then also check that value is the same + if (a.end() == it || !(b_entry.second == it->second)) { + return false; + } + } else { + // set: only check that the key is here + if (a.end() == it) { + return false; + } + } + } + return true; + } + + friend auto operator!=(table const& a, table const& b) -> bool { + return !(a == b); + } + }; + + } // namespace detail + + template< + class Key, + class T, + class Hash = hash, + class KeyEqual = std::equal_to, + class AllocatorOrContainer = std::allocator>, + class Bucket = bucket_type::standard, + class BucketContainer = detail::default_container_t> + using map = + detail::table; + + template< + class Key, + class T, + class Hash = hash, + class KeyEqual = std::equal_to, + class AllocatorOrContainer = std::allocator>, + class Bucket = bucket_type::standard, + class BucketContainer = detail::default_container_t> + using segmented_map = + detail::table; + + template< + class Key, + class Hash = hash, + class KeyEqual = std::equal_to, + class AllocatorOrContainer = std::allocator, + class Bucket = bucket_type::standard, + class BucketContainer = detail::default_container_t> + using set = detail:: + table; + + template< + class Key, + class Hash = hash, + class KeyEqual = std::equal_to, + class AllocatorOrContainer = std::allocator, + class Bucket = bucket_type::standard, + class BucketContainer = detail::default_container_t> + using segmented_set = detail:: + table; + + #if defined(ANKERL_UNORDERED_DENSE_PMR) + + namespace pmr { + + template< + class Key, + class T, + class Hash = hash, + class KeyEqual = std::equal_to, + class Bucket = bucket_type::standard> + using map = detail::table< + Key, + T, + Hash, + KeyEqual, + ANKERL_UNORDERED_DENSE_PMR::polymorphic_allocator>, + Bucket, + detail::default_container_t, + false>; + + template< + class Key, + class T, + class Hash = hash, + class KeyEqual = std::equal_to, + class Bucket = bucket_type::standard> + using segmented_map = detail::table< + Key, + T, + Hash, + KeyEqual, + ANKERL_UNORDERED_DENSE_PMR::polymorphic_allocator>, + Bucket, + detail::default_container_t, + true>; + + template< + class Key, + class Hash = hash, + class KeyEqual = std::equal_to, + class Bucket = bucket_type::standard> + using set = detail::table< + Key, + void, + Hash, + KeyEqual, + ANKERL_UNORDERED_DENSE_PMR::polymorphic_allocator, + Bucket, + detail::default_container_t, + false>; + + template< + class Key, + class Hash = hash, + class KeyEqual = std::equal_to, + class Bucket = bucket_type::standard> + using segmented_set = detail::table< + Key, + void, + Hash, + KeyEqual, + ANKERL_UNORDERED_DENSE_PMR::polymorphic_allocator, + Bucket, + detail::default_container_t, + true>; + + } // namespace pmr + + #endif + + // deduction guides /////////////////////////////////////////////////////////// + + // deduction guides for alias templates are only possible since C++20 + // see https://en.cppreference.com/w/cpp/language/class_template_argument_deduction + +} // namespace ANKERL_UNORDERED_DENSE_NAMESPACE +} // namespace ankerl::unordered_dense + +// std extensions ///////////////////////////////////////////////////////////// + +namespace std { // NOLINT(cert-dcl58-cpp) + +template< + class Key, + class T, + class Hash, + class KeyEqual, + class AllocatorOrContainer, + class Bucket, + class Pred, + class BucketContainer, + bool IsSegmented> +// NOLINTNEXTLINE(cert-dcl58-cpp) +auto erase_if( + ankerl::unordered_dense::detail:: + table& + map, + Pred pred +) -> std::size_t { + using map_t = ankerl::unordered_dense::detail:: + table; + + // going back to front because erase() invalidates the end iterator + auto const old_size = map.size(); + auto idx = old_size; + while (idx) { + --idx; + auto it = map.begin() + static_cast(idx); + if (pred(*it)) { + map.erase(it); + } + } + + return old_size - map.size(); +} + +} // namespace std + +#endif +#endif \ No newline at end of file diff --git a/wust_vision-main/CMakeLists.txt b/wust_vision-main/CMakeLists.txt new file mode 100644 index 0000000..daf003d --- /dev/null +++ b/wust_vision-main/CMakeLists.txt @@ -0,0 +1,160 @@ +cmake_minimum_required(VERSION 3.14) +cmake_policy(SET CMP0072 NEW) +project(wust_vision LANGUAGES CXX) + + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) +set(CMAKE_BUILD_TYPE "Release") +message(STATUS "--------------------CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}--------------------") + + +option(BUILD_WITH_TRT "Enable TensorRT backend" ON) +option(BUILD_WITH_OPENVINO "Enable OpenVINO backend" ON) +option(BUILD_WITH_NCNN "Enable NCNN backend" OFF) +option(BUILD_WITH_ORT "Enable ORT backend" ON) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/cmake) + + +file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS tasks/*.cpp) + + +# list(FILTER SOURCES EXCLUDE REGEX "tasks/auto_guidance/.*") +# list(FILTER SOURCES EXCLUDE REGEX "tasks/auto_sniper/.*") + +find_package(OpenCV REQUIRED) +find_package(Eigen3 REQUIRED) +find_package(fmt REQUIRED) +find_package(yaml-cpp REQUIRED) +find_package(Ceres REQUIRED) +find_package(HikSDK REQUIRED) +find_package(wust_vl REQUIRED) + +set(BUILD_DEFINITIONS "") +set(BUILD_LIBS "") + + +if(BUILD_WITH_OPENVINO) + find_package(OpenVINO REQUIRED COMPONENTS Runtime ONNX) + list(APPEND BUILD_DEFINITIONS USE_OPENVINO) + list(APPEND BUILD_LIBS openvino::runtime openvino::frontend::onnx) +endif() + +if(BUILD_WITH_TRT) + find_package(CUDAToolkit REQUIRED) + set(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH};/home/hy/TensorRT-10.6.0.26") + find_package(TensorRT REQUIRED) + include_directories(${TensorRT_INCLUDE_DIR}) + include_directories(/usr/local/cuda/include) + set(CMAKE_CUDA_COMPILER /usr/local/cuda/bin/nvcc) + set(CMAKE_CUDA_ARCHITECTURES 86) + enable_language(CUDA) + set(CMAKE_CUDA_STANDARD 14) + set(CMAKE_CUDA_STANDARD_REQUIRED ON) + set(CMAKE_CUDA_COMPILER_LAUNCHER ccache) + add_subdirectory(${CMAKE_SOURCE_DIR}/cuda_infer ${CMAKE_BINARY_DIR}/cuda_infer_build) + list(APPEND BUILD_DEFINITIONS USE_TRT) + list(APPEND BUILD_LIBS TensorRT::TensorRT TensorRT::nvonnxparser CUDA::cudart cuda_infer) +endif() + +if(BUILD_WITH_NCNN) + find_package(ncnn REQUIRED) + list(APPEND BUILD_DEFINITIONS USE_NCNN) + list(APPEND BUILD_LIBS ncnn) +endif() + +if(BUILD_WITH_ORT) + set(ort_root_path "/home/hy/onnxruntime-linux-x64-gpu-1.22.0") + find_package(Ort REQUIRED) + include_directories(${Ort_INCLUDE_DIR}) + list(APPEND BUILD_DEFINITIONS USE_ORT) + list(APPEND BUILD_LIBS ${Ort_LIB}) +endif() + + +add_library(${PROJECT_NAME} SHARED ${SOURCES}) +target_compile_definitions(${PROJECT_NAME} PUBLIC ${BUILD_DEFINITIONS}) +target_include_directories(${PROJECT_NAME} PUBLIC + ${PROJECT_SOURCE_DIR} + ${OpenCV_INCLUDE_DIRS} +) +target_link_libraries(${PROJECT_NAME} + wust_vl::wust_vl + HikSDK::HikSDK + yaml-cpp + fmt::fmt + Eigen3::Eigen + Ceres::ceres + ${OpenCV_LIBS} + ${BUILD_LIBS} +) +set(ROS2_PACKAGES + ament_cmake + rosidl_typesupport_cpp + rclcpp + geometry_msgs + tf2_ros + tf2_geometry_msgs + sensor_msgs + visualization_msgs + sentry_interfaces + nav_msgs +) + + + +function(add_common_executable exe_name src_file) + add_executable(${exe_name} ${src_file}) + target_link_libraries(${exe_name} ${PROJECT_NAME}) +endfunction() +add_common_executable(standard src/standard.cpp) +# add_common_executable(dart src/dart.cpp) +add_common_executable(test_usbcamera test/test_usbcamera.cpp) + + +foreach(pkg IN LISTS ROS2_PACKAGES) + find_package(${pkg} QUIET) +endforeach() + + +set(ROS2_FULL_FOUND TRUE) +foreach(pkg IN LISTS ROS2_PACKAGES) + if(NOT ${pkg}_FOUND) + set(ROS2_FULL_FOUND FALSE) + message(WARNING "ROS2 package ${pkg} not found") + endif() +endforeach() + +if(ROS2_FULL_FOUND) + message(STATUS "ROS2 full environment found, compiling ROS2 targets ...") + list(APPEND BUILD_DEFINITIONS USE_ROS2) + + set(ROS2_INCLUDES) + foreach(pkg IN LISTS ROS2_PACKAGES) + if(TARGET ${pkg}::${pkg}) + list(APPEND ROS2_INCLUDES ${${pkg}_INCLUDE_DIRS}) + endif() + endforeach() + include_directories(${ROS2_INCLUDES}) + # find_package(Open3D REQUIRED) + + # target_link_libraries(${PROJECT_NAME} + # Open3D::Open3D + # ) + ament_target_dependencies(${PROJECT_NAME} ${ROS2_PACKAGES}) + target_compile_definitions(${PROJECT_NAME} PUBLIC ${BUILD_DEFINITIONS}) + macro(add_ros2_executable exe_name src_file) + add_executable(${exe_name} ${src_file}) + target_link_libraries(${exe_name} ${PROJECT_NAME} ) + endmacro() + + + add_ros2_executable(sentry src/sentry.cpp) + add_ros2_executable(nav test/nav.cpp) +else() + message(WARNING "ROS2 dependencies incomplete, skipping ROS2 targets ...") +endif() + diff --git a/wust_vision-main/README.md b/wust_vision-main/README.md new file mode 100644 index 0000000..b57077e --- /dev/null +++ b/wust_vision-main/README.md @@ -0,0 +1,252 @@ +# WUST_VISION +武汉科技大学崇实战队视觉代码仓库 + +## 写在前面 +本项目基于[中南大学FYT战队2024赛季视觉框架开源](https://github.com/CSU-FYT-Vision/FYT2024_vision),华南师范大学PIONEER战队@chenjunnn[rm_vision](https://github.com/chenjunnn/rm_vision)修改与适配,参考了深圳北理莫斯科大学北极熊战队/四川大学火锅战队/沈阳航空航天大学TUP战队/北京科技大学Reborn战队/同济大学superpower战队/河北科技大学Actor&Thinker战队的部分代码与模型,感谢以上开源为本队以及本人的帮助(排名不分先后) + +## 依赖 +* [wust_vl](https://github.com/WUST-RM/wust_vl) +* OpenCV +* [OpenVINO](https://flowus.cn/7a2a3341-74a1-4db9-bced-99fe5d05ab75)/[TensorRT-cuda](https://flowus.cn/e98af178-de0b-4546-808d-a6f1ff199d62)/[NCNN](https://flowus.cn/664f6bee-8ea9-4d54-8a78-e2c0bf38ee9f)/[OnnxRunetime](https://flowus.cn/8fbecbbf-c0f9-49bb-bac5-7b4923f55c99)连接为简单部署文档 +* fmt +* ceres +* Eigen3 +* nlohmann +* yaml-cpp +## 环境配置 +``` +./script/install_depences.sh +``` +## Quick Start +``` +git clone --recurse-submodules https://github.com/WUST-RM/wust_vision.git +cd wust_vision +sudo ./run.sh run xx /rebuild/build #编译并运行xx可执行文件/删除build缓存重新编译/仅编译 +``` +### 注意:本项目可选择编译OpenVINO/TensorRT-cuda/NCNN/OnnxRunetime,需在build缓存前在[CMakeLists.txt](CMakeLists.txt)中修改对应编译选项,修改后需rebuild重新编译,无OpenVINO/TensorRT-cuda/NCNN/OnnxRunetime环境仍可以使用OpenCV的装甲板识别,装甲板的识别方案需要在[config/auto_aim.yaml](config/auto_aim.yaml)中修改 +## 文件树 +``` +. +├── 3rdparty +│   ├── angles.h +│   └── backward-cpp +│ +├── cmake +│   ├── FindG2O.cmake +│   ├── FindHikSDK.cmake +│   ├── FindOrt.cmake +│   └── FindTensorRT.cmake +├── CMakeLists.txt +├── config +│   ├── auto_aim.yaml +│   ├── auto_buff.yaml +│   ├── auto_guidance.yaml +│   ├── auto_sniper.yaml +│   ├── camera_info.yaml +│   ├── camera.yaml +│   ├── common.yaml +│   ├── config -> /home/hy/wust_vision/config +│   ├── detect_ncnn.yaml +│   ├── detect_opencv.yaml +│   ├── detect_openvino.yaml +│   ├── detect_ort.yaml +│   ├── detect_trt.yaml +│   └── guard.sh +├── cuda_infer +│   ├── armor_infer.cu +│   ├── armor_infer.hpp +│   ├── CMakeLists.txt +│   ├── letter_box.cu +│   └── letter_box.hpp +├── env.bash +├── format.sh +├── KalmanHyLib +│   ├── adaptive_extended_kalman_filter.hpp +│   ├── error_state_extended_kalman_filter.hpp +│   ├── extended_kalman_filter.hpp +│   ├── kalman_hybird_lib.hpp +│   ├── README.md +│   └── unscented_kalman_filter.hpp +├── model +│ +├── README.md +├── read_shm_image_mmap_only.py +├── ros2 +│   ├── CMakeLists.txt +│   ├── ros2.cpp +│   └── ros2.hpp +├── run.sh +├── script +│   ├── install_depences.sh +│   ├── rsync.sh +│   ├── setup_devenv.sh +│   └── setup_service.sh +├── src +│   ├── dart.cpp +│   ├── hero.cpp +│   ├── sentry.cpp +│   ├── sim.cpp +│   └── standard.cpp +├── static +│   ├── 崇实战队logo图标.png +│   ├── css +│   │   └── style.css +│   ├── js +│   │   ├── chart_logic.js +│   │   ├── json_view.js +│   │   └── main.js +│   └── logo.JPG +├── tasks +│   ├── auto_aim +│   │   ├── armor_control +│   │   │   ├── aimer.cpp +│   │   │   ├── aimer.hpp +│   │   │   ├── planner.cpp +│   │   │   ├── planner.hpp +│   │   │   ├── shooter.cpp +│   │   │   ├── shooter.hpp +│   │   │   └── tinympc +│ │ │ +│   │   ├── armor_detect +│   │   │   ├── armor_detect_common.cpp +│   │   │   ├── armor_detect_common.hpp +│   │   │   ├── armor_detector_base.hpp +│   │   │   ├── armor_infer.cpp +│   │   │   ├── armor_infer.hpp +│   │   │   ├── armor_pose_estimator.cpp +│   │   │   ├── armor_pose_estimator.hpp +│   │   │   ├── detector_factory.hpp +│   │   │   ├── light_corner_corrector.cpp +│   │   │   ├── light_corner_corrector.hpp +│   │   │   ├── ncnn +│   │   │   │   ├── armor_detector_ncnn.cpp +│   │   │   │   ├── armor_detector_ncnn.hpp +│   │   │   │   ├── armor_detector_ncnn_wrapper.cpp +│   │   │   │   └── armor_detector_ncnn_wrapper.hpp +│   │   │   ├── number_classifier.cpp +│   │   │   ├── number_classifier.hpp +│   │   │   ├── onnxruntime +│   │   │   │   ├── armor_detector_onnxruntime.cpp +│   │   │   │   ├── armor_detector_onnxruntime.hpp +│   │   │   │   ├── armor_detector_onnxruntime_wrapper.cpp +│   │   │   │   └── armor_detector_onnxruntime_wrapper.hpp +│   │   │   ├── opencv +│   │   │   │   ├── armor_detector_opencv.cpp +│   │   │   │   ├── armor_detector_opencv.hpp +│   │   │   │   ├── armor_detector_opencv_wrapper.cpp +│   │   │   │   └── armor_detector_opencv_wrapper.hpp +│   │   │   ├── openvino +│   │   │   │   ├── armor_detector_openvino.cpp +│   │   │   │   ├── armor_detector_openvino.hpp +│   │   │   │   ├── armor_detector_openvino_wrapper.cpp +│   │   │   │   └── armor_detector_openvino_wrapper.hpp +│   │   │   └── tensorrt +│   │   │   ├── armor_detector_tensorrt.cpp +│   │   │   ├── armor_detector_tensorrt.hpp +│   │   │   ├── armor_detector_tensorrt_wrapper.cpp +│   │   │   └── armor_detector_tensorrt_wrapper.hpp +│   │   ├── armor_optimize +│   │   │   ├── ba_solver.cpp +│   │   │   └── ba_solver.hpp +│   │   ├── armor_tracker +│   │   │   ├── motion_models +│   │   │   │   ├── acc_model.hpp +│   │   │   │   ├── motion_modela.hpp +│   │   │   │   ├── motion_modelonea.hpp +│   │   │   │   ├── motion_modeloneca.hpp +│   │   │   │   ├── motion_modeloneypd.hpp +│   │   │   │   ├── motion_modelr.hpp +│   │   │   │   ├── motion_modelrypd.hpp +│   │   │   │   ├── motion_modelypd.hpp +│   │   │   │   └── motion_modelypdv2.hpp +│   │   │   ├── target.cpp +│   │   │   ├── target.hpp +│   │   │   ├── tracker_manager.cpp +│   │   │   ├── tracker_manager.hpp +│   │   │   ├── trackerv3.cpp +│   │   │   └── trackerv3.hpp +│   │   ├── auto_aim.cpp +│   │   ├── auto_aim_fsm.hpp +│   │   ├── auto_aim.hpp +│   │   ├── CMakeLists.txt +│   │   ├── type.cpp +│   │   └── type.hpp +│   ├── auto_buff +│   │   ├── auto_buff.cpp +│   │   ├── auto_buff.hpp +│   │   ├── CMakeLists.txt +│   │   ├── rune_control +│   │   │   ├── aimer.cpp +│   │   │   └── aimer.hpp +│   │   ├── rune_detector +│   │   │   ├── rune_detector.cpp +│   │   │   └── rune_detector.hpp +│   │   ├── rune_optimize +│   │   │   ├── ba_solver.cpp +│   │   │   └── ba_solver.hpp +│   │   ├── rune_tracker +│   │   │   ├── motion_models +│   │   │   │   └── motion_modelrypd.hpp +│   │   │   ├── rune_target.cpp +│   │   │   ├── rune_target.hpp +│   │   │   ├── rune_tracker.cpp +│   │   │   ├── rune_tracker.hpp +│   │   │   └── spd_fitter.hpp +│   │   ├── type.cpp +│   │   └── type.hpp +│   ├── auto_guidance +│   │   ├── auto_guidance.cpp +│   │   ├── auto_guidance.hpp +│   │   ├── CMakeLists.txt +│   │   ├── debug.cpp +│   │   ├── debug.hpp +│   │   ├── guidance_detector +│   │   │   ├── detector_base.hpp +│   │   │   ├── detector_factory.hpp +│   │   │   ├── green_light_infer.cpp +│   │   │   ├── green_light_infer.hpp +│   │   │   ├── opencv +│   │   │   │   ├── guidance_detector_opencv.cpp +│   │   │   │   └── guidance_detector_opencv.hpp +│   │   │   └── openvino +│   │   │   ├── guidance_detector_openvino.cpp +│   │   │   └── guidance_detector_openvino.hpp +│   │   ├── guidance_tracker +│   │   │   ├── guidance_target.cpp +│   │   │   ├── guidance_target.hpp +│   │   │   ├── guidance_tracker.cpp +│   │   │   ├── guidance_tracker.hpp +│   │   │   └── motion_models +│   │   │   └── imgbox_model.hpp +│   │   └── type.hpp +│   ├── auto_offset +│   │   ├── auto_offset.cpp +│   │   ├── auto_offset.hpp +│   │   └── CMakeLists.txt +│   ├── auto_sniper +│   │   ├── auto_sniper.cpp +│   │   ├── auto_sniper.hpp +│   │   ├── CMakeLists.txt +│   │   └── trajectory_solver.hpp +│   ├── CMakeLists.txt +│   ├── debug.cpp +│   ├── debug.hpp +│   ├── main_base.hpp +│   ├── packet_typedef.hpp +│   ├── sinple_img_rotate_saver.hpp +│   ├── type_common.cpp +│   ├── type_common.hpp +│   ├── utils.cpp +│   ├── utils.hpp +│   ├── vision_base.cpp +│   └── vision_base.hpp +├── templates +│   └── index.html +├── test +│   ├── control.cpp +│   └── test_usbcamera.cpp +└── web.py + + + +``` diff --git a/wust_vision-main/cmake/FindHikSDK.cmake b/wust_vision-main/cmake/FindHikSDK.cmake new file mode 100644 index 0000000..e07ba0d --- /dev/null +++ b/wust_vision-main/cmake/FindHikSDK.cmake @@ -0,0 +1,147 @@ +# -------------------------------------------------------------------------------------------- +# FindHikSDK.cmake +# +# This module finds the HikRobot / Hikvision MVS Camera SDK. +# +# It defines the following variables: +# HikSDK_FOUND +# HikSDK_INCLUDE_DIR +# HikSDK_LIB +# +# And the following imported target: +# hiksdk +# -------------------------------------------------------------------------------------------- + +# ========================= +# 1. SDK 根路径 +# ========================= +if(WIN32) + set(HikSDK_Path "$ENV{MVCAM_COMMON_RUNENV}") +else() + set(HikSDK_Path "$ENV{MVCAM_SDK_PATH}") +endif() + +if(NOT HikSDK_Path OR HikSDK_Path STREQUAL "") + message(STATUS "HikSDK: MVCAM_SDK_PATH is not set") + set(HikSDK_FOUND FALSE) + return() +endif() + +# ========================= +# 2. 查找头文件 +# ========================= +find_path( + HikSDK_INCLUDE_DIR + NAMES + MvCameraControl.h + CameraParams.h + PixelType.h + MvErrorDefine.h + MvISPErrorDefine.h + PATHS + "${HikSDK_Path}/include" + "${HikSDK_Path}/Includes" + NO_DEFAULT_PATH +) + +# ========================= +# 3. 查找库文件(关键修复点) +# ========================= +if(UNIX) + find_library( + HikSDK_LIB + NAMES + MvCameraControl + libMvCameraControl.so + PATHS + "${HikSDK_Path}/lib" + "${HikSDK_Path}/lib64" + "${HikSDK_Path}/lib/arm" + "${HikSDK_Path}/lib/arm64" + "${HikSDK_Path}/lib/aarch64" + "${HikSDK_Path}/lib/x86" + "${HikSDK_Path}/lib/x64" + "${HikSDK_Path}/lib/64" + "${HikSDK_Path}/lib/32" + NO_DEFAULT_PATH + ) +endif() + +# ========================= +# 4. Windows(完整但不影响 Linux) +# ========================= +if(WIN32) + find_library( + HikSDK_LIB + NAMES MvCameraControl + PATHS + "${HikSDK_Path}/Libraries" + "${HikSDK_Path}/Libraries/win64" + "${HikSDK_Path}/Libraries/win32" + NO_DEFAULT_PATH + ) + + find_file( + HikSDK_DLL + NAMES MvCameraControl.dll + PATHS + "${HikSDK_Path}/Runtime" + "C:/Program Files (x86)/Common Files/MVS/Runtime" + NO_DEFAULT_PATH + ) +endif() + +# ========================= +# 5. 创建导入库目标 +# ========================= +if(HikSDK_LIB AND HikSDK_INCLUDE_DIR) + + if(NOT TARGET HikSDK::HikSDK) + + add_library(HikSDK::HikSDK SHARED IMPORTED GLOBAL) + + if(WIN32) + set_target_properties(HikSDK::HikSDK PROPERTIES + IMPORTED_IMPLIB "${HikSDK_LIB}" + IMPORTED_LOCATION "${HikSDK_DLL}" + INTERFACE_INCLUDE_DIRECTORIES "${HikSDK_INCLUDE_DIR}" + ) + else() + set_target_properties(HikSDK::HikSDK PROPERTIES + IMPORTED_LOCATION "${HikSDK_LIB}" + INTERFACE_INCLUDE_DIRECTORIES "${HikSDK_INCLUDE_DIR}" + ) + endif() + + endif() + +endif() +set(HikSDK_LIBS HikSDK::HikSDK) +set(HikSDK_INCLUDE_DIRS ${HikSDK_INCLUDE_DIR}) + + +# ========================= +# 6. 标准 find_package 处理 +# ========================= +include(FindPackageHandleStandardArgs) + +find_package_handle_standard_args( + HikSDK + REQUIRED_VARS HikSDK_LIB HikSDK_INCLUDE_DIR +) + +# ========================= +# 7. 调试输出(很重要) +# ========================= +if(HikSDK_FOUND) + message(STATUS "HikSDK found:") + message(STATUS " Include dir : ${HikSDK_INCLUDE_DIR}") + message(STATUS " Library : ${HikSDK_LIB}") +else() + message(STATUS "HikSDK NOT found") +endif() + +set(HikSDK_LIBS hiksdk) +set(HikSDK_INCLUDE_DIRS ${HikSDK_INCLUDE_DIR}) + +mark_as_advanced(HikSDK_LIB HikSDK_INCLUDE_DIR HikSDK_DLL) diff --git a/wust_vision-main/cmake/FindOrt.cmake b/wust_vision-main/cmake/FindOrt.cmake new file mode 100644 index 0000000..ebe75bb --- /dev/null +++ b/wust_vision-main/cmake/FindOrt.cmake @@ -0,0 +1,88 @@ +# -------------------------------------------------------------------------------------------- +# This file is used to find the ONNX-Runtime SDK, which provides the following variables: +# +# Cache Variables: +# - Ort_HEADER_FILES: Names of SDK header files +# +# Advanced Variables: +# - Ort_INCLUDE_DIR: Directory where SDK header files are located +# - Ort_LIB: Path to the SDK library file (import library on Windows, shared library +# on Linux) +# - Ort_DLL: Path to the SDK dynamic library file (only on Windows) +# +# Local Variables: +# - Ort_LIBS: CMake target name for the SDK, which is "onnxruntime" +# - Ort_INCLUDE_DIRS: Directory where SDK header files are located +# -------------------------------------------------------------------------------------------- + +# ------------------------------------------------------------------------------ +# find onnxruntime root path +# ------------------------------------------------------------------------------ +if(NOT ort_root_path) + set(ort_root_path "/usr/local") +endif() + +# ------------------------------------------------------------------------------ +# find onnxruntime include directory +# ------------------------------------------------------------------------------ +set(Ort_HEADER_FILES + cpu_provider_factory.h onnxruntime_run_options_config_keys.h + onnxruntime_c_api.h onnxruntime_session_options_config_keys.h + onnxruntime_cxx_api.h provider_options.h + onnxruntime_cxx_inline.h + CACHE INTERNAL "ONNX Runtime header files" +) + +find_path( + Ort_INCLUDE_DIR + PATHS "${ort_root_path}/include" + NAMES ${Ort_HEADER_FILES} + NO_DEFAULT_PATH +) + +# ------------------------------------------------------------------------------ +# find onnxruntime library file +# ------------------------------------------------------------------------------ +find_library( + Ort_LIB + NAMES "libonnxruntime.so" + PATHS "${ort_root_path}/lib" + NO_DEFAULT_PATH +) + +# ------------------------------------------------------------------------------ +# create imported target: onnxruntime +# ------------------------------------------------------------------------------ +if(NOT TARGET onnxruntime) + add_library(onnxruntime SHARED IMPORTED) + set_target_properties(onnxruntime PROPERTIES + IMPORTED_LOCATION "${Ort_LIB}" + INTERFACE_INCLUDE_DIRECTORIES "${Ort_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(Ort_INCLUDE_DIR Ort_LIB) + +# ------------------------------------------------------------------------------ +# set onnxruntime cmake variables and version variables +# ------------------------------------------------------------------------------ +set(Ort_LIBS "onnxruntime") +set(Ort_INCLUDE_DIRS "${Ort_INCLUDE_DIR}") + +if(Ort_INCLUDE_DIR) + file(STRINGS "${Ort_INCLUDE_DIR}/onnxruntime_c_api.h" Ort_VERSION + REGEX "#define ORT_API_VERSION [0-9]+" + ) + string(REGEX REPLACE "#define ORT_API_VERSION ([0-9]+)" "1.\\1" Ort_VERSION "${Ort_VERSION}") +endif() + +# ------------------------------------------------------------------------------ +# handle the package +# ------------------------------------------------------------------------------ +include(FindPackageHandleStandardArgs) + +find_package_handle_standard_args( + Ort + VERSION_VAR Ort_VERSION + REQUIRED_VARS Ort_LIB Ort_INCLUDE_DIR +) \ No newline at end of file diff --git a/wust_vision-main/cmake/FindTensorRT.cmake b/wust_vision-main/cmake/FindTensorRT.cmake new file mode 100644 index 0000000..e57a6e8 --- /dev/null +++ b/wust_vision-main/cmake/FindTensorRT.cmake @@ -0,0 +1,66 @@ +# FindTensorRT.cmake -- Locate NVIDIA TensorRT + +include(FindPackageHandleStandardArgs) + +# TensorRT root +if (DEFINED TensorRT_ROOT) + list(APPEND _TensorRT_SEARCH_PATHS + ${TensorRT_ROOT} + "$ENV{TensorRT_ROOT}" + ) +endif() +list(APPEND _TensorRT_SEARCH_PATHS /usr /usr/local) + +# Header +find_path(TensorRT_INCLUDE_DIR + NAMES NvInfer.h + PATHS ${_TensorRT_SEARCH_PATHS} + PATH_SUFFIXES include +) + +# Core library +find_library(TensorRT_LIBRARY + NAMES nvinfer + PATHS ${_TensorRT_SEARCH_PATHS} + PATH_SUFFIXES lib lib64 lib/x64 +) + +find_package_handle_standard_args(TensorRT + REQUIRED_VARS TensorRT_INCLUDE_DIR TensorRT_LIBRARY +) + +if (TensorRT_FOUND) + set(TensorRT_INCLUDE_DIRS ${TensorRT_INCLUDE_DIR}) + set(TensorRT_LIBRARIES ${TensorRT_LIBRARY}) + + # Optional components + foreach(_comp IN ITEMS nvinfer_plugin nvonnxparser nvparsers) + find_library(TensorRT_${_comp}_LIBRARY + NAMES ${_comp} + PATHS ${_TensorRT_SEARCH_PATHS} + PATH_SUFFIXES lib lib64 lib/x64 + ) + if (TensorRT_${_comp}_LIBRARY) + list(APPEND TensorRT_LIBRARIES ${TensorRT_${_comp}_LIBRARY}) + endif() + endforeach() + + # Core target + add_library(TensorRT::TensorRT UNKNOWN IMPORTED) + set_target_properties(TensorRT::TensorRT PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${TensorRT_INCLUDE_DIRS}" + IMPORTED_LOCATION "${TensorRT_LIBRARY}" + ) + + # Component targets + foreach(_comp IN ITEMS nvinfer_plugin nvonnxparser nvparsers) + if (TensorRT_${_comp}_LIBRARY) + add_library(TensorRT::${_comp} UNKNOWN IMPORTED) + set_target_properties(TensorRT::${_comp} PROPERTIES + IMPORTED_LOCATION "${TensorRT_${_comp}_LIBRARY}" + ) + endif() + endforeach() + + message(STATUS "Found TensorRT at ${TensorRT_INCLUDE_DIR}") +endif() diff --git a/wust_vision-main/config/auto_aim.yaml b/wust_vision-main/config/auto_aim.yaml new file mode 100644 index 0000000..cdf0f25 --- /dev/null +++ b/wust_vision-main/config/auto_aim.yaml @@ -0,0 +1,131 @@ +armor_detect_backend: tensorrt +max_detect_armors: 11 + + +armor_map: + SENTRY: 4 + NO1: 2 + NO2: 3 + NO3: 1 + NO4: 5 + NO5: 9 + OUTPOST: 6 + BASE: 7 + UNKNOWN: -1 + +armor_where: + yaw_opt: + mode: golden + golden_search_side_deg: 60 + distance_fix_a2: 0.0 + +armor_tracker: + lost_time_thres: 2.0 + tracking_thres: 10 + max_yaw_diff_deg: 60.0 + max_dis_diff: 3.0 + match_gate: 50 + qxyz_common: [200.0, 200.0, 1.0] + qyaw_common: 100.0 + qxyz_output: [10.0, 10.0, 0.5] + qyaw_output: 0.01 + q_r: 0.0000001 + q_l: 0.0000001 + q_h: 0.0000001 + q_outpost_dz: 0.5 + yp_r: 2e-3 + dis_r_front: 0.5 + dis_r_side: 2.5 + dis2_r_ratio: 0.1 + yaw_r_base_front: 0.2 + yaw_r_base_side: 0.06 + yaw_r_log_ratio: 0.02 + esekf_iter_num: 5 + + + +auto_aim_fsm: + single_whole_up: 1.5 + single_whole_down: 1.0 + whole_pair_up: 7.5 + whole_pair_down: 6.5 #1s内至少换2次板子 2pi 否则轨迹规划无意义 + pair_center_up: 16.5 + pair_center_down: 15.0 + transfer_thresh: 50 + +very_aimer: + fuck_test: false + fuck_test_thresh: 0.5 + type: "seg" #seg or mpc + + sample_total_time: 2.0 + + sample_horizon: 500 + + control_delay: 0.2 + delay_enable_fire_error: 0.0035 + + max_yaw_acc: 40 + #全向yaw加速度测算37 + max_pitch_acc: 25 + + + prediction_delay: 0.00 + comming_angle: 60 + leaving_angle: 20 + + yaw_limit_deg: 60 + shooting_range_h: 0.12 + shooting_range_small_w: 0.12 + shooting_range_big_w: 0.24 + min_enable_pitch_deg: 0.25 + min_enable_yaw_deg: 0.25 + base_offset: + yaw: -1.0 + pitch: -3.5 + trajectory_offset: + - d_max: 3 + d_min: 0 + h_max: 1.5 + h_min: -1 + pitch_off: 0 + yaw_off: 0 + - d_max: 4.5 + d_min: 3 + h_max: 1.5 + h_min: -1 + pitch_off: 0 + yaw_off: 0 + - d_max: 4.5 + d_min: 9 + h_max: 1.5 + h_min: -1 + pitch_off: 0 + yaw_off: 0 + #---mpc only + max_iter: 10 + Q_yaw: [7e6, 0] + R_yaw: [3.0] + Q_pitch: [7e6, 0] + R_pitch: [3.0] + + + +trajectory_compensator: + compenstator_type: resistance + gravity: 9.8 + iteration_times: 20 + resistance: 0.092 + k1: 0.0190 #大弹丸double k1_c = 0.47; double k1 = k1_c * 1.169 * (2 * M_PI * 0.02125 * 0.02125) / 2 / 0.041; + + + +auto_exposure: + enable: true + target_brightness: 25.0 + tolerance: 3.0 + step_gain: 15.0 + decay_step: 1.0 + exposure_min: 100.0 + exposure_max: 1500.0 + control_interval_ms: 300 diff --git a/wust_vision-main/config/auto_buff.yaml b/wust_vision-main/config/auto_buff.yaml new file mode 100644 index 0000000..dfbb63a --- /dev/null +++ b/wust_vision-main/config/auto_buff.yaml @@ -0,0 +1,76 @@ +rune_detector: + rune_center_min_area: 10 + rune_center_max_area: 2000 + rune_center_1x1ratio_tol: 0.7 + rune_center_fill_ratio_min: 0.3 + + rune_target_min_area: 100 + rune_target_max_area: 3000 + rune_target_max_square_ratio: 1.3 + rune_target_cluster_radius: 70 + + bin_threshold: 50 + color_diff_threshold: 40 + + +rune_where: + roll_opt: + mode: golden + golden_search_side_deg: 60 + +rune_tracker: + lost_time_thres: 2.0 + tracking_thres: 5 + max_dis_diff: 1.0 + match_gate: 15.0 + esekf_iter_num: 2 + q_roll: 10.0 + q_xyz: 0.5 + q_yaw: 0.5 + yp_r: 0.01 + dis_r: 0.05 + yaw_r: 0.1 + roll_r: 0.2 + big_window_sec: 2.0 + +aimer: + prediction_delay: -0.00 + shooting_range_h: 0.1 + shooting_range_w: 0.1 + min_enable_pitch_deg: 0.25 + min_enable_yaw_deg: 0.25 + base_offset: + yaw: -3.0 + pitch: -0.0 + trajectory_offset: + - d_max: 9 + d_min: 0 + h_max: 1.5 + h_min: -1 + pitch_off: -0 + yaw_off: -0 + - d_max: 10 + d_min: 9 + h_max: 0.4 + h_min: -1 + pitch_off: -0 + yaw_off: 0 + +trajectory_compensator: + compenstator_type: resistance + gravity: 9.8 + iteration_times: 20 + resistance: 0.092 + k1: 0.0190 #大弹丸double k1_c = 0.47; double k1 = k1_c * 1.169 * (2 * M_PI * 0.02125 * 0.02125) / 2 / 0.041; + + + +auto_exposure: + enable: false + target_brightness: 15.0 + tolerance: 3.0 + step_gain: 15.0 + decay_step: 1.0 + exposure_min: 100.0 + exposure_max: 2500.0 + control_interval_ms: 300 diff --git a/wust_vision-main/config/auto_guidance.yaml b/wust_vision-main/config/auto_guidance.yaml new file mode 100644 index 0000000..36bdbc2 --- /dev/null +++ b/wust_vision-main/config/auto_guidance.yaml @@ -0,0 +1,83 @@ +backend: opencv + +max_infer_running: 6 + +detector: + openvino: + model_path: /home/hy/2026_dart_guide/assets/Katrin.xml + top_k: 128 + conf_threshold: 0.2 + nms_threshold: 0.5 + device_type: CPU + use_throughputmode: false + opencv: + gui: true + HSV: + lowH: 25 + highH: 85 + lowS: 50 + highS: 255 + lowV: 150 + highV: 255 + contours: + min_area: 100 + max_area: 50000 + min_aspect_ratio: 0.5 + min_fill_ratio: 0.7 + +tracker: + lost_time_thres: 2.0 + tracking_thres: 10 + target: + xy_r: 0.05 + wh_r: 0.05 + q_xy: 100 + q_wh: 100 + iter_num: 2 + max_dis_diff: 2.0 + + +logger: + log_level: INFO + log_path: /home/hy/wust_log + use_logcli: true + use_logfile: true + use_simplelog: true + +control: + control_rate: 100 + communication_delay_us: 1100 + pitch_avg_windows: 1 + device_name: /dev/ttyACM_RMc + use_serial: true + + + +camera: + type: hik_camera + camera_info_path: ${VISION_ROOT}/config/camera_info.yaml + + hik_camera: + target_sn: DA1094119 #DA3038891 + acquisition_frame_rate: 80 + adc_bit_depth: Bits_12 + exposure_time: 5000 + gain: 16.9 + gamma: 0.5 #Bayer用不了 + pixel_format: BayerRG8 + use_raw: false + acquisition_frame_rate_enable: false + reverse_x: false + reverse_y: false + + video_player: + fps: 12 + loop: true + use_cvt: true + #path: /home/hy/wust_data/video/aaa.mp4 + #path: /home/hy/wust_data/video/jiao.avi + #path: /home/hy/Z_LION_AutoAim2025/src/auto_aim_publisher/videos/sample.avi + #path: /home/hy/wust_data/record/20251111_134416_187.avi + path: /home/hy/下载/sp_vision_25/records/1.avi + #path: /home/hy/wust_data/video/Sentry.mp4 + start_frame: 0 diff --git a/wust_vision-main/config/auto_sniper.yaml b/wust_vision-main/config/auto_sniper.yaml new file mode 100644 index 0000000..c1a1ea8 --- /dev/null +++ b/wust_vision-main/config/auto_sniper.yaml @@ -0,0 +1,27 @@ +map: + voxel_size: 0.1 + min_pos: [-5.0,-10.0,-1.0] + max_pos: [20.0,10.0,1.0] + + +solver: + k1: 0.0190 + g: 9.81 + target_armor_z: 0.5 + +offset_helper: + order: 5 + yaw_base_offset: 0.0 + pitch_base_offset: 0.0 + offset_table: + - distance: 2.0 + yaw: 0.1 + pitch: -0.2 + + - distance: 3.0 + yaw: 0.15 + pitch: -0.25 + + - distance: 4.0 + yaw: 0.2 + pitch: -0.3 \ No newline at end of file diff --git a/wust_vision-main/config/camera.yaml b/wust_vision-main/config/camera.yaml new file mode 100644 index 0000000..5c41a14 --- /dev/null +++ b/wust_vision-main/config/camera.yaml @@ -0,0 +1,50 @@ +type: hik_camera + +camera_info_path: ${VISION_ROOT}/config/camera_info.yaml + +hik_camera: + target_sn: DA3038934 + adc_bit_depth: Bits_8 + pixel_format: BayerRG8 #RGB8Packed + reverse_x: false + reverse_y: false + width: 1440 + height: 1080 + offset_x: 0 + offset_y: 0 + acquisition_frame_rate_enable: true + acquisition_frame_rate: 250 + exposure_time: 2000 + gain: 16.9 + gamma: 0.7 #Bayer用不了 + trigger_type: none + trigger_source: "" + trigger_activation: 0 + use_raw: false + use_rgb: false + use_ea: false + use_cuda_cvt: true + + +video_player: + fps: 60 + loop: true + use_cvt: true + trigger_mode: false + #path: /home/hy/wust_data/video/aaa.mp4 + #path: /home/hy/wust_data/video/jiao.avi + #path: /home/hy/Z_LION_AutoAim2025/src/auto_aim_publisher/videos/sample.avi + #path: /home/hy/wust_data/record/20251111_134416_187.avi + path: /home/hy/data/video_save/5.21成都.mp4 + #path: /home/hy/wust_data/video/Sentry.mp4 + start_frame: 0 + +uvc: + device_name: "/dev/v4l/by-path/pci-0000:00:14.0-usb-0:3.4:1.0-video-index0" + fps: 60 + width: 1280 + height: 720 + exposure: 100.0 + gain: 10.0 + gamma: 100 + trigger_mode: false \ No newline at end of file diff --git a/wust_vision-main/config/camera_info.yaml b/wust_vision-main/config/camera_info.yaml new file mode 100644 index 0000000..910d4bb --- /dev/null +++ b/wust_vision-main/config/camera_info.yaml @@ -0,0 +1,82 @@ +#8mm +# image_width: 1440 +# image_height: 1080 +# camera_name: narrow_stereo +# camera_matrix: +# rows: 3 +# cols: 3 +# data: [2419.64498, 0. , 719.05623, +# 0. , 2414.38209, 538.71651, +# 0. , 0. , 1. ] +# distortion_model: plumb_bob +# distortion_coefficients: +# rows: 1 +# cols: 5 +# data: [-0.033521, 0.087954, -0.002045, -0.001438, 0.000000] +# rectification_matrix: +# rows: 3 +# cols: 3 +# data: [1., 0., 0., +# 0., 1., 0., +# 0., 0., 1.] +# projection_matrix: +# rows: 3 +# cols: 4 +# data: [2413.22632, 0. , 717.5196 , 0. , +# 0. , 2408.83228, 537.43444, 0. , +# 0. , 0. , 1. , 0. ] +#6mm + +image_width: 1440 +image_height: 1080 +camera_name: narrow_stereo +camera_matrix: + rows: 3 + cols: 3 + data: [1814.19888, 0. , 730.27359, + 0. , 1807.37177, 529.57031, + 0. , 0. , 1. ] +distortion_model: plumb_bob +distortion_coefficients: + rows: 1 + cols: 5 + data: [-0.089138, 0.114766, 0.000034, 0.000238, 0.000000] +rectification_matrix: + rows: 3 + cols: 3 + data: [1., 0., 0., + 0., 1., 0., + 0., 0., 1.] +projection_matrix: + rows: 3 + cols: 4 + data: [1793.54229, 0. , 730.67194, 0. , + 0. , 1794.48784, 529.46193, 0. , + 0. , 0. , 1. , 0. ] +#12mm +# image_width: 1440 +# image_height: 1080 +# camera_name: narrow_stereo +# camera_matrix: +# rows: 3 +# cols: 3 +# data: [3655.11292, 0. , 748.66803, +# 0. , 3648.34439, 556.97683, +# 0. , 0. , 1. ] +# distortion_model: plumb_bob +# distortion_coefficients: +# rows: 1 +# cols: 5 +# data: [-0.058763, 1.430269, 0.001191, 0.001641, 0.000000] +# rectification_matrix: +# rows: 3 +# cols: 3 +# data: [1., 0., 0., +# 0., 1., 0., +# 0., 0., 1.] +# projection_matrix: +# rows: 3 +# cols: 4 +# data: [3659.22949, 0. , 748.71496, 0. , +# 0. , 3652.02661, 556.81908, 0. , +# 0. , 0. , 1. , 0. ] diff --git a/wust_vision-main/config/common.yaml b/wust_vision-main/config/common.yaml new file mode 100644 index 0000000..0b8a790 --- /dev/null +++ b/wust_vision-main/config/common.yaml @@ -0,0 +1,34 @@ +max_infer_running: 6 +detect_color: 0 +attack_mode: 0 +debug_fps: 60 +tf: + R_camera2gimbal: [0.0, 0.0, 1.0, -1.0, -0.0, 0.0, 0.0, -1.0, 0.0] + t_camera2gimbal: + [0.000434347168653831, 0.0141232476052895, 0.00736106400231024] +control: + control_rate: 1000 + communication_delay_us: 100.000 + device_name: /dev/stm32_acm + use_serial: true + yaw_ramp: 0.0 #send_cmd = cmd.pos+cmd.vel*ramp + pitch_ramp: 0.0 + +logger: + log_level: DEBUG + log_path: log + use_logcli: true + use_logfile: false + use_simplelog: true + +shoot: + bullet_speed: 26.0 + rate: 3 + +record: + use_record: false + folder_path: record + use_rotate_reader: false + read_csv_path: "" + + diff --git a/wust_vision-main/config/detect_ml.yaml b/wust_vision-main/config/detect_ml.yaml new file mode 100644 index 0000000..942de76 --- /dev/null +++ b/wust_vision-main/config/detect_ml.yaml @@ -0,0 +1,66 @@ +armor_detector: + cv: #装甲板灯条完整无损坏才可以用 + enable: true + armor: + max_angle: 40 + max_large_center_distance: 8 + max_small_center_distance: 3.5 + min_large_center_distance: 3.5 + min_light_ratio: 0.5 + min_small_center_distance: 0.05 + light: + binary_thres: 150 + expand_ratio_h: 1.9 + expand_ratio_w: 1.2 + max_angle: 45 + max_ratio: 0.4 + min_ratio: 0.01 + max_pts_error: 20.0 + max_angle_diff: 20.0 + color_diff_thresh: 40 + classify: + enable: true + label_path: ${VISION_ROOT}/model/label.txt + model_path: ${VISION_ROOT}/model/mlp.onnx + backend: opencv + threshold: 0.5 + + tensorrt: + conf_threshold: 0.2 + model_path: ${VISION_ROOT}/model/opt-1208-001.onnx + device_id: 0 + nms_threshold: 0.3 + top_k: 128 + max_infer_running: 3 + min_free_mem_ratio: 0.1 + use_cuda_pre: true + log_time: false + model_type: tup + openvino: + conf_threshold: 0.2 + device_name: GPU + model_path: ${VISION_ROOT}/model/opt-1208-001.onnx + nms_threshold: 0.3 + top_k: 128 + use_throughputmode: true + model_type: tup + onnxruntime: + conf_threshold: 0.2 + model_path: ${VISION_ROOT}/model/opt-1208-001.onnx + nms_threshold: 0.3 + top_k: 128 + model_type: tup + provider: CUDA + ncnn: + conf_threshold: 0.2 + model_path_param: ${VISION_ROOT}/model/opt-1208-001.param + model_path_bin: ${VISION_ROOT}/model/opt-1208-001.bin + input_name: images + output_name: output + use_gpu: true + use_lightmode: false + device_id: 0 + cpu_threads: 20 + nms_threshold: 0.3 + top_k: 128 + model_type: tup diff --git a/wust_vision-main/config/detect_opencv.yaml b/wust_vision-main/config/detect_opencv.yaml new file mode 100644 index 0000000..3025688 --- /dev/null +++ b/wust_vision-main/config/detect_opencv.yaml @@ -0,0 +1,19 @@ +armor_detector: + light: + binary_thres: 120 + max_angle: 40 + max_ratio: 0.4 + min_ratio: 0.001 + color_diff_thresh: 20 + max_angle_diff: 10.0 + armor: + min_light_ratio: 0.8 + min_small_center_distance: 0.8 + max_small_center_distance: 3.5 + min_large_center_distance: 3.5 + max_large_center_distance: 8.0 + max_angle: 40.0 + classify: + label_path: ${VISION_ROOT}/model/label.txt + model_path: ${VISION_ROOT}/model/reborn_number_classifier.onnx + threshold: 0.5 diff --git a/wust_vision-main/config/guard.sh b/wust_vision-main/config/guard.sh new file mode 100755 index 0000000..e000b74 --- /dev/null +++ b/wust_vision-main/config/guard.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +TARGET="$1" +shift +ARGS=("${@:3}") + + +echo "[GUARD] target: $TARGET" +echo "[GUARD] args: ${ARGS[*]}" +echo "[GUARD] Starting monitor..." + +while true; do + echo "[GUARD] Launching program..." + "$TARGET" "${ARGS[@]}" + RET=$? + + if [ $RET -eq 0 ]; then + echo "[GUARD] Program exited normally. Stopping guard." + exit 0 + fi + + echo "[GUARD] Crash detected, restarting in 1 second..." + sleep 1 +done diff --git a/wust_vision-main/config/omni/camera0.yaml b/wust_vision-main/config/omni/camera0.yaml new file mode 100644 index 0000000..158587f --- /dev/null +++ b/wust_vision-main/config/omni/camera0.yaml @@ -0,0 +1,51 @@ +yaw_in_big_yaw_deg: 90.0 +type: uvc + +camera_info_path: ${VISION_ROOT}/config/camera_info.yaml + +hik_camera: + target_sn: DA1094119 + adc_bit_depth: Bits_8 + pixel_format: BayerRG8 #RGB8Packed + reverse_x: false + reverse_y: false + width: 1440 + height: 1080 + offset_x: 0 + offset_y: 0 + acquisition_frame_rate_enable: true + acquisition_frame_rate: 250 + exposure_time: 2000 + gain: 16.9 + gamma: 0.7 #Bayer用不了 + trigger_type: none + trigger_source: "" + trigger_activation: 0 + use_raw: false + use_rgb: false + use_ea: false + use_cuda_cvt: false + + +video_player: + fps: 60 + loop: true + use_cvt: true + trigger_mode: false + #path: /home/hy/wust_data/video/aaa.mp4 + #path: /home/hy/wust_data/video/jiao.avi + #path: /home/hy/Z_LION_AutoAim2025/src/auto_aim_publisher/videos/sample.avi + #path: /home/hy/wust_data/record/20251111_134416_187.avi + path: /home/hy/rune_dl/runeA.mp4 + #path: /home/hy/wust_data/video/Sentry.mp4 + start_frame: 0 + +uvc: + device_name: "/dev/v4l/by-path/pci-0000:00:14.0-usb-0:9.4:1.0-video-index0" + fps: 30 + width: 1280 + height: 720 + exposure: 200 + gain: 10.0 + gamma: 100 + trigger_mode: true \ No newline at end of file diff --git a/wust_vision-main/config/omni/camera1.yaml b/wust_vision-main/config/omni/camera1.yaml new file mode 100644 index 0000000..04832c5 --- /dev/null +++ b/wust_vision-main/config/omni/camera1.yaml @@ -0,0 +1,51 @@ +yaw_in_big_yaw_deg: 107.0 +type: uvc + +camera_info_path: ${VISION_ROOT}/config/omni/camera_info.yaml + +hik_camera: + target_sn: DA1094119 + adc_bit_depth: Bits_8 + pixel_format: BayerRG8 #RGB8Packed + reverse_x: false + reverse_y: false + width: 1440 + height: 1080 + offset_x: 0 + offset_y: 0 + acquisition_frame_rate_enable: true + acquisition_frame_rate: 250 + exposure_time: 2000 + gain: 16.9 + gamma: 0.7 #Bayer用不了 + trigger_type: none + trigger_source: "" + trigger_activation: 0 + use_raw: false + use_rgb: false + use_ea: false + use_cuda_cvt: false + + +video_player: + fps: 60 + loop: true + use_cvt: true + trigger_mode: false + #path: /home/hy/wust_data/video/aaa.mp4 + #path: /home/hy/wust_data/video/jiao.avi + #path: /home/hy/Z_LION_AutoAim2025/src/auto_aim_publisher/videos/sample.avi + #path: /home/hy/wust_data/record/20251111_134416_187.avi + path: /home/hy/rune_dl/runeA.mp4 + #path: /home/hy/wust_data/video/Sentry.mp4 + start_frame: 0 + +uvc: + device_name: "/dev/v4l/by-path/pci-0000:00:14.0-usb-0:9.1:1.0-video-index0" + fps: 30 + width: 1280 + height: 720 + exposure: 200 + gain: 10.0 + gamma: 100 + trigger_mode: true \ No newline at end of file diff --git a/wust_vision-main/config/omni/camera_info.yaml b/wust_vision-main/config/omni/camera_info.yaml new file mode 100644 index 0000000..1ede5f6 --- /dev/null +++ b/wust_vision-main/config/omni/camera_info.yaml @@ -0,0 +1,28 @@ + +# camera_matrix = [759.73071, 0.0, 336.19053, 0.0, 761.16771, 231.83002, 0.0, 0.0, 1.0] +# distortion_coefficients = [0.000281, 0.144018, 0.005509, -0.004330, 0.0] +image_width: 1440 +image_height: 1080 +camera_name: narrow_stereo +camera_matrix: + rows: 3 + cols: 3 + data: [759.73071, 0.0, 336.19053, 0.0, 761.16771, 231.83002, 0.0, 0.0, 1.0 ] +distortion_model: plumb_bob +distortion_coefficients: + rows: 1 + cols: 5 + data: [0.000281, 0.144018, 0.005509, -0.004330, 0.0] +rectification_matrix: + rows: 3 + cols: 3 + data: [1., 0., 0., + 0., 1., 0., + 0., 0., 1.] +projection_matrix: + rows: 3 + cols: 4 + data: [1793.54229, 0. , 730.67194, 0. , + 0. , 1794.48784, 529.46193, 0. , + 0. , 0. , 1. , 0. ] + diff --git a/wust_vision-main/config/omni/detect_ml.yaml b/wust_vision-main/config/omni/detect_ml.yaml new file mode 100644 index 0000000..6b6d9e6 --- /dev/null +++ b/wust_vision-main/config/omni/detect_ml.yaml @@ -0,0 +1,66 @@ +armor_detector: + cv: #装甲板灯条完整无损坏才可以用 + enable: false + armor: + max_angle: 40 + max_large_center_distance: 8 + max_small_center_distance: 3.5 + min_large_center_distance: 3.5 + min_light_ratio: 0.5 + min_small_center_distance: 0.05 + light: + binary_thres: 150 + expand_ratio_h: 1.9 + expand_ratio_w: 1.2 + max_angle: 45 + max_ratio: 0.4 + min_ratio: 0.01 + max_pts_error: 20.0 + max_angle_diff: 20.0 + color_diff_thresh: 0 + classify: + enable: true + label_path: ${VISION_ROOT}/model/label.txt + model_path: ${VISION_ROOT}/model/mlp.onnx + backend: opencv + threshold: 0.5 + + tensorrt: + conf_threshold: 0.2 + model_path: ${VISION_ROOT}/model/opt-1208-001.onnx + device_id: 0 + nms_threshold: 0.3 + top_k: 128 + max_infer_running: 3 + min_free_mem_ratio: 0.1 + use_cuda_pre: true + log_time: false + model_type: tup + openvino: + conf_threshold: 0.2 + device_name: GPU + model_path: ${VISION_ROOT}/model/opt-1208-001.onnx + nms_threshold: 0.3 + top_k: 128 + use_throughputmode: true + model_type: tup + onnxruntime: + conf_threshold: 0.2 + model_path: ${VISION_ROOT}/model/opt-1208-001.onnx + nms_threshold: 0.3 + top_k: 128 + model_type: tup + provider: CUDA + ncnn: + conf_threshold: 0.2 + model_path_param: ${VISION_ROOT}/model/opt-1208-001.param + model_path_bin: ${VISION_ROOT}/model/opt-1208-001.bin + input_name: images + output_name: output + use_gpu: true + use_lightmode: false + device_id: 0 + cpu_threads: 20 + nms_threshold: 0.3 + top_k: 128 + model_type: tup diff --git a/wust_vision-main/config/omni/detect_opencv.yaml b/wust_vision-main/config/omni/detect_opencv.yaml new file mode 100644 index 0000000..3025688 --- /dev/null +++ b/wust_vision-main/config/omni/detect_opencv.yaml @@ -0,0 +1,19 @@ +armor_detector: + light: + binary_thres: 120 + max_angle: 40 + max_ratio: 0.4 + min_ratio: 0.001 + color_diff_thresh: 20 + max_angle_diff: 10.0 + armor: + min_light_ratio: 0.8 + min_small_center_distance: 0.8 + max_small_center_distance: 3.5 + min_large_center_distance: 3.5 + max_large_center_distance: 8.0 + max_angle: 40.0 + classify: + label_path: ${VISION_ROOT}/model/label.txt + model_path: ${VISION_ROOT}/model/reborn_number_classifier.onnx + threshold: 0.5 diff --git a/wust_vision-main/config/omni/omni.yaml b/wust_vision-main/config/omni/omni.yaml new file mode 100644 index 0000000..9e736d7 --- /dev/null +++ b/wust_vision-main/config/omni/omni.yaml @@ -0,0 +1,37 @@ +armor_detect_backend: openvino + +cameras: ["${VISION_ROOT}/config/omni/camera0.yaml", "${VISION_ROOT}/config/omni/camera1.yaml"] + +fps: 60 +max_infer_running: 3 +active_time: 1.0 +min_score: 15.0 + +armor_where: + yaw_opt: + mode: golden + golden_search_side_deg: 60 + distance_fix_a2: 0.0 + +armor_tracker: + lost_time_thres: 1.0 + tracking_thres: 5 + max_yaw_diff_deg: 90.0 + max_dis_diff: 3.0 + match_gate: 50 + qxyz_common: [1.0, 1.0, 1.0] + qyaw_common: 1.0 + qxyz_output: [1.0, 1.0, 0.5] + qyaw_output: 0.01 + q_r: 0.0000001 + q_l: 0.0000001 + q_h: 0.0000001 + q_outpost_dz: 0.5 + yp_r: 2e-3 + dis_r_front: 0.5 + dis_r_side: 2.5 + dis2_r_ratio: 0.1 + yaw_r_base_front: 0.2 + yaw_r_base_side: 0.06 + yaw_r_log_ratio: 0.02 + esekf_iter_num: 1 \ No newline at end of file diff --git a/wust_vision-main/cuda_infer/CMakeLists.txt b/wust_vision-main/cuda_infer/CMakeLists.txt new file mode 100644 index 0000000..69ceec2 --- /dev/null +++ b/wust_vision-main/cuda_infer/CMakeLists.txt @@ -0,0 +1,61 @@ +cmake_minimum_required(VERSION 3.10) +cmake_policy(SET CMP0079 NEW) + +project(cuda_infer LANGUAGES CXX CUDA) + +# 设置标准 +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CUDA_STANDARD 17) +set(CMAKE_CUDA_STANDARD_REQUIRED ON) +set(CUDA_USE_STATIC_CUDA_RUNTIME OFF) +set(CMAKE_BUILD_TYPE "Release") +# 抑制过时 API 警告 +add_compile_options(-Wno-deprecated-declarations) + +# 禁用 .rsp 响应文件(避免 nvcc 报错) +set(CMAKE_CUDA_USE_RESPONSE_FILE_FOR_OBJECTS OFF) +set(CMAKE_CUDA_USE_RESPONSE_FILE_FOR_INCLUDES OFF) + +# 查找依赖 +find_package(CUDAToolkit REQUIRED) +find_package(OpenCV REQUIRED) +find_package(Eigen3 REQUIRED) + +# 收集源码 +file(GLOB_RECURSE CUDA_INFER_SRC + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.cu +) + +# 添加静态库 +add_library(cuda_infer STATIC ${CUDA_INFER_SRC}) +set_target_properties(cuda_infer PROPERTIES POSITION_INDEPENDENT_CODE ON) + +# 设置包含路径 +target_include_directories(cuda_infer PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CUDAToolkit_INCLUDE_DIRS} + ${TensorRT_INCLUDE_DIR} + ${EIGEN3_INCLUDE_DIRS} + ${OpenCV_INCLUDE_DIRS} +) + +# 设置 CUDA 编译选项 +target_compile_options(cuda_infer PRIVATE + $<$: + --generate-code=arch=compute_86,code=sm_86 + -Xcompiler=-fPIC + -O3 + -w + -Wno-deprecated-gpu-targets + -Wno-error=deprecated-declarations + > +) + +# 链接库 +target_link_libraries(cuda_infer PRIVATE + ${OpenCV_LIBS} + CUDA::cudart + TensorRT::TensorRT +) diff --git a/wust_vision-main/cuda_infer/armor_infer.cu b/wust_vision-main/cuda_infer/armor_infer.cu new file mode 100644 index 0000000..e8c2940 --- /dev/null +++ b/wust_vision-main/cuda_infer/armor_infer.cu @@ -0,0 +1,323 @@ +// armor_cuda_infer.cu +#include "armor_infer.hpp" +#include "letter_box.hpp" +#include +#include +#include +#include +#include +#include +#define CUDA_CHECK(call) \ + do { \ + cudaError_t err = call; \ + if (err != cudaSuccess) { \ + fprintf( \ + stderr, \ + "CUDA error at %s:%d: %s\n", \ + __FILE__, \ + __LINE__, \ + cudaGetErrorString(err) \ + ); \ + exit(EXIT_FAILURE); \ + } \ + } while (0) +namespace armor_cuda_infer { +__global__ void nchw_float_to_hwc_uchar4( + const float* __restrict__ src, + uchar4* __restrict__ dst, + int W, + int H, + float norm +) { + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + if (x >= W || y >= H) + return; + + const int idx = y * W + x; + const int plane = W * H; + + float r = __ldg(src + idx + plane * 0); + float g = __ldg(src + idx + plane * 1); + float b = __ldg(src + idx + plane * 2); + + r = fminf(fmaxf(r / norm, 0.f), 255.f); + g = fminf(fmaxf(g / norm, 0.f), 255.f); + b = fminf(fmaxf(b / norm, 0.f), 255.f); + + dst[idx] = make_uchar4((unsigned char)b, (unsigned char)g, (unsigned char)r, 255); +} + +cv::Mat CudaInfer::tensorToMat(float* d_nchw, int W, int H, float norm, cudaStream_t stream) const { + static uchar4* d_hwc = nullptr; + static size_t cap = 0; + + const size_t need = W * H * sizeof(uchar4); + if (cap < need) { + if (d_hwc) + cudaFree(d_hwc); + cudaMalloc(&d_hwc, need); + cap = need; + } + + const dim3 block(TILE_W, TILE_H); + const dim3 grid((W + block.x - 1) / block.x, (H + block.y - 1) / block.y); + + nchw_float_to_hwc_uchar4<<>>(d_nchw, d_hwc, W, H, norm); + + cv::Mat img(H, W, CV_8UC4); + + cudaMemcpyAsync(img.data, d_hwc, need, cudaMemcpyDeviceToHost, stream); + + // cudaStreamSynchronize(stream); + return img; +} + +CudaInfer::CudaInfer() = default; +CudaInfer::~CudaInfer() { + release(); +} + +void CudaInfer::init(int max_src_w, int max_src_h, int input_w, int input_h) { + input_w_ = input_w; + input_h_ = input_h; + max_src_h_ = max_src_h; + max_src_w_ = max_src_w; + rellocMem(); +} +void CudaInfer::rellocMem() { + CUDA_CHECK(cudaMalloc(&d_input_bgr_, max_src_w_ * max_src_h_ * 3 * sizeof(unsigned char))); + CUDA_CHECK(cudaMallocPitch( + &d_input_bgr_pitched_, + &input_pitch_bytes_, + max_src_w_ * 3 * sizeof(unsigned char), + max_src_h_ + )); + CUDA_CHECK(cudaMalloc(&d_nchw_, input_w_ * input_h_ * 3 * sizeof(float))); + printf("Relloc memory for CudaInfer\n"); +} +void CudaInfer::getOutEnoughMem(int img_w, int img_h) { + if (img_w > max_src_w_ || img_h > max_src_h_) { + if (img_w > max_src_w_) { + max_src_w_ = img_w; + } + if (img_h > max_src_h_) { + max_src_h_ = img_h; + } + rellocMem(); + } +} + +void CudaInfer::release() { + if (d_input_bgr_) + cudaFree(d_input_bgr_), d_input_bgr_ = nullptr; + if (d_input_bgr_pitched_) + cudaFree(d_input_bgr_pitched_), d_input_bgr_pitched_ = nullptr; + if (d_nchw_) + cudaFree(d_nchw_), d_nchw_ = nullptr; +} + +float* CudaInfer::preprocess( + const unsigned char* input_bgr_host, + int img_w, + int img_h, + float norm, + bool swap_rb, + Eigen::Matrix3f& tf_matrix, + cudaStream_t stream +) { + if (!isInitialized()) { + throw std::runtime_error("CudaInfer not initialized properly."); + } + + if (!input_bgr_host || !d_input_bgr_ || !d_nchw_) { + fprintf(stderr, "[Error] Null pointer in preprocess input\n"); + return nullptr; + } + getOutEnoughMem(img_w, img_h); + float scale = fminf(input_w_ / (float)img_w, input_h_ / (float)img_h); + int rw = round(img_w * scale), rh = round(img_h * scale); + int pad_l = (input_w_ - rw) / 2, pad_t = (input_h_ - rh) / 2; + + tf_matrix << 1.f / scale, 0, -pad_l / scale, 0, 1.f / scale, -pad_t / scale, 0, 0, 1; + + size_t img_size = img_w * img_h * 3; + CUDA_CHECK( + cudaMemcpyAsync(d_input_bgr_, input_bgr_host, img_size, cudaMemcpyHostToDevice, stream) + ); + + dim3 threads(TILE_W, TILE_H); + dim3 blocks((input_w_ + TILE_W - 1) / TILE_W, (input_h_ + TILE_H - 1) / TILE_H); + + letterbox_kernel_shared<<>>( + d_input_bgr_, + img_w, + img_h, + d_nchw_, + input_w_, + input_h_, + scale, + pad_t, + pad_l, + norm, + swap_rb + ); + + CUDA_CHECK(cudaGetLastError()); + return d_nchw_; +} +float* CudaInfer::preprocess_gpu( + const unsigned char* input_bgr_device, + int img_w, + int img_h, + float norm, + bool swap_rb, + Eigen::Matrix3f& tf_matrix, + cudaStream_t stream +) { + if (!isInitialized()) { + throw std::runtime_error("CudaInfer not initialized properly."); + } + + if (!input_bgr_device || !d_nchw_) { + fprintf(stderr, "[Error] Null pointer in preprocess input\n"); + return nullptr; + } + getOutEnoughMem(img_w, img_h); + float scale = fminf(input_w_ / (float)img_w, input_h_ / (float)img_h); + int rw = round(img_w * scale), rh = round(img_h * scale); + int pad_l = (input_w_ - rw) / 2, pad_t = (input_h_ - rh) / 2; + + tf_matrix << 1.f / scale, 0, -pad_l / scale, 0, 1.f / scale, -pad_t / scale, 0, 0, 1; + + size_t img_size = img_w * img_h * 3; + + dim3 threads(TILE_W, TILE_H); + dim3 blocks((input_w_ + TILE_W - 1) / TILE_W, (input_h_ + TILE_H - 1) / TILE_H); + + letterbox_kernel_shared<<>>( + input_bgr_device, + img_w, + img_h, + d_nchw_, + input_w_, + input_h_, + scale, + pad_t, + pad_l, + norm, + swap_rb + ); + + CUDA_CHECK(cudaGetLastError()); + return d_nchw_; +} +float* CudaInfer::preprocess_pitched( + const unsigned char* input_bgr_host, + int img_w, + int img_h, + int host_step, + float norm, + bool swap_rb, + Eigen::Matrix3f& tf_matrix, + cudaStream_t stream +) { + if (!isInitialized()) { + throw std::runtime_error("CudaInfer not initialized properly."); + } + + if (!input_bgr_host || !d_nchw_) { + fprintf(stderr, "[Error] Null pointer in preprocess input\n"); + return nullptr; + } + getOutEnoughMem(img_w, img_h); + float scale = fminf((float)input_w_ / img_w, (float)input_h_ / img_h); + int rw = round(img_w * scale); + int rh = round(img_h * scale); + int pad_l = (input_w_ - rw) / 2; + int pad_t = (input_h_ - rh) / 2; + tf_matrix << 1.f / scale, 0, -pad_l / scale, 0, 1.f / scale, -pad_t / scale, 0, 0, 1; + CUDA_CHECK(cudaMemcpy2DAsync( + d_input_bgr_pitched_, + input_pitch_bytes_, + input_bgr_host, + host_step, + img_w * 3, + img_h, + cudaMemcpyHostToDevice, + stream + )); + dim3 threads(TILE_W, TILE_H); + dim3 blocks((input_w_ + TILE_W - 1) / TILE_W, (input_h_ + TILE_H - 1) / TILE_H); + + letterbox_kernel_pitched<<>>( + d_input_bgr_pitched_, + input_pitch_bytes_, + img_w, + img_h, + d_nchw_, + input_w_, + input_h_, + scale, + pad_t, + pad_l, + norm, + swap_rb + ); + + CUDA_CHECK(cudaGetLastError()); + return d_nchw_; +} +float* CudaInfer::preprocess_pitched_gpu( + const unsigned char* input_bgr_device, + int img_w, + int img_h, + int input_step, + float norm, + bool swap_rb, + Eigen::Matrix3f& tf_matrix, + cudaStream_t stream +) { + if (!isInitialized()) { + throw std::runtime_error("CudaInfer not initialized properly."); + } + + if (!input_bgr_device || !d_nchw_) { + fprintf(stderr, "[Error] Null pointer in preprocess_pitched_gpu\n"); + return nullptr; + } + getOutEnoughMem(img_w, img_h); + float scale = fminf(static_cast(input_w_) / img_w, static_cast(input_h_) / img_h); + + int rw = static_cast(roundf(img_w * scale)); + int rh = static_cast(roundf(img_h * scale)); + + int pad_l = (input_w_ - rw) / 2; + int pad_t = (input_h_ - rh) / 2; + + tf_matrix << 1.f / scale, 0.f, -pad_l / scale, 0.f, 1.f / scale, -pad_t / scale, 0.f, 0.f, 1.f; + + dim3 threads(TILE_W, TILE_H); + dim3 blocks((input_w_ + TILE_W - 1) / TILE_W, (input_h_ + TILE_H - 1) / TILE_H); + + letterbox_kernel_pitched<<>>( + input_bgr_device, + input_step, + img_w, + img_h, + d_nchw_, + input_w_, + input_h_, + scale, + pad_t, + pad_l, + norm, + swap_rb + ); + + CUDA_CHECK(cudaGetLastError()); + + return d_nchw_; +} + +} // namespace armor_cuda_infer \ No newline at end of file diff --git a/wust_vision-main/cuda_infer/armor_infer.hpp b/wust_vision-main/cuda_infer/armor_infer.hpp new file mode 100644 index 0000000..a273c95 --- /dev/null +++ b/wust_vision-main/cuda_infer/armor_infer.hpp @@ -0,0 +1,78 @@ +// armor_cuda_infer.hpp +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace armor_cuda_infer { + +class CudaInfer { +public: + CudaInfer(); + ~CudaInfer() noexcept; + + void init(int max_src_w, int max_src_h, int input_w, int input_h); + void release(); + bool isInitialized() const { + return d_input_bgr_ && d_nchw_ && d_input_bgr_pitched_; + } + void getOutEnoughMem(int img_w, int img_h); + void rellocMem(); + + float* preprocess( + const unsigned char* input_bgr_host, + int img_w, + int img_h, + float norm, + bool swap_rb, + Eigen::Matrix3f& tf_matrix, + cudaStream_t stream + ); + float* preprocess_pitched( + const unsigned char* input_bgr_host, + int img_w, + int img_h, + int host_step, + float norm, + bool swap_rb, + Eigen::Matrix3f& tf_matrix, + cudaStream_t stream + ); + float* preprocess_gpu( + const unsigned char* input_bgr_device, + int img_w, + int img_h, + float norm, + bool swap_rb, + Eigen::Matrix3f& tf_matrix, + cudaStream_t stream + ); + float* preprocess_pitched_gpu( + const unsigned char* input_bgr_device, + int img_w, + int img_h, + int host_step, + float norm, + bool swap_rb, + Eigen::Matrix3f& tf_matrix, + cudaStream_t stream + ); + cv::Mat tensorToMat(float* d_nchw, int W, int H, float norm, cudaStream_t stream) const; + +private: + CudaInfer(const CudaInfer&) = delete; + CudaInfer& operator=(const CudaInfer&) = delete; + unsigned char* d_input_bgr_ = nullptr; + float* d_nchw_ = nullptr; + unsigned char* d_input_bgr_pitched_ = nullptr; + size_t input_pitch_bytes_ = 0; + int input_w_; + int input_h_; + int max_src_w_, max_src_h_; +}; +} // namespace armor_cuda_infer \ No newline at end of file diff --git a/wust_vision-main/cuda_infer/letter_box.cu b/wust_vision-main/cuda_infer/letter_box.cu new file mode 100644 index 0000000..1937719 --- /dev/null +++ b/wust_vision-main/cuda_infer/letter_box.cu @@ -0,0 +1,147 @@ +#include "letter_box.hpp" +__global__ void letterbox_kernel_shared( + const uchar* __restrict__ input_bgr, + int in_w, + int in_h, + float* __restrict__ output_nchw, + int out_w, + int out_h, + float scale, + int pad_t, + int pad_l, + float norm, + bool swap_rb +) { + // global x/y + int x = blockIdx.x * TILE_W + threadIdx.x; + int y = blockIdx.y * TILE_H + threadIdx.y; + if (x >= out_w || y >= out_h) + return; + + // 共享内存 + halo + __shared__ uchar4 smem[TILE_H + 1][TILE_W + 1]; + + int tid = threadIdx.y * blockDim.x + threadIdx.x; + int total_smem = (TILE_W + 1) * (TILE_H + 1); + int threads_per_block = blockDim.x * blockDim.y; + int iter = (total_smem + threads_per_block - 1) / threads_per_block; + + float inv_scale = 1.0f / scale; + float block_start_x = blockIdx.x * TILE_W - pad_l; + float block_start_y = blockIdx.y * TILE_H - pad_t; + + // load shared memory + for (int i = 0; i < iter; i++) { + int idx = tid + i * threads_per_block; + if (idx < total_smem) { + int sx = idx % (TILE_W + 1); + int sy = idx / (TILE_W + 1); + + float in_x = (block_start_x + sx) * inv_scale; + float in_y = (block_start_y + sy) * inv_scale; + + int ix = floorf(in_x); + int iy = floorf(in_y); + + uchar4 p = make_uchar4(114, 114, 114, 0); // padding BGR + if (ix >= 0 && iy >= 0 && ix < in_w && iy < in_h) { + int offset = (iy * in_w + ix) * 3; + p.x = input_bgr[offset]; // b + p.y = input_bgr[offset + 1]; // g + p.z = input_bgr[offset + 2]; // r + } + + smem[sy][sx] = p; + } + } + __syncthreads(); + + // 双线性插值 + float in_x = (x - pad_l) * inv_scale; + float in_y = (y - pad_t) * inv_scale; + int tx = threadIdx.x; + int ty = threadIdx.y; + float dx = in_x - floorf(in_x); + float dy = in_y - floorf(in_y); + float dx1 = 1.0f - dx, dy1 = 1.0f - dy; + + uchar4 p00 = smem[ty][tx]; + uchar4 p01 = smem[ty][tx + 1]; + uchar4 p10 = smem[ty + 1][tx]; + uchar4 p11 = smem[ty + 1][tx + 1]; + + float out_r = dx1 * dy1 * p00.z + dx * dy1 * p01.z + dx1 * dy * p10.z + dx * dy * p11.z; + float out_g = dx1 * dy1 * p00.y + dx * dy1 * p01.y + dx1 * dy * p10.y + dx * dy * p11.y; + float out_b = dx1 * dy1 * p00.x + dx * dy1 * p01.x + dx1 * dy * p10.x + dx * dy * p11.x; + + int out_idx = y * out_w + x; + if (swap_rb) { + output_nchw[out_idx + 0 * out_w * out_h] = out_r * norm; + output_nchw[out_idx + 1 * out_w * out_h] = out_g * norm; + output_nchw[out_idx + 2 * out_w * out_h] = out_b * norm; + } else { + output_nchw[out_idx + 0 * out_w * out_h] = out_b * norm; + output_nchw[out_idx + 1 * out_w * out_h] = out_g * norm; + output_nchw[out_idx + 2 * out_w * out_h] = out_r * norm; + } +} + +__global__ void letterbox_kernel_pitched( + const unsigned char* __restrict__ d_input_bgr, + size_t pitch, + int src_w, + int src_h, + float* __restrict__ d_nchw, + int OUT_W, + int OUT_H, + float scale, + int pad_t, + int pad_l, + float norm, + bool swap_rb +) { + int ox = blockIdx.x * blockDim.x + threadIdx.x; + int oy = blockIdx.y * blockDim.y + threadIdx.y; + if (ox >= OUT_W || oy >= OUT_H) + return; + + float fx = (ox - pad_l) / scale; + float fy = (oy - pad_t) / scale; + + int out_idx = oy * OUT_W + ox; + int plane = OUT_W * OUT_H; + + // clamp coordinates + fx = fmaxf(0.f, fminf(fx, src_w - 2.f)); + fy = fmaxf(0.f, fminf(fy, src_h - 2.f)); + + int x0 = floorf(fx), y0 = floorf(fy); + int x1 = x0 + 1, y1 = y0 + 1; + + float dx = fx - x0, dy = fy - y0; + float dx1 = 1.f - dx, dy1 = 1.f - dy; + + // row pointers + const uchar3* row0 = (const uchar3*)((const char*)d_input_bgr + y0 * pitch); + const uchar3* row1 = (const uchar3*)((const char*)d_input_bgr + y1 * pitch); + + uchar3 p00 = row0[x0]; + uchar3 p01 = row0[x1]; + uchar3 p10 = row1[x0]; + uchar3 p11 = row1[x1]; + + // bilinear interpolation + float r = dx1 * dy1 * p00.z + dx * dy1 * p01.z + dx1 * dy * p10.z + dx * dy * p11.z; + float g = dx1 * dy1 * p00.y + dx * dy1 * p01.y + dx1 * dy * p10.y + dx * dy * p11.y; + float b = dx1 * dy1 * p00.x + dx * dy1 * p01.x + dx1 * dy * p10.x + dx * dy * p11.x; + + if (swap_rb) { + d_nchw[out_idx + 0 * plane] = r * norm; + d_nchw[out_idx + 1 * plane] = g * norm; + d_nchw[out_idx + 2 * plane] = b * norm; + } else { + d_nchw[out_idx + 0 * plane] = b * norm; + d_nchw[out_idx + 1 * plane] = g * norm; + d_nchw[out_idx + 2 * plane] = r * norm; + } +} diff --git a/wust_vision-main/cuda_infer/letter_box.hpp b/wust_vision-main/cuda_infer/letter_box.hpp new file mode 100644 index 0000000..c1ee12f --- /dev/null +++ b/wust_vision-main/cuda_infer/letter_box.hpp @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +static constexpr int TILE_W = 32; +static constexpr int TILE_H = 32; +__global__ void letterbox_kernel_shared( + const uchar* __restrict__ input_bgr, + int in_w, + int in_h, + float* __restrict__ output_nchw, + int out_w, + int out_h, + float scale, + int pad_t, + int pad_l, + float norm, + bool swap_rb +); +__global__ void letterbox_kernel_pitched( + const unsigned char* __restrict__ d_input_bgr, + size_t pitch, + int src_w, + int src_h, + float* __restrict__ d_nchw, + int OUT_W, + int OUT_H, + float scale, + int pad_t, + int pad_l, + float norm, + bool swap_rb +); diff --git a/wust_vision-main/env.bash b/wust_vision-main/env.bash new file mode 100644 index 0000000..4a8d3d0 --- /dev/null +++ b/wust_vision-main/env.bash @@ -0,0 +1,19 @@ +# #!/bin/bash +export MVCAM_SDK_PATH=/opt/MVS +export MVCAM_COMMON_RUNENV=/opt/MVS/lib +export MVCAM_GENICAM_CLPROTOCOL=/opt/MVS/lib/CLProtocol +export ALLUSERSPROFILE=/opt/MVS/MVFG +export LD_LIBRARY_PATH=/opt/MVS/lib/64:/opt/MVS/lib/32:$LD_LIBRARY_PATH +export MVCAM_SDK_PATH=/opt/MVS + +export MVCAM_SDK_VERSION= + +export MVCAM_COMMON_RUNENV=/opt/MVS/lib + +export MVCAM_GENICAM_CLPROTOCOL=/opt/MVS/lib/CLProtocol + +export ALLUSERSPROFILE=/opt/MVS/MVFG +export LD_LIBRARY_PATH=/opt/MVS/lib/aarch64:$LD_LIBRARY_PATH + +WORK_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +export VISION_ROOT="$WORK_DIR" \ No newline at end of file diff --git a/wust_vision-main/format.sh b/wust_vision-main/format.sh new file mode 100755 index 0000000..e630cb4 --- /dev/null +++ b/wust_vision-main/format.sh @@ -0,0 +1,3 @@ +find . -path ./build -prune -o \ + -type f \( -name '*.h' -o -name '*.hpp' -o -name '*.c' -o -name '*.cu' -o -name '*.cpp' \) \ + -exec clang-format -i {} + diff --git a/wust_vision-main/model/0526.engine b/wust_vision-main/model/0526.engine new file mode 100644 index 0000000000000000000000000000000000000000..c0484119a0c5f6ebece76c3f78ea3b59a3f47bd4 GIT binary patch literal 7255356 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

Q$zhwXka`<*ki=-8VOD#1noR)vsX2VX{V871{ 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%0D6u{Z$2Ki9T*p|cWx*O7@{@>4TO;HABY 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